Add custom game settings
Each kit now supports custom settings aswell as custom game settings like min_players
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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() )
|
||||
}
|
||||
|
||||
@@ -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() )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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 )
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 ))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<UUID, Long> = 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
455
src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt
Normal file
455
src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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<UUID, Long> = 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))
|
||||
|
||||
@@ -282,4 +282,31 @@ kits:
|
||||
messages:
|
||||
fist_mode_active: '<gray>⚡ Vibranium Fists active for 12 seconds!</gray>'
|
||||
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>'
|
||||
Reference in New Issue
Block a user