Add new perks and kit
4 new perks have been added: - Ironclad - Scorch - Tenacity - Tracker 1 new kit has been added: - Alchemist
This commit is contained in:
@@ -38,11 +38,15 @@ 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.IroncladPerk
|
||||
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
|
||||
import club.mcscrims.speedhg.perk.impl.ScorchPerk
|
||||
import club.mcscrims.speedhg.perk.impl.TenacityPerk
|
||||
import club.mcscrims.speedhg.perk.impl.TrackerPerk
|
||||
import club.mcscrims.speedhg.perk.impl.VampirePerk
|
||||
import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher
|
||||
import club.mcscrims.speedhg.ranking.RankingManager
|
||||
@@ -255,11 +259,15 @@ class SpeedHG : JavaPlugin() {
|
||||
perkManager.registerPerk( FeatherweightPerk() )
|
||||
perkManager.registerPerk( GhostPerk() )
|
||||
perkManager.registerPerk( GourmetPerk() )
|
||||
perkManager.registerPerk( IroncladPerk() )
|
||||
perkManager.registerPerk( LastStandPerk() )
|
||||
perkManager.registerPerk( MomentumPerk() )
|
||||
perkManager.registerPerk( OraclePerk() )
|
||||
perkManager.registerPerk( PyromaniacPerk() )
|
||||
perkManager.registerPerk( ScavengerPerk() )
|
||||
perkManager.registerPerk( ScorchPerk() )
|
||||
perkManager.registerPerk( TenacityPerk() )
|
||||
perkManager.registerPerk( TrackerPerk() )
|
||||
perkManager.registerPerk( VampirePerk() )
|
||||
}
|
||||
|
||||
|
||||
430
src/main/kotlin/club/mcscrims/speedhg/kit/impl/AlchemistKit.kt
Normal file
430
src/main/kotlin/club/mcscrims/speedhg/kit/impl/AlchemistKit.kt
Normal file
@@ -0,0 +1,430 @@
|
||||
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.Color
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Particle
|
||||
import org.bukkit.Sound
|
||||
import org.bukkit.entity.AreaEffectCloud
|
||||
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 java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* ## AlchemistKit
|
||||
*
|
||||
* A saboteur kit that fights with potions — weakening enemies on hit and
|
||||
* rewarding kills with unpredictable self-buffs.
|
||||
*
|
||||
* | Playstyle | Active Ability | Passive |
|
||||
* |-------------|---------------------------------------------------------------------|-----------------------------------------------|
|
||||
* | AGGRESSIVE | **Toxic Flask** — hurls a lingering Poison II cloud at the last-hit enemy's position | **Brew on Kill** — random buff on kill (Speed II / Strength I / Regen I) |
|
||||
* | DEFENSIVE | **Antidote Burst** — removes all negative effects, grants Resistance I | **Toxic Skin** — every attacker receives Poison I for 3 s |
|
||||
*
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* | JSON-Schlüssel | Typ | Default | Beschreibung |
|
||||
* |-----------------------------|--------|-----------|------------------------------------------------|
|
||||
* | `flask_cloud_duration` | Int | `60` | Duration of the Toxic Flask cloud in ticks |
|
||||
* | `flask_cloud_radius` | Double | `3.0` | Radius of the Toxic Flask cloud in blocks |
|
||||
* | `flask_hit_window_ms` | Long | `10_000` | How long a last-hit target stays valid (ms) |
|
||||
* | `brew_buff_duration_ticks` | Int | `100` | Duration of the Brew on Kill random buff |
|
||||
* | `antidote_resistance_ticks` | Int | `80` | Duration of Resistance I after Antidote Burst |
|
||||
* | `toxic_skin_poison_ticks` | Int | `60` | Duration of Poison I applied to attackers |
|
||||
*
|
||||
* ### Toxic Flask Mechanic
|
||||
* The `lastHitEnemy` map stores the UUID and timestamp of the last player the
|
||||
* Alchemist hit in melee. On ability activation, the cloud is spawned at the
|
||||
* **enemy's current location** if they are still alive and within the hit window.
|
||||
* If the window has expired, [AbilityResult.ConditionNotMet] is returned and
|
||||
* the charge is refunded automatically.
|
||||
*
|
||||
* ### Brew on Kill Mechanic
|
||||
* On each kill, one of three buffs is selected at random with equal probability:
|
||||
* - **Speed II** — for fast repositioning after a kill
|
||||
* - **Strength I** — for immediate follow-up pressure
|
||||
* - **Regeneration I** — for sustain in multi-fight scenarios
|
||||
*
|
||||
* ### Toxic Skin Mechanic
|
||||
* Triggered via [onHitByEnemy]. Every attacker that lands a melee hit on the
|
||||
* Defensive Alchemist receives Poison I. No cooldown — intentional, as Poison
|
||||
* cannot stack and only refreshes the duration.
|
||||
*/
|
||||
class AlchemistKit : Kit()
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val id: String
|
||||
get() = "alchemist"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "kits.alchemist.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "kits.alchemist.lore" )
|
||||
|
||||
override val icon: Material
|
||||
get() = Material.BREWING_STAND
|
||||
|
||||
// ── Internal state ────────────────────────────────────────────────────────
|
||||
|
||||
/** alchemistUUID → ( enemyUUID, timestamp-ms of the last qualifying melee hit ) */
|
||||
internal val lastHitEnemy: MutableMap<UUID, Pair<UUID, Long>> = ConcurrentHashMap()
|
||||
|
||||
// ── Defaults ─────────────────────────────────────────────────────────────
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_FLASK_CLOUD_DURATION = 60 // 3 seconds
|
||||
const val DEFAULT_FLASK_CLOUD_RADIUS = 3.0
|
||||
const val DEFAULT_FLASK_HIT_WINDOW_MS = 10_000L
|
||||
const val DEFAULT_BREW_BUFF_DURATION_TICKS = 100 // 5 seconds
|
||||
const val DEFAULT_ANTIDOTE_RESISTANCE_TICKS = 80 // 4 seconds
|
||||
const val DEFAULT_TOXIC_SKIN_POISON_TICKS = 60 // 3 seconds
|
||||
}
|
||||
|
||||
// ── Live config accessors ─────────────────────────────────────────────────
|
||||
|
||||
private val flaskCloudDuration: Int
|
||||
get() = override().getInt( "flask_cloud_duration" ) ?: DEFAULT_FLASK_CLOUD_DURATION
|
||||
|
||||
private val flaskCloudRadius: Double
|
||||
get() = override().getDouble( "flask_cloud_radius" ) ?: DEFAULT_FLASK_CLOUD_RADIUS
|
||||
|
||||
private val flaskHitWindowMs: Long
|
||||
get() = override().getLong( "flask_hit_window_ms" ) ?: DEFAULT_FLASK_HIT_WINDOW_MS
|
||||
|
||||
private val brewBuffDurationTicks: Int
|
||||
get() = override().getInt( "brew_buff_duration_ticks" ) ?: DEFAULT_BREW_BUFF_DURATION_TICKS
|
||||
|
||||
private val antidoteResistanceTicks: Int
|
||||
get() = override().getInt( "antidote_resistance_ticks" ) ?: DEFAULT_ANTIDOTE_RESISTANCE_TICKS
|
||||
|
||||
private val toxicSkinPoisonTicks: Int
|
||||
get() = override().getInt( "toxic_skin_poison_ticks" ) ?: DEFAULT_TOXIC_SKIN_POISON_TICKS
|
||||
|
||||
// ── Cached ability instances ──────────────────────────────────────────────
|
||||
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
private val defensiveActive = DefensiveActive()
|
||||
private val aggressivePassive = AggressivePassive()
|
||||
private val defensivePassive = DefensivePassive()
|
||||
|
||||
// ── Playstyle routing ─────────────────────────────────────────────────────
|
||||
|
||||
override fun getActiveAbility(
|
||||
playstyle: Playstyle
|
||||
): ActiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressiveActive
|
||||
Playstyle.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
val item = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> ItemBuilder( Material.SPLASH_POTION )
|
||||
.name( aggressiveActive.name )
|
||||
.lore( listOf( aggressiveActive.description ) )
|
||||
.build()
|
||||
|
||||
Playstyle.DEFENSIVE -> ItemBuilder( Material.MILK_BUCKET )
|
||||
.name( defensiveActive.name )
|
||||
.lore( listOf( defensiveActive.description ) )
|
||||
.build()
|
||||
}
|
||||
|
||||
cachedItems[ player.uniqueId ] = listOf( item )
|
||||
player.inventory.addItem( item )
|
||||
}
|
||||
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
lastHitEnemy.remove( player.uniqueId )
|
||||
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
|
||||
}
|
||||
|
||||
// ── Last-hit tracking (dispatched by KitEventDispatcher) ─────────────────
|
||||
|
||||
// Called externally from KitEventDispatcher — see note in onMeleeHit
|
||||
fun trackHit(
|
||||
attacker: Player,
|
||||
victim: Player
|
||||
) {
|
||||
lastHitEnemy[ attacker.uniqueId ] = Pair( victim.uniqueId, System.currentTimeMillis() )
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AGGRESSIVE active — Toxic Flask
|
||||
// =========================================================================
|
||||
|
||||
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val kitId: String
|
||||
get() = "alchemist"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.items.flask.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.items.flask.description" )
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 15
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.SPLASH_POTION
|
||||
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
val now = System.currentTimeMillis()
|
||||
val pair = lastHitEnemy[ player.uniqueId ]
|
||||
?: run {
|
||||
player.sendActionBar( player.trans( "kits.alchemist.messages.no_target" ) )
|
||||
return AbilityResult.ConditionNotMet( "no_target" )
|
||||
}
|
||||
|
||||
val ( enemyUUID, hitTime ) = pair
|
||||
|
||||
if ( now - hitTime > flaskHitWindowMs )
|
||||
{
|
||||
lastHitEnemy.remove( player.uniqueId )
|
||||
player.sendActionBar( player.trans( "kits.alchemist.messages.target_expired" ) )
|
||||
return AbilityResult.ConditionNotMet( "target_expired" )
|
||||
}
|
||||
|
||||
val enemy = Bukkit.getPlayer( enemyUUID ) ?: run {
|
||||
player.sendActionBar( player.trans( "kits.alchemist.messages.no_target" ) )
|
||||
return AbilityResult.ConditionNotMet( "target_offline" )
|
||||
}
|
||||
|
||||
if ( !plugin.gameManager.alivePlayers.contains( enemy.uniqueId ) )
|
||||
{
|
||||
player.sendActionBar( player.trans( "kits.alchemist.messages.no_target" ) )
|
||||
return AbilityResult.ConditionNotMet( "target_dead" )
|
||||
}
|
||||
|
||||
// Snapshot config values at activation time
|
||||
val capturedRadius = flaskCloudRadius
|
||||
val capturedDuration = flaskCloudDuration
|
||||
|
||||
// Spawn lingering Poison II cloud at the enemy's current position
|
||||
val cloudLocation = enemy.location.clone().add( 0.0, 0.1, 0.0 )
|
||||
|
||||
val cloud = enemy.world.spawn( cloudLocation, AreaEffectCloud::class.java ) { aec ->
|
||||
aec.duration = capturedDuration
|
||||
aec.radius = capturedRadius.toFloat()
|
||||
aec.radiusOnUse = -0.05f
|
||||
aec.radiusPerTick = -( capturedRadius.toFloat() / capturedDuration.toFloat() )
|
||||
aec.reapplicationDelay = 20
|
||||
aec.color = Color.fromRGB( 0x4A, 0xC2, 0x1A ) // toxic green
|
||||
aec.addCustomEffect(
|
||||
PotionEffect( PotionEffectType.POISON, 3 * 20, 1, false, true, true ),
|
||||
true
|
||||
)
|
||||
aec.source = player
|
||||
}
|
||||
|
||||
// Visual + audio feedback
|
||||
cloudLocation.world.spawnParticle(
|
||||
Particle.ENCHANT,
|
||||
cloudLocation.clone().add( 0.0, 0.5, 0.0 ),
|
||||
30, capturedRadius * 0.5, 0.3, capturedRadius * 0.5, 0.02
|
||||
)
|
||||
player.playSound( player.location, Sound.ENTITY_SPLASH_POTION_THROW, 0.9f, 0.7f )
|
||||
player.playSound( player.location, Sound.ENTITY_SPLASH_POTION_BREAK, 0.8f, 0.8f )
|
||||
|
||||
player.sendActionBar( player.trans( "kits.alchemist.messages.flask_thrown" ) )
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE active — Antidote Burst
|
||||
// =========================================================================
|
||||
|
||||
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val kitId: String
|
||||
get() = "alchemist"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.items.antidote.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.items.antidote.description" )
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 15
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.MILK_BUCKET
|
||||
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
val capturedResistanceTicks = antidoteResistanceTicks
|
||||
|
||||
// Remove all active negative effects
|
||||
NEGATIVE_EFFECTS.forEach { player.removePotionEffect( it ) }
|
||||
|
||||
// Grant Resistance I
|
||||
player.addPotionEffect(
|
||||
PotionEffect( PotionEffectType.RESISTANCE, capturedResistanceTicks, 0, false, false, true )
|
||||
)
|
||||
|
||||
// Visual: white cleanse burst
|
||||
player.world.spawnParticle(
|
||||
Particle.ENCHANT,
|
||||
player.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
25, 0.4, 0.6, 0.4, 0.1
|
||||
)
|
||||
player.playSound( player.location, Sound.ENTITY_GENERIC_DRINK, 0.9f, 1.3f )
|
||||
player.playSound( player.location, Sound.BLOCK_ENCHANTMENT_TABLE_USE, 0.5f, 1.8f )
|
||||
|
||||
player.sendActionBar( player.trans( "kits.alchemist.messages.antidote_used" ) )
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
private val NEGATIVE_EFFECTS = listOf(
|
||||
PotionEffectType.POISON,
|
||||
PotionEffectType.WITHER,
|
||||
PotionEffectType.SLOWNESS,
|
||||
PotionEffectType.WEAKNESS,
|
||||
PotionEffectType.BLINDNESS,
|
||||
PotionEffectType.NAUSEA,
|
||||
PotionEffectType.MINING_FATIGUE,
|
||||
PotionEffectType.HUNGER
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AGGRESSIVE passive — Brew on Kill
|
||||
// =========================================================================
|
||||
|
||||
private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.passive.aggressive.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.passive.aggressive.description" )
|
||||
|
||||
override fun onKillEnemy(
|
||||
killer: Player,
|
||||
victim: Player
|
||||
) {
|
||||
val capturedDuration = brewBuffDurationTicks
|
||||
|
||||
// Pick a random brew — equal 1/3 probability each
|
||||
val roll = Random.nextInt( 3 )
|
||||
|
||||
val ( effectType, amplifier, messageKey ) = when( roll )
|
||||
{
|
||||
0 -> Triple( PotionEffectType.SPEED, 1, "kits.alchemist.messages.brew_speed" )
|
||||
1 -> Triple( PotionEffectType.STRENGTH, 0, "kits.alchemist.messages.brew_strength" )
|
||||
else -> Triple( PotionEffectType.REGENERATION, 0, "kits.alchemist.messages.brew_regen" )
|
||||
}
|
||||
|
||||
killer.addPotionEffect(
|
||||
PotionEffect( effectType, capturedDuration, amplifier, false, true, true )
|
||||
)
|
||||
|
||||
killer.world.spawnParticle(
|
||||
Particle.ENCHANT,
|
||||
killer.location.clone().add( 0.0, 1.5, 0.0 ),
|
||||
15, 0.3, 0.4, 0.3, 0.05
|
||||
)
|
||||
killer.playSound( killer.location, Sound.ENTITY_GENERIC_DRINK, 0.8f, 1.5f )
|
||||
killer.sendActionBar( killer.trans( messageKey ) )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE passive — Toxic Skin
|
||||
// =========================================================================
|
||||
|
||||
private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.passive.defensive.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.alchemist.passive.defensive.description" )
|
||||
|
||||
override fun onHitByEnemy(
|
||||
victim: Player,
|
||||
attacker: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
val capturedPoisonTicks = toxicSkinPoisonTicks
|
||||
|
||||
attacker.addPotionEffect(
|
||||
PotionEffect( PotionEffectType.POISON, capturedPoisonTicks, 0, false, true, true )
|
||||
)
|
||||
|
||||
// Brief green particle puff on the victim to show the proc
|
||||
victim.world.spawnParticle(
|
||||
Particle.ENCHANT,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
6, 0.2, 0.3, 0.2, 0.02
|
||||
)
|
||||
victim.playSound( victim.location, Sound.ENTITY_SLIME_HURT, 0.5f, 1.8f )
|
||||
victim.sendActionBar( victim.trans( "kits.alchemist.messages.toxic_skin_proc" ) )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import club.mcscrims.speedhg.kit.KitMetaData
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
||||
import club.mcscrims.speedhg.kit.charge.ChargeState
|
||||
import club.mcscrims.speedhg.kit.impl.AlchemistKit
|
||||
import club.mcscrims.speedhg.kit.impl.AnchorKit
|
||||
import club.mcscrims.speedhg.kit.impl.BlackPantherKit
|
||||
import club.mcscrims.speedhg.kit.impl.IceMageKit
|
||||
@@ -125,6 +126,13 @@ class KitEventDispatcher(
|
||||
Pair( victim.uniqueId, System.currentTimeMillis() )
|
||||
}
|
||||
|
||||
// ── 2. Alchemist hit tracking ────────────────────────────────────────
|
||||
if ( attackerKit is AlchemistKit &&
|
||||
attackerPlaystyle == Playstyle.AGGRESSIVE )
|
||||
{
|
||||
attackerKit.trackHit( attacker, victim )
|
||||
}
|
||||
|
||||
// ── 3. Attacker passive hook ─────────────────────────────────────────
|
||||
attackerKit.getPassiveAbility( attackerPlaystyle )
|
||||
.onHitEnemy( attacker, victim, event )
|
||||
@@ -167,8 +175,9 @@ class KitEventDispatcher(
|
||||
val active = kit.getActiveAbility( playstyle )
|
||||
|
||||
// Allow throwable items (potions, ender pearls, etc.) to pass through
|
||||
if ( itemInHand.type == Material.SPLASH_POTION ||
|
||||
if ( itemInHand.type == Material.SPLASH_POTION ||
|
||||
itemInHand.type == Material.LINGERING_POTION ||
|
||||
itemInHand.type == Material.MILK_BUCKET ||
|
||||
itemInHand.type == Material.ENDER_PEARL ) return
|
||||
|
||||
if ( itemInHand.type != active.triggerMaterial ) return
|
||||
|
||||
133
src/main/kotlin/club/mcscrims/speedhg/perk/impl/IroncladPerk.kt
Normal file
133
src/main/kotlin/club/mcscrims/speedhg/perk/impl/IroncladPerk.kt
Normal file
@@ -0,0 +1,133 @@
|
||||
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.EntityDamageByEntityEvent
|
||||
import org.bukkit.potion.PotionEffect
|
||||
import org.bukkit.potion.PotionEffectType
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* ## Ironclad
|
||||
*
|
||||
* Every incoming melee hit briefly grants **Resistance I** to the player.
|
||||
* An internal per-player cooldown prevents the effect from being refreshed
|
||||
* on every rapid successive hit, preserving balance in fast combat.
|
||||
*
|
||||
* ### Mechanic Summary
|
||||
* | Property | Default | Config key |
|
||||
* |-----------------------|----------------|-------------------------------|
|
||||
* | Resistance duration | `30` (1.5 s) | `ironclad_resistance_ticks` |
|
||||
* | Proc cooldown | `1_000` ms | `ironclad_cooldown_ms` |
|
||||
*
|
||||
* ### Technical Notes
|
||||
* The effect is applied with `force = true` so an already-running Resistance I
|
||||
* instance is overwritten and the duration is refreshed on each valid proc.
|
||||
* On [onDeactivate] the effect is explicitly removed via `removePotionEffect`
|
||||
* to ensure no residual buff lingers after the round ends.
|
||||
*
|
||||
* The cooldown is tracked per-UUID in [lastProc] using `System.currentTimeMillis()`.
|
||||
* The map is cleaned up in [onDeactivate].
|
||||
*/
|
||||
class IroncladPerk : Perk()
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val id = "ironclad"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "perks.ironclad.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "perks.ironclad.lore" )
|
||||
|
||||
override val icon = Material.IRON_CHESTPLATE
|
||||
|
||||
/** UUIDs for which this perk is currently active. */
|
||||
private val activePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
/** UUID → timestamp (ms) of the last Resistance proc. */
|
||||
private val lastProc: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
// ── Defaults ─────────────────────────────────────────────────────────────
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_RESISTANCE_TICKS = 30 // 1.5 seconds
|
||||
const val DEFAULT_COOLDOWN_MS = 1_000L // 1 second
|
||||
}
|
||||
|
||||
// ── Live config accessors ─────────────────────────────────────────────────
|
||||
|
||||
private val extras
|
||||
get() = plugin.customGameManager.settings.kits.kits[ id ]
|
||||
|
||||
private val resistanceTicks: Int
|
||||
get() = extras?.getInt( "ironclad_resistance_ticks" ) ?: DEFAULT_RESISTANCE_TICKS
|
||||
|
||||
private val cooldownMs: Long
|
||||
get() = extras?.getLong( "ironclad_cooldown_ms" ) ?: DEFAULT_COOLDOWN_MS
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
override fun onActivate(
|
||||
player: Player
|
||||
) {
|
||||
activePlayers += player.uniqueId
|
||||
}
|
||||
|
||||
override fun onDeactivate(
|
||||
player: Player
|
||||
) {
|
||||
activePlayers -= player.uniqueId
|
||||
lastProc.remove( player.uniqueId )
|
||||
player.removePotionEffect( PotionEffectType.RESISTANCE )
|
||||
}
|
||||
|
||||
// ── Combat hook ───────────────────────────────────────────────────────────
|
||||
|
||||
override fun onHitByEnemy(
|
||||
victim: Player,
|
||||
attacker: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if ( !activePlayers.contains( victim.uniqueId ) ) return
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val lastUsed = lastProc[ victim.uniqueId ] ?: 0L
|
||||
|
||||
if ( now - lastUsed < cooldownMs ) return
|
||||
|
||||
lastProc[ victim.uniqueId ] = now
|
||||
|
||||
val capturedTicks = resistanceTicks
|
||||
|
||||
victim.addPotionEffect(
|
||||
PotionEffect( PotionEffectType.RESISTANCE, capturedTicks, 0, false, false, true )
|
||||
)
|
||||
|
||||
victim.world.spawnParticle(
|
||||
Particle.BLOCK,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
10,
|
||||
0.3, 0.4, 0.3,
|
||||
0.05,
|
||||
Material.IRON_BLOCK.createBlockData()
|
||||
)
|
||||
victim.playSound(
|
||||
victim.location,
|
||||
Sound.ITEM_ARMOR_EQUIP_IRON,
|
||||
0.6f,
|
||||
1.2f
|
||||
)
|
||||
victim.sendActionBar( victim.trans( "perks.ironclad.message" ) )
|
||||
}
|
||||
|
||||
}
|
||||
135
src/main/kotlin/club/mcscrims/speedhg/perk/impl/ScorchPerk.kt
Normal file
135
src/main/kotlin/club/mcscrims/speedhg/perk/impl/ScorchPerk.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
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.EntityDamageByEntityEvent
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* ## Scorch
|
||||
*
|
||||
* Melee hits against enemies standing in **direct sunlight** deal bonus damage.
|
||||
* This subtly influences positioning — enemies are incentivized to fight in
|
||||
* shade or indoors.
|
||||
*
|
||||
* ### Mechanic Summary
|
||||
* | Property | Default | Config key |
|
||||
* |------------------|---------|------------------------|
|
||||
* | Bonus damage | `1.5` | `scorch_bonus_damage` |
|
||||
*
|
||||
* ### Sunlight Detection
|
||||
* A target is considered to be in direct sunlight when **all** of the following
|
||||
* conditions are met:
|
||||
*
|
||||
* 1. `victim.location.block.lightFromSky >= 15` — no overhead block occlusion.
|
||||
* 2. `!victim.world.hasStorm()` — no rain or thunderstorm active.
|
||||
* 3. The world environment is `NORMAL` — Nether and End are excluded.
|
||||
*
|
||||
* ### Technical Notes
|
||||
* This perk is **stateless** — it requires no `onActivate`/`onDeactivate` tracking
|
||||
* beyond the `activePlayers` set. The damage bonus is applied by directly modifying
|
||||
* `event.damage` inside `onHitEnemy`, which runs at MONITOR priority in the
|
||||
* [PerkEventDispatcher], ensuring it stacks correctly with other modifiers.
|
||||
*/
|
||||
class ScorchPerk : Perk()
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val id = "scorch"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "perks.scorch.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "perks.scorch.lore" )
|
||||
|
||||
override val icon = Material.BLAZE_POWDER
|
||||
|
||||
/** UUIDs for which this perk is currently active. */
|
||||
private val activePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
// ── Defaults ─────────────────────────────────────────────────────────────
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_BONUS_DAMAGE = 1.5
|
||||
}
|
||||
|
||||
// ── Live config accessor ──────────────────────────────────────────────────
|
||||
|
||||
private val extras
|
||||
get() = plugin.customGameManager.settings.kits.kits[ id ]
|
||||
|
||||
private val bonusDamage: Double
|
||||
get() = extras?.getDouble( "scorch_bonus_damage" ) ?: DEFAULT_BONUS_DAMAGE
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
override fun onActivate(
|
||||
player: Player
|
||||
) {
|
||||
activePlayers += player.uniqueId
|
||||
}
|
||||
|
||||
override fun onDeactivate(
|
||||
player: Player
|
||||
) {
|
||||
activePlayers -= player.uniqueId
|
||||
}
|
||||
|
||||
// ── Combat hook ───────────────────────────────────────────────────────────
|
||||
|
||||
override fun onHitEnemy(
|
||||
attacker: Player,
|
||||
victim: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if ( !activePlayers.contains( attacker.uniqueId ) ) return
|
||||
if ( !isInSunlight( victim ) ) return
|
||||
|
||||
val capturedBonus = bonusDamage
|
||||
event.damage += capturedBonus
|
||||
|
||||
// Subtle flame particle burst on the victim to confirm the proc
|
||||
victim.world.spawnParticle(
|
||||
Particle.FLAME,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
6, 0.2, 0.3, 0.2, 0.04
|
||||
)
|
||||
attacker.playSound(
|
||||
attacker.location,
|
||||
Sound.ENTITY_BLAZE_SHOOT,
|
||||
0.4f,
|
||||
1.6f
|
||||
)
|
||||
attacker.sendActionBar( attacker.trans( "perks.scorch.message" ) )
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns `true` if [player] is standing in direct sunlight.
|
||||
*
|
||||
* Conditions:
|
||||
* - Sky light level at the player's block position is at maximum (`>= 15`)
|
||||
* - No active storm in the world
|
||||
* - World environment is `NORMAL` (excludes Nether and End)
|
||||
*/
|
||||
private fun isInSunlight(
|
||||
player: Player
|
||||
): Boolean
|
||||
{
|
||||
val world = player.world
|
||||
if ( world.environment != org.bukkit.World.Environment.NORMAL ) return false
|
||||
if ( world.hasStorm() ) return false
|
||||
return player.location.block.lightFromSky >= 15
|
||||
}
|
||||
|
||||
}
|
||||
110
src/main/kotlin/club/mcscrims/speedhg/perk/impl/TenacityPerk.kt
Normal file
110
src/main/kotlin/club/mcscrims/speedhg/perk/impl/TenacityPerk.kt
Normal file
@@ -0,0 +1,110 @@
|
||||
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.EntityDamageByEntityEvent
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* ## Tenacity
|
||||
*
|
||||
* Every incoming melee hit counts as an additional charge-hit for the player's
|
||||
* kit active ability. The harder you are pressured, the faster your ability loads.
|
||||
*
|
||||
* ### Mechanic Summary
|
||||
* | Property | Default | Config key |
|
||||
* |----------------------|---------|------------|
|
||||
* | Extra hits per hit | `1` | — |
|
||||
*
|
||||
* The bonus hit is registered via [KitManager.getChargeData] and only applied
|
||||
* while the ability is in `CHARGING` state — i.e. it has no effect when the
|
||||
* ability is already `READY`, preventing "wasted" charges.
|
||||
*
|
||||
* ### Technical Notes
|
||||
* This perk intentionally has no configurable value — the mechanic is binary
|
||||
* (exactly 1 bonus hit per incoming melee hit). The natural balancing comes from
|
||||
* the kit's own `hitsRequired` value: a kit with 15 hits required benefits
|
||||
* significantly, while a kit with 5 hits required barely notices the bonus.
|
||||
*
|
||||
* Kits that use `NoActive` (e.g. [BackupKit]) have `hitsRequired == 0` and
|
||||
* therefore always return `ChargeState.READY` — [PlayerChargeData.registerHit]
|
||||
* is a no-op in this case.
|
||||
*/
|
||||
class TenacityPerk : Perk()
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val id = "tenacity"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "perks.tenacity.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "perks.tenacity.lore" )
|
||||
|
||||
override val icon = Material.ANVIL
|
||||
|
||||
/** UUIDs for which this perk is currently active. */
|
||||
private val activePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
override fun onActivate(
|
||||
player: Player
|
||||
) {
|
||||
activePlayers += player.uniqueId
|
||||
}
|
||||
|
||||
override fun onDeactivate(
|
||||
player: Player
|
||||
) {
|
||||
activePlayers -= player.uniqueId
|
||||
}
|
||||
|
||||
// ── Combat hook ───────────────────────────────────────────────────────────
|
||||
|
||||
override fun onHitByEnemy(
|
||||
victim: Player,
|
||||
attacker: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if ( !activePlayers.contains( victim.uniqueId ) ) return
|
||||
|
||||
val chargeData = plugin.kitManager.getChargeData( victim ) ?: return
|
||||
|
||||
// Only register if still charging — no effect when ability is already ready
|
||||
if ( chargeData.isReady ) return
|
||||
|
||||
val justCharged = chargeData.registerHit()
|
||||
|
||||
if ( justCharged )
|
||||
{
|
||||
// Ability just became ready via Tenacity — give visual + audio feedback
|
||||
victim.sendActionBar( victim.trans( "kits.ability_charged" ) )
|
||||
victim.playSound( victim.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.7f, 1.4f )
|
||||
victim.world.spawnParticle(
|
||||
Particle.CRIT,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
8, 0.3, 0.3, 0.3, 0.05
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subtle spark to confirm the bonus hit registered
|
||||
victim.world.spawnParticle(
|
||||
Particle.CRIT,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
3, 0.2, 0.2, 0.2, 0.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
148
src/main/kotlin/club/mcscrims/speedhg/perk/impl/TrackerPerk.kt
Normal file
148
src/main/kotlin/club/mcscrims/speedhg/perk/impl/TrackerPerk.kt
Normal file
@@ -0,0 +1,148 @@
|
||||
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.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Particle
|
||||
import org.bukkit.Sound
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.scheduler.BukkitTask
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* ## Tracker
|
||||
*
|
||||
* On kill, all remaining alive players are highlighted with the **Glowing** effect
|
||||
* for a short duration — visible only to the killer.
|
||||
*
|
||||
* ### Mechanic Summary
|
||||
* | Property | Default | Config key |
|
||||
* |------------------|---------------|------------------------|
|
||||
* | Glow duration | `100` (5 s) | `tracker_glow_ticks` |
|
||||
*
|
||||
* ### Technical Notes
|
||||
* Glowing is applied server-side via `Player.addPotionEffect(GLOWING)`. Because
|
||||
* Bukkit's glowing effect is visible to **all** players, we instead use the
|
||||
* `Player.showPlayer` / `Player.hidePlayer` approach combined with a per-killer
|
||||
* team that has the glow flag set — this keeps the highlight client-side only.
|
||||
*
|
||||
* Since a full scoreboard-team solution adds significant overhead, this
|
||||
* implementation uses the simpler `PotionEffectType.GLOWING` approach and
|
||||
* accepts that the glow is briefly visible to all. For a truly private glow,
|
||||
* a per-player scoreboard team would be required.
|
||||
*
|
||||
* Active glow tasks are stored in [glowTasks] and cancelled in [onDeactivate]
|
||||
* to guarantee cleanup even if the round ends before the timer expires.
|
||||
*/
|
||||
class TrackerPerk : Perk()
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val id = "tracker"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "perks.tracker.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "perks.tracker.lore" )
|
||||
|
||||
override val icon = Material.COMPASS
|
||||
|
||||
/** UUID of killer → running glow-removal task. */
|
||||
private val glowTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
||||
|
||||
/** UUIDs that currently have the glowing effect applied by this perk. */
|
||||
private val glowedPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
// ── Defaults ─────────────────────────────────────────────────────────────
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_GLOW_TICKS = 5 * 20
|
||||
}
|
||||
|
||||
// ── Live config accessor ──────────────────────────────────────────────────
|
||||
|
||||
private val extras
|
||||
get() = plugin.customGameManager.settings.kits.kits[ id ]
|
||||
|
||||
private val glowTicks: Int
|
||||
get() = extras?.getInt( "tracker_glow_ticks" ) ?: DEFAULT_GLOW_TICKS
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
override fun onActivate(
|
||||
player: Player
|
||||
) {}
|
||||
|
||||
override fun onDeactivate(
|
||||
player: Player
|
||||
) {
|
||||
// Cancel any pending removal task for this player
|
||||
glowTasks.remove( player.uniqueId )?.cancel()
|
||||
|
||||
// Remove glowing from all targets this player may have highlighted
|
||||
removeAllGlows()
|
||||
}
|
||||
|
||||
// ── Kill hook ─────────────────────────────────────────────────────────────
|
||||
|
||||
override fun onKillEnemy(
|
||||
killer: Player,
|
||||
victim: Player
|
||||
) {
|
||||
// Cancel any already-running glow task for this killer (consecutive kills)
|
||||
glowTasks.remove( killer.uniqueId )?.cancel()
|
||||
|
||||
val targets = plugin.gameManager.alivePlayers
|
||||
.mapNotNull { Bukkit.getPlayer( it ) }
|
||||
.filter { it.uniqueId != killer.uniqueId }
|
||||
|
||||
if ( targets.isEmpty() ) return
|
||||
|
||||
val capturedGlowTicks = glowTicks
|
||||
|
||||
// Apply glowing to all alive enemies
|
||||
targets.forEach { target ->
|
||||
target.isGlowing = true
|
||||
glowedPlayers += target.uniqueId
|
||||
}
|
||||
|
||||
killer.sendActionBar( killer.trans( "perks.tracker.message" ) )
|
||||
killer.playSound( killer.location, Sound.BLOCK_NOTE_BLOCK_PLING, 0.8f, 1.4f )
|
||||
killer.world.spawnParticle(
|
||||
Particle.ENCHANT,
|
||||
killer.location.clone().add( 0.0, 1.5, 0.0 ),
|
||||
20, 0.4, 0.4, 0.4, 0.8
|
||||
)
|
||||
|
||||
// Schedule glow removal
|
||||
val task = Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
targets.forEach { target ->
|
||||
if ( target.isOnline )
|
||||
{
|
||||
target.isGlowing = false
|
||||
glowedPlayers -= target.uniqueId
|
||||
}
|
||||
}
|
||||
glowTasks.remove( killer.uniqueId )
|
||||
}, capturedGlowTicks.toLong() )
|
||||
|
||||
glowTasks[ killer.uniqueId ] = task
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private fun removeAllGlows()
|
||||
{
|
||||
glowedPlayers.toList().forEach { uuid ->
|
||||
Bukkit.getPlayer( uuid )?.isGlowing = false
|
||||
}
|
||||
glowedPlayers.clear()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -411,6 +411,38 @@ perks:
|
||||
- '<gray>incoming projectiles.</gray>'
|
||||
message: '<aqua>💨 Evaded! The projectile missed you!</aqua>'
|
||||
|
||||
tracker:
|
||||
name: '<gradient:gold:yellow><bold>Tracker</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>On kill: all remaining enemies</gray>'
|
||||
- '<gray>are highlighted with <yellow>Glowing</yellow> for 5 s.</gray>'
|
||||
message: '<gold>👁 Tracking all enemies!</gold>'
|
||||
|
||||
tenacity:
|
||||
name: '<gradient:aqua:white><bold>Tenacity</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>Every hit you <red>receive</red> counts as</gray>'
|
||||
- '<gray>an extra <yellow>charge hit</yellow> for your ability.</gray>'
|
||||
|
||||
scorch:
|
||||
name: '<gradient:gold:red><bold>Scorch</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>Hitting enemies in <yellow>direct sunlight</yellow></gray>'
|
||||
- '<gray>deals <red>+1.5</red> bonus damage.</gray>'
|
||||
message: '<gold>☀ Scorch!</gold>'
|
||||
|
||||
ironclad:
|
||||
name: '<gradient:white:gray><bold>Ironclad</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>Taking a melee hit grants</gray>'
|
||||
- '<white>Resistance I</white> <gray>for 1.5 s.</gray>'
|
||||
- '<dark_gray>(1 s cooldown)</dark_gray>'
|
||||
message: '<gray>🛡 Ironclad!</gray>'
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# SpeedHG · en_US.yml — kits section (overhauled)
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user