Add custom game settings

Each kit now supports custom settings aswell as custom game settings like min_players
This commit is contained in:
TDSTOS
2026-03-28 23:51:32 +01:00
parent 5b00b51193
commit 184443b7c6
18 changed files with 773 additions and 278 deletions

View File

@@ -25,7 +25,9 @@ dependencies {
implementation("com.zaxxer:HikariCP:5.1.0") implementation("com.zaxxer:HikariCP:5.1.0")
implementation("com.mysql:mysql-connector-j:8.4.0") implementation("com.mysql:mysql-connector-j:8.4.0")
implementation(libs.kotlinxCoroutines) implementation(libs.kotlinxCoroutines)
implementation(libs.kotlinxSerialization)
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
compileOnly("com.sk89q.worldedit:worldedit-core:7.2.17-SNAPSHOT") compileOnly("com.sk89q.worldedit:worldedit-core:7.2.17-SNAPSHOT")

View File

@@ -3,6 +3,8 @@ package club.mcscrims.speedhg
import club.mcscrims.speedhg.command.KitCommand import club.mcscrims.speedhg.command.KitCommand
import club.mcscrims.speedhg.command.LeaderboardCommand import club.mcscrims.speedhg.command.LeaderboardCommand
import club.mcscrims.speedhg.command.TimerCommand 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.config.LanguageManager
import club.mcscrims.speedhg.database.DatabaseManager import club.mcscrims.speedhg.database.DatabaseManager
import club.mcscrims.speedhg.database.StatsManager import club.mcscrims.speedhg.database.StatsManager
@@ -59,10 +61,16 @@ class SpeedHG : JavaPlugin() {
lateinit var discordWebhookManager: DiscordWebhookManager lateinit var discordWebhookManager: DiscordWebhookManager
private set private set
lateinit var customGameManager: CustomGameManager
private set
override fun onLoad() override fun onLoad()
{ {
instance = this instance = this
customGameManager = CustomGameManager( this )
customGameManager.load()
saveDefaultConfig() saveDefaultConfig()
reloadConfig() reloadConfig()
@@ -117,6 +125,7 @@ class SpeedHG : JavaPlugin() {
kitManager.registerKit( GoblinKit() ) kitManager.registerKit( GoblinKit() )
kitManager.registerKit( IceMageKit() ) kitManager.registerKit( IceMageKit() )
kitManager.registerKit( RattlesnakeKit() ) kitManager.registerKit( RattlesnakeKit() )
kitManager.registerKit( TheWorldKit() )
kitManager.registerKit( VenomKit() ) kitManager.registerKit( VenomKit() )
kitManager.registerKit( VoodooKit() ) kitManager.registerKit( VoodooKit() )
} }

View File

@@ -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<CustomGameSettings>( raw ) }
.onSuccess { plugin.logger.info( "[CustomGameManager] Settings geladen." ) }
.onFailure { plugin.logger.severe( "[CustomGameManager] Parse-Fehler: ${it.message}" ) }
.getOrDefault( CustomGameSettings() )
}
}
}

View File

@@ -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<String, KitOverride> = 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
)
}

View File

@@ -99,7 +99,11 @@ class KitManager(
val playstyle = getSelectedPlaystyle( player ) val playstyle = getSelectedPlaystyle( player )
val active = kit.getActiveAbility( playstyle ) 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.onAssign( player, playstyle )
kit.getPassiveAbility( playstyle ).onActivate( player ) kit.getPassiveAbility( playstyle ).onActivate( player )

View File

@@ -1,5 +1,6 @@
package club.mcscrims.speedhg.kit.ability package club.mcscrims.speedhg.kit.ability
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.entity.Player import org.bukkit.entity.Player
@@ -24,18 +25,43 @@ abstract class ActiveAbility(
abstract val name: String abstract val name: String
abstract val description: 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 material that triggers this ability on right-click.
* The item must be in the player's main hand. * The item must be in the player's main hand.
*/ */
abstract val triggerMaterial: Material 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. * Execute the ability. Called only when [PlayerChargeData.isReady] is true.
* The dispatcher has already called [PlayerChargeData.consume] before this runs. * The dispatcher has already called [PlayerChargeData.consume] before this runs.

View File

@@ -187,10 +187,11 @@ class ArmorerKit : Kit() {
// ========================================================================= // =========================================================================
inner class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) { inner class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) {
override val kitId: String = "armorer"
override val name = "None" override val name = "None"
override val description = "None" override val description = "None"
override val hitsRequired = 0
override val triggerMaterial = Material.BARRIER override val triggerMaterial = Material.BARRIER
override val hardcodedHitsRequired: Int = 0
override fun execute(player: Player) = AbilityResult.Success override fun execute(player: Player) = AbilityResult.Success
} }
} }

View File

@@ -61,13 +61,16 @@ class BackupKit : Kit() {
private class NoActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) { private class NoActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) {
override val kitId: String
get() = "backup"
override val name: String override val name: String
get() = "None" get() = "None"
override val description: String override val description: String
get() = "None" get() = "None"
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 0 get() = 0
override val triggerMaterial: Material override val triggerMaterial: Material

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg.kit.impl package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
@@ -61,14 +62,18 @@ class BlackPantherKit : Kit()
companion object companion object
{ {
private val kitOverride get() =
SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"]
?: CustomGameSettings.KitOverride()
/** PDC key string shared with [KitEventDispatcher] for push-projectiles. */ /** PDC key string shared with [KitEventDispatcher] for push-projectiles. */
const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile" const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile"
private const val FIST_MODE_MS = 12_000L // 12 seconds private val FIST_MODE_MS = kitOverride.fistModeDurationMs // 12 seconds
private const val PUSH_RADIUS = 5.0 private val PUSH_RADIUS = kitOverride.pushRadius
private const val POUNCE_MIN_FALL = 3.0f private val POUNCE_MIN_FALL = kitOverride.pounceMinFall
private const val POUNCE_RADIUS = 3.0 private val POUNCE_RADIUS = kitOverride.pounceRadius
private const val POUNCE_DAMAGE = 6.0 // 3 hearts = 6 HP private val POUNCE_DAMAGE = kitOverride.pounceDamage // 3 hearts = 6 HP
} }
// ── Cached ability instances ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────
@@ -114,11 +119,13 @@ class BlackPantherKit : Kit()
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String get() = "blackpanther"
override val hardcodedHitsRequired: Int get() = 15
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.name") get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.name")
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.description") get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.description")
override val hitsRequired = 15
override val triggerMaterial = Material.BLACK_DYE override val triggerMaterial = Material.BLACK_DYE
override fun execute(player: Player): AbilityResult { override fun execute(player: Player): AbilityResult {
@@ -184,9 +191,10 @@ class BlackPantherKit : Kit()
// ========================================================================= // =========================================================================
private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) {
override val kitId: String = "blackpanther"
override val name = "None" override val name = "None"
override val description = "None" override val description = "None"
override val hitsRequired = 0 override val hardcodedHitsRequired: Int = 0
override val triggerMaterial = Material.BARRIER override val triggerMaterial = Material.BARRIER
override fun execute(player: Player) = AbilityResult.Success override fun execute(player: Player) = AbilityResult.Success
} }

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg.kit.impl package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.KitMetaData import club.mcscrims.speedhg.kit.KitMetaData
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
@@ -41,6 +42,10 @@ class GladiatorKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.IRON_BARS get() = Material.IRON_BARS
private val kitOverride get() =
plugin.customGameManager.settings.kits.kits["gladiator"]
?: CustomGameSettings.KitOverride()
// ── Cached ability instances (avoid allocating per event call) ──────────── // ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AllActive( Playstyle.AGGRESSIVE ) private val aggressiveActive = AllActive( Playstyle.AGGRESSIVE )
private val defensiveActive = AllActive( Playstyle.DEFENSIVE ) private val defensiveActive = AllActive( Playstyle.DEFENSIVE )
@@ -95,13 +100,16 @@ class GladiatorKit : Kit() {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "gladiator"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.description" )
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
override val triggerMaterial: Material override val triggerMaterial: Material
@@ -118,8 +126,8 @@ class GladiatorKit : Kit() {
lineOfSight.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() )) lineOfSight.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ))
return AbilityResult.ConditionNotMet( "Already in gladiator fight" ) return AbilityResult.ConditionNotMet( "Already in gladiator fight" )
val radius = 23 val radius = kitOverride.arenaRadius
val height = 10 val height = kitOverride.arenaHeight
player.setMetadata( KitMetaData.IN_GLADIATOR.getKey(), FixedMetadataValue( plugin, true )) player.setMetadata( KitMetaData.IN_GLADIATOR.getKey(), FixedMetadataValue( plugin, true ))
lineOfSight.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 return true
} }
private class GladiatorFight( private inner class GladiatorFight(
val region: Region, val region: Region,
val gladiator: Player, val gladiator: Player,
val enemy: Player, val enemy: Player,
@@ -254,7 +262,7 @@ class GladiatorKit : Kit() {
{ {
timer++ timer++
if ( timer > 180 ) if ( timer > kitOverride.witherAfterSeconds )
{ {
gladiator.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 )) gladiator.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 ))
enemy.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 )) enemy.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 ))

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg.kit.impl package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
@@ -35,6 +36,10 @@ class GoblinKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.MOSSY_COBBLESTONE get() = Material.MOSSY_COBBLESTONE
private val kitOverride get() =
plugin.customGameManager.settings.kits.kits["goblin"]
?: CustomGameSettings.KitOverride()
// ── Cached ability instances (avoid allocating per event call) ──────────── // ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
@@ -103,17 +108,20 @@ class GoblinKit : Kit() {
items.forEach { player.inventory.remove( it ) } 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 private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "goblin"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.description" )
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
override val triggerMaterial: Material override val triggerMaterial: Material
@@ -151,7 +159,7 @@ class GoblinKit : Kit() {
plugin.kitManager.selectPlaystyle( player, currentPlaystyle ) plugin.kitManager.selectPlaystyle( player, currentPlaystyle )
plugin.kitManager.applyKit( player ) plugin.kitManager.applyKit( player )
} }
}, 20L * 60) }, 20L * kitOverride.stealDuration)
activeStealTasks[ player.uniqueId ] = task 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 private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "goblin"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.description" )
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
override val triggerMaterial: Material override val triggerMaterial: Material
@@ -202,7 +213,7 @@ class GoblinKit : Kit() {
WorldEditUtils.createSphere( WorldEditUtils.createSphere(
world, world,
location, location,
10.0, kitOverride.bunkerRadius,
false, false,
Material.MOSSY_COBBLESTONE Material.MOSSY_COBBLESTONE
) )
@@ -211,7 +222,7 @@ class GoblinKit : Kit() {
WorldEditUtils.createSphere( WorldEditUtils.createSphere(
world, world,
location, location,
10.0, kitOverride.bunkerRadius,
false, false,
Material.AIR Material.AIR
) )

View File

@@ -91,14 +91,17 @@ class IceMageKit : Kit() {
private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
override val kitId: String
get() = "icemage"
override val name: String override val name: String
get() = "None" get() = "None"
override val description: String override val description: String
get() = "None" get() = "None"
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 0 get() = 15
override val triggerMaterial: Material override val triggerMaterial: Material
get() = Material.BARRIER get() = Material.BARRIER
@@ -116,13 +119,16 @@ class IceMageKit : Kit() {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "icemage"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.description" )
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
override val triggerMaterial: Material override val triggerMaterial: Material

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg.kit.impl package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
@@ -56,11 +57,15 @@ class RattlesnakeKit : Kit() {
internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap() internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object { companion object {
private const val POUNCE_COOLDOWN_MS = 20_000L private val kitOverride get() =
private const val MAX_SNEAK_MS = 3_000L SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"]
private const val MIN_RANGE = 3.0 ?: CustomGameSettings.KitOverride()
private const val MAX_RANGE = 10.0
private const val POUNCE_TIMEOUT_TICKS = 30L // 1.5 s 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 ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────
@@ -117,11 +122,15 @@ class RattlesnakeKit : Kit() {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "rattlesnake"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.name") get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.name")
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.description") 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 val triggerMaterial = Material.SLIME_BALL
override fun execute(player: Player): AbilityResult { override fun execute(player: Player): AbilityResult {
@@ -235,9 +244,10 @@ class RattlesnakeKit : Kit() {
// ========================================================================= // =========================================================================
private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) {
override val kitId: String = "rattlesnake"
override val name = "None" override val name = "None"
override val description = "None" override val description = "None"
override val hitsRequired = 0 override val hardcodedHitsRequired: Int = 0
override val triggerMaterial = Material.BARRIER override val triggerMaterial = Material.BARRIER
override fun execute(player: Player) = AbilityResult.Success override fun execute(player: Player) = AbilityResult.Success
} }

View File

@@ -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<UUID, List<ItemStack>>()
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<UUID>
// 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<Player>()
// 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
}
}
}

View File

@@ -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<String>
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<UUID, Int> = ConcurrentHashMap()
/**
* Active freezes: victimUUID → (attackerUUID, [FrozenData]).
* Tracked separately per attacker so [onRemove] only thaws enemies
* frozen by the leaving player.
*/
internal val frozenEnemies: MutableMap<UUID, Pair<UUID, FrozenData>> = 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<UUID, List<ItemStack>>()
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<Player>()
.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<UUID, Long> = 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<UUID, Long> = 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<Player>()
.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"
}
}

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg.kit.impl package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
@@ -48,6 +49,10 @@ class VenomKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.SPIDER_EYE get() = Material.SPIDER_EYE
private val kitOverride get() =
plugin.customGameManager.settings.kits.kits["venom"]
?: CustomGameSettings.KitOverride()
// ── Cached ability instances (avoid allocating per event call) ──────────── // ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
@@ -115,17 +120,20 @@ class VenomKit : Kit() {
items.forEach { player.inventory.remove( it ) } 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 private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "venom"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.wither.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.wither.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.wither.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.wither.description" )
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
override val triggerMaterial: Material override val triggerMaterial: Material
@@ -168,13 +176,16 @@ class VenomKit : Kit() {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "venom"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.shield.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.shield.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.shield.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.venom.items.shield.description" )
override val hitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
override val triggerMaterial: Material override val triggerMaterial: Material
@@ -228,10 +239,10 @@ class VenomKit : Kit() {
if (activeShields.containsKey( player.uniqueId )) if (activeShields.containsKey( player.uniqueId ))
breakShield( player ) breakShield( player )
} }
}.runTaskLater( plugin, 160L ) }.runTaskLater( plugin, kitOverride.shieldDurationTicks )
activeShields[ player.uniqueId ] = ActiveShield( activeShields[ player.uniqueId ] = ActiveShield(
remainingCapacity = 15.0, remainingCapacity = kitOverride.shieldCapacity,
expireTask = expireTask, expireTask = expireTask,
particleTask = particleTask particleTask = particleTask
) )

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg.kit.impl package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
@@ -57,6 +58,10 @@ class VoodooKit : Kit() {
/** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */ /** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */
internal val cursedExpiry: MutableMap<UUID, Long> = ConcurrentHashMap() internal val cursedExpiry: MutableMap<UUID, Long> = ConcurrentHashMap()
private val kitOverride get() =
plugin.customGameManager.settings.kits.kits["voodoo"]
?: CustomGameSettings.KitOverride()
// ── Cached ability instances ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
@@ -100,11 +105,15 @@ class VoodooKit : Kit() {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "voodoo"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.name") get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.name")
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.description") 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 val triggerMaterial = Material.WITHER_ROSE
override fun execute(player: Player): AbilityResult { override fun execute(player: Player): AbilityResult {
@@ -164,11 +173,15 @@ class VoodooKit : Kit() {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "voodoo"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.name") get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.name")
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.description") 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 val triggerMaterial = Material.SOUL_TORCH
override fun execute(player: Player): AbilityResult { override fun execute(player: Player): AbilityResult {
@@ -180,7 +193,7 @@ class VoodooKit : Kit() {
if (targets.isEmpty()) if (targets.isEmpty())
return AbilityResult.ConditionNotMet("No enemies within 8 blocks!") return AbilityResult.ConditionNotMet("No enemies within 8 blocks!")
val expiry = System.currentTimeMillis() + 15_000L val expiry = System.currentTimeMillis() + kitOverride.curseDurationMs
targets.forEach { t -> targets.forEach { t ->
cursedExpiry[t.uniqueId] = expiry cursedExpiry[t.uniqueId] = expiry
t.addPotionEffect(PotionEffect(PotionEffectType.GLOWING, 15 * 20, 0, false, true, false)) t.addPotionEffect(PotionEffect(PotionEffectType.GLOWING, 15 * 20, 0, false, true, false))

View File

@@ -282,4 +282,31 @@ kits:
messages: messages:
fist_mode_active: '<gray>⚡ Vibranium Fists active for 12 seconds!</gray>' fist_mode_active: '<gray>⚡ Vibranium Fists active for 12 seconds!</gray>'
wakanda_impact: '<white>Wakanda Forever! Hit <count> enemy(s)!</white>' wakanda_impact: '<white>Wakanda Forever! Hit <count> enemy(s)!</white>'
ability_charged: '<yellow>Ability recharged!</yellow>' ability_charged: '<yellow>Ability recharged!</yellow>'
theworld:
name: '<gradient:dark_gray:white><bold>The World</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE: Shockwave + 3x Blink'
- 'DEFENSIVE: Shockwave + Freeze + 5-Hit Cap'
items:
clock:
aggressive:
name: '<gray>The World</gray>'
description: 'Shockwave → right-click 3x to blink in looking direction'
defensive:
name: '<gray>The World</gray>'
description: 'Shockwave + freeze nearby enemies (max 5 hits each)'
passive:
defensive:
name: '<aqua>Time Stop</aqua>'
description: 'Frozen enemies can only be hit 5 times'
messages:
shockwave_and_blink: '<gray>Shockwave! <white><charges> blink charge(s) ready.</white>'
teleport_charges: '<aqua>Blinked! <charges> charge(s) remaining.</aqua>'
charges_exhausted: '<red>All blink charges spent!</red>'
freeze_activated: '<aqua>⏸ Time stopped for <count> enemy(s)!</aqua>'
frozen_received: '<red>⏸ You are frozen for 10 seconds!</red>'
frozen_expired: '<gray>The freeze has worn off.</gray>'
freeze_broken: '<gold>Freeze broken — 5 hits reached!</gold>'
freeze_hits_left: '<aqua>Frozen enemy — <hits> hit(s) remaining.</aqua>'