From 6117903b3622700635d2097f03d3ec2ab7a4c4ff Mon Sep 17 00:00:00 2001 From: Callum Johnson <74352969+CallumJohnson@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:20:50 +0100 Subject: [PATCH 1/2] feat: Bukkit custom events --- .../willfp/ecomobs/event/EcoMobDamageEvent.kt | 50 ++++++++ .../willfp/ecomobs/event/EcoMobDropsEvent.kt | 52 ++++++++ .../willfp/ecomobs/event/EcoMobEggUseEvent.kt | 57 +++++++++ .../ecomobs/event/EcoMobKillPlayerEvent.kt | 27 +++++ .../event/EcoMobLifespanExpireEvent.kt | 40 +++++++ .../ecomobs/event/EcoMobSpawnerBreakEvent.kt | 49 ++++++++ .../event/EcoMobSpawnerExplodeEvent.kt | 33 ++++++ .../event/EcoMobSpawnerPickBlockEvent.kt | 42 +++++++ .../ecomobs/event/EcoMobSpawnerPlaceEvent.kt | 41 +++++++ .../ecomobs/event/EcoMobSpawnerSpawnEvent.kt | 42 +++++++ .../ecomobs/event/EcoMobSpawnerStackEvent.kt | 47 ++++++++ .../ecomobs/event/EcoMobSpawnerTickEvent.kt | 50 ++++++++ .../event/EcoMobSpawnerUnstackEvent.kt | 56 +++++++++ .../ecomobs/event/EcoMobStackDeathEvent.kt | 34 ++++++ .../ecomobs/event/EcoMobStackMergeEvent.kt | 48 ++++++++ .../ecomobs/event/EcoMobStackSplitEvent.kt | 49 ++++++++ .../ecomobs/event/EcoMobStageChangeEvent.kt | 45 +++++++ .../ecomobs/event/EcoMobTotemBuildEvent.kt | 43 +++++++ .../com/willfp/ecomobs/event/MobStackEvent.kt | 14 +++ .../com/willfp/ecomobs/event/SpawnerEvent.kt | 18 +++ .../ecomobs/handler/DamageModifierHandler.kt | 16 +++ .../willfp/ecomobs/handler/SpawnEggHandler.kt | 18 ++- .../ecomobs/handler/SpawnTotemHandler.kt | 11 +- .../willfp/ecomobs/handler/SpawnerHandler.kt | 111 ++++++++++++++---- .../ecomobs/handler/SpawnerStackHandler.kt | 4 +- .../willfp/ecomobs/handler/StackHandler.kt | 23 +++- .../mob/event/impl/MobEventKillPlayer.kt | 4 + .../ecomobs/mob/impl/ConfigDrivenEcoMob.kt | 10 +- .../willfp/ecomobs/mob/impl/LivingMobImpl.kt | 14 ++- .../willfp/ecomobs/mob/options/MobDrops.kt | 62 +++++++--- .../ecomobs/mob/stage/DamageStageTracker.kt | 12 +- .../willfp/ecomobs/spawner/PlacedSpawner.kt | 15 ++- .../willfp/ecomobs/spawner/SpawnerSpawning.kt | 18 ++- .../willfp/ecomobs/spawner/SpawnerStacks.kt | 22 +++- .../com/willfp/ecomobs/stacking/MobStacks.kt | 9 ++ .../ecomobs/tick/TickHandlerLifespan.kt | 15 ++- 36 files changed, 1141 insertions(+), 60 deletions(-) create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDamageEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDropsEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobEggUseEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobKillPlayerEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobLifespanExpireEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerBreakEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerExplodeEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPickBlockEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPlaceEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerSpawnEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerTickEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerUnstackEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackDeathEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackMergeEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackSplitEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStageChangeEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobTotemBuildEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/MobStackEvent.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/SpawnerEvent.kt diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDamageEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDamageEvent.kt new file mode 100644 index 00000000..711d05bd --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDamageEvent.kt @@ -0,0 +1,50 @@ +package com.willfp.ecomobs.event + +import com.willfp.ecomobs.mob.LivingMob +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList +import org.bukkit.event.entity.EntityDamageEvent.DamageCause + +/** + * Called when an EcoMob takes damage, after the mob's own damage modifiers have been + * applied and before its damage stages see the hit. + * + * Cancelling cancels the underlying [org.bukkit.event.entity.EntityDamageEvent]. + */ +class EcoMobDamageEvent( + override val mob: LivingMob, + /** + * The player who dealt the damage, or null if it came from elsewhere. + */ + val player: Player?, + val cause: DamageCause, + /** + * The damage dealt. Writing to it writes back to the bukkit event. + */ + var damage: Double +) : Event(), MobEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDropsEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDropsEvent.kt new file mode 100644 index 00000000..615d47a8 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobDropsEvent.kt @@ -0,0 +1,52 @@ +package com.willfp.ecomobs.event + +import com.willfp.ecomobs.mob.EcoMob +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList +import org.bukkit.inventory.ItemStack + +/** + * Called once the mob's drop table has been rolled, before anything is given out. + * Cancelling drops nothing at all. + * + * Fired once per mob of a stack, as each one rolls the table separately. + */ +class EcoMobDropsEvent( + val ecoMob: EcoMob, + val location: Location, + /** + * The player the drops go to, or null if they fall on the ground. + */ + val player: Player?, + /** + * The items rolled, which can be edited in place. + */ + val drops: MutableList, + var experience: Int +) : Event(), Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobEggUseEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobEggUseEvent.kt new file mode 100644 index 00000000..0974a1a7 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobEggUseEvent.kt @@ -0,0 +1,57 @@ +package com.willfp.ecomobs.event + +import com.willfp.ecomobs.mob.EcoMob +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when a spawn egg is about to spawn its mob, before the egg's conditions are + * tested. Cancelling stops the spawn and leaves the egg in the inventory. + */ +class EcoMobEggUseEvent( + val mob: EcoMob, + val location: Location, + val player: Player?, + val source: Source +) : Event(), Cancellable { + private var isCancelled: Boolean = false + + /** + * What used the egg. + */ + enum class Source { + /** + * A player right-clicked a block with the egg. + */ + PLAYER, + + /** + * A dispenser or dropper fired the egg. + */ + DISPENSER + } + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobKillPlayerEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobKillPlayerEvent.kt new file mode 100644 index 00000000..6077cbfe --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobKillPlayerEvent.kt @@ -0,0 +1,27 @@ +package com.willfp.ecomobs.event + +import com.willfp.ecomobs.mob.LivingMob +import org.bukkit.entity.Player +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent + +/** + * Called when an EcoMob kills a player. + */ +class EcoMobKillPlayerEvent( + override val mob: LivingMob, + player: Player +) : PlayerEvent(player), MobEvent { + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobLifespanExpireEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobLifespanExpireEvent.kt new file mode 100644 index 00000000..17e7ab68 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobLifespanExpireEvent.kt @@ -0,0 +1,40 @@ +package com.willfp.ecomobs.event + +import com.willfp.ecomobs.mob.LivingMob +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when a mob has run out its lifespan and is about to despawn, which is what + * separates it from the despawns [EcoMobDespawnEvent] covers. + * + * Cancelling keeps the mob alive for now. Its lifespan is still spent, so the event is + * called again on the next tick unless something else steps in. + */ +class EcoMobLifespanExpireEvent( + override val mob: LivingMob +) : Event(), MobEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerBreakEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerBreakEvent.kt new file mode 100644 index 00000000..cd101953 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerBreakEvent.kt @@ -0,0 +1,49 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent + +/** + * Called when a player breaks a custom spawner and the whole thing comes up, after the + * pickup rules have been applied. A stacked spawner that only loses one of its spawners + * fires [EcoMobSpawnerUnstackEvent] instead. + * + * Cancelling also cancels the block break. + */ +class EcoMobSpawnerBreakEvent( + player: Player, + override val location: Location, + override val mobId: String?, + val stackSize: Int, + /** + * Whether the spawner drops as an item. Starts as what the spawner's pickup setting + * and the player's permissions allow. + */ + var dropsItem: Boolean +) : PlayerEvent(player), SpawnerEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerExplodeEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerExplodeEvent.kt new file mode 100644 index 00000000..cf205a0e --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerExplodeEvent.kt @@ -0,0 +1,33 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called for every custom spawner caught in an explosion, whether or not it is + * explosion proof. Set [isProtected] to decide whether it survives. + */ +class EcoMobSpawnerExplodeEvent( + override val location: Location, + override val mobId: String?, + val stackSize: Int, + /** + * Whether the spawner survives the explosion. Starts as the spawner's own + * explosion-proof setting. + */ + var isProtected: Boolean +) : Event(), SpawnerEvent { + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPickBlockEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPickBlockEvent.kt new file mode 100644 index 00000000..1e140fa9 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPickBlockEvent.kt @@ -0,0 +1,42 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent +import org.bukkit.inventory.ItemStack + +/** + * Called when a creative player middle-clicks a custom spawner, before the spawner item + * is put in their hand. + */ +class EcoMobSpawnerPickBlockEvent( + player: Player, + override val location: Location, + override val mobId: String?, + val item: ItemStack +) : PlayerEvent(player), SpawnerEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPlaceEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPlaceEvent.kt new file mode 100644 index 00000000..1a1e15e8 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerPlaceEvent.kt @@ -0,0 +1,41 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent + +/** + * Called when a player places a custom spawner, before its settings are written to the + * placed block. Cancelling also cancels the block placement. + */ +class EcoMobSpawnerPlaceEvent( + player: Player, + override val location: Location, + override val mobId: String?, + val stackSize: Int +) : PlayerEvent(player), SpawnerEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerSpawnEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerSpawnEvent.kt new file mode 100644 index 00000000..b7d0f43b --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerSpawnEvent.kt @@ -0,0 +1,42 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called for every mob a spawner is about to spawn, in both spawner modes and once per + * mob in a stacked spawner's cycle. + */ +class EcoMobSpawnerSpawnEvent( + override val location: Location, + override val mobId: String?, + /** + * Where the mob appears, which is offset from the spawner itself. + */ + val spawnLocation: Location +) : Event(), SpawnerEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt new file mode 100644 index 00000000..50fc6dd7 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt @@ -0,0 +1,47 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when spawners are about to be added to a stack, whether by right-clicking it + * or by placing against it. + */ +class EcoMobSpawnerStackEvent( + override val location: Location, + override val mobId: String?, + /** + * The stack size before the merge. + */ + val currentSize: Int, + /** + * How many spawners are added. Already capped to the room left in the stack; + * lowering it takes fewer, and raising it is capped again afterwards. + */ + var amount: Int +) : Event(), SpawnerEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerTickEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerTickEvent.kt new file mode 100644 index 00000000..11ede9b9 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerTickEvent.kt @@ -0,0 +1,50 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when a spawner ticked by EcoMobs is about to run a spawn cycle, after its + * nearby-mob cap has been checked. + * + * Only fired in [com.willfp.ecomobs.spawner.SpawnerMode.ECOMOBS]; the server runs the + * cycle itself in the vanilla mode. + */ +class EcoMobSpawnerTickEvent( + override val location: Location, + override val mobId: String?, + /** + * How many mobs the spawner spawns per cycle. + */ + var spawnCount: Int, + /** + * How many spawners the stack holds, each of which spawns [spawnCount] mobs. One + * when spawner stacking is off. + */ + var stackSize: Int +) : Event(), SpawnerEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerUnstackEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerUnstackEvent.kt new file mode 100644 index 00000000..987b4b4c --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerUnstackEvent.kt @@ -0,0 +1,56 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent + +/** + * Called when a player breaks a stacked spawner and takes part of the stack, leaving + * the block in place. Breaking the last one, or the whole stack at once, fires + * [EcoMobSpawnerBreakEvent] instead. + * + * Cancelling leaves the stack untouched and cancels the block break. + */ +class EcoMobSpawnerUnstackEvent( + player: Player, + override val location: Location, + override val mobId: String?, + /** + * The stack size before the break. + */ + val currentSize: Int, + /** + * How many spawners are taken off the stack. + */ + var amount: Int, + /** + * Whether the spawners taken off drop as an item. Starts as what the spawner's + * pickup setting and the player's permissions allow. + */ + var dropsItem: Boolean +) : PlayerEvent(player), SpawnerEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackDeathEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackDeathEvent.kt new file mode 100644 index 00000000..78be96a1 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackDeathEvent.kt @@ -0,0 +1,34 @@ +package com.willfp.ecomobs.event + +import org.bukkit.entity.Mob +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when a stacked mob dies, before the stack is either paid out in full or split. + */ +class EcoMobStackDeathEvent( + override val entity: Mob, + /** + * The size of the stack that died. + */ + val size: Int, + /** + * Whether the whole stack dies at once, paying out every mob's rewards. When false, + * only the one mob dies and the rest come back. Starts as the configured setting. + */ + var killWholeStack: Boolean +) : Event(), MobStackEvent { + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackMergeEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackMergeEvent.kt new file mode 100644 index 00000000..cd82fe02 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackMergeEvent.kt @@ -0,0 +1,48 @@ +package com.willfp.ecomobs.event + +import org.bukkit.entity.Mob +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when a mob is about to be absorbed into a nearby stack. Cancelling leaves both + * mobs alone. + */ +class EcoMobStackMergeEvent( + /** + * The mob being absorbed, which is removed once the merge goes through. + */ + override val entity: Mob, + /** + * The mob it merges into. + */ + val target: Mob, + /** + * The size the target ends up at. + */ + val size: Int +) : Event(), MobStackEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackSplitEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackSplitEvent.kt new file mode 100644 index 00000000..afd33f7f --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStackSplitEvent.kt @@ -0,0 +1,49 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location +import org.bukkit.entity.Mob +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when one mob of a stack has died and the rest are about to come back as a + * fresh mob. Cancelling drops the remainder entirely. + */ +class EcoMobStackSplitEvent( + /** + * The mob that died. + */ + override val entity: Mob, + /** + * Where the remainder respawns. + */ + val location: Location, + /** + * How many mobs the respawned stack stands in for. + */ + var remaining: Int +) : Event(), MobStackEvent, Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStageChangeEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStageChangeEvent.kt new file mode 100644 index 00000000..cd71eb59 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobStageChangeEvent.kt @@ -0,0 +1,45 @@ +package com.willfp.ecomobs.event + +import com.willfp.ecomobs.mob.LivingMob +import com.willfp.ecomobs.mob.stage.DamageStage +import org.bukkit.entity.Player +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +/** + * Called when a mob finishes a damage stage, after that stage's end effects and before + * the next one's start effects. + */ +class EcoMobStageChangeEvent( + override val mob: LivingMob, + /** + * The stage that just finished. + */ + val previousStage: DamageStage, + /** + * The stage now in progress, or null if that was the last one and the mob is about + * to take its killing blow. + */ + val stage: DamageStage?, + /** + * The 1-based position of [stage], or one past the last stage when it is null. + */ + val stageNumber: Int, + /** + * The player whose hit ended the stage, or null if it came from elsewhere. + */ + val player: Player? +) : Event(), MobEvent { + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobTotemBuildEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobTotemBuildEvent.kt new file mode 100644 index 00000000..db8d0609 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobTotemBuildEvent.kt @@ -0,0 +1,43 @@ +package com.willfp.ecomobs.event + +import com.willfp.ecomobs.mob.EcoMob +import com.willfp.ecomobs.mob.SpawnTotem +import org.bukkit.Location +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.HandlerList +import org.bukkit.event.player.PlayerEvent + +/** + * Called when a player completes a spawn totem and its conditions are met, before the + * mob is spawned. + */ +class EcoMobTotemBuildEvent( + val mob: EcoMob, + val totem: SpawnTotem, + val location: Location, + player: Player +) : PlayerEvent(player), Cancellable { + private var isCancelled: Boolean = false + + override fun isCancelled(): Boolean { + return isCancelled + } + + override fun setCancelled(cancelled: Boolean) { + isCancelled = cancelled + } + + override fun getHandlers(): HandlerList { + return HANDLERS + } + + companion object { + private val HANDLERS = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList { + return HANDLERS + } + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/MobStackEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/MobStackEvent.kt new file mode 100644 index 00000000..8f247f5d --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/MobStackEvent.kt @@ -0,0 +1,14 @@ +package com.willfp.ecomobs.event + +import org.bukkit.entity.Mob + +/** + * An event about a stack of mobs. Vanilla mobs stack too, so these carry the bukkit + * entity rather than a [com.willfp.ecomobs.mob.LivingMob]. + */ +interface MobStackEvent { + /** + * The mob that stands in for the stack. + */ + val entity: Mob +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/SpawnerEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/SpawnerEvent.kt new file mode 100644 index 00000000..a2c7340d --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/SpawnerEvent.kt @@ -0,0 +1,18 @@ +package com.willfp.ecomobs.event + +import org.bukkit.Location + +/** + * An event about a spawner block, rather than about a mob. + */ +interface SpawnerEvent { + /** + * The location of the spawner. + */ + val location: Location + + /** + * The ID of the mob the spawner spawns, or null if it has never been set. + */ + val mobId: String? +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/DamageModifierHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/DamageModifierHandler.kt index dd61bbda..ae41e12d 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/DamageModifierHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/DamageModifierHandler.kt @@ -1,6 +1,8 @@ package com.willfp.ecomobs.handler +import com.willfp.ecomobs.event.EcoMobDamageEvent import com.willfp.ecomobs.mob.impl.ecoMob +import org.bukkit.Bukkit import org.bukkit.entity.Mob import org.bukkit.event.EventHandler import org.bukkit.event.Listener @@ -15,5 +17,19 @@ object DamageModifierHandler : Listener { val multiplier = ecoMob.getDamageModifier(event.cause) event.damage *= multiplier + + // Called here rather than from its own listener so it lands after the mob's own + // modifiers and before DamageStageHandler, which runs at HIGH. + val living = ecoMob.getLivingMob(bukkitMob) ?: return + + val damageEvent = EcoMobDamageEvent(living, event.attributedPlayer(), event.cause, event.damage) + Bukkit.getPluginManager().callEvent(damageEvent) + + if (damageEvent.isCancelled) { + event.isCancelled = true + return + } + + event.damage = damageEvent.damage } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnEggHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnEggHandler.kt index 676a7782..f7f0c4f9 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnEggHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnEggHandler.kt @@ -1,7 +1,9 @@ package com.willfp.ecomobs.handler +import com.willfp.ecomobs.event.EcoMobEggUseEvent import com.willfp.ecomobs.mob.options.ecoMobEgg import com.willfp.ecomobs.plugin +import org.bukkit.Bukkit import org.bukkit.Location import org.bukkit.block.Container import org.bukkit.block.data.Directional @@ -23,7 +25,7 @@ object SpawnEggHandler : Listener { val location = event.clickedBlock?.location?.add(0.0, 1.5, 0.0) ?: return - if (!this.handleSpawnEgg(event.item, location, event.player)) { + if (!this.handleSpawnEgg(event.item, location, event.player, EcoMobEggUseEvent.Source.PLAYER)) { return } @@ -42,7 +44,7 @@ object SpawnEggHandler : Listener { // What does the 1.7 do? I don't know, this is from old EcoBosses code. val location = event.block.location.add(facing.direction.multiply(1.7)) - if (!this.handleSpawnEgg(event.item, location, null)) { + if (!this.handleSpawnEgg(event.item, location, null, EcoMobEggUseEvent.Source.DISPENSER)) { return } @@ -65,12 +67,20 @@ object SpawnEggHandler : Listener { private fun handleSpawnEgg( item: ItemStack?, location: Location, - player: Player? + player: Player?, + source: EcoMobEggUseEvent.Source ): Boolean { val mob = item?.ecoMobEgg ?: return false val egg = mob.spawnEgg ?: return false - return egg.trySpawn(location, player) != null + val useEvent = EcoMobEggUseEvent(mob, location, player, source) + Bukkit.getPluginManager().callEvent(useEvent) + + if (useEvent.isCancelled) { + return false + } + + return egg.trySpawn(useEvent.location, player) != null } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnTotemHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnTotemHandler.kt index a3660b86..a33ccbce 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnTotemHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnTotemHandler.kt @@ -1,10 +1,12 @@ package com.willfp.ecomobs.handler +import com.willfp.ecomobs.event.EcoMobTotemBuildEvent import com.willfp.ecomobs.mob.EcoMobs import com.willfp.ecomobs.mob.SpawnReason import com.willfp.ecomobs.mob.SpawnTotem import com.willfp.libreforge.toDispatcher import com.willfp.libreforge.triggers.TriggerData +import org.bukkit.Bukkit import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.block.BlockPlaceEvent @@ -44,7 +46,14 @@ object SpawnTotemHandler : Listener { continue } - mob.spawn(location, SpawnReason.TOTEM) + val buildEvent = EcoMobTotemBuildEvent(mob, totem, location, player) + Bukkit.getPluginManager().callEvent(buildEvent) + + if (buildEvent.isCancelled) { + continue + } + + mob.spawn(buildEvent.location, SpawnReason.TOTEM) } } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt index 47692bc5..8b6b4663 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt @@ -2,6 +2,11 @@ package com.willfp.ecomobs.handler import com.willfp.eco.core.display.Display import com.willfp.eco.core.fast.fast +import com.willfp.ecomobs.event.EcoMobSpawnerBreakEvent +import com.willfp.ecomobs.event.EcoMobSpawnerExplodeEvent +import com.willfp.ecomobs.event.EcoMobSpawnerPickBlockEvent +import com.willfp.ecomobs.event.EcoMobSpawnerPlaceEvent +import com.willfp.ecomobs.event.EcoMobSpawnerUnstackEvent import com.willfp.ecomobs.plugin import com.willfp.ecomobs.spawner.PlacedSpawner import com.willfp.ecomobs.spawner.PlacedSpawners @@ -15,9 +20,11 @@ import com.willfp.ecomobs.spawner.spawnFromSpawner import com.willfp.ecomobs.spawner.spawner import com.willfp.ecomobs.spawner.toSpawnerItem import io.papermc.paper.event.player.PlayerPickItemEvent +import org.bukkit.Bukkit import org.bukkit.GameMode import org.bukkit.Location import org.bukkit.Material +import org.bukkit.block.Block import org.bukkit.block.CreatureSpawner import org.bukkit.enchantments.Enchantment import org.bukkit.entity.Player @@ -55,6 +62,14 @@ object SpawnerHandler : Listener { val mobId = placed.spawner.mob ?: return val animId = placed.spawner.particleAnim + val placeEvent = EcoMobSpawnerPlaceEvent(event.player, location, mobId, placed.spawner.stackSize) + Bukkit.getPluginManager().callEvent(placeEvent) + + if (placeEvent.isCancelled) { + event.isCancelled = true + return + } + plugin.scheduler.at(location).run { val state = location.block.state as? CreatureSpawner ?: return@run @@ -101,7 +116,7 @@ object SpawnerHandler : Listener { val stackSize = if (SpawnerStackSettings.enabled) state.spawner.stackSize else 1 repeat(stackSize) { - spawnFromSpawner(event.location, mobId) + spawnFromSpawner(state.location, event.location, mobId) } } @@ -131,18 +146,51 @@ object SpawnerHandler : Listener { if (SpawnerStackSettings.enabled && stackSize > 1 && !player.isSneaking) { event.isCancelled = true - state.spawner.stackSize = stackSize - 1 + val unstackEvent = EcoMobSpawnerUnstackEvent( + player, + block.location, + state.spawner.mob, + stackSize, + 1, + dropsItem + ) + + Bukkit.getPluginManager().callEvent(unstackEvent) + + if (unstackEvent.isCancelled) { + return + } + + // The block stays put on this path, so the stack always keeps at least one. + val taken = unstackEvent.amount.coerceIn(1, stackSize - 1) + + state.spawner.stackSize = stackSize - taken state.update() - if (dropsItem) { - block.world.dropItemNaturally(block.location, state.toSpawnerItem(1)) + if (unstackEvent.dropsItem) { + block.world.dropItemNaturally(block.location, state.toSpawnerItem(taken)) } SpawnerHolograms.refresh(block.location) return } - if (dropsItem) { + val breakEvent = EcoMobSpawnerBreakEvent( + player, + block.location, + state.spawner.mob, + stackSize, + dropsItem + ) + + Bukkit.getPluginManager().callEvent(breakEvent) + + if (breakEvent.isCancelled) { + event.isCancelled = true + return + } + + if (breakEvent.dropsItem) { event.isDropItems = false block.world.dropItemNaturally(block.location, state.toSpawnerItem()) } @@ -188,6 +236,15 @@ object SpawnerHandler : Listener { val state = target.state as? CreatureSpawner ?: return if (!state.spawner.isCustomSpawner) return val item = state.toSpawnerItem() + + val pickEvent = EcoMobSpawnerPickBlockEvent(player, target.location, state.spawner.mob, item) + Bukkit.getPluginManager().callEvent(pickEvent) + + if (pickEvent.isCancelled) { + event.isCancelled = true + return + } + Display.display(item, player) plugin.scheduler.on(player).run { player.inventory.setItem(player.inventory.heldItemSlot, item) @@ -241,25 +298,39 @@ object SpawnerHandler : Listener { @EventHandler(ignoreCancelled = true) fun handleEntityExplosion(event: EntityExplodeEvent) { - event.blockList().removeIf { block -> - if (block.type != Material.SPAWNER) return@removeIf false - val state = block.state as? CreatureSpawner ?: return@removeIf false - if (state.spawner.isCustomSpawner && state.spawner.explosionProof) return@removeIf true - PlacedSpawners.remove(block.location) - SpawnerHolograms.refresh(block.location) - false - } + event.blockList().removeIf(::survivesExplosion) } @EventHandler(ignoreCancelled = true) fun handleBlockExplosion(event: BlockExplodeEvent) { - event.blockList().removeIf { block -> - if (block.type != Material.SPAWNER) return@removeIf false - val state = block.state as? CreatureSpawner ?: return@removeIf false - if (state.spawner.isCustomSpawner && state.spawner.explosionProof) return@removeIf true - PlacedSpawners.remove(block.location) - SpawnerHolograms.refresh(block.location) - false + event.blockList().removeIf(::survivesExplosion) + } + + /** + * Whether [block] is taken out of the explosion's block list, which is how a spawner + * is left standing. Anything that does go up stops being tracked here. + */ + private fun survivesExplosion(block: Block): Boolean { + if (block.type != Material.SPAWNER) return false + val state = block.state as? CreatureSpawner ?: return false + + if (state.spawner.isCustomSpawner) { + val explodeEvent = EcoMobSpawnerExplodeEvent( + block.location, + state.spawner.mob, + state.spawner.stackSize, + state.spawner.explosionProof + ) + + Bukkit.getPluginManager().callEvent(explodeEvent) + + if (explodeEvent.isProtected) { + return true + } } + + PlacedSpawners.remove(block.location) + SpawnerHolograms.refresh(block.location) + return false } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt index 4875b9d3..5bdd0906 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt @@ -87,7 +87,9 @@ object SpawnerStackHandler : Listener { return } - item.amount -= amount + // Capped, as a listener on EcoMobSpawnerStackEvent can raise the amount taken + // above what the player is actually holding. + item.amount -= amount.coerceAtMost(item.amount) player.inventory.setItem(slot, item.takeIf { it.amount > 0 }) } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt index f0561cd2..71317611 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt @@ -1,11 +1,14 @@ package com.willfp.ecomobs.handler +import com.willfp.ecomobs.event.EcoMobStackDeathEvent +import com.willfp.ecomobs.event.EcoMobStackSplitEvent import com.willfp.ecomobs.mob.SpawnReason import com.willfp.ecomobs.mob.impl.ecoMob import com.willfp.ecomobs.plugin import com.willfp.ecomobs.stacking.MobStacks import com.willfp.ecomobs.stacking.StackSettings import com.willfp.ecomobs.stacking.stack +import org.bukkit.Bukkit import org.bukkit.entity.Mob import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority @@ -38,7 +41,10 @@ object StackHandler : Listener { return } - if (StackSettings.killWholeStack) { + val deathEvent = EcoMobStackDeathEvent(mob, size, StackSettings.killWholeStack) + Bukkit.getPluginManager().callEvent(deathEvent) + + if (deathEvent.killWholeStack) { multiplyRewards(event, mob, size) return } @@ -80,6 +86,19 @@ object StackHandler : Listener { val ecoMob = mob.ecoMob val type = mob.type + val splitEvent = EcoMobStackSplitEvent(mob, location, remaining) + Bukkit.getPluginManager().callEvent(splitEvent) + + if (splitEvent.isCancelled) { + return + } + + val left = splitEvent.remaining + + if (left <= 0) { + return + } + plugin.scheduler.at(location).run { val spawned = if (ecoMob != null) { ecoMob.spawn(location, SpawnReason.NATURAL)?.entity @@ -88,7 +107,7 @@ object StackHandler : Listener { } if (spawned != null) { - spawned.stack.size = remaining + spawned.stack.size = left } } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/event/impl/MobEventKillPlayer.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/event/impl/MobEventKillPlayer.kt index f764dd1a..c2171c5c 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/event/impl/MobEventKillPlayer.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/event/impl/MobEventKillPlayer.kt @@ -1,10 +1,12 @@ package com.willfp.ecomobs.mob.event.impl import com.willfp.eco.core.events.EntityDeathByEntityEvent +import com.willfp.ecomobs.event.EcoMobKillPlayerEvent import com.willfp.ecomobs.mob.event.MobEvent import com.willfp.ecomobs.mob.impl.ecoMob import com.willfp.libreforge.toDispatcher import com.willfp.libreforge.triggers.TriggerData +import org.bukkit.Bukkit import org.bukkit.entity.Mob import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -24,6 +26,8 @@ object MobEventKillPlayer : MobEvent("kill_player") { event = event ) + Bukkit.getPluginManager().callEvent(EcoMobKillPlayerEvent(living, player)) + living.handleEvent(this, data.dispatch(player.toDispatcher())) } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/ConfigDrivenEcoMob.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/ConfigDrivenEcoMob.kt index c7c1779f..b9e05d4b 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/ConfigDrivenEcoMob.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/ConfigDrivenEcoMob.kt @@ -25,6 +25,7 @@ import com.willfp.ecomobs.config.toConfigKey import com.willfp.ecomobs.config.validate import com.willfp.ecomobs.config.validateNotNull import com.willfp.ecomobs.display.BaseItem +import com.willfp.ecomobs.event.EcoMobDropsEvent import com.willfp.ecomobs.event.EcoMobPreSpawnEvent import com.willfp.ecomobs.event.EcoMobSpawnEvent import com.willfp.ecomobs.integrations.MobIntegration @@ -392,7 +393,14 @@ internal class ConfigDrivenEcoMob( } override fun spawnDrops(location: Location, player: Player?) { - drops.drop(location, player) + val dropsEvent = EcoMobDropsEvent(this, location, player, drops.roll(player), drops.experience) + Bukkit.getPluginManager().callEvent(dropsEvent) + + if (dropsEvent.isCancelled) { + return + } + + drops.give(location, player, dropsEvent.drops, dropsEvent.experience) } override fun handleEvent(event: MobEvent, trigger: DispatchedTrigger) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/LivingMobImpl.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/LivingMobImpl.kt index f5126499..dd2cb63d 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/LivingMobImpl.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/impl/LivingMobImpl.kt @@ -4,6 +4,7 @@ import com.willfp.eco.core.scheduling.EcoTask import com.willfp.eco.util.formatEco import com.willfp.eco.util.namespacedKeyOf import com.willfp.ecomobs.event.EcoMobDespawnEvent +import com.willfp.ecomobs.event.EcoMobStageChangeEvent import com.willfp.ecomobs.mob.EcoMob import com.willfp.ecomobs.mob.LivingMob import com.willfp.ecomobs.mob.event.MobEvent @@ -64,7 +65,7 @@ internal class LivingMobImpl( get() = mob.lifespan - tick internal val stageTracker = if (mob.usesDamageStages) { - DamageStageTracker(mob.damageStages, ::triggerStageEffects) + DamageStageTracker(mob.damageStages, ::triggerStageEffects, ::fireStageChange) } else { null } @@ -177,6 +178,17 @@ internal class LivingMobImpl( } } + private fun fireStageChange( + previousStage: DamageStage, + stage: DamageStage?, + stageNumber: Int, + player: Player? + ) { + Bukkit.getPluginManager().callEvent( + EcoMobStageChangeEvent(this, previousStage, stage, stageNumber, player) + ) + } + private fun triggerStageEffects(effects: Chain, player: Player?) { val trigger = TriggerData( player = player, diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/options/MobDrops.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/options/MobDrops.kt index 841c2bfd..7ac6b1a8 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/options/MobDrops.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/options/MobDrops.kt @@ -6,6 +6,7 @@ import com.willfp.eco.util.randDouble import org.bukkit.Location import org.bukkit.entity.ExperienceOrb import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack data class Drop( val chance: Double, @@ -16,30 +17,53 @@ data class MobDrops( val experience: Int, val drops: List ) { - fun drop(location: Location, player: Player?) { - if (player != null) { - val queue = DropQueue(player) - .addXP(experience) + /** + * Rolls the drop table, without giving anything out. Split from [give] so the drops + * can be shown to [com.willfp.ecomobs.event.EcoMobDropsEvent] before they land. + * + * A drop that hits its chance gives every one of its items to a player, and one of + * them at random when there is nobody to credit. + */ + fun roll(player: Player?): MutableList { + val rolled = mutableListOf() - for (drop in drops) { - if (randDouble(0.0, 100.0) <= drop.chance) { - queue.addItems(drop.items.map { it.item }) - } + for (drop in drops) { + if (randDouble(0.0, 100.0) > drop.chance) { + continue } - queue.push() - } else { - val world = location.world ?: throw IllegalStateException("Location has no world") - - for (drop in drops) { - if (randDouble(0.0, 100.0) <= drop.chance) { - world.dropItemNaturally(location, drop.items.random().item) - } + if (player != null) { + rolled += drop.items.map { it.item } + } else { + rolled += drop.items.random().item } + } - world.spawn(location, ExperienceOrb::class.java).apply { - experience = experience - } + return rolled + } + + /** + * Gives out [items] and [xp], to [player] where there is one and on the ground at + * [location] otherwise. + */ + fun give(location: Location, player: Player?, items: List, xp: Int) { + if (player != null) { + DropQueue(player) + .addXP(xp) + .addItems(items) + .push() + + return + } + + val world = location.world ?: throw IllegalStateException("Location has no world") + + for (item in items) { + world.dropItemNaturally(location, item) + } + + world.spawn(location, ExperienceOrb::class.java).apply { + experience = xp } } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/stage/DamageStageTracker.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/stage/DamageStageTracker.kt index 224342a2..74f2d179 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/stage/DamageStageTracker.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/mob/stage/DamageStageTracker.kt @@ -5,7 +5,12 @@ import org.bukkit.entity.Player internal class DamageStageTracker( private val stages: List, - private val triggerEffects: (Chain, Player?) -> Unit + private val triggerEffects: (Chain, Player?) -> Unit, + /** + * Called with the stage that finished, the one now in progress (null once they have + * all been used up), its 1-based position, and the player who ended the last one. + */ + private val onStageChange: (DamageStage, DamageStage?, Int, Player?) -> Unit ) { var index = 0 private set @@ -52,17 +57,20 @@ internal class DamageStageTracker( } // Overflow is discarded so a stage never starts partly drained. - stages[index].endEffects?.let { triggerEffects(it, player) } + val finished = stages[index] + finished.endEffects?.let { triggerEffects(it, player) } index++ if (isFinished) { // Floored, so the overshoot from a large count can't be read back as a // negative amount remaining. remaining = 0.0 + onStageChange(finished, null, index + 1, player) return true } remaining = stages[index].amount + onStageChange(finished, stages[index], index + 1, player) stages[index].startEffects?.let { triggerEffects(it, player) } return false } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt index 1081e104..9fde81ee 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt @@ -1,7 +1,9 @@ package com.willfp.ecomobs.spawner +import com.willfp.ecomobs.event.EcoMobSpawnerTickEvent import com.willfp.ecomobs.mob.EcoMobs import com.willfp.ecomobs.mob.impl.ecoMob +import org.bukkit.Bukkit import org.bukkit.Location import org.bukkit.block.CreatureSpawner import org.bukkit.entity.Mob @@ -88,8 +90,17 @@ class PlacedSpawner( // A stack of spawners spawns as many mobs as it holds, on the one cycle. val stackSize = if (SpawnerStackSettings.enabled) state.spawner.stackSize else 1 - repeat(state.spawnCount * stackSize) { - spawnFromSpawner(randomSpawnLocation(state.spawnRange), mobId) + val tickEvent = EcoMobSpawnerTickEvent(location, mobId, state.spawnCount, stackSize) + Bukkit.getPluginManager().callEvent(tickEvent) + + if (tickEvent.isCancelled) { + return + } + + val toSpawn = tickEvent.spawnCount.coerceAtLeast(0) * tickEvent.stackSize.coerceAtLeast(0) + + repeat(toSpawn) { + spawnFromSpawner(location, randomSpawnLocation(state.spawnRange), mobId) } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt index 2dccac29..79ff0c7c 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt @@ -1,18 +1,28 @@ package com.willfp.ecomobs.spawner import com.willfp.eco.core.entities.Entities +import com.willfp.ecomobs.event.EcoMobSpawnerSpawnEvent import com.willfp.ecomobs.mob.EcoMobs import com.willfp.ecomobs.mob.SpawnReason +import org.bukkit.Bukkit import org.bukkit.Location import org.bukkit.persistence.PersistentDataType /** - * Spawns [mobId] at [location] as a spawner would, marking the entity so the rest of - * the plugin can tell it came from a spawner. + * Spawns [mobId] at [location] as the spawner at [spawnerLocation] would, marking the + * entity so the rest of the plugin can tell it came from a spawner. * - * Shared by both spawner modes so they can't drift apart. + * Shared by both spawner modes so they can't drift apart, which also makes it the one + * place [EcoMobSpawnerSpawnEvent] has to be called from. */ -fun spawnFromSpawner(location: Location, mobId: String) { +fun spawnFromSpawner(spawnerLocation: Location, location: Location, mobId: String) { + val spawnEvent = EcoMobSpawnerSpawnEvent(spawnerLocation, mobId, location) + Bukkit.getPluginManager().callEvent(spawnEvent) + + if (spawnEvent.isCancelled) { + return + } + val ecoMob = EcoMobs[mobId] val entity = if (ecoMob != null) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt index 94199f5a..94fac5ac 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt @@ -1,6 +1,8 @@ package com.willfp.ecomobs.spawner import com.willfp.eco.core.fast.FastItemStack +import com.willfp.ecomobs.event.EcoMobSpawnerStackEvent +import org.bukkit.Bukkit import org.bukkit.block.CreatureSpawner object SpawnerStacks { @@ -28,7 +30,25 @@ object SpawnerStacks { return 0 } - val taken = minOf(amount, space) + val stackEvent = EcoMobSpawnerStackEvent( + state.location, + state.spawner.mob, + current, + minOf(amount, space) + ) + + Bukkit.getPluginManager().callEvent(stackEvent) + + if (stackEvent.isCancelled) { + return 0 + } + + // Capped again, as a listener is free to raise the amount past what fits. + val taken = stackEvent.amount.coerceIn(0, space) + + if (taken <= 0) { + return 0 + } state.spawner.stackSize = current + taken state.update() diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/MobStacks.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/MobStacks.kt index ab7c7801..eefd35ef 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/MobStacks.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/MobStacks.kt @@ -1,6 +1,8 @@ package com.willfp.ecomobs.stacking +import com.willfp.ecomobs.event.EcoMobStackMergeEvent import com.willfp.ecomobs.mob.impl.ecoMob +import org.bukkit.Bukkit import org.bukkit.entity.Ageable import org.bukkit.entity.Mob import org.bukkit.entity.Tameable @@ -104,6 +106,13 @@ object MobStacks { .minByOrNull { it.location.distanceSquared(mob.location) } ?: return false + val mergeEvent = EcoMobStackMergeEvent(mob, target, target.stack.size + size) + Bukkit.getPluginManager().callEvent(mergeEvent) + + if (mergeEvent.isCancelled) { + return false + } + target.stack.size += size remove(mob) diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/tick/TickHandlerLifespan.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/tick/TickHandlerLifespan.kt index c9ae30c6..99f3cada 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/tick/TickHandlerLifespan.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/tick/TickHandlerLifespan.kt @@ -1,11 +1,22 @@ package com.willfp.ecomobs.tick +import com.willfp.ecomobs.event.EcoMobLifespanExpireEvent import com.willfp.ecomobs.mob.LivingMob +import org.bukkit.Bukkit class TickHandlerLifespan: TickHandler { override fun tick(mob: LivingMob, tick: Int) { - if (mob.ticksLeft <= 0) { - mob.despawn() + if (mob.ticksLeft > 0) { + return } + + val event = EcoMobLifespanExpireEvent(mob) + Bukkit.getPluginManager().callEvent(event) + + if (event.isCancelled) { + return + } + + mob.despawn() } } From 13ac267defffc242bc074a9ed0f3aa2fb519b721 Mon Sep 17 00:00:00 2001 From: Callum Johnson <74352969+CallumJohnson@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:38:45 +0100 Subject: [PATCH 2/2] feat: Libreforge triggers (#100) * feat: Libreforge triggers * feat: Small Changes (#101) * feat: `no-ai` flag on Spawner mobs * chore: Stack size perm registration * feat: `PlacedSpawners` mirror block data * feat: Max stack size lore line * feat: Place all on Sneak * feat: Toggleable death anims * feat: CoreProtect hook --- build.gradle.kts | 1 + eco-core/core-plugin/build.gradle.kts | 1 + .../com/willfp/ecomobs/EcoMobsPlugin.kt | 4 + .../ecomobs/commands/CommandSpawnerModify.kt | 3 +- .../ecomobs/display/SpawnerItemDisplay.kt | 3 + .../ecomobs/event/EcoMobSpawnerStackEvent.kt | 5 + .../willfp/ecomobs/handler/SpawnerHandler.kt | 78 +++++++- .../ecomobs/handler/SpawnerStackHandler.kt | 2 +- .../willfp/ecomobs/handler/StackHandler.kt | 20 ++- .../coreprotect/IntegrationCoreProtect.kt | 168 ++++++++++++++++++ .../willfp/ecomobs/spawner/PlacedSpawner.kt | 22 ++- .../willfp/ecomobs/spawner/PlacedSpawners.kt | 8 + .../ecomobs/spawner/SpawnerAttributes.kt | 9 +- .../ecomobs/spawner/SpawnerHolograms.kt | 19 +- .../com/willfp/ecomobs/spawner/SpawnerKeys.kt | 21 ++- .../willfp/ecomobs/spawner/SpawnerSpawning.kt | 14 +- .../willfp/ecomobs/spawner/SpawnerStacks.kt | 9 +- .../willfp/ecomobs/stacking/StackSettings.kt | 4 + .../willfp/ecomobs/trigger/EcoMobsTriggers.kt | 27 +++ .../ecomobs/trigger/TriggerBreakSpawner.kt | 53 ++++++ .../ecomobs/trigger/TriggerBuildTotem.kt | 42 +++++ .../ecomobs/trigger/TriggerDamageMob.kt | 53 ++++++ .../willfp/ecomobs/trigger/TriggerMobDrops.kt | 57 ++++++ .../ecomobs/trigger/TriggerPlaceSpawner.kt | 48 +++++ .../ecomobs/trigger/TriggerSpawnerSpawn.kt | 45 +++++ .../ecomobs/trigger/TriggerStackDeath.kt | 55 ++++++ .../ecomobs/trigger/TriggerStackMerge.kt | 51 ++++++ .../ecomobs/trigger/TriggerStackSpawner.kt | 51 ++++++ .../ecomobs/trigger/TriggerStageChange.kt | 54 ++++++ .../ecomobs/trigger/TriggerUnstackSpawner.kt | 56 ++++++ .../ecomobs/trigger/TriggerUseSpawnEgg.kt | 47 +++++ .../core-plugin/src/main/resources/config.yml | 8 +- .../core-plugin/src/main/resources/plugin.yml | 11 ++ ecohub/ecomobs/commands-and-permissions.md | 1 + ecohub/ecomobs/mob-stacking.md | 11 ++ ecohub/ecomobs/plugin-config.md | 2 + 36 files changed, 1030 insertions(+), 33 deletions(-) create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/integrations/coreprotect/IntegrationCoreProtect.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/EcoMobsTriggers.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBreakSpawner.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBuildTotem.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerDamageMob.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerMobDrops.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerPlaceSpawner.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerSpawnerSpawn.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackDeath.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackMerge.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackSpawner.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStageChange.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUnstackSpawner.kt create mode 100644 eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUseSpawnEgg.kt diff --git a/build.gradle.kts b/build.gradle.kts index f85466f0..c9f4ad1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,6 +96,7 @@ allprojects { maven("https://repo.auxilor.io/repository/maven-public/") maven("https://mvn.lib.co.nz/public") maven("https://mvn.lumine.io/repository/maven-public/") + maven("https://maven.playpro.com") } dependencies { diff --git a/eco-core/core-plugin/build.gradle.kts b/eco-core/core-plugin/build.gradle.kts index f3bae23b..f0d2ad6a 100644 --- a/eco-core/core-plugin/build.gradle.kts +++ b/eco-core/core-plugin/build.gradle.kts @@ -5,6 +5,7 @@ dependencies { compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT") compileOnly("io.github.arcaneplugins:levelledmobs-plugin:4.0.2") compileOnly("me.libraryaddict.disguises:libsdisguises:11.0.14") + compileOnly("net.coreprotect:coreprotect:24.0") implementation("com.willfp:ModelEngineBridge:1.4.0") } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/EcoMobsPlugin.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/EcoMobsPlugin.kt index 2ef4b26c..31df9d1a 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/EcoMobsPlugin.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/EcoMobsPlugin.kt @@ -24,6 +24,7 @@ import com.willfp.ecomobs.handler.SpawnerStackHandler import com.willfp.ecomobs.handler.StackHandler import com.willfp.ecomobs.handler.VanillaCompatibilityHandlers import com.willfp.ecomobs.integrations.bettermodel.IntegrationBetterModel +import com.willfp.ecomobs.integrations.coreprotect.IntegrationCoreProtect import com.willfp.ecomobs.integrations.levelledmobs.IntegrationLevelledMobs import com.willfp.ecomobs.integrations.libsdisguises.IntegrationLibsDisguises import com.willfp.ecomobs.integrations.modelengine.IntegrationModelEngine @@ -40,6 +41,7 @@ import com.willfp.ecomobs.spawner.SpawnerSettings import com.willfp.ecomobs.spawner.SpawnerSpawnLoop import com.willfp.ecomobs.stacking.MobStackTicker import com.willfp.ecomobs.stacking.StackSettings +import com.willfp.ecomobs.trigger.EcoMobsTriggers import com.willfp.ecomobs.spawner.particle.SpawnerParticleAnimations import com.willfp.libreforge.EntityProvidedHolder import com.willfp.libreforge.loader.LibreforgePlugin @@ -70,6 +72,7 @@ class EcoMobsPlugin : LibreforgePlugin() { override fun handleLoad() { EntityGoals.register(EntityGoalRandomTeleport.Deserializer) Items.registerItemProvider(SpawnerItems) + EcoMobsTriggers.registerAll() } override fun loadConfigCategories(): List { @@ -128,6 +131,7 @@ class EcoMobsPlugin : LibreforgePlugin() { IntegrationLoader("ModelEngine") { this.eventManager.registerListener(IntegrationModelEngine) }, IntegrationLoader("BetterModel") { this.eventManager.registerListener(IntegrationBetterModel) }, IntegrationLoader("LibsDisguises") { this.eventManager.registerListener(IntegrationLibsDisguises) }, + IntegrationLoader("CoreProtect") { this.eventManager.registerListener(IntegrationCoreProtect) }, ) } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/commands/CommandSpawnerModify.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/commands/CommandSpawnerModify.kt index 6fe0d762..610ad86d 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/commands/CommandSpawnerModify.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/commands/CommandSpawnerModify.kt @@ -3,7 +3,6 @@ package com.willfp.ecomobs.commands import com.willfp.eco.core.command.impl.Subcommand import com.willfp.eco.core.fast.fast import com.willfp.ecomobs.plugin -import com.willfp.ecomobs.spawner.PlacedSpawner import com.willfp.ecomobs.spawner.PlacedSpawners import com.willfp.ecomobs.spawner.SpawnerAttributes import com.willfp.ecomobs.spawner.applyVanillaSettings @@ -80,7 +79,7 @@ object CommandSpawnerModify : Subcommand( } state.update() - PlacedSpawners.set(block.location, PlacedSpawner(block.location, state.spawner.particleAnim)) + PlacedSpawners.sync(state) } sender.sendMessage( diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/display/SpawnerItemDisplay.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/display/SpawnerItemDisplay.kt index d24d7841..bf334d05 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/display/SpawnerItemDisplay.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/display/SpawnerItemDisplay.kt @@ -9,6 +9,7 @@ import com.willfp.eco.core.placeholder.context.placeholderContext import com.willfp.eco.util.formatEco import com.willfp.eco.util.titlecase import com.willfp.ecomobs.plugin +import com.willfp.ecomobs.spawner.SpawnerStackSettings import com.willfp.ecomobs.spawner.spawner import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack @@ -28,7 +29,9 @@ private fun FastItemStack.applySpawnerPlaceholders(text: String): String { .replace("%pickup%", data.pickup) .replace("%particle%", data.particleAnim ?: "none") .replace("%explosion_proof%", data.explosionProof.toString()) + .replace("%no_ai%", data.noAI.toString()) .replace("%size%", data.stackSize.toString()) + .replace("%max_stack_size%", SpawnerStackSettings.maxSize.toString()) } object SpawnerItemDisplay : DisplayModule(plugin, DisplayPriority.LOW) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt index 50fc6dd7..b396e436 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/event/EcoMobSpawnerStackEvent.kt @@ -1,6 +1,7 @@ package com.willfp.ecomobs.event import org.bukkit.Location +import org.bukkit.entity.Player import org.bukkit.event.Cancellable import org.bukkit.event.Event import org.bukkit.event.HandlerList @@ -12,6 +13,10 @@ import org.bukkit.event.HandlerList class EcoMobSpawnerStackEvent( override val location: Location, override val mobId: String?, + /** + * The player stacking the spawners, or null if something else did it. + */ + val player: Player?, /** * The stack size before the merge. */ diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt index 7c300fae..14df4d2f 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerHandler.kt @@ -8,7 +8,6 @@ import com.willfp.ecomobs.event.EcoMobSpawnerPickBlockEvent import com.willfp.ecomobs.event.EcoMobSpawnerPlaceEvent import com.willfp.ecomobs.event.EcoMobSpawnerUnstackEvent import com.willfp.ecomobs.plugin -import com.willfp.ecomobs.spawner.PlacedSpawner import com.willfp.ecomobs.spawner.PlacedSpawners import com.willfp.ecomobs.spawner.SpawnerHolograms import com.willfp.ecomobs.spawner.SpawnerStackSettings @@ -18,6 +17,7 @@ import com.willfp.ecomobs.spawner.isTrackedByEcoMobs import com.willfp.ecomobs.spawner.resolveEntityType import com.willfp.ecomobs.spawner.spawnFromSpawner import com.willfp.ecomobs.spawner.spawner +import com.willfp.ecomobs.spawner.toPlacedSpawner import com.willfp.ecomobs.spawner.toSpawnerItem import io.papermc.paper.event.player.PlayerPickItemEvent import org.bukkit.Bukkit @@ -28,6 +28,7 @@ import org.bukkit.Material import org.bukkit.block.Block import org.bukkit.block.CreatureSpawner import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.Listener @@ -53,7 +54,7 @@ object SpawnerHandler : Listener { plugin.scheduler.at(location).run { val state = location.block.state as? CreatureSpawner ?: return@run if (state.isTrackedByEcoMobs) { - PlacedSpawners.set(location, PlacedSpawner(location, null)) + PlacedSpawners.sync(state) SpawnerHolograms.refresh(location) } } @@ -61,9 +62,20 @@ object SpawnerHandler : Listener { } val mobId = placed.spawner.mob ?: return - val animId = placed.spawner.particleAnim - val placeEvent = EcoMobSpawnerPlaceEvent(event.player, location, mobId, placed.spawner.stackSize) + val perItem = placed.spawner.stackSize.coerceAtLeast(1) + + // Sneaking places the whole held stack at once, mirroring sneak-break taking it all. + val extraItems = if (SpawnerStackSettings.enabled && event.player.isSneaking) { + val maxExtra = (SpawnerStackSettings.maxSize / perItem - 1).coerceAtLeast(0) + (event.itemInHand.amount - 1).coerceIn(0, maxExtra) + } else { + 0 + } + + val stackSize = perItem * (extraItems + 1) + + val placeEvent = EcoMobSpawnerPlaceEvent(event.player, location, mobId, stackSize) Bukkit.getPluginManager().callEvent(placeEvent) if (placeEvent.isCancelled) { @@ -75,13 +87,48 @@ object SpawnerHandler : Listener { val state = location.block.state as? CreatureSpawner ?: return@run placed.spawner.copyTo(state.spawner) + state.spawner.stackSize = stackSize state.applyVanillaSettings() resolveEntityType(mobId)?.let { state.spawnedType = it } state.update() - PlacedSpawners.set(location, PlacedSpawner(location, animId)) + PlacedSpawners.sync(state) SpawnerHolograms.refresh(location) } + + if (extraItems > 0) { + consumeExtra(event, extraItems) + } + } + + /** + * Takes the [extra] spawner items beyond the one vanilla removes for the placement. + * + * Deferred a tick, as a later handler (stacking onto the spawner placed against) can + * still cancel the placement, in which case nothing extra is taken. + */ + private fun consumeExtra(event: BlockPlaceEvent, extra: Int) { + val player = event.player + + if (player.gameMode == GameMode.CREATIVE) { + return + } + + plugin.scheduler.on(player).run { + if (event.isCancelled) { + return@run + } + + val item = player.inventory.getItem(event.hand) ?: return@run + + if (item.type != Material.SPAWNER) { + return@run + } + + item.amount -= extra.coerceAtMost(item.amount) + + player.inventory.setItem(event.hand, item.takeIf { it.amount > 0 }) + } } @EventHandler(ignoreCancelled = true) @@ -95,7 +142,7 @@ object SpawnerHandler : Listener { val added = PlacedSpawners.setIfAbsent( spawnerLocation, - PlacedSpawner(spawnerLocation, state.spawner.particleAnim) + state.toPlacedSpawner() ) if (added) { @@ -109,7 +156,18 @@ object SpawnerHandler : Listener { return } - val mobId = state.spawner.mob ?: return + val noAI = state.spawner.noAI + val mobId = state.spawner.mob + + // Vanilla spawns the entity itself, so a spawner with no custom mob still has + // its AI stripped here, before the mob is added to the world. + if (mobId == null) { + if (noAI) { + (event.entity as? LivingEntity)?.setAI(false) + } + + return + } event.isCancelled = true @@ -117,7 +175,7 @@ object SpawnerHandler : Listener { val stackSize = if (SpawnerStackSettings.enabled) state.spawner.stackSize else 1 repeat(stackSize) { - spawnFromSpawner(state.location, event.location, mobId) + spawnFromSpawner(state.location, event.location, mobId, noAI) } } @@ -168,6 +226,8 @@ object SpawnerHandler : Listener { state.spawner.stackSize = stackSize - taken state.update() + PlacedSpawners.sync(state) + if (unstackEvent.dropsItem) { block.world.dropItemNaturally(block.location, state.toSpawnerItem(taken)) } @@ -274,7 +334,7 @@ object SpawnerHandler : Listener { PlacedSpawners.set( blockState.location, - PlacedSpawner(blockState.location, blockState.spawner.particleAnim) + blockState.toPlacedSpawner() ) loaded += blockState.location diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt index 5bdd0906..c211e360 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/SpawnerStackHandler.kt @@ -73,7 +73,7 @@ object SpawnerStackHandler : Listener { val amount = if (player.isSneaking) held.amount else 1 - return SpawnerStacks.add(state, item, amount) + return SpawnerStacks.add(state, item, amount, player) } private fun consume(player: Player, slot: EquipmentSlot, amount: Int) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt index 71317611..07eaf4bb 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/handler/StackHandler.kt @@ -46,10 +46,26 @@ object StackHandler : Listener { if (deathEvent.killWholeStack) { multiplyRewards(event, mob, size) - return + } else { + respawnRemainder(mob, size - 1) + } + + if (StackSettings.hideDeathAnimation) { + hideCorpse(mob) } + } - respawnRemainder(mob, size - 1) + /** + * Takes the corpse away a tick after the kill, so the twenty-tick death animation + * never plays out next to what's left of the stack. + * + * Deferred rather than removed here, as taking the entity out during its own death + * event loses the XP it was about to drop. + */ + private fun hideCorpse(mob: Mob) { + plugin.scheduler.at(mob.location).run { + mob.remove() + } } private fun multiplyRewards(event: EntityDeathEvent, mob: Mob, size: Int) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/integrations/coreprotect/IntegrationCoreProtect.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/integrations/coreprotect/IntegrationCoreProtect.kt new file mode 100644 index 00000000..b4391a8d --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/integrations/coreprotect/IntegrationCoreProtect.kt @@ -0,0 +1,168 @@ +package com.willfp.ecomobs.integrations.coreprotect + +import com.willfp.eco.core.integrations.Integration +import com.willfp.ecomobs.event.EcoMobSpawnerBreakEvent +import com.willfp.ecomobs.event.EcoMobSpawnerExplodeEvent +import com.willfp.ecomobs.event.EcoMobSpawnerPlaceEvent +import com.willfp.ecomobs.event.EcoMobSpawnerStackEvent +import com.willfp.ecomobs.event.EcoMobSpawnerUnstackEvent +import com.willfp.ecomobs.plugin +import com.willfp.ecomobs.spawner.SpawnerStackSettings +import com.willfp.ecomobs.spawner.spawner +import net.coreprotect.CoreProtect +import net.coreprotect.CoreProtectAPI +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.block.CreatureSpawner +import org.bukkit.block.data.BlockData +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener + +/** + * Logs custom spawner changes to CoreProtect. + * + * CoreProtect has no notion of a stacked block: a row is always one block. A stack of + * eight is therefore logged as eight rows, so that a lookup shows the real count and + * rolling the whole thing back leaves nothing standing. + * + * CoreProtect logs the block change itself for a place, a full break and an explosion, + * so only the rest of the stack is logged here. Stacking and unstacking change no block + * at all - unstacking cancels the break - so those are logged in full. + */ +object IntegrationCoreProtect : Listener, Integration { + /** + * Logged for a spawner that goes up in an explosion, where there is no player. + * CoreProtect shows a name starting with # as a non-player source. + */ + private const val NON_PLAYER_USER = "#ecomobs" + + /** + * Only ever used to tell CoreProtect what the block was. A spawner has no block + * state worth recording, so one instance serves every row. + */ + private val spawnerData: BlockData by lazy { Material.SPAWNER.createBlockData() } + + private val coreProtect: CoreProtect? by lazy { + Bukkit.getPluginManager().getPlugin("CoreProtect") as? CoreProtect + } + + /** + * Null when CoreProtect's database failed to come up, in which case it drops + * everything logged to it anyway. + */ + private val api: CoreProtectAPI? + get() = coreProtect?.api?.takeIf { it.isEnabled } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun handlePlace(event: EcoMobSpawnerPlaceEvent) { + val rest = event.stackSize - 1 + if (rest <= 0) return + + val user = event.player.name + val location = event.location + + // A later listener can still cancel the block placement and stack the spawner + // onto the one it was placed against, so the world gets the final say. + afterTick(location) { + if (!isCustomSpawner(location)) { + return@afterTick + } + + logPlacements(user, location, rest) + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun handleBreak(event: EcoMobSpawnerBreakEvent) { + val rest = event.stackSize - 1 + if (rest <= 0) return + + val user = event.player.name + val location = event.location + + // Another plugin can still cancel the block break, leaving the stack standing. + afterTick(location) { + if (isCustomSpawner(location)) { + return@afterTick + } + + logRemovals(user, location, rest) + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun handleStack(event: EcoMobSpawnerStackEvent) { + // Mirrors the cap SpawnerStacks applies after the event, as a listener is free + // to raise the amount past what fits. + val space = SpawnerStackSettings.maxSize - event.currentSize + val taken = event.amount.coerceIn(0, space.coerceAtLeast(0)) + + if (taken <= 0) return + + logPlacements(event.player?.name ?: NON_PLAYER_USER, event.location, taken) + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + fun handleUnstack(event: EcoMobSpawnerUnstackEvent) { + if (event.currentSize <= 1) return + + // Mirrors the cap SpawnerHandler applies after the event: the block stays put + // on this path, so the stack always keeps at least one. + val taken = event.amount.coerceIn(1, event.currentSize - 1) + + logRemovals(event.player.name, event.location, taken) + } + + @EventHandler(priority = EventPriority.MONITOR) + fun handleExplode(event: EcoMobSpawnerExplodeEvent) { + if (event.isProtected) return + + val rest = event.stackSize - 1 + if (rest <= 0) return + + val location = event.location + + // The explosion itself has not happened yet, and can still be cancelled. + afterTick(location) { + if (isCustomSpawner(location)) { + return@afterTick + } + + logRemovals(NON_PLAYER_USER, location, rest) + } + } + + private fun logPlacements(user: String, location: Location, amount: Int) { + val api = api ?: return + + repeat(amount) { + api.logPlacement(user, location, Material.SPAWNER, spawnerData) + } + } + + private fun logRemovals(user: String, location: Location, amount: Int) { + val api = api ?: return + + repeat(amount) { + api.logRemoval(user, location, Material.SPAWNER, spawnerData) + } + } + + /** + * Runs [block] on the next tick, on the region owning [location]. + */ + private inline fun afterTick(location: Location, crossinline block: () -> Unit) { + plugin.scheduler.at(location).runLater(1) { block() } + } + + private fun isCustomSpawner(location: Location): Boolean { + val state = location.block.state as? CreatureSpawner ?: return false + return state.spawner.isCustomSpawner + } + + override fun getPluginName(): String { + return "CoreProtect" + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt index 9fde81ee..e78f6a6f 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawner.kt @@ -9,9 +9,26 @@ import org.bukkit.block.CreatureSpawner import org.bukkit.entity.Mob import kotlin.random.Random +/** + * A spawner EcoMobs tracks. + * + * Everything needed to describe the spawner is mirrored here, so anything that only + * has to name it - holograms, the API - can do that without loading its chunk to read + * the block state back. + */ class PlacedSpawner( val location: Location, - val animationId: String? + val animationId: String?, + + /** + * The mob the spawner spawns, mirrored from its block state. + */ + val mobId: String? = null, + + /** + * How many spawners it stands in for, mirrored from its block state. + */ + val stackSize: Int = 1 ) { /** * Ticks left until the next spawn attempt. Only used in [SpawnerMode.ECOMOBS]. @@ -89,6 +106,7 @@ class PlacedSpawner( // A stack of spawners spawns as many mobs as it holds, on the one cycle. val stackSize = if (SpawnerStackSettings.enabled) state.spawner.stackSize else 1 + val noAI = state.spawner.noAI val tickEvent = EcoMobSpawnerTickEvent(location, mobId, state.spawnCount, stackSize) Bukkit.getPluginManager().callEvent(tickEvent) @@ -100,7 +118,7 @@ class PlacedSpawner( val toSpawn = tickEvent.spawnCount.coerceAtLeast(0) * tickEvent.stackSize.coerceAtLeast(0) repeat(toSpawn) { - spawnFromSpawner(location, randomSpawnLocation(state.spawnRange), mobId) + spawnFromSpawner(location, randomSpawnLocation(state.spawnRange), mobId, noAI) } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawners.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawners.kt index 10a070eb..eb456a9a 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawners.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/PlacedSpawners.kt @@ -4,6 +4,7 @@ import org.bukkit.Bukkit import org.bukkit.Chunk import org.bukkit.Location import org.bukkit.World +import org.bukkit.block.CreatureSpawner import java.util.Collections import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -73,6 +74,13 @@ object PlacedSpawners { return Collections.unmodifiableList(locations.mapNotNull { loaded[it] }) } + /** + * Re-reads [state] into the index, for after its data has been written. + */ + fun sync(state: CreatureSpawner) { + set(state.location, state.toPlacedSpawner()) + } + fun contains(location: Location): Boolean = loaded.containsKey(location) fun remove(location: Location) { diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerAttributes.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerAttributes.kt index 5fb4be6f..78b362b9 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerAttributes.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerAttributes.kt @@ -10,7 +10,7 @@ val PICKUP_VALUES = listOf("allow", "silk_touch", "deny") * spawner items and to placed spawner blocks. */ object SpawnerAttributes { - val ATTRIBUTES = listOf("mob", "delay", "radius", "player-radius", "count", "max-nearby", "pickup", "particle", "explosion-proof", "stack-size") + val ATTRIBUTES = listOf("mob", "delay", "radius", "player-radius", "count", "max-nearby", "pickup", "particle", "explosion-proof", "no-ai", "stack-size") fun valueCount(attribute: String): Int = if (attribute == "delay") 2 else 1 @@ -63,6 +63,11 @@ object SpawnerAttributes { if (value != "true" && value != "false") return false data.explosionProof = value == "true" } + "no-ai" -> { + val value = args.getOrNull(0)?.lowercase() ?: return false + if (value != "true" && value != "false") return false + data.noAI = value == "true" + } "stack-size" -> { val value = args.getOrNull(0)?.toIntOrNull()?.takeIf { it >= 1 } ?: return false data.stackSize = value @@ -77,7 +82,7 @@ object SpawnerAttributes { "delay" -> if (valueIndex == 0) listOf("200", "400", "800") else listOf("400", "800", "1600") "pickup" -> PICKUP_VALUES "particle" -> listOf("none") + SpawnerAnimations.keys() - "explosion-proof" -> listOf("true", "false") + "explosion-proof", "no-ai" -> listOf("true", "false") "stack-size" -> listOf("1", "8", "16", "64") else -> listOf("1", "4", "8", "16") } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerHolograms.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerHolograms.kt index c2e13c57..ef240723 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerHolograms.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerHolograms.kt @@ -9,7 +9,6 @@ import com.willfp.ecomobs.folia.atRegion import org.bukkit.Chunk import org.bukkit.Location import org.bukkit.World -import org.bukkit.block.CreatureSpawner import org.bukkit.entity.Player import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -161,8 +160,8 @@ object SpawnerHolograms { return } - // The column is tracked even while its chunk is unloaded, and reading its block - // states to build the lines would drag the chunk back in. + // The hologram is a real entity, so it can only exist while the chunk it sits + // in is loaded. The lines themselves come from the index, not the block states. if (!top.isWorldLoaded || !top.isChunkLoaded) { return } @@ -195,15 +194,12 @@ object SpawnerHolograms { var y = top.blockY while (isTracked(world, x, y, z)) { - val state = blockLocation(world, x, y, z).block.state as? CreatureSpawner + val spawner = PlacedSpawners[blockLocation(world, x, y, z)] + val mob = spawner?.mobId - if (state != null) { - val mob = state.spawner.mob - - if (mob != null) { - sizes[mob] = (sizes[mob] ?: 0) + state.spawner.stackSize - members++ - } + if (mob != null) { + sizes[mob] = (sizes[mob] ?: 0) + spawner.stackSize + members++ } y-- @@ -222,6 +218,7 @@ object SpawnerHolograms { .map { (mob, size) -> SpawnerStackSettings.hologramLine .replace("%size%", size.toString()) + .replace("%max_stack_size%", SpawnerStackSettings.maxSize.toString()) .replace("%mob%", mob) .replace("%mob_formatted%", mob.replace("_", " ").titlecase()) .formatEco() diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerKeys.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerKeys.kt index 700a92a7..2b5372a2 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerKeys.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerKeys.kt @@ -25,6 +25,7 @@ val spawnerPickupKey = namespacedKeyOf("ecomobs", "spawner_pickup") val spawnerParticleAnimKey = namespacedKeyOf("ecomobs", "spawner_particle_anim") val spawnerExplosionProofKey = namespacedKeyOf("ecomobs", "spawner_explosion_proof") val spawnerStackSizeKey = namespacedKeyOf("ecomobs", "spawner_stack_size") +val spawnerNoAIKey = namespacedKeyOf("ecomobs", "spawner_no_ai") /** * The values a spawner falls back to when its data has never been written. @@ -104,6 +105,15 @@ value class SpawnerData(val pdc: PersistentDataContainer) { pdc.set(spawnerExplosionProofKey, PersistentDataType.BYTE, if (value) 1 else 0) } + /** + * Whether the mobs this spawner spawns have their AI stripped. + */ + var noAI: Boolean + get() = pdc.get(spawnerNoAIKey, PersistentDataType.BYTE) == 1.toByte() + set(value) { + pdc.set(spawnerNoAIKey, PersistentDataType.BYTE, if (value) 1 else 0) + } + /** * How many spawners this one stands in for. */ @@ -131,7 +141,8 @@ value class SpawnerData(val pdc: PersistentDataContainer) { maxNearby == other.maxNearby && pickup == other.pickup && particleAnim == other.particleAnim && - explosionProof == other.explosionProof + explosionProof == other.explosionProof && + noAI == other.noAI fun copyTo(other: SpawnerData) { other.mob = mob @@ -144,6 +155,7 @@ value class SpawnerData(val pdc: PersistentDataContainer) { other.pickup = pickup other.particleAnim = particleAnim other.explosionProof = explosionProof + other.noAI = noAI other.stackSize = stackSize } } @@ -182,6 +194,13 @@ fun resolveEntityType(mobId: String): EntityType? { return entityTypeOrNull(baseMobId) } +/** + * The spawner as it is tracked in [PlacedSpawners], taken from the block state so the + * index carries the same data the block does. + */ +fun CreatureSpawner.toPlacedSpawner(): PlacedSpawner = + PlacedSpawner(location, spawner.particleAnim, spawner.mob, spawner.stackSize) + /** * The spawner as an item, carrying its whole stack unless [stackSize] says otherwise. */ diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt index 79ff0c7c..b588b64f 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerSpawning.kt @@ -6,16 +6,24 @@ import com.willfp.ecomobs.mob.EcoMobs import com.willfp.ecomobs.mob.SpawnReason import org.bukkit.Bukkit import org.bukkit.Location +import org.bukkit.entity.LivingEntity import org.bukkit.persistence.PersistentDataType /** * Spawns [mobId] at [location] as the spawner at [spawnerLocation] would, marking the * entity so the rest of the plugin can tell it came from a spawner. * + * With [noAI], the mob is spawned inert, exactly as vanilla's NoAI does. + * * Shared by both spawner modes so they can't drift apart, which also makes it the one * place [EcoMobSpawnerSpawnEvent] has to be called from. */ -fun spawnFromSpawner(spawnerLocation: Location, location: Location, mobId: String) { +fun spawnFromSpawner( + spawnerLocation: Location, + location: Location, + mobId: String, + noAI: Boolean = false +) { val spawnEvent = EcoMobSpawnerSpawnEvent(spawnerLocation, mobId, location) Bukkit.getPluginManager().callEvent(spawnEvent) @@ -32,4 +40,8 @@ fun spawnFromSpawner(spawnerLocation: Location, location: Location, mobId: Strin } entity?.persistentDataContainer?.set(entityFromSpawnerKey, PersistentDataType.BYTE, 1) + + if (noAI) { + (entity as? LivingEntity)?.setAI(false) + } } diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt index 94fac5ac..b59080a6 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/spawner/SpawnerStacks.kt @@ -4,13 +4,17 @@ import com.willfp.eco.core.fast.FastItemStack import com.willfp.ecomobs.event.EcoMobSpawnerStackEvent import org.bukkit.Bukkit import org.bukkit.block.CreatureSpawner +import org.bukkit.entity.Player object SpawnerStacks { /** * Adds up to [amount] spawners from [item] onto the stack at [state], returning how * many were actually taken. Zero means the two don't match, or the stack is full. + * + * [player] is whoever is stacking them, and is only carried through to + * [EcoMobSpawnerStackEvent]. */ - fun add(state: CreatureSpawner, item: FastItemStack, amount: Int): Int { + fun add(state: CreatureSpawner, item: FastItemStack, amount: Int, player: Player? = null): Int { if (!SpawnerStackSettings.enabled) { return 0 } @@ -33,6 +37,7 @@ object SpawnerStacks { val stackEvent = EcoMobSpawnerStackEvent( state.location, state.spawner.mob, + player, current, minOf(amount, space) ) @@ -53,6 +58,8 @@ object SpawnerStacks { state.spawner.stackSize = current + taken state.update() + PlacedSpawners.sync(state) + SpawnerHolograms.refresh(state.location) return taken diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/StackSettings.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/StackSettings.kt index 36f41bfc..1c5f9aba 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/StackSettings.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/stacking/StackSettings.kt @@ -21,6 +21,9 @@ object StackSettings { var killWholeStack = false private set + var hideDeathAnimation = false + private set + var matchAge = true private set @@ -53,6 +56,7 @@ object StackSettings { maxSize = config.getInt("stacking.max-size").coerceAtLeast(2) sweepRate = config.getInt("stacking.sweep-rate").coerceAtLeast(1) killWholeStack = config.getBool("stacking.kill-whole-stack") + hideDeathAnimation = config.getBool("stacking.hide-death-animation") matchAge = config.getBool("stacking.match-age") nameplate = config.getString("stacking.nameplate") blacklist = config.getStrings("stacking.blacklist").map { it.lowercase() }.toSet() diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/EcoMobsTriggers.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/EcoMobsTriggers.kt new file mode 100644 index 00000000..c6f321c4 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/EcoMobsTriggers.kt @@ -0,0 +1,27 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.libreforge.triggers.Triggers + +/** + * The triggers EcoMobs adds to libreforge, so effects in any libreforge plugin can + * listen for them. + * + * Triggers are only enabled once a config asks for one by ID, so registering them all + * up front costs nothing. + */ +object EcoMobsTriggers { + fun registerAll() { + Triggers.register(TriggerBreakSpawner) + Triggers.register(TriggerBuildTotem) + Triggers.register(TriggerDamageMob) + Triggers.register(TriggerMobDrops) + Triggers.register(TriggerPlaceSpawner) + Triggers.register(TriggerSpawnerSpawn) + Triggers.register(TriggerStackDeath) + Triggers.register(TriggerStackMerge) + Triggers.register(TriggerStackSpawner) + Triggers.register(TriggerStageChange) + Triggers.register(TriggerUnstackSpawner) + Triggers.register(TriggerUseSpawnEgg) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBreakSpawner.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBreakSpawner.kt new file mode 100644 index 00000000..003a3605 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBreakSpawner.kt @@ -0,0 +1,53 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobSpawnerBreakEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerBreakSpawner : Trigger("ecomobs_break_spawner") { + override val description = "Fires when a player breaks a custom spawner and the whole thing comes up." + + override val categories = setOf("ecomobs") + + override val additionalInfo = listOf( + "Taking part of a stack off without the block coming up fires " + + "ecomobs_unstack_spawner instead." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player who broke the spawner.", + TriggerParameter.BLOCK to "The spawner block.", + TriggerParameter.LOCATION to "Where the spawner was.", + TriggerParameter.TEXT to "The ID of the mob the spawner spawns.", + TriggerParameter.VALUE to "How many spawners the broken stack held." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.BLOCK, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobSpawnerBreakEvent) { + val player = event.player + + this.dispatch( + player.toDispatcher(), + TriggerData( + player = player, + block = event.location.block, + location = event.location, + text = event.mobId, + value = event.stackSize.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBuildTotem.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBuildTotem.kt new file mode 100644 index 00000000..3cecfffb --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerBuildTotem.kt @@ -0,0 +1,42 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobTotemBuildEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerBuildTotem : Trigger("ecomobs_build_totem") { + override val description = "Fires when a player completes a spawn totem and its conditions are met." + + override val categories = setOf("ecomobs") + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player who finished the totem.", + TriggerParameter.LOCATION to "Where the mob is about to spawn.", + TriggerParameter.TEXT to "The ID of the mob the totem spawns." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobTotemBuildEvent) { + val player = event.player + + this.dispatch( + player.toDispatcher(), + TriggerData( + player = player, + location = event.location, + text = event.mob.id, + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerDamageMob.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerDamageMob.kt new file mode 100644 index 00000000..09a1c8d7 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerDamageMob.kt @@ -0,0 +1,53 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobDamageEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerDamageMob : Trigger("ecomobs_damage_mob") { + override val description = "Fires when an EcoMob takes damage, after the mob's own damage modifiers." + + override val categories = setOf("ecomobs", "combat") + + override val additionalInfo = listOf( + "Dispatched on the damager when a player dealt the damage, and on the mob itself otherwise." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player who dealt the damage, if there was one.", + TriggerParameter.VICTIM to "The EcoMob that was damaged.", + TriggerParameter.LOCATION to "Where the mob was damaged.", + TriggerParameter.TEXT to "The ID of the mob that was damaged.", + TriggerParameter.VALUE to "The damage dealt." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.VICTIM, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobDamageEvent) { + val living = event.mob + val player = event.player + + this.dispatch( + player?.toDispatcher() ?: living.entity.toDispatcher(), + TriggerData( + player = player, + victim = living.entity, + location = living.entity.location, + text = living.mob.id, + value = event.damage, + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerMobDrops.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerMobDrops.kt new file mode 100644 index 00000000..dce523f7 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerMobDrops.kt @@ -0,0 +1,57 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobDropsEvent +import com.willfp.libreforge.Dispatcher +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerMobDrops : Trigger("ecomobs_mob_drops") { + override val description = "Fires when an EcoMob's drop table has been rolled, before anything is given out." + + override val categories = setOf("ecomobs") + + override val additionalInfo = listOf( + "Dispatched on the player the drops go to, and on the drop location otherwise.", + "A stacked mob rolls its table once per mob, so this fires once per roll." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player the drops go to, if there is one.", + TriggerParameter.LOCATION to "Where the drops land.", + TriggerParameter.TEXT to "The ID of the mob that dropped them.", + TriggerParameter.VALUE to "The experience dropped.", + TriggerParameter.ALT_VALUE to "How many items were rolled." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.ALT_VALUE, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobDropsEvent) { + val player = event.player + + // The drop location stands in when the drops fall on the ground. + val dispatcher: Dispatcher<*> = player?.toDispatcher() ?: event.location.toDispatcher() + + this.dispatch( + dispatcher, + TriggerData( + player = player, + location = event.location, + text = event.ecoMob.id, + value = event.experience.toDouble(), + altValue = event.drops.size.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerPlaceSpawner.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerPlaceSpawner.kt new file mode 100644 index 00000000..8b79d2c7 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerPlaceSpawner.kt @@ -0,0 +1,48 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobSpawnerPlaceEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerPlaceSpawner : Trigger("ecomobs_place_spawner") { + override val description = "Fires when a player places a custom spawner." + + override val categories = setOf("ecomobs") + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player who placed the spawner.", + TriggerParameter.BLOCK to "The spawner block.", + TriggerParameter.LOCATION to "Where the spawner was placed.", + TriggerParameter.TEXT to "The ID of the mob the spawner spawns.", + TriggerParameter.VALUE to "How many spawners the placed stack holds." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.BLOCK, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobSpawnerPlaceEvent) { + val player = event.player + + this.dispatch( + player.toDispatcher(), + TriggerData( + player = player, + block = event.location.block, + location = event.location, + text = event.mobId, + value = event.stackSize.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerSpawnerSpawn.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerSpawnerSpawn.kt new file mode 100644 index 00000000..deb65ef2 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerSpawnerSpawn.kt @@ -0,0 +1,45 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobSpawnerSpawnEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerSpawnerSpawn : Trigger("ecomobs_spawner_spawn") { + override val description = "Fires for every mob a spawner is about to spawn." + + override val categories = setOf("ecomobs") + + override val additionalInfo = listOf( + "Dispatched on the spawner's location, as there is no player involved.", + "A stacked spawner fires this once per mob, so it can fire many times a cycle." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.BLOCK to "The spawner block.", + TriggerParameter.LOCATION to "Where the mob is about to appear, offset from the spawner.", + TriggerParameter.TEXT to "The ID of the mob being spawned." + ) + + override val parameters = setOf( + TriggerParameter.BLOCK, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobSpawnerSpawnEvent) { + this.dispatch( + event.location.toDispatcher(), + TriggerData( + block = event.location.block, + location = event.spawnLocation, + text = event.mobId, + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackDeath.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackDeath.kt new file mode 100644 index 00000000..cd45aa68 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackDeath.kt @@ -0,0 +1,55 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobStackDeathEvent +import com.willfp.ecomobs.mob.impl.ecoMob +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerStackDeath : Trigger("ecomobs_stack_death") { + override val description = "Fires when a stacked mob dies, before the stack is paid out or split." + + override val categories = setOf("ecomobs", "combat") + + override val additionalInfo = listOf( + "Dispatched on the killer where there is one, and on the mob itself otherwise.", + "Only fires for mobs that stand in for more than one mob." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player who killed the mob, if there was one.", + TriggerParameter.VICTIM to "The mob that died.", + TriggerParameter.LOCATION to "Where the mob died.", + TriggerParameter.TEXT to "The ID of the EcoMob, or empty for a vanilla mob.", + TriggerParameter.VALUE to "The size of the stack that died." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.VICTIM, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.EVENT + ) + + @EventHandler + fun handle(event: EcoMobStackDeathEvent) { + val entity = event.entity + val killer = entity.killer + + this.dispatch( + killer?.toDispatcher() ?: entity.toDispatcher(), + TriggerData( + player = killer, + victim = entity, + location = entity.location, + text = entity.ecoMob?.id, + value = event.size.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackMerge.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackMerge.kt new file mode 100644 index 00000000..5153ffcf --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackMerge.kt @@ -0,0 +1,51 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobStackMergeEvent +import com.willfp.ecomobs.mob.impl.ecoMob +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerStackMerge : Trigger("ecomobs_stack_merge") { + override val description = "Fires when a mob is about to be absorbed into a nearby stack." + + override val categories = setOf("ecomobs") + + override val additionalInfo = listOf( + "Dispatched on the mob being merged into, as there is no player involved.", + "Vanilla mobs stack too, so the text is empty unless the stack is of EcoMobs." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.VICTIM to "The mob being merged into.", + TriggerParameter.LOCATION to "Where the stack is.", + TriggerParameter.TEXT to "The ID of the EcoMob, or empty for a vanilla mob.", + TriggerParameter.VALUE to "The size the stack ends up at." + ) + + override val parameters = setOf( + TriggerParameter.VICTIM, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobStackMergeEvent) { + val target = event.target + + this.dispatch( + target.toDispatcher(), + TriggerData( + victim = target, + location = target.location, + text = target.ecoMob?.id, + value = event.size.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackSpawner.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackSpawner.kt new file mode 100644 index 00000000..7a1dfbc4 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStackSpawner.kt @@ -0,0 +1,51 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobSpawnerStackEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerStackSpawner : Trigger("ecomobs_stack_spawner") { + override val description = "Fires when a player adds spawners to a stack." + + override val categories = setOf("ecomobs") + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player stacking the spawners.", + TriggerParameter.BLOCK to "The spawner block being stacked onto.", + TriggerParameter.LOCATION to "Where the stack is.", + TriggerParameter.TEXT to "The ID of the mob the spawner spawns.", + TriggerParameter.VALUE to "How many spawners are being added.", + TriggerParameter.ALT_VALUE to "The stack size before the merge." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.BLOCK, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.ALT_VALUE, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobSpawnerStackEvent) { + val player = event.player ?: return + + this.dispatch( + player.toDispatcher(), + TriggerData( + player = player, + block = event.location.block, + location = event.location, + text = event.mobId, + value = event.amount.toDouble(), + altValue = event.currentSize.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStageChange.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStageChange.kt new file mode 100644 index 00000000..3f5875d0 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerStageChange.kt @@ -0,0 +1,54 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobStageChangeEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerStageChange : Trigger("ecomobs_stage_change") { + override val description = "Fires when an EcoMob finishes one of its damage stages." + + override val categories = setOf("ecomobs") + + override val additionalInfo = listOf( + "Dispatched on the player whose hit ended the stage, and on the mob itself otherwise.", + "Also fires as the last stage ends, where the value is one past the number of stages." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player whose hit ended the stage, if there was one.", + TriggerParameter.VICTIM to "The mob whose stage changed.", + TriggerParameter.LOCATION to "Where the mob is.", + TriggerParameter.TEXT to "The ID of the mob whose stage changed.", + TriggerParameter.VALUE to "The 1-based position of the stage now in progress." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.VICTIM, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.EVENT + ) + + @EventHandler + fun handle(event: EcoMobStageChangeEvent) { + val living = event.mob + val player = event.player + + this.dispatch( + player?.toDispatcher() ?: living.entity.toDispatcher(), + TriggerData( + player = player, + victim = living.entity, + location = living.entity.location, + text = living.mob.id, + value = event.stageNumber.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUnstackSpawner.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUnstackSpawner.kt new file mode 100644 index 00000000..65a806fd --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUnstackSpawner.kt @@ -0,0 +1,56 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobSpawnerUnstackEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerUnstackSpawner : Trigger("ecomobs_unstack_spawner") { + override val description = "Fires when a player takes spawners off a stack, leaving the block in place." + + override val categories = setOf("ecomobs") + + override val additionalInfo = listOf( + "Breaking the last spawner, or the whole stack at once, fires " + + "ecomobs_break_spawner instead." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player taking the spawners.", + TriggerParameter.BLOCK to "The spawner block left behind.", + TriggerParameter.LOCATION to "Where the stack is.", + TriggerParameter.TEXT to "The ID of the mob the spawner spawns.", + TriggerParameter.VALUE to "How many spawners are being taken.", + TriggerParameter.ALT_VALUE to "The stack size before the break." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.BLOCK, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.VALUE, + TriggerParameter.ALT_VALUE, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobSpawnerUnstackEvent) { + val player = event.player + + this.dispatch( + player.toDispatcher(), + TriggerData( + player = player, + block = event.location.block, + location = event.location, + text = event.mobId, + value = event.amount.toDouble(), + altValue = event.currentSize.toDouble(), + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUseSpawnEgg.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUseSpawnEgg.kt new file mode 100644 index 00000000..94998c74 --- /dev/null +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/ecomobs/trigger/TriggerUseSpawnEgg.kt @@ -0,0 +1,47 @@ +package com.willfp.ecomobs.trigger + +import com.willfp.ecomobs.event.EcoMobEggUseEvent +import com.willfp.libreforge.toDispatcher +import com.willfp.libreforge.triggers.Trigger +import com.willfp.libreforge.triggers.TriggerData +import com.willfp.libreforge.triggers.TriggerParameter +import org.bukkit.event.EventHandler + +object TriggerUseSpawnEgg : Trigger("ecomobs_use_spawn_egg") { + override val description = "Fires when a player spawns an EcoMob with a spawn egg." + + override val categories = setOf("ecomobs") + + override val additionalInfo = listOf( + "Only fires for eggs used by a player. Eggs fired from a dispenser have nobody " + + "to dispatch on, so they do not fire this trigger." + ) + + override val parameterDescriptions = mapOf( + TriggerParameter.PLAYER to "The player who used the egg.", + TriggerParameter.LOCATION to "Where the mob is about to spawn.", + TriggerParameter.TEXT to "The ID of the mob the egg spawns." + ) + + override val parameters = setOf( + TriggerParameter.PLAYER, + TriggerParameter.LOCATION, + TriggerParameter.TEXT, + TriggerParameter.EVENT + ) + + @EventHandler(ignoreCancelled = true) + fun handle(event: EcoMobEggUseEvent) { + val player = event.player ?: return + + this.dispatch( + player.toDispatcher(), + TriggerData( + player = player, + location = event.location, + text = event.mob.id, + event = event + ) + ) + } +} diff --git a/eco-core/core-plugin/src/main/resources/config.yml b/eco-core/core-plugin/src/main/resources/config.yml index 07eae17a..93249d74 100644 --- a/eco-core/core-plugin/src/main/resources/config.yml +++ b/eco-core/core-plugin/src/main/resources/config.yml @@ -41,6 +41,11 @@ stacking: # mob in it. When false, each kill takes a single mob off the stack. kill-whole-stack: false + # Whether the death animation is hidden when a stacked mob is killed, for both + # whole-stack kills and single kills off the stack. The corpse is taken away right + # after the kill instead of lying there for a second; drops and XP are unaffected. + hide-death-animation: false + # Whether babies and adults are kept in separate stacks. # Set to false to let babies stack into adult stacks, and vice versa. match-age: true @@ -129,10 +134,11 @@ spawner-display: - "&8Pickup: &f%pickup%" - "&8Particle: &f%particle%" - "&8Explosion-Proof: &f%explosion_proof%" + - "&8No AI: &f%no_ai%" # Added to the lore only when the spawner item holds more than one spawner. stacked-lore: - - "&8Stack: &f%size%" + - "&8Stack: &f%size%&8/&f%max_stack_size%" animations: circle: diff --git a/eco-core/core-plugin/src/main/resources/plugin.yml b/eco-core/core-plugin/src/main/resources/plugin.yml index e79b5959..08dadbc9 100644 --- a/eco-core/core-plugin/src/main/resources/plugin.yml +++ b/eco-core/core-plugin/src/main/resources/plugin.yml @@ -13,6 +13,7 @@ softdepend: - ModelEngine - LibsDisguises - BetterModel + - CoreProtect commands: ecomobs: @@ -86,6 +87,8 @@ permissions: ecomobs.command.spawner.modify.pickup: true ecomobs.command.spawner.modify.particle: true ecomobs.command.spawner.modify.explosion-proof: true + ecomobs.command.spawner.modify.no-ai: true + ecomobs.command.spawner.modify.stack-size: true ecomobs.command.spawner.modify.mob: description: Modify spawner mob type @@ -123,6 +126,14 @@ permissions: description: Modify spawner explosion-proof setting default: op + ecomobs.command.spawner.modify.no-ai: + description: Modify whether spawned mobs have their AI disabled + default: op + + ecomobs.command.spawner.modify.stack-size: + description: Modify how many spawners the spawner stands in for + default: op + ecomobs.spawner.*: description: Spawner runtime permissions default: op diff --git a/ecohub/ecomobs/commands-and-permissions.md b/ecohub/ecomobs/commands-and-permissions.md index c3fa3ead..548fdce5 100644 --- a/ecohub/ecomobs/commands-and-permissions.md +++ b/ecohub/ecomobs/commands-and-permissions.md @@ -40,6 +40,7 @@ Both the `give` and `modify` commands accept the following attributes: | `pickup` | `allow` \| `silk_touch` \| `deny` | How players can pick up the spawner | `ecomobs.command.spawner.modify.pickup` | | `particle` | `none` \| `` | Particle animation displayed by the spawner | `ecomobs.command.spawner.modify.particle` | | `explosion-proof` | `true` \| `false` | Whether the spawner survives explosions | `ecomobs.command.spawner.modify.explosion-proof` | +| `no-ai` | `true` | `false` | Whether spawned mobs have their AI disabled | `ecomobs.command.spawner.modify.no-ai` | | `stack-size` | `` | How many spawners the spawner stands in for (min 1) | `ecomobs.command.spawner.modify.stack-size` | ### Additional Permissions diff --git a/ecohub/ecomobs/mob-stacking.md b/ecohub/ecomobs/mob-stacking.md index 37583dac..f3fe4fcb 100644 --- a/ecohub/ecomobs/mob-stacking.md +++ b/ecohub/ecomobs/mob-stacking.md @@ -23,6 +23,9 @@ stacking: # mob in it. When false, each kill takes a single mob off the stack. kill-whole-stack: false + # Whether the death animation is hidden when a stacked mob is killed. + hide-death-animation: false + # Whether babies and adults are kept in separate stacks. match-age: true @@ -95,6 +98,14 @@ For EcoMobs, the drop table is rolled again per mob, so chance-based drops stay Best for grinders and arenas, where clearing a stack in one hit is the point. +### `hide-death-animation: false` (default) + +Whether the dead mob's body plays out its death animation. This applies to both settings above — the whole stack dying at once, and a single mob coming off the stack. + +Left at `false`, the corpse tips over and fades out the way any mob does, taking about a second. On a stack killed one mob at a time that second is spent lying next to the replacement mob, so a fast grinder ends up with a pile of bodies on top of the live stack. + +Set it to `true` and the corpse is taken away right after the kill, so the mob simply vanishes. Drops, XP, death messages, and everything else that hangs off the kill are unaffected — only the body goes early. + :::warning `kill-whole-stack: true` multiplies loot, and a large `max-size` multiplies it a lot. Check what a full stack of your most valuable mob pays out before turning it on. ::: diff --git a/ecohub/ecomobs/plugin-config.md b/ecohub/ecomobs/plugin-config.md index f564fda7..b01c7dd6 100644 --- a/ecohub/ecomobs/plugin-config.md +++ b/ecohub/ecomobs/plugin-config.md @@ -27,6 +27,7 @@ stacking: # Merges nearby mobs of the same kind into one entity; see Mob Stackin max-size: 64 # The most mobs one stack can hold sweep-rate: 100 # Ticks between sweeps for mobs that drifted together after spawning kill-whole-stack: false # true kills and pays out the whole stack at once + hide-death-animation: false # true takes the corpse away instead of playing the death animation match-age: true # Whether babies and adults are kept in separate stacks nameplate: "&f%name% &7x%size%" # Shown above a stack; %size% and %name% blacklist: # Never stacks; EcoMob IDs and vanilla entity types both work @@ -69,6 +70,7 @@ spawner-display: # The held/placed spawner item's name and lore; supports the pl - "&8Pickup: &f%pickup%" - "&8Particle: &f%particle%" - "&8Explosion-Proof: &f%explosion_proof%" + - "&8No AI: &f%no_ai%" stacked-lore: # Added only when the item holds more than one spawner - "&8Stack: &f%size%"