Add extensible kit config and use in kits
Introduce a generic `extras` map and typed accessors to CustomGameSettings.KitOverride (getLong/getInt/getDouble/getFloat/getBoolean) and add documentation/examples for runtime JSON overrides. Expose Kit.override() to return the live KitOverride from SpeedHG and document how kits should read config values. Refactor NinjaKit to read all runtime parameters from overrides (teleport cooldown, hit window, smoke radius/duration/refresh/effect), snapshot config at activation, simplify smoke rendering/logic, and clean up cooldown/teleport code. Refactor TheWorldKit to use typed override fields and extras (shockwave, blink and freeze settings), snapshot values for abilities, tidy particle/physics code and improve lifecycle handling. Miscellaneous formatting, comments and small API cleanups across files.
This commit is contained in:
@@ -2,6 +2,10 @@ package club.mcscrims.speedhg.config
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
|
||||
@Serializable
|
||||
data class CustomGameSettings(
|
||||
@@ -29,7 +33,6 @@ data class CustomGameSettings(
|
||||
/**
|
||||
* Kit-spezifische Overrides.
|
||||
* Key = Kit.id (z. B. "gladiator", "venom").
|
||||
* Unbekannte Keys werden von kotlinx.serialization ignoriert.
|
||||
*/
|
||||
val kits: Map<String, KitOverride> = emptyMap()
|
||||
|
||||
@@ -46,7 +49,6 @@ data class CustomGameSettings(
|
||||
hardcodedDefault: Int
|
||||
): Int
|
||||
{
|
||||
// A hardcoded 0 means the kit is explicitly cooldown-based — never override it.
|
||||
if ( hardcodedDefault == 0 ) return 0
|
||||
|
||||
return kits[ kitId ]?.hitsRequired?.takeIf { it >= 0 }
|
||||
@@ -55,37 +57,64 @@ data class CustomGameSettings(
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
// Kit-spezifische Override-Klassen
|
||||
// -----------------------------------------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gemeinsamer Wrapper für alle Kit-Overrides.
|
||||
* `hitsRequired = -1` bedeutet "nicht gesetzt, nutze Global/Default".
|
||||
* `extra` nimmt beliebige zusätzliche Kit-Felder auf, ohne dass
|
||||
* für jedes Kit eine eigene Klasse notwendig ist.
|
||||
*
|
||||
* ## Bekannte Felder
|
||||
* Häufig verwendete Felder sind als typisierte Properties vorhanden, damit
|
||||
* der JSON direkt deserialisiert werden kann.
|
||||
*
|
||||
* ## Generische Felder (`extras`)
|
||||
* Kits können beliebige zusätzliche Einstellungen über den `extras`-Map
|
||||
* hinterlegen. Der Schlüssel ist ein freier String (z. B. `"cooldown_ms"`),
|
||||
* der Wert ist ein [JsonElement].
|
||||
*
|
||||
* Zugriff über [getLong], [getInt], [getDouble] — diese geben `null` zurück
|
||||
* wenn der Schlüssel nicht vorhanden ist, sodass der Aufrufer auf seinen
|
||||
* eigenen Default zurückfallen kann.
|
||||
*
|
||||
* ### Beispiel JSON
|
||||
* ```json
|
||||
* {
|
||||
* "kits": {
|
||||
* "ninja": {
|
||||
* "hits_required": 12,
|
||||
* "extras": {
|
||||
* "teleport_cooldown_ms": 8000,
|
||||
* "smoke_radius": 4.0,
|
||||
* "smoke_max_duration_ms": 12000,
|
||||
* "hit_window_ms": 15000
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Serializable
|
||||
data class KitOverride(
|
||||
@SerialName("hits_required") val hitsRequired: Int = -1,
|
||||
|
||||
// Goblin
|
||||
// ── Goblin ────────────────────────────────────────────────────────────
|
||||
@SerialName("steal_duration_seconds") val stealDuration: Int = 60,
|
||||
@SerialName("bunker_radius") val bunkerRadius: Double = 10.0,
|
||||
|
||||
// Gladiator
|
||||
// ── Gladiator ─────────────────────────────────────────────────────────
|
||||
@SerialName("arena_radius") val arenaRadius: Int = 11,
|
||||
@SerialName("arena_height") val arenaHeight: Int = 7,
|
||||
@SerialName("wither_after_seconds") val witherAfterSeconds: Int = 180,
|
||||
|
||||
// Venom
|
||||
// ── Venom ─────────────────────────────────────────────────────────────
|
||||
@SerialName("shield_duration_ticks") val shieldDurationTicks: Long = 160L,
|
||||
@SerialName("shield_capacity") val shieldCapacity: Double = 15.0,
|
||||
|
||||
// Voodoo
|
||||
// ── Voodoo ────────────────────────────────────────────────────────────
|
||||
@SerialName("curse_duration_ms") val curseDurationMs: Long = 15_000L,
|
||||
|
||||
// BlackPanther
|
||||
// ── BlackPanther ──────────────────────────────────────────────────────
|
||||
@SerialName("fist_mode_ms") val fistModeDurationMs: Long = 12_000L,
|
||||
@SerialName("push_bonus_damage") val pushBonusDamage: Double = 4.0,
|
||||
@SerialName("push_radius") val pushRadius: Double = 5.0,
|
||||
@@ -93,19 +122,72 @@ data class CustomGameSettings(
|
||||
@SerialName("pounce_radius") val pounceRadius: Double = 3.0,
|
||||
@SerialName("pounce_damage") val pounceDamage: Double = 6.0,
|
||||
|
||||
// Rattlesnake
|
||||
// ── Rattlesnake ───────────────────────────────────────────────────────
|
||||
@SerialName("pounce_cooldown_ms") val pounceCooldownMs: Long = 20_000L,
|
||||
@SerialName("pounce_max_sneak_ms") val pounceMaxSneakMs: Long = 3_000L,
|
||||
@SerialName("pounce_min_range") val pounceMinRange: Double = 3.0,
|
||||
@SerialName("pounce_max_range") val pounceMaxRange: Double = 10.0,
|
||||
@SerialName("pounce_timeout_ticks") val pounceTimeoutTicks: Long = 30L,
|
||||
|
||||
// TheWorld
|
||||
@SerialName("tw_ability_cooldown_ms") val abilityCooldownMs: Long = 25_000L,
|
||||
@SerialName("tw_shockwave_radius") val shockwaveRadius: Double = 6.0,
|
||||
@SerialName("tw_teleport_range") val teleportRange: Double = 10.0,
|
||||
@SerialName("tw_max_teleport_charges") val maxTeleportCharges: Int = 3,
|
||||
@SerialName("tw_freeze_duration_ticks") val freezeDurationTicks: Int = 200,
|
||||
@SerialName("tw_max_hits_on_frozen") val maxHitsOnFrozen: Int = 5
|
||||
)
|
||||
// ── TheWorld ──────────────────────────────────────────────────────────
|
||||
@SerialName("ability_cooldown_ms") val abilityCooldownMs: Long = 20_000L,
|
||||
@SerialName("shockwave_radius") val shockwaveRadius: Double = 6.0,
|
||||
@SerialName("teleport_range") val teleportRange: Double = 10.0,
|
||||
@SerialName("max_teleport_charges") val maxTeleportCharges: Int = 3,
|
||||
@SerialName("freeze_duration_ticks") val freezeDurationTicks: Int = 200,
|
||||
@SerialName("max_hits_on_frozen") val maxHitsOnFrozen: Int = 5,
|
||||
|
||||
/**
|
||||
* Generische Erweiterungs-Map für kit-spezifische Einstellungen, die
|
||||
* kein eigenes typisiertes Feld haben.
|
||||
*
|
||||
* Serialisiert als JSON-Objekt, dessen Werte [JsonElement] sind.
|
||||
* Zugriff via [getLong], [getInt], [getDouble].
|
||||
*/
|
||||
val extras: Map<String, JsonElement> = emptyMap()
|
||||
) {
|
||||
|
||||
// ── Typed accessors for `extras` ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt den `extras`-Wert für [key] als [Long] zurück, oder `null`
|
||||
* wenn der Schlüssel fehlt oder der Wert kein gültiger Ganzzahlwert ist.
|
||||
*/
|
||||
fun getLong(
|
||||
key: String
|
||||
): Long? = ( extras[ key ] as? JsonPrimitive )?.longOrNull
|
||||
|
||||
/**
|
||||
* Gibt den `extras`-Wert für [key] als [Int] zurück, oder `null`
|
||||
* wenn der Schlüssel fehlt oder der Wert nicht konvertierbar ist.
|
||||
*/
|
||||
fun getInt(
|
||||
key: String
|
||||
): Int? = getLong( key )?.toInt()
|
||||
|
||||
/**
|
||||
* Gibt den `extras`-Wert für [key] als [Double] zurück, oder `null`
|
||||
* wenn der Schlüssel fehlt oder der Wert kein gültiger Dezimalwert ist.
|
||||
*/
|
||||
fun getDouble(
|
||||
key: String
|
||||
): Double? = ( extras[ key ] as? JsonPrimitive )?.doubleOrNull
|
||||
|
||||
/**
|
||||
* Gibt den `extras`-Wert für [key] als [Float] zurück, oder `null`.
|
||||
*/
|
||||
fun getFloat(
|
||||
key: String
|
||||
): Float? = getDouble( key )?.toFloat()
|
||||
|
||||
/**
|
||||
* Gibt den `extras`-Wert für [key] als [Boolean] zurück, oder `null`.
|
||||
*/
|
||||
fun getBoolean(
|
||||
key: String
|
||||
): Boolean? = ( extras[ key ] as? JsonPrimitive )?.let {
|
||||
it.content.lowercase() == "true"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package club.mcscrims.speedhg.kit
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.config.CustomGameSettings
|
||||
import club.mcscrims.speedhg.kit.ability.ActiveAbility
|
||||
import club.mcscrims.speedhg.kit.ability.PassiveAbility
|
||||
import net.kyori.adventure.text.Component
|
||||
@@ -21,7 +23,18 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* using a `when(playstyle)` expression.
|
||||
* 5. Register the kit via `plugin.kitManager.registerKit(YourKit())` in [SpeedHG.onEnable].
|
||||
*
|
||||
* See [TemplateKit] for a fully annotated example.
|
||||
* ## Configuration
|
||||
* Each kit's settings are configurable via `SPEEDHG_CUSTOM_SETTINGS` environment variable.
|
||||
* Use [override] to access the kit's [CustomGameSettings.KitOverride], then call
|
||||
* [CustomGameSettings.KitOverride.getLong], [CustomGameSettings.KitOverride.getInt], or
|
||||
* [CustomGameSettings.KitOverride.getDouble] with a fallback:
|
||||
*
|
||||
* ```kotlin
|
||||
* private val cooldownMs: Long
|
||||
* get() = override().getLong( "cooldown_ms" ) ?: DEFAULT_COOLDOWN_MS
|
||||
* ```
|
||||
*
|
||||
* See [NinjaKit] for a full example.
|
||||
*/
|
||||
abstract class Kit {
|
||||
|
||||
@@ -37,6 +50,30 @@ abstract class Kit {
|
||||
/** Icon used in the kit selection GUI. */
|
||||
abstract val icon: Material
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Configuration helper
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns this kit's [CustomGameSettings.KitOverride], or a default instance
|
||||
* if no override has been configured for this kit.
|
||||
*
|
||||
* Intended to be called inside inner ability classes via the outer kit reference:
|
||||
* ```kotlin
|
||||
* private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) {
|
||||
* private val cooldownMs: Long
|
||||
* get() = override().getLong( "cooldown_ms" ) ?: DEFAULT_COOLDOWN_MS
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The method is `open` so kits that cache their override as a `lazy` property
|
||||
* can override this for performance if desired. In most cases the default
|
||||
* implementation (live lookup) is sufficient — it touches only an in-memory map.
|
||||
*/
|
||||
open fun override(): CustomGameSettings.KitOverride =
|
||||
SpeedHG.instance.customGameManager.settings.kits.kits[ id ]
|
||||
?: CustomGameSettings.KitOverride()
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Playstyle-specific abilities — implement with a `when` expression
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -44,16 +81,8 @@ abstract class Kit {
|
||||
/**
|
||||
* Return the [ActiveAbility] for the given [playstyle].
|
||||
*
|
||||
* ```kotlin
|
||||
* override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||
* Playstyle.AGGRESSIVE -> AggressiveActive()
|
||||
* Playstyle.DEFENSIVE -> DefensiveActive()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Performance note:** This is called frequently by [KitEventDispatcher].
|
||||
* Prefer returning a cached singleton (`private val aggressiveActive = AggressiveActive()`)
|
||||
* over allocating a new instance on each call.
|
||||
* **Performance note:** Prefer returning a cached singleton over allocating
|
||||
* a new instance on each call.
|
||||
*/
|
||||
abstract fun getActiveAbility( playstyle: Playstyle ): ActiveAbility
|
||||
|
||||
@@ -68,8 +97,6 @@ abstract class Kit {
|
||||
|
||||
/**
|
||||
* Give the player their kit-specific items at game start (after teleportation).
|
||||
* The standard HG items (soup, compass, etc.) are already given by [GameManager].
|
||||
* Only add kit-exclusive items here.
|
||||
*/
|
||||
abstract fun giveItems( player: Player, playstyle: Playstyle )
|
||||
|
||||
@@ -77,34 +104,14 @@ abstract class Kit {
|
||||
// Lifecycle hooks (optional)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called once per round when the kit is applied to [player].
|
||||
* Use for permanent potion effects, scoreboard objectives, repeating tasks, etc.
|
||||
* The matching [PassiveAbility.onActivate] is called immediately after this.
|
||||
*/
|
||||
open fun onAssign( player: Player, playstyle: Playstyle ) {}
|
||||
|
||||
/**
|
||||
* Called when the kit is removed (game over / round reset).
|
||||
* The matching [PassiveAbility.onDeactivate] is called immediately before this.
|
||||
*/
|
||||
open fun onRemove( player: Player ) {}
|
||||
|
||||
/**
|
||||
* Called when the player using this kit scores a kill.
|
||||
* Dispatched by [KitEventDispatcher] via [PlayerDeathEvent].
|
||||
*/
|
||||
open fun onKillEnemy( killer: Player, victim: Player ) {}
|
||||
|
||||
/**
|
||||
* Called when the player toggles sneak. Dispatched by [KitEventDispatcher].
|
||||
* @param isSneaking true = player just started sneaking.
|
||||
*/
|
||||
open fun onToggleSneak( player: Player, isSneaking: Boolean ) {}
|
||||
|
||||
/**
|
||||
* Called when a player's item breaks. Use to replace kit armor automatically.
|
||||
*/
|
||||
open fun onItemBreak( player: Player, brokenItem: ItemStack ) {}
|
||||
|
||||
}
|
||||
@@ -27,19 +27,48 @@ import kotlin.math.sin
|
||||
* ## NinjaKit
|
||||
*
|
||||
* | Playstyle | Aktive Fähigkeit | Passive |
|
||||
* |-------------|------------------------------------------------------------------|-------------------------------------|
|
||||
* | AGGRESSIVE | Sneak → teleportiert hinter den letzten Gegner (10-s-Fenster) | - |
|
||||
* | DEFENSIVE | Smoke-Aura (Blindness I + Slow I) | - |
|
||||
* |-------------|------------------------------------------------------------------|---------|
|
||||
* | AGGRESSIVE | Sneak → teleportiert hinter den letzten Gegner (Hit-Fenster) | – |
|
||||
* | DEFENSIVE | Smoke-Aura (Blindness I + Slow I) im Umkreis | – |
|
||||
*
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* Alle Werte können zur Laufzeit per `extras`-Map im JSON überschrieben werden.
|
||||
* Nicht vorhandene Schlüssel fallen automatisch auf die Defaults im [companion object] zurück.
|
||||
*
|
||||
* | JSON-Schlüssel | Typ | Default | Beschreibung |
|
||||
* |-------------------------|-------|----------------|--------------------------------------------|
|
||||
* | `teleport_cooldown_ms` | Long | `12_000` | Cooldown zwischen Teleports (ms) |
|
||||
* | `hit_window_ms` | Long | `10_000` | Wie lange ein Treffer als Ziel gilt (ms) |
|
||||
* | `smoke_radius` | Double| `3.0` | Radius der Smoke-Aura (Blöcke) |
|
||||
* | `smoke_duration_ticks` | Long | `200` | Dauer der Smoke-Aura in Ticks |
|
||||
* | `smoke_refresh_ticks` | Long | `10` | Ticks zwischen Aura-Aktualisierungen |
|
||||
* | `smoke_effect_ticks` | Int | `30` | Dauer des Blindness/Slowness-Effekts |
|
||||
*
|
||||
* ### Beispiel JSON
|
||||
* ```json
|
||||
* {
|
||||
* "kits": {
|
||||
* "ninja": {
|
||||
* "hits_required": 12,
|
||||
* "extras": {
|
||||
* "teleport_cooldown_ms": 8000,
|
||||
* "hit_window_ms": 15000,
|
||||
* "smoke_radius": 4.5,
|
||||
* "smoke_duration_ticks": 300
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Teleport-Mechanismus
|
||||
* `onToggleSneak` wird vom [KitEventDispatcher] aufgerufen. Er prüft das
|
||||
* [lastHitEnemy]-Fenster (10 s) und berechnet eine Position 1,8 Blöcke
|
||||
* hinter dem Feind (entgegen seiner Blickrichtung).
|
||||
* [onToggleSneak] wird vom [KitEventDispatcher] aufgerufen. Er prüft das
|
||||
* [lastHitEnemy]-Fenster und berechnet eine Position 1,8 Blöcke hinter dem Feind.
|
||||
*
|
||||
* ### Smoke-Mechanismus
|
||||
* Ein BukkitTask (10 Ticks) spawnt einen Partikelring mit [SMOKE_RADIUS] Blöcken
|
||||
* Radius. Jeder Feind im Ring erhält Blindness I + Slowness I (30 Ticks),
|
||||
* die alle 0,5 s erneuert werden, solange er im Rauch bleibt.
|
||||
* Ein BukkitTask spawnt alle [smokRefreshTicks] einen Partikelring. Jeder Feind
|
||||
* im Ring erhält Blindness I + Slowness I, die regelmäßig erneuert werden.
|
||||
*/
|
||||
class NinjaKit : Kit() {
|
||||
|
||||
@@ -54,19 +83,75 @@ class NinjaKit : Kit() {
|
||||
override val icon: Material
|
||||
get() = Material.FEATHER
|
||||
|
||||
/** ninjaUUID → (enemyUUID, System.currentTimeMillis() des letzten Treffers) */
|
||||
// ── Internal state ────────────────────────────────────────────────────────
|
||||
|
||||
/** ninjaUUID → (enemyUUID, timestamp-ms of the last qualifying hit) */
|
||||
internal val lastHitEnemy: MutableMap<UUID, Pair<UUID, Long>> = ConcurrentHashMap()
|
||||
|
||||
private val smokeTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
||||
private val teleportCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
// ── Defaults (used as fallback when no custom settings are present) ───────
|
||||
|
||||
companion object {
|
||||
const val HIT_WINDOW_MS = 10_000L // 10s - Gültigkeit des Teleport-Ziels
|
||||
const val SMOKE_RADIUS = 3.0 // Blöcke
|
||||
const val SMOKE_MAX_DURATION = 10_000L // 10s
|
||||
const val TELEPORT_COOLDOWN_MS = 12_000L // 12s zwischen Teleports
|
||||
/**
|
||||
* All default values are defined here as named constants so they can be
|
||||
* referenced in documentation and serve as the canonical fallback values
|
||||
* when a game host has not provided a custom override.
|
||||
*/
|
||||
const val DEFAULT_TELEPORT_COOLDOWN_MS = 12_000L
|
||||
const val DEFAULT_HIT_WINDOW_MS = 10_000L
|
||||
const val DEFAULT_SMOKE_RADIUS = 3.0
|
||||
const val DEFAULT_SMOKE_DURATION_TICKS = 200L
|
||||
const val DEFAULT_SMOKE_REFRESH_TICKS = 10L
|
||||
const val DEFAULT_SMOKE_EFFECT_TICKS = 30
|
||||
}
|
||||
|
||||
// ── Gecachte Instanzen ────────────────────────────────────────────────────
|
||||
// ── Live config accessors (read from override or fall back to defaults) ───
|
||||
|
||||
/**
|
||||
* Milliseconds a player must wait between successive teleports.
|
||||
* JSON key: `teleport_cooldown_ms`
|
||||
*/
|
||||
private val teleportCooldownMs: Long
|
||||
get() = override().getLong( "teleport_cooldown_ms" ) ?: DEFAULT_TELEPORT_COOLDOWN_MS
|
||||
|
||||
/**
|
||||
* Milliseconds during which the last-hit enemy is considered a valid teleport target.
|
||||
* JSON key: `hit_window_ms`
|
||||
*/
|
||||
private val hitWindowMs: Long
|
||||
get() = override().getLong( "hit_window_ms" ) ?: DEFAULT_HIT_WINDOW_MS
|
||||
|
||||
/**
|
||||
* Block radius of the defensive smoke aura.
|
||||
* JSON key: `smoke_radius`
|
||||
*/
|
||||
private val smokeRadius: Double
|
||||
get() = override().getDouble( "smoke_radius" ) ?: DEFAULT_SMOKE_RADIUS
|
||||
|
||||
/**
|
||||
* Total duration (in ticks) of the smoke aura after activation.
|
||||
* JSON key: `smoke_duration_ticks`
|
||||
*/
|
||||
private val smokeDurationTicks: Long
|
||||
get() = override().getLong( "smoke_duration_ticks" ) ?: DEFAULT_SMOKE_DURATION_TICKS
|
||||
|
||||
/**
|
||||
* How often (in ticks) the smoke aura refreshes particles and reapplies effects.
|
||||
* JSON key: `smoke_refresh_ticks`
|
||||
*/
|
||||
private val smokeRefreshTicks: Long
|
||||
get() = override().getLong( "smoke_refresh_ticks" ) ?: DEFAULT_SMOKE_REFRESH_TICKS
|
||||
|
||||
/**
|
||||
* Duration (in ticks) of Blindness and Slowness applied to enemies inside the smoke.
|
||||
* JSON key: `smoke_effect_ticks`
|
||||
*/
|
||||
private val smokeEffectTicks: Int
|
||||
get() = override().getInt( "smoke_effect_ticks" ) ?: DEFAULT_SMOKE_EFFECT_TICKS
|
||||
|
||||
// ── Cached ability instances ──────────────────────────────────────────────
|
||||
|
||||
private val aggressiveActive = NoActive( Playstyle.AGGRESSIVE )
|
||||
private val defensiveActive = DefensiveActive()
|
||||
@@ -89,14 +174,15 @@ class NinjaKit : Kit() {
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
if ( playstyle != Playstyle.DEFENSIVE )
|
||||
return
|
||||
if ( playstyle != Playstyle.DEFENSIVE ) return
|
||||
|
||||
val item = ItemBuilder( Material.FEATHER )
|
||||
.name( defensiveActive.name )
|
||||
@@ -117,7 +203,7 @@ class NinjaKit : Kit() {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Sneak → Teleport (nur AGGRESSIVE, via KitEventDispatcher)
|
||||
// Sneak → Teleport (AGGRESSIVE, dispatched by KitEventDispatcher)
|
||||
// =========================================================================
|
||||
|
||||
override fun onToggleSneak(
|
||||
@@ -125,14 +211,14 @@ class NinjaKit : Kit() {
|
||||
isSneaking: Boolean
|
||||
) {
|
||||
if ( !isSneaking ) return
|
||||
if (plugin.kitManager.getSelectedPlaystyle( player ) != Playstyle.AGGRESSIVE ) return
|
||||
if ( plugin.kitManager.getSelectedPlaystyle( player ) != Playstyle.AGGRESSIVE ) return
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val lastUse = teleportCooldowns[ player.uniqueId ] ?: 0L
|
||||
|
||||
if ( now - lastUse < TELEPORT_COOLDOWN_MS )
|
||||
if ( now - lastUse < teleportCooldownMs )
|
||||
{
|
||||
val secLeft = ( TELEPORT_COOLDOWN_MS - ( now - lastUse )) / 1000
|
||||
val secLeft = ( teleportCooldownMs - ( now - lastUse )) / 1000
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.cooldown", "time" to secLeft.toString() ))
|
||||
return
|
||||
}
|
||||
@@ -142,7 +228,7 @@ class NinjaKit : Kit() {
|
||||
return
|
||||
}
|
||||
|
||||
if ( now - hitTime > HIT_WINDOW_MS )
|
||||
if ( now - hitTime > hitWindowMs )
|
||||
{
|
||||
lastHitEnemy.remove( player.uniqueId )
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.target_expired" ))
|
||||
@@ -150,19 +236,21 @@ class NinjaKit : Kit() {
|
||||
}
|
||||
|
||||
val enemy = Bukkit.getPlayer( enemyUUID ) ?: return
|
||||
if (!plugin.gameManager.alivePlayers.contains( enemy.uniqueId )) return
|
||||
if ( !plugin.gameManager.alivePlayers.contains( enemy.uniqueId ) ) return
|
||||
|
||||
performTeleport( player, enemy )
|
||||
teleportCooldowns[ player.uniqueId ] = now
|
||||
}
|
||||
|
||||
// ── Teleport implementation ───────────────────────────────────────────────
|
||||
|
||||
private fun performTeleport(
|
||||
player: Player,
|
||||
enemy: Player
|
||||
) {
|
||||
val enemyDir = enemy.location.direction.normalize()
|
||||
var dest = enemy.location.clone()
|
||||
.subtract(enemyDir.multiply( 1.8 ))
|
||||
.subtract( enemyDir.multiply( 1.8 ) )
|
||||
.add( 0.0, 0.1, 0.0 )
|
||||
|
||||
if ( !dest.block.type.isAir ) dest = dest.add( 0.0, 1.0, 0.0 )
|
||||
@@ -189,6 +277,10 @@ class NinjaKit : Kit() {
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.teleported" ))
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE active – Smoke Aura
|
||||
// =========================================================================
|
||||
|
||||
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
@@ -208,51 +300,91 @@ class NinjaKit : Kit() {
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
// Cancel any existing smoke aura before starting a new one
|
||||
smokeTasks.remove( player.uniqueId )?.cancel()
|
||||
|
||||
// Snapshot the config values at activation time so mid-round changes
|
||||
// don't alter an already-running aura unexpectedly.
|
||||
val capturedRefreshTicks = smokeRefreshTicks
|
||||
val capturedRadius = smokeRadius
|
||||
val capturedEffectTicks = smokeEffectTicks
|
||||
|
||||
val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ))
|
||||
if ( !player.isOnline ||
|
||||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
|
||||
{
|
||||
smokeTasks.remove( player.uniqueId )?.cancel()
|
||||
return@runTaskTimer
|
||||
}
|
||||
|
||||
val center = player.location
|
||||
spawnSmokeRing( player, capturedRadius )
|
||||
applyEffectsToEnemies( player, capturedRadius, capturedEffectTicks )
|
||||
|
||||
for ( i in 0 until 10 )
|
||||
{
|
||||
val angle = i * ( 2.0 * Math.PI / 10.0 )
|
||||
center.world.spawnParticle(
|
||||
Particle.CAMPFIRE_COSY_SMOKE,
|
||||
center.clone().add(cos( angle ) * SMOKE_RADIUS, 0.8, sin( angle ) * SMOKE_RADIUS),
|
||||
1, 0.05, 0.12, 0.05, 0.004
|
||||
)
|
||||
}
|
||||
|
||||
center.world
|
||||
.getNearbyEntities( center, SMOKE_RADIUS, 2.0, SMOKE_RADIUS )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { enemy ->
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.BLINDNESS, 30, 0, false, false, true
|
||||
))
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.SLOWNESS, 30, 0, false, false, true
|
||||
))
|
||||
}
|
||||
}, 0L, 10L )
|
||||
}, 0L, capturedRefreshTicks )
|
||||
|
||||
smokeTasks[ player.uniqueId ] = task
|
||||
|
||||
// Schedule automatic aura expiry
|
||||
val capturedDurationTicks = smokeDurationTicks
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
smokeTasks.remove( player.uniqueId )?.cancel()
|
||||
}, SMOKE_MAX_DURATION * 20L )
|
||||
}, capturedDurationTicks )
|
||||
|
||||
player.playSound( player.location, Sound.ENTITY_ENDERMAN_AMBIENT, 0.7f, 1.8f )
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.smoke_activated" ))
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
// ── Rendering helpers (private to this inner class) ───────────────────
|
||||
|
||||
private fun spawnSmokeRing(
|
||||
player: Player,
|
||||
radius: Double
|
||||
) {
|
||||
val center = player.location
|
||||
val steps = 10
|
||||
|
||||
for ( i in 0 until steps )
|
||||
{
|
||||
val angle = i * ( 2.0 * Math.PI / steps )
|
||||
center.world.spawnParticle(
|
||||
Particle.CAMPFIRE_COSY_SMOKE,
|
||||
center.clone().add(
|
||||
cos( angle ) * radius,
|
||||
0.8,
|
||||
sin( angle ) * radius
|
||||
),
|
||||
1, 0.05, 0.12, 0.05, 0.004
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyEffectsToEnemies(
|
||||
player: Player,
|
||||
radius: Double,
|
||||
effectTicks: Int
|
||||
) {
|
||||
player.location.world
|
||||
.getNearbyEntities( player.location, radius, 2.0, radius )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player &&
|
||||
plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { enemy ->
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.BLINDNESS, effectTicks, 0,
|
||||
false, false, true
|
||||
))
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.SLOWNESS, effectTicks, 0,
|
||||
false, false, true
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Stubs
|
||||
// =========================================================================
|
||||
|
||||
private class NoActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) {
|
||||
override val kitId = "ninja"
|
||||
|
||||
@@ -30,27 +30,65 @@ import kotlin.math.sin
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|-------------------------------------------------|------------------------------------------|
|
||||
* | AGGRESSIVE | Shockwave + 3× Blink in looking direction | – |
|
||||
* | DEFENSIVE | Shockwave + Freeze nearby enemies for 10 s | Hit-cap: frozen enemies survive max 5 hits|
|
||||
* | DEFENSIVE | Shockwave + Freeze nearby enemies | Hit-cap: frozen enemies survive max 5 hits|
|
||||
*
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* Typisierte Felder in [CustomGameSettings.KitOverride] werden direkt gelesen.
|
||||
* Zusätzliche Einstellungen können über die `extras`-Map konfiguriert werden.
|
||||
*
|
||||
* | Quelle | JSON-Schlüssel | Typ | Default | Beschreibung |
|
||||
* |---------------------|------------------------------|--------|---------|-------------------------------------------|
|
||||
* | Typisiertes Feld | `tw_ability_cooldown_ms` | Long | `20000` | Cooldown beider Aktiv-Fähigkeiten (ms) |
|
||||
* | Typisiertes Feld | `tw_shockwave_radius` | Double | `6.0` | Radius der Schockwelle (Blöcke) |
|
||||
* | Typisiertes Feld | `tw_teleport_range` | Double | `10.0` | Max. Blink-Reichweite (Blöcke) |
|
||||
* | Typisiertes Feld | `tw_max_teleport_charges` | Int | `3` | Blink-Charges pro Schockwellen-Use |
|
||||
* | Typisiertes Feld | `tw_freeze_duration_ticks` | Int | `200` | Freeze-Dauer in Ticks (10 s) |
|
||||
* | Typisiertes Feld | `tw_max_hits_on_frozen` | Int | `5` | Max. Treffer auf gefrorene Gegner |
|
||||
* | `extras` | `shockwave_knockback_speed` | Double | `2.0` | Velocity-Multiplikator der Schockwelle |
|
||||
* | `extras` | `shockwave_knockback_y` | Double | `0.45` | Vertikaler Y-Impuls der Schockwelle |
|
||||
* | `extras` | `blink_step_size` | Double | `0.4` | Raycast-Schrittgröße beim Blink (Blöcke) |
|
||||
* | `extras` | `freeze_refresh_ticks` | Int | `20` | Ticks zwischen Slowness-Refreshes |
|
||||
* | `extras` | `freeze_powder_snow_ticks` | Int | `140` | Powder-Snow-Freeze-Ticks (visuell) |
|
||||
*
|
||||
* ### Beispiel JSON
|
||||
* ```json
|
||||
* {
|
||||
* "kits": {
|
||||
* "kits": {
|
||||
* "theworld": {
|
||||
* "tw_ability_cooldown_ms": 15000,
|
||||
* "tw_shockwave_radius": 8.0,
|
||||
* "tw_max_teleport_charges": 2,
|
||||
* "tw_freeze_duration_ticks": 160,
|
||||
* "tw_max_hits_on_frozen": 3,
|
||||
* "extras": {
|
||||
* "shockwave_knockback_speed": 2.5,
|
||||
* "shockwave_knockback_y": 0.5,
|
||||
* "blink_step_size": 0.3,
|
||||
* "freeze_refresh_ticks": 15
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### AGGRESSIVE active
|
||||
* First use (off cooldown): radial shockwave → grants 3 blink charges.
|
||||
* Each subsequent right-click: teleport up to [TELEPORT_RANGE] blocks in the
|
||||
* player's looking direction (stops before solid blocks). After all 3 charges
|
||||
* are spent, the 20 s cooldown begins.
|
||||
* Erster Use (off cooldown): radiale Schockwelle → gewährt [maxTeleportCharges] Blink-Charges.
|
||||
* Jeder folgende Rechtsklick: teleportiert bis zu [teleportRange] Blöcke in Blickrichtung
|
||||
* (stoppt vor soliden Blöcken). Nach allen Charges beginnt der [abilityCooldownMs]-Cooldown.
|
||||
*
|
||||
* ### DEFENSIVE active
|
||||
* Radial shockwave + [applyFreeze] on every nearby alive enemy. Each frozen
|
||||
* enemy gets a 1-tick velocity-zeroing task for 10 s. The [DefensivePassive]
|
||||
* monitors hits from this player on frozen enemies and unfreezes them after
|
||||
* [MAX_HITS_ON_FROZEN] hits or when time expires.
|
||||
* Radiale Schockwelle + [applyFreeze] auf jeden nahen lebenden Gegner. Ein 1-Tick-Task
|
||||
* setzt die Velocity gefrorener Spieler für [freezeDurationTicks] Ticks auf 0.
|
||||
* [DefensivePassive] beendet den Freeze nach [maxHitsOnFrozen] Treffern.
|
||||
*
|
||||
* ### Why hitsRequired = 0?
|
||||
* Both active abilities require full control over when [execute] fires. Using
|
||||
* the built-in charge system (hitsRequired > 0) would block [execute] after
|
||||
* the first use and prevent the blink/freeze logic from re-running per click.
|
||||
* With hitsRequired = 0 the charge state stays READY permanently and
|
||||
* [execute] is called on every right-click — internal cooldown maps govern
|
||||
* actual recharge.
|
||||
* ### Warum hitsRequired = 0?
|
||||
* Beide Aktiv-Fähigkeiten steuern intern wann [execute] feuert. Das eingebaute
|
||||
* Charge-System würde nach dem ersten Use blockieren und die Blink/Freeze-Logik
|
||||
* pro Klick verhindern. Mit `hitsRequired = 0` bleibt der Charge-State dauerhaft
|
||||
* READY und der interne Cooldown regiert den tatsächlichen Recharge.
|
||||
*/
|
||||
class TheWorldKit : Kit() {
|
||||
|
||||
@@ -58,23 +96,133 @@ class TheWorldKit : Kit() {
|
||||
|
||||
override val id = "theworld"
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent("kits.theworld.name", mapOf())
|
||||
get() = plugin.languageManager.getDefaultComponent( "kits.theworld.name", mapOf() )
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList("kits.theworld.lore")
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "kits.theworld.lore" )
|
||||
override val icon = Material.CLOCK
|
||||
|
||||
// ── Shared kit state ──────────────────────────────────────────────────────
|
||||
// =========================================================================
|
||||
// Defaults
|
||||
// =========================================================================
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_ABILITY_COOLDOWN_MS = 20_000L
|
||||
const val DEFAULT_SHOCKWAVE_RADIUS = 6.0
|
||||
const val DEFAULT_TELEPORT_RANGE = 10.0
|
||||
const val DEFAULT_MAX_TELEPORT_CHARGES = 3
|
||||
const val DEFAULT_FREEZE_DURATION_TICKS = 200
|
||||
const val DEFAULT_MAX_HITS_ON_FROZEN = 5
|
||||
const val DEFAULT_SHOCKWAVE_KNOCKBACK_SPEED = 2.0
|
||||
const val DEFAULT_SHOCKWAVE_KNOCKBACK_Y = 0.45
|
||||
const val DEFAULT_BLINK_STEP_SIZE = 0.4
|
||||
const val DEFAULT_FREEZE_REFRESH_TICKS = 20
|
||||
const val DEFAULT_FREEZE_POWDER_SNOW_TICKS = 140
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Live config accessors
|
||||
//
|
||||
// Typisierte KitOverride-Felder werden direkt von override() gelesen.
|
||||
// extras-Werte fallen auf die Companion-Defaults zurück.
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Aggressive blink charges: playerUUID → remaining uses.
|
||||
* Set to [MAX_TELEPORT_CHARGES] on first right-click, decremented per blink.
|
||||
* Cooldown beider Aktiv-Fähigkeiten in Millisekunden.
|
||||
* Quelle: typisiertes Feld `tw_ability_cooldown_ms`.
|
||||
*/
|
||||
private val abilityCooldownMs: Long
|
||||
get() = override().abilityCooldownMs
|
||||
|
||||
/**
|
||||
* Radius der Schockwelle in Blöcken.
|
||||
* Quelle: typisiertes Feld `tw_shockwave_radius`.
|
||||
*/
|
||||
private val shockwaveRadius: Double
|
||||
get() = override().shockwaveRadius
|
||||
|
||||
/**
|
||||
* Maximale Blink-Reichweite in Blöcken.
|
||||
* Quelle: typisiertes Feld `tw_teleport_range`.
|
||||
*/
|
||||
private val teleportRange: Double
|
||||
get() = override().teleportRange
|
||||
|
||||
/**
|
||||
* Anzahl der Blink-Charges nach einer Schockwelle.
|
||||
* Quelle: typisiertes Feld `tw_max_teleport_charges`.
|
||||
*/
|
||||
private val maxTeleportCharges: Int
|
||||
get() = override().maxTeleportCharges
|
||||
|
||||
/**
|
||||
* Dauer des Freezes in Ticks.
|
||||
* Quelle: typisiertes Feld `tw_freeze_duration_ticks`.
|
||||
*/
|
||||
private val freezeDurationTicks: Int
|
||||
get() = override().freezeDurationTicks
|
||||
|
||||
/**
|
||||
* Maximale Anzahl an Treffern gegen einen gefrorenen Gegner.
|
||||
* Quelle: typisiertes Feld `tw_max_hits_on_frozen`.
|
||||
*/
|
||||
private val maxHitsOnFrozen: Int
|
||||
get() = override().maxHitsOnFrozen
|
||||
|
||||
/**
|
||||
* Horizontaler Velocity-Multiplikator der Schockwelle.
|
||||
* Quelle: `extras["shockwave_knockback_speed"]`.
|
||||
*/
|
||||
private val shockwaveKnockbackSpeed: Double
|
||||
get() = override().getDouble( "shockwave_knockback_speed" )
|
||||
?: DEFAULT_SHOCKWAVE_KNOCKBACK_SPEED
|
||||
|
||||
/**
|
||||
* Vertikaler Y-Impuls der Schockwelle.
|
||||
* Quelle: `extras["shockwave_knockback_y"]`.
|
||||
*/
|
||||
private val shockwaveKnockbackY: Double
|
||||
get() = override().getDouble( "shockwave_knockback_y" )
|
||||
?: DEFAULT_SHOCKWAVE_KNOCKBACK_Y
|
||||
|
||||
/**
|
||||
* Raycast-Schrittgröße in Blöcken beim Blink.
|
||||
* Kleinere Werte = präzisere Kollisionserkennung, mehr CPU-Last.
|
||||
* Quelle: `extras["blink_step_size"]`.
|
||||
*/
|
||||
private val blinkStepSize: Double
|
||||
get() = override().getDouble( "blink_step_size" )
|
||||
?: DEFAULT_BLINK_STEP_SIZE
|
||||
|
||||
/**
|
||||
* Ticks zwischen Slowness-Refreshes während des Freezes.
|
||||
* Quelle: `extras["freeze_refresh_ticks"]`.
|
||||
*/
|
||||
private val freezeRefreshTicks: Int
|
||||
get() = override().getInt( "freeze_refresh_ticks" )
|
||||
?: DEFAULT_FREEZE_REFRESH_TICKS
|
||||
|
||||
/**
|
||||
* Powder-Snow-Freeze-Ticks (rein visuell, kein Gameplay-Effekt).
|
||||
* Quelle: `extras["freeze_powder_snow_ticks"]`.
|
||||
*/
|
||||
private val freezePowderSnowTicks: Int
|
||||
get() = override().getInt( "freeze_powder_snow_ticks" )
|
||||
?: DEFAULT_FREEZE_POWDER_SNOW_TICKS
|
||||
|
||||
// =========================================================================
|
||||
// Shared kit state
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Aggressive Blink-Charges: playerUUID → verbleibende Uses.
|
||||
* Wird auf [maxTeleportCharges] gesetzt beim ersten Rechtsklick, dann dekrementiert.
|
||||
*/
|
||||
internal val teleportCharges: MutableMap<UUID, Int> = ConcurrentHashMap()
|
||||
|
||||
/**
|
||||
* Active freezes: victimUUID → (attackerUUID, [FrozenData]).
|
||||
* Tracked separately per attacker so [onRemove] only thaws enemies
|
||||
* frozen by the leaving player.
|
||||
* Aktive Freezes: victimUUID → (attackerUUID, [FrozenData]).
|
||||
* Getrennt pro Angreifer, damit [onRemove] nur vom verlassenden Spieler verursachte
|
||||
* Freezes auftaut.
|
||||
*/
|
||||
internal val frozenEnemies: MutableMap<UUID, Pair<UUID, FrozenData>> = ConcurrentHashMap()
|
||||
|
||||
@@ -83,63 +231,64 @@ class TheWorldKit : Kit() {
|
||||
val task: BukkitTask
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["theworld"]
|
||||
?: CustomGameSettings.KitOverride()
|
||||
// =========================================================================
|
||||
// Cached ability instances
|
||||
// =========================================================================
|
||||
|
||||
private val ABILITY_COOLDOWN_MS = override().abilityCooldownMs
|
||||
private val SHOCKWAVE_RADIUS = override().shockwaveRadius
|
||||
private val TELEPORT_RANGE = override().teleportRange
|
||||
private val MAX_TELEPORT_CHARGES = override().maxTeleportCharges
|
||||
private val FREEZE_DURATION_TICKS = override().freezeDurationTicks
|
||||
private val MAX_HITS_ON_FROZEN = override().maxHitsOnFrozen
|
||||
}
|
||||
|
||||
// ── Cached ability instances ──────────────────────────────────────────────
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
private val defensiveActive = DefensiveActive()
|
||||
private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE)
|
||||
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
|
||||
private val defensivePassive = DefensivePassive()
|
||||
|
||||
override fun getActiveAbility(playstyle: Playstyle): ActiveAbility = when (playstyle) {
|
||||
override fun getActiveAbility(
|
||||
playstyle: Playstyle
|
||||
): ActiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressiveActive
|
||||
Playstyle.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
|
||||
override fun getPassiveAbility(playstyle: Playstyle): PassiveAbility = when (playstyle) {
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
): PassiveAbility = 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)
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
val active = getActiveAbility( playstyle )
|
||||
|
||||
val item = ItemBuilder(Material.CLOCK)
|
||||
.name(active.name)
|
||||
.lore(listOf(active.description))
|
||||
val item = ItemBuilder( Material.CLOCK )
|
||||
.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 )
|
||||
}
|
||||
|
||||
override fun onRemove(player: Player) {
|
||||
teleportCharges.remove(player.uniqueId)
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
teleportCharges.remove( player.uniqueId )
|
||||
|
||||
// Cancel tasks + thaw every enemy that was frozen by this player
|
||||
val toUnfreeze = frozenEnemies.entries
|
||||
.filter { (_, pair) -> pair.first == player.uniqueId }
|
||||
.map { (victimUUID, pair) -> victimUUID to pair.second }
|
||||
|
||||
toUnfreeze.forEach { (victimUUID, data) ->
|
||||
// Alle vom verlassenden Spieler verursachten Freezes auftauen
|
||||
frozenEnemies.entries
|
||||
.filter { ( _, pair ) -> pair.first == player.uniqueId }
|
||||
.map { ( victimUUID, pair ) -> victimUUID to pair.second }
|
||||
.forEach { ( victimUUID, data ) ->
|
||||
data.task.cancel()
|
||||
frozenEnemies.remove(victimUUID)
|
||||
Bukkit.getPlayer(victimUUID)?.clearFreezeEffects()
|
||||
frozenEnemies.remove( victimUUID )
|
||||
Bukkit.getPlayer( victimUUID )?.clearFreezeEffects()
|
||||
}
|
||||
|
||||
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) }
|
||||
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -147,302 +296,383 @@ class TheWorldKit : Kit() {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Expanding ring of particles + radial knockback.
|
||||
* Expandierender Partikelring + radialer Rückschlag.
|
||||
*
|
||||
* The ring BukkitRunnable adds one ring per tick, radius grows by 1 block/tick.
|
||||
* This gives the visual impression of a shockwave spreading outward.
|
||||
* Der Ring-Task wächst um 1 Block/Tick bis [shockwaveRadius] + 1.
|
||||
* Knockback-Stärke und Y-Impuls werden aus der Live-Config gelesen,
|
||||
* aber zum Aktivierungszeitpunkt als lokale Variablen gesnapshot,
|
||||
* damit eine Mitte-Runde-Konfigurationsänderung die laufende Animation
|
||||
* nicht inkonsistent macht.
|
||||
*/
|
||||
private fun doShockwave(origin: Player) {
|
||||
private fun doShockwave(
|
||||
origin: Player
|
||||
) {
|
||||
val world = origin.world
|
||||
|
||||
// ── Visual: expanding particle ring ───────────────────────────────────
|
||||
// Werte zum Aktivierungszeitpunkt snapshotten
|
||||
val capturedRadius = shockwaveRadius
|
||||
val capturedKnockbackSpeed = shockwaveKnockbackSpeed
|
||||
val capturedKnockbackY = shockwaveKnockbackY
|
||||
|
||||
// ── Visueller Partikelring ────────────────────────────────────────────
|
||||
object : BukkitRunnable() {
|
||||
var r = 0.5
|
||||
override fun run() {
|
||||
if (r > SHOCKWAVE_RADIUS + 1.0) { cancel(); return }
|
||||
val steps = (2 * Math.PI * r * 5).toInt().coerceAtLeast(8)
|
||||
for (i in 0 until steps) {
|
||||
override fun run()
|
||||
{
|
||||
if ( r > capturedRadius + 1.0 ) { cancel(); return }
|
||||
val steps = ( 2 * Math.PI * r * 5 ).toInt().coerceAtLeast( 8 )
|
||||
for ( i in 0 until steps )
|
||||
{
|
||||
val angle = 2 * Math.PI * i / steps
|
||||
val loc = origin.location.clone().add(cos(angle) * r, 1.0, sin(angle) * r)
|
||||
world.spawnParticle(Particle.SWEEP_ATTACK, loc, 1, 0.0, 0.0, 0.0, 0.0)
|
||||
world.spawnParticle(Particle.CRIT, loc, 2, 0.1, 0.1, 0.1, 0.0)
|
||||
val loc = origin.location.clone().add(
|
||||
cos( angle ) * r, 1.0, sin( angle ) * r
|
||||
)
|
||||
world.spawnParticle( Particle.SWEEP_ATTACK, loc, 1, 0.0, 0.0, 0.0, 0.0 )
|
||||
world.spawnParticle( Particle.CRIT, loc, 2, 0.1, 0.1, 0.1, 0.0 )
|
||||
}
|
||||
r += 1.0
|
||||
}
|
||||
}.runTaskTimer(plugin, 0L, 1L)
|
||||
}.runTaskTimer( plugin, 0L, 1L )
|
||||
|
||||
// ── Physics: knock all nearby alive enemies outward ───────────────────
|
||||
world.getNearbyEntities(origin.location, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS)
|
||||
// ── Physik: alle nahen Gegner nach außen schleudern ───────────────────
|
||||
world.getNearbyEntities(
|
||||
origin.location,
|
||||
capturedRadius, capturedRadius, capturedRadius
|
||||
)
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != origin && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
.filter { it != origin &&
|
||||
plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { enemy ->
|
||||
val dir = enemy.location.toVector()
|
||||
.subtract(origin.location.toVector())
|
||||
.subtract( origin.location.toVector() )
|
||||
.normalize()
|
||||
.multiply(2.0)
|
||||
.setY(0.45)
|
||||
.multiply( capturedKnockbackSpeed )
|
||||
.setY( capturedKnockbackY )
|
||||
enemy.velocity = dir
|
||||
}
|
||||
|
||||
world.playSound(origin.location, Sound.ENTITY_WARDEN_SONIC_BOOM, 1f, 0.8f)
|
||||
world.playSound(origin.location, Sound.BLOCK_BEACON_ACTIVATE, 0.6f, 1.5f)
|
||||
world.playSound( origin.location, Sound.ENTITY_WARDEN_SONIC_BOOM, 1f, 0.8f )
|
||||
world.playSound( origin.location, Sound.BLOCK_BEACON_ACTIVATE, 0.6f, 1.5f )
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleports [player] up to [TELEPORT_RANGE] blocks in their looking direction.
|
||||
* Raycast in 0.4-block increments — stops at the last non-solid block.
|
||||
* Teleportiert [player] bis zu [teleportRange] Blöcke in Blickrichtung.
|
||||
* Raycast in [blinkStepSize]-Schritten — stoppt am letzten nicht-soliden Block.
|
||||
*/
|
||||
private fun blink(player: Player) {
|
||||
private fun blink(
|
||||
player: Player
|
||||
) {
|
||||
val capturedRange = teleportRange
|
||||
val capturedStepSize = blinkStepSize
|
||||
|
||||
val dir = player.location.direction.normalize()
|
||||
var target = player.eyeLocation.clone()
|
||||
|
||||
repeat((TELEPORT_RANGE / 0.4).toInt()) {
|
||||
val next = target.clone().add(dir.clone().multiply(0.4))
|
||||
if (next.block.type.isSolid) return@repeat
|
||||
repeat( ( capturedRange / capturedStepSize ).toInt() ) {
|
||||
val next = target.clone().add( dir.clone().multiply( capturedStepSize ) )
|
||||
if ( next.block.type.isSolid ) return@repeat
|
||||
target = next
|
||||
}
|
||||
|
||||
// Adjust to feet position
|
||||
// Auf Füße-Position anpassen
|
||||
target.y -= 1.0
|
||||
target.yaw = player.location.yaw
|
||||
target.pitch = player.location.pitch
|
||||
|
||||
player.teleport(target)
|
||||
player.world.spawnParticle(Particle.PORTAL, target, 30, 0.2, 0.5, 0.2, 0.05)
|
||||
player.world.spawnParticle(Particle.REVERSE_PORTAL, target, 12, 0.3, 0.5, 0.3, 0.0)
|
||||
player.playSound(target, Sound.ENTITY_ENDERMAN_TELEPORT, 0.9f, 1.4f)
|
||||
player.teleport( target )
|
||||
player.world.spawnParticle( Particle.PORTAL, target, 30, 0.2, 0.5, 0.2, 0.05 )
|
||||
player.world.spawnParticle( Particle.REVERSE_PORTAL, target, 12, 0.3, 0.5, 0.3, 0.0 )
|
||||
player.playSound( target, Sound.ENTITY_ENDERMAN_TELEPORT, 0.9f, 1.4f )
|
||||
}
|
||||
|
||||
/**
|
||||
* Immobilises [target], capping hits from [attacker] at [MAX_HITS_ON_FROZEN].
|
||||
* A 1-tick repeating task zeros horizontal + upward velocity for 10 seconds.
|
||||
* Immobilisiert [target] und begrenzt Treffer von [attacker] auf [maxHitsOnFrozen].
|
||||
* Ein 1-Tick-Task setzt horizontale + aufwärts gerichtete Velocity auf 0.
|
||||
*
|
||||
* Konfigurationswerte werden zum Aktivierungszeitpunkt gesnapshot, damit
|
||||
* eine Konfigurationsänderung den laufenden Freeze nicht inkonsistent macht.
|
||||
*/
|
||||
private fun applyFreeze(attacker: Player, target: Player) {
|
||||
// Overwrite any existing freeze
|
||||
frozenEnemies.remove(target.uniqueId)?.second?.task?.cancel()
|
||||
private fun applyFreeze(
|
||||
attacker: Player,
|
||||
target: Player
|
||||
) {
|
||||
// Vorhandenen Freeze überschreiben
|
||||
frozenEnemies.remove( target.uniqueId )?.second?.task?.cancel()
|
||||
|
||||
val capturedDurationTicks = freezeDurationTicks
|
||||
val capturedRefreshTicks = freezeRefreshTicks
|
||||
val capturedPowderSnowTicks = freezePowderSnowTicks
|
||||
val capturedMaxHits = maxHitsOnFrozen
|
||||
|
||||
target.applyFreezeEffects()
|
||||
|
||||
val task = object : BukkitRunnable() {
|
||||
var ticks = 0
|
||||
override fun run() {
|
||||
override fun run()
|
||||
{
|
||||
ticks++
|
||||
|
||||
if (ticks >= FREEZE_DURATION_TICKS || !target.isOnline ||
|
||||
!plugin.gameManager.alivePlayers.contains(target.uniqueId) ||
|
||||
!frozenEnemies.containsKey(target.uniqueId) ||
|
||||
plugin.gameManager.currentState == GameState.ENDING) // ← neu
|
||||
if ( ticks >= capturedDurationTicks ||
|
||||
!target.isOnline ||
|
||||
!plugin.gameManager.alivePlayers.contains( target.uniqueId ) ||
|
||||
!frozenEnemies.containsKey( target.uniqueId ) ||
|
||||
plugin.gameManager.currentState == GameState.ENDING )
|
||||
{
|
||||
doUnfreeze(target)
|
||||
doUnfreeze( target )
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
// Zero horizontal + upward velocity every tick
|
||||
// Horizontale + aufwärts Velocity jedes Tick nullen
|
||||
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 }
|
||||
target.velocity = v.setX( 0.0 ).setZ( 0.0 )
|
||||
.let { if ( it.y > 0.0 ) it.setY( 0.0 ) else it }
|
||||
|
||||
// Refresh slowness every second so it doesn't expire mid-freeze
|
||||
if (ticks % 20 == 0) target.applyFreezeEffects()
|
||||
// Slowness jede Sekunde refreshen damit sie nicht ausläuft
|
||||
if ( ticks % capturedRefreshTicks == 0 )
|
||||
target.applyFreezeEffects()
|
||||
|
||||
// Powder-snow visual (cosmetic)
|
||||
if (target.freezeTicks < 140) target.freezeTicks = 140
|
||||
// Powder-Snow-Visuals (rein kosmetisch)
|
||||
if ( target.freezeTicks < capturedPowderSnowTicks )
|
||||
target.freezeTicks = capturedPowderSnowTicks
|
||||
}
|
||||
}.runTaskTimer(plugin, 0L, 1L)
|
||||
}.runTaskTimer( plugin, 0L, 1L )
|
||||
|
||||
frozenEnemies[target.uniqueId] = Pair(
|
||||
frozenEnemies[ target.uniqueId ] = Pair(
|
||||
attacker.uniqueId,
|
||||
FrozenData(hitsRemaining = MAX_HITS_ON_FROZEN, task = task)
|
||||
FrozenData( hitsRemaining = capturedMaxHits, task = task )
|
||||
)
|
||||
|
||||
target.sendActionBar(target.trans("kits.theworld.messages.frozen_received"))
|
||||
target.world.spawnParticle(Particle.SNOWFLAKE,
|
||||
target.location.clone().add(0.0, 1.0, 0.0), 15, 0.3, 0.5, 0.3, 0.05)
|
||||
target.sendActionBar( target.trans( "kits.theworld.messages.frozen_received" ) )
|
||||
target.world.spawnParticle(
|
||||
Particle.SNOWFLAKE,
|
||||
target.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
15, 0.3, 0.5, 0.3, 0.05
|
||||
)
|
||||
}
|
||||
|
||||
private fun doUnfreeze(target: Player) {
|
||||
frozenEnemies.remove(target.uniqueId)
|
||||
private fun doUnfreeze(
|
||||
target: Player
|
||||
) {
|
||||
frozenEnemies.remove( target.uniqueId )
|
||||
target.clearFreezeEffects()
|
||||
if (target.isOnline)
|
||||
target.sendActionBar(target.trans("kits.theworld.messages.frozen_expired"))
|
||||
if ( target.isOnline )
|
||||
target.sendActionBar( target.trans( "kits.theworld.messages.frozen_expired" ) )
|
||||
}
|
||||
|
||||
// ── Player extension helpers ──────────────────────────────────────────────
|
||||
// ── Player-Extension-Helpers ──────────────────────────────────────────────
|
||||
|
||||
private fun Player.applyFreezeEffects() {
|
||||
addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS,
|
||||
/* duration */ 25,
|
||||
/* amplifier */ 127,
|
||||
/* ambient */ false,
|
||||
/* particles */ false,
|
||||
/* icon */ true))
|
||||
addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE,
|
||||
25, 127, false, false, false))
|
||||
private fun Player.applyFreezeEffects()
|
||||
{
|
||||
addPotionEffect(PotionEffect(
|
||||
PotionEffectType.SLOWNESS,
|
||||
25, 127,
|
||||
false, false, true
|
||||
))
|
||||
addPotionEffect(PotionEffect(
|
||||
PotionEffectType.MINING_FATIGUE,
|
||||
25, 127,
|
||||
false, false, false
|
||||
))
|
||||
}
|
||||
|
||||
private fun Player.clearFreezeEffects() {
|
||||
removePotionEffect(PotionEffectType.SLOWNESS)
|
||||
removePotionEffect(PotionEffectType.MINING_FATIGUE)
|
||||
private fun Player.clearFreezeEffects()
|
||||
{
|
||||
removePotionEffect( PotionEffectType.SLOWNESS )
|
||||
removePotionEffect( PotionEffectType.MINING_FATIGUE )
|
||||
freezeTicks = 0
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AGGRESSIVE active – Shockwave → 3× blink
|
||||
// AGGRESSIVE active – Schockwelle → 3× Blink
|
||||
// =========================================================================
|
||||
|
||||
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) {
|
||||
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
/** Cooldowns pro Spieler: UUID → letzter Aktivierungs-Timestamp (ms). */
|
||||
private val cooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
override val kitId: String
|
||||
get() = "theworld"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage(
|
||||
"kits.theworld.items.clock.aggressive.name")
|
||||
"kits.theworld.items.clock.aggressive.name"
|
||||
)
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage(
|
||||
"kits.theworld.items.clock.aggressive.description")
|
||||
"kits.theworld.items.clock.aggressive.description"
|
||||
)
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 0
|
||||
override val triggerMaterial = Material.CLOCK
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
|
||||
// ── Spend a blink charge if any are available ─────────────────────
|
||||
val charges = teleportCharges[player.uniqueId] ?: 0
|
||||
if (charges > 0) {
|
||||
blink(player)
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
// ── Vorhandene Blink-Charge verbrauchen ───────────────────────────
|
||||
val charges = teleportCharges[ player.uniqueId ] ?: 0
|
||||
if ( charges > 0 )
|
||||
{
|
||||
blink( player )
|
||||
val remaining = charges - 1
|
||||
teleportCharges[player.uniqueId] = remaining
|
||||
teleportCharges[ player.uniqueId ] = remaining
|
||||
|
||||
if (remaining > 0)
|
||||
if ( remaining > 0 )
|
||||
player.sendActionBar(player.trans(
|
||||
"kits.theworld.messages.teleport_charges",
|
||||
mapOf("charges" to remaining.toString())))
|
||||
else {
|
||||
teleportCharges.remove(player.uniqueId)
|
||||
player.sendActionBar(player.trans("kits.theworld.messages.charges_exhausted"))
|
||||
mapOf( "charges" to remaining.toString() )
|
||||
))
|
||||
else
|
||||
{
|
||||
teleportCharges.remove( player.uniqueId )
|
||||
player.sendActionBar(player.trans( "kits.theworld.messages.charges_exhausted" ))
|
||||
}
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
// ── Cooldown gate ─────────────────────────────────────────────────
|
||||
// ── Cooldown prüfen ───────────────────────────────────────────────
|
||||
val now = System.currentTimeMillis()
|
||||
val lastUse = cooldowns[player.uniqueId] ?: 0L
|
||||
if (now - lastUse < ABILITY_COOLDOWN_MS) {
|
||||
val secsLeft = (ABILITY_COOLDOWN_MS - (now - lastUse)) / 1000
|
||||
return AbilityResult.ConditionNotMet("Cooldown: ${secsLeft}s")
|
||||
val lastUse = cooldowns[ player.uniqueId ] ?: 0L
|
||||
val capturedCooldown = abilityCooldownMs
|
||||
|
||||
if ( now - lastUse < capturedCooldown )
|
||||
{
|
||||
val secsLeft = ( capturedCooldown - ( now - lastUse ) ) / 1000
|
||||
return AbilityResult.ConditionNotMet( "Cooldown: ${secsLeft}s" )
|
||||
}
|
||||
|
||||
// ── Shockwave + grant 3 blink charges ────────────────────────────
|
||||
doShockwave(player)
|
||||
teleportCharges[player.uniqueId] = MAX_TELEPORT_CHARGES
|
||||
cooldowns[player.uniqueId] = now
|
||||
// ── Schockwelle + Charges vergeben ────────────────────────────────
|
||||
val capturedCharges = maxTeleportCharges
|
||||
|
||||
doShockwave( player )
|
||||
teleportCharges[ player.uniqueId ] = capturedCharges
|
||||
cooldowns[ player.uniqueId ] = now
|
||||
|
||||
player.sendActionBar(player.trans(
|
||||
"kits.theworld.messages.shockwave_and_blink",
|
||||
mapOf("charges" to MAX_TELEPORT_CHARGES.toString())))
|
||||
mapOf( "charges" to capturedCharges.toString() )
|
||||
))
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE active – Shockwave + freeze + 5-hit cap
|
||||
// DEFENSIVE active – Schockwelle + Freeze + 5-Treffer-Cap
|
||||
// =========================================================================
|
||||
|
||||
private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) {
|
||||
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
/** Cooldowns pro Spieler: UUID → letzter Aktivierungs-Timestamp (ms). */
|
||||
private val cooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
override val kitId: String
|
||||
get() = "theworld"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage(
|
||||
"kits.theworld.items.clock.defensive.name")
|
||||
"kits.theworld.items.clock.defensive.name"
|
||||
)
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage(
|
||||
"kits.theworld.items.clock.defensive.description")
|
||||
"kits.theworld.items.clock.defensive.description"
|
||||
)
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 0
|
||||
override val triggerMaterial = Material.CLOCK
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
|
||||
// ── Cooldown gate ─────────────────────────────────────────────────
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
// ── Cooldown prüfen ───────────────────────────────────────────────
|
||||
val now = System.currentTimeMillis()
|
||||
val lastUse = cooldowns[player.uniqueId] ?: 0L
|
||||
if (now - lastUse < ABILITY_COOLDOWN_MS) {
|
||||
val secsLeft = (ABILITY_COOLDOWN_MS - (now - lastUse)) / 1000
|
||||
return AbilityResult.ConditionNotMet("Cooldown: ${secsLeft}s")
|
||||
val lastUse = cooldowns[ player.uniqueId ] ?: 0L
|
||||
val capturedCooldown = abilityCooldownMs
|
||||
|
||||
if ( now - lastUse < capturedCooldown )
|
||||
{
|
||||
val secsLeft = ( capturedCooldown - ( now - lastUse ) ) / 1000
|
||||
return AbilityResult.ConditionNotMet( "Cooldown: ${secsLeft}s" )
|
||||
}
|
||||
|
||||
// ── Ziele ermitteln ───────────────────────────────────────────────
|
||||
val capturedRadius = shockwaveRadius
|
||||
|
||||
val targets = player.world
|
||||
.getNearbyEntities(
|
||||
player.location,
|
||||
SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS)
|
||||
capturedRadius, capturedRadius, capturedRadius
|
||||
)
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player &&
|
||||
plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
|
||||
if (targets.isEmpty())
|
||||
return AbilityResult.ConditionNotMet("No enemies within range!")
|
||||
if ( targets.isEmpty() )
|
||||
return AbilityResult.ConditionNotMet( "No enemies within range!" )
|
||||
|
||||
doShockwave(player)
|
||||
targets.forEach { applyFreeze(player, it) }
|
||||
cooldowns[player.uniqueId] = now
|
||||
// ── Schockwelle + Freeze ──────────────────────────────────────────
|
||||
doShockwave( player )
|
||||
targets.forEach { applyFreeze( player, it ) }
|
||||
cooldowns[ player.uniqueId ] = now
|
||||
|
||||
player.sendActionBar(player.trans(
|
||||
"kits.theworld.messages.freeze_activated",
|
||||
mapOf("count" to targets.size.toString())))
|
||||
mapOf( "count" to targets.size.toString() )
|
||||
))
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE passive – 5-hit cap on frozen enemies
|
||||
// DEFENSIVE passive – 5-Treffer-Cap auf gefrorene Gegner
|
||||
// =========================================================================
|
||||
|
||||
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.theworld.passive.defensive.name")
|
||||
"kits.theworld.passive.defensive.name"
|
||||
)
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage(
|
||||
"kits.theworld.passive.defensive.description")
|
||||
"kits.theworld.passive.defensive.description"
|
||||
)
|
||||
|
||||
/**
|
||||
* Called only when the TheWorld player (attacker) hits someone.
|
||||
* If that someone is frozen and was frozen by this attacker,
|
||||
* decrement their remaining hit allowance.
|
||||
* Wird nur aufgerufen wenn der TheWorld-Spieler (Angreifer) jemanden trifft.
|
||||
* Wenn das Opfer vom gleichen Angreifer eingefroren wurde, Treffer-Cap dekrementieren.
|
||||
*/
|
||||
override fun onHitEnemy(
|
||||
attacker: Player,
|
||||
victim: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
val (frozenBy, data) = frozenEnemies[victim.uniqueId] ?: return
|
||||
// Only count hits from the player who applied this specific freeze
|
||||
if (frozenBy != attacker.uniqueId) return
|
||||
val ( frozenBy, data ) = frozenEnemies[ victim.uniqueId ] ?: return
|
||||
|
||||
// Nur Treffer vom Spieler zählen, der den Freeze ausgelöst hat
|
||||
if ( frozenBy != attacker.uniqueId ) return
|
||||
|
||||
data.hitsRemaining--
|
||||
|
||||
if (data.hitsRemaining <= 0) {
|
||||
doUnfreeze(victim)
|
||||
attacker.sendActionBar(attacker.trans("kits.theworld.messages.freeze_broken"))
|
||||
} else {
|
||||
if ( data.hitsRemaining <= 0 )
|
||||
{
|
||||
doUnfreeze( victim )
|
||||
attacker.sendActionBar( attacker.trans( "kits.theworld.messages.freeze_broken" ) )
|
||||
}
|
||||
else
|
||||
{
|
||||
attacker.sendActionBar(attacker.trans(
|
||||
"kits.theworld.messages.freeze_hits_left",
|
||||
mapOf("hits" to data.hitsRemaining.toString())))
|
||||
mapOf( "hits" to data.hitsRemaining.toString() )
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -451,7 +681,7 @@ class TheWorldKit : Kit() {
|
||||
// AGGRESSIVE no-passive
|
||||
// =========================================================================
|
||||
|
||||
private class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) {
|
||||
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) {
|
||||
override val name = "None"
|
||||
override val description = "None"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user