diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index c7948bb..5310af6 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -224,6 +224,7 @@ class SpeedHG : JavaPlugin() { kitManager.registerKit( NinjaKit() ) kitManager.registerKit( PuppetKit() ) kitManager.registerKit( RattlesnakeKit() ) + kitManager.registerKit( SpieloKit() ) kitManager.registerKit( TeslaKit() ) kitManager.registerKit( TheWorldKit() ) kitManager.registerKit( TridentKit() ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/gui/listener/MenuListener.kt b/src/main/kotlin/club/mcscrims/speedhg/gui/listener/MenuListener.kt index 35f05c4..cb641ae 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/gui/listener/MenuListener.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/gui/listener/MenuListener.kt @@ -3,6 +3,7 @@ package club.mcscrims.speedhg.gui.listener import club.mcscrims.speedhg.gui.anvil.AnvilSearchMenu import club.mcscrims.speedhg.gui.anvil.AnvilSearchTracker import club.mcscrims.speedhg.gui.menu.MenuHolder +import club.mcscrims.speedhg.kit.impl.SpieloKit import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority @@ -56,6 +57,15 @@ class MenuListener : Listener { 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) ─────────────────────────────────── val holder = event.inventory.holder as? MenuHolder ?: return val menu = holder.menu @@ -91,6 +101,14 @@ class MenuListener : Listener { return } + // ── Spielo-SlotMachine ──────────────────────────────────────────────── + val spieloHolder = event.inventory.holder as? SpieloKit.SlotMachineGui + if ( spieloHolder != null ) + { + spieloHolder.onClose() + return + } + // ── Chest-Menü: onClose-Hook aufrufen ───────────────────────────────── val holder = event.inventory.holder as? MenuHolder ?: return holder.menu.onClose(event, player) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/SpieloKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/SpieloKit.kt new file mode 100644 index 0000000..7a2801a --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/SpieloKit.kt @@ -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 + 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 = ConcurrentHashMap.newKeySet() + + // Cooldowns für den Aggressive-Automaten + private val activeCooldowns: MutableMap = 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>() + + 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("🎰 Slot-Automat") + ) + + 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("⟳ Dreht...") + else + mm.deserialize("▶ Drehen!") + + return ItemStack(mat).also { item -> + item.editMeta { meta -> + meta.displayName(name.decoration(TextDecoration.ITALIC, false)) + if (!spinning) { + meta.lore(listOf( + Component.empty(), + mm.deserialize("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) + } + } +} \ No newline at end of file