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.FeatherweightPerk
|
||||||
import club.mcscrims.speedhg.perk.impl.GhostPerk
|
import club.mcscrims.speedhg.perk.impl.GhostPerk
|
||||||
import club.mcscrims.speedhg.perk.impl.GourmetPerk
|
import club.mcscrims.speedhg.perk.impl.GourmetPerk
|
||||||
|
import club.mcscrims.speedhg.perk.impl.IroncladPerk
|
||||||
import club.mcscrims.speedhg.perk.impl.LastStandPerk
|
import club.mcscrims.speedhg.perk.impl.LastStandPerk
|
||||||
import club.mcscrims.speedhg.perk.impl.MomentumPerk
|
import club.mcscrims.speedhg.perk.impl.MomentumPerk
|
||||||
import club.mcscrims.speedhg.perk.impl.OraclePerk
|
import club.mcscrims.speedhg.perk.impl.OraclePerk
|
||||||
import club.mcscrims.speedhg.perk.impl.PyromaniacPerk
|
import club.mcscrims.speedhg.perk.impl.PyromaniacPerk
|
||||||
import club.mcscrims.speedhg.perk.impl.ScavengerPerk
|
import club.mcscrims.speedhg.perk.impl.ScavengerPerk
|
||||||
|
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.impl.VampirePerk
|
||||||
import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher
|
import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher
|
||||||
import club.mcscrims.speedhg.ranking.RankingManager
|
import club.mcscrims.speedhg.ranking.RankingManager
|
||||||
@@ -255,11 +259,15 @@ class SpeedHG : JavaPlugin() {
|
|||||||
perkManager.registerPerk( FeatherweightPerk() )
|
perkManager.registerPerk( FeatherweightPerk() )
|
||||||
perkManager.registerPerk( GhostPerk() )
|
perkManager.registerPerk( GhostPerk() )
|
||||||
perkManager.registerPerk( GourmetPerk() )
|
perkManager.registerPerk( GourmetPerk() )
|
||||||
|
perkManager.registerPerk( IroncladPerk() )
|
||||||
perkManager.registerPerk( LastStandPerk() )
|
perkManager.registerPerk( LastStandPerk() )
|
||||||
perkManager.registerPerk( MomentumPerk() )
|
perkManager.registerPerk( MomentumPerk() )
|
||||||
perkManager.registerPerk( OraclePerk() )
|
perkManager.registerPerk( OraclePerk() )
|
||||||
perkManager.registerPerk( PyromaniacPerk() )
|
perkManager.registerPerk( PyromaniacPerk() )
|
||||||
perkManager.registerPerk( ScavengerPerk() )
|
perkManager.registerPerk( ScavengerPerk() )
|
||||||
|
perkManager.registerPerk( ScorchPerk() )
|
||||||
|
perkManager.registerPerk( TenacityPerk() )
|
||||||
|
perkManager.registerPerk( TrackerPerk() )
|
||||||
perkManager.registerPerk( VampirePerk() )
|
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.Playstyle
|
||||||
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
||||||
import club.mcscrims.speedhg.kit.charge.ChargeState
|
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.AnchorKit
|
||||||
import club.mcscrims.speedhg.kit.impl.BlackPantherKit
|
import club.mcscrims.speedhg.kit.impl.BlackPantherKit
|
||||||
import club.mcscrims.speedhg.kit.impl.IceMageKit
|
import club.mcscrims.speedhg.kit.impl.IceMageKit
|
||||||
@@ -125,6 +126,13 @@ class KitEventDispatcher(
|
|||||||
Pair( victim.uniqueId, System.currentTimeMillis() )
|
Pair( victim.uniqueId, System.currentTimeMillis() )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 2. Alchemist hit tracking ────────────────────────────────────────
|
||||||
|
if ( attackerKit is AlchemistKit &&
|
||||||
|
attackerPlaystyle == Playstyle.AGGRESSIVE )
|
||||||
|
{
|
||||||
|
attackerKit.trackHit( attacker, victim )
|
||||||
|
}
|
||||||
|
|
||||||
// ── 3. Attacker passive hook ─────────────────────────────────────────
|
// ── 3. Attacker passive hook ─────────────────────────────────────────
|
||||||
attackerKit.getPassiveAbility( attackerPlaystyle )
|
attackerKit.getPassiveAbility( attackerPlaystyle )
|
||||||
.onHitEnemy( attacker, victim, event )
|
.onHitEnemy( attacker, victim, event )
|
||||||
@@ -167,8 +175,9 @@ class KitEventDispatcher(
|
|||||||
val active = kit.getActiveAbility( playstyle )
|
val active = kit.getActiveAbility( playstyle )
|
||||||
|
|
||||||
// Allow throwable items (potions, ender pearls, etc.) to pass through
|
// 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.LINGERING_POTION ||
|
||||||
|
itemInHand.type == Material.MILK_BUCKET ||
|
||||||
itemInHand.type == Material.ENDER_PEARL ) return
|
itemInHand.type == Material.ENDER_PEARL ) return
|
||||||
|
|
||||||
if ( itemInHand.type != active.triggerMaterial ) 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>'
|
- '<gray>incoming projectiles.</gray>'
|
||||||
message: '<aqua>💨 Evaded! The projectile missed you!</aqua>'
|
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)
|
# SpeedHG · en_US.yml — kits section (overhauled)
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user