From 4b379b9121675c999a0feb0740e8b1af9c2a2a54 Mon Sep 17 00:00:00 2001 From: TDSTOS Date: Sun, 12 Apr 2026 19:32:32 +0200 Subject: [PATCH] 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. --- .../kotlin/club/mcscrims/speedhg/SpeedHG.kt | 10 ++ .../speedhg/perk/impl/BerserkerPerk.kt | 83 ++++++++++ .../mcscrims/speedhg/perk/impl/EvasionPerk.kt | 131 ++++++++++++++++ .../mcscrims/speedhg/perk/impl/GourmetPerk.kt | 115 ++++++++++++++ .../speedhg/perk/impl/LastStandPerk.kt | 106 +++++++++++++ .../speedhg/perk/impl/MomentumPerk.kt | 145 ++++++++++++++++++ src/main/resources/languages/en_US.yml | 40 +++++ 7 files changed, 630 insertions(+) create mode 100644 src/main/kotlin/club/mcscrims/speedhg/perk/impl/BerserkerPerk.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/perk/impl/EvasionPerk.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/perk/impl/GourmetPerk.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/perk/impl/LastStandPerk.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/perk/impl/MomentumPerk.kt diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index 65c6e4e..dd5239c 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -29,10 +29,15 @@ import club.mcscrims.speedhg.listener.SoupListener import club.mcscrims.speedhg.listener.StatsListener import club.mcscrims.speedhg.perk.PerkManager 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.EnderbluePerk +import club.mcscrims.speedhg.perk.impl.EvasionPerk import club.mcscrims.speedhg.perk.impl.FeatherweightPerk 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.PyromaniacPerk import club.mcscrims.speedhg.perk.impl.ScavengerPerk @@ -247,10 +252,15 @@ class SpeedHG : JavaPlugin() { private fun registerPerks() { perkManager.registerPerk( AdrenalinePerk() ) + perkManager.registerPerk( BerserkerPerk() ) perkManager.registerPerk( BloodlustPerk() ) perkManager.registerPerk( EnderbluePerk() ) + perkManager.registerPerk( EvasionPerk() ) perkManager.registerPerk( FeatherweightPerk() ) perkManager.registerPerk( GhostPerk() ) + perkManager.registerPerk( GourmetPerk() ) + perkManager.registerPerk( LastStandPerk() ) + perkManager.registerPerk( MomentumPerk() ) perkManager.registerPerk( OraclePerk() ) perkManager.registerPerk( PyromaniacPerk() ) perkManager.registerPerk( ScavengerPerk() ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/perk/impl/BerserkerPerk.kt b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/BerserkerPerk.kt new file mode 100644 index 0000000..869dfec --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/BerserkerPerk.kt @@ -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 + 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 + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/perk/impl/EvasionPerk.kt b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/EvasionPerk.kt new file mode 100644 index 0000000..fcc8a47 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/EvasionPerk.kt @@ -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 + 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 = 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 + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/perk/impl/GourmetPerk.kt b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/GourmetPerk.kt new file mode 100644 index 0000000..f722336 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/GourmetPerk.kt @@ -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 + 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 = 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 ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/perk/impl/LastStandPerk.kt b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/LastStandPerk.kt new file mode 100644 index 0000000..ce0be13 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/LastStandPerk.kt @@ -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 + 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 = 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 ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/perk/impl/MomentumPerk.kt b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/MomentumPerk.kt new file mode 100644 index 0000000..b724392 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/perk/impl/MomentumPerk.kt @@ -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 + get() = plugin.languageManager.getDefaultRawMessageList( "perks.momentum.lore" ) + + override val icon = Material.FEATHER + + /** UUID → accumulated sprint ticks since last reset. */ + private val sprintTicks: MutableMap = ConcurrentHashMap() + /** UUID → whether the Speed buff is currently active for this player. */ + private val buffActive: MutableMap = ConcurrentHashMap() + /** UUID → the polling task. */ + private val pollTasks: MutableMap = 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 ) + } + } +} \ No newline at end of file diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index adb481b..fb565ac 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -306,6 +306,46 @@ perks: - 'Golden Apple at the corpse.' message: '🍎 Scavenged a Golden Apple!' + last_stand: + name: 'Last Stand' + lore: + - ' ' + - 'Taking damage below 3 hearts' + - 'grants Resistance II + Absorption I (4s).' + - '(60 s cooldown)' + message: '🛡 Last Stand! Resistance II + Absorption I active!' + + momentum: + name: 'Momentum' + lore: + - ' ' + - 'Sprint for 4 seconds without' + - 'combat to gain Speed I.' + message: '💨 Momentum! Speed I active!' + + gourmet: + name: 'Gourmet' + lore: + - ' ' + - 'Consuming soup grants' + - 'Regen I + Speed I for 2 s.' + message: '🍲 Gourmet! Regen I + Speed I for 2 seconds!' + + berserker: + name: 'Berserker' + lore: + - ' ' + - 'Below 4 hearts: deal' + - '15% more melee damage.' + + evasion: + name: 'Evasion' + lore: + - ' ' + - '15% chance to dodge' + - 'incoming projectiles.' + message: '💨 Evaded! The projectile missed you!' + kits: needed_hits: '⚡ Ability: / Hits' ability_charged: '⚡ ABILITY READY!'