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.
This commit is contained in:
TDSTOS
2026-03-28 17:23:39 +01:00
parent 84be2a30bc
commit bab703601e
9 changed files with 1187 additions and 11 deletions

View File

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

View File

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

View File

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

View File

@@ -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<String>
get() = plugin.languageManager.getDefaultRawMessageList("kits.armorer.lore")
override val icon = Material.IRON_CHESTPLATE
// ── Kill tracking ─────────────────────────────────────────────────────────
internal val killCounts: MutableMap<UUID, Int> = 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<UUID, List<ItemStack>>()
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
}
}

View File

@@ -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<String>
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<UUID, Long> = 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<UUID, List<ItemStack>>()
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<Player>()
.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<Player>()
.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
}
}
}

View File

@@ -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 (310 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<String>
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<UUID, Long> = ConcurrentHashMap()
internal val pouncingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
internal val lastPounceUse: MutableMap<UUID, Long> = 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<UUID, List<ItemStack>>()
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<Player>()
.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<Player>()
.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<Player>()
.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"))
}
}
}

View File

@@ -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<String>
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<UUID, Long> = 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<UUID, List<ItemStack>>()
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<Player>()
.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<UUID, BukkitTask> = 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))
}
}
}
}

View File

@@ -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
// =========================================================================

View File

@@ -176,4 +176,88 @@ kits:
wither_beam: '<gray>You have summoned your deafening beam!</gray>'
shield_activate: '<gray>Your shield of darkness has been activated!</gray>'
shield_break: '<red>Your shield of darkness has broken!</red>'
ability_charged: '<yellow>Your ability has been recharged</yellow>'
ability_charged: '<yellow>Your ability has been recharged</yellow>'
rattlesnake:
name: '<gradient:green:lime><bold>Rattlesnake</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE: Sneak-charged pounce'
- 'DEFENSIVE: 25% counter-venom on hit'
items:
pounce:
name: '<green>Pounce</green>'
description: 'Sneak + right-click to lunge at an enemy'
passive:
aggressive:
name: '<green>Pounce Strike</green>'
description: 'Apply Poison II on pounce-hit (x3 after Feast)'
defensive:
name: '<green>Counter Venom</green>'
description: '25% chance to reflect Poison when hit'
messages:
pounce_hit: '<green>Pounce hit <count> target(s)! Poison II applied!</green>'
pounce_miss: '<red>Pounce missed! Nearby enemies were disoriented.</red>'
venom_proc: '<green>Counter Venom triggered!</green>'
armorer:
name: '<gradient:gray:white><bold>Armorer</bold></gradient>'
lore:
- ' '
- 'Upgrade armor every 2 kills.'
- 'Broken armor is auto-replaced.'
passive:
aggressive:
name: '<gold>Battle High</gold>'
description: 'Gain Strength I for 5 s after each kill'
defensive:
name: '<aqua>Fortified</aqua>'
description: 'Iron / Diamond armor receives Protection I'
messages:
armor_upgraded: '<gold>Armor upgraded to tier <tier>!</gold>'
armor_replaced: '<yellow>Broken armor replaced automatically.</yellow>'
voodoo:
name: '<gradient:dark_purple:light_purple><bold>Voodoo</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE: Wither on hit + root below 50%'
- 'DEFENSIVE: Curse enemies for buffs'
items:
root:
name: '<dark_purple>Root</dark_purple>'
description: 'Root an enemy below 50% HP for 5 seconds'
curse:
name: '<light_purple>Curse</light_purple>'
description: 'Curse nearby enemies for 15 seconds'
passive:
aggressive:
name: '<dark_purple>Wither Touch</dark_purple>'
description: '20% chance to apply Wither on melee hit'
defensive:
name: '<light_purple>Dark Pact</light_purple>'
description: 'Speed + Regen while cursed enemies are nearby'
messages:
root_activated: '<dark_purple>Enemy rooted for 5 seconds!</dark_purple>'
root_received: '<red>You are rooted!</red>'
curse_cast: '<light_purple>Cursed <count> enemy(s) for 15 seconds!</light_purple>'
curse_received: '<red>You have been cursed by a Voodoo player!</red>'
ability_charged: '<yellow>Ability recharged!</yellow>'
blackpanther:
name: '<gradient:dark_gray:white><bold>Black Panther</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE: Push + Vibranium Fists'
- 'DEFENSIVE: Wakanda Forever! fall-pounce'
items:
push:
name: '<gray>Vibranium Pulse</gray>'
description: 'Knock back all nearby enemies and activate Fist Mode'
passive:
aggressive:
name: '<gray>Vibranium Fists</gray>'
description: '6.5 bare-hand damage for 12 s after Push'
defensive:
name: '<white>Wakanda Forever!</white>'
description: 'Fall from 3+ blocks onto an enemy for AOE damage + crater'
messages:
fist_mode_active: '<gray>⚡ Vibranium Fists active for 12 seconds!</gray>'
wakanda_impact: '<white>Wakanda Forever! Hit <count> enemy(s)!</white>'
ability_charged: '<yellow>Ability recharged!</yellow>'