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:
@@ -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<String>
|
||||
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<UUID, IronGolem> = ConcurrentHashMap()
|
||||
private val monitorTasks : MutableMap<UUID, BukkitTask> = 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<UUID, IronGolem> = ConcurrentHashMap()
|
||||
private val monitorTasks: MutableMap<UUID, BukkitTask> = 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<UUID, List<ItemStack>>()
|
||||
|
||||
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("<gray>⚓ <white>Anker</white>"))
|
||||
// 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( "<gray>⚓ <white>Anker</white>" ))
|
||||
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 )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String>
|
||||
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<UUID, List<ItemStack>>()
|
||||
|
||||
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<Player>()
|
||||
.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<Player>()
|
||||
.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
|
||||
}
|
||||
|
||||
@@ -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<UUID, Long> = 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<Player>()
|
||||
.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<Player>()
|
||||
?.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<Player>()
|
||||
.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<Player>()
|
||||
.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 )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user