Add new perks and register them

Introduce five new perks (Berserker, Evasion, Gourmet, Last Stand, Momentum) with implementations and translations. SpeedHG.kt was updated to import and register these perks. en_US.yml was extended with display names, lore and messages for each perk. Brief mechanics:
- Berserker: increased melee damage below a health threshold.
- Evasion: chance to dodge projectiles (arrow/snowball/egg) with visual/sound feedback.
- Gourmet: grants short Regeneration I + Speed I when consuming mushroom stew.
- Last Stand: grants Resistance II + Absorption I when damage drops health below threshold (with cooldown).
- Momentum: awards Speed I after sprinting continuously for a configured duration; removed on combat or stopping sprint.
This commit is contained in:
TDSTOS
2026-04-12 19:32:32 +02:00
parent 72cc136658
commit 4b379b9121
7 changed files with 630 additions and 0 deletions

View File

@@ -29,10 +29,15 @@ import club.mcscrims.speedhg.listener.SoupListener
import club.mcscrims.speedhg.listener.StatsListener import club.mcscrims.speedhg.listener.StatsListener
import club.mcscrims.speedhg.perk.PerkManager import club.mcscrims.speedhg.perk.PerkManager
import club.mcscrims.speedhg.perk.impl.AdrenalinePerk import club.mcscrims.speedhg.perk.impl.AdrenalinePerk
import club.mcscrims.speedhg.perk.impl.BerserkerPerk
import club.mcscrims.speedhg.perk.impl.BloodlustPerk import club.mcscrims.speedhg.perk.impl.BloodlustPerk
import club.mcscrims.speedhg.perk.impl.EnderbluePerk import club.mcscrims.speedhg.perk.impl.EnderbluePerk
import club.mcscrims.speedhg.perk.impl.EvasionPerk
import club.mcscrims.speedhg.perk.impl.FeatherweightPerk import club.mcscrims.speedhg.perk.impl.FeatherweightPerk
import club.mcscrims.speedhg.perk.impl.GhostPerk import club.mcscrims.speedhg.perk.impl.GhostPerk
import club.mcscrims.speedhg.perk.impl.GourmetPerk
import club.mcscrims.speedhg.perk.impl.LastStandPerk
import club.mcscrims.speedhg.perk.impl.MomentumPerk
import club.mcscrims.speedhg.perk.impl.OraclePerk import club.mcscrims.speedhg.perk.impl.OraclePerk
import club.mcscrims.speedhg.perk.impl.PyromaniacPerk import club.mcscrims.speedhg.perk.impl.PyromaniacPerk
import club.mcscrims.speedhg.perk.impl.ScavengerPerk import club.mcscrims.speedhg.perk.impl.ScavengerPerk
@@ -247,10 +252,15 @@ class SpeedHG : JavaPlugin() {
private fun registerPerks() private fun registerPerks()
{ {
perkManager.registerPerk( AdrenalinePerk() ) perkManager.registerPerk( AdrenalinePerk() )
perkManager.registerPerk( BerserkerPerk() )
perkManager.registerPerk( BloodlustPerk() ) perkManager.registerPerk( BloodlustPerk() )
perkManager.registerPerk( EnderbluePerk() ) perkManager.registerPerk( EnderbluePerk() )
perkManager.registerPerk( EvasionPerk() )
perkManager.registerPerk( FeatherweightPerk() ) perkManager.registerPerk( FeatherweightPerk() )
perkManager.registerPerk( GhostPerk() ) perkManager.registerPerk( GhostPerk() )
perkManager.registerPerk( GourmetPerk() )
perkManager.registerPerk( LastStandPerk() )
perkManager.registerPerk( MomentumPerk() )
perkManager.registerPerk( OraclePerk() ) perkManager.registerPerk( OraclePerk() )
perkManager.registerPerk( PyromaniacPerk() ) perkManager.registerPerk( PyromaniacPerk() )
perkManager.registerPerk( ScavengerPerk() ) perkManager.registerPerk( ScavengerPerk() )

View File

@@ -0,0 +1,83 @@
package club.mcscrims.speedhg.perk.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.perk.Perk
import net.kyori.adventure.text.Component
import org.bukkit.Material
import org.bukkit.Particle
import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
/**
* ## Berserker
*
* The player deals increased melee damage while their own health is low.
*
* ### Mechanic Summary
* | Property | Default | Config key |
* |--------------------|----------------|-------------------------------|
* | Health threshold | `8.0` HP (4♥) | `berserker_threshold` |
* | Damage multiplier | `1.15` (15%) | `berserker_damage_multiplier` |
*
* ### Technical Notes
* The damage boost is applied by scaling `event.damage` directly inside
* [onHitEnemy], which runs at MONITOR priority after all other modifiers.
* `player.health` is read at the moment of the hit — this is the current
* health **before** the attacker receives any counter-damage, making it the
* correct value for a "low-health" check on the aggressor.
*/
class BerserkerPerk : Perk() {
private val plugin get() = SpeedHG.instance
override val id = "berserker"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "perks.berserker.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "perks.berserker.lore" )
override val icon = Material.IRON_AXE
// ── Defaults ─────────────────────────────────────────────────────────────
companion object {
const val DEFAULT_HP_THRESHOLD = 8.0
const val DEFAULT_DAMAGE_MULTIPLIER = 1.15
}
// ── Live config accessors ─────────────────────────────────────────────────
private val extras
get() = plugin.customGameManager.settings.kits.kits[ id ]
private val hpThreshold: Double
get() = extras?.getDouble( "berserker_threshold" ) ?: DEFAULT_HP_THRESHOLD
private val damageMultiplier: Double
get() = extras?.getDouble( "berserker_damage_multiplier" ) ?: DEFAULT_DAMAGE_MULTIPLIER
// ── Hook ──────────────────────────────────────────────────────────────────
override fun onHitEnemy(
attacker: Player,
victim: Player,
event: EntityDamageByEntityEvent
) {
if ( attacker.health > hpThreshold ) return
event.damage = event.damage * damageMultiplier
// Subtle visual feedback — small crit burst on the victim.
victim.world.spawnParticle(
Particle.CRIT,
victim.location.clone().add( 0.0, 1.0, 0.0 ),
6,
0.2, 0.2, 0.2,
0.1
)
}
}

View File

@@ -0,0 +1,131 @@
package club.mcscrims.speedhg.perk.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.perk.Perk
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Material
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.entity.Arrow
import org.bukkit.entity.Egg
import org.bukkit.entity.Player
import org.bukkit.entity.Snowball
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.random.Random
/**
* ## Evasion
*
* Grants the player a percentage chance to completely dodge incoming projectiles
* (arrows, snowballs, eggs). On a successful dodge the projectile is despawned,
* the damage event is cancelled, and a visual + audio cue is played.
*
* ### Mechanic Summary
* | Property | Default | Config key |
* |----------------|-----------|-----------------------|
* | Dodge chance | `15.0` % | `evasion_dodge_chance`|
*
* ### Technical Notes
* Projectile hits are **not** exposed through [PerkEventDispatcher], so this perk
* self-registers as a [Listener]. It hooks into [EntityDamageByEntityEvent] at
* HIGH priority (before damage is applied) and checks whether the damager is a
* tracked projectile type. Cancelling at HIGH ensures no downstream listeners
* apply health changes.
*
* The sound `ENTITY_ENDER_DRAGON_FLAP` is used as the "whoosh" effect to give
* the dodge a dramatic, distinct feel that is easy to recognise mid-fight.
*/
class EvasionPerk : Perk(), Listener {
private val plugin get() = SpeedHG.instance
override val id = "evasion"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "perks.evasion.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "perks.evasion.lore" )
override val icon = Material.LEATHER_BOOTS
/** UUIDs for which this perk is currently active. */
private val activePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
// ── Defaults ─────────────────────────────────────────────────────────────
companion object {
const val DEFAULT_DODGE_CHANCE = 15.0
}
// ── Live config accessor ──────────────────────────────────────────────────
private val extras
get() = plugin.customGameManager.settings.kits.kits[ id ]
private val dodgeChance: Double
get() = extras?.getDouble( "evasion_dodge_chance" ) ?: DEFAULT_DODGE_CHANCE
// ── Lifecycle ─────────────────────────────────────────────────────────────
override fun onActivate(
player: Player
) {
if ( activePlayers.isEmpty() ) {
plugin.server.pluginManager.registerEvents( this, plugin )
}
activePlayers += player.uniqueId
}
override fun onDeactivate(
player: Player
) {
activePlayers -= player.uniqueId
}
// ── Projectile dodge listener ─────────────────────────────────────────────
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
fun onProjectileDamage(
event: EntityDamageByEntityEvent
) {
val victim = event.entity as? Player ?: return
if ( !activePlayers.contains( victim.uniqueId ) ) return
val projectile = when( event.damager )
{
is Arrow -> event.damager
is Snowball -> event.damager
is Egg -> event.damager
else -> return
}
val roll = Random.nextDouble( 0.0, 100.0 )
if ( roll >= dodgeChance ) return
// ── Successful dodge ──────────────────────────────────────────────────
event.isCancelled = true
projectile.remove()
victim.sendActionBar( victim.trans( "perks.evasion.message" ) )
victim.world.spawnParticle(
Particle.SWEEP_ATTACK,
victim.location.clone().add( 0.0, 1.0, 0.0 ),
5,
0.3, 0.3, 0.3,
0.0
)
victim.playSound(
victim.location,
Sound.ENTITY_ENDER_DRAGON_FLAP,
0.5f,
1.8f
)
}
}

View File

@@ -0,0 +1,115 @@
package club.mcscrims.speedhg.perk.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.perk.Perk
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Material
import org.bukkit.Sound
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.inventory.EquipmentSlot
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
/**
* ## Gourmet
*
* Every time the player consumes a Mushroom Stew (soup), they gain
* Regeneration I and Speed I for a short duration.
*
* ### Mechanic Summary
* | Property | Default | Config key |
* |-------------------|---------------|--------------------------|
* | Effect duration | `40` (2 s) | `gourmet_duration_ticks` |
*
* ### Technical Notes
* Soup consumption is detected via `PlayerInteractEvent` on RIGHT_CLICK with
* `MUSHROOM_STEW` in hand — the standard SpeedHG soup healing pattern. The
* event is checked at MONITOR priority and only fired for the main hand to
* avoid double-firing.
*
* Since [PerkEventDispatcher] does not expose a soup-specific hook, this perk
* self-registers as a [Listener] during [onActivate] and unregisters by
* cancelling its handler reference in [onDeactivate] via a UUID guard set.
*/
class GourmetPerk : Perk(), Listener {
private val plugin get() = SpeedHG.instance
override val id = "gourmet"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "perks.gourmet.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "perks.gourmet.lore" )
override val icon = Material.MUSHROOM_STEW
/** UUIDs for which this perk is currently active. */
private val activePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
// ── Defaults ─────────────────────────────────────────────────────────────
companion object {
const val DEFAULT_DURATION_TICKS = 2 * 20
}
// ── Live config accessor ──────────────────────────────────────────────────
private val extras
get() = plugin.customGameManager.settings.kits.kits[ id ]
private val durationTicks: Int
get() = extras?.getInt( "gourmet_duration_ticks" ) ?: DEFAULT_DURATION_TICKS
// ── Lifecycle ─────────────────────────────────────────────────────────────
override fun onActivate(
player: Player
) {
if ( activePlayers.isEmpty() ) {
// Register the listener the first time any player activates this perk.
plugin.server.pluginManager.registerEvents( this, plugin )
}
activePlayers += player.uniqueId
}
override fun onDeactivate(
player: Player
) {
activePlayers -= player.uniqueId
}
// ── Soup listener ─────────────────────────────────────────────────────────
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
fun onSoupConsume(
event: PlayerInteractEvent
) {
if ( event.hand != EquipmentSlot.HAND ) return
val player = event.player
if ( !activePlayers.contains( player.uniqueId ) ) return
val item = event.item ?: return
if ( item.type != Material.MUSHROOM_STEW ) return
// Only fire when the item is actually going to be consumed — i.e. when
// the player is not at full hunger (vanilla consumption gate).
if ( player.foodLevel >= 20 ) return
val dur = durationTicks
player.addPotionEffect( PotionEffect( PotionEffectType.REGENERATION, dur, 0, false, true, true ) )
player.addPotionEffect( PotionEffect( PotionEffectType.SPEED, dur, 0, false, true, true ) )
player.sendActionBar( player.trans( "perks.gourmet.message" ) )
player.playSound( player.location, Sound.ENTITY_GENERIC_EAT, 0.6f, 1.2f )
}
}

View File

@@ -0,0 +1,106 @@
package club.mcscrims.speedhg.perk.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.perk.Perk
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Material
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
/**
* ## Last Stand
*
* When the player takes damage that drops their health below the configured
* threshold, they are granted Resistance II and Absorption I for a short window.
*
* ### Mechanic Summary
* | Property | Default | Config key |
* |--------------------|----------------------|-------------------------|
* | Cooldown | `60_000` ms | `last_stand_cooldown_ms`|
* | Health threshold | `6.0` HP (3 hearts) | `last_stand_threshold` |
* | Effect duration | `80` ticks (4 s) | `last_stand_duration` |
*
* ### Technical Notes
* Uses [onPostDamage] so that `player.health - event.finalDamage` reflects the
* true health after the hit. This is more accurate than reading `player.health`
* inside [onHitByEnemy], where the health value is still pre-damage.
*/
class LastStandPerk : Perk() {
private val plugin get() = SpeedHG.instance
override val id = "last_stand"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "perks.last_stand.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "perks.last_stand.lore" )
override val icon = Material.TOTEM_OF_UNDYING
/** UUID → timestamp (ms) of the last proc. */
private val lastProc: MutableMap<UUID, Long> = ConcurrentHashMap()
// ── Defaults ─────────────────────────────────────────────────────────────
companion object {
const val DEFAULT_COOLDOWN_MS = 60_000L
const val DEFAULT_HP_THRESHOLD = 6.0
const val DEFAULT_DURATION_TICKS = 4 * 20
}
// ── Live config accessors ─────────────────────────────────────────────────
private val extras
get() = plugin.customGameManager.settings.kits.kits[ id ]
private val cooldownMs: Long
get() = extras?.getLong( "last_stand_cooldown_ms" ) ?: DEFAULT_COOLDOWN_MS
private val hpThreshold: Double
get() = extras?.getDouble( "last_stand_threshold" ) ?: DEFAULT_HP_THRESHOLD
private val durationTicks: Int
get() = extras?.getInt( "last_stand_duration" ) ?: DEFAULT_DURATION_TICKS
// ── Lifecycle ─────────────────────────────────────────────────────────────
override fun onDeactivate(
player: Player
) {
lastProc.remove( player.uniqueId )
}
// ── Hook ──────────────────────────────────────────────────────────────────
override fun onPostDamage(
player: Player,
event: EntityDamageEvent
) {
val healthAfter = player.health - event.finalDamage
if ( healthAfter >= hpThreshold ) return
if ( healthAfter <= 0.0 ) return
val now = System.currentTimeMillis()
val last = lastProc[ player.uniqueId ] ?: 0L
if ( now - last < cooldownMs ) return
lastProc[ player.uniqueId ] = now
val dur = durationTicks
player.addPotionEffect( PotionEffect( PotionEffectType.RESISTANCE, dur, 1, false, true, true ) )
player.addPotionEffect( PotionEffect( PotionEffectType.ABSORPTION, dur, 0, false, true, true ) )
player.sendActionBar( player.trans( "perks.last_stand.message" ) )
player.world.spawnParticle( Particle.TOTEM_OF_UNDYING, player.location.clone().add( 0.0, 1.0, 0.0 ), 20 )
player.playSound( player.location, Sound.ITEM_TOTEM_USE, 0.6f, 1.4f )
}
}

View File

@@ -0,0 +1,145 @@
package club.mcscrims.speedhg.perk.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.perk.Perk
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.scheduler.BukkitRunnable
import org.bukkit.scheduler.BukkitTask
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
/**
* ## Momentum
*
* If a player sprints continuously for the configured duration without
* dealing or receiving damage, they are granted Speed I. The buff is
* removed immediately upon taking/dealing damage or stopping their sprint.
*
* ### Mechanic Summary
* | Property | Default | Config key |
* |-------------------------|---------------|-----------------------------|
* | Sprint ticks required | `80` (4 s) | `momentum_sprint_ticks` |
*
* ### Technical Notes
* A repeating [BukkitRunnable] started in [onActivate] polls `player.isSprinting`
* every tick. The sprint streak is tracked as a tick counter in [sprintTicks].
* Both [onHitEnemy] and [onHitByEnemy] clear the streak and strip the Speed buff.
*/
class MomentumPerk : Perk() {
private val plugin get() = SpeedHG.instance
override val id = "momentum"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "perks.momentum.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "perks.momentum.lore" )
override val icon = Material.FEATHER
/** UUID → accumulated sprint ticks since last reset. */
private val sprintTicks: MutableMap<UUID, Int> = ConcurrentHashMap()
/** UUID → whether the Speed buff is currently active for this player. */
private val buffActive: MutableMap<UUID, Boolean> = ConcurrentHashMap()
/** UUID → the polling task. */
private val pollTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
// ── Defaults ─────────────────────────────────────────────────────────────
companion object {
const val DEFAULT_SPRINT_TICKS = 4 * 20
}
// ── Live config accessor ──────────────────────────────────────────────────
private val extras
get() = plugin.customGameManager.settings.kits.kits[ id ]
private val sprintTicksRequired: Int
get() = extras?.getInt( "momentum_sprint_ticks" ) ?: DEFAULT_SPRINT_TICKS
// ── Lifecycle ─────────────────────────────────────────────────────────────
override fun onActivate(
player: Player
) {
sprintTicks[ player.uniqueId ] = 0
buffActive[ player.uniqueId ] = false
val task = object : BukkitRunnable() {
override fun run() {
if ( !player.isOnline ) { cancel(); return }
if ( player.isSprinting ) {
val ticks = ( sprintTicks[ player.uniqueId ] ?: 0 ) + 1
sprintTicks[ player.uniqueId ] = ticks
if ( ticks >= sprintTicksRequired && buffActive[ player.uniqueId ] == false ) {
applyBuff( player )
}
} else {
resetStreak( player )
}
}
}.runTaskTimer( plugin, 0L, 1L )
pollTasks[ player.uniqueId ] = task
}
override fun onDeactivate(
player: Player
) {
pollTasks.remove( player.uniqueId )?.cancel()
sprintTicks.remove( player.uniqueId )
buffActive.remove( player.uniqueId )
player.removePotionEffect( PotionEffectType.SPEED )
}
// ── Combat hooks (streak breakers) ────────────────────────────────────────
override fun onHitEnemy(
attacker: Player,
victim: Player,
event: EntityDamageByEntityEvent
) {
resetStreak( attacker )
}
override fun onHitByEnemy(
victim: Player,
attacker: Player,
event: EntityDamageByEntityEvent
) {
resetStreak( victim )
}
// ── Internal helpers ──────────────────────────────────────────────────────
private fun applyBuff(
player: Player
) {
buffActive[ player.uniqueId ] = true
// Infinite duration — we manage removal manually.
player.addPotionEffect( PotionEffect( PotionEffectType.SPEED, Int.MAX_VALUE, 0, false, true, true ) )
player.sendActionBar( player.trans( "perks.momentum.message" ) )
}
private fun resetStreak(
player: Player
) {
sprintTicks[ player.uniqueId ] = 0
if ( buffActive[ player.uniqueId ] == true ) {
buffActive[ player.uniqueId ] = false
player.removePotionEffect( PotionEffectType.SPEED )
}
}
}

View File

@@ -306,6 +306,46 @@ perks:
- '<gold>Golden Apple</gold> <gray>at the corpse.</gray>' - '<gold>Golden Apple</gold> <gray>at the corpse.</gray>'
message: '<gold>🍎 Scavenged a Golden Apple!</gold>' message: '<gold>🍎 Scavenged a Golden Apple!</gold>'
last_stand:
name: '<gradient:white:red><bold>Last Stand</bold></gradient>'
lore:
- ' '
- '<gray>Taking damage below <red>3 hearts</red></gray>'
- '<gray>grants <white>Resistance II</white> + <yellow>Absorption I</yellow> (4s).</gray>'
- '<dark_gray>(60 s cooldown)</dark_gray>'
message: '<red>🛡 Last Stand! <white>Resistance II</white> + <yellow>Absorption I</yellow> active!</red>'
momentum:
name: '<gradient:aqua:white><bold>Momentum</bold></gradient>'
lore:
- ' '
- '<gray>Sprint for 4 seconds without</gray>'
- '<gray>combat to gain <yellow>Speed I</yellow>.</gray>'
message: '<yellow>💨 Momentum! <white>Speed I</white> active!</yellow>'
gourmet:
name: '<gradient:green:gold><bold>Gourmet</bold></gradient>'
lore:
- ' '
- '<gray>Consuming soup grants</gray>'
- '<green>Regen I</green> <gray>+</gray> <yellow>Speed I</yellow> <gray>for 2 s.</gray>'
message: '<green>🍲 Gourmet! <white>Regen I</white> + <yellow>Speed I</yellow> for 2 seconds!</green>'
berserker:
name: '<gradient:dark_red:red><bold>Berserker</bold></gradient>'
lore:
- ' '
- '<gray>Below <red>4 hearts</red>: deal</gray>'
- '<red>15% more</red> <gray>melee damage.</gray>'
evasion:
name: '<gradient:aqua:dark_aqua><bold>Evasion</bold></gradient>'
lore:
- ' '
- '<gray>15% chance to dodge</gray>'
- '<gray>incoming projectiles.</gray>'
message: '<aqua>💨 Evaded! The projectile missed you!</aqua>'
kits: kits:
needed_hits: '<gold>⚡ Ability: <white><current>/<required> Hits</white></gold>' needed_hits: '<gold>⚡ Ability: <white><current>/<required> Hits</white></gold>'
ability_charged: '<green><bold>⚡ ABILITY READY!</bold></green>' ability_charged: '<green><bold>⚡ ABILITY READY!</bold></green>'