diff --git a/build.gradle.kts b/build.gradle.kts index ab0260e..cb890ec 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,9 @@ dependencies { implementation("com.zaxxer:HikariCP:5.1.0") implementation("com.mysql:mysql-connector-j:8.4.0") + implementation(libs.kotlinxCoroutines) + implementation(libs.kotlinxSerialization) compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") compileOnly("com.sk89q.worldedit:worldedit-core:7.2.17-SNAPSHOT") diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index 7c49a2e..f7fc4c5 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -3,6 +3,8 @@ package club.mcscrims.speedhg import club.mcscrims.speedhg.command.KitCommand import club.mcscrims.speedhg.command.LeaderboardCommand import club.mcscrims.speedhg.command.TimerCommand +import club.mcscrims.speedhg.config.CustomGameManager +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.config.LanguageManager import club.mcscrims.speedhg.database.DatabaseManager import club.mcscrims.speedhg.database.StatsManager @@ -59,10 +61,16 @@ class SpeedHG : JavaPlugin() { lateinit var discordWebhookManager: DiscordWebhookManager private set + lateinit var customGameManager: CustomGameManager + private set + override fun onLoad() { instance = this + customGameManager = CustomGameManager( this ) + customGameManager.load() + saveDefaultConfig() reloadConfig() @@ -117,6 +125,7 @@ class SpeedHG : JavaPlugin() { kitManager.registerKit( GoblinKit() ) kitManager.registerKit( IceMageKit() ) kitManager.registerKit( RattlesnakeKit() ) + kitManager.registerKit( TheWorldKit() ) kitManager.registerKit( VenomKit() ) kitManager.registerKit( VoodooKit() ) } diff --git a/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameManager.kt b/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameManager.kt new file mode 100644 index 0000000..3cd216b --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameManager.kt @@ -0,0 +1,34 @@ +package club.mcscrims.speedhg.config + +import club.mcscrims.speedhg.SpeedHG +import kotlinx.serialization.json.Json + +class CustomGameManager( + private val plugin: SpeedHG +) { + + private val json = Json { + ignoreUnknownKeys = true + isLenient = true + encodeDefaults = false + } + + var settings: CustomGameSettings = CustomGameSettings() + private set + + fun load() + { + val raw = System.getenv("SPEEDHG_CUSTOM_SETTINGS") + + settings = if ( raw.isNullOrBlank() ) { + plugin.logger.info("[CustomGameManager] Keine SPEEDHG_CUSTOM_SETTINGS gefunden - nutze Defaults.") + CustomGameSettings() + } else { + runCatching { json.decodeFromString( raw ) } + .onSuccess { plugin.logger.info( "[CustomGameManager] Settings geladen." ) } + .onFailure { plugin.logger.severe( "[CustomGameManager] Parse-Fehler: ${it.message}" ) } + .getOrDefault( CustomGameSettings() ) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameSettings.kt b/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameSettings.kt new file mode 100644 index 0000000..03ac5d3 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameSettings.kt @@ -0,0 +1,98 @@ +package club.mcscrims.speedhg.config + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CustomGameSettings( + val game: GameSettings = GameSettings(), + val kits: KitSettings = KitSettings() +) { + + @Serializable + data class GameSettings( + @SerialName("min_players") val minPlayers: Int = 2, + @SerialName("lobby_time") val lobbyTime: Int = 60, + @SerialName("invincibility_time") val invincibilityTime: Int = 60, + @SerialName("border_start") val borderStart: Double = 300.0, + @SerialName("border_end") val borderEnd: Double = 20.0, + @SerialName("border_shrink_time") val borderShrinkTime: Long = 600L + ) + + @Serializable + data class KitSettings( + + /** Globaler Fallback – gilt für alle Kits, die keinen eigenen Wert setzen. */ + @SerialName("global_hits_required") + val globalHitsRequired: Int = 15, + + /** + * Kit-spezifische Overrides. + * Key = Kit.id (z. B. "gladiator", "venom"). + * Unbekannte Keys werden von kotlinx.serialization ignoriert. + */ + val kits: Map = emptyMap() + + ) { + /** + * Gibt den hitsRequired-Wert für ein Kit zurück. + * Priorität: kit-spezifisch > global > hardcoded Default + */ + fun hitsRequired(kitId: String, hardcodedDefault: Int): Int = + kits[kitId]?.hitsRequired ?: globalHitsRequired.takeIf { it >= 0 } ?: hardcodedDefault + } + + // ----------------------------------------------------------------- + // 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. + */ + @Serializable + data class KitOverride( + @SerialName("hits_required") val hitsRequired: Int = -1, + + // Goblin + @SerialName("steal_duration_seconds") val stealDuration: Int = 60, + @SerialName("bunker_radius") val bunkerRadius: Double = 10.0, + + // Gladiator + @SerialName("arena_radius") val arenaRadius: Int = 23, + @SerialName("arena_height") val arenaHeight: Int = 10, + @SerialName("wither_after_seconds") val witherAfterSeconds: Int = 180, + + // Venom + @SerialName("shield_duration_ticks") val shieldDurationTicks: Long = 160L, + @SerialName("shield_capacity") val shieldCapacity: Double = 15.0, + + // Voodoo + @SerialName("curse_duration_ms") val curseDurationMs: Long = 15_000L, + + // 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, + @SerialName("pounce_min_fall") val pounceMinFall: Float = 3.0f, + @SerialName("pounce_radius") val pounceRadius: Double = 3.0, + @SerialName("pounce_damage") val pounceDamage: Double = 6.0, + + // 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 = 20_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 + ) +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt index b19f4d9..69a9649 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt @@ -99,7 +99,11 @@ class KitManager( val playstyle = getSelectedPlaystyle( player ) val active = kit.getActiveAbility( playstyle ) - chargeData[player.uniqueId] = PlayerChargeData( active.hitsRequired ) + // Settings einmalig in die Ability cachen + active.cacheHitsRequired( plugin.customGameManager.settings ) + + val chargeData = PlayerChargeData( active.hitsRequired ) + this.chargeData[ player.uniqueId ] = chargeData kit.onAssign( player, playstyle ) kit.getPassiveAbility( playstyle ).onActivate( player ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt index b80d993..52f0b0e 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt @@ -1,5 +1,6 @@ package club.mcscrims.speedhg.kit.ability +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Playstyle import org.bukkit.Material import org.bukkit.entity.Player @@ -24,18 +25,43 @@ abstract class ActiveAbility( abstract val name: String abstract val description: String - /** - * Melee hits required to recharge after one use. - * Set to 0 for an unlimited / always-ready ability (e.g. in debug kits). - */ - abstract val hitsRequired: Int - /** * The item material that triggers this ability on right-click. * The item must be in the player's main hand. */ abstract val triggerMaterial: Material + /** + * Der hardcodierte Default dieses spezifischen Kits/Playstyles. + * Wird nur als letzter Fallback genutzt. + */ + protected abstract val hardcodedHitsRequired: Int + + /** + * Die Kit-ID, unter der Settings nachgeschlagen werden. + * Muss von der äußeren Kit-Klasse gesetzt werden. + */ + abstract val kitId: String + + /** + * Gecachter Wert – wird einmalig in [cacheHitsRequired] berechnet + * und dann O(1) gelesen. Initialisiert mit dem Hardcoded-Default + * als Safety-Net falls cacheHitsRequired() nie aufgerufen wird. + */ + private var _hitsRequired: Int = -1 + + val hitsRequired: Int + get() = _hitsRequired.takeIf { it >= 0 } ?: hardcodedHitsRequired + + /** + * Einmalig beim applyKit() aufgerufen – danach ist der Wert gecacht. + */ + fun cacheHitsRequired( + settings: CustomGameSettings + ) { + _hitsRequired = settings.kits.hitsRequired( kitId, hardcodedHitsRequired ) + } + /** * Execute the ability. Called only when [PlayerChargeData.isReady] is true. * The dispatcher has already called [PlayerChargeData.consume] before this runs. diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt index 468f2f4..fb0aff0 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/ArmorerKit.kt @@ -187,10 +187,11 @@ class ArmorerKit : Kit() { // ========================================================================= inner class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) { + override val kitId: String = "armorer" override val name = "None" override val description = "None" - override val hitsRequired = 0 override val triggerMaterial = Material.BARRIER + override val hardcodedHitsRequired: Int = 0 override fun execute(player: Player) = AbilityResult.Success } } \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BackupKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BackupKit.kt index dea4464..8322060 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BackupKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BackupKit.kt @@ -61,13 +61,16 @@ class BackupKit : Kit() { private class NoActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) { + override val kitId: String + get() = "backup" + override val name: String get() = "None" override val description: String get() = "None" - override val hitsRequired: Int + override val hardcodedHitsRequired: Int get() = 0 override val triggerMaterial: Material diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt index b1ba811..6e4cf1d 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt @@ -1,6 +1,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -61,14 +62,18 @@ class BlackPantherKit : Kit() companion object { + private val kitOverride get() = + SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"] + ?: CustomGameSettings.KitOverride() + /** PDC key string shared with [KitEventDispatcher] for push-projectiles. */ const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile" - private const val FIST_MODE_MS = 12_000L // 12 seconds - private const val PUSH_RADIUS = 5.0 - private const val POUNCE_MIN_FALL = 3.0f - private const val POUNCE_RADIUS = 3.0 - private const val POUNCE_DAMAGE = 6.0 // 3 hearts = 6 HP + private val FIST_MODE_MS = kitOverride.fistModeDurationMs // 12 seconds + private val PUSH_RADIUS = kitOverride.pushRadius + private val POUNCE_MIN_FALL = kitOverride.pounceMinFall + private val POUNCE_RADIUS = kitOverride.pounceRadius + private val POUNCE_DAMAGE = kitOverride.pounceDamage // 3 hearts = 6 HP } // ── Cached ability instances ────────────────────────────────────────────── @@ -114,11 +119,13 @@ class BlackPantherKit : Kit() private val plugin get() = SpeedHG.instance + override val kitId: String get() = "blackpanther" + override val hardcodedHitsRequired: Int get() = 15 + override val name: String get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.name") override val description: String get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.description") - override val hitsRequired = 15 override val triggerMaterial = Material.BLACK_DYE override fun execute(player: Player): AbilityResult { @@ -184,9 +191,10 @@ class BlackPantherKit : Kit() // ========================================================================= private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { + override val kitId: String = "blackpanther" override val name = "None" override val description = "None" - override val hitsRequired = 0 + override val hardcodedHitsRequired: Int = 0 override val triggerMaterial = Material.BARRIER override fun execute(player: Player) = AbilityResult.Success } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt index 6270ea1..93025e6 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt @@ -1,6 +1,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.KitMetaData import club.mcscrims.speedhg.kit.Playstyle @@ -41,6 +42,10 @@ class GladiatorKit : Kit() { override val icon: Material get() = Material.IRON_BARS + private val kitOverride get() = + plugin.customGameManager.settings.kits.kits["gladiator"] + ?: CustomGameSettings.KitOverride() + // ── Cached ability instances (avoid allocating per event call) ──────────── private val aggressiveActive = AllActive( Playstyle.AGGRESSIVE ) private val defensiveActive = AllActive( Playstyle.DEFENSIVE ) @@ -95,13 +100,16 @@ class GladiatorKit : Kit() { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "gladiator" + override val name: String get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.name" ) override val description: String get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.description" ) - override val hitsRequired: Int + override val hardcodedHitsRequired: Int get() = 15 override val triggerMaterial: Material @@ -118,8 +126,8 @@ class GladiatorKit : Kit() { lineOfSight.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() )) return AbilityResult.ConditionNotMet( "Already in gladiator fight" ) - val radius = 23 - val height = 10 + val radius = kitOverride.arenaRadius + val height = kitOverride.arenaHeight player.setMetadata( KitMetaData.IN_GLADIATOR.getKey(), FixedMetadataValue( plugin, true )) lineOfSight.setMetadata( KitMetaData.IN_GLADIATOR.getKey(), FixedMetadataValue( plugin, true )) @@ -209,7 +217,7 @@ class GladiatorKit : Kit() { return true } - private class GladiatorFight( + private inner class GladiatorFight( val region: Region, val gladiator: Player, val enemy: Player, @@ -254,7 +262,7 @@ class GladiatorKit : Kit() { { timer++ - if ( timer > 180 ) + if ( timer > kitOverride.witherAfterSeconds ) { gladiator.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 )) enemy.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 )) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt index 50bacda..a9e102c 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt @@ -1,6 +1,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -35,6 +36,10 @@ class GoblinKit : Kit() { override val icon: Material get() = Material.MOSSY_COBBLESTONE + private val kitOverride get() = + plugin.customGameManager.settings.kits.kits["goblin"] + ?: CustomGameSettings.KitOverride() + // ── Cached ability instances (avoid allocating per event call) ──────────── private val aggressiveActive = AggressiveActive() private val defensiveActive = DefensiveActive() @@ -103,17 +108,20 @@ class GoblinKit : Kit() { items.forEach { player.inventory.remove( it ) } } - private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { + private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "goblin" + override val name: String get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.name" ) override val description: String get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.description" ) - override val hitsRequired: Int + override val hardcodedHitsRequired: Int get() = 15 override val triggerMaterial: Material @@ -151,7 +159,7 @@ class GoblinKit : Kit() { plugin.kitManager.selectPlaystyle( player, currentPlaystyle ) plugin.kitManager.applyKit( player ) } - }, 20L * 60) + }, 20L * kitOverride.stealDuration) activeStealTasks[ player.uniqueId ] = task @@ -176,17 +184,20 @@ class GoblinKit : Kit() { } - private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { + private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "goblin" + override val name: String get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.name" ) override val description: String get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.description" ) - override val hitsRequired: Int + override val hardcodedHitsRequired: Int get() = 15 override val triggerMaterial: Material @@ -202,7 +213,7 @@ class GoblinKit : Kit() { WorldEditUtils.createSphere( world, location, - 10.0, + kitOverride.bunkerRadius, false, Material.MOSSY_COBBLESTONE ) @@ -211,7 +222,7 @@ class GoblinKit : Kit() { WorldEditUtils.createSphere( world, location, - 10.0, + kitOverride.bunkerRadius, false, Material.AIR ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt index 44c30bb..14c1913 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt @@ -91,14 +91,17 @@ class IceMageKit : Kit() { private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { + override val kitId: String + get() = "icemage" + override val name: String get() = "None" override val description: String get() = "None" - override val hitsRequired: Int - get() = 0 + override val hardcodedHitsRequired: Int + get() = 15 override val triggerMaterial: Material get() = Material.BARRIER @@ -116,13 +119,16 @@ class IceMageKit : Kit() { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "icemage" + override val name: String get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.name" ) override val description: String get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.description" ) - override val hitsRequired: Int + override val hardcodedHitsRequired: Int get() = 15 override val triggerMaterial: Material diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt index 27bf097..dcfc248 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt @@ -1,6 +1,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -56,11 +57,15 @@ class RattlesnakeKit : Kit() { internal val lastPounceUse: MutableMap = ConcurrentHashMap() companion object { - private const val POUNCE_COOLDOWN_MS = 20_000L - private const val MAX_SNEAK_MS = 3_000L - private const val MIN_RANGE = 3.0 - private const val MAX_RANGE = 10.0 - private const val POUNCE_TIMEOUT_TICKS = 30L // 1.5 s + private val kitOverride get() = + SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"] + ?: CustomGameSettings.KitOverride() + + private val POUNCE_COOLDOWN_MS = kitOverride.pounceCooldownMs + private val MAX_SNEAK_MS = kitOverride.pounceMaxSneakMs + private val MIN_RANGE = kitOverride.pounceMinRange + private val MAX_RANGE = kitOverride.pounceMaxRange + private val POUNCE_TIMEOUT_TICKS = kitOverride.pounceTimeoutTicks } // ── Cached ability instances ────────────────────────────────────────────── @@ -117,11 +122,15 @@ class RattlesnakeKit : Kit() { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "rattlesnake" + override val name: String get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.name") override val description: String get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.description") - override val hitsRequired = 0 // charged via sneaking, not hits + override val hardcodedHitsRequired: Int + get() = 0 override val triggerMaterial = Material.SLIME_BALL override fun execute(player: Player): AbilityResult { @@ -235,9 +244,10 @@ class RattlesnakeKit : Kit() { // ========================================================================= private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { + override val kitId: String = "rattlesnake" override val name = "None" override val description = "None" - override val hitsRequired = 0 + override val hardcodedHitsRequired: Int = 0 override val triggerMaterial = Material.BARRIER override fun execute(player: Player) = AbilityResult.Success } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt deleted file mode 100644 index 387be46..0000000 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt +++ /dev/null @@ -1,231 +0,0 @@ -package club.mcscrims.speedhg.kit.impl - -import club.mcscrims.speedhg.kit.Kit -import club.mcscrims.speedhg.kit.Playstyle -import club.mcscrims.speedhg.kit.ability.AbilityResult -import club.mcscrims.speedhg.kit.ability.ActiveAbility -import club.mcscrims.speedhg.kit.ability.PassiveAbility -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import org.bukkit.Material -import org.bukkit.Sound -import org.bukkit.entity.Player -import org.bukkit.event.entity.EntityDamageByEntityEvent -import org.bukkit.inventory.ItemStack -import org.bukkit.potion.PotionEffect -import org.bukkit.potion.PotionEffectType -import java.util.* -import java.util.concurrent.ConcurrentHashMap - -/** - * ────────────────────────────────────────────────────────────────────────────── - * TEMPLATE KIT — reference implementation, do not use in production - * ────────────────────────────────────────────────────────────────────────────── - * - * Copy this file, rename the class, change the abilities, and register your kit: - * ```kotlin - * // In SpeedHG.onEnable(): - * kitManager.registerKit(YourKit()) - * ``` - * - * ## Playstyle overview (Template) - * - * | Playstyle | Active | Passive | - * |-------------|----------------------------|----------------------------| - * | AGGRESSIVE | Power Strike (10 hits) | Bloodlust – speed on hit | - * | DEFENSIVE | Iron Skin (5 hits) | Fortitude – 10% dmg reduc. | - */ -class TemplateKit : Kit() { - - override val id = "template" - override val displayName: Component = Component.text("Template Kit", NamedTextColor.YELLOW) - override val lore = listOf("An example kit.", "Replace with your own.") - override val icon = Material.NETHER_STAR - - // ── Cached ability instances (avoid allocating per event call) ──────────── - private val aggressiveActive = AggressiveActive() - private val defensiveActive = DefensiveActive() - private val aggressivePassive = AggressivePassive() - private val defensivePassive = DefensivePassive() - - // ── Playstyle routing ───────────────────────────────────────────────────── - - override fun getActiveAbility(playstyle: Playstyle): ActiveAbility = when (playstyle) { - Playstyle.AGGRESSIVE -> aggressiveActive - Playstyle.DEFENSIVE -> defensiveActive - } - - override fun getPassiveAbility(playstyle: Playstyle): PassiveAbility = when (playstyle) { - Playstyle.AGGRESSIVE -> aggressivePassive - Playstyle.DEFENSIVE -> defensivePassive - } - - // ── Item distribution ───────────────────────────────────────────────────── - - override val cachedItems = ConcurrentHashMap>() - - override fun giveItems(player: Player, playstyle: Playstyle) { - // Slot 8 = ability trigger item (always present) - player.inventory.setItem(8, ItemStack(Material.BLAZE_ROD)) - - when (playstyle) { - Playstyle.AGGRESSIVE -> { - // e.g. extra offensive item - } - Playstyle.DEFENSIVE -> { - // e.g. extra defensive item - } - } - } - - // ── Optional lifecycle hooks ────────────────────────────────────────────── - - override fun onAssign(player: Player, playstyle: Playstyle) { - // Example: a kit that always gives permanent speed I - // player.addPotionEffect(PotionEffect(PotionEffectType.SPEED, Int.MAX_VALUE, 0, false, false, true)) - } - - override fun onRemove(player: Player) { - // Undo anything done in onAssign - // player.removePotionEffect(PotionEffectType.SPEED) - } - - // ========================================================================= - // AGGRESSIVE active ability - // ========================================================================= - - /** - * Power Strike — marks the player so their next hit deals 1.5× damage. - * - * Demonstrates: - * - Returning [AbilityResult.Success] - * - Returning [AbilityResult.ConditionNotMet] (no example here, but shown below) - * - [onFullyCharged] for charge-complete feedback - */ - private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { - - override val name = "Power Strike" - override val description = "Your next melee attack deals 1.5× damage." - override val hitsRequired = 10 - override val triggerMaterial = Material.BLAZE_ROD - - // In a real kit, store pending-strike players in a companion Set - // and apply the bonus in onHitEnemy / a damage event listener. - - override fun execute(player: Player): AbilityResult { - // Example: guard clause returning ConditionNotMet - // val nearbyEnemies = player.getNearbyEntities(10.0, 10.0, 10.0).filterIsInstance() - // if (nearbyEnemies.isEmpty()) return AbilityResult.ConditionNotMet("No enemies nearby!") - - // TODO: add player.uniqueId to a "powerStrikePending" set - - player.playSound(player.location, Sound.ENTITY_BLAZE_SHOOT, 1f, 1.2f) - player.sendActionBar(Component.text("⚔ Power Strike ready!", NamedTextColor.RED)) - return AbilityResult.Success - } - - override fun onFullyCharged(player: Player) { - player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f) - player.sendActionBar(Component.text("⚡ Ability recharged!", NamedTextColor.GREEN)) - } - } - - // ========================================================================= - // DEFENSIVE active ability - // ========================================================================= - - /** - * Iron Skin — grants Resistance I for 4 seconds. - * - * Demonstrates a simpler ability with fewer required hits (5 vs 10). - */ - private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { - - override val name = "Iron Skin" - override val description = "Gain Resistance I for 4 seconds." - override val hitsRequired = 5 - override val triggerMaterial = Material.BLAZE_ROD - - override fun execute(player: Player): AbilityResult { - player.addPotionEffect( - PotionEffect( - PotionEffectType.RESISTANCE, - /* duration */ 4 * 20, - /* amplifier */ 0, - /* ambient */ false, - /* particles */ false, - /* icon */ true - ) - ) - player.playSound(player.location, Sound.ITEM_TOTEM_USE, 0.8f, 1.5f) - player.sendActionBar(Component.text("🛡 Iron Skin active!", NamedTextColor.AQUA)) - return AbilityResult.Success - } - - override fun onFullyCharged(player: Player) { - player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f) - player.sendActionBar(Component.text("⚡ Ability recharged!", NamedTextColor.GREEN)) - } - } - - // ========================================================================= - // AGGRESSIVE passive — Bloodlust - // ========================================================================= - - /** - * Bloodlust — grants Speed I for 2 seconds after landing a hit. - * - * Demonstrates [onHitEnemy] and [onActivate] / [onDeactivate] usage. - */ - private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { - - override val name = "Bloodlust" - override val description = "Gain Speed I for 2 s after hitting an enemy." - - override fun onActivate(player: Player) { - // Called once at game start. - // Start any repeating BukkitTasks here and store the returned BukkitTask - // so you can cancel it in onDeactivate. Example: - // task = Bukkit.getScheduler().runTaskTimer(plugin, { checkCooldowns(player) }, 0L, 10L) - } - - override fun onDeactivate(player: Player) { - // task?.cancel() - } - - // NOTE: Called AFTER the charge counter has already been incremented. - override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { - attacker.addPotionEffect( - PotionEffect( - PotionEffectType.SPEED, - /* duration */ 2 * 20, - /* amplifier */ 0, - /* ambient */ false, - /* particles */ false, - /* icon */ false - ) - ) - } - } - - // ========================================================================= - // DEFENSIVE passive — Fortitude - // ========================================================================= - - /** - * Fortitude — reduces all incoming melee damage by 10%. - * - * Demonstrates [onHitByEnemy] — the simplest passive pattern. - */ - private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { - - override val name = "Fortitude" - override val description = "Take 10% less damage from melee attacks." - - // onActivate / onDeactivate are no-ops for this passive (default impl. is fine) - - override fun onHitByEnemy(victim: Player, attacker: Player, event: EntityDamageByEntityEvent) { - event.damage *= 0.90 - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt new file mode 100644 index 0000000..f38f9aa --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt @@ -0,0 +1,455 @@ +package club.mcscrims.speedhg.kit.impl + +import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.config.CustomGameSettings +import club.mcscrims.speedhg.kit.Kit +import club.mcscrims.speedhg.kit.Playstyle +import club.mcscrims.speedhg.kit.ability.AbilityResult +import club.mcscrims.speedhg.kit.ability.ActiveAbility +import club.mcscrims.speedhg.kit.ability.PassiveAbility +import club.mcscrims.speedhg.util.ItemBuilder +import club.mcscrims.speedhg.util.trans +import net.kyori.adventure.text.Component +import org.bukkit.* +import org.bukkit.entity.Player +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.scheduler.BukkitRunnable +import org.bukkit.scheduler.BukkitTask +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.math.cos +import kotlin.math.sin + +/** + * ## TheWorld + * + * | 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| + * + * ### 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. + * + * ### 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. + * + * ### 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. + */ +class TheWorldKit : Kit() { + + private val plugin get() = SpeedHG.instance + + override val id = "theworld" + override val displayName: Component + get() = plugin.languageManager.getDefaultComponent("kits.theworld.name", mapOf()) + override val lore: List + get() = plugin.languageManager.getDefaultRawMessageList("kits.theworld.lore") + override val icon = Material.CLOCK + + // ── Shared kit state ────────────────────────────────────────────────────── + + /** + * Aggressive blink charges: playerUUID → remaining uses. + * Set to [MAX_TELEPORT_CHARGES] on first right-click, decremented per blink. + */ + internal val teleportCharges: MutableMap = ConcurrentHashMap() + + /** + * Active freezes: victimUUID → (attackerUUID, [FrozenData]). + * Tracked separately per attacker so [onRemove] only thaws enemies + * frozen by the leaving player. + */ + internal val frozenEnemies: MutableMap> = ConcurrentHashMap() + + data class FrozenData( + var hitsRemaining: Int, + val task: BukkitTask + ) + + companion object { + private val kitOverride get() = + SpeedHG.instance.customGameManager.settings.kits.kits["theworld"] + ?: CustomGameSettings.KitOverride() + + private val ABILITY_COOLDOWN_MS = kitOverride.abilityCooldownMs + private val SHOCKWAVE_RADIUS = kitOverride.shockwaveRadius + private val TELEPORT_RANGE = kitOverride.teleportRange + private val MAX_TELEPORT_CHARGES = kitOverride.maxTeleportCharges + private val FREEZE_DURATION_TICKS = kitOverride.freezeDurationTicks + private val MAX_HITS_ON_FROZEN = kitOverride.maxHitsOnFrozen + } + + // ── Cached ability instances ────────────────────────────────────────────── + private val aggressiveActive = AggressiveActive() + private val defensiveActive = DefensiveActive() + private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE) + private val defensivePassive = DefensivePassive() + + override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressiveActive + Playstyle.DEFENSIVE -> defensiveActive + } + override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) { + Playstyle.AGGRESSIVE -> aggressivePassive + Playstyle.DEFENSIVE -> defensivePassive + } + + override val cachedItems = ConcurrentHashMap>() + + override fun giveItems(player: Player, playstyle: Playstyle) { + val active = getActiveAbility(playstyle) + val item = ItemBuilder(Material.CLOCK) + .name(active.name) + .lore(listOf(active.description)) + .build() + cachedItems[player.uniqueId] = listOf(item) + player.inventory.addItem(item) + } + + 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) -> + data.task.cancel() + frozenEnemies.remove(victimUUID) + Bukkit.getPlayer(victimUUID)?.clearFreezeEffects() + } + + cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } + } + + // ========================================================================= + // Shared helpers + // ========================================================================= + + /** + * Expanding ring of particles + radial knockback. + * + * The ring BukkitRunnable adds one ring per tick, radius grows by 1 block/tick. + * This gives the visual impression of a shockwave spreading outward. + */ + private fun doShockwave(origin: Player) { + val world = origin.world + + // ── Visual: expanding particle ring ─────────────────────────────────── + 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) { + 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) + } + r += 1.0 + } + }.runTaskTimer(plugin, 0L, 1L) + + // ── Physics: knock all nearby alive enemies outward ─────────────────── + world.getNearbyEntities(origin.location, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS) + .filterIsInstance() + .filter { it != origin && plugin.gameManager.alivePlayers.contains(it.uniqueId) } + .forEach { enemy -> + val dir = enemy.location.toVector() + .subtract(origin.location.toVector()) + .normalize() + .multiply(2.0) + .setY(0.45) + 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) + } + + /** + * Teleports [player] up to [TELEPORT_RANGE] blocks in their looking direction. + * Raycast in 0.4-block increments — stops at the last non-solid block. + */ + private fun blink(player: Player) { + 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 + target = next + } + + // Adjust to feet position + 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) + } + + /** + * Immobilises [target], capping hits from [attacker] at [MAX_HITS_ON_FROZEN]. + * A 1-tick repeating task zeros horizontal + upward velocity for 10 seconds. + */ + private fun applyFreeze(attacker: Player, target: Player) { + // Overwrite any existing freeze + frozenEnemies.remove(target.uniqueId)?.second?.task?.cancel() + + target.applyFreezeEffects() + + val task = object : BukkitRunnable() { + var ticks = 0 + override fun run() { + ticks++ + + if (ticks >= FREEZE_DURATION_TICKS || + !target.isOnline || + !plugin.gameManager.alivePlayers.contains(target.uniqueId) || + !frozenEnemies.containsKey(target.uniqueId)) + { + doUnfreeze(target) + cancel() + return + } + + // Zero horizontal + upward velocity every tick + 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 } + + // Refresh slowness every second so it doesn't expire mid-freeze + if (ticks % 20 == 0) target.applyFreezeEffects() + + // Powder-snow visual (cosmetic) + if (target.freezeTicks < 140) target.freezeTicks = 140 + } + }.runTaskTimer(plugin, 0L, 1L) + + frozenEnemies[target.uniqueId] = Pair( + attacker.uniqueId, + FrozenData(hitsRemaining = MAX_HITS_ON_FROZEN, 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) + } + + private fun doUnfreeze(target: Player) { + frozenEnemies.remove(target.uniqueId) + target.clearFreezeEffects() + if (target.isOnline) + target.sendActionBar(target.trans("kits.theworld.messages.frozen_expired")) + } + + // ── 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.clearFreezeEffects() { + removePotionEffect(PotionEffectType.SLOWNESS) + removePotionEffect(PotionEffectType.MINING_FATIGUE) + freezeTicks = 0 + } + + // ========================================================================= + // AGGRESSIVE active – Shockwave → 3× blink + // ========================================================================= + + private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { + + private val plugin get() = SpeedHG.instance + private val cooldowns: MutableMap = ConcurrentHashMap() + + override val kitId: String + get() = "theworld" + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage( + "kits.theworld.items.clock.aggressive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage( + "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) + val remaining = charges - 1 + teleportCharges[player.uniqueId] = remaining + + 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")) + } + return AbilityResult.Success + } + + // ── Cooldown gate ───────────────────────────────────────────────── + 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") + } + + // ── Shockwave + grant 3 blink charges ──────────────────────────── + doShockwave(player) + teleportCharges[player.uniqueId] = MAX_TELEPORT_CHARGES + cooldowns[player.uniqueId] = now + + player.sendActionBar(player.trans( + "kits.theworld.messages.shockwave_and_blink", + mapOf("charges" to MAX_TELEPORT_CHARGES.toString()))) + + return AbilityResult.Success + } + } + + // ========================================================================= + // DEFENSIVE active – Shockwave + freeze + 5-hit cap + // ========================================================================= + + private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { + + private val plugin get() = SpeedHG.instance + private val cooldowns: MutableMap = ConcurrentHashMap() + + override val kitId: String + get() = "theworld" + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage( + "kits.theworld.items.clock.defensive.name") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage( + "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 ───────────────────────────────────────────────── + 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 targets = player.world + .getNearbyEntities( + player.location, + SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS, SHOCKWAVE_RADIUS) + .filterIsInstance() + .filter { it != player && + plugin.gameManager.alivePlayers.contains(it.uniqueId) } + + if (targets.isEmpty()) + return AbilityResult.ConditionNotMet("No enemies within range!") + + 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()))) + + return AbilityResult.Success + } + } + + // ========================================================================= + // DEFENSIVE passive – 5-hit cap on frozen enemies + // ========================================================================= + + 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") + override val description: String + get() = plugin.languageManager.getDefaultRawMessage( + "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. + */ + 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 + + data.hitsRemaining-- + + 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()))) + } + } + } + + // ========================================================================= + // AGGRESSIVE no-passive + // ========================================================================= + + private class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) { + override val name = "None" + override val description = "None" + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt index 1b06229..8328679 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt @@ -1,6 +1,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -48,6 +49,10 @@ class VenomKit : Kit() { override val icon: Material get() = Material.SPIDER_EYE + private val kitOverride get() = + plugin.customGameManager.settings.kits.kits["venom"] + ?: CustomGameSettings.KitOverride() + // ── Cached ability instances (avoid allocating per event call) ──────────── private val aggressiveActive = AggressiveActive() private val defensiveActive = DefensiveActive() @@ -115,17 +120,20 @@ class VenomKit : Kit() { items.forEach { player.inventory.remove( it ) } } - private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { + private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "venom" + override val name: String get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.wither.name" ) override val description: String get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.wither.description" ) - override val hitsRequired: Int + override val hardcodedHitsRequired: Int get() = 15 override val triggerMaterial: Material @@ -168,13 +176,16 @@ class VenomKit : Kit() { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "venom" + override val name: String get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.shield.name" ) override val description: String get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.shield.description" ) - override val hitsRequired: Int + override val hardcodedHitsRequired: Int get() = 15 override val triggerMaterial: Material @@ -228,10 +239,10 @@ class VenomKit : Kit() { if (activeShields.containsKey( player.uniqueId )) breakShield( player ) } - }.runTaskLater( plugin, 160L ) + }.runTaskLater( plugin, kitOverride.shieldDurationTicks ) activeShields[ player.uniqueId ] = ActiveShield( - remainingCapacity = 15.0, + remainingCapacity = kitOverride.shieldCapacity, expireTask = expireTask, particleTask = particleTask ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt index 866f302..47133e0 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt @@ -1,6 +1,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -57,6 +58,10 @@ class VoodooKit : Kit() { /** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */ internal val cursedExpiry: MutableMap = ConcurrentHashMap() + private val kitOverride get() = + plugin.customGameManager.settings.kits.kits["voodoo"] + ?: CustomGameSettings.KitOverride() + // ── Cached ability instances ────────────────────────────────────────────── private val aggressiveActive = AggressiveActive() private val defensiveActive = DefensiveActive() @@ -100,11 +105,15 @@ class VoodooKit : Kit() { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "voodoo" + override val name: String get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.name") override val description: String get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.description") - override val hitsRequired = 15 + override val hardcodedHitsRequired: Int + get() = 15 override val triggerMaterial = Material.WITHER_ROSE override fun execute(player: Player): AbilityResult { @@ -164,11 +173,15 @@ class VoodooKit : Kit() { private val plugin get() = SpeedHG.instance + override val kitId: String + get() = "voodoo" + override val name: String get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.name") override val description: String get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.description") - override val hitsRequired = 10 + override val hardcodedHitsRequired: Int + get() = 10 override val triggerMaterial = Material.SOUL_TORCH override fun execute(player: Player): AbilityResult { @@ -180,7 +193,7 @@ class VoodooKit : Kit() { if (targets.isEmpty()) return AbilityResult.ConditionNotMet("No enemies within 8 blocks!") - val expiry = System.currentTimeMillis() + 15_000L + val expiry = System.currentTimeMillis() + kitOverride.curseDurationMs targets.forEach { t -> cursedExpiry[t.uniqueId] = expiry t.addPotionEffect(PotionEffect(PotionEffectType.GLOWING, 15 * 20, 0, false, true, false)) diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index 77d7ee3..06a21a0 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -282,4 +282,31 @@ kits: messages: fist_mode_active: '⚡ Vibranium Fists active for 12 seconds!' wakanda_impact: 'Wakanda Forever! Hit enemy(s)!' - ability_charged: 'Ability recharged!' \ No newline at end of file + ability_charged: 'Ability recharged!' + theworld: + name: 'The World' + lore: + - ' ' + - 'AGGRESSIVE: Shockwave + 3x Blink' + - 'DEFENSIVE: Shockwave + Freeze + 5-Hit Cap' + items: + clock: + aggressive: + name: 'The World' + description: 'Shockwave → right-click 3x to blink in looking direction' + defensive: + name: 'The World' + description: 'Shockwave + freeze nearby enemies (max 5 hits each)' + passive: + defensive: + name: 'Time Stop' + description: 'Frozen enemies can only be hit 5 times' + messages: + shockwave_and_blink: 'Shockwave! blink charge(s) ready.' + teleport_charges: 'Blinked! charge(s) remaining.' + charges_exhausted: 'All blink charges spent!' + freeze_activated: '⏸ Time stopped for enemy(s)!' + frozen_received: '⏸ You are frozen for 10 seconds!' + frozen_expired: 'The freeze has worn off.' + freeze_broken: 'Freeze broken — 5 hits reached!' + freeze_hits_left: 'Frozen enemy — hit(s) remaining.' \ No newline at end of file