diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt index 9d2cdcc..a367a55 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt @@ -19,7 +19,6 @@ import org.bukkit.Sound import org.bukkit.attribute.Attribute import org.bukkit.entity.IronGolem import org.bukkit.entity.Player -import org.bukkit.event.entity.EntityDamageByEntityEvent import org.bukkit.inventory.ItemStack import org.bukkit.persistence.PersistentDataType import org.bukkit.scheduler.BukkitTask @@ -29,198 +28,265 @@ import java.util.concurrent.ConcurrentHashMap /** * ## 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". * - 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. * - Nur ein aktiver Anker gleichzeitig; neuer Anker entfernt den alten. * - * | Playstyle | Radius | Bonus-Schaden | - * |-------------|--------|----------------------------| - * | AGGRESSIVE | 5 Blöcke | +1,0 HP (0,5 Herzen) auf jedem Treffer | - * | DEFENSIVE | 8 Blöcke | kein Schaden-Bonus, aber +Resistance I | + * | Playstyle | Radius | Bonus-Schaden | + * |-------------|-------------------|---------------------------------------------------| + * | AGGRESSIVE | [aggressiveRadius] Blöcke | +[aggressiveBonusDmg] HP (0,5 Herzen) auf jedem Treffer | + * | DEFENSIVE | [defensiveRadius] Blöcke | kein Schaden-Bonus, aber +Resistance I | * - * ### Technische Lösung – Golem-Tod-Erkennung ohne eigenen Listener: - * 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. + * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`) * - * ### Rückschlag-Reduktion: - * `onAssign` setzt `GENERIC_KNOCKBACK_RESISTANCE.baseValue = PARTIAL_RESISTANCE`. - * Ein periodischer Task aktualisiert den Wert auf 1.0 (wenn im Radius) oder zurück - * auf PARTIAL_RESISTANCE (wenn außerhalb). - * `onRemove` setzt den Attributwert auf 0,0 zurück. + * | JSON-Schlüssel | Typ | Default | Beschreibung | + * |-----------------------------|--------|---------|---------------------------------------------------| + * | `partial_resistance` | Double | `0.4` | Basis-Rückschlag-Reduktion (40 %) | + * | `golem_hp` | Double | `20.0` | HP des Anker-Golems (10 Herzen) | + * | `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 mm = MiniMessage.miniMessage() override val id = "anchor" override val displayName: Component - get() = plugin.languageManager.getDefaultComponent("kits.anchor.name", mapOf()) + get() = plugin.languageManager.getDefaultComponent( "kits.anchor.name", mapOf() ) override val lore: List - get() = plugin.languageManager.getDefaultRawMessageList("kits.anchor.lore") + get() = plugin.languageManager.getDefaultRawMessageList( "kits.anchor.lore" ) override val icon = Material.ANVIL companion object { - const val PARTIAL_RESISTANCE = 0.4 // 40 % – immer aktiv - const val GOLEM_HP = 20.0 // 10 Herzen - const val AGGRESSIVE_RADIUS = 5.0 - const val DEFENSIVE_RADIUS = 8.0 - const val AGGRESSIVE_BONUS_DMG = 1.0 // +0,5 Herzen - const val MONITOR_INTERVAL_TICKS = 10L // alle 0,5 s prüfen + const val DEFAULT_PARTIAL_RESISTANCE = 0.4 + const val DEFAULT_GOLEM_HP = 20.0 + const val DEFAULT_AGGRESSIVE_RADIUS = 5.0 + const val DEFAULT_DEFENSIVE_RADIUS = 8.0 + const val DEFAULT_AGGRESSIVE_BONUS_DMG = 1.0 + const val DEFAULT_MONITOR_INTERVAL_TICKS = 10L const val PDC_KEY = "anchor_owner_uuid" } - private val anchorGolems : MutableMap = ConcurrentHashMap() - private val monitorTasks : MutableMap = ConcurrentHashMap() + // ── 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 = ConcurrentHashMap() + private val monitorTasks: MutableMap = ConcurrentHashMap() // ── Gecachte Instanzen ──────────────────────────────────────────────────── - private val aggressiveActive = AnchorActive(Playstyle.AGGRESSIVE, AGGRESSIVE_RADIUS) - private val defensiveActive = AnchorActive(Playstyle.DEFENSIVE, DEFENSIVE_RADIUS) - private val aggressivePassive = AnchorPassive(Playstyle.AGGRESSIVE, AGGRESSIVE_RADIUS, bonusDamage = AGGRESSIVE_BONUS_DMG, resistanceBonus = false) - private val defensivePassive = AnchorPassive(Playstyle.DEFENSIVE, DEFENSIVE_RADIUS, bonusDamage = 0.0, resistanceBonus = true) + private val aggressiveActive = AnchorActive( Playstyle.AGGRESSIVE ) + private val defensiveActive = AnchorActive( Playstyle.DEFENSIVE ) + private val aggressivePassive = AnchorPassive( Playstyle.AGGRESSIVE, bonusDamage = true, resistanceBonus = false ) + 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.DEFENSIVE -> defensiveActive } - override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) { + + override fun getPassiveAbility( + playstyle: Playstyle + ) = when( playstyle ) + { Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.DEFENSIVE -> defensivePassive } override val cachedItems = ConcurrentHashMap>() - override fun giveItems(player: Player, playstyle: Playstyle) { - val active = getActiveAbility(playstyle) - val item = ItemBuilder(Material.CHAIN) - .name(active.name) - .lore(listOf(active.description)) + override fun giveItems( + player: Player, + playstyle: Playstyle + ) { + val active = getActiveAbility( playstyle ) + val item = ItemBuilder( Material.CHAIN ) + .name( active.name ) + .lore(listOf( active.description )) .build() - cachedItems[player.uniqueId] = listOf(item) - player.inventory.addItem(item) + cachedItems[ player.uniqueId ] = listOf( item ) + player.inventory.addItem( item ) } - // ── Lifecycle: Rückschlag-Basis-Resistenz setzen/entfernen ─────────────── + // ── Lifecycle ───────────────────────────────────────────────────────────── - override fun onAssign(player: Player, playstyle: Playstyle) { - player.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE) - ?.baseValue = PARTIAL_RESISTANCE + override fun onAssign( + player: Player, + playstyle: Playstyle + ) { + val capturedPartialResistance = partialResistance + player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE ) + ?.baseValue = capturedPartialResistance } - override fun onRemove(player: Player) { - // Golem entfernen - removeAnchor(player, playDeathSound = false) - // Rückschlag-Resistenz zurücksetzen - player.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE) + override fun onRemove( + player: Player + ) { + removeAnchor( player, playDeathSound = false ) + player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE ) ?.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( - playstyle: Playstyle, - private val radius: Double - ) : ActiveAbility(playstyle) { + playstyle: Playstyle + ) : ActiveAbility( playstyle ) { private val plugin get() = SpeedHG.instance override val kitId = "anchor" override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.anchor.items.chain.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.anchor.items.chain.name" ) override val description: String - get() = plugin.languageManager.getDefaultRawMessage("kits.anchor.items.chain.description") + get() = plugin.languageManager.getDefaultRawMessage( "kits.anchor.items.chain.description" ) override val hardcodedHitsRequired = 15 override val triggerMaterial = Material.ANVIL - override fun execute(player: Player): AbilityResult { - // Alten Anker entfernen (kein Todesklang – Spieler beschwört neuen) - removeAnchor(player, playDeathSound = false) + override fun execute( + player: Player + ): AbilityResult + { + removeAnchor( player, playDeathSound = false ) 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 - val golem = world.spawn(spawnLoc, IronGolem::class.java) { g -> - g.setAI(false) // keine Bewegung, kein Angriff - g.isSilent = true // Todesklang manuell kontrollieren - g.isInvulnerable = false // muss zerstörbar sein - g.customName(mm.deserialize("Anker")) + // 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 -> + g.setAI( false ) + g.isSilent = true + g.isInvulnerable = false + g.customName(mm.deserialize( "Anker" )) g.isCustomNameVisible = true - // HP reduzieren (vanilla = 100 HP) - g.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.baseValue = GOLEM_HP - g.health = GOLEM_HP + g.getAttribute( Attribute.GENERIC_MAX_HEALTH )?.baseValue = capturedGolemHp + g.health = capturedGolemHp - // PDC: Besitzer-UUID für spätere Identifikation g.persistentDataContainer.set( - NamespacedKey(plugin, PDC_KEY), + NamespacedKey( plugin, PDC_KEY ), PersistentDataType.STRING, player.uniqueId.toString() ) } - 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 activeGolem = anchorGolems[player.uniqueId] + val task = Bukkit.getScheduler().runTaskTimer( plugin, { -> + val activeGolem = anchorGolems[ player.uniqueId ] - if (activeGolem == null || activeGolem.isDead || !activeGolem.isValid) { - // Golem wurde von Gegnern zerstört - if (activeGolem?.isDead == true) { - onAnchorDestroyed(player, activeGolem.location) + if ( activeGolem == null || activeGolem.isDead || !activeGolem.isValid ) + { + if ( activeGolem?.isDead == true ) + { + onAnchorDestroyed( player, activeGolem.location ) } - monitorTasks.remove(player.uniqueId)?.cancel() - // Resistenz zurück auf Basis-Wert (Golem ist weg) - if (player.isOnline) { - player.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE) - ?.baseValue = PARTIAL_RESISTANCE + monitorTasks.remove( player.uniqueId )?.cancel() + if ( player.isOnline ) + { + player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE ) + ?.baseValue = capturedPartialResistance } return@runTaskTimer } - if (!player.isOnline) { + if ( !player.isOnline ) + { activeGolem.remove() - anchorGolems.remove(player.uniqueId) - monitorTasks.remove(player.uniqueId)?.cancel() + anchorGolems.remove( player.uniqueId ) + monitorTasks.remove( player.uniqueId )?.cancel() return@runTaskTimer } - // Radius-Check: voller NoKnock im Anker-Radius - val inRadius = player.location.distanceSquared(activeGolem.location) <= radius * radius - val targetResistance = if (inRadius) 1.0 else PARTIAL_RESISTANCE - player.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE)?.baseValue = targetResistance + val inRadius = player.location.distanceSquared( activeGolem.location ) <= capturedRadius * capturedRadius + val targetResistance = if ( inRadius ) 1.0 else capturedPartialResistance + player.getAttribute( Attribute.GENERIC_KNOCKBACK_RESISTANCE )?.baseValue = targetResistance - // Visueller Indikator am Golem (Partikelring) - if (inRadius) { + if ( inRadius ) + { world.spawnParticle( Particle.CRIT, - activeGolem.location.clone().add(0.0, 2.5, 0.0), + activeGolem.location.clone().add( 0.0, 2.5, 0.0 ), 2, 0.1, 0.1, 0.1, 0.0 ) } - }, 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.spawnParticle(Particle.CLOUD, spawnLoc.clone().add(0.0, 1.0, 0.0), 20, 0.5, 0.3, 0.5, 0.05) + 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 ) player.sendActionBar( - player.trans("kits.anchor.messages.anchor_placed", - "radius" to radius.toInt().toString()) + player.trans( "kits.anchor.messages.anchor_placed", + "radius" to capturedRadius.toInt().toString() ) ) return AbilityResult.Success } @@ -232,42 +298,56 @@ class AnchorKit : Kit() { inner class AnchorPassive( playstyle: Playstyle, - private val radius: Double, - private val bonusDamage: Double, + private val bonusDamage: Boolean, private val resistanceBonus: Boolean - ) : PassiveAbility(playstyle) { + ) : PassiveAbility( playstyle ) { private val plugin get() = SpeedHG.instance override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.anchor.passive.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.anchor.passive.name" ) 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) { - val golem = anchorGolems[attacker.uniqueId] ?: return + override fun onHitEnemy( + attacker: Player, + victim: Player, + event: org.bukkit.event.entity.EntityDamageByEntityEvent + ) { + val golem = anchorGolems[ attacker.uniqueId ] ?: return - // Nur wirksam wenn Angreifer im Radius - if (attacker.location.distanceSquared(golem.location) > radius * radius) return + val capturedRadius = when( playstyle ) + { + Playstyle.AGGRESSIVE -> aggressiveRadius + Playstyle.DEFENSIVE -> defensiveRadius + } - // Bonus-Schaden (Aggressive playstyle) - if (bonusDamage > 0.0) { - event.damage += bonusDamage + if ( attacker.location.distanceSquared( golem.location ) > capturedRadius * capturedRadius ) return + + if ( bonusDamage ) + { + val capturedBonusDmg = aggressiveBonusDmg + event.damage += capturedBonusDmg attacker.world.spawnParticle( Particle.CRIT, - victim.location.clone().add(0.0, 1.2, 0.0), + victim.location.clone().add( 0.0, 1.2, 0.0 ), 5, 0.2, 0.2, 0.2, 0.0 ) } } - override fun onHitByEnemy(victim: Player, attacker: Player, event: EntityDamageByEntityEvent) { - if (!resistanceBonus) return - val golem = anchorGolems[victim.uniqueId] ?: return + override fun onHitByEnemy( + victim: Player, + attacker: Player, + event: org.bukkit.event.entity.EntityDamageByEntityEvent + ) { + if ( !resistanceBonus ) return + val golem = anchorGolems[ victim.uniqueId ] ?: return - // Resistance I während im Radius (Defensive playstyle) - if (victim.location.distanceSquared(golem.location) <= radius * radius) { - // Schaden um ~20 % reduzieren (Resistance I Äquivalent) + val capturedRadius = defensiveRadius + + if ( victim.location.distanceSquared( golem.location ) <= capturedRadius * capturedRadius ) + { event.damage *= 0.80 } } @@ -277,36 +357,36 @@ class AnchorKit : Kit() { // Hilfsmethoden // ========================================================================= - /** - * Entfernt den aktiven Anker eines Spielers sauber. - * @param playDeathSound Falls `true`, wird der Eisengolem-Todesklang abgespielt. - */ - private fun removeAnchor(player: Player, playDeathSound: Boolean) { - monitorTasks.remove(player.uniqueId)?.cancel() + private fun removeAnchor( + player: Player, + playDeathSound: Boolean + ) { + monitorTasks.remove( player.uniqueId )?.cancel() - val golem = anchorGolems.remove(player.uniqueId) ?: return - if (playDeathSound && golem.isValid) { - golem.world.playSound(golem.location, Sound.ENTITY_IRON_GOLEM_DEATH, 1f, 1f) + val golem = anchorGolems.remove( player.uniqueId ) ?: return + if ( playDeathSound && golem.isValid ) + { + golem.world.playSound( golem.location, Sound.ENTITY_IRON_GOLEM_DEATH, 1f, 1f ) } - if (golem.isValid) golem.remove() + if ( golem.isValid ) golem.remove() } - /** - * Wird aufgerufen, wenn der Golem von Gegnern zerstört wurde (HP == 0). - * Der Golem ist zu diesem Zeitpunkt bereits `isDead`, wir spielen den Sound manuell - * (weil der Golem mit `isSilent = true` gespawnt wurde). - */ - private fun onAnchorDestroyed(player: Player, deathLocation: Location) { - anchorGolems.remove(player.uniqueId) + private fun onAnchorDestroyed( + player: Player, + deathLocation: Location + ) { + 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 ) deathLocation.world?.spawnParticle( Particle.EXPLOSION, deathLocation, 3, 0.3, 0.3, 0.3, 0.0 ) - if (player.isOnline) { - player.sendActionBar(player.trans("kits.anchor.messages.anchor_destroyed")) - player.playSound(player.location, Sound.ENTITY_IRON_GOLEM_DEATH, 0.8f, 1.3f) + if ( player.isOnline ) + { + player.sendActionBar( player.trans( "kits.anchor.messages.anchor_destroyed" ) ) + player.playSound( player.location, Sound.ENTITY_IRON_GOLEM_DEATH, 0.8f, 1.3f ) } } + } \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt index ecff76d..b99cf40 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt @@ -1,7 +1,6 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG -import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -27,25 +26,27 @@ import java.util.concurrent.ConcurrentHashMap * * | 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 | | * | DEFENSIVE | – (no active item) | **Wakanda Forever!** – fall-pounce on enemies | * - * ### Push (AGGRESSIVE active) - * 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. + * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`) * - * ### Vibranium Fists (AGGRESSIVE passive) - * During Fist Mode, bare-hand attacks (`itemInMainHand == AIR`) override the - * normal damage to **6.5 HP (3.25 hearts)**. + * Typisierte Felder in [CustomGameSettings.KitOverride] werden direkt gelesen. + * Zusätzliche Einstellungen über die `extras`-Map. * - * ### Wakanda Forever! (DEFENSIVE passive) - * Triggers in [onHitEnemy] when `attacker.fallDistance ≥ 3`. Deals **6 HP** - * to all enemies within 3 blocks of the victim, then creates an explosion - * visual and a small WorldEdit crater at the landing site. + * | Quelle | JSON-Schlüssel | Typ | Default | Beschreibung | + * |------------------|-----------------------------|--------|----------|-------------------------------------------| + * | Typisiertes Feld | `fist_mode_ms` | Long | `12000` | Dauer des Fist-Modus (ms) | + * | 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() { @@ -54,9 +55,9 @@ class BlackPantherKit : Kit() override val id = "blackpanther" override val displayName: Component - get() = plugin.languageManager.getDefaultComponent("kits.blackpanther.name", mapOf()) + get() = plugin.languageManager.getDefaultComponent( "kits.blackpanther.name", mapOf() ) override val lore: List - get() = plugin.languageManager.getDefaultRawMessageList("kits.blackpanther.lore") + get() = plugin.languageManager.getDefaultRawMessageList( "kits.blackpanther.lore" ) override val icon = Material.BLACK_DYE /** Players currently in Fist Mode: UUID → expiry timestamp (ms). */ @@ -67,47 +68,129 @@ class BlackPantherKit : Kit() companion object { - private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"] - ?: CustomGameSettings.KitOverride() - /** PDC key string shared with [KitEventDispatcher] for push-projectiles. */ const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile" - private val FIST_MODE_MS = override().fistModeDurationMs // 12 seconds - private val PUSH_RADIUS = override().pushRadius - private val POUNCE_MIN_FALL = override().pounceMinFall - private val POUNCE_RADIUS = override().pounceRadius - private val POUNCE_DAMAGE = override().pounceDamage // 3 hearts = 6 HP + const val DEFAULT_FIST_MODE_DURATION_MS = 12_000L + const val DEFAULT_PUSH_RADIUS = 5.0 + const val DEFAULT_PUSH_BONUS_DAMAGE = 4.0 + const val DEFAULT_POUNCE_MIN_FALL = 3.0f + 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 ────────────────────────────────────────────── - private val aggressiveActive = AggressiveActive() - private val defensiveActive = DefensiveActive() - private val aggressivePassive = AggressivePassive() - private val defensivePassive = DefensivePassive() + // ── Live config accessors ───────────────────────────────────────────────── - override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) + /** + * 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 defensiveActive = DefensiveActive() + private val aggressivePassive = AggressivePassive() + private val defensivePassive = DefensivePassive() + + override fun getActiveAbility( + playstyle: Playstyle + ) = when( playstyle ) { 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.DEFENSIVE -> defensivePassive + Playstyle.DEFENSIVE -> defensivePassive } override val cachedItems = ConcurrentHashMap>() - override fun giveItems(player: Player, playstyle: Playstyle) { - if (playstyle != Playstyle.AGGRESSIVE) return - val item = ItemBuilder(Material.BLACK_DYE) - .name(aggressiveActive.name) - .lore(listOf(aggressiveActive.description)) + override fun giveItems( + player: Player, + playstyle: Playstyle + ) { + if ( playstyle != Playstyle.AGGRESSIVE ) return + val item = ItemBuilder( Material.BLACK_DYE ) + .name( aggressiveActive.name ) + .lore(listOf( aggressiveActive.description )) .build() - cachedItems[player.uniqueId] = listOf(item) - player.inventory.addItem(item) + cachedItems[ player.uniqueId ] = listOf( item ) + player.inventory.addItem( item ) } override fun onRemove( @@ -118,21 +201,24 @@ class BlackPantherKit : Kit() cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) } } -// ========================================================================= -// 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 - override val kitId: String get() = "blackpanther" - override val hardcodedHitsRequired: Int get() = 15 + override val kitId: String + get() = "blackpanther" + override val hardcodedHitsRequired: Int + get() = 15 override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.items.push.name" ) override val description: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.description") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.items.push.description" ) override val triggerMaterial = Material.BLACK_DYE override fun execute( @@ -144,148 +230,171 @@ class BlackPantherKit : Kit() 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 - .getNearbyEntities(player.location, PUSH_RADIUS, PUSH_RADIUS, PUSH_RADIUS) + .getNearbyEntities( player.location, capturedPushRadius, capturedPushRadius, capturedPushRadius ) .filterIsInstance() - .filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } - if (enemies.isEmpty()) - return AbilityResult.ConditionNotMet("No enemies within ${PUSH_RADIUS.toInt()} blocks!") + if ( enemies.isEmpty() ) + return AbilityResult.ConditionNotMet( "No enemies within ${capturedPushRadius.toInt()} blocks!" ) - val pushKey = (plugin.kitManager.getSelectedKit(player) as? BlackPantherKit) - ?.let { NamespacedKey(plugin, PUSH_PROJECTILE_KEY) } + val pushKey = NamespacedKey( plugin, PUSH_PROJECTILE_KEY ) enemies.forEach { enemy -> - // ── Knockback ────────────────────────────────────────────────── val knockDir = enemy.location.toVector() - .subtract(player.location.toVector()) + .subtract( player.location.toVector() ) .normalize() - .multiply(2.0) - .setY(0.45) + .multiply( capturedKnockbackSpeed ) + .setY( capturedKnockbackY ) enemy.velocity = knockDir - enemy.world.spawnParticle(Particle.CRIT, - enemy.location.clone().add(0.0, 1.0, 0.0), 10, 0.3, 0.3, 0.3, 0.0) + enemy.world.spawnParticle( + 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, { -> - if (!player.isOnline) return@runTaskLater - val snowball = player.world.spawn( - player.eyeLocation, Snowball::class.java - ) - snowball.shooter = player - val travelDir = enemy.location.toVector() - .subtract(player.eyeLocation.toVector()) - .normalize() - .multiply(1.8) - snowball.velocity = travelDir - snowball.persistentDataContainer.set(pushKey, PersistentDataType.BYTE, 1) - }, 5L) - } + Bukkit.getScheduler().runTaskLater( plugin, { -> + if ( !player.isOnline ) return@runTaskLater + val snowball = player.world.spawn( player.eyeLocation, Snowball::class.java ) + snowball.shooter = player + val travelDir = enemy.location.toVector() + .subtract( player.eyeLocation.toVector() ) + .normalize() + .multiply( 1.8 ) + snowball.velocity = travelDir + snowball.persistentDataContainer.set( pushKey, PersistentDataType.BYTE, 1 ) + }, capturedProjectileDelay ) } - // ── Activate Fist Mode ───────────────────────────────────────────── - fistModeExpiry[player.uniqueId] = System.currentTimeMillis() + FIST_MODE_MS - player.sendActionBar(player.trans("kits.blackpanther.messages.fist_mode_active")) + fistModeExpiry[ player.uniqueId ] = System.currentTimeMillis() + capturedFistModeDurationMs + 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_PLAYER_ATTACK_SWEEP, 0.8f, 0.7f) + player.world.playSound( player.location, Sound.ENTITY_RAVAGER_ROAR, 1f, 1.1f ) + player.world.playSound( player.location, Sound.ENTITY_PLAYER_ATTACK_SWEEP, 0.8f, 0.7f ) return AbilityResult.Success } } -// ========================================================================= -// 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 name = "None" override val description = "None" override val hardcodedHitsRequired: Int = 0 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 override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.aggressive.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.passive.aggressive.name" ) 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) { - val expiry = fistModeExpiry[attacker.uniqueId] ?: return - if (System.currentTimeMillis() > expiry) { - fistModeExpiry.remove(attacker.uniqueId) + override fun onHitEnemy( + attacker: Player, + victim: Player, + event: EntityDamageByEntityEvent + ) { + val expiry = fistModeExpiry[ attacker.uniqueId ] ?: return + if ( System.currentTimeMillis() > expiry ) + { + fistModeExpiry.remove( attacker.uniqueId ) return } - if (attacker.inventory.itemInMainHand.type != Material.AIR) return + if ( attacker.inventory.itemInMainHand.type != Material.AIR ) return - event.damage = 6.5 // 3.25 hearts - 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) + val capturedFistDamage = fistModeDamage + event.damage = capturedFistDamage + 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 ) } } -// ========================================================================= -// 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 override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.defensive.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.passive.defensive.name" ) override val description: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.defensive.description") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blackpanther.passive.defensive.description" ) override fun onMove( player: Player, event: PlayerMoveEvent ) { 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 if ( !blockBelow.type.isSolid ) return val impactLoc = event.to.clone() + // Werte zum Aktivierungszeitpunkt snapshotten + val capturedPounceRadius = pounceRadius + val capturedPounceDamage = pounceDamage + val splashTargets = impactLoc.world - .getNearbyEntities( impactLoc, POUNCE_RADIUS, POUNCE_RADIUS, POUNCE_RADIUS ) + .getNearbyEntities( impactLoc, capturedPounceRadius, capturedPounceRadius, capturedPounceRadius ) .filterIsInstance() .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.LARGE_SMOKE, impactLoc, 20, 1.0, 0.5, 1.0, 0.05) - impactLoc.world.playSound(impactLoc, Sound.ENTITY_GENERIC_EXPLODE, 1f, 0.7f) - impactLoc.world.playSound(impactLoc, Sound.ENTITY_IRON_GOLEM_HURT, 1f, 0.5f) + 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.playSound( impactLoc, Sound.ENTITY_GENERIC_EXPLODE, 1f, 0.7f ) + impactLoc.world.playSound( impactLoc, Sound.ENTITY_IRON_GOLEM_HURT, 1f, 0.5f ) - Bukkit.getScheduler().runTaskLater(plugin, Runnable { + Bukkit.getScheduler().runTaskLater( plugin, Runnable { WorldEditUtils.createCylinder( - impactLoc.world, impactLoc.clone().subtract(0.0, 1.0, 0.0), + impactLoc.world, impactLoc.clone().subtract( 0.0, 1.0, 0.0 ), 3, true, 2, Material.AIR ) - }, 2L) + }, 2L ) - player.sendActionBar(player.trans("kits.blackpanther.messages.wakanda_impact", - mapOf("count" to splashTargets.size.toString()))) + player.sendActionBar( + player.trans( "kits.blackpanther.messages.wakanda_impact", + mapOf( "count" to splashTargets.size.toString() ) ) + ) - // Suppress fall damage for this landing noFallDamagePlayers.add( player.uniqueId ) player.fallDistance = 0f } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt index 3f0e8b7..d540de9 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt @@ -6,7 +6,6 @@ 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.kit.listener.KitEventDispatcher import club.mcscrims.speedhg.kit.listener.KitEventDispatcher.Companion.MAX_KNOCKBACK_HEIGHT_Y import club.mcscrims.speedhg.util.ItemBuilder import club.mcscrims.speedhg.util.trans @@ -35,26 +34,28 @@ import kotlin.math.sin * | Playstyle | Aktive Fähigkeit | * |-------------|---------------------------------------------------------------| * | AGGRESSIVE | **Hook** – zieht ersten Feind in der Schusslinie heran | - * | DEFENSIVE | **Stun** – friert alle nahen Feinde für 3 s ein | - * | Beide | **Ult** – expandierende Schockwelle + AoE-Schaden | + * | DEFENSIVE | **Stun** – friert alle nahen Feinde für [stunDurationTicks] Ticks ein | + * | Beide | **Ult** – expandierende Schockwelle + AoE-Schaden | * - * ### Hook – synchroner Raycast - * 0,4-Block-Schritte von `eyeLocation` entlang `eyeLocation.direction`. - * Erster Feind getroffen → Velocity-Pull Richtung Caster. Alle Partikel werden - * synchron im selben Tick gezeichnet. + * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`) * - * ### Stun – Freeze-Mechanismus - * Slowness 127 + Mining Fatigue 127 für [STUN_DURATION_TICKS] Ticks. - * Zusätzlich setzt ein BukkitTask die Velocity aller gestunnten Spieler auf 0. + * Alle Werte werden über die `extras`-Map konfiguriert und fallen auf die + * Defaults im [companion object] zurück, wenn kein Wert gesetzt ist. * - * ### Ult – passive onInteract als Auslöser - * Das Ult-Item (BLAZE_POWDER) besitzt einen PDC-Tag ([ultItemKey]). - * `KitEventDispatcher.onInteract` ruft **zuerst** `passive.onInteract` auf, - * dann erst den triggerMaterial-Check. [UltPassive.onInteract] fängt das - * BLAZE_POWDER-Rechtsklick-Event ab und cancelt es, bevor der Dispatcher - * etwas unternimmt → kein Dispatcher-Umbau notwendig. + * | JSON-Schlüssel | Typ | Default | Beschreibung | + * |-------------------------|--------|-------------|----------------------------------------------| + * | `hook_range` | Double | `10.0` | Maximale Reichweite des Hooks (Blöcke) | + * | `hook_pull_strength` | Double | `2.7` | Velocity-Multiplikator beim Pull | + * | `stun_radius` | Double | `5.0` | AoE-Radius des Stuns (Blöcke) | + * | `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 @@ -76,15 +77,84 @@ class BlitzcrankKit : Kit() { private val ultCooldowns: MutableMap = ConcurrentHashMap() companion object { - const val HOOK_RANGE = 10.0 // Blöcke - const val HOOK_PULL_STRENGTH = 2.7 // Velocity-Multiplikator - const val STUN_RADIUS = 5.0 // Blöcke - const val STUN_DURATION_TICKS = 60 // 3 Sekunden - const val ULT_RADIUS = 6.0 // Blöcke - const val ULT_DAMAGE = 5.0 // 2,5 Herzen - const val ULT_COOLDOWN_MS = 30_000L + const val DEFAULT_HOOK_RANGE = 10.0 + const val DEFAULT_HOOK_PULL_STRENGTH = 2.7 + const val DEFAULT_STUN_RADIUS = 5.0 + const val DEFAULT_STUN_DURATION_TICKS = 60L + const val DEFAULT_ULT_RADIUS = 6.0 + const val DEFAULT_ULT_DAMAGE = 5.0 + 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 defensiveActive = StunActive() private val aggressivePassive = UltPassive( Playstyle.AGGRESSIVE ) @@ -112,7 +182,8 @@ class BlitzcrankKit : Kit() { player: Player, playstyle: Playstyle ) { - val mainItem = when (playstyle) { + val mainItem = when( playstyle ) + { Playstyle.AGGRESSIVE -> ItemBuilder( Material.FISHING_ROD ) .name( aggressiveActive.name ) .lore(listOf( aggressiveActive.description )) @@ -150,28 +221,33 @@ class BlitzcrankKit : Kit() { ) { if ( caster.location.y > MAX_KNOCKBACK_HEIGHT_Y ) { - caster.sendActionBar(caster.trans( "kits.height_restriction" )) + caster.sendActionBar( caster.trans( "kits.height_restriction" ) ) return } - val now = System.currentTimeMillis() + val now = System.currentTimeMillis() 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 - caster.sendActionBar(caster.trans( "kits.blitzcrank.messages.ult_cooldown", "time" to secLeft.toString() )) + val secLeft = ( capturedUltCooldownMs - ( now - lastUlt ) ) / 1000 + caster.sendActionBar( caster.trans( "kits.blitzcrank.messages.ult_cooldown", "time" to secLeft.toString() ) ) return } val targets = caster.world - .getNearbyEntities( caster.location, ULT_RADIUS, ULT_RADIUS, ULT_RADIUS ) + .getNearbyEntities( caster.location, capturedUltRadius, capturedUltRadius, capturedUltRadius ) .filterIsInstance() .filter { it != caster && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } if ( targets.isEmpty() ) { - caster.sendActionBar(caster.trans( "kits.blitzcrank.messages.ult_no_targets" )) + caster.sendActionBar( caster.trans( "kits.blitzcrank.messages.ult_no_targets" ) ) return } @@ -180,13 +256,13 @@ class BlitzcrankKit : Kit() { 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 ) repeat( steps ) { i -> val angle = 2.0 * Math.PI * i / steps caster.world.spawnParticle( Particle.ELECTRIC_SPARK, - caster.location.clone().add(cos( angle ) * r, 1.0, sin( angle ) * r ), + caster.location.clone().add( cos( angle ) * r, 1.0, sin( angle ) * r ), 1, 0.0, 0.0, 0.0, 0.0 ) } @@ -195,7 +271,7 @@ class BlitzcrankKit : Kit() { }.runTaskTimer( plugin, 0L, 1L ) targets.forEach { target -> - target.damage( ULT_DAMAGE, caster ) + target.damage( capturedUltDamage, caster ) target.velocity = target.location.toVector() .subtract( caster.location.toVector() ) .normalize() @@ -203,9 +279,9 @@ class BlitzcrankKit : Kit() { .setY( 0.5 ) } - caster.world.playSound( caster.location, Sound.ENTITY_GENERIC_EXPLODE, 1f, 1.5f ) + caster.world.playSound( caster.location, Sound.ENTITY_GENERIC_EXPLODE, 1f, 1.5f ) caster.world.playSound( caster.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 1.8f ) - caster.sendActionBar(caster.trans( "kits.blitzcrank.messages.ult_fired", "count" to targets.size.toString() )) + caster.sendActionBar( caster.trans( "kits.blitzcrank.messages.ult_fired", "count" to targets.size.toString() ) ) ultCooldowns[ caster.uniqueId ] = now } @@ -214,19 +290,22 @@ class BlitzcrankKit : Kit() { // 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 override val kitId = "blitzcrank" override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.hook.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.items.hook.name" ) override val description: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.hook.description") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.items.hook.description" ) override val hardcodedHitsRequired = 15 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 ) return AbilityResult.ConditionNotMet( @@ -236,51 +315,57 @@ class BlitzcrankKit : Kit() { val eyeLoc = player.eyeLocation val dir = eyeLoc.direction.normalize() + // Werte zum Aktivierungszeitpunkt snapshotten + val capturedHookRange = hookRange + val capturedHookStepSize = hookStepSize + val capturedHookHitRadius = hookHitRadius + val capturedPullStrength = hookPullStrength + var hookTarget: Player? = null - var dist = 0.4 + var dist = capturedHookStepSize - // Synchroner Scan: trivial schnell (max ~25 Iterationen) - while (dist <= HOOK_RANGE && hookTarget == null) { - val point = eyeLoc.clone().add(dir.clone().multiply(dist)) + while ( dist <= capturedHookRange && hookTarget == null ) + { + 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 - ?.getNearbyEntities(point, 0.6, 0.6, 0.6) + ?.getNearbyEntities( point, capturedHookHitRadius, capturedHookHitRadius, capturedHookHitRadius ) ?.filterIsInstance() - ?.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } - ?.minByOrNull { it.location.distanceSquared(point) } + ?.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } + ?.minByOrNull { it.location.distanceSquared( point ) } - dist += 0.4 + dist += capturedHookStepSize } - if (hookTarget == null) { - // Kein Treffer – Funken am Strahlende - val endPt = eyeLoc.clone().add(dir.multiply(dist.coerceAtMost(HOOK_RANGE))) - player.world.spawnParticle(Particle.ELECTRIC_SPARK, endPt, 10, 0.3, 0.3, 0.3, 0.06) - return AbilityResult.ConditionNotMet("Kein Ziel in Reichweite!") + if ( hookTarget == null ) + { + 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 ) + return AbilityResult.ConditionNotMet( "Kein Ziel in Reichweite!" ) } val target = hookTarget - // Pull: Velocity in Richtung Caster target.velocity = player.location.toVector() - .subtract(target.location.toVector()) + .subtract( target.location.toVector() ) .normalize() - .multiply(HOOK_PULL_STRENGTH) - .setY(0.65) + .multiply( capturedPullStrength ) + .setY( 0.65 ) - target.world.spawnParticle(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.sendActionBar(target.trans("kits.blitzcrank.messages.hooked")) + target.world.spawnParticle( + 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.sendActionBar( target.trans( "kits.blitzcrank.messages.hooked" ) ) - player.playSound(player.location, Sound.ENTITY_FISHING_BOBBER_RETRIEVE, 1f, 0.4f) - player.sendActionBar(player.trans("kits.blitzcrank.messages.hook_hit")) + player.playSound( player.location, Sound.ENTITY_FISHING_BOBBER_RETRIEVE, 1f, 0.4f ) + player.sendActionBar( player.trans( "kits.blitzcrank.messages.hook_hit" ) ) return AbilityResult.Success } @@ -290,15 +375,16 @@ class BlitzcrankKit : Kit() { // 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 override val kitId = "blitzcrank" override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.stun.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.items.stun.name" ) override val description: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.stun.description") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.items.stun.description" ) override val hardcodedHitsRequired = 15 override val triggerMaterial = Material.PISTON @@ -311,48 +397,60 @@ class BlitzcrankKit : Kit() { plugin.languageManager.getRawMessage( player, "kits.height_restriction" ) ) - val targets = player.world - .getNearbyEntities(player.location, STUN_RADIUS, STUN_RADIUS, STUN_RADIUS) - .filterIsInstance() - .filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + // Werte zum Aktivierungszeitpunkt snapshotten + val capturedStunRadius = stunRadius + val capturedStunDurationTicks = stunDurationTicks - if (targets.isEmpty()) - return AbilityResult.ConditionNotMet("Keine Feinde in ${STUN_RADIUS.toInt()} Blöcken!") + val targets = player.world + .getNearbyEntities( player.location, capturedStunRadius, capturedStunRadius, capturedStunRadius ) + .filterIsInstance() + .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } + + if ( targets.isEmpty() ) + return AbilityResult.ConditionNotMet( "Keine Feinde in ${capturedStunRadius.toInt()} Blöcken!" ) targets.forEach { target -> - // Potion-Effekte für maximales Einfrieren (Amplifier 127 = sofortiger Stopp) target.addPotionEffect( - PotionEffect(PotionEffectType.SLOWNESS, STUN_DURATION_TICKS, 127, false, false, true) + PotionEffect( PotionEffectType.SLOWNESS, capturedStunDurationTicks.toInt(), 127, false, false, true ) ) 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 - val task = Bukkit.getScheduler().runTaskTimer(plugin, { -> + val task = Bukkit.getScheduler().runTaskTimer( plugin, { -> stunTick++ - if (stunTick >= STUN_DURATION_TICKS || !target.isOnline || - !plugin.gameManager.alivePlayers.contains(target.uniqueId)) { - stunTasks.remove(target.uniqueId)?.cancel() + if ( stunTick >= capturedStunDurationTicks || + !target.isOnline || + !plugin.gameManager.alivePlayers.contains( target.uniqueId ) ) + { + stunTasks.remove( target.uniqueId )?.cancel() return@runTaskTimer } val v = target.velocity - target.velocity = v.setX(0.0).setZ(0.0).let { if (it.y > 0.0) it.setY(0.0) else it } - }, 0L, 1L) + target.velocity = v.setX( 0.0 ).setZ( 0.0 ).let { if ( it.y > 0.0 ) it.setY( 0.0 ) else it } + }, 0L, 1L ) - stunTasks[target.uniqueId] = task + stunTasks[ target.uniqueId ] = task - target.world.spawnParticle(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.world.spawnParticle( + 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" ) ) } - player.world.playSound(player.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 1f, 0.7f) - player.world.spawnParticle(Particle.ELECTRIC_SPARK, - player.location.clone().add(0.0, 1.0, 0.0), 35, 2.0, 0.5, 2.0, 0.14) - player.sendActionBar(player.trans("kits.blitzcrank.messages.stun_cast", - "count" to targets.size.toString())) + player.world.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 1f, 0.7f ) + player.world.spawnParticle( + Particle.ELECTRIC_SPARK, + player.location.clone().add( 0.0, 1.0, 0.0 ), + 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 } @@ -362,28 +460,31 @@ class BlitzcrankKit : Kit() { // 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 override val name: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.passive.name") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.passive.name" ) override val description: String - get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.passive.description") + get() = plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.passive.description" ) - /** - * Wird vom KitEventDispatcher **vor** dem triggerMaterial-Check aufgerufen. - * Prüft PDC-Tag → falls Ult-Item: Event canceln + Ult feuern. - */ - override fun onInteract(player: Player, event: PlayerInteractEvent) { - if (!event.action.isRightClick) return + override fun onInteract( + player: Player, + event: PlayerInteractEvent + ) { + if ( !event.action.isRightClick ) return val pdc = player.inventory.itemInMainHand.itemMeta ?.persistentDataContainer ?: return - if (!pdc.has(ultItemKey, PersistentDataType.BYTE)) return + if ( !pdc.has( ultItemKey, PersistentDataType.BYTE ) ) return - event.isCancelled = true // Vanilla-Interaktion (Feuer-Charge) unterbinden - fireUlt(player) + event.isCancelled = true + fireUlt( player ) } } + } \ No newline at end of file