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:
@@ -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() )
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
131
src/main/kotlin/club/mcscrims/speedhg/perk/impl/EvasionPerk.kt
Normal file
131
src/main/kotlin/club/mcscrims/speedhg/perk/impl/EvasionPerk.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
115
src/main/kotlin/club/mcscrims/speedhg/perk/impl/GourmetPerk.kt
Normal file
115
src/main/kotlin/club/mcscrims/speedhg/perk/impl/GourmetPerk.kt
Normal 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 )
|
||||
}
|
||||
}
|
||||
106
src/main/kotlin/club/mcscrims/speedhg/perk/impl/LastStandPerk.kt
Normal file
106
src/main/kotlin/club/mcscrims/speedhg/perk/impl/LastStandPerk.kt
Normal 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 )
|
||||
}
|
||||
}
|
||||
145
src/main/kotlin/club/mcscrims/speedhg/perk/impl/MomentumPerk.kt
Normal file
145
src/main/kotlin/club/mcscrims/speedhg/perk/impl/MomentumPerk.kt
Normal 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 )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,6 +306,46 @@ perks:
|
||||
- '<gold>Golden Apple</gold> <gray>at the corpse.</gray>'
|
||||
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:
|
||||
needed_hits: '<gold>⚡ Ability: <white><current>/<required> Hits</white></gold>'
|
||||
ability_charged: '<green><bold>⚡ ABILITY READY!</bold></green>'
|
||||
|
||||
Reference in New Issue
Block a user