Add SpieloKit gambling kit and UI hooks
Introduce a new kit SpieloKit with aggressive (instant gamble) and defensive (slot-machine GUI) playstyles. Implements outcome resolution (instant death, disaster events, negative/neutral/positive loot), animations, cooldowns, safe-radius checks, and a 3×9 SlotMachineGui with spinning reel animation and spin button. Adds registration of SpieloKit in SpeedHG and integrates SlotMachineGui dispatch into MenuListener (inventory click + close handlers). Includes utility methods for effects, sounds, particles and loot pools.
This commit is contained in:
@@ -224,6 +224,7 @@ class SpeedHG : JavaPlugin() {
|
|||||||
kitManager.registerKit( NinjaKit() )
|
kitManager.registerKit( NinjaKit() )
|
||||||
kitManager.registerKit( PuppetKit() )
|
kitManager.registerKit( PuppetKit() )
|
||||||
kitManager.registerKit( RattlesnakeKit() )
|
kitManager.registerKit( RattlesnakeKit() )
|
||||||
|
kitManager.registerKit( SpieloKit() )
|
||||||
kitManager.registerKit( TeslaKit() )
|
kitManager.registerKit( TeslaKit() )
|
||||||
kitManager.registerKit( TheWorldKit() )
|
kitManager.registerKit( TheWorldKit() )
|
||||||
kitManager.registerKit( TridentKit() )
|
kitManager.registerKit( TridentKit() )
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package club.mcscrims.speedhg.gui.listener
|
|||||||
import club.mcscrims.speedhg.gui.anvil.AnvilSearchMenu
|
import club.mcscrims.speedhg.gui.anvil.AnvilSearchMenu
|
||||||
import club.mcscrims.speedhg.gui.anvil.AnvilSearchTracker
|
import club.mcscrims.speedhg.gui.anvil.AnvilSearchTracker
|
||||||
import club.mcscrims.speedhg.gui.menu.MenuHolder
|
import club.mcscrims.speedhg.gui.menu.MenuHolder
|
||||||
|
import club.mcscrims.speedhg.kit.impl.SpieloKit
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
import org.bukkit.event.EventHandler
|
import org.bukkit.event.EventHandler
|
||||||
import org.bukkit.event.EventPriority
|
import org.bukkit.event.EventPriority
|
||||||
@@ -56,6 +57,15 @@ class MenuListener : Listener {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Spielo-SlotMachine ────────────────────────────────────────────────
|
||||||
|
val spieloHolder = event.inventory.holder as? SpieloKit.SlotMachineGui
|
||||||
|
if ( spieloHolder != null )
|
||||||
|
{
|
||||||
|
event.isCancelled = true
|
||||||
|
spieloHolder.onClick( event )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ── Chest-Menü (MenuHolder-Dispatch) ───────────────────────────────────
|
// ── Chest-Menü (MenuHolder-Dispatch) ───────────────────────────────────
|
||||||
val holder = event.inventory.holder as? MenuHolder ?: return
|
val holder = event.inventory.holder as? MenuHolder ?: return
|
||||||
val menu = holder.menu
|
val menu = holder.menu
|
||||||
@@ -91,6 +101,14 @@ class MenuListener : Listener {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Spielo-SlotMachine ────────────────────────────────────────────────
|
||||||
|
val spieloHolder = event.inventory.holder as? SpieloKit.SlotMachineGui
|
||||||
|
if ( spieloHolder != null )
|
||||||
|
{
|
||||||
|
spieloHolder.onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ── Chest-Menü: onClose-Hook aufrufen ─────────────────────────────────
|
// ── Chest-Menü: onClose-Hook aufrufen ─────────────────────────────────
|
||||||
val holder = event.inventory.holder as? MenuHolder ?: return
|
val holder = event.inventory.holder as? MenuHolder ?: return
|
||||||
holder.menu.onClose(event, player)
|
holder.menu.onClose(event, player)
|
||||||
|
|||||||
544
src/main/kotlin/club/mcscrims/speedhg/kit/impl/SpieloKit.kt
Normal file
544
src/main/kotlin/club/mcscrims/speedhg/kit/impl/SpieloKit.kt
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
package club.mcscrims.speedhg.kit.impl
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.disaster.impl.EarthquakeDisaster
|
||||||
|
import club.mcscrims.speedhg.disaster.impl.MeteorDisaster
|
||||||
|
import club.mcscrims.speedhg.disaster.impl.ThunderDisaster
|
||||||
|
import club.mcscrims.speedhg.disaster.impl.TornadoDisaster
|
||||||
|
import club.mcscrims.speedhg.kit.Kit
|
||||||
|
import club.mcscrims.speedhg.kit.Playstyle
|
||||||
|
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
||||||
|
import club.mcscrims.speedhg.kit.ability.ActiveAbility
|
||||||
|
import club.mcscrims.speedhg.kit.ability.PassiveAbility
|
||||||
|
import club.mcscrims.speedhg.util.ItemBuilder
|
||||||
|
import club.mcscrims.speedhg.util.trans
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.Particle
|
||||||
|
import org.bukkit.Sound
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
|
import org.bukkit.inventory.Inventory
|
||||||
|
import org.bukkit.inventory.InventoryHolder
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
import org.bukkit.potion.PotionEffect
|
||||||
|
import org.bukkit.potion.PotionEffectType
|
||||||
|
import org.bukkit.scheduler.BukkitRunnable
|
||||||
|
import org.bukkit.scheduler.BukkitTask
|
||||||
|
import java.util.Random
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ## SpieloKit
|
||||||
|
*
|
||||||
|
* | Playstyle | Beschreibung |
|
||||||
|
* |-------------|---------------------------------------------------------------------------------|
|
||||||
|
* | AGGRESSIVE | Gambeln per Knopfdruck – Items, Events oder **Instant Death** möglich |
|
||||||
|
* | DEFENSIVE | Öffnet eine Slot-Maschinen-GUI (nur wenn kein Feind in der Nähe) – sicherer: |
|
||||||
|
* | | keine Dia-Armor, kein Instant-Death-Outcome |
|
||||||
|
*
|
||||||
|
* ### Aggressive – Outcome-Wahrscheinlichkeiten
|
||||||
|
* | 5 % | Instant Death |
|
||||||
|
* | 15 % | Disaster-Event (Meteor, Tornado, ...) |
|
||||||
|
* | 10 % | Negative Effekte (Slowness, Nausea, ...) |
|
||||||
|
* | 20 % | Neutrale Items |
|
||||||
|
* | 50 % | Positive Items (inkl. möglicher Dia-Armor) |
|
||||||
|
*
|
||||||
|
* ### Defensive – Slot-Maschinen-GUI
|
||||||
|
* Öffnet sich nur wenn kein Feind in [SAFE_RADIUS] Blöcken ist.
|
||||||
|
* Gleiche Outcome-Tabelle, ABER ohne Instant-Death und ohne Dia-Armor.
|
||||||
|
* Die GUI animiert drei Walzen nacheinander, bevor das Ergebnis feststeht.
|
||||||
|
*
|
||||||
|
* ### Integration
|
||||||
|
* Die [SlotMachineGui] nutzt einen eigenen [InventoryHolder]. Der Click-Dispatch
|
||||||
|
* läuft über den zentralen [MenuListener] – dafür muss in [MenuListener.onInventoryClick]
|
||||||
|
* ein zusätzlicher Branch ergänzt werden:
|
||||||
|
* ```kotlin
|
||||||
|
* val spieloHolder = event.inventory.holder as? SpieloKit.SlotMachineGui ?: ...
|
||||||
|
* spieloHolder.onClick(event)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class SpieloKit : Kit() {
|
||||||
|
|
||||||
|
private val plugin get() = SpeedHG.instance
|
||||||
|
private val rng = Random()
|
||||||
|
private val mm = MiniMessage.miniMessage()
|
||||||
|
|
||||||
|
override val id = "spielo"
|
||||||
|
override val displayName: Component
|
||||||
|
get() = plugin.languageManager.getDefaultComponent("kits.spielo.name", mapOf())
|
||||||
|
override val lore: List<String>
|
||||||
|
get() = plugin.languageManager.getDefaultRawMessageList("kits.spielo.lore")
|
||||||
|
override val icon = Material.GOLD_NUGGET
|
||||||
|
|
||||||
|
// Blockiert Doppel-Trigger während eine Animation läuft
|
||||||
|
internal val gamblingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||||
|
|
||||||
|
// Cooldowns für den Aggressive-Automaten
|
||||||
|
private val activeCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTIVE_COOLDOWN_MS = 12_000L // 12 s zwischen Aggressive-Uses
|
||||||
|
const val SAFE_RADIUS = 12.0 // Feind-Radius für Defensive-GUI-Sperrung
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gecachte Instanzen ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private val aggressiveActive = AggressiveActive()
|
||||||
|
private val defensiveActive = DefensiveActive()
|
||||||
|
private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE)
|
||||||
|
private val defensivePassive = NoPassive(Playstyle.DEFENSIVE)
|
||||||
|
|
||||||
|
override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||||
|
Playstyle.AGGRESSIVE -> aggressiveActive
|
||||||
|
Playstyle.DEFENSIVE -> defensiveActive
|
||||||
|
}
|
||||||
|
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||||
|
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||||
|
Playstyle.DEFENSIVE -> defensivePassive
|
||||||
|
}
|
||||||
|
|
||||||
|
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||||
|
|
||||||
|
override fun giveItems(player: Player, playstyle: Playstyle) {
|
||||||
|
val (mat, active) = when (playstyle) {
|
||||||
|
Playstyle.AGGRESSIVE -> Material.GOLD_NUGGET to aggressiveActive
|
||||||
|
Playstyle.DEFENSIVE -> Material.GOLD_BLOCK to defensiveActive
|
||||||
|
}
|
||||||
|
val item = ItemBuilder(mat)
|
||||||
|
.name(active.name)
|
||||||
|
.lore(listOf(active.description))
|
||||||
|
.build()
|
||||||
|
cachedItems[player.uniqueId] = listOf(item)
|
||||||
|
player.inventory.addItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemove(player: Player) {
|
||||||
|
gamblingPlayers.remove(player.uniqueId)
|
||||||
|
activeCooldowns.remove(player.uniqueId)
|
||||||
|
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// AGGRESSIVE active – Sofort-Gamble (Instant-Death möglich)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) {
|
||||||
|
|
||||||
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
|
override val kitId = "spielo"
|
||||||
|
override val name: String
|
||||||
|
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.automat.name")
|
||||||
|
override val description: String
|
||||||
|
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.automat.description")
|
||||||
|
override val hardcodedHitsRequired = 12
|
||||||
|
override val triggerMaterial = Material.GOLD_NUGGET
|
||||||
|
|
||||||
|
override fun execute(player: Player): AbilityResult {
|
||||||
|
if (gamblingPlayers.contains(player.uniqueId))
|
||||||
|
return AbilityResult.ConditionNotMet("Automat läuft bereits!")
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val lastUse = activeCooldowns[player.uniqueId] ?: 0L
|
||||||
|
if (now - lastUse < ACTIVE_COOLDOWN_MS) {
|
||||||
|
val secLeft = (ACTIVE_COOLDOWN_MS - (now - lastUse)) / 1000
|
||||||
|
return AbilityResult.ConditionNotMet("Cooldown: ${secLeft}s")
|
||||||
|
}
|
||||||
|
|
||||||
|
activeCooldowns[player.uniqueId] = now
|
||||||
|
gamblingPlayers.add(player.uniqueId)
|
||||||
|
|
||||||
|
// Kurze Sound-Animation (0,8 s) → dann Ergebnis
|
||||||
|
playQuickAnimation(player) {
|
||||||
|
gamblingPlayers.remove(player.uniqueId)
|
||||||
|
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@playQuickAnimation
|
||||||
|
resolveOutcome(player, allowInstantDeath = true, allowDiamondArmor = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return AbilityResult.Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// DEFENSIVE active – Slot-Maschinen-GUI öffnen
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) {
|
||||||
|
|
||||||
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
|
override val kitId = "spielo"
|
||||||
|
override val name: String
|
||||||
|
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.slotautomat.name")
|
||||||
|
override val description: String
|
||||||
|
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.slotautomat.description")
|
||||||
|
override val hardcodedHitsRequired = 15
|
||||||
|
override val triggerMaterial = Material.GOLD_BLOCK
|
||||||
|
|
||||||
|
override fun execute(player: Player): AbilityResult {
|
||||||
|
// Prüfen ob ein Feind zu nah ist
|
||||||
|
val enemyNearby = plugin.gameManager.alivePlayers
|
||||||
|
.asSequence()
|
||||||
|
.filter { it != player.uniqueId }
|
||||||
|
.mapNotNull { Bukkit.getPlayer(it) }
|
||||||
|
.any { it.location.distanceSquared(player.location) <= SAFE_RADIUS * SAFE_RADIUS }
|
||||||
|
|
||||||
|
if (gamblingPlayers.contains(player.uniqueId))
|
||||||
|
return AbilityResult.ConditionNotMet("Automat läuft bereits!")
|
||||||
|
|
||||||
|
if (enemyNearby)
|
||||||
|
{
|
||||||
|
playQuickAnimation(player) {
|
||||||
|
gamblingPlayers.remove(player.uniqueId)
|
||||||
|
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@playQuickAnimation
|
||||||
|
resolveOutcome(player, allowInstantDeath = false, allowDiamondArmor = false)
|
||||||
|
}
|
||||||
|
return AbilityResult.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
SlotMachineGui(player).open()
|
||||||
|
return AbilityResult.Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Slot-Maschinen-GUI
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3×9-Chest-GUI mit drei animierten Walzen.
|
||||||
|
*
|
||||||
|
* ### Slot-Layout (27 Slots):
|
||||||
|
* ```
|
||||||
|
* [ F ][ F ][ F ][ F ][ F ][ F ][ F ][ F ][ F ] ← Filler
|
||||||
|
* [ F ][ F ][W1 ][ F ][W2 ][ F ][W3 ][ F ][ F ] ← Walzen (11, 13, 15)
|
||||||
|
* [ F ][ F ][ F ][ F ][BTN][ F ][ F ][ F ][ F ] ← Spin-Button (22)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Ablauf:
|
||||||
|
* 1. Spieler öffnet GUI → Walzen zeigen zufällige Symbole, Button ist grün.
|
||||||
|
* 2. Spieler klickt Slot 22 ("Drehen") → Animation startet, Button wird gelb.
|
||||||
|
* 3. Walzen stoppen gestaffelt (Walze 1 → 2 → 3).
|
||||||
|
* 4. Nach dem letzten Stop: Outcome auflösen, GUI schließen.
|
||||||
|
*
|
||||||
|
* Der Click-Dispatch muss im [MenuListener] ergänzt werden:
|
||||||
|
* ```kotlin
|
||||||
|
* (event.inventory.holder as? SpieloKit.SlotMachineGui)?.onClick(event)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
inner class SlotMachineGui(private val player: Player) : InventoryHolder {
|
||||||
|
|
||||||
|
private val inv: Inventory = Bukkit.createInventory(
|
||||||
|
this, 27,
|
||||||
|
mm.deserialize("<gold><bold>🎰 Slot-Automat</bold></gold>")
|
||||||
|
)
|
||||||
|
|
||||||
|
private val reelSlots = intArrayOf(11, 13, 15)
|
||||||
|
private val spinButton = 22
|
||||||
|
|
||||||
|
// Symbole die auf den Walzen erscheinen (nur visuell – kein Einfluss auf Outcome)
|
||||||
|
private val reelSymbols = listOf(
|
||||||
|
Material.GOLD_NUGGET, Material.EMERALD, Material.IRON_INGOT,
|
||||||
|
Material.GOLDEN_APPLE, Material.MUSHROOM_STEW, Material.EXPERIENCE_BOTTLE,
|
||||||
|
Material.TNT, Material.BARRIER, Material.NETHER_STAR, Material.LAPIS_LAZULI
|
||||||
|
)
|
||||||
|
|
||||||
|
private var isSpinning = false
|
||||||
|
private var lastAnimTask: BukkitTask? = null
|
||||||
|
|
||||||
|
override fun getInventory(): Inventory = inv
|
||||||
|
|
||||||
|
fun open() {
|
||||||
|
drawLayout()
|
||||||
|
player.openInventory(inv)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawLayout() {
|
||||||
|
val filler = buildFiller()
|
||||||
|
repeat(27) { inv.setItem(it, filler) }
|
||||||
|
reelSlots.forEach { inv.setItem(it, buildReelItem(reelSymbols.random())) }
|
||||||
|
inv.setItem(spinButton, buildSpinButton(spinning = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Event-Hooks (aufgerufen von MenuListener) ─────────────────────────
|
||||||
|
|
||||||
|
fun onClick(event: InventoryClickEvent) {
|
||||||
|
event.isCancelled = true
|
||||||
|
if (isSpinning) return
|
||||||
|
if (event.rawSlot != spinButton) return
|
||||||
|
|
||||||
|
isSpinning = true
|
||||||
|
gamblingPlayers.add(player.uniqueId)
|
||||||
|
inv.setItem(spinButton, buildSpinButton(spinning = true))
|
||||||
|
|
||||||
|
startSpinAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aufgerufen wenn Inventar geschlossen wird (z.B. ESC). */
|
||||||
|
fun onClose() {
|
||||||
|
lastAnimTask?.cancel()
|
||||||
|
// Charge nur zurückgeben wenn noch nicht gedreht wurde
|
||||||
|
if (!isSpinning) {
|
||||||
|
gamblingPlayers.remove(player.uniqueId)
|
||||||
|
}
|
||||||
|
// Wenn isSpinning == true läuft die Animation noch – Cleanup in onAllReelsStopped
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Animation ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet die gestaffelte Walzen-Animation.
|
||||||
|
* Walze 1 stoppt nach 8 Frames, Walze 2 nach 12, Walze 3 nach 16.
|
||||||
|
* Jeder Frame dauert 2 Ticks (0,1 s). Starts sind versetzt (+5 Ticks pro Walze).
|
||||||
|
*/
|
||||||
|
private fun startSpinAnimation() {
|
||||||
|
val framesPerReel = intArrayOf(8, 12, 16)
|
||||||
|
val startDelays = longArrayOf(0L, 5L, 10L)
|
||||||
|
val ticksPerFrame = 2L
|
||||||
|
var stoppedReels = 0
|
||||||
|
|
||||||
|
for (reelIdx in 0..2) {
|
||||||
|
val slot = reelSlots[reelIdx]
|
||||||
|
val maxFrames = framesPerReel[reelIdx]
|
||||||
|
var frame = 0
|
||||||
|
|
||||||
|
val task = object : BukkitRunnable() {
|
||||||
|
override fun run()
|
||||||
|
{
|
||||||
|
if (!player.isOnline) {
|
||||||
|
this.cancel(); return
|
||||||
|
}
|
||||||
|
frame++
|
||||||
|
|
||||||
|
if (frame <= maxFrames) {
|
||||||
|
// Zufälliges Walzen-Symbol während Rotation
|
||||||
|
inv.setItem(slot, buildReelItem(reelSymbols.random()))
|
||||||
|
val pitch = (0.6f + frame * 0.07f).coerceAtMost(2.0f)
|
||||||
|
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.4f, pitch)
|
||||||
|
} else {
|
||||||
|
// Einrasten – finales Symbol (zufällig, rein visuell)
|
||||||
|
inv.setItem(slot, buildReelItem(reelSymbols.random()))
|
||||||
|
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_CHIME, 0.9f, 1.1f + reelIdx * 0.2f)
|
||||||
|
|
||||||
|
stoppedReels++
|
||||||
|
if (stoppedReels == 3) onAllReelsStopped()
|
||||||
|
|
||||||
|
this.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.runTaskTimer( plugin, startDelays[ reelIdx ], ticksPerFrame )
|
||||||
|
|
||||||
|
lastAnimTask = task
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAllReelsStopped() {
|
||||||
|
player.playSound(player.location, Sound.ENTITY_PLAYER_LEVELUP, 0.7f, 1.5f)
|
||||||
|
|
||||||
|
// Kurze Pause, dann Outcome auslösen und GUI schließen
|
||||||
|
Bukkit.getScheduler().runTaskLater(plugin, { ->
|
||||||
|
gamblingPlayers.remove(player.uniqueId)
|
||||||
|
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
|
||||||
|
player.closeInventory()
|
||||||
|
// Defensive: kein Instant-Death, keine Dia-Armor
|
||||||
|
resolveOutcome(player, allowInstantDeath = false, allowDiamondArmor = false)
|
||||||
|
}, 20L)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Item-Builder ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun buildReelItem(material: Material) = ItemStack(material).also { item ->
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(Component.text(" ").decoration(TextDecoration.ITALIC, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSpinButton(spinning: Boolean): ItemStack {
|
||||||
|
val mat = if (spinning) Material.YELLOW_CONCRETE else Material.LIME_CONCRETE
|
||||||
|
val name = if (spinning)
|
||||||
|
mm.deserialize("<yellow><bold>⟳ Dreht...</bold></yellow>")
|
||||||
|
else
|
||||||
|
mm.deserialize("<green><bold>▶ Drehen!</bold></green>")
|
||||||
|
|
||||||
|
return ItemStack(mat).also { item ->
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(name.decoration(TextDecoration.ITALIC, false))
|
||||||
|
if (!spinning) {
|
||||||
|
meta.lore(listOf(
|
||||||
|
Component.empty(),
|
||||||
|
mm.deserialize("<gray>Klicken um die Walzen zu drehen.")
|
||||||
|
.decoration(TextDecoration.ITALIC, false),
|
||||||
|
Component.empty()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFiller() = ItemStack(Material.BLACK_STAINED_GLASS_PANE).also { item ->
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(Component.text(" ").decoration(TextDecoration.ITALIC, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Outcome-Auflösung – gemeinsam für Aggressive und Defensive
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst das Gamble-Ergebnis auf.
|
||||||
|
* @param allowInstantDeath true = Aggressive (5 % Instant Death möglich)
|
||||||
|
* @param allowDiamondArmor true = Aggressive (Dia-Armor in Loot möglich)
|
||||||
|
*/
|
||||||
|
fun resolveOutcome(player: Player, allowInstantDeath: Boolean, allowDiamondArmor: Boolean) {
|
||||||
|
val roll = rng.nextDouble()
|
||||||
|
|
||||||
|
when {
|
||||||
|
allowInstantDeath && roll < 0.05 -> triggerInstantDeath(player)
|
||||||
|
allowInstantDeath && roll < 0.20 -> triggerRandomDisaster(player)
|
||||||
|
roll < (if (allowInstantDeath) 0.30 else 0.10) -> applyNegativeEffect(player)
|
||||||
|
roll < (if (allowInstantDeath) 0.50 else 0.30) -> giveNeutralItems(player)
|
||||||
|
else -> givePositiveItems(player, allowDiamondArmor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Einzelne Outcome-Typen ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun triggerInstantDeath(player: Player) {
|
||||||
|
player.world.spawnParticle(Particle.EXPLOSION, player.location, 5, 0.5, 0.5, 0.5, 0.0)
|
||||||
|
player.world.playSound(player.location, Sound.ENTITY_WITHER_SPAWN, 1f, 1.5f)
|
||||||
|
player.sendActionBar(player.trans("kits.spielo.messages.instant_death"))
|
||||||
|
|
||||||
|
// Einen Tick später töten damit das ActionBar-Paket noch ankommt
|
||||||
|
Bukkit.getScheduler().runTaskLater(plugin, { ->
|
||||||
|
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
|
||||||
|
player.health = 0.0
|
||||||
|
}, 3L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun triggerRandomDisaster(player: Player) {
|
||||||
|
val disaster = listOf(
|
||||||
|
MeteorDisaster(), TornadoDisaster(), EarthquakeDisaster(), ThunderDisaster()
|
||||||
|
).random()
|
||||||
|
|
||||||
|
disaster.warn(player)
|
||||||
|
Bukkit.getScheduler().runTaskLater(plugin, { ->
|
||||||
|
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
|
||||||
|
disaster.trigger(plugin, player)
|
||||||
|
}, disaster.warningDelayTicks)
|
||||||
|
|
||||||
|
player.world.playSound(player.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 0.6f)
|
||||||
|
player.sendActionBar(player.trans("kits.spielo.messages.gamble_event"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyNegativeEffect(player: Player) {
|
||||||
|
val outcomes: List<() -> Unit> = listOf(
|
||||||
|
{ player.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 6 * 20, 1)) },
|
||||||
|
{ player.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 6 * 20, 1)) },
|
||||||
|
{ player.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 5 * 20, 0)) },
|
||||||
|
{ player.addPotionEffect(PotionEffect(PotionEffectType.WEAKNESS, 8 * 20, 0)) },
|
||||||
|
{ player.fireTicks = 4 * 20 }
|
||||||
|
)
|
||||||
|
outcomes.random().invoke()
|
||||||
|
|
||||||
|
player.playSound(player.location, Sound.ENTITY_VILLAGER_NO, 1f, 0.8f)
|
||||||
|
player.world.spawnParticle(
|
||||||
|
Particle.ANGRY_VILLAGER,
|
||||||
|
player.location.clone().add(0.0, 2.0, 0.0),
|
||||||
|
8, 0.4, 0.3, 0.4, 0.0
|
||||||
|
)
|
||||||
|
player.sendActionBar(player.trans("kits.spielo.messages.gamble_bad"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun giveNeutralItems(player: Player) {
|
||||||
|
val items = listOf(
|
||||||
|
ItemStack(Material.ARROW, rng.nextInt(5) + 3),
|
||||||
|
ItemStack(Material.BREAD, rng.nextInt(4) + 2),
|
||||||
|
ItemStack(Material.IRON_INGOT, rng.nextInt(3) + 1),
|
||||||
|
ItemStack(Material.COBBLESTONE, rng.nextInt(8) + 4),
|
||||||
|
)
|
||||||
|
player.inventory.addItem(items.random())
|
||||||
|
|
||||||
|
player.playSound(player.location, Sound.ENTITY_ITEM_PICKUP, 0.8f, 1.0f)
|
||||||
|
player.sendActionBar(player.trans("kits.spielo.messages.gamble_neutral"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givePositiveItems(player: Player, allowDiamondArmor: Boolean) {
|
||||||
|
data class LootEntry(val item: ItemStack, val weight: Int)
|
||||||
|
|
||||||
|
val pool = buildList {
|
||||||
|
add(LootEntry(ItemStack(Material.MUSHROOM_STEW, 3), 30))
|
||||||
|
add(LootEntry(ItemStack(Material.MUSHROOM_STEW, 5), 15))
|
||||||
|
add(LootEntry(ItemStack(Material.GOLDEN_APPLE), 20))
|
||||||
|
add(LootEntry(ItemStack(Material.ENCHANTED_GOLDEN_APPLE), 3))
|
||||||
|
add(LootEntry(ItemStack(Material.EXPERIENCE_BOTTLE, 5), 12))
|
||||||
|
add(LootEntry(buildSplashPotion(PotionEffectType.STRENGTH, 200, 0), 8))
|
||||||
|
add(LootEntry(buildSplashPotion(PotionEffectType.SPEED, 400, 0), 8))
|
||||||
|
add(LootEntry(buildSplashPotion(PotionEffectType.REGENERATION, 160, 1), 8))
|
||||||
|
// Eisen-Rüstung: immer möglich
|
||||||
|
add(LootEntry(ItemStack(Material.IRON_CHESTPLATE), 4))
|
||||||
|
add(LootEntry(ItemStack(Material.IRON_HELMET), 4))
|
||||||
|
// Dia-Rüstung: nur Aggressive
|
||||||
|
if (allowDiamondArmor) {
|
||||||
|
add(LootEntry(ItemStack(Material.DIAMOND_CHESTPLATE), 2))
|
||||||
|
add(LootEntry(ItemStack(Material.DIAMOND_HELMET), 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalWeight = pool.sumOf { it.weight }
|
||||||
|
var roll = rng.nextInt(totalWeight)
|
||||||
|
val chosen = pool.first { entry -> roll -= entry.weight; roll < 0 }
|
||||||
|
player.inventory.addItem(chosen.item.clone())
|
||||||
|
|
||||||
|
player.playSound(player.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.4f)
|
||||||
|
player.world.spawnParticle(
|
||||||
|
Particle.HAPPY_VILLAGER,
|
||||||
|
player.location.clone().add(0.0, 1.5, 0.0),
|
||||||
|
12, 0.4, 0.4, 0.4, 0.0
|
||||||
|
)
|
||||||
|
player.sendActionBar(player.trans("kits.spielo.messages.gamble_good"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Stubs
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) {
|
||||||
|
override val name = "None"
|
||||||
|
override val description = "None"
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Hilfsmethoden
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/** Klicker-Sounds mit steigendem Pitch, danach Callback. */
|
||||||
|
private fun playQuickAnimation(player: Player, onFinish: () -> Unit) {
|
||||||
|
for (i in 0..5) {
|
||||||
|
Bukkit.getScheduler().runTaskLater(plugin, { ->
|
||||||
|
if (!player.isOnline) return@runTaskLater
|
||||||
|
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.9f, 0.5f + i * 0.25f)
|
||||||
|
player.world.spawnParticle(
|
||||||
|
Particle.NOTE,
|
||||||
|
player.location.clone().add(0.0, 2.3, 0.0),
|
||||||
|
1, 0.2, 0.1, 0.2, 0.0
|
||||||
|
)
|
||||||
|
}, i * 3L)
|
||||||
|
}
|
||||||
|
Bukkit.getScheduler().runTaskLater(plugin, Runnable(onFinish), 18L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSplashPotion(type: PotionEffectType, duration: Int, amplifier: Int) =
|
||||||
|
ItemStack(Material.SPLASH_POTION).also { potion ->
|
||||||
|
potion.editMeta { meta ->
|
||||||
|
if (meta is org.bukkit.inventory.meta.PotionMeta)
|
||||||
|
meta.addCustomEffect(PotionEffect(type, duration, amplifier), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user