Make kit parameters configurable and refactor

Introduce live-configurable parameters and refactor three kits (Anchor, BlackPanther, Blitzcrank).

- AnchorKit: replace hardcoded constants with default values + live config accessors (partial_resistance, golem_hp, radii, bonus dmg, monitor interval). Snapshot config on activation, update doc comments to JSON keys, minor API/formatting cleanup, and make passive/active logic use dynamic radii and damages.
- BlackPantherKit: add defaults and live getters for fist mode, push, pounce and extras (push_knockback_speed/y, fist_mode_damage, projectile_delay_ticks). Snapshot values on activate, update docs to expose JSON keys, and small behavioral formatting fixes.
- BlitzcrankKit: add defaults and live getters for hook, stun and ult settings (range, pull_strength, radii, durations, cooldown, raycast params), update docs and remove unused imports; snapshot config where needed.

Also: several minor code cleanups (whitespace/formatting, removal of unused imports, fully qualified event references) and improved in-code documentation for custom settings. These changes enable per-kit overrides via SPEEDHG_CUSTOM_SETTINGS and make runtime behavior consistent by snapshotting values at ability execution.
This commit is contained in:
TDSTOS
2026-04-12 04:08:08 +02:00
parent c1be2ddabd
commit 411b77cc8d
3 changed files with 661 additions and 371 deletions

View File

@@ -19,7 +19,6 @@ import org.bukkit.Sound
import org.bukkit.attribute.Attribute import org.bukkit.attribute.Attribute
import org.bukkit.entity.IronGolem import org.bukkit.entity.IronGolem
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataType import org.bukkit.persistence.PersistentDataType
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask
@@ -29,32 +28,32 @@ import java.util.concurrent.ConcurrentHashMap
/** /**
* ## AnchorKit * ## AnchorKit
* *
* **Passiv (immer aktiv):** 40 % Rückschlag-Reduktion über `GENERIC_KNOCKBACK_RESISTANCE`. * **Passiv (immer aktiv):** [partialResistance] Rückschlag-Reduktion über `GENERIC_KNOCKBACK_RESISTANCE`.
* *
* **Active (beide Playstyles):** Beschwört einen Eisengolem als „Anker". * **Active (beide Playstyles):** Beschwört einen Eisengolem als „Anker".
* - Während der Spieler im Radius des Ankers ist: voller NoKnock + Bonus-Schaden. * - Während der Spieler im Radius des Ankers ist: voller NoKnock + Bonus-Schaden.
* - Der Golem kann von Gegnern zerstört werden (20 HP). Bei Tod spielt er den * - Der Golem kann von Gegnern zerstört werden ([golemHp] HP). Bei Tod spielt er den
* Eisengolem-Todesklang und benachrichtigt den Besitzer. * Eisengolem-Todesklang und benachrichtigt den Besitzer.
* - Nur ein aktiver Anker gleichzeitig; neuer Anker entfernt den alten. * - Nur ein aktiver Anker gleichzeitig; neuer Anker entfernt den alten.
* *
* | Playstyle | Radius | Bonus-Schaden | * | Playstyle | Radius | Bonus-Schaden |
* |-------------|--------|----------------------------| * |-------------|-------------------|---------------------------------------------------|
* | AGGRESSIVE | 5 Blöcke | +1,0 HP (0,5 Herzen) auf jedem Treffer | * | AGGRESSIVE | [aggressiveRadius] Blöcke | +[aggressiveBonusDmg] HP (0,5 Herzen) auf jedem Treffer |
* | DEFENSIVE | 8 Blöcke | kein Schaden-Bonus, aber +Resistance I | * | DEFENSIVE | [defensiveRadius] Blöcke | kein Schaden-Bonus, aber +Resistance I |
* *
* ### Technische Lösung Golem-Tod-Erkennung ohne eigenen Listener: * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* Ein `BukkitTask` prüft alle 10 Ticks (0,5 s), ob `golem.isDead || !golem.isValid`.
* Der Golem wird mit `isSilent = true` gespawnt, sodass wir den Eisengolem-Todesklang
* manuell abspielen können (kein unerwarteter Doppel-Sound).
* Der Golem erhält 20 HP (statt 100 vanilla), damit er in HG-Kämpfen destroybar ist.
* *
* ### Rückschlag-Reduktion: * | JSON-Schlüssel | Typ | Default | Beschreibung |
* `onAssign` setzt `GENERIC_KNOCKBACK_RESISTANCE.baseValue = PARTIAL_RESISTANCE`. * |-----------------------------|--------|---------|---------------------------------------------------|
* Ein periodischer Task aktualisiert den Wert auf 1.0 (wenn im Radius) oder zurück * | `partial_resistance` | Double | `0.4` | Basis-Rückschlag-Reduktion (40 %) |
* auf PARTIAL_RESISTANCE (wenn außerhalb). * | `golem_hp` | Double | `20.0` | HP des Anker-Golems (10 Herzen) |
* `onRemove` setzt den Attributwert auf 0,0 zurück. * | `aggressive_radius` | Double | `5.0` | Radius im Aggressive-Modus (Blöcke) |
* | `defensive_radius` | Double | `8.0` | Radius im Defensive-Modus (Blöcke) |
* | `aggressive_bonus_dmg` | Double | `1.0` | Bonus-Schaden im Radius (HP) |
* | `monitor_interval_ticks` | Long | `10` | Ticks zwischen Golem-Zustand-Prüfungen |
*/ */
class AnchorKit : Kit() { class AnchorKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val mm = MiniMessage.miniMessage() private val mm = MiniMessage.miniMessage()
@@ -67,38 +66,94 @@ class AnchorKit : Kit() {
override val icon = Material.ANVIL override val icon = Material.ANVIL
companion object { companion object {
const val PARTIAL_RESISTANCE = 0.4 // 40 % immer aktiv const val DEFAULT_PARTIAL_RESISTANCE = 0.4
const val GOLEM_HP = 20.0 // 10 Herzen const val DEFAULT_GOLEM_HP = 20.0
const val AGGRESSIVE_RADIUS = 5.0 const val DEFAULT_AGGRESSIVE_RADIUS = 5.0
const val DEFENSIVE_RADIUS = 8.0 const val DEFAULT_DEFENSIVE_RADIUS = 8.0
const val AGGRESSIVE_BONUS_DMG = 1.0 // +0,5 Herzen const val DEFAULT_AGGRESSIVE_BONUS_DMG = 1.0
const val MONITOR_INTERVAL_TICKS = 10L // alle 0,5 s prüfen const val DEFAULT_MONITOR_INTERVAL_TICKS = 10L
const val PDC_KEY = "anchor_owner_uuid" const val PDC_KEY = "anchor_owner_uuid"
} }
// ── Live config accessors ─────────────────────────────────────────────────
/**
* Basis-Rückschlag-Reduktion, die immer aktiv ist (40 % Standard).
* JSON-Schlüssel: `partial_resistance`
*/
private val partialResistance: Double
get() = override().getDouble( "partial_resistance" ) ?: DEFAULT_PARTIAL_RESISTANCE
/**
* HP des Eisengolem-Ankers. Vanilla = 100 HP; niedrigerer Wert macht ihn destroybar.
* JSON-Schlüssel: `golem_hp`
*/
private val golemHp: Double
get() = override().getDouble( "golem_hp" ) ?: DEFAULT_GOLEM_HP
/**
* Radius im Aggressive-Modus in Blöcken.
* JSON-Schlüssel: `aggressive_radius`
*/
private val aggressiveRadius: Double
get() = override().getDouble( "aggressive_radius" ) ?: DEFAULT_AGGRESSIVE_RADIUS
/**
* Radius im Defensive-Modus in Blöcken.
* JSON-Schlüssel: `defensive_radius`
*/
private val defensiveRadius: Double
get() = override().getDouble( "defensive_radius" ) ?: DEFAULT_DEFENSIVE_RADIUS
/**
* Bonus-Schaden pro Treffer im Aggressive-Modus (+0,5 Herzen Standard).
* JSON-Schlüssel: `aggressive_bonus_dmg`
*/
private val aggressiveBonusDmg: Double
get() = override().getDouble( "aggressive_bonus_dmg" ) ?: DEFAULT_AGGRESSIVE_BONUS_DMG
/**
* Ticks zwischen Golem-Zustand-Prüfungen und Resistenz-Updates.
* JSON-Schlüssel: `monitor_interval_ticks`
*/
private val monitorIntervalTicks: Long
get() = override().getLong( "monitor_interval_ticks" ) ?: DEFAULT_MONITOR_INTERVAL_TICKS
// ── Shared kit state ──────────────────────────────────────────────────────
private val anchorGolems: MutableMap<UUID, IronGolem> = ConcurrentHashMap() private val anchorGolems: MutableMap<UUID, IronGolem> = ConcurrentHashMap()
private val monitorTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap() private val monitorTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
// ── Gecachte Instanzen ──────────────────────────────────────────────────── // ── Gecachte Instanzen ────────────────────────────────────────────────────
private val aggressiveActive = AnchorActive(Playstyle.AGGRESSIVE, AGGRESSIVE_RADIUS) private val aggressiveActive = AnchorActive( Playstyle.AGGRESSIVE )
private val defensiveActive = AnchorActive(Playstyle.DEFENSIVE, DEFENSIVE_RADIUS) private val defensiveActive = AnchorActive( Playstyle.DEFENSIVE )
private val aggressivePassive = AnchorPassive(Playstyle.AGGRESSIVE, AGGRESSIVE_RADIUS, bonusDamage = AGGRESSIVE_BONUS_DMG, resistanceBonus = false) private val aggressivePassive = AnchorPassive( Playstyle.AGGRESSIVE, bonusDamage = true, resistanceBonus = false )
private val defensivePassive = AnchorPassive(Playstyle.DEFENSIVE, DEFENSIVE_RADIUS, bonusDamage = 0.0, resistanceBonus = true) private val defensivePassive = AnchorPassive( Playstyle.DEFENSIVE, bonusDamage = false, resistanceBonus = true )
override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) { override fun getActiveAbility(
playstyle: Playstyle
) = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
override fun getPassiveAbility(
playstyle: Playstyle
) = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(player: Player, playstyle: Playstyle) { override fun giveItems(
player: Player,
playstyle: Playstyle
) {
val active = getActiveAbility( playstyle ) val active = getActiveAbility( playstyle )
val item = ItemBuilder( Material.CHAIN ) val item = ItemBuilder( Material.CHAIN )
.name( active.name ) .name( active.name )
@@ -108,29 +163,32 @@ class AnchorKit : Kit() {
player.inventory.addItem( item ) player.inventory.addItem( item )
} }
// ── Lifecycle: Rückschlag-Basis-Resistenz setzen/entfernen ─────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
override fun onAssign(player: Player, playstyle: Playstyle) { override fun onAssign(
player: Player,
playstyle: Playstyle
) {
val capturedPartialResistance = partialResistance
player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE ) player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE )
?.baseValue = PARTIAL_RESISTANCE ?.baseValue = capturedPartialResistance
} }
override fun onRemove(player: Player) { override fun onRemove(
// Golem entfernen player: Player
) {
removeAnchor( player, playDeathSound = false ) removeAnchor( player, playDeathSound = false )
// Rückschlag-Resistenz zurücksetzen
player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE ) player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE )
?.baseValue = 0.0 ?.baseValue = 0.0
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) } cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
} }
// ========================================================================= // =========================================================================
// Active Ability Anker-Golem beschwören (beide Playstyles, unterschiedlicher Radius) // Active Ability Anker-Golem beschwören (beide Playstyles)
// ========================================================================= // =========================================================================
inner class AnchorActive( inner class AnchorActive(
playstyle: Playstyle, playstyle: Playstyle
private val radius: Double
) : ActiveAbility( playstyle ) { ) : ActiveAbility( playstyle ) {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -143,26 +201,35 @@ class AnchorKit : Kit() {
override val hardcodedHitsRequired = 15 override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.ANVIL override val triggerMaterial = Material.ANVIL
override fun execute(player: Player): AbilityResult { override fun execute(
// Alten Anker entfernen (kein Todesklang Spieler beschwört neuen) player: Player
): AbilityResult
{
removeAnchor( player, playDeathSound = false ) removeAnchor( player, playDeathSound = false )
val spawnLoc = player.location.clone() val spawnLoc = player.location.clone()
val world = spawnLoc.world ?: return AbilityResult.ConditionNotMet( "World is null" ) val world = spawnLoc.world ?: return AbilityResult.ConditionNotMet( "World is null" )
// Eisengolem spawnen // Werte zum Aktivierungszeitpunkt snapshotten
val capturedGolemHp = golemHp
val capturedPartialResistance = partialResistance
val capturedMonitorInterval = monitorIntervalTicks
val capturedRadius = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveRadius
Playstyle.DEFENSIVE -> defensiveRadius
}
val golem = world.spawn( spawnLoc, IronGolem::class.java ) { g -> val golem = world.spawn( spawnLoc, IronGolem::class.java ) { g ->
g.setAI(false) // keine Bewegung, kein Angriff g.setAI( false )
g.isSilent = true // Todesklang manuell kontrollieren g.isSilent = true
g.isInvulnerable = false // muss zerstörbar sein g.isInvulnerable = false
g.customName(mm.deserialize( "<gray>⚓ <white>Anker</white>" )) g.customName(mm.deserialize( "<gray>⚓ <white>Anker</white>" ))
g.isCustomNameVisible = true g.isCustomNameVisible = true
// HP reduzieren (vanilla = 100 HP) g.getAttribute( Attribute.GENERIC_MAX_HEALTH )?.baseValue = capturedGolemHp
g.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.baseValue = GOLEM_HP g.health = capturedGolemHp
g.health = GOLEM_HP
// PDC: Besitzer-UUID für spätere Identifikation
g.persistentDataContainer.set( g.persistentDataContainer.set(
NamespacedKey( plugin, PDC_KEY ), NamespacedKey( plugin, PDC_KEY ),
PersistentDataType.STRING, PersistentDataType.STRING,
@@ -172,38 +239,38 @@ class AnchorKit : Kit() {
anchorGolems[ player.uniqueId ] = golem anchorGolems[ player.uniqueId ] = golem
// Monitor-Task: prüft Golem-Zustand + aktualisiert Rückschlag-Resistenz
val task = Bukkit.getScheduler().runTaskTimer( plugin, { -> val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
val activeGolem = anchorGolems[ player.uniqueId ] val activeGolem = anchorGolems[ player.uniqueId ]
if (activeGolem == null || activeGolem.isDead || !activeGolem.isValid) { if ( activeGolem == null || activeGolem.isDead || !activeGolem.isValid )
// Golem wurde von Gegnern zerstört {
if (activeGolem?.isDead == true) { if ( activeGolem?.isDead == true )
{
onAnchorDestroyed( player, activeGolem.location ) onAnchorDestroyed( player, activeGolem.location )
} }
monitorTasks.remove( player.uniqueId )?.cancel() monitorTasks.remove( player.uniqueId )?.cancel()
// Resistenz zurück auf Basis-Wert (Golem ist weg) if ( player.isOnline )
if (player.isOnline) { {
player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE ) player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE )
?.baseValue = PARTIAL_RESISTANCE ?.baseValue = capturedPartialResistance
} }
return@runTaskTimer return@runTaskTimer
} }
if (!player.isOnline) { if ( !player.isOnline )
{
activeGolem.remove() activeGolem.remove()
anchorGolems.remove( player.uniqueId ) anchorGolems.remove( player.uniqueId )
monitorTasks.remove( player.uniqueId )?.cancel() monitorTasks.remove( player.uniqueId )?.cancel()
return@runTaskTimer return@runTaskTimer
} }
// Radius-Check: voller NoKnock im Anker-Radius val inRadius = player.location.distanceSquared( activeGolem.location ) <= capturedRadius * capturedRadius
val inRadius = player.location.distanceSquared(activeGolem.location) <= radius * radius val targetResistance = if ( inRadius ) 1.0 else capturedPartialResistance
val targetResistance = if (inRadius) 1.0 else PARTIAL_RESISTANCE
player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE )?.baseValue = targetResistance player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE )?.baseValue = targetResistance
// Visueller Indikator am Golem (Partikelring) if ( inRadius )
if (inRadius) { {
world.spawnParticle( world.spawnParticle(
Particle.CRIT, Particle.CRIT,
activeGolem.location.clone().add( 0.0, 2.5, 0.0 ), activeGolem.location.clone().add( 0.0, 2.5, 0.0 ),
@@ -211,16 +278,15 @@ class AnchorKit : Kit() {
) )
} }
}, 0L, MONITOR_INTERVAL_TICKS) }, 0L, capturedMonitorInterval )
monitorTasks[ player.uniqueId ] = task monitorTasks[ player.uniqueId ] = task
// Feedback
world.playSound( spawnLoc, Sound.ENTITY_IRON_GOLEM_HURT, 1f, 0.5f ) world.playSound( spawnLoc, Sound.ENTITY_IRON_GOLEM_HURT, 1f, 0.5f )
world.spawnParticle( Particle.CLOUD, spawnLoc.clone().add( 0.0, 1.0, 0.0 ), 20, 0.5, 0.3, 0.5, 0.05 ) world.spawnParticle( Particle.CLOUD, spawnLoc.clone().add( 0.0, 1.0, 0.0 ), 20, 0.5, 0.3, 0.5, 0.05 )
player.sendActionBar( player.sendActionBar(
player.trans( "kits.anchor.messages.anchor_placed", player.trans( "kits.anchor.messages.anchor_placed",
"radius" to radius.toInt().toString()) "radius" to capturedRadius.toInt().toString() )
) )
return AbilityResult.Success return AbilityResult.Success
} }
@@ -232,8 +298,7 @@ class AnchorKit : Kit() {
inner class AnchorPassive( inner class AnchorPassive(
playstyle: Playstyle, playstyle: Playstyle,
private val radius: Double, private val bonusDamage: Boolean,
private val bonusDamage: Double,
private val resistanceBonus: Boolean private val resistanceBonus: Boolean
) : PassiveAbility( playstyle ) { ) : PassiveAbility( playstyle ) {
@@ -244,15 +309,25 @@ class AnchorKit : Kit() {
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.anchor.passive.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.anchor.passive.description" )
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { override fun onHitEnemy(
attacker: Player,
victim: Player,
event: org.bukkit.event.entity.EntityDamageByEntityEvent
) {
val golem = anchorGolems[ attacker.uniqueId ] ?: return val golem = anchorGolems[ attacker.uniqueId ] ?: return
// Nur wirksam wenn Angreifer im Radius val capturedRadius = when( playstyle )
if (attacker.location.distanceSquared(golem.location) > radius * radius) return {
Playstyle.AGGRESSIVE -> aggressiveRadius
Playstyle.DEFENSIVE -> defensiveRadius
}
// Bonus-Schaden (Aggressive playstyle) if ( attacker.location.distanceSquared( golem.location ) > capturedRadius * capturedRadius ) return
if (bonusDamage > 0.0) {
event.damage += bonusDamage if ( bonusDamage )
{
val capturedBonusDmg = aggressiveBonusDmg
event.damage += capturedBonusDmg
attacker.world.spawnParticle( attacker.world.spawnParticle(
Particle.CRIT, Particle.CRIT,
victim.location.clone().add( 0.0, 1.2, 0.0 ), victim.location.clone().add( 0.0, 1.2, 0.0 ),
@@ -261,13 +336,18 @@ class AnchorKit : Kit() {
} }
} }
override fun onHitByEnemy(victim: Player, attacker: Player, event: EntityDamageByEntityEvent) { override fun onHitByEnemy(
victim: Player,
attacker: Player,
event: org.bukkit.event.entity.EntityDamageByEntityEvent
) {
if ( !resistanceBonus ) return if ( !resistanceBonus ) return
val golem = anchorGolems[ victim.uniqueId ] ?: return val golem = anchorGolems[ victim.uniqueId ] ?: return
// Resistance I während im Radius (Defensive playstyle) val capturedRadius = defensiveRadius
if (victim.location.distanceSquared(golem.location) <= radius * radius) {
// Schaden um ~20 % reduzieren (Resistance I Äquivalent) if ( victim.location.distanceSquared( golem.location ) <= capturedRadius * capturedRadius )
{
event.damage *= 0.80 event.damage *= 0.80
} }
} }
@@ -277,26 +357,24 @@ class AnchorKit : Kit() {
// Hilfsmethoden // Hilfsmethoden
// ========================================================================= // =========================================================================
/** private fun removeAnchor(
* Entfernt den aktiven Anker eines Spielers sauber. player: Player,
* @param playDeathSound Falls `true`, wird der Eisengolem-Todesklang abgespielt. playDeathSound: Boolean
*/ ) {
private fun removeAnchor(player: Player, playDeathSound: Boolean) {
monitorTasks.remove( player.uniqueId )?.cancel() monitorTasks.remove( player.uniqueId )?.cancel()
val golem = anchorGolems.remove( player.uniqueId ) ?: return val golem = anchorGolems.remove( player.uniqueId ) ?: return
if (playDeathSound && golem.isValid) { if ( playDeathSound && golem.isValid )
{
golem.world.playSound( golem.location, Sound.ENTITY_IRON_GOLEM_DEATH, 1f, 1f ) golem.world.playSound( golem.location, Sound.ENTITY_IRON_GOLEM_DEATH, 1f, 1f )
} }
if ( golem.isValid ) golem.remove() if ( golem.isValid ) golem.remove()
} }
/** private fun onAnchorDestroyed(
* Wird aufgerufen, wenn der Golem von Gegnern zerstört wurde (HP == 0). player: Player,
* Der Golem ist zu diesem Zeitpunkt bereits `isDead`, wir spielen den Sound manuell deathLocation: Location
* (weil der Golem mit `isSilent = true` gespawnt wurde). ) {
*/
private fun onAnchorDestroyed(player: Player, deathLocation: Location) {
anchorGolems.remove( player.uniqueId ) anchorGolems.remove( player.uniqueId )
deathLocation.world?.playSound( deathLocation, Sound.ENTITY_IRON_GOLEM_DEATH, 1f, 1f ) deathLocation.world?.playSound( deathLocation, Sound.ENTITY_IRON_GOLEM_DEATH, 1f, 1f )
@@ -304,9 +382,11 @@ class AnchorKit : Kit() {
Particle.EXPLOSION, deathLocation, 3, 0.3, 0.3, 0.3, 0.0 Particle.EXPLOSION, deathLocation, 3, 0.3, 0.3, 0.3, 0.0
) )
if (player.isOnline) { if ( player.isOnline )
{
player.sendActionBar( player.trans( "kits.anchor.messages.anchor_destroyed" ) ) player.sendActionBar( player.trans( "kits.anchor.messages.anchor_destroyed" ) )
player.playSound( player.location, Sound.ENTITY_IRON_GOLEM_DEATH, 0.8f, 1.3f ) player.playSound( player.location, Sound.ENTITY_IRON_GOLEM_DEATH, 0.8f, 1.3f )
} }
} }
} }

View File

@@ -1,7 +1,6 @@
package club.mcscrims.speedhg.kit.impl package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
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
@@ -27,25 +26,27 @@ import java.util.concurrent.ConcurrentHashMap
* *
* | Playstyle | Active | Passive | * | Playstyle | Active | Passive |
* |-------------|-------------------------------------------------|-------------------------------------------------| * |-------------|-------------------------------------------------|-------------------------------------------------|
* | AGGRESSIVE | **Push** knockback all enemies ≤ 5 blocks + | **Vibranium Fists** 6.5 dmg bare-hand for 12 s| * | AGGRESSIVE | **Push** knockback all enemies ≤ [pushRadius] blocks + | **Vibranium Fists** [fistModeDurationMs]ms bare-hand |
* | | shoot push-projectiles + activate Fist Mode | | * | | shoot push-projectiles + activate Fist Mode | |
* | DEFENSIVE | (no active item) | **Wakanda Forever!** fall-pounce on enemies | * | DEFENSIVE | (no active item) | **Wakanda Forever!** fall-pounce on enemies |
* *
* ### Push (AGGRESSIVE active) * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* All enemies within 5 blocks are launched outward. A marked Snowball is fired
* toward each pushed enemy 5 ticks later; on hit it deals **4 bonus damage**
* (handled by [KitEventDispatcher] via [PUSH_PROJECTILE_KEY]).
* CRIT particles spawn at each pushed enemy's position for visual feedback.
* Directly after the push, **Fist Mode** activates for 12 seconds.
* *
* ### Vibranium Fists (AGGRESSIVE passive) * Typisierte Felder in [CustomGameSettings.KitOverride] werden direkt gelesen.
* During Fist Mode, bare-hand attacks (`itemInMainHand == AIR`) override the * Zusätzliche Einstellungen über die `extras`-Map.
* normal damage to **6.5 HP (3.25 hearts)**.
* *
* ### Wakanda Forever! (DEFENSIVE passive) * | Quelle | JSON-Schlüssel | Typ | Default | Beschreibung |
* Triggers in [onHitEnemy] when `attacker.fallDistance ≥ 3`. Deals **6 HP** * |------------------|-----------------------------|--------|----------|-------------------------------------------|
* to all enemies within 3 blocks of the victim, then creates an explosion * | Typisiertes Feld | `fist_mode_ms` | Long | `12000` | Dauer des Fist-Modus (ms) |
* visual and a small WorldEdit crater at the landing site. * | Typisiertes Feld | `push_radius` | Double | `5.0` | Radius der Push-Schockwelle (Blöcke) |
* | Typisiertes Feld | `push_bonus_damage` | Double | `4.0` | Bonus-Schaden der Push-Projektile (HP) |
* | Typisiertes Feld | `pounce_min_fall` | Float | `3.0` | Mindest-Fallhöhe für Wakanda-Pounce |
* | Typisiertes Feld | `pounce_radius` | Double | `3.0` | AoE-Radius des Pounce-Aufpralls (Blöcke) |
* | Typisiertes Feld | `pounce_damage` | Double | `6.0` | Schaden des Pounce-Aufpralls (HP) |
* | `extras` | `push_knockback_speed` | Double | `2.0` | Horizontaler Velocity-Multiplikator |
* | `extras` | `push_knockback_y` | Double | `0.45` | Vertikaler Y-Impuls des Pushes |
* | `extras` | `fist_mode_damage` | Double | `6.5` | Schaden der Vibranium-Fists (HP) |
* | `extras` | `projectile_delay_ticks` | Long | `5` | Ticks Verzögerung vor dem Projektil |
*/ */
class BlackPantherKit : Kit() class BlackPantherKit : Kit()
{ {
@@ -67,32 +68,111 @@ class BlackPantherKit : Kit()
companion object companion object
{ {
private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"]
?: CustomGameSettings.KitOverride()
/** PDC key string shared with [KitEventDispatcher] for push-projectiles. */ /** PDC key string shared with [KitEventDispatcher] for push-projectiles. */
const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile" const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile"
private val FIST_MODE_MS = override().fistModeDurationMs // 12 seconds const val DEFAULT_FIST_MODE_DURATION_MS = 12_000L
private val PUSH_RADIUS = override().pushRadius const val DEFAULT_PUSH_RADIUS = 5.0
private val POUNCE_MIN_FALL = override().pounceMinFall const val DEFAULT_PUSH_BONUS_DAMAGE = 4.0
private val POUNCE_RADIUS = override().pounceRadius const val DEFAULT_POUNCE_MIN_FALL = 3.0f
private val POUNCE_DAMAGE = override().pounceDamage // 3 hearts = 6 HP const val DEFAULT_POUNCE_RADIUS = 3.0
const val DEFAULT_POUNCE_DAMAGE = 6.0
const val DEFAULT_PUSH_KNOCKBACK_SPEED = 2.0
const val DEFAULT_PUSH_KNOCKBACK_Y = 0.45
const val DEFAULT_FIST_MODE_DAMAGE = 6.5
const val DEFAULT_PROJECTILE_DELAY_TICKS = 5L
} }
// ── Cached ability instances ────────────────────────────────────────────── // ── Live config accessors ─────────────────────────────────────────────────
/**
* Dauer des Vibranium-Fist-Modus in Millisekunden.
* Quelle: typisiertes Feld `fist_mode_ms`.
*/
private val fistModeDurationMs: Long
get() = override().fistModeDurationMs
/**
* Radius der Push-Schockwelle in Blöcken.
* Quelle: typisiertes Feld `push_radius`.
*/
private val pushRadius: Double
get() = override().pushRadius
/**
* Bonus-Schaden der nachfolgenden Push-Projektile in HP.
* Quelle: typisiertes Feld `push_bonus_damage`.
*/
private val pushBonusDamage: Double
get() = override().pushBonusDamage
/**
* Mindest-Fallhöhe für den Wakanda-Forever-Pounce.
* Quelle: typisiertes Feld `pounce_min_fall`.
*/
private val pounceMinFall: Float
get() = override().pounceMinFall
/**
* AoE-Radius des Wakanda-Pounce-Aufpralls in Blöcken.
* Quelle: typisiertes Feld `pounce_radius`.
*/
private val pounceRadius: Double
get() = override().pounceRadius
/**
* Schaden des Wakanda-Pounce-Aufpralls in HP.
* Quelle: typisiertes Feld `pounce_damage`.
*/
private val pounceDamage: Double
get() = override().pounceDamage
/**
* Horizontaler Velocity-Multiplikator des Pushes.
* Quelle: `extras["push_knockback_speed"]`.
*/
private val pushKnockbackSpeed: Double
get() = override().getDouble( "push_knockback_speed" ) ?: DEFAULT_PUSH_KNOCKBACK_SPEED
/**
* Vertikaler Y-Impuls des Pushes.
* Quelle: `extras["push_knockback_y"]`.
*/
private val pushKnockbackY: Double
get() = override().getDouble( "push_knockback_y" ) ?: DEFAULT_PUSH_KNOCKBACK_Y
/**
* Schaden der Vibranium-Fists pro Treffer in HP (3,25 Herzen Standard).
* Quelle: `extras["fist_mode_damage"]`.
*/
private val fistModeDamage: Double
get() = override().getDouble( "fist_mode_damage" ) ?: DEFAULT_FIST_MODE_DAMAGE
/**
* Ticks Verzögerung vor dem Abschuss des Push-Projektils.
* Quelle: `extras["projectile_delay_ticks"]`.
*/
private val projectileDelayTicks: Long
get() = override().getLong( "projectile_delay_ticks" ) ?: DEFAULT_PROJECTILE_DELAY_TICKS
// ── Gecachte Instanzen ────────────────────────────────────────────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
private val aggressivePassive = AggressivePassive() private val aggressivePassive = AggressivePassive()
private val defensivePassive = DefensivePassive() private val defensivePassive = DefensivePassive()
override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) override fun getActiveAbility(
playstyle: Playstyle
) = when( playstyle )
{ {
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) override fun getPassiveAbility(
playstyle: Playstyle
) = when( playstyle )
{ {
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
@@ -100,7 +180,10 @@ class BlackPantherKit : Kit()
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(player: Player, playstyle: Playstyle) { override fun giveItems(
player: Player,
playstyle: Playstyle
) {
if ( playstyle != Playstyle.AGGRESSIVE ) return if ( playstyle != Playstyle.AGGRESSIVE ) return
val item = ItemBuilder( Material.BLACK_DYE ) val item = ItemBuilder( Material.BLACK_DYE )
.name( aggressiveActive.name ) .name( aggressiveActive.name )
@@ -122,12 +205,15 @@ class BlackPantherKit : Kit()
// AGGRESSIVE active Push + activate Fist Mode // AGGRESSIVE active Push + activate Fist Mode
// ========================================================================= // =========================================================================
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String get() = "blackpanther" override val kitId: String
override val hardcodedHitsRequired: Int get() = 15 get() = "blackpanther"
override val hardcodedHitsRequired: Int
get() = 15
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.items.push.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.items.push.name" )
@@ -144,36 +230,40 @@ class BlackPantherKit : Kit()
plugin.languageManager.getRawMessage( player, "kits.height_restriction" ) plugin.languageManager.getRawMessage( player, "kits.height_restriction" )
) )
// Werte zum Aktivierungszeitpunkt snapshotten
val capturedPushRadius = pushRadius
val capturedKnockbackSpeed = pushKnockbackSpeed
val capturedKnockbackY = pushKnockbackY
val capturedFistModeDurationMs = fistModeDurationMs
val capturedProjectileDelay = projectileDelayTicks
val enemies = player.world val enemies = player.world
.getNearbyEntities(player.location, PUSH_RADIUS, PUSH_RADIUS, PUSH_RADIUS) .getNearbyEntities( player.location, capturedPushRadius, capturedPushRadius, capturedPushRadius )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
if ( enemies.isEmpty() ) if ( enemies.isEmpty() )
return AbilityResult.ConditionNotMet("No enemies within ${PUSH_RADIUS.toInt()} blocks!") return AbilityResult.ConditionNotMet( "No enemies within ${capturedPushRadius.toInt()} blocks!" )
val pushKey = (plugin.kitManager.getSelectedKit(player) as? BlackPantherKit) val pushKey = NamespacedKey( plugin, PUSH_PROJECTILE_KEY )
?.let { NamespacedKey(plugin, PUSH_PROJECTILE_KEY) }
enemies.forEach { enemy -> enemies.forEach { enemy ->
// ── Knockback ──────────────────────────────────────────────────
val knockDir = enemy.location.toVector() val knockDir = enemy.location.toVector()
.subtract( player.location.toVector() ) .subtract( player.location.toVector() )
.normalize() .normalize()
.multiply(2.0) .multiply( capturedKnockbackSpeed )
.setY(0.45) .setY( capturedKnockbackY )
enemy.velocity = knockDir enemy.velocity = knockDir
enemy.world.spawnParticle(Particle.CRIT, enemy.world.spawnParticle(
enemy.location.clone().add(0.0, 1.0, 0.0), 10, 0.3, 0.3, 0.3, 0.0) Particle.CRIT,
enemy.location.clone().add( 0.0, 1.0, 0.0 ),
10, 0.3, 0.3, 0.3, 0.0
)
// ── Trailing push-projectile (deals 4 HP on hit) ──────────────
if (pushKey != null) {
Bukkit.getScheduler().runTaskLater( plugin, { -> Bukkit.getScheduler().runTaskLater( plugin, { ->
if ( !player.isOnline ) return@runTaskLater if ( !player.isOnline ) return@runTaskLater
val snowball = player.world.spawn( val snowball = player.world.spawn( player.eyeLocation, Snowball::class.java )
player.eyeLocation, Snowball::class.java
)
snowball.shooter = player snowball.shooter = player
val travelDir = enemy.location.toVector() val travelDir = enemy.location.toVector()
.subtract( player.eyeLocation.toVector() ) .subtract( player.eyeLocation.toVector() )
@@ -181,12 +271,10 @@ class BlackPantherKit : Kit()
.multiply( 1.8 ) .multiply( 1.8 )
snowball.velocity = travelDir snowball.velocity = travelDir
snowball.persistentDataContainer.set( pushKey, PersistentDataType.BYTE, 1 ) snowball.persistentDataContainer.set( pushKey, PersistentDataType.BYTE, 1 )
}, 5L) }, capturedProjectileDelay )
}
} }
// ── Activate Fist Mode ───────────────────────────────────────────── fistModeExpiry[ player.uniqueId ] = System.currentTimeMillis() + capturedFistModeDurationMs
fistModeExpiry[player.uniqueId] = System.currentTimeMillis() + FIST_MODE_MS
player.sendActionBar( player.trans( "kits.blackpanther.messages.fist_mode_active" ) ) player.sendActionBar( player.trans( "kits.blackpanther.messages.fist_mode_active" ) )
player.world.playSound( player.location, Sound.ENTITY_RAVAGER_ROAR, 1f, 1.1f ) player.world.playSound( player.location, Sound.ENTITY_RAVAGER_ROAR, 1f, 1.1f )
@@ -200,20 +288,24 @@ class BlackPantherKit : Kit()
// DEFENSIVE active no active ability // DEFENSIVE active no active ability
// ========================================================================= // =========================================================================
private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
{
override val kitId: String = "blackpanther" override val kitId: String = "blackpanther"
override val name = "None" override val name = "None"
override val description = "None" override val description = "None"
override val hardcodedHitsRequired: Int = 0 override val hardcodedHitsRequired: Int = 0
override val triggerMaterial = Material.BARRIER override val triggerMaterial = Material.BARRIER
override fun execute(player: Player) = AbilityResult.Success override fun execute(
player: Player
) = AbilityResult.Success
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE passive Vibranium Fists (6.5 dmg bare-hand during Fist Mode) // AGGRESSIVE passive Vibranium Fists (bare-hand during Fist Mode)
// ========================================================================= // =========================================================================
private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -222,18 +314,27 @@ class BlackPantherKit : Kit()
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.passive.aggressive.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.passive.aggressive.description" )
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { override fun onHitEnemy(
attacker: Player,
victim: Player,
event: EntityDamageByEntityEvent
) {
val expiry = fistModeExpiry[ attacker.uniqueId ] ?: return val expiry = fistModeExpiry[ attacker.uniqueId ] ?: return
if (System.currentTimeMillis() > expiry) { if ( System.currentTimeMillis() > expiry )
{
fistModeExpiry.remove( attacker.uniqueId ) fistModeExpiry.remove( attacker.uniqueId )
return return
} }
if ( attacker.inventory.itemInMainHand.type != Material.AIR ) return if ( attacker.inventory.itemInMainHand.type != Material.AIR ) return
event.damage = 6.5 // 3.25 hearts val capturedFistDamage = fistModeDamage
victim.world.spawnParticle(Particle.CRIT, event.damage = capturedFistDamage
victim.location.clone().add(0.0, 1.0, 0.0), 8, 0.3, 0.3, 0.3, 0.0) victim.world.spawnParticle(
Particle.CRIT,
victim.location.clone().add( 0.0, 1.0, 0.0 ),
8, 0.3, 0.3, 0.3, 0.0
)
attacker.playSound( attacker.location, Sound.ENTITY_PLAYER_ATTACK_CRIT, 1f, 0.9f ) attacker.playSound( attacker.location, Sound.ENTITY_PLAYER_ATTACK_CRIT, 1f, 0.9f )
} }
} }
@@ -242,7 +343,8 @@ class BlackPantherKit : Kit()
// DEFENSIVE passive Wakanda Forever! (fall-pounce → AOE + crater) // DEFENSIVE passive Wakanda Forever! (fall-pounce → AOE + crater)
// ========================================================================= // =========================================================================
private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -256,19 +358,25 @@ class BlackPantherKit : Kit()
event: PlayerMoveEvent event: PlayerMoveEvent
) { ) {
if ( event.to.y >= event.from.y ) return if ( event.to.y >= event.from.y ) return
if ( player.fallDistance < POUNCE_MIN_FALL ) return
val capturedPounceMinFall = pounceMinFall
if ( player.fallDistance < capturedPounceMinFall ) return
val blockBelow = event.to.clone().subtract( 0.0, 0.1, 0.0 ).block val blockBelow = event.to.clone().subtract( 0.0, 0.1, 0.0 ).block
if ( !blockBelow.type.isSolid ) return if ( !blockBelow.type.isSolid ) return
val impactLoc = event.to.clone() val impactLoc = event.to.clone()
// Werte zum Aktivierungszeitpunkt snapshotten
val capturedPounceRadius = pounceRadius
val capturedPounceDamage = pounceDamage
val splashTargets = impactLoc.world val splashTargets = impactLoc.world
.getNearbyEntities( impactLoc, POUNCE_RADIUS, POUNCE_RADIUS, POUNCE_RADIUS ) .getNearbyEntities( impactLoc, capturedPounceRadius, capturedPounceRadius, capturedPounceRadius )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
splashTargets.forEach { it.damage( POUNCE_DAMAGE, player ) } splashTargets.forEach { it.damage( capturedPounceDamage, player ) }
impactLoc.world.spawnParticle( Particle.EXPLOSION, impactLoc, 3, 0.5, 0.5, 0.5, 0.0 ) impactLoc.world.spawnParticle( Particle.EXPLOSION, impactLoc, 3, 0.5, 0.5, 0.5, 0.0 )
impactLoc.world.spawnParticle( Particle.LARGE_SMOKE, impactLoc, 20, 1.0, 0.5, 1.0, 0.05 ) impactLoc.world.spawnParticle( Particle.LARGE_SMOKE, impactLoc, 20, 1.0, 0.5, 1.0, 0.05 )
@@ -282,10 +390,11 @@ class BlackPantherKit : Kit()
) )
}, 2L ) }, 2L )
player.sendActionBar(player.trans("kits.blackpanther.messages.wakanda_impact", player.sendActionBar(
mapOf("count" to splashTargets.size.toString()))) player.trans( "kits.blackpanther.messages.wakanda_impact",
mapOf( "count" to splashTargets.size.toString() ) )
)
// Suppress fall damage for this landing
noFallDamagePlayers.add( player.uniqueId ) noFallDamagePlayers.add( player.uniqueId )
player.fallDistance = 0f player.fallDistance = 0f
} }

View File

@@ -6,7 +6,6 @@ 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.ability.ActiveAbility import club.mcscrims.speedhg.kit.ability.ActiveAbility
import club.mcscrims.speedhg.kit.ability.PassiveAbility import club.mcscrims.speedhg.kit.ability.PassiveAbility
import club.mcscrims.speedhg.kit.listener.KitEventDispatcher
import club.mcscrims.speedhg.kit.listener.KitEventDispatcher.Companion.MAX_KNOCKBACK_HEIGHT_Y import club.mcscrims.speedhg.kit.listener.KitEventDispatcher.Companion.MAX_KNOCKBACK_HEIGHT_Y
import club.mcscrims.speedhg.util.ItemBuilder import club.mcscrims.speedhg.util.ItemBuilder
import club.mcscrims.speedhg.util.trans import club.mcscrims.speedhg.util.trans
@@ -35,26 +34,28 @@ import kotlin.math.sin
* | Playstyle | Aktive Fähigkeit | * | Playstyle | Aktive Fähigkeit |
* |-------------|---------------------------------------------------------------| * |-------------|---------------------------------------------------------------|
* | AGGRESSIVE | **Hook** zieht ersten Feind in der Schusslinie heran | * | AGGRESSIVE | **Hook** zieht ersten Feind in der Schusslinie heran |
* | DEFENSIVE | **Stun** friert alle nahen Feinde für 3 s ein | * | DEFENSIVE | **Stun** friert alle nahen Feinde für [stunDurationTicks] Ticks ein |
* | Beide | **Ult** expandierende Schockwelle + AoE-Schaden | * | Beide | **Ult** expandierende Schockwelle + AoE-Schaden |
* *
* ### Hook synchroner Raycast * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* 0,4-Block-Schritte von `eyeLocation` entlang `eyeLocation.direction`.
* Erster Feind getroffen → Velocity-Pull Richtung Caster. Alle Partikel werden
* synchron im selben Tick gezeichnet.
* *
* ### Stun Freeze-Mechanismus * Alle Werte werden über die `extras`-Map konfiguriert und fallen auf die
* Slowness 127 + Mining Fatigue 127 für [STUN_DURATION_TICKS] Ticks. * Defaults im [companion object] zurück, wenn kein Wert gesetzt ist.
* Zusätzlich setzt ein BukkitTask die Velocity aller gestunnten Spieler auf 0.
* *
* ### Ult passive onInteract als Auslöser * | JSON-Schlüssel | Typ | Default | Beschreibung |
* Das Ult-Item (BLAZE_POWDER) besitzt einen PDC-Tag ([ultItemKey]). * |-------------------------|--------|-------------|----------------------------------------------|
* `KitEventDispatcher.onInteract` ruft **zuerst** `passive.onInteract` auf, * | `hook_range` | Double | `10.0` | Maximale Reichweite des Hooks (Blöcke) |
* dann erst den triggerMaterial-Check. [UltPassive.onInteract] fängt das * | `hook_pull_strength` | Double | `2.7` | Velocity-Multiplikator beim Pull |
* BLAZE_POWDER-Rechtsklick-Event ab und cancelt es, bevor der Dispatcher * | `stun_radius` | Double | `5.0` | AoE-Radius des Stuns (Blöcke) |
* etwas unternimmt → kein Dispatcher-Umbau notwendig. * | `stun_duration_ticks` | Long | `60` | Dauer des Stuns in Ticks (3 Sekunden) |
* | `ult_radius` | Double | `6.0` | Radius der Ult-Schockwelle (Blöcke) |
* | `ult_damage` | Double | `5.0` | Schaden der Ult pro Treffer (HP) |
* | `ult_cooldown_ms` | Long | `30000` | Cooldown der Ult zwischen Uses (ms) |
* | `hook_step_size` | Double | `0.4` | Raycast-Schrittgröße für den Hook (Blöcke) |
* | `hook_hit_radius` | Double | `0.6` | Kollisionsradius des Hooks (Blöcke) |
*/ */
class BlitzcrankKit : Kit() { class BlitzcrankKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -76,15 +77,84 @@ class BlitzcrankKit : Kit() {
private val ultCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap() private val ultCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object { companion object {
const val HOOK_RANGE = 10.0 // Blöcke const val DEFAULT_HOOK_RANGE = 10.0
const val HOOK_PULL_STRENGTH = 2.7 // Velocity-Multiplikator const val DEFAULT_HOOK_PULL_STRENGTH = 2.7
const val STUN_RADIUS = 5.0 // Blöcke const val DEFAULT_STUN_RADIUS = 5.0
const val STUN_DURATION_TICKS = 60 // 3 Sekunden const val DEFAULT_STUN_DURATION_TICKS = 60L
const val ULT_RADIUS = 6.0 // Blöcke const val DEFAULT_ULT_RADIUS = 6.0
const val ULT_DAMAGE = 5.0 // 2,5 Herzen const val DEFAULT_ULT_DAMAGE = 5.0
const val ULT_COOLDOWN_MS = 30_000L const val DEFAULT_ULT_COOLDOWN_MS = 30_000L
const val DEFAULT_HOOK_STEP_SIZE = 0.4
const val DEFAULT_HOOK_HIT_RADIUS = 0.6
} }
// ── Live config accessors ─────────────────────────────────────────────────
/**
* Maximale Reichweite des Hooks in Blöcken.
* JSON-Schlüssel: `hook_range`
*/
private val hookRange: Double
get() = override().getDouble( "hook_range" ) ?: DEFAULT_HOOK_RANGE
/**
* Velocity-Multiplikator des Hook-Pulls.
* JSON-Schlüssel: `hook_pull_strength`
*/
private val hookPullStrength: Double
get() = override().getDouble( "hook_pull_strength" ) ?: DEFAULT_HOOK_PULL_STRENGTH
/**
* AoE-Radius des Stuns in Blöcken.
* JSON-Schlüssel: `stun_radius`
*/
private val stunRadius: Double
get() = override().getDouble( "stun_radius" ) ?: DEFAULT_STUN_RADIUS
/**
* Dauer des Stuns in Ticks.
* JSON-Schlüssel: `stun_duration_ticks`
*/
private val stunDurationTicks: Long
get() = override().getLong( "stun_duration_ticks" ) ?: DEFAULT_STUN_DURATION_TICKS
/**
* Radius der Ult-Schockwelle in Blöcken.
* JSON-Schlüssel: `ult_radius`
*/
private val ultRadius: Double
get() = override().getDouble( "ult_radius" ) ?: DEFAULT_ULT_RADIUS
/**
* Schaden der Ult pro getroffenen Spieler in HP.
* JSON-Schlüssel: `ult_damage`
*/
private val ultDamage: Double
get() = override().getDouble( "ult_damage" ) ?: DEFAULT_ULT_DAMAGE
/**
* Cooldown der Ult in Millisekunden.
* JSON-Schlüssel: `ult_cooldown_ms`
*/
private val ultCooldownMs: Long
get() = override().getLong( "ult_cooldown_ms" ) ?: DEFAULT_ULT_COOLDOWN_MS
/**
* Raycast-Schrittgröße des Hooks in Blöcken.
* JSON-Schlüssel: `hook_step_size`
*/
private val hookStepSize: Double
get() = override().getDouble( "hook_step_size" ) ?: DEFAULT_HOOK_STEP_SIZE
/**
* Kollisionsradius des Hooks in Blöcken.
* JSON-Schlüssel: `hook_hit_radius`
*/
private val hookHitRadius: Double
get() = override().getDouble( "hook_hit_radius" ) ?: DEFAULT_HOOK_HIT_RADIUS
// ── Gecachte Instanzen ────────────────────────────────────────────────────
private val aggressiveActive = HookActive() private val aggressiveActive = HookActive()
private val defensiveActive = StunActive() private val defensiveActive = StunActive()
private val aggressivePassive = UltPassive( Playstyle.AGGRESSIVE ) private val aggressivePassive = UltPassive( Playstyle.AGGRESSIVE )
@@ -112,7 +182,8 @@ class BlitzcrankKit : Kit() {
player: Player, player: Player,
playstyle: Playstyle playstyle: Playstyle
) { ) {
val mainItem = when (playstyle) { val mainItem = when( playstyle )
{
Playstyle.AGGRESSIVE -> ItemBuilder( Material.FISHING_ROD ) Playstyle.AGGRESSIVE -> ItemBuilder( Material.FISHING_ROD )
.name( aggressiveActive.name ) .name( aggressiveActive.name )
.lore(listOf( aggressiveActive.description )) .lore(listOf( aggressiveActive.description ))
@@ -157,15 +228,20 @@ class BlitzcrankKit : Kit() {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val lastUlt = ultCooldowns[ caster.uniqueId ] ?: 0L val lastUlt = ultCooldowns[ caster.uniqueId ] ?: 0L
if ( now - lastUlt < ULT_COOLDOWN_MS ) // Werte zum Aktivierungszeitpunkt snapshotten
val capturedUltCooldownMs = ultCooldownMs
val capturedUltRadius = ultRadius
val capturedUltDamage = ultDamage
if ( now - lastUlt < capturedUltCooldownMs )
{ {
val secLeft = ( ULT_COOLDOWN_MS - ( now - lastUlt )) / 1000 val secLeft = ( capturedUltCooldownMs - ( now - lastUlt ) ) / 1000
caster.sendActionBar( caster.trans( "kits.blitzcrank.messages.ult_cooldown", "time" to secLeft.toString() ) ) caster.sendActionBar( caster.trans( "kits.blitzcrank.messages.ult_cooldown", "time" to secLeft.toString() ) )
return return
} }
val targets = caster.world val targets = caster.world
.getNearbyEntities( caster.location, ULT_RADIUS, ULT_RADIUS, ULT_RADIUS ) .getNearbyEntities( caster.location, capturedUltRadius, capturedUltRadius, capturedUltRadius )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != caster && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .filter { it != caster && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
@@ -180,7 +256,7 @@ class BlitzcrankKit : Kit() {
override fun run() override fun run()
{ {
if ( r > ULT_RADIUS + 1.0 ) { cancel(); return } if ( r > capturedUltRadius + 1.0 ) { cancel(); return }
val steps = ( 2 * Math.PI * r * 5 ).toInt().coerceAtLeast( 8 ) val steps = ( 2 * Math.PI * r * 5 ).toInt().coerceAtLeast( 8 )
repeat( steps ) { i -> repeat( steps ) { i ->
val angle = 2.0 * Math.PI * i / steps val angle = 2.0 * Math.PI * i / steps
@@ -195,7 +271,7 @@ class BlitzcrankKit : Kit() {
}.runTaskTimer( plugin, 0L, 1L ) }.runTaskTimer( plugin, 0L, 1L )
targets.forEach { target -> targets.forEach { target ->
target.damage( ULT_DAMAGE, caster ) target.damage( capturedUltDamage, caster )
target.velocity = target.location.toVector() target.velocity = target.location.toVector()
.subtract( caster.location.toVector() ) .subtract( caster.location.toVector() )
.normalize() .normalize()
@@ -214,7 +290,8 @@ class BlitzcrankKit : Kit() {
// AGGRESSIVE active Hook (synchroner Raycast) // AGGRESSIVE active Hook (synchroner Raycast)
// ========================================================================= // =========================================================================
private inner class HookActive : ActiveAbility(Playstyle.AGGRESSIVE) { private inner class HookActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -226,7 +303,9 @@ class BlitzcrankKit : Kit() {
override val hardcodedHitsRequired = 15 override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.FISHING_ROD override val triggerMaterial = Material.FISHING_ROD
override fun execute(player: Player): AbilityResult override fun execute(
player: Player
): AbilityResult
{ {
if ( player.location.y > MAX_KNOCKBACK_HEIGHT_Y ) if ( player.location.y > MAX_KNOCKBACK_HEIGHT_Y )
return AbilityResult.ConditionNotMet( return AbilityResult.ConditionNotMet(
@@ -236,46 +315,52 @@ class BlitzcrankKit : Kit() {
val eyeLoc = player.eyeLocation val eyeLoc = player.eyeLocation
val dir = eyeLoc.direction.normalize() val dir = eyeLoc.direction.normalize()
var hookTarget: Player? = null // Werte zum Aktivierungszeitpunkt snapshotten
var dist = 0.4 val capturedHookRange = hookRange
val capturedHookStepSize = hookStepSize
val capturedHookHitRadius = hookHitRadius
val capturedPullStrength = hookPullStrength
// Synchroner Scan: trivial schnell (max ~25 Iterationen) var hookTarget: Player? = null
while (dist <= HOOK_RANGE && hookTarget == null) { var dist = capturedHookStepSize
while ( dist <= capturedHookRange && hookTarget == null )
{
val point = eyeLoc.clone().add( dir.clone().multiply( dist ) ) val point = eyeLoc.clone().add( dir.clone().multiply( dist ) )
// Block im Weg → Hook stoppt hier
if ( point.block.type.isSolid ) break if ( point.block.type.isSolid ) break
// Partikel-Trail entlang des Strahls
player.world.spawnParticle( Particle.ELECTRIC_SPARK, point, 1, 0.0, 0.0, 0.0, 0.0 ) player.world.spawnParticle( Particle.ELECTRIC_SPARK, point, 1, 0.0, 0.0, 0.0, 0.0 )
hookTarget = point.world hookTarget = point.world
?.getNearbyEntities(point, 0.6, 0.6, 0.6) ?.getNearbyEntities( point, capturedHookHitRadius, capturedHookHitRadius, capturedHookHitRadius )
?.filterIsInstance<Player>() ?.filterIsInstance<Player>()
?.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } ?.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
?.minByOrNull { it.location.distanceSquared( point ) } ?.minByOrNull { it.location.distanceSquared( point ) }
dist += 0.4 dist += capturedHookStepSize
} }
if (hookTarget == null) { if ( hookTarget == null )
// Kein Treffer Funken am Strahlende {
val endPt = eyeLoc.clone().add(dir.multiply(dist.coerceAtMost(HOOK_RANGE))) val endPt = eyeLoc.clone().add( dir.multiply( dist.coerceAtMost( capturedHookRange ) ) )
player.world.spawnParticle( Particle.ELECTRIC_SPARK, endPt, 10, 0.3, 0.3, 0.3, 0.06 ) player.world.spawnParticle( Particle.ELECTRIC_SPARK, endPt, 10, 0.3, 0.3, 0.3, 0.06 )
return AbilityResult.ConditionNotMet( "Kein Ziel in Reichweite!" ) return AbilityResult.ConditionNotMet( "Kein Ziel in Reichweite!" )
} }
val target = hookTarget val target = hookTarget
// Pull: Velocity in Richtung Caster
target.velocity = player.location.toVector() target.velocity = player.location.toVector()
.subtract( target.location.toVector() ) .subtract( target.location.toVector() )
.normalize() .normalize()
.multiply(HOOK_PULL_STRENGTH) .multiply( capturedPullStrength )
.setY( 0.65 ) .setY( 0.65 )
target.world.spawnParticle(Particle.ELECTRIC_SPARK, target.world.spawnParticle(
target.location.clone().add(0.0, 1.0, 0.0), 22, 0.4, 0.4, 0.4, 0.14) Particle.ELECTRIC_SPARK,
target.location.clone().add( 0.0, 1.0, 0.0 ),
22, 0.4, 0.4, 0.4, 0.14
)
target.world.playSound( target.location, Sound.ENTITY_IRON_GOLEM_HURT, 0.9f, 1.6f ) target.world.playSound( target.location, Sound.ENTITY_IRON_GOLEM_HURT, 0.9f, 1.6f )
target.sendActionBar( target.trans( "kits.blitzcrank.messages.hooked" ) ) target.sendActionBar( target.trans( "kits.blitzcrank.messages.hooked" ) )
@@ -290,7 +375,8 @@ class BlitzcrankKit : Kit() {
// DEFENSIVE active Stun (AoE-Freeze) // DEFENSIVE active Stun (AoE-Freeze)
// ========================================================================= // =========================================================================
private inner class StunActive : ActiveAbility(Playstyle.DEFENSIVE) { private inner class StunActive : ActiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -311,29 +397,33 @@ class BlitzcrankKit : Kit() {
plugin.languageManager.getRawMessage( player, "kits.height_restriction" ) plugin.languageManager.getRawMessage( player, "kits.height_restriction" )
) )
// Werte zum Aktivierungszeitpunkt snapshotten
val capturedStunRadius = stunRadius
val capturedStunDurationTicks = stunDurationTicks
val targets = player.world val targets = player.world
.getNearbyEntities(player.location, STUN_RADIUS, STUN_RADIUS, STUN_RADIUS) .getNearbyEntities( player.location, capturedStunRadius, capturedStunRadius, capturedStunRadius )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
if ( targets.isEmpty() ) if ( targets.isEmpty() )
return AbilityResult.ConditionNotMet("Keine Feinde in ${STUN_RADIUS.toInt()} Blöcken!") return AbilityResult.ConditionNotMet( "Keine Feinde in ${capturedStunRadius.toInt()} Blöcken!" )
targets.forEach { target -> targets.forEach { target ->
// Potion-Effekte für maximales Einfrieren (Amplifier 127 = sofortiger Stopp)
target.addPotionEffect( target.addPotionEffect(
PotionEffect(PotionEffectType.SLOWNESS, STUN_DURATION_TICKS, 127, false, false, true) PotionEffect( PotionEffectType.SLOWNESS, capturedStunDurationTicks.toInt(), 127, false, false, true )
) )
target.addPotionEffect( target.addPotionEffect(
PotionEffect(PotionEffectType.MINING_FATIGUE, STUN_DURATION_TICKS, 127, false, false, false) PotionEffect( PotionEffectType.MINING_FATIGUE, capturedStunDurationTicks.toInt(), 127, false, false, false )
) )
// Velocity-Reset-Task: verhindert Springen und Rutschen
var stunTick = 0 var stunTick = 0
val task = Bukkit.getScheduler().runTaskTimer( plugin, { -> val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
stunTick++ stunTick++
if (stunTick >= STUN_DURATION_TICKS || !target.isOnline || if ( stunTick >= capturedStunDurationTicks ||
!plugin.gameManager.alivePlayers.contains(target.uniqueId)) { !target.isOnline ||
!plugin.gameManager.alivePlayers.contains( target.uniqueId ) )
{
stunTasks.remove( target.uniqueId )?.cancel() stunTasks.remove( target.uniqueId )?.cancel()
return@runTaskTimer return@runTaskTimer
} }
@@ -343,16 +433,24 @@ class BlitzcrankKit : Kit() {
stunTasks[ target.uniqueId ] = task stunTasks[ target.uniqueId ] = task
target.world.spawnParticle(Particle.ELECTRIC_SPARK, target.world.spawnParticle(
target.location.clone().add(0.0, 1.5, 0.0), 25, 0.3, 0.5, 0.3, 0.14) Particle.ELECTRIC_SPARK,
target.location.clone().add( 0.0, 1.5, 0.0 ),
25, 0.3, 0.5, 0.3, 0.14
)
target.sendActionBar( target.trans( "kits.blitzcrank.messages.stunned" ) ) target.sendActionBar( target.trans( "kits.blitzcrank.messages.stunned" ) )
} }
player.world.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 1f, 0.7f ) player.world.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 1f, 0.7f )
player.world.spawnParticle(Particle.ELECTRIC_SPARK, player.world.spawnParticle(
player.location.clone().add(0.0, 1.0, 0.0), 35, 2.0, 0.5, 2.0, 0.14) Particle.ELECTRIC_SPARK,
player.sendActionBar(player.trans("kits.blitzcrank.messages.stun_cast", player.location.clone().add( 0.0, 1.0, 0.0 ),
"count" to targets.size.toString())) 35, 2.0, 0.5, 2.0, 0.14
)
player.sendActionBar(
player.trans( "kits.blitzcrank.messages.stun_cast",
"count" to targets.size.toString() )
)
return AbilityResult.Success return AbilityResult.Success
} }
@@ -362,7 +460,10 @@ class BlitzcrankKit : Kit() {
// Shared Ult-Passive fängt BLAZE_POWDER-Rechtsklick via onInteract ab // Shared Ult-Passive fängt BLAZE_POWDER-Rechtsklick via onInteract ab
// ========================================================================= // =========================================================================
inner class UltPassive(playstyle: Playstyle) : PassiveAbility(playstyle) { inner class UltPassive(
playstyle: Playstyle
) : PassiveAbility( playstyle )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -371,19 +472,19 @@ class BlitzcrankKit : Kit() {
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.passive.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.passive.description" )
/** override fun onInteract(
* Wird vom KitEventDispatcher **vor** dem triggerMaterial-Check aufgerufen. player: Player,
* Prüft PDC-Tag → falls Ult-Item: Event canceln + Ult feuern. event: PlayerInteractEvent
*/ ) {
override fun onInteract(player: Player, event: PlayerInteractEvent) {
if ( !event.action.isRightClick ) return if ( !event.action.isRightClick ) return
val pdc = player.inventory.itemInMainHand.itemMeta val pdc = player.inventory.itemInMainHand.itemMeta
?.persistentDataContainer ?: return ?.persistentDataContainer ?: return
if ( !pdc.has( ultItemKey, PersistentDataType.BYTE ) ) return if ( !pdc.has( ultItemKey, PersistentDataType.BYTE ) ) return
event.isCancelled = true // Vanilla-Interaktion (Feuer-Charge) unterbinden event.isCancelled = true
fireUlt( player ) fireUlt( player )
} }
} }
} }