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