From bab703601e794993436eec17ee1b23940f531dc7 Mon Sep 17 00:00:00 2001 From: TDSTOS Date: Sat, 28 Mar 2026 17:23:39 +0100 Subject: [PATCH] Add four new kits and kit event hooks Introduce Armorer, BlackPanther, Rattlesnake and Voodoo kit implementations and wire them into the plugin. Add new Kit hooks (onKillEnemy, onToggleSneak, onItemBreak) and PassiveAbility.onKillEnemy so kits can react to kills and item/sneak events. Update KitEventDispatcher to dispatch kill, sneak-toggle and item-break events, handle BlackPanther push-projectiles, and expose the ICE/black-panther PDC keys. Register the new kits in SpeedHG and add language entries for Rattlesnake and Armorer messages. --- .../kotlin/club/mcscrims/speedhg/SpeedHG.kt | 4 + .../kotlin/club/mcscrims/speedhg/kit/Kit.kt | 18 ++ .../speedhg/kit/ability/PassiveAbility.kt | 3 + .../mcscrims/speedhg/kit/impl/ArmorerKit.kt | 196 ++++++++++++ .../speedhg/kit/impl/BlackPantherKit.kt | 272 +++++++++++++++++ .../speedhg/kit/impl/RattlesnakeKit.kt | 268 ++++++++++++++++ .../mcscrims/speedhg/kit/impl/VoodooKit.kt | 286 ++++++++++++++++++ .../kit/listener/KitEventDispatcher.kt | 65 +++- src/main/resources/languages/en_US.yml | 86 +++++- 9 files changed, 1187 insertions(+), 11 deletions(-) create mode 100644 src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index cee4dc5..7c49a2e 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -110,11 +110,15 @@ class SpeedHG : JavaPlugin() { private fun registerKits() { + kitManager.registerKit( ArmorerKit() ) kitManager.registerKit( BackupKit() ) + kitManager.registerKit( BlackPantherKit() ) kitManager.registerKit( GladiatorKit() ) kitManager.registerKit( GoblinKit() ) kitManager.registerKit( IceMageKit() ) + kitManager.registerKit( RattlesnakeKit() ) kitManager.registerKit( VenomKit() ) + kitManager.registerKit( VoodooKit() ) } private fun registerCommands() diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt index 3605ebc..d51a001 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt @@ -89,4 +89,22 @@ abstract class Kit { * The matching [PassiveAbility.onDeactivate] is called immediately before this. */ open fun onRemove( player: Player ) {} + + /** + * Called when the player using this kit scores a kill. + * Dispatched by [KitEventDispatcher] via [PlayerDeathEvent]. + */ + open fun onKillEnemy( killer: Player, victim: Player ) {} + + /** + * Called when the player toggles sneak. Dispatched by [KitEventDispatcher]. + * @param isSneaking true = player just started sneaking. + */ + open fun onToggleSneak( player: Player, isSneaking: Boolean ) {} + + /** + * Called when a player's item breaks. Use to replace kit armor automatically. + */ + open fun onItemBreak( player: Player, brokenItem: ItemStack ) {} + } \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/PassiveAbility.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/PassiveAbility.kt index 3646f0d..dfb0904 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/PassiveAbility.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/PassiveAbility.kt @@ -50,6 +50,9 @@ abstract class PassiveAbility( /** [victim] (this player) just received a melee hit from [attacker]. */ open fun onHitByEnemy( victim: Player, attacker: Player, event: EntityDamageByEntityEvent ) {} + /** Called when this player kills another player via melee. */ + open fun onKillEnemy( killer: Player, victim: Player ) {} + // ------------------------------------------------------------------------- // Interaction hooks (called for non-trigger-material right-clicks) // ------------------------------------------------------------------------- diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt new file mode 100644 index 0000000..468f2f4 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt @@ -0,0 +1,196 @@ +package club.mcscrims.speedhg.kit.impl + +import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.kit.Kit +import club.mcscrims.speedhg.kit.Playstyle +import club.mcscrims.speedhg.kit.ability.AbilityResult +import club.mcscrims.speedhg.kit.ability.ActiveAbility +import club.mcscrims.speedhg.kit.ability.PassiveAbility +import club.mcscrims.speedhg.util.trans +import net.kyori.adventure.text.Component +import org.bukkit.Material +import org.bukkit.Sound +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * ## Armorer + * + * Upgrades the player's **Chestplate + Boots** every 2 kills. + * + * | Tier | Kills | Material | + * |------|-------|-----------| + * | 0 | 0 | Leather | + * | 1 | 2 | Iron | + * | 2 | 4 | Diamond | + * + * When a piece breaks ([onItemBreak]), it is immediately replaced with the + * current tier so the player is never left without armor. + * + * - **AGGRESSIVE passive**: +Strength I (5 s) for every kill. + * - **DEFENSIVE passive**: +Protection I enchant on iron/diamond pieces. + */ +class ArmorerKit : Kit() { + + private val plugin get() = SpeedHG.instance + + override val id = "armorer" + override val displayName: Component + get() = plugin.languageManager.getDefaultComponent("kits.armorer.name", mapOf()) + override val lore: List + get() = plugin.languageManager.getDefaultRawMessageList("kits.armorer.lore") + override val icon = Material.IRON_CHESTPLATE + + // ── Kill tracking ───────────────────────────────────────────────────────── + internal val killCounts: MutableMap = ConcurrentHashMap() + + companion object { + private val CHESTPLATE_TIERS = listOf( + Material.LEATHER_CHESTPLATE, + Material.IRON_CHESTPLATE, + Material.DIAMOND_CHESTPLATE + ) + private val BOOT_TIERS = listOf( + Material.LEATHER_BOOTS, + Material.IRON_BOOTS, + Material.DIAMOND_BOOTS + ) + private val EQUIP_SOUNDS = listOf( + Sound.ITEM_ARMOR_EQUIP_LEATHER, + Sound.ITEM_ARMOR_EQUIP_IRON, + Sound.ITEM_ARMOR_EQUIP_DIAMOND + ) + } + + // ── Cached ability instances ────────────────────────────────────────────── + private val aggressiveActive = NoActive(Playstyle.AGGRESSIVE) + private val defensiveActive = NoActive(Playstyle.DEFENSIVE) + private val aggressivePassive = AggressivePassive() + private val defensivePassive = DefensivePassive() + + override fun getActiveAbility (playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressiveActive + Playstyle.DEFENSIVE -> defensiveActive + } + override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressivePassive + Playstyle.DEFENSIVE -> defensivePassive + } + + override val cachedItems = ConcurrentHashMap>() + + override fun giveItems(player: Player, playstyle: Playstyle) { /* armor is given in onAssign */ } + + override fun onAssign(player: Player, playstyle: Playstyle) { + killCounts[player.uniqueId] = 0 + setArmorTier(player, tier = 0, playstyle) + } + + override fun onRemove(player: Player) { + killCounts.remove(player.uniqueId) + } + + // ── Kit-level kill hook (upgrades armor) ────────────────────────────────── + + override fun onKillEnemy(killer: Player, victim: Player) { + val newKills = killCounts.compute(killer.uniqueId) { _, v -> (v ?: 0) + 1 } ?: return + val tier = (newKills / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex) + val prevTier = ((newKills - 1) / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex) + + if (tier > prevTier) { + val playstyle = plugin.kitManager.getSelectedPlaystyle(killer) + setArmorTier(killer, tier, playstyle) + killer.playSound(killer.location, EQUIP_SOUNDS[tier], 1f, 1f) + killer.sendActionBar(killer.trans("kits.armorer.messages.armor_upgraded", + mapOf("tier" to (tier + 1).toString()))) + } + } + + // ── Auto-replace on armor break ─────────────────────────────────────────── + + override fun onItemBreak(player: Player, brokenItem: ItemStack) { + val kills = killCounts[player.uniqueId] ?: return + val tier = (kills / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex) + val playstyle = plugin.kitManager.getSelectedPlaystyle(player) + + when { + brokenItem.type.name.endsWith("_CHESTPLATE") -> { + player.inventory.chestplate = buildPiece(CHESTPLATE_TIERS[tier], tier, playstyle) + player.sendActionBar(player.trans("kits.armorer.messages.armor_replaced")) + } + brokenItem.type.name.endsWith("_BOOTS") -> { + player.inventory.boots = buildPiece(BOOT_TIERS[tier], tier, playstyle) + player.sendActionBar(player.trans("kits.armorer.messages.armor_replaced")) + } + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private fun setArmorTier(player: Player, tier: Int, playstyle: Playstyle) { + player.inventory.chestplate = buildPiece(CHESTPLATE_TIERS[tier], tier, playstyle) + player.inventory.boots = buildPiece(BOOT_TIERS[tier], tier, playstyle) + } + + /** + * Builds an armor ItemStack, adding Protection I for DEFENSIVE builds + * starting at iron tier (tier ≥ 1). + */ + private fun buildPiece(material: Material, tier: Int, playstyle: Playstyle): ItemStack { + val item = ItemStack(material) + if (playstyle == Playstyle.DEFENSIVE && tier >= 1) { + item.editMeta { it.addEnchant(Enchantment.PROTECTION, 1, true) } + } + return item + } + + // ========================================================================= + // AGGRESSIVE passive – Strength I on every kill + // ========================================================================= + + private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.aggressive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.aggressive.description") + + override fun onKillEnemy(killer: Player, victim: Player) { + killer.addPotionEffect(PotionEffect(PotionEffectType.STRENGTH, 5 * 20, 0)) + killer.playSound(killer.location, Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.4f) + } + } + + // ========================================================================= + // DEFENSIVE passive – Protection enchant is sufficient; no extra hook needed + // ========================================================================= + + private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.defensive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.defensive.description") + } + + // ========================================================================= + // Shared no-ability active + // ========================================================================= + + inner class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) { + override val name = "None" + override val description = "None" + override val hitsRequired = 0 + override val triggerMaterial = Material.BARRIER + override fun execute(player: Player) = AbilityResult.Success + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt new file mode 100644 index 0000000..b1ba811 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt @@ -0,0 +1,272 @@ +package club.mcscrims.speedhg.kit.impl + +import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.kit.Kit +import club.mcscrims.speedhg.kit.Playstyle +import club.mcscrims.speedhg.kit.ability.AbilityResult +import club.mcscrims.speedhg.kit.ability.ActiveAbility +import club.mcscrims.speedhg.kit.ability.PassiveAbility +import club.mcscrims.speedhg.util.ItemBuilder +import club.mcscrims.speedhg.util.WorldEditUtils +import club.mcscrims.speedhg.util.trans +import net.kyori.adventure.text.Component +import org.bukkit.* +import org.bukkit.entity.Player +import org.bukkit.entity.Snowball +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataType +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * ## Black Panther + * + * | Playstyle | Active | Passive | + * |-------------|-------------------------------------------------|-------------------------------------------------| + * | AGGRESSIVE | **Push** – knockback all enemies ≤ 5 blocks + | **Vibranium Fists** – 6.5 dmg bare-hand for 12 s| + * | | shoot push-projectiles + activate Fist Mode | | + * | DEFENSIVE | – (no active item) | **Wakanda Forever!** – fall-pounce on enemies | + * + * ### Push (AGGRESSIVE active) + * All enemies within 5 blocks are launched outward. A marked Snowball is fired + * toward each pushed enemy 5 ticks later; on hit it deals **4 bonus damage** + * (handled by [KitEventDispatcher] via [PUSH_PROJECTILE_KEY]). + * CRIT particles spawn at each pushed enemy's position for visual feedback. + * Directly after the push, **Fist Mode** activates for 12 seconds. + * + * ### Vibranium Fists (AGGRESSIVE passive) + * During Fist Mode, bare-hand attacks (`itemInMainHand == AIR`) override the + * normal damage to **6.5 HP (3.25 hearts)**. + * + * ### Wakanda Forever! (DEFENSIVE passive) + * Triggers in [onHitEnemy] when `attacker.fallDistance ≥ 3`. Deals **6 HP** + * to all enemies within 3 blocks of the victim, then creates an explosion + * visual and a small WorldEdit crater at the landing site. + */ +class BlackPantherKit : Kit() +{ + + private val plugin get() = SpeedHG.instance + + override val id = "blackpanther" + override val displayName: Component + get() = plugin.languageManager.getDefaultComponent("kits.blackpanther.name", mapOf()) + override val lore: List + get() = plugin.languageManager.getDefaultRawMessageList("kits.blackpanther.lore") + override val icon = Material.BLACK_DYE + + /** Players currently in Fist Mode: UUID → expiry timestamp (ms). */ + internal val fistModeExpiry: MutableMap = ConcurrentHashMap() + + companion object + { + /** PDC key string shared with [KitEventDispatcher] for push-projectiles. */ + const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile" + + private const val FIST_MODE_MS = 12_000L // 12 seconds + private const val PUSH_RADIUS = 5.0 + private const val POUNCE_MIN_FALL = 3.0f + private const val POUNCE_RADIUS = 3.0 + private const val POUNCE_DAMAGE = 6.0 // 3 hearts = 6 HP + } + + // ── Cached ability instances ────────────────────────────────────────────── + private val aggressiveActive = AggressiveActive() + private val defensiveActive = DefensiveActive() + private val aggressivePassive = AggressivePassive() + private val defensivePassive = DefensivePassive() + + override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) + { + Playstyle.AGGRESSIVE -> aggressiveActive + Playstyle.DEFENSIVE -> defensiveActive + } + + override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) + { + Playstyle.AGGRESSIVE -> aggressivePassive + Playstyle.DEFENSIVE -> defensivePassive + } + + override val cachedItems = ConcurrentHashMap>() + + override fun giveItems(player: Player, playstyle: Playstyle) { + if (playstyle != Playstyle.AGGRESSIVE) return + val item = ItemBuilder(Material.BLACK_DYE) + .name(aggressiveActive.name) + .lore(listOf(aggressiveActive.description)) + .build() + cachedItems[player.uniqueId] = listOf(item) + player.inventory.addItem(item) + } + + override fun onRemove(player: Player) { + fistModeExpiry.remove(player.uniqueId) + cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } + } + +// ========================================================================= +// AGGRESSIVE active – Push + activate Fist Mode +// ========================================================================= + + private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.description") + override val hitsRequired = 15 + override val triggerMaterial = Material.BLACK_DYE + + override fun execute(player: Player): AbilityResult { + val enemies = player.world + .getNearbyEntities(player.location, PUSH_RADIUS, PUSH_RADIUS, PUSH_RADIUS) + .filterIsInstance() + .filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + + if (enemies.isEmpty()) + return AbilityResult.ConditionNotMet("No enemies within ${PUSH_RADIUS.toInt()} blocks!") + + val pushKey = (plugin.kitManager.getSelectedKit(player) as? BlackPantherKit) + ?.let { NamespacedKey(plugin, PUSH_PROJECTILE_KEY) } + + enemies.forEach { enemy -> + // ── Knockback ────────────────────────────────────────────────── + val knockDir = enemy.location.toVector() + .subtract(player.location.toVector()) + .normalize() + .multiply(2.0) + .setY(0.45) + + enemy.velocity = knockDir + enemy.world.spawnParticle(Particle.CRIT, + enemy.location.clone().add(0.0, 1.0, 0.0), 10, 0.3, 0.3, 0.3, 0.0) + + // ── Trailing push-projectile (deals 4 HP on hit) ────────────── + if (pushKey != null) { + Bukkit.getScheduler().runTaskLater(plugin, { -> + if (!player.isOnline) return@runTaskLater + val snowball = player.world.spawn( + player.eyeLocation, Snowball::class.java + ) + snowball.shooter = player + val travelDir = enemy.location.toVector() + .subtract(player.eyeLocation.toVector()) + .normalize() + .multiply(1.8) + snowball.velocity = travelDir + snowball.persistentDataContainer.set(pushKey, PersistentDataType.BYTE, 1) + }, 5L) + } + } + + // ── Activate Fist Mode ───────────────────────────────────────────── + fistModeExpiry[player.uniqueId] = System.currentTimeMillis() + FIST_MODE_MS + player.sendActionBar(player.trans("kits.blackpanther.messages.fist_mode_active")) + + player.world.playSound(player.location, Sound.ENTITY_RAVAGER_ROAR, 1f, 1.1f) + player.world.playSound(player.location, Sound.ENTITY_PLAYER_ATTACK_SWEEP, 0.8f, 0.7f) + + return AbilityResult.Success + } + + override fun onFullyCharged(player: Player) { + player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f) + player.sendActionBar(player.trans("kits.blackpanther.messages.ability_charged")) + } + } + +// ========================================================================= +// DEFENSIVE active – no active ability +// ========================================================================= + + private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { + override val name = "None" + override val description = "None" + override val hitsRequired = 0 + override val triggerMaterial = Material.BARRIER + override fun execute(player: Player) = AbilityResult.Success + } + +// ========================================================================= +// AGGRESSIVE passive – Vibranium Fists (6.5 dmg bare-hand during Fist Mode) +// ========================================================================= + + private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.aggressive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.aggressive.description") + + override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { + val expiry = fistModeExpiry[attacker.uniqueId] ?: return + if (System.currentTimeMillis() > expiry) { + fistModeExpiry.remove(attacker.uniqueId) + return + } + + if (attacker.inventory.itemInMainHand.type != Material.AIR) return + + event.damage = 6.5 // 3.25 hearts + victim.world.spawnParticle(Particle.CRIT, + victim.location.clone().add(0.0, 1.0, 0.0), 8, 0.3, 0.3, 0.3, 0.0) + attacker.playSound(attacker.location, Sound.ENTITY_PLAYER_ATTACK_CRIT, 1f, 0.9f) + } + } + +// ========================================================================= +// DEFENSIVE passive – Wakanda Forever! (fall-pounce → AOE + crater) +// ========================================================================= + + private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.defensive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.defensive.description") + + override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { + if (attacker.fallDistance < POUNCE_MIN_FALL) return + + // ── Instant damage to all enemies in splash radius ───────────────── + val splashTargets = victim.world + .getNearbyEntities(victim.location, POUNCE_RADIUS, POUNCE_RADIUS, POUNCE_RADIUS) + .filterIsInstance() + .filter { it != attacker && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + .onEach { t -> t.damage(POUNCE_DAMAGE, attacker) } + + // Direct victim also receives meteor damage + event.damage = POUNCE_DAMAGE + + // ── Explosion VFX + sound ────────────────────────────────────────── + val impactLoc = attacker.location.clone() + impactLoc.world.spawnParticle(Particle.EXPLOSION, impactLoc, 3, 0.5, 0.5, 0.5, 0.0) + impactLoc.world.spawnParticle(Particle.LARGE_SMOKE, impactLoc, 20, 1.0, 0.5, 1.0, 0.05) + impactLoc.world.playSound(impactLoc, Sound.ENTITY_GENERIC_EXPLODE, 1f, 0.7f) + impactLoc.world.playSound(impactLoc, Sound.ENTITY_IRON_GOLEM_HURT, 1f, 0.5f) + + // ── Crater (radius 3, hollow = false so the depression looks natural) ─ + Bukkit.getScheduler().runTaskLater(plugin, { -> + WorldEditUtils.createCylinder( + impactLoc.world, impactLoc.clone().subtract(0.0, 1.0, 0.0), + 3, true, 2, Material.AIR + ) + }, 2L) + + attacker.sendActionBar(attacker.trans("kits.blackpanther.messages.wakanda_impact", + mapOf("count" to (splashTargets.size + 1).toString()))) + + // Reset fall distance so a second consecutive pounce requires a real fall + attacker.fallDistance = 0f + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt new file mode 100644 index 0000000..27bf097 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt @@ -0,0 +1,268 @@ +package club.mcscrims.speedhg.kit.impl + +import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.kit.Kit +import club.mcscrims.speedhg.kit.Playstyle +import club.mcscrims.speedhg.kit.ability.AbilityResult +import club.mcscrims.speedhg.kit.ability.ActiveAbility +import club.mcscrims.speedhg.kit.ability.PassiveAbility +import club.mcscrims.speedhg.util.ItemBuilder +import club.mcscrims.speedhg.util.trans +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.Particle +import org.bukkit.Sound +import org.bukkit.entity.Player +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.util.Vector +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * ## Rattlesnake + * + * | Playstyle | Active | Passive | + * |-------------|------------------------------------------------|--------------------------------------| + * | AGGRESSIVE | Sneak-charged pounce (3–10 blocks) | Poison II on pounce-hit | + * | DEFENSIVE | – | 25 % counter-venom on being hit | + * + * Both playstyles receive **Speed II** at game start. + * + * ### Pounce mechanics + * 1. Hold sneak (up to 3 s) → charge builds linearly from 3 to 10 blocks. + * 2. Right-click the SLIME_BALL to launch. A 1.5 s timeout task is scheduled. + * 3. If [onHitEnemy] fires while the player is marked as *pouncing*: apply Poison II + * to 1 target (before Feast) or 3 targets (after Feast) and clear the flag. + * 4. If the timeout fires first (miss): apply Nausea + Slowness to nearby enemies. + */ +class RattlesnakeKit : Kit() { + + private val plugin get() = SpeedHG.instance + + override val id = "rattlesnake" + override val displayName: Component + get() = plugin.languageManager.getDefaultComponent("kits.rattlesnake.name", mapOf()) + override val lore: List + get() = plugin.languageManager.getDefaultRawMessageList("kits.rattlesnake.lore") + override val icon = Material.SLIME_BALL + + // ── Shared state (accessed by inner ability classes via outer-class reference) ─ + internal val sneakStartTimes: MutableMap = ConcurrentHashMap() + internal val pouncingPlayers: MutableSet = ConcurrentHashMap.newKeySet() + internal val lastPounceUse: MutableMap = ConcurrentHashMap() + + companion object { + private const val POUNCE_COOLDOWN_MS = 20_000L + private const val MAX_SNEAK_MS = 3_000L + private const val MIN_RANGE = 3.0 + private const val MAX_RANGE = 10.0 + private const val POUNCE_TIMEOUT_TICKS = 30L // 1.5 s + } + + // ── Cached ability instances ────────────────────────────────────────────── + private val aggressiveActive = AggressiveActive() + private val defensiveActive = DefensiveActive() + private val aggressivePassive = AggressivePassive() + private val defensivePassive = DefensivePassive() + + override fun getActiveAbility (playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressiveActive + Playstyle.DEFENSIVE -> defensiveActive + } + override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressivePassive + Playstyle.DEFENSIVE -> defensivePassive + } + + override val cachedItems = ConcurrentHashMap>() + + override fun giveItems(player: Player, playstyle: Playstyle) { + if (playstyle != Playstyle.AGGRESSIVE) return + val item = ItemBuilder(Material.SLIME_BALL) + .name(aggressiveActive.name) + .lore(listOf(aggressiveActive.description)) + .build() + cachedItems[player.uniqueId] = listOf(item) + player.inventory.addItem(item) + } + + override fun onAssign(player: Player, playstyle: Playstyle) { + player.addPotionEffect( + PotionEffect(PotionEffectType.SPEED, Int.MAX_VALUE, 1, false, false, true) + ) + } + + override fun onRemove(player: Player) { + player.removePotionEffect(PotionEffectType.SPEED) + sneakStartTimes.remove(player.uniqueId) + pouncingPlayers.remove(player.uniqueId) + lastPounceUse.remove(player.uniqueId) + cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } + } + + override fun onToggleSneak(player: Player, isSneaking: Boolean) { + if (plugin.kitManager.getSelectedPlaystyle(player) != Playstyle.AGGRESSIVE) return + if (isSneaking) sneakStartTimes[player.uniqueId] = System.currentTimeMillis() + } + + // ========================================================================= + // AGGRESSIVE active – sneak-charged pounce + // ========================================================================= + + private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.description") + override val hitsRequired = 0 // charged via sneaking, not hits + override val triggerMaterial = Material.SLIME_BALL + + override fun execute(player: Player): AbilityResult { + if (!player.isSneaking) + return AbilityResult.ConditionNotMet("Sneak while activating the pounce!") + + val now = System.currentTimeMillis() + if (now - (lastPounceUse[player.uniqueId] ?: 0L) < POUNCE_COOLDOWN_MS) { + val remaining = ((POUNCE_COOLDOWN_MS - (now - (lastPounceUse[player.uniqueId] ?: 0L))) / 1000) + return AbilityResult.ConditionNotMet("Cooldown: ${remaining}s remaining") + } + + // Sneak duration → range (3 – 10 blocks) + val sneakDuration = (now - (sneakStartTimes[player.uniqueId] ?: now)) + .coerceIn(0L, MAX_SNEAK_MS) + val range = MIN_RANGE + (sneakDuration.toDouble() / MAX_SNEAK_MS) * (MAX_RANGE - MIN_RANGE) + + val target = player.world + .getNearbyEntities(player.location, range, range, range) + .filterIsInstance() + .filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + .minByOrNull { it.location.distanceSquared(player.location) } + ?: return AbilityResult.ConditionNotMet("No enemies within ${range.toInt()} blocks!") + + // ── Launch ─────────────────────────────────────────────────────── + val launchVec: Vector = target.location.toVector() + .subtract(player.location.toVector()) + .normalize() + .multiply(1.9) + .setY(0.55) + + player.velocity = launchVec + player.playSound(player.location, Sound.ENTITY_SLIME_JUMP, 1f, 1.7f) + + pouncingPlayers.add(player.uniqueId) + lastPounceUse[player.uniqueId] = now + + // ── Miss timeout ────────────────────────────────────────────────── + Bukkit.getScheduler().runTaskLater(plugin, { -> + if (!pouncingPlayers.remove(player.uniqueId)) return@runTaskLater // already hit + + player.world.getNearbyEntities(player.location, 5.0, 5.0, 5.0) + .filterIsInstance() + .filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + .forEach { enemy -> + enemy.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 3 * 20, 0)) + enemy.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 3 * 20, 0)) + } + if (player.isOnline) + player.sendActionBar(player.trans("kits.rattlesnake.messages.pounce_miss")) + }, POUNCE_TIMEOUT_TICKS) + + return AbilityResult.Success + } + + override fun onFullyCharged(player: Player) { /* not used – hitsRequired = 0 */ } + } + + // ========================================================================= + // AGGRESSIVE passive – pounce-hit processing + // ========================================================================= + + private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.aggressive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.aggressive.description") + + /** + * Called AFTER the normal damage has been applied. + * If the attacker is currently pouncing, consume the flag and apply Poison II + * to 1 target (before Feast) or up to 3 targets (after Feast). + */ + override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { + if (!pouncingPlayers.remove(attacker.uniqueId)) return // not a pounce-hit + + val maxTargets = if (plugin.gameManager.feastManager.hasSpawned) 3 else 1 + + val targets = buildList { + add(victim) + if (maxTargets > 1) { + victim.world + .getNearbyEntities(victim.location, 4.0, 4.0, 4.0) + .filterIsInstance() + .filter { it != victim && it != attacker && + plugin.gameManager.alivePlayers.contains(it.uniqueId) } + .take(maxTargets - 1) + .forEach { add(it) } + } + } + + targets.forEach { t -> + t.addPotionEffect(PotionEffect(PotionEffectType.POISON, 8 * 20, 1)) // Poison II + t.world.spawnParticle(Particle.ITEM_SLIME, t.location.clone().add(0.0, 1.0, 0.0), + 12, 0.4, 0.4, 0.4, 0.0) + } + + attacker.playSound(attacker.location, Sound.ENTITY_SLIME_ATTACK, 1f, 0.7f) + attacker.sendActionBar( + attacker.trans("kits.rattlesnake.messages.pounce_hit", + mapOf("count" to targets.size.toString())) + ) + } + } + + // ========================================================================= + // DEFENSIVE active – no active ability + // ========================================================================= + + private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { + override val name = "None" + override val description = "None" + override val hitsRequired = 0 + override val triggerMaterial = Material.BARRIER + override fun execute(player: Player) = AbilityResult.Success + } + + // ========================================================================= + // DEFENSIVE passive – counter-venom (25 % proc on being hit) + // ========================================================================= + + private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { + + private val plugin get() = SpeedHG.instance + private val rng = Random() + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.defensive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.defensive.description") + + override fun onHitByEnemy(victim: Player, attacker: Player, event: EntityDamageByEntityEvent) { + if (rng.nextDouble() >= 0.25) return + + attacker.addPotionEffect(PotionEffect(PotionEffectType.POISON, 3 * 20, 0)) // Poison I, 3 s + victim.addPotionEffect(PotionEffect(PotionEffectType.SPEED, 3 * 20, 1)) // Speed II, 3 s + victim.playSound(victim.location, Sound.ENTITY_SLIME_HURT, 0.8f, 1.8f) + victim.sendActionBar(victim.trans("kits.rattlesnake.messages.venom_proc")) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt new file mode 100644 index 0000000..866f302 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt @@ -0,0 +1,286 @@ +package club.mcscrims.speedhg.kit.impl + +import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.kit.Kit +import club.mcscrims.speedhg.kit.Playstyle +import club.mcscrims.speedhg.kit.ability.AbilityResult +import club.mcscrims.speedhg.kit.ability.ActiveAbility +import club.mcscrims.speedhg.kit.ability.PassiveAbility +import club.mcscrims.speedhg.util.ItemBuilder +import club.mcscrims.speedhg.util.trans +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.Particle +import org.bukkit.Sound +import org.bukkit.attribute.Attribute +import org.bukkit.entity.Player +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.scheduler.BukkitRunnable +import org.bukkit.scheduler.BukkitTask +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * ## Voodoo + * + * | Playstyle | Active | Passive | + * |-------------|-----------------------------------------------------|---------------------------------------------| + * | AGGRESSIVE | Root enemy if HP < 50 % (5 s hold) | 20 % chance to apply Wither on hit | + * | DEFENSIVE | Curse nearby enemies for 15 s (Slow + MiningFatigue)| Speed + Regen while cursed enemies are nearby| + * + * ### Root mechanic (AGGRESSIVE) + * Zeros horizontal velocity every tick for 5 s + applies Slowness 127 so the + * player cannot walk even if the velocity reset misses a frame. + * + * ### Curse mechanic (DEFENSIVE) + * Cursed players are stored in [cursedExpiry] (UUID → expiry timestamp). + * A per-player repeating task (started in [DefensivePassive.onActivate]) checks + * every second: cleans expired curses, applies debuffs to still-cursed enemies, + * and grants Speed + Regen to the Voodoo player if at least one cursed enemy + * is within 10 blocks. + */ +class VoodooKit : Kit() { + + private val plugin get() = SpeedHG.instance + + override val id = "voodoo" + override val displayName: Component + get() = plugin.languageManager.getDefaultComponent("kits.voodoo.name", mapOf()) + override val lore: List + get() = plugin.languageManager.getDefaultRawMessageList("kits.voodoo.lore") + override val icon = Material.WITHER_ROSE + + /** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */ + internal val cursedExpiry: MutableMap = ConcurrentHashMap() + + // ── Cached ability instances ────────────────────────────────────────────── + private val aggressiveActive = AggressiveActive() + private val defensiveActive = DefensiveActive() + private val aggressivePassive = AggressivePassive() + private val defensivePassive = DefensivePassive() + + override fun getActiveAbility (playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressiveActive + Playstyle.DEFENSIVE -> defensiveActive + } + override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressivePassive + Playstyle.DEFENSIVE -> defensivePassive + } + + override val cachedItems = ConcurrentHashMap>() + + override fun giveItems(player: Player, playstyle: Playstyle) { + val (mat, active) = when (playstyle) { + Playstyle.AGGRESSIVE -> Material.WITHER_ROSE to aggressiveActive + Playstyle.DEFENSIVE -> Material.SOUL_TORCH to defensiveActive + } + val item = ItemBuilder(mat) + .name(active.name) + .lore(listOf(active.description)) + .build() + cachedItems[player.uniqueId] = listOf(item) + player.inventory.addItem(item) + } + + override fun onRemove(player: Player) { + cursedExpiry.remove(player.uniqueId) + cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } + } + + // ========================================================================= + // AGGRESSIVE active – root enemy if below 50 % HP + // ========================================================================= + + private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.description") + override val hitsRequired = 15 + override val triggerMaterial = Material.WITHER_ROSE + + override fun execute(player: Player): AbilityResult { + val target = player.getTargetEntity(6) as? Player + ?: return AbilityResult.ConditionNotMet("No player in line of sight!") + + if (!plugin.gameManager.alivePlayers.contains(target.uniqueId)) + return AbilityResult.ConditionNotMet("Target is not alive!") + + val maxHp = target.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.value ?: 20.0 + if (target.health > maxHp * 0.5) + return AbilityResult.ConditionNotMet("Target must be below 50 % health!") + + // ── Immobilise ──────────────────────────────────────────────────── + target.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 5 * 20, 127, false, false, true)) + target.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 5 * 20, 127, false, false, false)) + target.addPotionEffect(PotionEffect(PotionEffectType.GLOWING, 5 * 20, 0, false, false, false)) + + // Zero horizontal velocity every tick for 5 seconds (100 ticks) + object : BukkitRunnable() { + var ticks = 0 + override fun run() { + if (ticks++ >= 100 || !target.isOnline || + !plugin.gameManager.alivePlayers.contains(target.uniqueId)) + { + target.removePotionEffect(PotionEffectType.GLOWING) + cancel(); return + } + val v = target.velocity + target.velocity = v.clone().setX(0.0).setZ(0.0) + .let { if (it.y > 0.0) it.setY(0.0) else it } + } + }.runTaskTimer(plugin, 0L, 1L) + + player.playSound(player.location, Sound.ENTITY_WITHER_SHOOT, 1f, 0.5f) + target.playSound(target.location, Sound.ENTITY_WITHER_HURT, 1f, 0.5f) + target.world.spawnParticle(Particle.SOUL, target.location.clone().add(0.0, 1.0, 0.0), + 20, 0.4, 0.6, 0.4, 0.02) + + player.sendActionBar(player.trans("kits.voodoo.messages.root_activated")) + target.sendActionBar(target.trans("kits.voodoo.messages.root_received")) + + return AbilityResult.Success + } + + override fun onFullyCharged(player: Player) { + player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f) + player.sendActionBar(player.trans("kits.voodoo.messages.ability_charged")) + } + } + + // ========================================================================= + // DEFENSIVE active – curse nearby enemies + // ========================================================================= + + private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.description") + override val hitsRequired = 10 + override val triggerMaterial = Material.SOUL_TORCH + + override fun execute(player: Player): AbilityResult { + val targets = player.world + .getNearbyEntities(player.location, 8.0, 8.0, 8.0) + .filterIsInstance() + .filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + + if (targets.isEmpty()) + return AbilityResult.ConditionNotMet("No enemies within 8 blocks!") + + val expiry = System.currentTimeMillis() + 15_000L + targets.forEach { t -> + cursedExpiry[t.uniqueId] = expiry + t.addPotionEffect(PotionEffect(PotionEffectType.GLOWING, 15 * 20, 0, false, true, false)) + t.sendActionBar(t.trans("kits.voodoo.messages.curse_received")) + t.world.spawnParticle(Particle.SOUL_FIRE_FLAME, t.location.clone().add(0.0, 1.0, 0.0), + 10, 0.3, 0.4, 0.3, 0.05) + } + + player.playSound(player.location, Sound.ENTITY_WITHER_AMBIENT, 1f, 0.3f) + player.sendActionBar(player.trans("kits.voodoo.messages.curse_cast", + mapOf("count" to targets.size.toString()))) + + return AbilityResult.Success + } + + override fun onFullyCharged(player: Player) { + player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f) + player.sendActionBar(player.trans("kits.voodoo.messages.ability_charged")) + } + } + + // ========================================================================= + // AGGRESSIVE passive – 20 % Wither on hit + // ========================================================================= + + private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + private val rng = Random() + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.aggressive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.aggressive.description") + + override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { + if (rng.nextDouble() >= 0.20) return + victim.addPotionEffect(PotionEffect(PotionEffectType.WITHER, 3 * 20, 0)) + attacker.playSound(attacker.location, Sound.ENTITY_WITHER_AMBIENT, 0.5f, 1.8f) + victim.world.spawnParticle(Particle.SOUL, victim.location.clone().add(0.0, 1.0, 0.0), + 5, 0.2, 0.3, 0.2, 0.0) + } + } + + // ========================================================================= + // DEFENSIVE passive – buff while cursed enemies are nearby + // ========================================================================= + + private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { + + private val plugin get() = SpeedHG.instance + private val tasks: MutableMap = ConcurrentHashMap() + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.defensive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.defensive.description") + + override fun onActivate(player: Player) { + val task = object : BukkitRunnable() { + override fun run() { + if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) { + cancel(); return + } + tickPassive(player) + } + }.runTaskTimer(plugin, 0L, 20L) + tasks[player.uniqueId] = task + } + + override fun onDeactivate(player: Player) { + tasks.remove(player.uniqueId)?.cancel() + } + + private fun tickPassive(voodooPlayer: Player) { + val now = System.currentTimeMillis() + + // ── Expire stale curses ─────────────────────────────────────────── + cursedExpiry.entries.removeIf { (uuid, expiry) -> + if (now > expiry) { + Bukkit.getPlayer(uuid)?.removePotionEffect(PotionEffectType.GLOWING) + true + } else false + } + + // ── Apply debuffs to all still-cursed + alive players ───────────── + val cursedNearby = cursedExpiry.keys + .mapNotNull { Bukkit.getPlayer(it) } + .filter { it.isOnline && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + .onEach { cursed -> + cursed.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 30, 0)) + cursed.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 30, 0)) + } + .filter { it.location.distanceSquared(voodooPlayer.location) <= 100.0 } // ≤ 10 blocks + + // ── Buff voodoo player if cursed enemy nearby ───────────────────── + if (cursedNearby.isNotEmpty()) { + voodooPlayer.addPotionEffect(PotionEffect(PotionEffectType.SPEED, 30, 0)) + voodooPlayer.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 30, 0)) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt index 973d6e0..0c876ce 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt @@ -6,12 +6,14 @@ import club.mcscrims.speedhg.kit.KitManager import club.mcscrims.speedhg.kit.KitMetaData import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult +import club.mcscrims.speedhg.kit.impl.BlackPantherKit import club.mcscrims.speedhg.kit.impl.IceMageKit import club.mcscrims.speedhg.kit.impl.VenomKit import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.Material import org.bukkit.NamespacedKey +import org.bukkit.Particle import org.bukkit.Sound import org.bukkit.block.Block import org.bukkit.entity.* @@ -22,10 +24,13 @@ import org.bukkit.event.Listener import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.entity.EntityDamageByEntityEvent import org.bukkit.event.entity.EntityExplodeEvent +import org.bukkit.event.entity.PlayerDeathEvent import org.bukkit.event.entity.ProjectileHitEvent import org.bukkit.event.entity.ProjectileLaunchEvent import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerItemBreakEvent import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.event.player.PlayerToggleSneakEvent import org.bukkit.inventory.EquipmentSlot import org.bukkit.persistence.PersistentDataType import org.bukkit.potion.PotionEffect @@ -191,10 +196,11 @@ class KitEventDispatcher( } // ========================================================================= - // IceMage Listener + // Kit Listeners // ========================================================================= - private val iceMageKey = NamespacedKey( plugin, "icemage_snowball" ) + internal val iceMageKey = NamespacedKey( plugin, "icemage_snowball" ) + internal val blackPantherPushKey = NamespacedKey( plugin, BlackPantherKit.PUSH_PROJECTILE_KEY ) @EventHandler fun onSnowballThrow( @@ -250,10 +256,6 @@ class KitEventDispatcher( } } - // ========================================================================= - // Venom Listener - // ========================================================================= - @EventHandler fun onProjectileHit( event: ProjectileHitEvent @@ -264,6 +266,20 @@ class KitEventDispatcher( val victim = event.hitEntity as? Player ?: return val projectile = event.entity + val pdc = projectile.persistentDataContainer + if (pdc.has( blackPantherPushKey, PersistentDataType.BYTE )) + { + val shooter = projectile.shooter as? Player ?: run { projectile.remove(); return } + if ( victim != shooter && + plugin.gameManager.alivePlayers.contains( victim.uniqueId )) + { + victim.damage( 4.0, shooter ) + victim.world.spawnParticle( Particle.CRIT, victim.location.clone().add( 0.0, 1.0, 0.0 ), 8 ) + } + projectile.remove() + return + } + if (kitManager.getSelectedKit( victim ) !is VenomKit ) return if (kitManager.getSelectedPlaystyle( victim ) != Playstyle.DEFENSIVE ) return @@ -287,10 +303,6 @@ class KitEventDispatcher( } } - // ========================================================================= - // Gladiator Listener - // ========================================================================= - @EventHandler fun onBlockBreak( event: BlockBreakEvent @@ -326,6 +338,39 @@ class KitEventDispatcher( .forEach { block -> changeGladiatorBlock( event, block ) } } + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + fun onPlayerKill( + event: PlayerDeathEvent + ) { + if ( !isIngame() ) return + val victim = event.entity + val killer = victim.killer ?: return + + val kit = kitManager.getSelectedKit( killer ) ?: return + val playstyle = kitManager.getSelectedPlaystyle( killer ) + + kit.onKillEnemy( killer, victim ) + kit.getPassiveAbility( playstyle ).onKillEnemy( killer, victim ) + } + + @EventHandler + fun onPlayerToggleSneak( + event: PlayerToggleSneakEvent + ) { + if ( !isIngame() ) return + val kit = kitManager.getSelectedKit( event.player ) ?: return + kit.onToggleSneak( event.player, event.isSneaking ) + } + + @EventHandler + fun onPlayerItemBreak( + event: PlayerItemBreakEvent + ) { + if ( !isIngame() ) return + val kit = kitManager.getSelectedKit( event.player ) ?: return + kit.onItemBreak( event.player, event.brokenItem ) + } + // ========================================================================= // Helpers // ========================================================================= diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index 8c25af6..7616e86 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -176,4 +176,88 @@ kits: wither_beam: 'You have summoned your deafening beam!' shield_activate: 'Your shield of darkness has been activated!' shield_break: 'Your shield of darkness has broken!' - ability_charged: 'Your ability has been recharged' \ No newline at end of file + ability_charged: 'Your ability has been recharged' + rattlesnake: + name: 'Rattlesnake' + lore: + - ' ' + - 'AGGRESSIVE: Sneak-charged pounce' + - 'DEFENSIVE: 25% counter-venom on hit' + items: + pounce: + name: 'Pounce' + description: 'Sneak + right-click to lunge at an enemy' + passive: + aggressive: + name: 'Pounce Strike' + description: 'Apply Poison II on pounce-hit (x3 after Feast)' + defensive: + name: 'Counter Venom' + description: '25% chance to reflect Poison when hit' + messages: + pounce_hit: 'Pounce hit target(s)! Poison II applied!' + pounce_miss: 'Pounce missed! Nearby enemies were disoriented.' + venom_proc: 'Counter Venom triggered!' + armorer: + name: 'Armorer' + lore: + - ' ' + - 'Upgrade armor every 2 kills.' + - 'Broken armor is auto-replaced.' + passive: + aggressive: + name: 'Battle High' + description: 'Gain Strength I for 5 s after each kill' + defensive: + name: 'Fortified' + description: 'Iron / Diamond armor receives Protection I' + messages: + armor_upgraded: 'Armor upgraded to tier !' + armor_replaced: 'Broken armor replaced automatically.' + voodoo: + name: 'Voodoo' + lore: + - ' ' + - 'AGGRESSIVE: Wither on hit + root below 50%' + - 'DEFENSIVE: Curse enemies for buffs' + items: + root: + name: 'Root' + description: 'Root an enemy below 50% HP for 5 seconds' + curse: + name: 'Curse' + description: 'Curse nearby enemies for 15 seconds' + passive: + aggressive: + name: 'Wither Touch' + description: '20% chance to apply Wither on melee hit' + defensive: + name: 'Dark Pact' + description: 'Speed + Regen while cursed enemies are nearby' + messages: + root_activated: 'Enemy rooted for 5 seconds!' + root_received: 'You are rooted!' + curse_cast: 'Cursed enemy(s) for 15 seconds!' + curse_received: 'You have been cursed by a Voodoo player!' + ability_charged: 'Ability recharged!' + blackpanther: + name: 'Black Panther' + lore: + - ' ' + - 'AGGRESSIVE: Push + Vibranium Fists' + - 'DEFENSIVE: Wakanda Forever! fall-pounce' + items: + push: + name: 'Vibranium Pulse' + description: 'Knock back all nearby enemies and activate Fist Mode' + passive: + aggressive: + name: 'Vibranium Fists' + description: '6.5 bare-hand damage for 12 s after Push' + defensive: + name: 'Wakanda Forever!' + description: 'Fall from 3+ blocks onto an enemy for AOE damage + crater' + messages: + fist_mode_active: '⚡ Vibranium Fists active for 12 seconds!' + wakanda_impact: 'Wakanda Forever! Hit enemy(s)!' + ability_charged: 'Ability recharged!' \ No newline at end of file