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.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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
|
@Serializable
|
||||||
data class CustomGameSettings(
|
data class CustomGameSettings(
|
||||||
@@ -29,7 +33,6 @@ data class CustomGameSettings(
|
|||||||
/**
|
/**
|
||||||
* Kit-spezifische Overrides.
|
* Kit-spezifische Overrides.
|
||||||
* Key = Kit.id (z. B. "gladiator", "venom").
|
* Key = Kit.id (z. B. "gladiator", "venom").
|
||||||
* Unbekannte Keys werden von kotlinx.serialization ignoriert.
|
|
||||||
*/
|
*/
|
||||||
val kits: Map<String, KitOverride> = emptyMap()
|
val kits: Map<String, KitOverride> = emptyMap()
|
||||||
|
|
||||||
@@ -46,7 +49,6 @@ data class CustomGameSettings(
|
|||||||
hardcodedDefault: Int
|
hardcodedDefault: Int
|
||||||
): Int
|
): Int
|
||||||
{
|
{
|
||||||
// A hardcoded 0 means the kit is explicitly cooldown-based — never override it.
|
|
||||||
if ( hardcodedDefault == 0 ) return 0
|
if ( hardcodedDefault == 0 ) return 0
|
||||||
|
|
||||||
return kits[ kitId ]?.hitsRequired?.takeIf { it >= 0 }
|
return kits[ kitId ]?.hitsRequired?.takeIf { it >= 0 }
|
||||||
@@ -55,37 +57,64 @@ data class CustomGameSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Kit-spezifische Override-Klassen
|
// Kit-spezifische Override-Klassen
|
||||||
// -----------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gemeinsamer Wrapper für alle Kit-Overrides.
|
* 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
|
* ## Bekannte Felder
|
||||||
* für jedes Kit eine eigene Klasse notwendig ist.
|
* 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
|
@Serializable
|
||||||
data class KitOverride(
|
data class KitOverride(
|
||||||
@SerialName("hits_required") val hitsRequired: Int = -1,
|
@SerialName("hits_required") val hitsRequired: Int = -1,
|
||||||
|
|
||||||
// Goblin
|
// ── Goblin ────────────────────────────────────────────────────────────
|
||||||
@SerialName("steal_duration_seconds") val stealDuration: Int = 60,
|
@SerialName("steal_duration_seconds") val stealDuration: Int = 60,
|
||||||
@SerialName("bunker_radius") val bunkerRadius: Double = 10.0,
|
@SerialName("bunker_radius") val bunkerRadius: Double = 10.0,
|
||||||
|
|
||||||
// Gladiator
|
// ── Gladiator ─────────────────────────────────────────────────────────
|
||||||
@SerialName("arena_radius") val arenaRadius: Int = 11,
|
@SerialName("arena_radius") val arenaRadius: Int = 11,
|
||||||
@SerialName("arena_height") val arenaHeight: Int = 7,
|
@SerialName("arena_height") val arenaHeight: Int = 7,
|
||||||
@SerialName("wither_after_seconds") val witherAfterSeconds: Int = 180,
|
@SerialName("wither_after_seconds") val witherAfterSeconds: Int = 180,
|
||||||
|
|
||||||
// Venom
|
// ── Venom ─────────────────────────────────────────────────────────────
|
||||||
@SerialName("shield_duration_ticks") val shieldDurationTicks: Long = 160L,
|
@SerialName("shield_duration_ticks") val shieldDurationTicks: Long = 160L,
|
||||||
@SerialName("shield_capacity") val shieldCapacity: Double = 15.0,
|
@SerialName("shield_capacity") val shieldCapacity: Double = 15.0,
|
||||||
|
|
||||||
// Voodoo
|
// ── Voodoo ────────────────────────────────────────────────────────────
|
||||||
@SerialName("curse_duration_ms") val curseDurationMs: Long = 15_000L,
|
@SerialName("curse_duration_ms") val curseDurationMs: Long = 15_000L,
|
||||||
|
|
||||||
// BlackPanther
|
// ── BlackPanther ──────────────────────────────────────────────────────
|
||||||
@SerialName("fist_mode_ms") val fistModeDurationMs: Long = 12_000L,
|
@SerialName("fist_mode_ms") val fistModeDurationMs: Long = 12_000L,
|
||||||
@SerialName("push_bonus_damage") val pushBonusDamage: Double = 4.0,
|
@SerialName("push_bonus_damage") val pushBonusDamage: Double = 4.0,
|
||||||
@SerialName("push_radius") val pushRadius: Double = 5.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_radius") val pounceRadius: Double = 3.0,
|
||||||
@SerialName("pounce_damage") val pounceDamage: Double = 6.0,
|
@SerialName("pounce_damage") val pounceDamage: Double = 6.0,
|
||||||
|
|
||||||
// Rattlesnake
|
// ── Rattlesnake ───────────────────────────────────────────────────────
|
||||||
@SerialName("pounce_cooldown_ms") val pounceCooldownMs: Long = 20_000L,
|
@SerialName("pounce_cooldown_ms") val pounceCooldownMs: Long = 20_000L,
|
||||||
@SerialName("pounce_max_sneak_ms") val pounceMaxSneakMs: Long = 3_000L,
|
@SerialName("pounce_max_sneak_ms") val pounceMaxSneakMs: Long = 3_000L,
|
||||||
@SerialName("pounce_min_range") val pounceMinRange: Double = 3.0,
|
@SerialName("pounce_min_range") val pounceMinRange: Double = 3.0,
|
||||||
@SerialName("pounce_max_range") val pounceMaxRange: Double = 10.0,
|
@SerialName("pounce_max_range") val pounceMaxRange: Double = 10.0,
|
||||||
@SerialName("pounce_timeout_ticks") val pounceTimeoutTicks: Long = 30L,
|
@SerialName("pounce_timeout_ticks") val pounceTimeoutTicks: Long = 30L,
|
||||||
|
|
||||||
// TheWorld
|
// ── TheWorld ──────────────────────────────────────────────────────────
|
||||||
@SerialName("tw_ability_cooldown_ms") val abilityCooldownMs: Long = 25_000L,
|
@SerialName("ability_cooldown_ms") val abilityCooldownMs: Long = 20_000L,
|
||||||
@SerialName("tw_shockwave_radius") val shockwaveRadius: Double = 6.0,
|
@SerialName("shockwave_radius") val shockwaveRadius: Double = 6.0,
|
||||||
@SerialName("tw_teleport_range") val teleportRange: Double = 10.0,
|
@SerialName("teleport_range") val teleportRange: Double = 10.0,
|
||||||
@SerialName("tw_max_teleport_charges") val maxTeleportCharges: Int = 3,
|
@SerialName("max_teleport_charges") val maxTeleportCharges: Int = 3,
|
||||||
@SerialName("tw_freeze_duration_ticks") val freezeDurationTicks: Int = 200,
|
@SerialName("freeze_duration_ticks") val freezeDurationTicks: Int = 200,
|
||||||
@SerialName("tw_max_hits_on_frozen") val maxHitsOnFrozen: Int = 5
|
@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
|
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.ActiveAbility
|
||||||
import club.mcscrims.speedhg.kit.ability.PassiveAbility
|
import club.mcscrims.speedhg.kit.ability.PassiveAbility
|
||||||
import net.kyori.adventure.text.Component
|
import net.kyori.adventure.text.Component
|
||||||
@@ -21,7 +23,18 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
* using a `when(playstyle)` expression.
|
* using a `when(playstyle)` expression.
|
||||||
* 5. Register the kit via `plugin.kitManager.registerKit(YourKit())` in [SpeedHG.onEnable].
|
* 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 {
|
abstract class Kit {
|
||||||
|
|
||||||
@@ -37,6 +50,30 @@ abstract class Kit {
|
|||||||
/** Icon used in the kit selection GUI. */
|
/** Icon used in the kit selection GUI. */
|
||||||
abstract val icon: Material
|
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
|
// Playstyle-specific abilities — implement with a `when` expression
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -44,16 +81,8 @@ abstract class Kit {
|
|||||||
/**
|
/**
|
||||||
* Return the [ActiveAbility] for the given [playstyle].
|
* Return the [ActiveAbility] for the given [playstyle].
|
||||||
*
|
*
|
||||||
* ```kotlin
|
* **Performance note:** Prefer returning a cached singleton over allocating
|
||||||
* override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) {
|
* a new instance on each call.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
abstract fun getActiveAbility( playstyle: Playstyle ): ActiveAbility
|
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).
|
* 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 )
|
abstract fun giveItems( player: Player, playstyle: Playstyle )
|
||||||
|
|
||||||
@@ -77,34 +104,14 @@ abstract class Kit {
|
|||||||
// Lifecycle hooks (optional)
|
// 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 ) {}
|
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 ) {}
|
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 ) {}
|
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 ) {}
|
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 ) {}
|
open fun onItemBreak( player: Player, brokenItem: ItemStack ) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -27,19 +27,48 @@ import kotlin.math.sin
|
|||||||
* ## NinjaKit
|
* ## NinjaKit
|
||||||
*
|
*
|
||||||
* | Playstyle | Aktive Fähigkeit | Passive |
|
* | Playstyle | Aktive Fähigkeit | Passive |
|
||||||
* |-------------|------------------------------------------------------------------|-------------------------------------|
|
* |-------------|------------------------------------------------------------------|---------|
|
||||||
* | AGGRESSIVE | Sneak → teleportiert hinter den letzten Gegner (10-s-Fenster) | - |
|
* | AGGRESSIVE | Sneak → teleportiert hinter den letzten Gegner (Hit-Fenster) | – |
|
||||||
* | DEFENSIVE | Smoke-Aura (Blindness I + Slow I) | - |
|
* | 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
|
* ### Teleport-Mechanismus
|
||||||
* `onToggleSneak` wird vom [KitEventDispatcher] aufgerufen. Er prüft das
|
* [onToggleSneak] wird vom [KitEventDispatcher] aufgerufen. Er prüft das
|
||||||
* [lastHitEnemy]-Fenster (10 s) und berechnet eine Position 1,8 Blöcke
|
* [lastHitEnemy]-Fenster und berechnet eine Position 1,8 Blöcke hinter dem Feind.
|
||||||
* hinter dem Feind (entgegen seiner Blickrichtung).
|
|
||||||
*
|
*
|
||||||
* ### Smoke-Mechanismus
|
* ### Smoke-Mechanismus
|
||||||
* Ein BukkitTask (10 Ticks) spawnt einen Partikelring mit [SMOKE_RADIUS] Blöcken
|
* Ein BukkitTask spawnt alle [smokRefreshTicks] einen Partikelring. Jeder Feind
|
||||||
* Radius. Jeder Feind im Ring erhält Blindness I + Slowness I (30 Ticks),
|
* im Ring erhält Blindness I + Slowness I, die regelmäßig erneuert werden.
|
||||||
* die alle 0,5 s erneuert werden, solange er im Rauch bleibt.
|
|
||||||
*/
|
*/
|
||||||
class NinjaKit : Kit() {
|
class NinjaKit : Kit() {
|
||||||
|
|
||||||
@@ -54,19 +83,75 @@ class NinjaKit : Kit() {
|
|||||||
override val icon: Material
|
override val icon: Material
|
||||||
get() = Material.FEATHER
|
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()
|
internal val lastHitEnemy: MutableMap<UUID, Pair<UUID, Long>> = ConcurrentHashMap()
|
||||||
|
|
||||||
private val smokeTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
private val smokeTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
||||||
private val teleportCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
private val teleportCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||||
|
|
||||||
|
// ── Defaults (used as fallback when no custom settings are present) ───────
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HIT_WINDOW_MS = 10_000L // 10s - Gültigkeit des Teleport-Ziels
|
/**
|
||||||
const val SMOKE_RADIUS = 3.0 // Blöcke
|
* All default values are defined here as named constants so they can be
|
||||||
const val SMOKE_MAX_DURATION = 10_000L // 10s
|
* referenced in documentation and serve as the canonical fallback values
|
||||||
const val TELEPORT_COOLDOWN_MS = 12_000L // 12s zwischen Teleports
|
* 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 aggressiveActive = NoActive( Playstyle.AGGRESSIVE )
|
||||||
private val defensiveActive = DefensiveActive()
|
private val defensiveActive = DefensiveActive()
|
||||||
@@ -89,14 +174,15 @@ class NinjaKit : Kit() {
|
|||||||
Playstyle.DEFENSIVE -> defensivePassive
|
Playstyle.DEFENSIVE -> defensivePassive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Item distribution ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||||
|
|
||||||
override fun giveItems(
|
override fun giveItems(
|
||||||
player: Player,
|
player: Player,
|
||||||
playstyle: Playstyle
|
playstyle: Playstyle
|
||||||
) {
|
) {
|
||||||
if ( playstyle != Playstyle.DEFENSIVE )
|
if ( playstyle != Playstyle.DEFENSIVE ) return
|
||||||
return
|
|
||||||
|
|
||||||
val item = ItemBuilder( Material.FEATHER )
|
val item = ItemBuilder( Material.FEATHER )
|
||||||
.name( defensiveActive.name )
|
.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(
|
override fun onToggleSneak(
|
||||||
@@ -125,14 +211,14 @@ class NinjaKit : Kit() {
|
|||||||
isSneaking: Boolean
|
isSneaking: Boolean
|
||||||
) {
|
) {
|
||||||
if ( !isSneaking ) return
|
if ( !isSneaking ) return
|
||||||
if (plugin.kitManager.getSelectedPlaystyle( player ) != Playstyle.AGGRESSIVE ) return
|
if ( plugin.kitManager.getSelectedPlaystyle( player ) != Playstyle.AGGRESSIVE ) return
|
||||||
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val lastUse = teleportCooldowns[ player.uniqueId ] ?: 0L
|
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() ))
|
player.sendActionBar(player.trans( "kits.ninja.messages.cooldown", "time" to secLeft.toString() ))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,7 +228,7 @@ class NinjaKit : Kit() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( now - hitTime > HIT_WINDOW_MS )
|
if ( now - hitTime > hitWindowMs )
|
||||||
{
|
{
|
||||||
lastHitEnemy.remove( player.uniqueId )
|
lastHitEnemy.remove( player.uniqueId )
|
||||||
player.sendActionBar(player.trans( "kits.ninja.messages.target_expired" ))
|
player.sendActionBar(player.trans( "kits.ninja.messages.target_expired" ))
|
||||||
@@ -150,19 +236,21 @@ class NinjaKit : Kit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val enemy = Bukkit.getPlayer( enemyUUID ) ?: return
|
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 )
|
performTeleport( player, enemy )
|
||||||
teleportCooldowns[ player.uniqueId ] = now
|
teleportCooldowns[ player.uniqueId ] = now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Teleport implementation ───────────────────────────────────────────────
|
||||||
|
|
||||||
private fun performTeleport(
|
private fun performTeleport(
|
||||||
player: Player,
|
player: Player,
|
||||||
enemy: Player
|
enemy: Player
|
||||||
) {
|
) {
|
||||||
val enemyDir = enemy.location.direction.normalize()
|
val enemyDir = enemy.location.direction.normalize()
|
||||||
var dest = enemy.location.clone()
|
var dest = enemy.location.clone()
|
||||||
.subtract(enemyDir.multiply( 1.8 ))
|
.subtract( enemyDir.multiply( 1.8 ) )
|
||||||
.add( 0.0, 0.1, 0.0 )
|
.add( 0.0, 0.1, 0.0 )
|
||||||
|
|
||||||
if ( !dest.block.type.isAir ) dest = dest.add( 0.0, 1.0, 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" ))
|
player.sendActionBar(player.trans( "kits.ninja.messages.teleported" ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// DEFENSIVE active – Smoke Aura
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
|
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
|
||||||
|
|
||||||
private val plugin get() = SpeedHG.instance
|
private val plugin get() = SpeedHG.instance
|
||||||
@@ -208,51 +300,91 @@ class NinjaKit : Kit() {
|
|||||||
player: Player
|
player: Player
|
||||||
): AbilityResult
|
): AbilityResult
|
||||||
{
|
{
|
||||||
|
// Cancel any existing smoke aura before starting a new one
|
||||||
smokeTasks.remove( player.uniqueId )?.cancel()
|
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, { ->
|
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()
|
smokeTasks.remove( player.uniqueId )?.cancel()
|
||||||
return@runTaskTimer
|
return@runTaskTimer
|
||||||
}
|
}
|
||||||
|
|
||||||
val center = player.location
|
spawnSmokeRing( player, capturedRadius )
|
||||||
|
applyEffectsToEnemies( player, capturedRadius, capturedEffectTicks )
|
||||||
|
|
||||||
for ( i in 0 until 10 )
|
}, 0L, capturedRefreshTicks )
|
||||||
{
|
|
||||||
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 )
|
|
||||||
|
|
||||||
smokeTasks[ player.uniqueId ] = task
|
smokeTasks[ player.uniqueId ] = task
|
||||||
|
|
||||||
|
// Schedule automatic aura expiry
|
||||||
|
val capturedDurationTicks = smokeDurationTicks
|
||||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||||
smokeTasks.remove( player.uniqueId )?.cancel()
|
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
|
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 ) {
|
private class NoActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) {
|
||||||
override val kitId = "ninja"
|
override val kitId = "ninja"
|
||||||
|
|||||||
@@ -30,27 +30,65 @@ import kotlin.math.sin
|
|||||||
* | Playstyle | Active | Passive |
|
* | Playstyle | Active | Passive |
|
||||||
* |-------------|-------------------------------------------------|------------------------------------------|
|
* |-------------|-------------------------------------------------|------------------------------------------|
|
||||||
* | AGGRESSIVE | Shockwave + 3× Blink in looking direction | – |
|
* | 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
|
* ### AGGRESSIVE active
|
||||||
* First use (off cooldown): radial shockwave → grants 3 blink charges.
|
* Erster Use (off cooldown): radiale Schockwelle → gewährt [maxTeleportCharges] Blink-Charges.
|
||||||
* Each subsequent right-click: teleport up to [TELEPORT_RANGE] blocks in the
|
* Jeder folgende Rechtsklick: teleportiert bis zu [teleportRange] Blöcke in Blickrichtung
|
||||||
* player's looking direction (stops before solid blocks). After all 3 charges
|
* (stoppt vor soliden Blöcken). Nach allen Charges beginnt der [abilityCooldownMs]-Cooldown.
|
||||||
* are spent, the 20 s cooldown begins.
|
|
||||||
*
|
*
|
||||||
* ### DEFENSIVE active
|
* ### DEFENSIVE active
|
||||||
* Radial shockwave + [applyFreeze] on every nearby alive enemy. Each frozen
|
* Radiale Schockwelle + [applyFreeze] auf jeden nahen lebenden Gegner. Ein 1-Tick-Task
|
||||||
* enemy gets a 1-tick velocity-zeroing task for 10 s. The [DefensivePassive]
|
* setzt die Velocity gefrorener Spieler für [freezeDurationTicks] Ticks auf 0.
|
||||||
* monitors hits from this player on frozen enemies and unfreezes them after
|
* [DefensivePassive] beendet den Freeze nach [maxHitsOnFrozen] Treffern.
|
||||||
* [MAX_HITS_ON_FROZEN] hits or when time expires.
|
|
||||||
*
|
*
|
||||||
* ### Why hitsRequired = 0?
|
* ### Warum hitsRequired = 0?
|
||||||
* Both active abilities require full control over when [execute] fires. Using
|
* Beide Aktiv-Fähigkeiten steuern intern wann [execute] feuert. Das eingebaute
|
||||||
* the built-in charge system (hitsRequired > 0) would block [execute] after
|
* Charge-System würde nach dem ersten Use blockieren und die Blink/Freeze-Logik
|
||||||
* the first use and prevent the blink/freeze logic from re-running per click.
|
* pro Klick verhindern. Mit `hitsRequired = 0` bleibt der Charge-State dauerhaft
|
||||||
* With hitsRequired = 0 the charge state stays READY permanently and
|
* READY und der interne Cooldown regiert den tatsächlichen Recharge.
|
||||||
* [execute] is called on every right-click — internal cooldown maps govern
|
|
||||||
* actual recharge.
|
|
||||||
*/
|
*/
|
||||||
class TheWorldKit : Kit() {
|
class TheWorldKit : Kit() {
|
||||||
|
|
||||||
@@ -58,23 +96,133 @@ class TheWorldKit : Kit() {
|
|||||||
|
|
||||||
override val id = "theworld"
|
override val id = "theworld"
|
||||||
override val displayName: Component
|
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>
|
override val lore: List<String>
|
||||||
get() = plugin.languageManager.getDefaultRawMessageList("kits.theworld.lore")
|
get() = plugin.languageManager.getDefaultRawMessageList( "kits.theworld.lore" )
|
||||||
override val icon = Material.CLOCK
|
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.
|
* Cooldown beider Aktiv-Fähigkeiten in Millisekunden.
|
||||||
* Set to [MAX_TELEPORT_CHARGES] on first right-click, decremented per blink.
|
* 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()
|
internal val teleportCharges: MutableMap<UUID, Int> = ConcurrentHashMap()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Active freezes: victimUUID → (attackerUUID, [FrozenData]).
|
* Aktive Freezes: victimUUID → (attackerUUID, [FrozenData]).
|
||||||
* Tracked separately per attacker so [onRemove] only thaws enemies
|
* Getrennt pro Angreifer, damit [onRemove] nur vom verlassenden Spieler verursachte
|
||||||
* frozen by the leaving player.
|
* Freezes auftaut.
|
||||||
*/
|
*/
|
||||||
internal val frozenEnemies: MutableMap<UUID, Pair<UUID, FrozenData>> = ConcurrentHashMap()
|
internal val frozenEnemies: MutableMap<UUID, Pair<UUID, FrozenData>> = ConcurrentHashMap()
|
||||||
|
|
||||||
@@ -83,63 +231,64 @@ class TheWorldKit : Kit() {
|
|||||||
val task: BukkitTask
|
val task: BukkitTask
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
// =========================================================================
|
||||||
private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["theworld"]
|
// Cached ability instances
|
||||||
?: CustomGameSettings.KitOverride()
|
// =========================================================================
|
||||||
|
|
||||||
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 aggressiveActive = AggressiveActive()
|
||||||
private val defensiveActive = DefensiveActive()
|
private val defensiveActive = DefensiveActive()
|
||||||
private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE)
|
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
|
||||||
private val defensivePassive = DefensivePassive()
|
private val defensivePassive = DefensivePassive()
|
||||||
|
|
||||||
override fun getActiveAbility(playstyle: Playstyle): ActiveAbility = when (playstyle) {
|
override fun getActiveAbility(
|
||||||
|
playstyle: Playstyle
|
||||||
|
): ActiveAbility = when( playstyle )
|
||||||
|
{
|
||||||
Playstyle.AGGRESSIVE -> aggressiveActive
|
Playstyle.AGGRESSIVE -> aggressiveActive
|
||||||
Playstyle.DEFENSIVE -> defensiveActive
|
Playstyle.DEFENSIVE -> defensiveActive
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPassiveAbility(playstyle: Playstyle): PassiveAbility = when (playstyle) {
|
override fun getPassiveAbility(
|
||||||
|
playstyle: Playstyle
|
||||||
|
): PassiveAbility = when( playstyle )
|
||||||
|
{
|
||||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||||
Playstyle.DEFENSIVE -> defensivePassive
|
Playstyle.DEFENSIVE -> defensivePassive
|
||||||
}
|
}
|
||||||
|
|
||||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||||
|
|
||||||
override fun giveItems(player: Player, playstyle: Playstyle) {
|
override fun giveItems(
|
||||||
val active = getActiveAbility(playstyle)
|
player: Player,
|
||||||
|
playstyle: Playstyle
|
||||||
|
) {
|
||||||
|
val active = getActiveAbility( playstyle )
|
||||||
|
|
||||||
val item = ItemBuilder(Material.CLOCK)
|
val item = ItemBuilder( Material.CLOCK )
|
||||||
.name(active.name)
|
.name( active.name )
|
||||||
.lore(listOf(active.description))
|
.lore(listOf( active.description ))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
cachedItems[player.uniqueId] = listOf(item)
|
cachedItems[ player.uniqueId ] = listOf( item )
|
||||||
player.inventory.addItem(item)
|
player.inventory.addItem( item )
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRemove(player: Player) {
|
override fun onRemove(
|
||||||
teleportCharges.remove(player.uniqueId)
|
player: Player
|
||||||
|
) {
|
||||||
|
teleportCharges.remove( player.uniqueId )
|
||||||
|
|
||||||
// Cancel tasks + thaw every enemy that was frozen by this player
|
// Alle vom verlassenden Spieler verursachten Freezes auftauen
|
||||||
val toUnfreeze = frozenEnemies.entries
|
frozenEnemies.entries
|
||||||
.filter { (_, pair) -> pair.first == player.uniqueId }
|
.filter { ( _, pair ) -> pair.first == player.uniqueId }
|
||||||
.map { (victimUUID, pair) -> victimUUID to pair.second }
|
.map { ( victimUUID, pair ) -> victimUUID to pair.second }
|
||||||
|
.forEach { ( victimUUID, data ) ->
|
||||||
toUnfreeze.forEach { (victimUUID, data) ->
|
|
||||||
data.task.cancel()
|
data.task.cancel()
|
||||||
frozenEnemies.remove(victimUUID)
|
frozenEnemies.remove( victimUUID )
|
||||||
Bukkit.getPlayer(victimUUID)?.clearFreezeEffects()
|
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.
|
* Der Ring-Task wächst um 1 Block/Tick bis [shockwaveRadius] + 1.
|
||||||
* This gives the visual impression of a shockwave spreading outward.
|
* 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
|
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() {
|
object : BukkitRunnable() {
|
||||||
var r = 0.5
|
var r = 0.5
|
||||||
override fun run() {
|
override fun run()
|
||||||
if (r > SHOCKWAVE_RADIUS + 1.0) { cancel(); return }
|
{
|
||||||
val steps = (2 * Math.PI * r * 5).toInt().coerceAtLeast(8)
|
if ( r > capturedRadius + 1.0 ) { cancel(); return }
|
||||||
for (i in 0 until steps) {
|
val steps = ( 2 * Math.PI * r * 5 ).toInt().coerceAtLeast( 8 )
|
||||||
|
for ( i in 0 until steps )
|
||||||
|
{
|
||||||
val angle = 2 * Math.PI * i / steps
|
val angle = 2 * Math.PI * i / steps
|
||||||
val loc = origin.location.clone().add(cos(angle) * r, 1.0, sin(angle) * r)
|
val loc = origin.location.clone().add(
|
||||||
world.spawnParticle(Particle.SWEEP_ATTACK, loc, 1, 0.0, 0.0, 0.0, 0.0)
|
cos( angle ) * r, 1.0, sin( angle ) * r
|
||||||
world.spawnParticle(Particle.CRIT, loc, 2, 0.1, 0.1, 0.1, 0.0)
|
)
|
||||||
|
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
|
r += 1.0
|
||||||
}
|
}
|
||||||
}.runTaskTimer(plugin, 0L, 1L)
|
}.runTaskTimer( plugin, 0L, 1L )
|
||||||
|
|
||||||
// ── Physics: knock all nearby alive enemies outward ───────────────────
|
// ── Physik: alle nahen Gegner nach außen schleudern ───────────────────
|
||||||
world.getNearbyEntities(origin.location, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS)
|
world.getNearbyEntities(
|
||||||
|
origin.location,
|
||||||
|
capturedRadius, capturedRadius, capturedRadius
|
||||||
|
)
|
||||||
.filterIsInstance<Player>()
|
.filterIsInstance<Player>()
|
||||||
.filter { it != origin && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
.filter { it != origin &&
|
||||||
|
plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||||
.forEach { enemy ->
|
.forEach { enemy ->
|
||||||
val dir = enemy.location.toVector()
|
val dir = enemy.location.toVector()
|
||||||
.subtract(origin.location.toVector())
|
.subtract( origin.location.toVector() )
|
||||||
.normalize()
|
.normalize()
|
||||||
.multiply(2.0)
|
.multiply( capturedKnockbackSpeed )
|
||||||
.setY(0.45)
|
.setY( capturedKnockbackY )
|
||||||
enemy.velocity = dir
|
enemy.velocity = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
world.playSound(origin.location, Sound.ENTITY_WARDEN_SONIC_BOOM, 1f, 0.8f)
|
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.BLOCK_BEACON_ACTIVATE, 0.6f, 1.5f )
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Teleports [player] up to [TELEPORT_RANGE] blocks in their looking direction.
|
* Teleportiert [player] bis zu [teleportRange] Blöcke in Blickrichtung.
|
||||||
* Raycast in 0.4-block increments — stops at the last non-solid block.
|
* 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()
|
val dir = player.location.direction.normalize()
|
||||||
var target = player.eyeLocation.clone()
|
var target = player.eyeLocation.clone()
|
||||||
|
|
||||||
repeat((TELEPORT_RANGE / 0.4).toInt()) {
|
repeat( ( capturedRange / capturedStepSize ).toInt() ) {
|
||||||
val next = target.clone().add(dir.clone().multiply(0.4))
|
val next = target.clone().add( dir.clone().multiply( capturedStepSize ) )
|
||||||
if (next.block.type.isSolid) return@repeat
|
if ( next.block.type.isSolid ) return@repeat
|
||||||
target = next
|
target = next
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust to feet position
|
// Auf Füße-Position anpassen
|
||||||
target.y -= 1.0
|
target.y -= 1.0
|
||||||
target.yaw = player.location.yaw
|
target.yaw = player.location.yaw
|
||||||
target.pitch = player.location.pitch
|
target.pitch = player.location.pitch
|
||||||
|
|
||||||
player.teleport(target)
|
player.teleport( target )
|
||||||
player.world.spawnParticle(Particle.PORTAL, target, 30, 0.2, 0.5, 0.2, 0.05)
|
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.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.playSound( target, Sound.ENTITY_ENDERMAN_TELEPORT, 0.9f, 1.4f )
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immobilises [target], capping hits from [attacker] at [MAX_HITS_ON_FROZEN].
|
* Immobilisiert [target] und begrenzt Treffer von [attacker] auf [maxHitsOnFrozen].
|
||||||
* A 1-tick repeating task zeros horizontal + upward velocity for 10 seconds.
|
* 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) {
|
private fun applyFreeze(
|
||||||
// Overwrite any existing freeze
|
attacker: Player,
|
||||||
frozenEnemies.remove(target.uniqueId)?.second?.task?.cancel()
|
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()
|
target.applyFreezeEffects()
|
||||||
|
|
||||||
val task = object : BukkitRunnable() {
|
val task = object : BukkitRunnable() {
|
||||||
var ticks = 0
|
var ticks = 0
|
||||||
override fun run() {
|
override fun run()
|
||||||
|
{
|
||||||
ticks++
|
ticks++
|
||||||
|
|
||||||
if (ticks >= FREEZE_DURATION_TICKS || !target.isOnline ||
|
if ( ticks >= capturedDurationTicks ||
|
||||||
!plugin.gameManager.alivePlayers.contains(target.uniqueId) ||
|
!target.isOnline ||
|
||||||
!frozenEnemies.containsKey(target.uniqueId) ||
|
!plugin.gameManager.alivePlayers.contains( target.uniqueId ) ||
|
||||||
plugin.gameManager.currentState == GameState.ENDING) // ← neu
|
!frozenEnemies.containsKey( target.uniqueId ) ||
|
||||||
|
plugin.gameManager.currentState == GameState.ENDING )
|
||||||
{
|
{
|
||||||
doUnfreeze(target)
|
doUnfreeze( target )
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zero horizontal + upward velocity every tick
|
// Horizontale + aufwärts Velocity jedes Tick nullen
|
||||||
val v = target.velocity
|
val v = target.velocity
|
||||||
target.velocity = v.setX(0.0).setZ(0.0)
|
target.velocity = v.setX( 0.0 ).setZ( 0.0 )
|
||||||
.let { if (it.y > 0.0) it.setY(0.0) else it }
|
.let { if ( it.y > 0.0 ) it.setY( 0.0 ) else it }
|
||||||
|
|
||||||
// Refresh slowness every second so it doesn't expire mid-freeze
|
// Slowness jede Sekunde refreshen damit sie nicht ausläuft
|
||||||
if (ticks % 20 == 0) target.applyFreezeEffects()
|
if ( ticks % capturedRefreshTicks == 0 )
|
||||||
|
target.applyFreezeEffects()
|
||||||
|
|
||||||
// Powder-snow visual (cosmetic)
|
// Powder-Snow-Visuals (rein kosmetisch)
|
||||||
if (target.freezeTicks < 140) target.freezeTicks = 140
|
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,
|
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.sendActionBar( target.trans( "kits.theworld.messages.frozen_received" ) )
|
||||||
target.world.spawnParticle(Particle.SNOWFLAKE,
|
target.world.spawnParticle(
|
||||||
target.location.clone().add(0.0, 1.0, 0.0), 15, 0.3, 0.5, 0.3, 0.05)
|
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) {
|
private fun doUnfreeze(
|
||||||
frozenEnemies.remove(target.uniqueId)
|
target: Player
|
||||||
|
) {
|
||||||
|
frozenEnemies.remove( target.uniqueId )
|
||||||
target.clearFreezeEffects()
|
target.clearFreezeEffects()
|
||||||
if (target.isOnline)
|
if ( target.isOnline )
|
||||||
target.sendActionBar(target.trans("kits.theworld.messages.frozen_expired"))
|
target.sendActionBar( target.trans( "kits.theworld.messages.frozen_expired" ) )
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Player extension helpers ──────────────────────────────────────────────
|
// ── Player-Extension-Helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
private fun Player.applyFreezeEffects() {
|
private fun Player.applyFreezeEffects()
|
||||||
addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS,
|
{
|
||||||
/* duration */ 25,
|
addPotionEffect(PotionEffect(
|
||||||
/* amplifier */ 127,
|
PotionEffectType.SLOWNESS,
|
||||||
/* ambient */ false,
|
25, 127,
|
||||||
/* particles */ false,
|
false, false, true
|
||||||
/* icon */ true))
|
))
|
||||||
addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE,
|
addPotionEffect(PotionEffect(
|
||||||
25, 127, false, false, false))
|
PotionEffectType.MINING_FATIGUE,
|
||||||
|
25, 127,
|
||||||
|
false, false, false
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Player.clearFreezeEffects() {
|
private fun Player.clearFreezeEffects()
|
||||||
removePotionEffect(PotionEffectType.SLOWNESS)
|
{
|
||||||
removePotionEffect(PotionEffectType.MINING_FATIGUE)
|
removePotionEffect( PotionEffectType.SLOWNESS )
|
||||||
|
removePotionEffect( PotionEffectType.MINING_FATIGUE )
|
||||||
freezeTicks = 0
|
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
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
|
/** Cooldowns pro Spieler: UUID → letzter Aktivierungs-Timestamp (ms). */
|
||||||
private val cooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
private val cooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||||
|
|
||||||
override val kitId: String
|
override val kitId: String
|
||||||
get() = "theworld"
|
get() = "theworld"
|
||||||
|
|
||||||
override val name: String
|
override val name: String
|
||||||
get() = plugin.languageManager.getDefaultRawMessage(
|
get() = plugin.languageManager.getDefaultRawMessage(
|
||||||
"kits.theworld.items.clock.aggressive.name")
|
"kits.theworld.items.clock.aggressive.name"
|
||||||
|
)
|
||||||
override val description: String
|
override val description: String
|
||||||
get() = plugin.languageManager.getDefaultRawMessage(
|
get() = plugin.languageManager.getDefaultRawMessage(
|
||||||
"kits.theworld.items.clock.aggressive.description")
|
"kits.theworld.items.clock.aggressive.description"
|
||||||
|
)
|
||||||
override val hardcodedHitsRequired: Int
|
override val hardcodedHitsRequired: Int
|
||||||
get() = 0
|
get() = 0
|
||||||
override val triggerMaterial = Material.CLOCK
|
override val triggerMaterial = Material.CLOCK
|
||||||
|
|
||||||
override fun execute(player: Player): AbilityResult {
|
override fun execute(
|
||||||
|
player: Player
|
||||||
// ── Spend a blink charge if any are available ─────────────────────
|
): AbilityResult
|
||||||
val charges = teleportCharges[player.uniqueId] ?: 0
|
{
|
||||||
if (charges > 0) {
|
// ── Vorhandene Blink-Charge verbrauchen ───────────────────────────
|
||||||
blink(player)
|
val charges = teleportCharges[ player.uniqueId ] ?: 0
|
||||||
|
if ( charges > 0 )
|
||||||
|
{
|
||||||
|
blink( player )
|
||||||
val remaining = charges - 1
|
val remaining = charges - 1
|
||||||
teleportCharges[player.uniqueId] = remaining
|
teleportCharges[ player.uniqueId ] = remaining
|
||||||
|
|
||||||
if (remaining > 0)
|
if ( remaining > 0 )
|
||||||
player.sendActionBar(player.trans(
|
player.sendActionBar(player.trans(
|
||||||
"kits.theworld.messages.teleport_charges",
|
"kits.theworld.messages.teleport_charges",
|
||||||
mapOf("charges" to remaining.toString())))
|
mapOf( "charges" to remaining.toString() )
|
||||||
else {
|
))
|
||||||
teleportCharges.remove(player.uniqueId)
|
else
|
||||||
player.sendActionBar(player.trans("kits.theworld.messages.charges_exhausted"))
|
{
|
||||||
|
teleportCharges.remove( player.uniqueId )
|
||||||
|
player.sendActionBar(player.trans( "kits.theworld.messages.charges_exhausted" ))
|
||||||
}
|
}
|
||||||
return AbilityResult.Success
|
return AbilityResult.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cooldown gate ─────────────────────────────────────────────────
|
// ── Cooldown prüfen ───────────────────────────────────────────────
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val lastUse = cooldowns[player.uniqueId] ?: 0L
|
val lastUse = cooldowns[ player.uniqueId ] ?: 0L
|
||||||
if (now - lastUse < ABILITY_COOLDOWN_MS) {
|
val capturedCooldown = abilityCooldownMs
|
||||||
val secsLeft = (ABILITY_COOLDOWN_MS - (now - lastUse)) / 1000
|
|
||||||
return AbilityResult.ConditionNotMet("Cooldown: ${secsLeft}s")
|
if ( now - lastUse < capturedCooldown )
|
||||||
|
{
|
||||||
|
val secsLeft = ( capturedCooldown - ( now - lastUse ) ) / 1000
|
||||||
|
return AbilityResult.ConditionNotMet( "Cooldown: ${secsLeft}s" )
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Shockwave + grant 3 blink charges ────────────────────────────
|
// ── Schockwelle + Charges vergeben ────────────────────────────────
|
||||||
doShockwave(player)
|
val capturedCharges = maxTeleportCharges
|
||||||
teleportCharges[player.uniqueId] = MAX_TELEPORT_CHARGES
|
|
||||||
cooldowns[player.uniqueId] = now
|
doShockwave( player )
|
||||||
|
teleportCharges[ player.uniqueId ] = capturedCharges
|
||||||
|
cooldowns[ player.uniqueId ] = now
|
||||||
|
|
||||||
player.sendActionBar(player.trans(
|
player.sendActionBar(player.trans(
|
||||||
"kits.theworld.messages.shockwave_and_blink",
|
"kits.theworld.messages.shockwave_and_blink",
|
||||||
mapOf("charges" to MAX_TELEPORT_CHARGES.toString())))
|
mapOf( "charges" to capturedCharges.toString() )
|
||||||
|
))
|
||||||
|
|
||||||
return AbilityResult.Success
|
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
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
|
/** Cooldowns pro Spieler: UUID → letzter Aktivierungs-Timestamp (ms). */
|
||||||
private val cooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
private val cooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||||
|
|
||||||
override val kitId: String
|
override val kitId: String
|
||||||
get() = "theworld"
|
get() = "theworld"
|
||||||
|
|
||||||
override val name: String
|
override val name: String
|
||||||
get() = plugin.languageManager.getDefaultRawMessage(
|
get() = plugin.languageManager.getDefaultRawMessage(
|
||||||
"kits.theworld.items.clock.defensive.name")
|
"kits.theworld.items.clock.defensive.name"
|
||||||
|
)
|
||||||
override val description: String
|
override val description: String
|
||||||
get() = plugin.languageManager.getDefaultRawMessage(
|
get() = plugin.languageManager.getDefaultRawMessage(
|
||||||
"kits.theworld.items.clock.defensive.description")
|
"kits.theworld.items.clock.defensive.description"
|
||||||
|
)
|
||||||
override val hardcodedHitsRequired: Int
|
override val hardcodedHitsRequired: Int
|
||||||
get() = 0
|
get() = 0
|
||||||
override val triggerMaterial = Material.CLOCK
|
override val triggerMaterial = Material.CLOCK
|
||||||
|
|
||||||
override fun execute(player: Player): AbilityResult {
|
override fun execute(
|
||||||
|
player: Player
|
||||||
// ── Cooldown gate ─────────────────────────────────────────────────
|
): AbilityResult
|
||||||
|
{
|
||||||
|
// ── Cooldown prüfen ───────────────────────────────────────────────
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val lastUse = cooldowns[player.uniqueId] ?: 0L
|
val lastUse = cooldowns[ player.uniqueId ] ?: 0L
|
||||||
if (now - lastUse < ABILITY_COOLDOWN_MS) {
|
val capturedCooldown = abilityCooldownMs
|
||||||
val secsLeft = (ABILITY_COOLDOWN_MS - (now - lastUse)) / 1000
|
|
||||||
return AbilityResult.ConditionNotMet("Cooldown: ${secsLeft}s")
|
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
|
val targets = player.world
|
||||||
.getNearbyEntities(
|
.getNearbyEntities(
|
||||||
player.location,
|
player.location,
|
||||||
SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS)
|
capturedRadius, capturedRadius, capturedRadius
|
||||||
|
)
|
||||||
.filterIsInstance<Player>()
|
.filterIsInstance<Player>()
|
||||||
.filter { it != player &&
|
.filter { it != player &&
|
||||||
plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||||
|
|
||||||
if (targets.isEmpty())
|
if ( targets.isEmpty() )
|
||||||
return AbilityResult.ConditionNotMet("No enemies within range!")
|
return AbilityResult.ConditionNotMet( "No enemies within range!" )
|
||||||
|
|
||||||
doShockwave(player)
|
// ── Schockwelle + Freeze ──────────────────────────────────────────
|
||||||
targets.forEach { applyFreeze(player, it) }
|
doShockwave( player )
|
||||||
cooldowns[player.uniqueId] = now
|
targets.forEach { applyFreeze( player, it ) }
|
||||||
|
cooldowns[ player.uniqueId ] = now
|
||||||
|
|
||||||
player.sendActionBar(player.trans(
|
player.sendActionBar(player.trans(
|
||||||
"kits.theworld.messages.freeze_activated",
|
"kits.theworld.messages.freeze_activated",
|
||||||
mapOf("count" to targets.size.toString())))
|
mapOf( "count" to targets.size.toString() )
|
||||||
|
))
|
||||||
|
|
||||||
return AbilityResult.Success
|
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
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
override val name: String
|
override val name: String
|
||||||
get() = plugin.languageManager.getDefaultRawMessage(
|
get() = plugin.languageManager.getDefaultRawMessage(
|
||||||
"kits.theworld.passive.defensive.name")
|
"kits.theworld.passive.defensive.name"
|
||||||
|
)
|
||||||
override val description: String
|
override val description: String
|
||||||
get() = plugin.languageManager.getDefaultRawMessage(
|
get() = plugin.languageManager.getDefaultRawMessage(
|
||||||
"kits.theworld.passive.defensive.description")
|
"kits.theworld.passive.defensive.description"
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called only when the TheWorld player (attacker) hits someone.
|
* Wird nur aufgerufen wenn der TheWorld-Spieler (Angreifer) jemanden trifft.
|
||||||
* If that someone is frozen and was frozen by this attacker,
|
* Wenn das Opfer vom gleichen Angreifer eingefroren wurde, Treffer-Cap dekrementieren.
|
||||||
* decrement their remaining hit allowance.
|
|
||||||
*/
|
*/
|
||||||
override fun onHitEnemy(
|
override fun onHitEnemy(
|
||||||
attacker: Player,
|
attacker: Player,
|
||||||
victim: Player,
|
victim: Player,
|
||||||
event: EntityDamageByEntityEvent
|
event: EntityDamageByEntityEvent
|
||||||
) {
|
) {
|
||||||
val (frozenBy, data) = frozenEnemies[victim.uniqueId] ?: return
|
val ( frozenBy, data ) = frozenEnemies[ victim.uniqueId ] ?: return
|
||||||
// Only count hits from the player who applied this specific freeze
|
|
||||||
if (frozenBy != attacker.uniqueId) return
|
// Nur Treffer vom Spieler zählen, der den Freeze ausgelöst hat
|
||||||
|
if ( frozenBy != attacker.uniqueId ) return
|
||||||
|
|
||||||
data.hitsRemaining--
|
data.hitsRemaining--
|
||||||
|
|
||||||
if (data.hitsRemaining <= 0) {
|
if ( data.hitsRemaining <= 0 )
|
||||||
doUnfreeze(victim)
|
{
|
||||||
attacker.sendActionBar(attacker.trans("kits.theworld.messages.freeze_broken"))
|
doUnfreeze( victim )
|
||||||
} else {
|
attacker.sendActionBar( attacker.trans( "kits.theworld.messages.freeze_broken" ) )
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
attacker.sendActionBar(attacker.trans(
|
attacker.sendActionBar(attacker.trans(
|
||||||
"kits.theworld.messages.freeze_hits_left",
|
"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
|
// AGGRESSIVE no-passive
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
private class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) {
|
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) {
|
||||||
override val name = "None"
|
override val name = "None"
|
||||||
override val description = "None"
|
override val description = "None"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user