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:
TDSTOS
2026-04-11 22:48:51 +02:00
parent f6eb654d47
commit 59cdc4d202
3 changed files with 563 additions and 0 deletions

View File

@@ -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() )

View File

@@ -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)

View 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)
}
}
}