Add live config overrides and refactor kits

Introduce runtime-configurable settings and defaults across multiple kits and clean up implementations. Kits (Armorer, Backup, Gladiator, Goblin, IceMage, Puppet, and others) now expose companion-object default constants and read values via override() (SPEEDHG_CUSTOM_SETTINGS) with typed accessors. Activation and tick-time behaviour snapshot these values so mid-round config changes don't produce inconsistent effects. Also includes general refactors: clearer getters, small API/formatting cleanup, improved no-op ability placeholders, and minor behavior protections (e.g. disallow stealing Backup kit).
This commit is contained in:
TDSTOS
2026-04-12 05:06:43 +02:00
parent 411b77cc8d
commit 5a828a3993
13 changed files with 2016 additions and 970 deletions

View File

@@ -19,9 +19,9 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
/** /**
* ## Armorer * ## ArmorerKit
* *
* Upgrades the player's **Chestplate + Boots** every 2 kills. * Upgrades the player's **Chestplate + Boots** every [killsPerTier] kills.
* *
* | Tier | Kills | Material | * | Tier | Kills | Material |
* |------|-------|-----------| * |------|-------|-----------|
@@ -32,22 +32,31 @@ import java.util.concurrent.ConcurrentHashMap
* When a piece breaks ([onItemBreak]), it is immediately replaced with the * When a piece breaks ([onItemBreak]), it is immediately replaced with the
* current tier so the player is never left without armor. * current tier so the player is never left without armor.
* *
* - **AGGRESSIVE passive**: +Strength I (5 s) for every kill. * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* - **DEFENSIVE passive**: +Protection I enchant on iron/diamond pieces. *
* All values read from the `extras` map with companion-object defaults as fallback.
*
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |-----------------------|-----|---------|-----------------------------------------------------------|
* | `kills_per_tier` | Int | `2` | Number of kills required to advance one armor tier |
* | `strength_ticks` | Int | `100` | Duration in ticks of Strength I granted on kill (aggressive) |
*/ */
class ArmorerKit : Kit() { class ArmorerKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val id = "armorer" override val id: String
override val displayName: Component get() = "armorer"
get() = plugin.languageManager.getDefaultComponent("kits.armorer.name", mapOf())
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList("kits.armorer.lore")
override val icon = Material.IRON_CHESTPLATE
// ── Kill tracking ───────────────────────────────────────────────────────── override val displayName: Component
internal val killCounts: MutableMap<UUID, Int> = ConcurrentHashMap() get() = plugin.languageManager.getDefaultComponent( "kits.armorer.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.armorer.lore" )
override val icon: Material
get() = Material.IRON_CHESTPLATE
companion object { companion object {
private val CHESTPLATE_TIERS = listOf( private val CHESTPLATE_TIERS = listOf(
@@ -65,87 +74,156 @@ class ArmorerKit : Kit() {
Sound.ITEM_ARMOR_EQUIP_IRON, Sound.ITEM_ARMOR_EQUIP_IRON,
Sound.ITEM_ARMOR_EQUIP_DIAMOND Sound.ITEM_ARMOR_EQUIP_DIAMOND
) )
const val DEFAULT_KILLS_PER_TIER = 2
const val DEFAULT_STRENGTH_TICKS = 100
} }
// ── Live config accessors ─────────────────────────────────────────────────
/**
* Number of kills required to advance one armor tier.
* JSON key: `kills_per_tier`
*/
private val killsPerTier: Int
get() = override().getInt( "kills_per_tier" ) ?: DEFAULT_KILLS_PER_TIER
/**
* Duration in ticks of Strength I applied to the killer on each kill (aggressive).
* JSON key: `strength_ticks`
*/
private val strengthTicks: Int
get() = override().getInt( "strength_ticks" ) ?: DEFAULT_STRENGTH_TICKS
// ── Kill tracking ─────────────────────────────────────────────────────────
internal val killCounts: MutableMap<UUID, Int> = ConcurrentHashMap()
// ── Cached ability instances ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────
private val aggressiveActive = NoActive(Playstyle.AGGRESSIVE) private val aggressiveActive = NoActive( Playstyle.AGGRESSIVE )
private val defensiveActive = NoActive(Playstyle.DEFENSIVE) private val defensiveActive = NoActive( Playstyle.DEFENSIVE )
private val aggressivePassive = AggressivePassive() private val aggressivePassive = AggressivePassive()
private val defensivePassive = DefensivePassive() private val defensivePassive = DefensivePassive()
override fun getActiveAbility (playstyle: Playstyle) = when (playstyle) { // ── Playstyle routing ─────────────────────────────────────────────────────
override fun getActiveAbility(
playstyle: Playstyle
): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
override fun getPassiveAbility(
playstyle: Playstyle
): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
// ── Item distribution ─────────────────────────────────────────────────────
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(player: Player, playstyle: Playstyle) { /* armor is given in onAssign */ } override fun giveItems(
player: Player,
playstyle: Playstyle
) { /* armor is given in onAssign */ }
override fun onAssign(player: Player, playstyle: Playstyle) { // ── Lifecycle hooks ───────────────────────────────────────────────────────
killCounts[player.uniqueId] = 0
setArmorTier(player, tier = 0, playstyle) override fun onAssign(
player: Player,
playstyle: Playstyle
) {
killCounts[ player.uniqueId ] = 0
setArmorTier( player, tier = 0, playstyle )
} }
override fun onRemove(player: Player) { override fun onRemove(
killCounts.remove(player.uniqueId) player: Player
) {
killCounts.remove( player.uniqueId )
} }
// ── Kit-level kill hook (upgrades armor) ────────────────────────────────── // ── Kit-level kill hook (upgrades armor) ──────────────────────────────────
override fun onKillEnemy(killer: Player, victim: Player) { override fun onKillEnemy(
val newKills = killCounts.compute(killer.uniqueId) { _, v -> (v ?: 0) + 1 } ?: return killer: Player,
val tier = (newKills / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex) victim: Player
val prevTier = ((newKills - 1) / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex) ) {
val newKills = killCounts.compute( killer.uniqueId ) { _, v -> ( v ?: 0 ) + 1 } ?: return
if (tier > prevTier) { // Snapshot the tier threshold at kill time
val playstyle = plugin.kitManager.getSelectedPlaystyle(killer) val capturedKillsPerTier = killsPerTier
setArmorTier(killer, tier, playstyle)
killer.playSound(killer.location, EQUIP_SOUNDS[tier], 1f, 1f) val tier = ( newKills / capturedKillsPerTier ).coerceAtMost( CHESTPLATE_TIERS.lastIndex )
killer.sendActionBar(killer.trans("kits.armorer.messages.armor_upgraded", val prevTier = ( ( newKills - 1 ) / capturedKillsPerTier ).coerceAtMost( CHESTPLATE_TIERS.lastIndex )
mapOf("tier" to (tier + 1).toString())))
if ( tier > prevTier )
{
val playstyle = plugin.kitManager.getSelectedPlaystyle( killer )
setArmorTier( killer, tier, playstyle )
killer.playSound( killer.location, EQUIP_SOUNDS[ tier ], 1f, 1f )
killer.sendActionBar(
killer.trans(
"kits.armorer.messages.armor_upgraded",
mapOf( "tier" to ( tier + 1 ).toString() )
)
)
} }
} }
// ── Auto-replace on armor break ─────────────────────────────────────────── // ── Auto-replace on armor break ───────────────────────────────────────────
override fun onItemBreak(player: Player, brokenItem: ItemStack) { override fun onItemBreak(
val kills = killCounts[player.uniqueId] ?: return player: Player,
val tier = (kills / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex) brokenItem: ItemStack
val playstyle = plugin.kitManager.getSelectedPlaystyle(player) ) {
val kills = killCounts[ player.uniqueId ] ?: return
val tier = ( kills / killsPerTier ).coerceAtMost( CHESTPLATE_TIERS.lastIndex )
val playstyle = plugin.kitManager.getSelectedPlaystyle( player )
when { when
brokenItem.type.name.endsWith("_CHESTPLATE") -> { {
player.inventory.chestplate = buildPiece(CHESTPLATE_TIERS[tier], tier, playstyle) brokenItem.type.name.endsWith( "_CHESTPLATE" ) ->
player.sendActionBar(player.trans("kits.armorer.messages.armor_replaced")) {
player.inventory.chestplate = buildPiece( CHESTPLATE_TIERS[ tier ], tier, playstyle )
player.sendActionBar( player.trans( "kits.armorer.messages.armor_replaced" ) )
} }
brokenItem.type.name.endsWith("_BOOTS") -> { brokenItem.type.name.endsWith( "_BOOTS" ) ->
player.inventory.boots = buildPiece(BOOT_TIERS[tier], tier, playstyle) {
player.sendActionBar(player.trans("kits.armorer.messages.armor_replaced")) player.inventory.boots = buildPiece( BOOT_TIERS[ tier ], tier, playstyle )
player.sendActionBar( player.trans( "kits.armorer.messages.armor_replaced" ) )
} }
} }
} }
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
private fun setArmorTier(player: Player, tier: Int, playstyle: Playstyle) { private fun setArmorTier(
player.inventory.chestplate = buildPiece(CHESTPLATE_TIERS[tier], tier, playstyle) player: Player,
player.inventory.boots = buildPiece(BOOT_TIERS[tier], tier, playstyle) tier: Int,
playstyle: Playstyle
) {
player.inventory.chestplate = buildPiece( CHESTPLATE_TIERS[ tier ], tier, playstyle )
player.inventory.boots = buildPiece( BOOT_TIERS[ tier ], tier, playstyle )
} }
/** /**
* Builds an armor ItemStack, adding Protection I for DEFENSIVE builds * Builds an armor [ItemStack], adding Protection I for DEFENSIVE builds
* starting at iron tier (tier ≥ 1). * starting at iron tier (tier ≥ 1).
*/ */
private fun buildPiece(material: Material, tier: Int, playstyle: Playstyle): ItemStack { private fun buildPiece(
val item = ItemStack(material) material: Material,
if (playstyle == Playstyle.DEFENSIVE && tier >= 1) { tier: Int,
item.editMeta { it.addEnchant(Enchantment.PROTECTION, 1, true) } playstyle: Playstyle
} ): ItemStack
{
val item = ItemStack( material )
if ( playstyle == Playstyle.DEFENSIVE && tier >= 1 )
item.editMeta { it.addEnchant( Enchantment.PROTECTION, 1, true ) }
return item return item
} }
@@ -153,45 +231,75 @@ class ArmorerKit : Kit() {
// AGGRESSIVE passive Strength I on every kill // AGGRESSIVE passive Strength I on every kill
// ========================================================================= // =========================================================================
private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.aggressive.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.armorer.passive.aggressive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.aggressive.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.armorer.passive.aggressive.description" )
override fun onKillEnemy(killer: Player, victim: Player) { override fun onKillEnemy(
killer.addPotionEffect(PotionEffect(PotionEffectType.STRENGTH, 5 * 20, 0)) killer: Player,
killer.playSound(killer.location, Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.4f) victim: Player
) {
// Snapshot at kill time so a mid-round config change is consistent
val capturedStrengthTicks = strengthTicks
killer.addPotionEffect( PotionEffect( PotionEffectType.STRENGTH, capturedStrengthTicks, 0 ) )
killer.playSound( killer.location, Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.4f )
} }
} }
// ========================================================================= // =========================================================================
// DEFENSIVE passive Protection enchant is sufficient; no extra hook needed // DEFENSIVE passive Protection enchant is applied at tier; no extra hook needed
// ========================================================================= // =========================================================================
private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.defensive.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.armorer.passive.defensive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.defensive.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.armorer.passive.defensive.description" )
} }
// ========================================================================= // =========================================================================
// Shared no-ability active // Shared no-active placeholder
// ========================================================================= // =========================================================================
inner class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) { inner class NoActive(
override val kitId: String = "armorer" playstyle: Playstyle
override val name = "None" ) : ActiveAbility( playstyle )
override val description = "None" {
override val triggerMaterial = Material.BARRIER
override val hardcodedHitsRequired: Int = 0 override val kitId: String
override fun execute(player: Player) = AbilityResult.Success get() = "armorer"
override val name: String
get() = "None"
override val description: String
get() = "None"
override val triggerMaterial: Material
get() = Material.BARRIER
override val hardcodedHitsRequired: Int
get() = 0
override fun execute(
player: Player
): AbilityResult = AbilityResult.Success
} }
} }

View File

@@ -13,7 +13,20 @@ import org.bukkit.inventory.ItemStack
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class BackupKit : Kit() { /**
* ## BackupKit
*
* A stub kit that currently has no active or passive abilities.
* All ability slots return no-op implementations.
*
* This kit intentionally exposes no configurable constants — there is nothing
* to override via `SPEEDHG_CUSTOM_SETTINGS` at this time. When abilities are
* added in the future, their constants should be placed in the `companion object`
* and wired through `override().getInt( ... ) ?: DEFAULT_*` following the
* standard pattern.
*/
class BackupKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -57,9 +70,15 @@ class BackupKit : Kit() {
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems( player: Player, playstyle: Playstyle ) {} override fun giveItems(
player: Player,
playstyle: Playstyle
) {}
private class NoActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) { private class NoActive(
playstyle: Playstyle
) : ActiveAbility( playstyle )
{
override val kitId: String override val kitId: String
get() = "backup" get() = "backup"
@@ -76,11 +95,16 @@ class BackupKit : Kit() {
override val triggerMaterial: Material override val triggerMaterial: Material
get() = Material.BARRIER get() = Material.BARRIER
override fun execute( player: Player ): AbilityResult { return AbilityResult.Success } override fun execute(
player: Player
): AbilityResult = AbilityResult.Success
} }
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) { private class NoPassive(
playstyle: Playstyle
) : PassiveAbility( playstyle )
{
override val name: String override val name: String
get() = "None" get() = "None"

View File

@@ -25,7 +25,24 @@ import org.bukkit.scheduler.BukkitRunnable
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class GladiatorKit : Kit() { /**
* ## GladiatorKit
*
* Both playstyles share the same active ability — challenging a nearby enemy to a
* 1v1 duel inside a glass cylinder spawned high above the world.
*
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
*
* All values read from the typed [CustomGameSettings.KitOverride] fields.
*
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |------------------------|-----|---------|---------------------------------------------------|
* | `arena_radius` | Int | `11` | Radius of the glass arena cylinder (blocks) |
* | `arena_height` | Int | `7` | Height of the glass arena cylinder (blocks) |
* | `wither_after_seconds` | Int | `180` | Seconds before Wither IV is applied to both players|
*/
class GladiatorKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -41,11 +58,35 @@ class GladiatorKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.IRON_BARS get() = Material.IRON_BARS
private val kitOverride: CustomGameSettings.KitOverride by lazy { companion object {
plugin.customGameManager.settings.kits.kits["gladiator"] const val DEFAULT_ARENA_RADIUS = 11
?: CustomGameSettings.KitOverride() const val DEFAULT_ARENA_HEIGHT = 7
const val DEFAULT_WITHER_AFTER_SECONDS = 180
} }
// ── Live config accessors ─────────────────────────────────────────────────
/**
* Radius of the gladiator arena cylinder in blocks.
* Source: typed field `arena_radius`.
*/
private val arenaRadius: Int
get() = override().arenaRadius
/**
* Height of the gladiator arena cylinder in blocks.
* Source: typed field `arena_height`.
*/
private val arenaHeight: Int
get() = override().arenaHeight
/**
* Seconds into the fight before Wither IV is applied to force a resolution.
* Source: typed field `wither_after_seconds`.
*/
private val witherAfterSeconds: Int
get() = override().witherAfterSeconds
// ── 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 )
@@ -96,7 +137,10 @@ class GladiatorKit : Kit() {
items.forEach { player.inventory.remove( it ) } items.forEach { player.inventory.remove( it ) }
} }
private inner class AllActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) { private inner class AllActive(
playstyle: Playstyle
) : ActiveAbility( playstyle )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -110,7 +154,7 @@ class GladiatorKit : Kit() {
get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.description" )
override val hardcodedHitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = DEFAULT_WITHER_AFTER_SECONDS
override val triggerMaterial: Material override val triggerMaterial: Material
get() = Material.IRON_BARS get() = Material.IRON_BARS
@@ -122,40 +166,54 @@ class GladiatorKit : Kit() {
val lineOfSight = player.getTargetEntity( 3 ) as? Player val lineOfSight = player.getTargetEntity( 3 ) as? Player
?: return AbilityResult.ConditionNotMet( "No player in line of sight" ) ?: return AbilityResult.ConditionNotMet( "No player in line of sight" )
if (player.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ) || if ( player.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ) ||
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 = kitOverride.arenaRadius // Snapshot config values at activation time
val height = kitOverride.arenaHeight val capturedRadius = arenaRadius
val capturedHeight = 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 ) )
val gladiatorRegion = getGladiatorLocation(player.location.clone().add( 0.0, 64.0, 0.0 ), radius, height + 2 ) val gladiatorRegion = getGladiatorLocation(
player.location.clone().add( 0.0, 64.0, 0.0 ),
capturedRadius,
capturedHeight + 2
)
val center = BukkitAdapter.adapt( player.world, gladiatorRegion.center ) val center = BukkitAdapter.adapt( player.world, gladiatorRegion.center )
WorldEditUtils.createCylinder( player.world, center, radius - 1, true, 1, Material.WHITE_STAINED_GLASS ) WorldEditUtils.createCylinder( player.world, center, capturedRadius - 1, true, 1, Material.WHITE_STAINED_GLASS )
WorldEditUtils.createCylinder( player.world, center, radius - 1, false, height, Material.WHITE_STAINED_GLASS ) WorldEditUtils.createCylinder( player.world, center, capturedRadius - 1, false, capturedHeight, Material.WHITE_STAINED_GLASS )
WorldEditUtils.createCylinder( player.world, center.clone().add( 0.0, height - 1.0, 0.0 ), radius -1, true, 1, Material.WHITE_STAINED_GLASS ) WorldEditUtils.createCylinder( player.world, center.clone().add( 0.0, capturedHeight - 1.0, 0.0 ), capturedRadius - 1, true, 1, Material.WHITE_STAINED_GLASS )
Bukkit.getScheduler().runTaskLater( plugin, { -> Bukkit.getScheduler().runTaskLater( plugin, { ->
for ( vector3 in gladiatorRegion ) for ( vector3 in gladiatorRegion )
{ {
val block = player.world.getBlockAt(BukkitAdapter.adapt( player.world, vector3 )) val block = player.world.getBlockAt( BukkitAdapter.adapt( player.world, vector3 ) )
if ( block.type.isAir ) continue if ( block.type.isAir ) continue
block.setMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), FixedMetadataValue( plugin, true )) block.setMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), FixedMetadataValue( plugin, true ) )
} }
}, 5L ) }, 5L )
val gladiatorFight = GladiatorFight( gladiatorRegion, player, lineOfSight, radius, height ) val gladiatorFight = GladiatorFight(
gladiatorRegion,
player,
lineOfSight,
capturedRadius,
capturedHeight
)
gladiatorFight.runTaskTimer( plugin, 0, 20 ) gladiatorFight.runTaskTimer( plugin, 0, 20 )
return AbilityResult.Success return AbilityResult.Success
} }
} }
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) { private class NoPassive(
playstyle: Playstyle
) : PassiveAbility( playstyle )
{
override val name: String override val name: String
get() = "None" get() = "None"
@@ -182,8 +240,16 @@ class GladiatorKit : Kit() {
location.blockY, location.blockY + height location.blockY, location.blockY + height
) )
return if (!hasEnoughSpace( region )) return if ( !hasEnoughSpace( region ) )
getGladiatorLocation(location.add(if ( random.nextBoolean() ) -10.0 else 10.0, 5.0, if ( random.nextBoolean() ) -10.0 else 10.0 ), radius, height ) getGladiatorLocation(
location.add(
if ( random.nextBoolean() ) -10.0 else 10.0,
5.0,
if ( random.nextBoolean() ) -10.0 else 10.0
),
radius,
height
)
else region else region
} }
@@ -201,10 +267,10 @@ class GladiatorKit : Kit() {
{ {
val adapt = BukkitAdapter.adapt( world, vector3 ) val adapt = BukkitAdapter.adapt( world, vector3 )
if (!world.worldBorder.isInside( adapt )) if ( !world.worldBorder.isInside( adapt ) )
return false return false
if (!world.getBlockAt( adapt ).type.isAir) if ( !world.getBlockAt( adapt ).type.isAir )
return false return false
} }
return true return true
@@ -216,7 +282,8 @@ class GladiatorKit : Kit() {
val enemy: Player, val enemy: Player,
val radius: Int, val radius: Int,
val height: Int val height: Int
) : BukkitRunnable() { ) : BukkitRunnable()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -230,10 +297,10 @@ class GladiatorKit : Kit() {
fun init() fun init()
{ {
gladiator.addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 )) gladiator.addPotionEffect( PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ) )
enemy.addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 )) enemy.addPotionEffect( PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ) )
gladiator.teleport(Location( world, center.x + radius / 2, center.y + 1, center.z, 90f, 0f )) gladiator.teleport( Location( world, center.x + radius / 2, center.y + 1, center.z, 90f, 0f ) )
enemy.teleport(Location( world, center.x - radius / 2, center.y + 1, center.z, -90f, 0f )) enemy.teleport( Location( world, center.x - radius / 2, center.y + 1, center.z, -90f, 0f ) )
} }
private var ended = false private var ended = false
@@ -252,15 +319,18 @@ class GladiatorKit : Kit() {
return return
} }
if (region.contains(BukkitAdapter.asBlockVector( gladiator.location )) && if ( region.contains( BukkitAdapter.asBlockVector( gladiator.location ) ) &&
region.contains(BukkitAdapter.asBlockVector( enemy.location ))) region.contains( BukkitAdapter.asBlockVector( enemy.location ) ) )
{ {
timer++ timer++
if ( timer > kitOverride.witherAfterSeconds ) // Snapshot at tick time so a mid-fight config change is consistent
val capturedWitherAfterSeconds = witherAfterSeconds
if ( timer > capturedWitherAfterSeconds )
{ {
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 ) )
} }
} }
} }
@@ -273,25 +343,25 @@ class GladiatorKit : Kit() {
gladiator.apply { gladiator.apply {
removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin ) removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin )
removePotionEffect( PotionEffectType.WITHER ) removePotionEffect( PotionEffectType.WITHER )
addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 )) addPotionEffect( PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ) )
if ( isOnline && plugin.gameManager.alivePlayers.contains( uniqueId )) if ( isOnline && plugin.gameManager.alivePlayers.contains( uniqueId ) )
teleport( oldLocationGladiator ) teleport( oldLocationGladiator )
} }
enemy.apply { enemy.apply {
removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin ) removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin )
removePotionEffect( PotionEffectType.WITHER ) removePotionEffect( PotionEffectType.WITHER )
addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 )) addPotionEffect( PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ) )
if ( isOnline && plugin.gameManager.alivePlayers.contains( uniqueId )) if ( isOnline && plugin.gameManager.alivePlayers.contains( uniqueId ) )
teleport( oldLocationEnemy ) teleport( oldLocationEnemy )
} }
for ( vector3 in region ) for ( vector3 in region )
{ {
val block = world.getBlockAt(BukkitAdapter.adapt( world, vector3 )) val block = world.getBlockAt( BukkitAdapter.adapt( world, vector3 ) )
if (!block.hasMetadata( KitMetaData.GLADIATOR_BLOCK.getKey() )) continue if ( !block.hasMetadata( KitMetaData.GLADIATOR_BLOCK.getKey() ) ) continue
block.removeMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), plugin ) block.removeMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), plugin )
} }

View File

@@ -21,7 +21,25 @@ import org.bukkit.scheduler.BukkitTask
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class GoblinKit : Kit() { /**
* ## GoblinKit
*
* | Playstyle | Active | Passive |
* |-------------|------------------------------------------------------------------------|---------|
* | AGGRESSIVE | **Steal** copies the target's kit for [stealDuration] seconds | |
* | DEFENSIVE | **Bunker** spawns a hollow [bunkerRadius]-block cobblestone sphere | |
*
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
*
* All values read from the typed [CustomGameSettings.KitOverride] fields.
*
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |-------------------------|--------|---------|-------------------------------------------------------|
* | `steal_duration_seconds`| Int | `60` | Seconds the stolen kit is active before reverting |
* | `bunker_radius` | Double | `10.0` | Radius of the hollow cobblestone bunker sphere |
*/
class GoblinKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -37,11 +55,27 @@ class GoblinKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.MOSSY_COBBLESTONE get() = Material.MOSSY_COBBLESTONE
private val kitOverride: CustomGameSettings.KitOverride by lazy { companion object {
plugin.customGameManager.settings.kits.kits["goblin"] const val DEFAULT_STEAL_DURATION_SECONDS = 60
?: CustomGameSettings.KitOverride() const val DEFAULT_BUNKER_RADIUS = 10.0
} }
// ── Live config accessors ─────────────────────────────────────────────────
/**
* Seconds the stolen kit remains active before reverting to the player's own kit.
* Source: typed field `steal_duration_seconds`.
*/
private val stealDuration: Int
get() = override().stealDuration
/**
* Radius of the hollow cobblestone bunker sphere in blocks.
* Source: typed field `bunker_radius`.
*/
private val bunkerRadius: Double
get() = override().bunkerRadius
// ── 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()
@@ -110,7 +144,8 @@ class GoblinKit : Kit() {
items.forEach { player.inventory.remove( it ) } items.forEach { player.inventory.remove( it ) }
} }
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -138,10 +173,15 @@ class GoblinKit : Kit() {
val lineOfSight = player.getTargetEntity( 3 ) as? Player val lineOfSight = player.getTargetEntity( 3 ) as? Player
?: return AbilityResult.ConditionNotMet( "No player in line of sight" ) ?: return AbilityResult.ConditionNotMet( "No player in line of sight" )
val targetKit = plugin.kitManager.getSelectedKit( lineOfSight ) ?: return AbilityResult.ConditionNotMet( "Target has no kit" ) val targetKit = plugin.kitManager.getSelectedKit( lineOfSight )
?: return AbilityResult.ConditionNotMet( "Target has no kit" )
val targetPlaystyle = plugin.kitManager.getSelectedPlaystyle( lineOfSight ) val targetPlaystyle = plugin.kitManager.getSelectedPlaystyle( lineOfSight )
val currentKit = plugin.kitManager.getSelectedKit( player ) ?: return AbilityResult.ConditionNotMet( "Error while copying kit" ) if ( targetKit is BackupKit )
return AbilityResult.ConditionNotMet( "Backup kit cannot be stolen" )
val currentKit = plugin.kitManager.getSelectedKit( player )
?: return AbilityResult.ConditionNotMet( "Error while copying kit" )
val currentPlaystyle = plugin.kitManager.getSelectedPlaystyle( player ) val currentPlaystyle = plugin.kitManager.getSelectedPlaystyle( player )
activeStealTasks.remove( player.uniqueId ) activeStealTasks.remove( player.uniqueId )
@@ -151,10 +191,13 @@ class GoblinKit : Kit() {
plugin.kitManager.selectPlaystyle( player, targetPlaystyle ) plugin.kitManager.selectPlaystyle( player, targetPlaystyle )
plugin.kitManager.applyKit( player ) plugin.kitManager.applyKit( player )
// Snapshot the duration at activation time
val capturedStealDuration = stealDuration
val task = Bukkit.getScheduler().runTaskLater( plugin, { -> val task = Bukkit.getScheduler().runTaskLater( plugin, { ->
activeStealTasks.remove( player.uniqueId ) activeStealTasks.remove( player.uniqueId )
// Nur wiederherstellen, wenn Spieler noch alive und Spiel läuft // Only restore if player is still alive and game is running
if (plugin.gameManager.alivePlayers.contains( player.uniqueId ) && if ( plugin.gameManager.alivePlayers.contains( player.uniqueId ) &&
plugin.gameManager.currentState == GameState.INGAME ) plugin.gameManager.currentState == GameState.INGAME )
{ {
plugin.kitManager.removeKit( player ) plugin.kitManager.removeKit( player )
@@ -162,12 +205,14 @@ class GoblinKit : Kit() {
plugin.kitManager.selectPlaystyle( player, currentPlaystyle ) plugin.kitManager.selectPlaystyle( player, currentPlaystyle )
plugin.kitManager.applyKit( player ) plugin.kitManager.applyKit( player )
} }
}, 20L * kitOverride.stealDuration) }, 20L * capturedStealDuration )
activeStealTasks[ player.uniqueId ] = task activeStealTasks[ player.uniqueId ] = task
player.playSound( player.location, Sound.ENTITY_EVOKER_CAST_SPELL, 1f, 1.5f ) player.playSound( player.location, Sound.ENTITY_EVOKER_CAST_SPELL, 1f, 1.5f )
player.sendActionBar(player.trans( "kits.goblin.messages.stole_kit", mapOf(), "kit" to targetKit.displayName )) player.sendActionBar(
player.trans( "kits.goblin.messages.stole_kit", mapOf(), "kit" to targetKit.displayName )
)
return AbilityResult.Success return AbilityResult.Success
} }
@@ -180,7 +225,8 @@ class GoblinKit : Kit() {
} }
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -206,10 +252,13 @@ class GoblinKit : Kit() {
val world = player.world val world = player.world
val location = player.location val location = player.location
// Snapshot the radius at activation time
val capturedBunkerRadius = bunkerRadius
WorldEditUtils.createSphere( WorldEditUtils.createSphere(
world, world,
location, location,
kitOverride.bunkerRadius, capturedBunkerRadius,
false, false,
Material.MOSSY_COBBLESTONE Material.MOSSY_COBBLESTONE
) )
@@ -218,21 +267,22 @@ class GoblinKit : Kit() {
WorldEditUtils.createSphere( WorldEditUtils.createSphere(
world, world,
location, location,
kitOverride.bunkerRadius, capturedBunkerRadius,
false, false,
Material.AIR Material.AIR
) )
}, 20L * 15 ) }, 20L * 15 )
player.playSound( player.location, Sound.BLOCK_PISTON_EXTEND, 1f, 0.8f ) player.playSound( player.location, Sound.BLOCK_PISTON_EXTEND, 1f, 0.8f )
player.sendActionBar(player.trans( "kits.goblin.messages.spawn_bunker" )) player.sendActionBar( player.trans( "kits.goblin.messages.spawn_bunker" ) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
private class AggressiveNoPassive : PassiveAbility( Playstyle.AGGRESSIVE ) { private class AggressiveNoPassive : PassiveAbility( Playstyle.AGGRESSIVE )
{
override val name: String override val name: String
get() = "None" get() = "None"
@@ -242,7 +292,8 @@ class GoblinKit : Kit() {
} }
private class DefensiveNoPassive : PassiveAbility( Playstyle.DEFENSIVE ) { private class DefensiveNoPassive : PassiveAbility( Playstyle.DEFENSIVE )
{
override val name: String override val name: String
get() = "None" get() = "None"

View File

@@ -20,7 +20,28 @@ import org.bukkit.potion.PotionEffectType
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class IceMageKit : Kit() { /**
* ## IceMageKit
*
* | Playstyle | Active | Passive |
* |-------------|-------------------------------------------------------------------|-----------------------------------------------------|
* | AGGRESSIVE | | Speed I in ice biomes; [slowChance] Slowness on hit |
* | DEFENSIVE | **Snowball** throws a 360° ring of frozen snowballs | |
*
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
*
* All values read from the `extras` map with companion-object defaults as fallback.
*
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |---------------------|--------|---------|-----------------------------------------------------------------|
* | `slow_chance_denom` | Int | `3` | `1-in-N` chance to apply Slowness on melee hit (aggressive) |
* | `snowball_count` | Int | `16` | Number of snowballs fired in the 360° ring (defensive) |
* | `snowball_speed` | Double | `1.5` | Launch speed of each snowball |
* | `freeze_ticks` | Int | `60` | Freeze ticks applied to enemies hit by a snowball |
* | `slow_ticks` | Int | `40` | Slowness II ticks applied to enemies hit by a snowball |
*/
class IceMageKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -36,6 +57,52 @@ class IceMageKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.SNOWBALL get() = Material.SNOWBALL
companion object {
const val DEFAULT_SLOW_CHANCE_DENOM = 3
const val DEFAULT_SNOWBALL_COUNT = 16
const val DEFAULT_SNOWBALL_SPEED = 1.5
const val DEFAULT_FREEZE_TICKS = 60
const val DEFAULT_SLOW_TICKS = 40
}
// ── Live config accessors ─────────────────────────────────────────────────
/**
* Denominator of the `1-in-N` Slowness-on-hit chance for the aggressive passive.
* A value of `3` means roughly a 33 % chance.
* JSON key: `slow_chance_denom`
*/
private val slowChanceDenom: Int
get() = override().getInt( "slow_chance_denom" ) ?: DEFAULT_SLOW_CHANCE_DENOM
/**
* Number of snowballs launched in the 360° ring by the defensive active.
* JSON key: `snowball_count`
*/
private val snowballCount: Int
get() = override().getInt( "snowball_count" ) ?: DEFAULT_SNOWBALL_COUNT
/**
* Launch speed of each snowball in the ring.
* JSON key: `snowball_speed`
*/
private val snowballSpeed: Double
get() = override().getDouble( "snowball_speed" ) ?: DEFAULT_SNOWBALL_SPEED
/**
* Freeze ticks applied to enemies hit by an IceMage snowball.
* JSON key: `freeze_ticks`
*/
private val freezeTicks: Int
get() = override().getInt( "freeze_ticks" ) ?: DEFAULT_FREEZE_TICKS
/**
* Slowness II ticks applied to enemies hit by an IceMage snowball.
* JSON key: `slow_ticks`
*/
private val slowTicks: Int
get() = override().getInt( "slow_ticks" ) ?: DEFAULT_SLOW_TICKS
// ── 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()
@@ -89,7 +156,8 @@ class IceMageKit : Kit() {
items.forEach { player.inventory.remove( it ) } items.forEach { player.inventory.remove( it ) }
} }
private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
override val kitId: String override val kitId: String
get() = "icemage" get() = "icemage"
@@ -115,7 +183,8 @@ class IceMageKit : 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
@@ -139,13 +208,14 @@ class IceMageKit : Kit() {
): AbilityResult ): AbilityResult
{ {
player.playSound( player.location, Sound.ENTITY_PLAYER_HURT_FREEZE, 1f, 1.5f ) player.playSound( player.location, Sound.ENTITY_PLAYER_HURT_FREEZE, 1f, 1.5f )
player.sendActionBar(player.trans( "kits.icemage.messages.shoot_snowballs" )) player.sendActionBar( player.trans( "kits.icemage.messages.shoot_snowballs" ) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
private class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE ) { private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val random = Random() private val random = Random()
@@ -172,8 +242,8 @@ class IceMageKit : Kit() {
event: PlayerMoveEvent event: PlayerMoveEvent
) { ) {
val biome = player.world.getBiome( player.location ) val biome = player.world.getBiome( player.location )
if (!biomeList.contains( biome.name.lowercase() )) return if ( !biomeList.contains( biome.name.lowercase() ) ) return
player.addPotionEffect(PotionEffect( PotionEffectType.SPEED, 20, 0 )) player.addPotionEffect( PotionEffect( PotionEffectType.SPEED, 20, 0 ) )
} }
override fun onHitEnemy( override fun onHitEnemy(
@@ -181,13 +251,17 @@ class IceMageKit : Kit() {
victim: Player, victim: Player,
event: EntityDamageByEntityEvent event: EntityDamageByEntityEvent
) { ) {
if (random.nextInt( 3 ) < 1 ) // Snapshot at hit time for consistency
victim.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 60, 0 )) val capturedDenom = slowChanceDenom
if ( random.nextInt( capturedDenom ) < 1 )
victim.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, slowTicks, 0 ) )
} }
} }
private class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) { private class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
{
override val name: String override val name: String
get() = "None" get() = "None"

View File

@@ -19,7 +19,7 @@ import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType import org.bukkit.potion.PotionEffectType
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask
import java.util.UUID import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
/** /**
@@ -27,243 +27,353 @@ import java.util.concurrent.ConcurrentHashMap
* *
* | Playstyle | Fähigkeit | * | Playstyle | Fähigkeit |
* |-------------|----------------------------------------------------------------------------------------| * |-------------|----------------------------------------------------------------------------------------|
* | AGGRESSIVE | **Life Drain** saugt 4 ♥/s pro Gegner in der Nähe (max. 8 ♥, 2 s). Sneak: Cancel. | * | AGGRESSIVE | **Life Drain** saugt [healPerEnemyPerSHp] HP/s pro Gegner (max. [maxTotalHealHp], [drainDurationTicks] ticks). Sneak: Cancel. |
* | DEFENSIVE | **Puppeteer's Fear** Blindness + Slowness III an alle Nahkämpfer für 4 Sekunden. | * | DEFENSIVE | **Puppeteer's Fear** Blindness + Slowness III an alle Nahkämpfer in [fearRadius] Blöcken für [fearDurationTicks] Ticks. |
* *
* ### Cancel-Mechanismus (Aggressive): * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* `onToggleSneak` (Hook in [Kit]) wird aufgerufen, wenn der Spieler die Shift-Taste drückt.
* Falls ein Drain-Task aktiv ist, wird er sofort beendet. Das Laden (Charge-State: CHARGING)
* läuft weiter der Spieler bekommt keine Erstattung, da die Fähigkeit bereits angefangen hat.
* *
* ### Drain-Timing: * All values read from the `extras` map with companion-object defaults as fallback.
* Der Task feuert alle 20 Ticks (= 1 s) genau zweimal (0s + 1s → insgesamt 2 Sekunden). *
* Pro Feuer: `min(8 × numEnemies, 16 totalHealed_hp)` HP wird auf den Caster übertragen. * | JSON-Schlüssel | Typ | Default | Beschreibung |
* Healing: Direkt über `player.health = (player.health + healAmount).coerceAtMost(maxHp)`. * |-----------------------------|--------|---------|------------------------------------------------------------|
* Drain: Jeder Gegner nimmt `DRAIN_HP_PER_ENEMY_PER_SECOND` Schaden. * | `drain_radius` | Double | `7.0` | Radius in blocks in which enemies are drained |
* | `drain_duration_ticks` | Long | `40` | Total drain duration in ticks (2 seconds) |
* | `drain_tick_interval` | Long | `20` | Ticks between each drain pulse |
* | `heal_per_enemy_per_s_hp` | Double | `8.0` | HP healed per nearby enemy per drain pulse |
* | `max_total_heal_hp` | Double | `16.0` | Maximum total HP the caster can heal per activation |
* | `drain_dmg_per_enemy_per_s` | Double | `4.0` | Damage dealt to each enemy per drain pulse |
* | `fear_radius` | Double | `7.0` | Radius in blocks for the Fear ability |
* | `fear_duration_ticks` | Int | `80` | Duration in ticks of Blindness and Slowness from Fear |
*/ */
class PuppetKit : Kit() { class PuppetKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val id = "puppet" override val id: String
override val displayName: Component get() = "puppet"
get() = plugin.languageManager.getDefaultComponent("kits.puppet.name", mapOf())
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList("kits.puppet.lore")
override val icon = Material.PHANTOM_MEMBRANE
// Laufende Drain-Tasks: PlayerUUID → BukkitTask override val displayName: Component
internal val activeDrainTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap() get() = plugin.languageManager.getDefaultComponent( "kits.puppet.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.puppet.lore" )
override val icon: Material
get() = Material.PHANTOM_MEMBRANE
companion object { companion object {
const val DRAIN_RADIUS = 7.0 const val DEFAULT_DRAIN_RADIUS = 7.0
const val DRAIN_DURATION_TICKS = 40L // 2 Sekunden const val DEFAULT_DRAIN_DURATION_TICKS = 40L
const val DRAIN_TICK_INTERVAL = 20L // pro Sekunde einmal const val DEFAULT_DRAIN_TICK_INTERVAL = 20L
const val HEAL_PER_ENEMY_PER_S_HP = 8.0 // 4 Herzen = 8 HP const val DEFAULT_HEAL_PER_ENEMY_PER_S_HP = 8.0
const val MAX_TOTAL_HEAL_HP = 16.0 // 8 Herzen = 16 HP const val DEFAULT_MAX_TOTAL_HEAL_HP = 16.0
const val DRAIN_DMG_PER_ENEMY_PER_S = 4.0 // Gegner verlieren 2 Herzen/s const val DEFAULT_DRAIN_DMG_PER_ENEMY_PER_S = 4.0
const val FEAR_RADIUS = 7.0 const val DEFAULT_FEAR_RADIUS = 7.0
const val FEAR_DURATION_TICKS = 80 // 4 Sekunden const val DEFAULT_FEAR_DURATION_TICKS = 80
} }
// ── Gecachte Instanzen ──────────────────────────────────────────────────── // ── Live config accessors ─────────────────────────────────────────────────
/**
* Radius in blocks in which nearby enemies are included in the drain.
* JSON key: `drain_radius`
*/
private val drainRadius: Double
get() = override().getDouble( "drain_radius" ) ?: DEFAULT_DRAIN_RADIUS
/**
* Total duration of the drain in ticks.
* JSON key: `drain_duration_ticks`
*/
private val drainDurationTicks: Long
get() = override().getLong( "drain_duration_ticks" ) ?: DEFAULT_DRAIN_DURATION_TICKS
/**
* Ticks between each drain pulse (heal + damage tick).
* JSON key: `drain_tick_interval`
*/
private val drainTickInterval: Long
get() = override().getLong( "drain_tick_interval" ) ?: DEFAULT_DRAIN_TICK_INTERVAL
/**
* HP healed per nearby enemy per drain pulse.
* JSON key: `heal_per_enemy_per_s_hp`
*/
private val healPerEnemyPerSHp: Double
get() = override().getDouble( "heal_per_enemy_per_s_hp" ) ?: DEFAULT_HEAL_PER_ENEMY_PER_S_HP
/**
* Maximum total HP the caster can heal across the full drain duration.
* JSON key: `max_total_heal_hp`
*/
private val maxTotalHealHp: Double
get() = override().getDouble( "max_total_heal_hp" ) ?: DEFAULT_MAX_TOTAL_HEAL_HP
/**
* Damage dealt to each drained enemy per pulse.
* JSON key: `drain_dmg_per_enemy_per_s`
*/
private val drainDmgPerEnemyPerS: Double
get() = override().getDouble( "drain_dmg_per_enemy_per_s" ) ?: DEFAULT_DRAIN_DMG_PER_ENEMY_PER_S
/**
* Radius in blocks for the Puppeteer's Fear ability.
* JSON key: `fear_radius`
*/
private val fearRadius: Double
get() = override().getDouble( "fear_radius" ) ?: DEFAULT_FEAR_RADIUS
/**
* Duration in ticks of Blindness and Slowness applied by Fear.
* JSON key: `fear_duration_ticks`
*/
private val fearDurationTicks: Int
get() = override().getInt( "fear_duration_ticks" ) ?: DEFAULT_FEAR_DURATION_TICKS
// ── Running drain tasks: PlayerUUID → BukkitTask ──────────────────────────
internal val activeDrainTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
// ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE) private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
private val defensivePassive = NoPassive(Playstyle.DEFENSIVE) private val defensivePassive = NoPassive( Playstyle.DEFENSIVE )
override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) { // ── Playstyle routing ─────────────────────────────────────────────────────
override fun getActiveAbility(
playstyle: Playstyle
): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
override fun getPassiveAbility(
playstyle: Playstyle
): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
// ── Item distribution ─────────────────────────────────────────────────────
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(player: Player, playstyle: Playstyle) { override fun giveItems(
val (mat, active) = when (playstyle) { player: Player,
playstyle: Playstyle
) {
val ( mat, active ) = when( playstyle )
{
Playstyle.AGGRESSIVE -> Material.PHANTOM_MEMBRANE to aggressiveActive Playstyle.AGGRESSIVE -> Material.PHANTOM_MEMBRANE to aggressiveActive
Playstyle.DEFENSIVE -> Material.BLAZE_ROD to defensiveActive Playstyle.DEFENSIVE -> Material.BLAZE_ROD to defensiveActive
} }
val item = ItemBuilder(mat)
.name(active.name) val item = ItemBuilder( mat )
.lore(listOf(active.description)) .name( active.name )
.lore(listOf( active.description ))
.build() .build()
cachedItems[player.uniqueId] = listOf(item)
player.inventory.addItem(item) cachedItems[ player.uniqueId ] = listOf( item )
player.inventory.addItem( item )
} }
override fun onRemove(player: Player) { // ── Lifecycle hooks ───────────────────────────────────────────────────────
// Laufenden Drain abbrechen (z.B. bei Spielende)
activeDrainTasks.remove(player.uniqueId)?.cancel() override fun onRemove(
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } player: Player
) {
activeDrainTasks.remove( player.uniqueId )?.cancel()
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
} }
/** /**
* Sneak → bricht einen laufenden Drain ab. * Sneak → cancels a running drain.
* Wird von [KitEventDispatcher.onPlayerToggleSneak] aufgerufen. * Dispatched by [KitEventDispatcher.onPlayerToggleSneak].
*/ */
override fun onToggleSneak(player: Player, isSneaking: Boolean) { override fun onToggleSneak(
if (!isSneaking) return player: Player,
val task = activeDrainTasks.remove(player.uniqueId) ?: return isSneaking: Boolean
) {
if ( !isSneaking ) return
val task = activeDrainTasks.remove( player.uniqueId ) ?: return
task.cancel() task.cancel()
player.playSound(player.location, Sound.ENTITY_VEX_HURT, 0.6f, 1.8f) player.playSound( player.location, Sound.ENTITY_VEX_HURT, 0.6f, 1.8f )
player.sendActionBar(player.trans("kits.puppet.messages.drain_cancelled")) player.sendActionBar( player.trans( "kits.puppet.messages.drain_cancelled" ) )
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE active Life Drain // AGGRESSIVE active Life Drain
// ========================================================================= // =========================================================================
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId = "puppet" override val kitId: String
get() = "puppet"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.puppet.items.drain.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.puppet.items.drain.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.puppet.items.drain.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.puppet.items.drain.description" )
override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.PHANTOM_MEMBRANE
override fun execute(player: Player): AbilityResult { override val hardcodedHitsRequired: Int
// Sicherheit: kein doppelter Drain (kann eigentlich nicht passieren, da get() = 15
// Charge in CHARGING-State ist, aber defensiv trotzdem prüfen)
if (activeDrainTasks.containsKey(player.uniqueId))
return AbilityResult.ConditionNotMet("Drain already active!")
// Sofort prüfen ob Gegner in der Nähe sind override val triggerMaterial: Material
val initialEnemies = player.world get() = Material.PHANTOM_MEMBRANE
.getNearbyEntities(player.location, DRAIN_RADIUS, DRAIN_RADIUS, DRAIN_RADIUS)
.filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
if (initialEnemies.isEmpty()) override fun execute(
return AbilityResult.ConditionNotMet( player: Player
plugin.languageManager.getDefaultRawMessage("kits.puppet.messages.no_enemies") ): AbilityResult
) {
// Defensive guard: a double-drain cannot happen in practice because the
// charge enters CHARGING state, but we protect against it explicitly.
if ( activeDrainTasks.containsKey( player.uniqueId ) )
return AbilityResult.ConditionNotMet( "Drain already active!" )
// Snapshot all config values at activation time so a mid-round change
// cannot alter an already-running drain unexpectedly.
val capturedDrainRadius = drainRadius
val capturedDrainDurationTicks = drainDurationTicks
val capturedDrainTickInterval = drainTickInterval
val capturedHealPerEnemy = healPerEnemyPerSHp
val capturedMaxTotalHeal = maxTotalHealHp
val capturedDrainDmg = drainDmgPerEnemyPerS
var totalHealedHp = 0.0 var totalHealedHp = 0.0
var ticksFired = 0 var ticksElapsed = 0L
val task = Bukkit.getScheduler().runTaskTimer(plugin, { -> val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
if ( ticksElapsed >= capturedDrainDurationTicks ||
ticksFired++ totalHealedHp >= capturedMaxTotalHeal )
{
// Task selbst beenden wenn: offline, tot, max Heilung erreicht, Zeit abgelaufen activeDrainTasks.remove( player.uniqueId )?.cancel()
if (!player.isOnline ||
!plugin.gameManager.alivePlayers.contains(player.uniqueId) ||
totalHealedHp >= MAX_TOTAL_HEAL_HP ||
ticksFired * DRAIN_TICK_INTERVAL > DRAIN_DURATION_TICKS) {
activeDrainTasks.remove(player.uniqueId)?.cancel()
return@runTaskTimer return@runTaskTimer
} }
val currentEnemies = player.world ticksElapsed += capturedDrainTickInterval
.getNearbyEntities(player.location, DRAIN_RADIUS, DRAIN_RADIUS, DRAIN_RADIUS)
.filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
if (currentEnemies.isEmpty()) { val enemies = player.world
activeDrainTasks.remove(player.uniqueId)?.cancel() .getNearbyEntities(
return@runTaskTimer player.location,
} capturedDrainRadius,
capturedDrainRadius,
// Heilmenge: 4♥ pro Gegner, gedeckelt auf verbleibendes Maximum capturedDrainRadius
val potentialHeal = HEAL_PER_ENEMY_PER_S_HP * currentEnemies.size
val actualHeal = potentialHeal.coerceAtMost(MAX_TOTAL_HEAL_HP - totalHealedHp)
// Gegner entwässern
currentEnemies.forEach { enemy ->
enemy.damage(DRAIN_DMG_PER_ENEMY_PER_S, player)
// Partikel-Sog: von Gegner zur Puppeteer-Position
enemy.world.spawnParticle(
Particle.CRIMSON_SPORE,
enemy.location.clone().add(0.0, 1.3, 0.0),
8, 0.3, 0.3, 0.3, 0.02
) )
.filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
if ( enemies.isEmpty() ) return@runTaskTimer
enemies.forEach { enemy ->
enemy.damage( capturedDrainDmg, player )
} }
// Caster heilen val remainingHealBudget = capturedMaxTotalHeal - totalHealedHp
val maxHp = player.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.value ?: 20.0 val rawHeal = capturedHealPerEnemy * enemies.size
player.health = (player.health + actualHeal).coerceAtMost(maxHp) val actualHeal = rawHeal.coerceAtMost( remainingHealBudget )
val maxHp = player.getAttribute( Attribute.GENERIC_MAX_HEALTH )?.value ?: 20.0
player.health = ( player.health + actualHeal ).coerceAtMost( maxHp )
totalHealedHp += actualHeal totalHealedHp += actualHeal
// Audio-Visual Feedback // Audio-visual feedback
player.world.spawnParticle( player.world.spawnParticle(
Particle.HEART, Particle.HEART,
player.location.clone().add(0.0, 2.0, 0.0), player.location.clone().add( 0.0, 2.0, 0.0 ),
3, 0.4, 0.2, 0.4, 0.0 3, 0.4, 0.2, 0.4, 0.0
) )
player.playSound(player.location, Sound.ENTITY_GENERIC_DRINK, 0.5f, 0.4f) player.playSound( player.location, Sound.ENTITY_GENERIC_DRINK, 0.5f, 0.4f )
player.sendActionBar( player.sendActionBar(
player.trans( player.trans(
"kits.puppet.messages.draining", "kits.puppet.messages.draining",
"healed" to "%.1f".format(totalHealedHp / 2.0), // in Herzen "healed" to "%.1f".format( totalHealedHp / 2.0 ),
"max" to (MAX_TOTAL_HEAL_HP / 2.0).toInt().toString() "max" to ( capturedMaxTotalHeal / 2.0 ).toInt().toString()
) )
) )
}, 0L, DRAIN_TICK_INTERVAL) }, 0L, capturedDrainTickInterval )
activeDrainTasks[player.uniqueId] = task activeDrainTasks[ player.uniqueId ] = task
player.playSound(player.location, Sound.ENTITY_VEX_AMBIENT, 1f, 0.4f) player.playSound( player.location, Sound.ENTITY_VEX_AMBIENT, 1f, 0.4f )
player.sendActionBar(player.trans("kits.puppet.messages.drain_start")) player.sendActionBar( player.trans( "kits.puppet.messages.drain_start" ) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
// ========================================================================= // =========================================================================
// DEFENSIVE active Puppeteer's Fear (Blindness + Slowness) // DEFENSIVE active Puppeteer's Fear (Blindness + Slowness)
// ========================================================================= // =========================================================================
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 = "puppet" override val kitId: String
get() = "puppet"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.puppet.items.fear.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.puppet.items.fear.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.puppet.items.fear.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.puppet.items.fear.description" )
override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.BLAZE_ROD override val hardcodedHitsRequired: Int
get() = 15
override val triggerMaterial: Material
get() = Material.BLAZE_ROD
override fun execute(
player: Player
): AbilityResult
{
// Snapshot config values at activation time
val capturedFearRadius = fearRadius
val capturedFearDurationTicks = fearDurationTicks
override fun execute(player: Player): AbilityResult {
val targets = player.world val targets = player.world
.getNearbyEntities(player.location, FEAR_RADIUS, FEAR_RADIUS, FEAR_RADIUS) .getNearbyEntities(
player.location,
capturedFearRadius,
capturedFearRadius,
capturedFearRadius
)
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
if (targets.isEmpty()) if ( targets.isEmpty() )
return AbilityResult.ConditionNotMet( return AbilityResult.ConditionNotMet(
plugin.languageManager.getDefaultRawMessage("kits.puppet.messages.no_enemies") plugin.languageManager.getDefaultRawMessage( "kits.puppet.messages.no_enemies" )
) )
targets.forEach { target -> targets.forEach { target ->
target.addPotionEffect( target.addPotionEffect(
PotionEffect(PotionEffectType.BLINDNESS, FEAR_DURATION_TICKS, 0, false, false, true) PotionEffect( PotionEffectType.BLINDNESS, capturedFearDurationTicks, 0, false, false, true )
) )
target.addPotionEffect( target.addPotionEffect(
PotionEffect(PotionEffectType.SLOWNESS, FEAR_DURATION_TICKS, 2, false, false, true) PotionEffect( PotionEffectType.SLOWNESS, capturedFearDurationTicks, 2, false, false, true )
) )
target.sendActionBar(target.trans("kits.puppet.messages.feared")) target.sendActionBar( target.trans( "kits.puppet.messages.feared" ) )
target.world.spawnParticle( target.world.spawnParticle(
Particle.SOUL, Particle.SOUL,
target.location.clone().add(0.0, 1.5, 0.0), target.location.clone().add( 0.0, 1.5, 0.0 ),
15, 0.4, 0.5, 0.4, 0.03 15, 0.4, 0.5, 0.4, 0.03
) )
target.playSound(target.location, Sound.ENTITY_PHANTOM_AMBIENT, 0.8f, 0.3f) target.playSound( target.location, Sound.ENTITY_PHANTOM_AMBIENT, 0.8f, 0.3f )
} }
player.playSound(player.location, Sound.ENTITY_WITHER_SHOOT, 1f, 0.3f) player.playSound( player.location, Sound.ENTITY_WITHER_SHOOT, 1f, 0.3f )
player.sendActionBar( player.sendActionBar(
player.trans( player.trans(
"kits.puppet.messages.fear_cast", "kits.puppet.messages.fear_cast",
@@ -272,10 +382,24 @@ class PuppetKit : Kit() {
) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) { // =========================================================================
override val name = "None" // Shared no-passive placeholder
override val description = "None" // =========================================================================
class NoPassive(
playstyle: Playstyle
) : PassiveAbility( playstyle )
{
override val name: String
get() = "None"
override val description: String
get() = "None"
} }
} }

View File

@@ -1,7 +1,6 @@
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
@@ -24,100 +23,174 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
/** /**
* ## Rattlesnake * ## RattlesnakeKit
* *
* | Playstyle | Active | Passive | * | Playstyle | Active | Passive |
* |-------------|------------------------------------------------|--------------------------------------| * |-------------|-------------------------------------------------|-----------------------------------|
* | AGGRESSIVE | Sneak-charged pounce (310 blocks) | Poison II on pounce-hit | * | AGGRESSIVE | Sneak-charged pounce ([pounceMinRange][pounceMaxRange] blocks) | Poison II on pounce-hit |
* | DEFENSIVE | | 25 % counter-venom on being hit | * | DEFENSIVE | | 25 % counter-venom on being hit |
* *
* Both playstyles receive **Speed II** at game start. * Both playstyles receive **Speed II** permanently at game start.
* *
* ### Pounce mechanics * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* 1. Hold sneak (up to 3 s) → charge builds linearly from 3 to 10 blocks. *
* 2. Right-click the SLIME_BALL to launch. A 1.5 s timeout task is scheduled. * The five pounce-related values are stored as **typed fields** in
* 3. If [onHitEnemy] fires while the player is marked as *pouncing*: apply Poison II * [CustomGameSettings.KitOverride] and are therefore accessed directly as
* to 1 target (before Feast) or 3 targets (after Feast) and clear the flag. * properties rather than through `extras`.
* 4. If the timeout fires first (miss): apply Nausea + Slowness to nearby enemies. *
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |-----------------------|--------|------------|-------------------------------------------------------|
* | `pounce_cooldown_ms` | Long | `20_000` | Cooldown between pounce uses in milliseconds |
* | `pounce_max_sneak_ms` | Long | `3_000` | Maximum sneak duration that contributes to range |
* | `pounce_min_range` | Double | `3.0` | Minimum pounce range (blocks) at zero sneak charge |
* | `pounce_max_range` | Double | `10.0` | Maximum pounce range (blocks) at full sneak charge |
* | `pounce_timeout_ticks`| Long | `30` | Ticks before a pounce-in-flight times out (miss) |
*/ */
class RattlesnakeKit : Kit() { class RattlesnakeKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val id = "rattlesnake" override val id: String
override val displayName: Component get() = "rattlesnake"
get() = plugin.languageManager.getDefaultComponent("kits.rattlesnake.name", mapOf())
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList("kits.rattlesnake.lore")
override val icon = Material.SLIME_BALL
// ── Shared state (accessed by inner ability classes via outer-class reference) ─ override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.rattlesnake.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.rattlesnake.lore" )
override val icon: Material
get() = Material.SLIME_BALL
companion object {
const val DEFAULT_POUNCE_COOLDOWN_MS = 20_000L
const val DEFAULT_MAX_SNEAK_MS = 3_000L
const val DEFAULT_POUNCE_MIN_RANGE = 3.0
const val DEFAULT_POUNCE_MAX_RANGE = 10.0
const val DEFAULT_POUNCE_TIMEOUT_TICKS = 30L
}
// ── Live config accessors (typed KitOverride fields) ──────────────────────
/**
* Cooldown between pounce uses in milliseconds.
* Source: typed field `pounce_cooldown_ms`.
*/
private val pounceCooldownMs: Long
get() = override().pounceCooldownMs
/**
* Maximum sneak duration (ms) whose charge contributes to pounce range.
* Source: typed field `pounce_max_sneak_ms`.
*/
private val maxSneakMs: Long
get() = override().pounceMaxSneakMs
/**
* Minimum pounce range in blocks at zero sneak charge.
* Source: typed field `pounce_min_range`.
*/
private val pounceMinRange: Double
get() = override().pounceMinRange
/**
* Maximum pounce range in blocks at full sneak charge.
* Source: typed field `pounce_max_range`.
*/
private val pounceMaxRange: Double
get() = override().pounceMaxRange
/**
* Ticks after launch before a pounce that hasn't connected is treated as a miss.
* Source: typed field `pounce_timeout_ticks`.
*/
private val pounceTimeoutTicks: Long
get() = override().pounceTimeoutTicks
// ── Shared mutable state (accessed by inner ability classes) ──────────────
internal val sneakStartTimes: MutableMap<UUID, Long> = ConcurrentHashMap() internal val sneakStartTimes: MutableMap<UUID, Long> = ConcurrentHashMap()
internal val pouncingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet() internal val pouncingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap() internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object { // ── Cached ability instances (avoid allocating per event call) ────────────
private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"]
?: CustomGameSettings.KitOverride()
private val POUNCE_COOLDOWN_MS = override().pounceCooldownMs
private val MAX_SNEAK_MS = override().pounceMaxSneakMs
private val MIN_RANGE = override().pounceMinRange
private val MAX_RANGE = override().pounceMaxRange
private val POUNCE_TIMEOUT_TICKS = override().pounceTimeoutTicks
}
// ── Cached ability instances ──────────────────────────────────────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
private val aggressivePassive = AggressivePassive() private val aggressivePassive = AggressivePassive()
private val defensivePassive = DefensivePassive() private val defensivePassive = DefensivePassive()
override fun getActiveAbility (playstyle: Playstyle) = when (playstyle) { // ── Playstyle routing ─────────────────────────────────────────────────────
override fun getActiveAbility(
playstyle: Playstyle
): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
override fun getPassiveAbility(
playstyle: Playstyle
): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
// ── Item distribution ─────────────────────────────────────────────────────
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(player: Player, playstyle: Playstyle) { override fun giveItems(
if (playstyle != Playstyle.AGGRESSIVE) return player: Player,
val item = ItemBuilder(Material.SLIME_BALL) playstyle: Playstyle
.name(aggressiveActive.name) ) {
.lore(listOf(aggressiveActive.description)) if ( playstyle != Playstyle.AGGRESSIVE ) return
val item = ItemBuilder( Material.SLIME_BALL )
.name( aggressiveActive.name )
.lore(listOf( aggressiveActive.description ))
.build() .build()
cachedItems[player.uniqueId] = listOf(item)
player.inventory.addItem(item) cachedItems[ player.uniqueId ] = listOf( item )
player.inventory.addItem( item )
} }
override fun onAssign(player: Player, playstyle: Playstyle) { // ── Lifecycle hooks ───────────────────────────────────────────────────────
override fun onAssign(
player: Player,
playstyle: Playstyle
) {
player.addPotionEffect( player.addPotionEffect(
PotionEffect(PotionEffectType.SPEED, Int.MAX_VALUE, 1, false, false, true) PotionEffect( PotionEffectType.SPEED, Int.MAX_VALUE, 1, false, false, true )
) )
} }
override fun onRemove(player: Player) { override fun onRemove(
player.removePotionEffect(PotionEffectType.SPEED) player: Player
sneakStartTimes.remove(player.uniqueId) ) {
pouncingPlayers.remove(player.uniqueId) player.removePotionEffect( PotionEffectType.SPEED )
lastPounceUse.remove(player.uniqueId) sneakStartTimes.remove( player.uniqueId )
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } pouncingPlayers.remove( player.uniqueId )
lastPounceUse.remove( player.uniqueId )
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
} }
override fun onToggleSneak(player: Player, isSneaking: Boolean) { override fun onToggleSneak(
if (plugin.kitManager.getSelectedPlaystyle(player) != Playstyle.AGGRESSIVE) return player: Player,
if (isSneaking) sneakStartTimes[player.uniqueId] = System.currentTimeMillis() isSneaking: Boolean
) {
if ( plugin.kitManager.getSelectedPlaystyle( player ) != Playstyle.AGGRESSIVE ) return
if ( isSneaking ) sneakStartTimes[ player.uniqueId ] = System.currentTimeMillis()
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE active sneak-charged pounce // AGGRESSIVE active sneak-charged pounce
// ========================================================================= // =========================================================================
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -125,151 +198,205 @@ class RattlesnakeKit : Kit() {
get() = "rattlesnake" 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 hardcodedHitsRequired: Int override val hardcodedHitsRequired: Int
get() = 0 get() = 0
override val triggerMaterial = Material.SLIME_BALL
override fun execute(player: Player): AbilityResult { override val triggerMaterial: Material
if (!player.isSneaking) get() = Material.SLIME_BALL
return AbilityResult.ConditionNotMet("Sneak while activating the pounce!")
override fun execute(
player: Player
): AbilityResult
{
if ( !player.isSneaking )
return AbilityResult.ConditionNotMet( "Sneak while activating the pounce!" )
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - (lastPounceUse[player.uniqueId] ?: 0L) < POUNCE_COOLDOWN_MS) {
val remaining = ((POUNCE_COOLDOWN_MS - (now - (lastPounceUse[player.uniqueId] ?: 0L))) / 1000) // Snapshot all config values at activation time
return AbilityResult.ConditionNotMet("Cooldown: ${remaining}s remaining") val capturedPounceCooldownMs = pounceCooldownMs
val capturedMaxSneakMs = maxSneakMs
val capturedPounceMinRange = pounceMinRange
val capturedPounceMaxRange = pounceMaxRange
val capturedPounceTimeoutTicks = pounceTimeoutTicks
if ( now - ( lastPounceUse[ player.uniqueId ] ?: 0L ) < capturedPounceCooldownMs )
{
val remaining = ( capturedPounceCooldownMs - ( now - ( lastPounceUse[ player.uniqueId ] ?: 0L ) ) ) / 1000
return AbilityResult.ConditionNotMet( "Cooldown: ${remaining}s remaining" )
} }
// Sneak duration → range (3 10 blocks) // Sneak duration → range (minmax blocks)
val sneakDuration = (now - (sneakStartTimes[player.uniqueId] ?: now)) val sneakDuration = ( now - ( sneakStartTimes[ player.uniqueId ] ?: now ) )
.coerceIn(0L, MAX_SNEAK_MS) .coerceIn( 0L, capturedMaxSneakMs )
val range = MIN_RANGE + (sneakDuration.toDouble() / MAX_SNEAK_MS) * (MAX_RANGE - MIN_RANGE) val range = capturedPounceMinRange +
( sneakDuration.toDouble() / capturedMaxSneakMs ) * ( capturedPounceMaxRange - capturedPounceMinRange )
val target = player.world val target = player.world
.getNearbyEntities(player.location, range, range, range) .getNearbyEntities( player.location, range, range, range )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.minByOrNull { it.location.distanceSquared(player.location) } .minByOrNull { it.location.distanceSquared( player.location ) }
?: return AbilityResult.ConditionNotMet("No enemies within ${range.toInt()} blocks!") ?: return AbilityResult.ConditionNotMet( "No enemies within ${range.toInt()} blocks!" )
// ── Launch ─────────────────────────────────────────────────────── // ── Launch ───────────────────────────────────────────────────────
val launchVec: Vector = target.location.toVector() val launchVec: Vector = target.location.toVector()
.subtract(player.location.toVector()) .subtract( player.location.toVector() )
.normalize() .normalize()
.multiply(1.9) .multiply( 1.9 )
.setY(0.55) .setY( 0.55 )
player.velocity = launchVec player.velocity = launchVec
player.playSound(player.location, Sound.ENTITY_SLIME_JUMP, 1f, 1.7f) player.playSound( player.location, Sound.ENTITY_SLIME_JUMP, 1f, 1.7f )
pouncingPlayers.add(player.uniqueId) pouncingPlayers.add( player.uniqueId )
lastPounceUse[player.uniqueId] = now lastPounceUse[ player.uniqueId ] = now
// ── Miss timeout ────────────────────────────────────────────────── // ── Miss timeout ──────────────────────────────────────────────────
Bukkit.getScheduler().runTaskLater(plugin, { -> Bukkit.getScheduler().runTaskLater( plugin, { ->
if (!pouncingPlayers.remove(player.uniqueId)) return@runTaskLater // already hit if ( !pouncingPlayers.remove( player.uniqueId ) ) return@runTaskLater // already hit
if ( !player.isOnline ) return@runTaskLater if ( !player.isOnline ) return@runTaskLater
player.world.getNearbyEntities(player.location, 5.0, 5.0, 5.0) player.world.getNearbyEntities( player.location, 5.0, 5.0, 5.0 )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach { enemy -> .forEach { enemy ->
enemy.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 3 * 20, 0)) enemy.addPotionEffect( PotionEffect( PotionEffectType.NAUSEA, 3 * 20, 0 ) )
enemy.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 3 * 20, 0)) enemy.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, 3 * 20, 0 ) )
} }
player.sendActionBar(player.trans("kits.rattlesnake.messages.pounce_miss")) player.sendActionBar( player.trans( "kits.rattlesnake.messages.pounce_miss" ) )
}, POUNCE_TIMEOUT_TICKS) }, capturedPounceTimeoutTicks )
return AbilityResult.Success return AbilityResult.Success
} }
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE passive pounce-hit processing // AGGRESSIVE passive pounce-hit processing
// ========================================================================= // =========================================================================
private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.aggressive.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.rattlesnake.passive.aggressive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.aggressive.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.rattlesnake.passive.aggressive.description" )
/** /**
* Called AFTER the normal damage has been applied. * Called AFTER normal damage has been applied.
* If the attacker is currently pouncing, consume the flag and apply Poison II * If the attacker is currently pouncing, consume the flag and apply Poison II
* to 1 target (before Feast) or up to 3 targets (after Feast). * to 1 target (before Feast) or up to 3 targets (after Feast).
*/ */
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { override fun onHitEnemy(
if (!pouncingPlayers.remove(attacker.uniqueId)) return // not a pounce-hit attacker: Player,
victim: Player,
event: EntityDamageByEntityEvent
) {
if ( !pouncingPlayers.remove( attacker.uniqueId ) ) return // not a pounce-hit
val maxTargets = if (plugin.gameManager.feastManager.hasSpawned) 3 else 1 val maxTargets = if ( plugin.gameManager.feastManager.hasSpawned ) 3 else 1
val targets = buildList { val targets = buildList {
add(victim) add( victim )
if (maxTargets > 1) { if ( maxTargets > 1 )
{
victim.world victim.world
.getNearbyEntities(victim.location, 4.0, 4.0, 4.0) .getNearbyEntities( victim.location, 4.0, 4.0, 4.0 )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != victim && it != attacker && .filter { it != victim && it != attacker &&
plugin.gameManager.alivePlayers.contains(it.uniqueId) } plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.take(maxTargets - 1) .take( maxTargets - 1 )
.forEach { add(it) } .forEach { add( it ) }
} }
} }
targets.forEach { t -> targets.forEach { t ->
t.addPotionEffect(PotionEffect(PotionEffectType.POISON, 8 * 20, 1)) // Poison II t.addPotionEffect( PotionEffect( PotionEffectType.POISON, 8 * 20, 1 ) )
t.world.spawnParticle(Particle.ITEM_SLIME, t.location.clone().add(0.0, 1.0, 0.0), t.world.spawnParticle(
12, 0.4, 0.4, 0.4, 0.0) Particle.ITEM_SLIME,
} t.location.clone().add( 0.0, 1.0, 0.0 ),
12, 0.4, 0.4, 0.4, 0.0
attacker.playSound(attacker.location, Sound.ENTITY_SLIME_ATTACK, 1f, 0.7f)
attacker.sendActionBar(
attacker.trans("kits.rattlesnake.messages.pounce_hit",
mapOf("count" to targets.size.toString()))
) )
} }
attacker.playSound( attacker.location, Sound.ENTITY_SLIME_ATTACK, 1f, 0.7f )
attacker.sendActionBar(
attacker.trans(
"kits.rattlesnake.messages.pounce_hit",
mapOf( "count" to targets.size.toString() )
)
)
}
} }
// ========================================================================= // =========================================================================
// DEFENSIVE active no active ability // DEFENSIVE active no active ability
// ========================================================================= // =========================================================================
private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
override val kitId: String = "rattlesnake" {
override val name = "None"
override val description = "None" override val kitId: String
override val hardcodedHitsRequired: Int = 0 get() = "rattlesnake"
override val triggerMaterial = Material.BARRIER
override fun execute(player: Player) = AbilityResult.Success override val name: String
get() = "None"
override val description: String
get() = "None"
override val hardcodedHitsRequired: Int
get() = 0
override val triggerMaterial: Material
get() = Material.BARRIER
override fun execute(
player: Player
): AbilityResult = AbilityResult.Success
} }
// ========================================================================= // =========================================================================
// DEFENSIVE passive counter-venom (25 % proc on being hit) // DEFENSIVE passive counter-venom (25 % proc on being hit)
// ========================================================================= // =========================================================================
private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val rng = Random() private val rng = Random()
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.defensive.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.rattlesnake.passive.defensive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.passive.defensive.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.rattlesnake.passive.defensive.description" )
override fun onHitByEnemy(victim: Player, attacker: Player, event: EntityDamageByEntityEvent) { override fun onHitByEnemy(
if (rng.nextDouble() >= 0.25) return victim: Player,
attacker: Player,
event: EntityDamageByEntityEvent
) {
if ( rng.nextDouble() >= 0.25 ) return
attacker.addPotionEffect(PotionEffect(PotionEffectType.POISON, 3 * 20, 0)) // Poison I, 3 s attacker.addPotionEffect( PotionEffect( PotionEffectType.POISON, 3 * 20, 0 ) ) // Poison I, 3 s
victim.addPotionEffect(PotionEffect(PotionEffectType.SPEED, 3 * 20, 1)) // Speed II, 3 s victim.addPotionEffect( PotionEffect( PotionEffectType.SPEED, 3 * 20, 1 ) ) // Speed II, 3 s
victim.playSound(victim.location, Sound.ENTITY_SLIME_HURT, 0.8f, 1.8f) victim.playSound( victim.location, Sound.ENTITY_SLIME_HURT, 0.8f, 1.8f )
victim.sendActionBar(victim.trans("kits.rattlesnake.messages.venom_proc")) victim.sendActionBar( victim.trans( "kits.rattlesnake.messages.venom_proc" ) )
} }
} }
} }

View File

@@ -38,8 +38,7 @@ import java.util.concurrent.ConcurrentHashMap
* | Playstyle | Beschreibung | * | Playstyle | Beschreibung |
* |-------------|---------------------------------------------------------------------------------| * |-------------|---------------------------------------------------------------------------------|
* | AGGRESSIVE | Gambeln per Knopfdruck Items, Events oder **Instant Death** möglich | * | AGGRESSIVE | Gambeln per Knopfdruck Items, Events oder **Instant Death** möglich |
* | DEFENSIVE | Öffnet eine Slot-Maschinen-GUI (nur wenn kein Feind in der Nähe) sicherer: | * | DEFENSIVE | Öffnet eine Slot-Maschinen-GUI (nur wenn kein Feind in [safeRadius] Blöcken) |
* | | keine Dia-Armor, kein Instant-Death-Outcome |
* *
* ### Aggressive Outcome-Wahrscheinlichkeiten * ### Aggressive Outcome-Wahrscheinlichkeiten
* | 5 % | Instant Death | * | 5 % | Instant Death |
@@ -48,162 +47,233 @@ import java.util.concurrent.ConcurrentHashMap
* | 20 % | Neutrale Items | * | 20 % | Neutrale Items |
* | 50 % | Positive Items (inkl. möglicher Dia-Armor) | * | 50 % | Positive Items (inkl. möglicher Dia-Armor) |
* *
* ### Defensive Slot-Maschinen-GUI * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* Öffnet sich nur wenn kein Feind in [SAFE_RADIUS] Blöcken ist.
* Gleiche Outcome-Tabelle, ABER ohne Instant-Death und ohne Dia-Armor.
* Die GUI animiert drei Walzen nacheinander, bevor das Ergebnis feststeht.
* *
* ### Integration * All values read from the `extras` map with companion-object defaults as fallback.
* Die [SlotMachineGui] nutzt einen eigenen [InventoryHolder]. Der Click-Dispatch *
* läuft über den zentralen [MenuListener] dafür muss in [MenuListener.onInventoryClick] * | JSON-Schlüssel | Typ | Default | Beschreibung |
* ein zusätzlicher Branch ergänzt werden: * |----------------------|--------|------------|----------------------------------------------------------|
* ```kotlin * | `active_cooldown_ms` | Long | `12_000` | Cooldown between aggressive gamble uses in milliseconds |
* val spieloHolder = event.inventory.holder as? SpieloKit.SlotMachineGui ?: ... * | `safe_radius` | Double | `12.0` | Radius (blocks) checked for enemies before opening GUI |
* spieloHolder.onClick(event)
* ```
*/ */
class SpieloKit : Kit() { class SpieloKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val rng = Random() private val rng = Random()
private val mm = MiniMessage.miniMessage() private val mm = MiniMessage.miniMessage()
override val id = "spielo" override val id: String
get() = "spielo"
override val displayName: Component override val displayName: Component
get() = plugin.languageManager.getDefaultComponent("kits.spielo.name", mapOf()) get() = plugin.languageManager.getDefaultComponent( "kits.spielo.name", mapOf() )
override val lore: List<String> override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList("kits.spielo.lore") get() = plugin.languageManager.getDefaultRawMessageList( "kits.spielo.lore" )
override val icon = Material.GOLD_NUGGET
// Blockiert Doppel-Trigger während eine Animation läuft override val icon: Material
internal val gamblingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet() get() = Material.GOLD_NUGGET
// Cooldowns für den Aggressive-Automaten
private val activeCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object { companion object {
const val ACTIVE_COOLDOWN_MS = 12_000L // 12 s zwischen Aggressive-Uses const val DEFAULT_ACTIVE_COOLDOWN_MS = 12_000L
const val SAFE_RADIUS = 12.0 // Feind-Radius für Defensive-GUI-Sperrung const val DEFAULT_SAFE_RADIUS = 12.0
} }
// ── Gecachte Instanzen ──────────────────────────────────────────────────── // ── Live config accessors ─────────────────────────────────────────────────
/**
* Cooldown between aggressive gamble uses in milliseconds.
* JSON key: `active_cooldown_ms`
*/
private val activeCooldownMs: Long
get() = override().getLong( "active_cooldown_ms" ) ?: DEFAULT_ACTIVE_COOLDOWN_MS
/**
* Radius in blocks checked for enemies before the defensive slot-machine GUI opens.
* If an enemy is within this radius the quick animation fires instead.
* JSON key: `safe_radius`
*/
private val safeRadius: Double
get() = override().getDouble( "safe_radius" ) ?: DEFAULT_SAFE_RADIUS
// ── Kit-level state ───────────────────────────────────────────────────────
/** Blocks double-trigger while an animation is running. */
internal val gamblingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
/** Cooldown timestamps for the aggressive instant-gamble. */
private val activeCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
// ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE) private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
private val defensivePassive = NoPassive(Playstyle.DEFENSIVE) private val defensivePassive = NoPassive( Playstyle.DEFENSIVE )
override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) { // ── Playstyle routing ─────────────────────────────────────────────────────
override fun getActiveAbility(
playstyle: Playstyle
): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
override fun getPassiveAbility(
playstyle: Playstyle
): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
// ── Item distribution ─────────────────────────────────────────────────────
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(player: Player, playstyle: Playstyle) { override fun giveItems(
val (mat, active) = when (playstyle) { player: Player,
playstyle: Playstyle
) {
val ( mat, active ) = when( playstyle )
{
Playstyle.AGGRESSIVE -> Material.GOLD_NUGGET to aggressiveActive Playstyle.AGGRESSIVE -> Material.GOLD_NUGGET to aggressiveActive
Playstyle.DEFENSIVE -> Material.GOLD_BLOCK to defensiveActive Playstyle.DEFENSIVE -> Material.GOLD_BLOCK to defensiveActive
} }
val item = ItemBuilder(mat)
.name(active.name) val item = ItemBuilder( mat )
.lore(listOf(active.description)) .name( active.name )
.lore(listOf( active.description ))
.build() .build()
cachedItems[player.uniqueId] = listOf(item)
player.inventory.addItem(item) cachedItems[ player.uniqueId ] = listOf( item )
player.inventory.addItem( item )
} }
override fun onRemove(player: Player) { // ── Lifecycle hooks ───────────────────────────────────────────────────────
gamblingPlayers.remove(player.uniqueId)
activeCooldowns.remove(player.uniqueId) override fun onRemove(
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } player: Player
) {
gamblingPlayers.remove( player.uniqueId )
activeCooldowns.remove( player.uniqueId )
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE active Sofort-Gamble (Instant-Death möglich) // AGGRESSIVE active Sofort-Gamble (Instant-Death möglich)
// ========================================================================= // =========================================================================
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId = "spielo" override val kitId: String
override val name: String get() = "spielo"
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.automat.name")
override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.automat.description")
override val hardcodedHitsRequired = 12
override val triggerMaterial = Material.GOLD_NUGGET
override fun execute(player: Player): AbilityResult { override val name: String
if (gamblingPlayers.contains(player.uniqueId)) get() = plugin.languageManager.getDefaultRawMessage( "kits.spielo.items.automat.name" )
return AbilityResult.ConditionNotMet("Slotmachine is already running!")
override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.spielo.items.automat.description" )
override val hardcodedHitsRequired: Int
get() = 12
override val triggerMaterial: Material
get() = Material.GOLD_NUGGET
override fun execute(
player: Player
): AbilityResult
{
if ( gamblingPlayers.contains( player.uniqueId ) )
return AbilityResult.ConditionNotMet( "Slotmachine is already running!" )
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val lastUse = activeCooldowns[player.uniqueId] ?: 0L val lastUse = activeCooldowns[ player.uniqueId ] ?: 0L
if (now - lastUse < ACTIVE_COOLDOWN_MS) {
val secLeft = (ACTIVE_COOLDOWN_MS - (now - lastUse)) / 1000 // Snapshot the cooldown at activation time
return AbilityResult.ConditionNotMet("Cooldown: ${secLeft}s") val capturedCooldownMs = activeCooldownMs
if ( now - lastUse < capturedCooldownMs )
{
val secLeft = ( capturedCooldownMs - ( now - lastUse ) ) / 1000
return AbilityResult.ConditionNotMet( "Cooldown: ${secLeft}s" )
} }
activeCooldowns[player.uniqueId] = now activeCooldowns[ player.uniqueId ] = now
gamblingPlayers.add(player.uniqueId) gamblingPlayers.add( player.uniqueId )
// Kurze Sound-Animation (0,8 s) → dann Ergebnis // Short sound animation (≈ 0.9 s) → then resolve
playQuickAnimation(player) { playQuickAnimation( player ) {
gamblingPlayers.remove(player.uniqueId) gamblingPlayers.remove( player.uniqueId )
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@playQuickAnimation if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@playQuickAnimation
resolveOutcome(player, allowInstantDeath = true, allowDiamondArmor = true) resolveOutcome( player, allowInstantDeath = true, allowDiamondArmor = true )
} }
return AbilityResult.Success return AbilityResult.Success
} }
} }
// ========================================================================= // =========================================================================
// DEFENSIVE active Slot-Maschinen-GUI öffnen // DEFENSIVE active Slot-Maschinen-GUI öffnen
// ========================================================================= // =========================================================================
private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId = "spielo" override val kitId: String
override val name: String get() = "spielo"
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.slotautomat.name")
override val description: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.slotautomat.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.spielo.items.slotautomat.name" )
override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.GOLD_BLOCK override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.spielo.items.slotautomat.description" )
override val hardcodedHitsRequired: Int
get() = 15
override val triggerMaterial: Material
get() = Material.GOLD_BLOCK
override fun execute(
player: Player
): AbilityResult
{
if ( gamblingPlayers.contains( player.uniqueId ) )
return AbilityResult.ConditionNotMet( "Slotmachine is already running!" )
// Snapshot the radius at activation time
val capturedSafeRadius = safeRadius
override fun execute(player: Player): AbilityResult {
// Prüfen ob ein Feind zu nah ist
val enemyNearby = plugin.gameManager.alivePlayers val enemyNearby = plugin.gameManager.alivePlayers
.asSequence() .asSequence()
.filter { it != player.uniqueId } .filter { it != player.uniqueId }
.mapNotNull { Bukkit.getPlayer(it) } .mapNotNull { Bukkit.getPlayer( it ) }
.any { it.location.distanceSquared(player.location) <= SAFE_RADIUS * SAFE_RADIUS } .any { it.location.distanceSquared( player.location ) <= capturedSafeRadius * capturedSafeRadius }
if (gamblingPlayers.contains(player.uniqueId)) if ( enemyNearby )
return AbilityResult.ConditionNotMet("Slotmachine is already running!")
if (enemyNearby)
{ {
playQuickAnimation(player) { playQuickAnimation( player ) {
gamblingPlayers.remove(player.uniqueId) gamblingPlayers.remove( player.uniqueId )
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@playQuickAnimation if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@playQuickAnimation
resolveOutcome(player, allowInstantDeath = false, allowDiamondArmor = false) resolveOutcome( player, allowInstantDeath = false, allowDiamondArmor = false )
} }
return AbilityResult.Success return AbilityResult.Success
} }
SlotMachineGui(player).open() SlotMachineGui( player ).open()
return AbilityResult.Success return AbilityResult.Success
} }
} }
// ========================================================================= // =========================================================================
@@ -225,23 +295,20 @@ class SpieloKit : Kit() {
* 2. Spieler klickt Slot 22 ("Drehen") → Animation startet, Button wird gelb. * 2. Spieler klickt Slot 22 ("Drehen") → Animation startet, Button wird gelb.
* 3. Walzen stoppen gestaffelt (Walze 1 → 2 → 3). * 3. Walzen stoppen gestaffelt (Walze 1 → 2 → 3).
* 4. Nach dem letzten Stop: Outcome auflösen, GUI schließen. * 4. Nach dem letzten Stop: Outcome auflösen, GUI schließen.
*
* Der Click-Dispatch muss im [MenuListener] ergänzt werden:
* ```kotlin
* (event.inventory.holder as? SpieloKit.SlotMachineGui)?.onClick(event)
* ```
*/ */
inner class SlotMachineGui(private val player: Player) : InventoryHolder { inner class SlotMachineGui(
private val player: Player
) : InventoryHolder
{
private val inv: Inventory = Bukkit.createInventory( private val inv: Inventory = Bukkit.createInventory(
this, 27, this, 27,
mm.deserialize("<gold><bold>🎰 Slot-Machine</bold></gold>") mm.deserialize( "<gold><bold>🎰 Slot-Machine</bold></gold>" )
) )
private val reelSlots = intArrayOf(11, 13, 15) private val reelSlots = intArrayOf( 11, 13, 15 )
private val spinButton = 22 private val spinButton = 22
// Symbole die auf den Walzen erscheinen (nur visuell kein Einfluss auf Outcome)
private val reelSymbols = listOf( private val reelSymbols = listOf(
Material.GOLD_NUGGET, Material.EMERALD, Material.IRON_INGOT, Material.GOLD_NUGGET, Material.EMERALD, Material.IRON_INGOT,
Material.GOLDEN_APPLE, Material.MUSHROOM_STEW, Material.EXPERIENCE_BOTTLE, Material.GOLDEN_APPLE, Material.MUSHROOM_STEW, Material.EXPERIENCE_BOTTLE,
@@ -253,80 +320,89 @@ class SpieloKit : Kit() {
override fun getInventory(): Inventory = inv override fun getInventory(): Inventory = inv
fun open() { fun open()
{
drawLayout() drawLayout()
player.openInventory(inv) player.openInventory( inv )
} }
private fun drawLayout() { private fun drawLayout()
{
val filler = buildFiller() val filler = buildFiller()
repeat(27) { inv.setItem(it, filler) } repeat( 27 ) { inv.setItem( it, filler ) }
reelSlots.forEach { inv.setItem(it, buildReelItem(reelSymbols.random())) } reelSlots.forEach { inv.setItem( it, buildReelItem( reelSymbols.random() ) ) }
inv.setItem(spinButton, buildSpinButton(spinning = false)) inv.setItem( spinButton, buildSpinButton( spinning = false ) )
} }
// ── Event-Hooks (aufgerufen von MenuListener) ──────────────────────── // ── Event hooks (dispatched from MenuListener) ────────────────────────
fun onClick(event: InventoryClickEvent) { fun onClick(
event: InventoryClickEvent
) {
event.isCancelled = true event.isCancelled = true
if (isSpinning) return if ( isSpinning ) return
if (event.rawSlot != spinButton) return if ( event.rawSlot != spinButton ) return
isSpinning = true isSpinning = true
gamblingPlayers.add(player.uniqueId) gamblingPlayers.add( player.uniqueId )
inv.setItem(spinButton, buildSpinButton(spinning = true)) inv.setItem( spinButton, buildSpinButton( spinning = true ) )
startSpinAnimation() startSpinAnimation()
} }
/** Aufgerufen wenn Inventar geschlossen wird (z.B. ESC). */ /** Called when the inventory is closed (e.g. ESC). */
fun onClose() { fun onClose()
{
lastAnimTask?.cancel() lastAnimTask?.cancel()
// Charge nur zurückgeben wenn noch nicht gedreht wurde // Only refund the lock if the player never actually spun
if (!isSpinning) { if ( !isSpinning ) gamblingPlayers.remove( player.uniqueId )
gamblingPlayers.remove(player.uniqueId) // If isSpinning == true the animation is still running — cleanup in onAllReelsStopped
}
// Wenn isSpinning == true läuft die Animation noch Cleanup in onAllReelsStopped
} }
// ── Animation ───────────────────────────────────────────────────────── // ── Animation ─────────────────────────────────────────────────────────
/** /**
* Startet die gestaffelte Walzen-Animation. * Starts the staggered reel animation.
* Walze 1 stoppt nach 8 Frames, Walze 2 nach 12, Walze 3 nach 16. * Reel 1 stops after 8 frames, reel 2 after 12, reel 3 after 16.
* Jeder Frame dauert 2 Ticks (0,1 s). Starts sind versetzt (+5 Ticks pro Walze). * Each frame takes 2 ticks (0.1 s). Starts are staggered (+5 ticks per reel).
*/ */
private fun startSpinAnimation() { private fun startSpinAnimation()
val framesPerReel = intArrayOf(8, 12, 16) {
val startDelays = longArrayOf(0L, 5L, 10L) val framesPerReel = intArrayOf( 8, 12, 16 )
val startDelays = longArrayOf( 0L, 5L, 10L )
val ticksPerFrame = 2L val ticksPerFrame = 2L
var stoppedReels = 0 var stoppedReels = 0
for (reelIdx in 0..2) { for ( reelIdx in 0..2 )
val slot = reelSlots[reelIdx] {
val maxFrames = framesPerReel[reelIdx] val slot = reelSlots[ reelIdx ]
val maxFrames = framesPerReel[ reelIdx ]
var frame = 0 var frame = 0
val task = object : BukkitRunnable() { val task = object : BukkitRunnable()
{
override fun run() override fun run()
{ {
if (!player.isOnline) { if ( !player.isOnline ) { this.cancel(); return }
this.cancel(); return
}
frame++ frame++
if (frame <= maxFrames) { if ( frame <= maxFrames )
// Zufälliges Walzen-Symbol während Rotation {
inv.setItem(slot, buildReelItem(reelSymbols.random())) inv.setItem( slot, buildReelItem( reelSymbols.random() ) )
val pitch = (0.6f + frame * 0.07f).coerceAtMost(2.0f) val pitch = ( 0.6f + frame * 0.07f ).coerceAtMost( 2.0f )
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.4f, pitch) player.playSound( player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.4f, pitch )
} else { }
// Einrasten finales Symbol (zufällig, rein visuell) else
inv.setItem(slot, buildReelItem(reelSymbols.random())) {
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_CHIME, 0.9f, 1.1f + reelIdx * 0.2f) inv.setItem( slot, buildReelItem( reelSymbols.random() ) )
player.playSound(
player.location,
Sound.BLOCK_NOTE_BLOCK_CHIME,
0.9f, 1.1f + reelIdx * 0.2f
)
stoppedReels++ stoppedReels++
if (stoppedReels == 3) onAllReelsStopped() if ( stoppedReels == 3 ) onAllReelsStopped()
this.cancel() this.cancel()
} }
@@ -337,42 +413,47 @@ class SpieloKit : Kit() {
} }
} }
private fun onAllReelsStopped() { private fun onAllReelsStopped()
player.playSound(player.location, Sound.ENTITY_PLAYER_LEVELUP, 0.7f, 1.5f) {
player.playSound( player.location, Sound.ENTITY_PLAYER_LEVELUP, 0.7f, 1.5f )
// Kurze Pause, dann Outcome auslösen und GUI schließen Bukkit.getScheduler().runTaskLater( plugin, { ->
Bukkit.getScheduler().runTaskLater(plugin, { -> gamblingPlayers.remove( player.uniqueId )
gamblingPlayers.remove(player.uniqueId) if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@runTaskLater
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
player.closeInventory() player.closeInventory()
// Defensive: kein Instant-Death, keine Dia-Armor resolveOutcome( player, allowInstantDeath = false, allowDiamondArmor = false )
resolveOutcome(player, allowInstantDeath = false, allowDiamondArmor = false) }, 20L )
}, 20L)
} }
// ── Item-Builder ───────────────────────────────────────────────────── // ── Item builders ─────────────────────────────────────────────────────
private fun buildReelItem(material: Material) = ItemStack(material).also { item -> private fun buildReelItem(
material: Material
) = ItemStack( material ).also { item ->
item.editMeta { meta -> item.editMeta { meta ->
meta.displayName(Component.text(" ").decoration(TextDecoration.ITALIC, false)) meta.displayName( Component.text( " " ).decoration( TextDecoration.ITALIC, false ) )
} }
} }
private fun buildSpinButton(spinning: Boolean): ItemStack { private fun buildSpinButton(
val mat = if (spinning) Material.YELLOW_CONCRETE else Material.LIME_CONCRETE spinning: Boolean
val name = if (spinning) ): ItemStack
mm.deserialize("<yellow><bold>⟳ Spins...</bold></yellow>") {
val mat = if ( spinning ) Material.YELLOW_CONCRETE else Material.LIME_CONCRETE
val name = if ( spinning )
mm.deserialize( "<yellow><bold>⟳ Spins...</bold></yellow>" )
else else
mm.deserialize("<green><bold>▶ Spin!</bold></green>") mm.deserialize( "<green><bold>▶ Spin!</bold></green>" )
return ItemStack(mat).also { item -> return ItemStack( mat ).also { item ->
item.editMeta { meta -> item.editMeta { meta ->
meta.displayName(name.decoration(TextDecoration.ITALIC, false)) meta.displayName( name.decoration( TextDecoration.ITALIC, false ) )
if (!spinning) { if ( !spinning )
{
meta.lore(listOf( meta.lore(listOf(
Component.empty(), Component.empty(),
mm.deserialize("<gray>Click to spin the reels.") mm.deserialize( "<gray>Click to spin the reels." )
.decoration(TextDecoration.ITALIC, false), .decoration( TextDecoration.ITALIC, false ),
Component.empty() Component.empty()
)) ))
} }
@@ -380,165 +461,196 @@ class SpieloKit : Kit() {
} }
} }
private fun buildFiller() = ItemStack(Material.BLACK_STAINED_GLASS_PANE).also { item -> private fun buildFiller() = ItemStack( Material.BLACK_STAINED_GLASS_PANE ).also { item ->
item.editMeta { meta -> item.editMeta { meta ->
meta.displayName(Component.text(" ").decoration(TextDecoration.ITALIC, false)) meta.displayName( Component.text( " " ).decoration( TextDecoration.ITALIC, false ) )
}
} }
} }
}
// ========================================================================= // =========================================================================
// Outcome-Auflösung gemeinsam für Aggressive und Defensive // Outcome resolution shared by both playstyles
// ========================================================================= // =========================================================================
/** /**
* Löst das Gamble-Ergebnis auf. * Resolves the gamble result.
* @param allowInstantDeath true = Aggressive (5 % Instant Death möglich) * @param allowInstantDeath `true` = aggressive (5 % instant death possible)
* @param allowDiamondArmor true = Aggressive (Dia-Armor in Loot möglich) * @param allowDiamondArmor `true` = aggressive (diamond armor in loot pool)
*/ */
fun resolveOutcome(player: Player, allowInstantDeath: Boolean, allowDiamondArmor: Boolean) { fun resolveOutcome(
player: Player,
allowInstantDeath: Boolean,
allowDiamondArmor: Boolean
) {
val roll = rng.nextDouble() val roll = rng.nextDouble()
when { when
allowInstantDeath && roll < 0.05 -> triggerInstantDeath(player) {
allowInstantDeath && roll < 0.20 -> triggerRandomDisaster(player) allowInstantDeath && roll < 0.05 -> triggerInstantDeath( player )
roll < (if (allowInstantDeath) 0.30 else 0.10) -> applyNegativeEffect(player) allowInstantDeath && roll < 0.20 -> triggerRandomDisaster( player )
roll < (if (allowInstantDeath) 0.50 else 0.30) -> giveNeutralItems(player) roll < ( if ( allowInstantDeath ) 0.30 else 0.10 ) -> applyNegativeEffect( player )
else -> givePositiveItems(player, allowDiamondArmor) roll < ( if ( allowInstantDeath ) 0.50 else 0.30 ) -> giveNeutralItems( player )
else -> givePositiveItems( player, allowDiamondArmor )
} }
} }
// ── Einzelne Outcome-Typen ──────────────────────────────────────────────── // ── Individual outcome types ──────────────────────────────────────────────
private fun triggerInstantDeath(player: Player) { private fun triggerInstantDeath(
player.world.spawnParticle(Particle.EXPLOSION, player.location, 5, 0.5, 0.5, 0.5, 0.0) player: Player
player.world.playSound(player.location, Sound.ENTITY_WITHER_SPAWN, 1f, 1.5f) ) {
player.sendActionBar(player.trans("kits.spielo.messages.instant_death")) player.world.spawnParticle( Particle.EXPLOSION, player.location, 5, 0.5, 0.5, 0.5, 0.0 )
player.world.playSound( player.location, Sound.ENTITY_WITHER_SPAWN, 1f, 1.5f )
player.sendActionBar( player.trans( "kits.spielo.messages.instant_death" ) )
// Einen Tick später töten damit das ActionBar-Paket noch ankommt Bukkit.getScheduler().runTaskLater( plugin, { ->
Bukkit.getScheduler().runTaskLater(plugin, { -> if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@runTaskLater
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
player.health = 0.0 player.health = 0.0
}, 3L) }, 3L )
} }
private fun triggerRandomDisaster(player: Player) { private fun triggerRandomDisaster(
player: Player
) {
val disaster = listOf( val disaster = listOf(
MeteorDisaster(), TornadoDisaster(), EarthquakeDisaster(), ThunderDisaster() MeteorDisaster(), TornadoDisaster(), EarthquakeDisaster(), ThunderDisaster()
).random() ).random()
disaster.warn(player) disaster.warn( player )
Bukkit.getScheduler().runTaskLater(plugin, { -> Bukkit.getScheduler().runTaskLater( plugin, { ->
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@runTaskLater
disaster.trigger(plugin, player) disaster.trigger( plugin, player )
}, disaster.warningDelayTicks) }, disaster.warningDelayTicks )
player.world.playSound(player.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 0.6f) player.world.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 0.6f )
player.sendActionBar(player.trans("kits.spielo.messages.gamble_event")) player.sendActionBar( player.trans( "kits.spielo.messages.gamble_event" ) )
} }
private fun applyNegativeEffect(player: Player) { private fun applyNegativeEffect(
player: Player
) {
val outcomes: List<() -> Unit> = listOf( val outcomes: List<() -> Unit> = listOf(
{ player.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 6 * 20, 1)) }, { player.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, 6 * 20, 1 ) ) },
{ player.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 6 * 20, 1)) }, { player.addPotionEffect( PotionEffect( PotionEffectType.MINING_FATIGUE, 6 * 20, 1 ) ) },
{ player.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 5 * 20, 0)) }, { player.addPotionEffect( PotionEffect( PotionEffectType.NAUSEA, 5 * 20, 0 ) ) },
{ player.addPotionEffect(PotionEffect(PotionEffectType.WEAKNESS, 8 * 20, 0)) }, { player.addPotionEffect( PotionEffect( PotionEffectType.WEAKNESS, 8 * 20, 0 ) ) },
{ player.fireTicks = 4 * 20 } { player.fireTicks = 4 * 20 }
) )
outcomes.random().invoke() outcomes.random().invoke()
player.playSound(player.location, Sound.ENTITY_VILLAGER_NO, 1f, 0.8f) player.playSound( player.location, Sound.ENTITY_VILLAGER_NO, 1f, 0.8f )
player.world.spawnParticle( player.world.spawnParticle(
Particle.ANGRY_VILLAGER, Particle.ANGRY_VILLAGER,
player.location.clone().add(0.0, 2.0, 0.0), player.location.clone().add( 0.0, 2.0, 0.0 ),
8, 0.4, 0.3, 0.4, 0.0 8, 0.4, 0.3, 0.4, 0.0
) )
player.sendActionBar(player.trans("kits.spielo.messages.gamble_bad")) player.sendActionBar( player.trans( "kits.spielo.messages.gamble_bad" ) )
} }
private fun giveNeutralItems(player: Player) { private fun giveNeutralItems(
player: Player
) {
val items = listOf( val items = listOf(
ItemStack(Material.ARROW, rng.nextInt(5) + 3), ItemStack( Material.ARROW, rng.nextInt( 5 ) + 3 ),
ItemStack(Material.BREAD, rng.nextInt(4) + 2), ItemStack( Material.BREAD, rng.nextInt( 4 ) + 2 ),
ItemStack(Material.IRON_INGOT, rng.nextInt(3) + 1), ItemStack( Material.IRON_INGOT, rng.nextInt( 3 ) + 1 ),
ItemStack(Material.COBBLESTONE, rng.nextInt(8) + 4), ItemStack( Material.COBBLESTONE, rng.nextInt( 8 ) + 4 )
) )
player.inventory.addItem(items.random()) player.inventory.addItem( items.random() )
player.playSound(player.location, Sound.ENTITY_ITEM_PICKUP, 0.8f, 1.0f) player.playSound( player.location, Sound.ENTITY_ITEM_PICKUP, 0.8f, 1.0f )
player.sendActionBar(player.trans("kits.spielo.messages.gamble_neutral")) player.sendActionBar( player.trans( "kits.spielo.messages.gamble_neutral" ) )
} }
private fun givePositiveItems(player: Player, allowDiamondArmor: Boolean) { private fun givePositiveItems(
data class LootEntry(val item: ItemStack, val weight: Int) player: Player,
allowDiamondArmor: Boolean
) {
data class LootEntry( val item: ItemStack, val weight: Int )
val pool = buildList { val pool = buildList {
add(LootEntry(ItemStack(Material.MUSHROOM_STEW, 3), 30)) add( LootEntry( ItemStack( Material.MUSHROOM_STEW, 3 ), 30 ) )
add(LootEntry(ItemStack(Material.MUSHROOM_STEW, 5), 15)) add( LootEntry( ItemStack( Material.MUSHROOM_STEW, 5 ), 15 ) )
add(LootEntry(ItemStack(Material.GOLDEN_APPLE), 20)) add( LootEntry( ItemStack( Material.GOLDEN_APPLE ), 20 ) )
add(LootEntry(ItemStack(Material.ENCHANTED_GOLDEN_APPLE), 3)) add( LootEntry( ItemStack( Material.ENCHANTED_GOLDEN_APPLE ), 3 ) )
add(LootEntry(ItemStack(Material.EXPERIENCE_BOTTLE, 5), 12)) add( LootEntry( ItemStack( Material.EXPERIENCE_BOTTLE, 5 ), 12 ) )
add(LootEntry(buildSplashPotion(PotionEffectType.STRENGTH, 200, 0), 8)) add( LootEntry( buildSplashPotion( PotionEffectType.STRENGTH, 200, 0 ), 8 ) )
add(LootEntry(buildSplashPotion(PotionEffectType.SPEED, 400, 0), 8)) add( LootEntry( buildSplashPotion( PotionEffectType.SPEED, 400, 0 ), 8 ) )
add(LootEntry(buildSplashPotion(PotionEffectType.REGENERATION, 160, 1), 8)) add( LootEntry( buildSplashPotion( PotionEffectType.REGENERATION, 160, 1 ), 8 ) )
// Eisen-Rüstung: immer möglich add( LootEntry( ItemStack( Material.IRON_CHESTPLATE ), 4 ) )
add(LootEntry(ItemStack(Material.IRON_CHESTPLATE), 4)) add( LootEntry( ItemStack( Material.IRON_HELMET ), 4 ) )
add(LootEntry(ItemStack(Material.IRON_HELMET), 4)) if ( allowDiamondArmor )
// Dia-Rüstung: nur Aggressive {
if (allowDiamondArmor) { add( LootEntry( ItemStack( Material.DIAMOND_CHESTPLATE ), 2 ) )
add(LootEntry(ItemStack(Material.DIAMOND_CHESTPLATE), 2)) add( LootEntry( ItemStack( Material.DIAMOND_HELMET ), 2 ) )
add(LootEntry(ItemStack(Material.DIAMOND_HELMET), 2))
} }
} }
val totalWeight = pool.sumOf { it.weight } val totalWeight = pool.sumOf { it.weight }
var roll = rng.nextInt(totalWeight) var roll = rng.nextInt( totalWeight )
val chosen = pool.first { entry -> roll -= entry.weight; roll < 0 } val chosen = pool.first { entry -> roll -= entry.weight; roll < 0 }
player.inventory.addItem(chosen.item.clone()) player.inventory.addItem( chosen.item.clone() )
player.playSound(player.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.4f) player.playSound( player.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1.4f )
player.world.spawnParticle( player.world.spawnParticle(
Particle.HAPPY_VILLAGER, Particle.HAPPY_VILLAGER,
player.location.clone().add(0.0, 1.5, 0.0), player.location.clone().add( 0.0, 1.5, 0.0 ),
12, 0.4, 0.4, 0.4, 0.0 12, 0.4, 0.4, 0.4, 0.0
) )
player.sendActionBar(player.trans("kits.spielo.messages.gamble_good")) player.sendActionBar( player.trans( "kits.spielo.messages.gamble_good" ) )
} }
// ========================================================================= // =========================================================================
// Stubs // Shared no-passive placeholder
// ========================================================================= // =========================================================================
class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) { class NoPassive(
override val name = "None" playstyle: Playstyle
override val description = "None" ) : PassiveAbility( playstyle )
{
override val name: String
get() = "None"
override val description: String
get() = "None"
} }
// ========================================================================= // =========================================================================
// Hilfsmethoden // Shared helpers
// ========================================================================= // =========================================================================
/** Klicker-Sounds mit steigendem Pitch, danach Callback. */ /** Click-sounds with rising pitch, then fires [onFinish] callback. */
private fun playQuickAnimation(player: Player, onFinish: () -> Unit) { private fun playQuickAnimation(
for (i in 0..5) { player: Player,
Bukkit.getScheduler().runTaskLater(plugin, { -> onFinish: () -> Unit
if (!player.isOnline) return@runTaskLater ) {
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.9f, 0.5f + i * 0.25f) for ( i in 0..5 )
{
Bukkit.getScheduler().runTaskLater( plugin, { ->
if ( !player.isOnline ) return@runTaskLater
player.playSound( player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.9f, 0.5f + i * 0.25f )
player.world.spawnParticle( player.world.spawnParticle(
Particle.NOTE, Particle.NOTE,
player.location.clone().add(0.0, 2.3, 0.0), player.location.clone().add( 0.0, 2.3, 0.0 ),
1, 0.2, 0.1, 0.2, 0.0 1, 0.2, 0.1, 0.2, 0.0
) )
}, i * 3L) }, i * 3L )
} }
Bukkit.getScheduler().runTaskLater(plugin, Runnable(onFinish), 18L) Bukkit.getScheduler().runTaskLater( plugin, Runnable( onFinish ), 18L )
} }
private fun buildSplashPotion(type: PotionEffectType, duration: Int, amplifier: Int) = private fun buildSplashPotion(
ItemStack(Material.SPLASH_POTION).also { potion -> type: PotionEffectType,
duration: Int,
amplifier: Int
) = ItemStack( Material.SPLASH_POTION ).also { potion ->
potion.editMeta { meta -> potion.editMeta { meta ->
if (meta is org.bukkit.inventory.meta.PotionMeta) if ( meta is org.bukkit.inventory.meta.PotionMeta )
meta.addCustomEffect(PotionEffect(type, duration, amplifier), true) meta.addCustomEffect( PotionEffect( type, duration, amplifier ), true )
} }
} }
} }

View File

@@ -16,8 +16,6 @@ import org.bukkit.Sound
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.scheduler.BukkitTask
import org.bukkit.util.Vector
import java.util.Random import java.util.Random
import java.util.UUID import java.util.UUID
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -28,87 +26,137 @@ import kotlin.math.sin
* ## TeslaKit * ## TeslaKit
* *
* | Playstyle | Active | Passive | * | Playstyle | Active | Passive |
* |-------------|---------------------------------------------------|------------------------------------------------| * |-------------|---------------------------------------------------|--------------------------------------------------|
* | AGGRESSIVE | 5 Blitze im 5-Block-Radius (1.5 ♥ pro Treffer) | Rückschlag + Brandschaden-Aura alle 3 s (klein)| * | AGGRESSIVE | [lightningBoltCount] bolts in [lightningRadius] blocks ([lightningDamage] HP each) | [auraChance] % counter-shock on hit |
* | DEFENSIVE | | Rückschlag + Brandschaden-Aura alle 3 s (groß) | * | DEFENSIVE | | [auraChance] % counter-shock on hit |
* *
* **Höhen-Einschränkung**: Beide Mechaniken deaktivieren sich ab Y > [MAX_HEIGHT_Y] * **Height restriction**: Both mechanics deactivate above Y > [MAX_HEIGHT_Y].
* (~50 Blöcke über Meeresspiegel). Tesla braucht Erdkontakt. * Tesla needs ground contact.
* *
* ### Technische Lösung Visueller Blitz + manueller Schaden": * ### Technical note "Visual lightning + manual damage":
* `world.strikeLightningEffect()` erzeugt nur Partikel/Sound keinen Block-/Entity-Schaden. * `world.strikeLightningEffect()` produces only particles/sound — no block or entity damage.
* Direkt danach werden Spieler im 1,5-Block-Radius per `entity.damage()` manuell getroffen. * Players within 1.5 blocks of the strike are then damaged manually via `entity.damage()`.
* Das verhindert ungewollte Nebeneffekte (Feuer, Dorfbewohner-Schaden, eigener Tod durch * This prevents unwanted side-effects (fire, villager conversion, self-death from random bolts).
* zufälligen Blitzschlag).
* *
* ### Passive Aura: * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* Ein `BukkitRunnable` (gestartet in `onActivate`, gestoppt in `onDeactivate`) prüft alle *
* [AURA_INTERVAL_TICKS] Ticks, ob Gegner in [AURA_RADIUS] Blöcken sind. Falls ja → Velocity-Push * All values read from the `extras` map with companion-object defaults as fallback.
* nach außen + `fireTicks`. Aggressive-Playstyle hat schwächeren Rückschlag, Defensive stärkeren. *
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |-----------------------|--------|---------|--------------------------------------------------------|
* | `lightning_radius` | Double | `5.0` | Radius (blocks) in which bolts are scattered |
* | `lightning_damage` | Double | `3.0` | HP damage per player hit by a bolt |
* | `lightning_bolt_count`| Int | `5` | Number of bolts fired per activation |
* | `bolt_stagger_ticks` | Long | `8` | Ticks between each successive bolt |
* | `aura_chance` | Double | `0.05` | Probability (01) of the counter-shock passive firing |
* | `aura_fire_ticks` | Int | `60` | Fire ticks applied to the attacker by the counter-shock|
*/ */
class TeslaKit : Kit() { class TeslaKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val id: String override val id: String
get() = "tesla" get() = "tesla"
override val displayName: Component override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.tesla.name", mapOf() ) get() = plugin.languageManager.getDefaultComponent( "kits.tesla.name", mapOf() )
override val lore: List<String> override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.tesla.lore" ) get() = plugin.languageManager.getDefaultRawMessageList( "kits.tesla.lore" )
override val icon: Material override val icon: Material
get() = Material.LIGHTNING_ROD get() = Material.LIGHTNING_ROD
companion object { companion object {
/** /** ~50 blocks above sea level (Y ≈ 63 + 50 = 113). Above this both abilities deactivate. */
* ~50 Blöcke über Meeresspiegel ( Y ≈ 63 + 50 = 113 )
* Oberhalb dieser Grenze sind beide Fähigkeiten deaktiviert.
*/
const val MAX_HEIGHT_Y = 113.0 const val MAX_HEIGHT_Y = 113.0
// Aggressive Active const val DEFAULT_LIGHTNING_RADIUS = 5.0
const val LIGHTNING_RADIUS = 5.0 const val DEFAULT_LIGHTNING_DAMAGE = 3.0
const val LIGHTNING_DAMAGE = 3.0 const val DEFAULT_LIGHTNING_BOLT_COUNT = 5
const val LIGHTNING_BOLT_COUNT = 5 const val DEFAULT_BOLT_STAGGER_TICKS = 8L
const val BOLT_STAGGER_TICKS = 8L const val DEFAULT_AURA_CHANCE = 0.05
const val DEFAULT_AURA_FIRE_TICKS = 60
// Passive Aura
const val AURA_CHANCE = 0.05
const val AURA_FIRE_TICKS = 60
} }
// ── Gecachte Instanzen ──────────────────────────────────────────────────── // ── Live config accessors ─────────────────────────────────────────────────
/**
* Radius in blocks within which lightning bolts are randomly scattered.
* JSON key: `lightning_radius`
*/
private val lightningRadius: Double
get() = override().getDouble( "lightning_radius" ) ?: DEFAULT_LIGHTNING_RADIUS
/**
* HP damage dealt to each player struck by a bolt (within 1.5 blocks of impact).
* JSON key: `lightning_damage`
*/
private val lightningDamage: Double
get() = override().getDouble( "lightning_damage" ) ?: DEFAULT_LIGHTNING_DAMAGE
/**
* Total number of bolts fired per activation.
* JSON key: `lightning_bolt_count`
*/
private val lightningBoltCount: Int
get() = override().getInt( "lightning_bolt_count" ) ?: DEFAULT_LIGHTNING_BOLT_COUNT
/**
* Ticks between each successive bolt in the staggered sequence.
* JSON key: `bolt_stagger_ticks`
*/
private val boltStaggerTicks: Long
get() = override().getLong( "bolt_stagger_ticks" ) ?: DEFAULT_BOLT_STAGGER_TICKS
/**
* Probability (0.01.0) that the counter-shock passive fires on being hit.
* JSON key: `aura_chance`
*/
private val auraChance: Double
get() = override().getDouble( "aura_chance" ) ?: DEFAULT_AURA_CHANCE
/**
* Fire ticks applied to the attacker when the counter-shock passive procs.
* JSON key: `aura_fire_ticks`
*/
private val auraFireTicks: Int
get() = override().getInt( "aura_fire_ticks" ) ?: DEFAULT_AURA_FIRE_TICKS
// ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = NoActive(Playstyle.DEFENSIVE) private val defensiveActive = NoActive( Playstyle.DEFENSIVE )
private val aggressivePassive = TeslaPassive( private val aggressivePassive = TeslaPassive( Playstyle.AGGRESSIVE )
playstyle = Playstyle.AGGRESSIVE private val defensivePassive = TeslaPassive( Playstyle.DEFENSIVE )
)
private val defensivePassive = TeslaPassive( // ── Playstyle routing ─────────────────────────────────────────────────────
playstyle = Playstyle.DEFENSIVE
)
override fun getActiveAbility( override fun getActiveAbility(
playstyle: Playstyle playstyle: Playstyle
) = when (playstyle) { ): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility( override fun getPassiveAbility(
playstyle: Playstyle playstyle: Playstyle
) = when (playstyle) { ): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
// ── Item distribution ─────────────────────────────────────────────────────
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems( override fun giveItems(
player: Player, player: Player,
playstyle: Playstyle playstyle: Playstyle
) { ) {
if ( playstyle != Playstyle.AGGRESSIVE ) if ( playstyle != Playstyle.AGGRESSIVE ) return
return
val item = ItemBuilder( Material.LIGHTNING_ROD ) val item = ItemBuilder( Material.LIGHTNING_ROD )
.name( aggressiveActive.name ) .name( aggressiveActive.name )
@@ -119,6 +167,8 @@ class TeslaKit : Kit() {
player.inventory.addItem( item ) player.inventory.addItem( item )
} }
// ── Lifecycle hooks ───────────────────────────────────────────────────────
override fun onRemove( override fun onRemove(
player: Player player: Player
) { ) {
@@ -126,22 +176,27 @@ class TeslaKit : Kit() {
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE active gestaffelte Blitze im Nahbereich // AGGRESSIVE active staggered lightning bolts in melee range
// ========================================================================= // =========================================================================
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
private val rng = Random() private val rng = Random()
override val kitId: String override val kitId: String
get() = "tesla" get() = "tesla"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.items.rod.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.items.rod.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.items.rod.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.items.rod.description" )
override val triggerMaterial: Material override val triggerMaterial: Material
get() = Material.LIGHTNING_ROD get() = Material.LIGHTNING_ROD
override val hardcodedHitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
@@ -156,32 +211,37 @@ class TeslaKit : Kit() {
val world = player.world val world = player.world
repeat( LIGHTNING_BOLT_COUNT ) { index -> // Snapshot all config values at activation time so a mid-round
Bukkit.getScheduler().runTaskLater( plugin, { -> // config change cannot alter a bolt sequence already in flight.
if ( !player.isOnline ) val capturedRadius = lightningRadius
return@runTaskLater val capturedDamage = lightningDamage
val capturedBoltCount = lightningBoltCount
val capturedStaggerTicks = boltStaggerTicks
repeat( capturedBoltCount ) { index ->
Bukkit.getScheduler().runTaskLater( plugin, { ->
if ( !player.isOnline ) return@runTaskLater
// Zufällige Position innerhalb des Radius
val angle = rng.nextDouble() * 2.0 * Math.PI val angle = rng.nextDouble() * 2.0 * Math.PI
val dist = rng.nextDouble() * LIGHTNING_RADIUS val dist = rng.nextDouble() * capturedRadius
val strikeLoc = player.location.clone().add( val strikeLoc = player.location.clone().add(
cos( angle ) * dist, cos( angle ) * dist,
0.0, 0.0,
sin( angle ) * dist sin( angle ) * dist
) )
// Oberfläche bestimmen (Blitze sollen am Boden landen) // Land the bolt on the surface
strikeLoc.y = world.getHighestBlockYAt( strikeLoc ).toDouble() + 1.0 strikeLoc.y = world.getHighestBlockYAt( strikeLoc ).toDouble() + 1.0
// Nur visueller Effekt KEIN Block-/Feuer-Schaden // Visual only — no block/fire damage
world.strikeLightningEffect( strikeLoc ) world.strikeLightningEffect( strikeLoc )
// Manueller Schaden an Spielern im Nahbereich des Einschlags // Manual damage to nearby players
world.getNearbyEntities( strikeLoc, 1.5, 1.5, 1.5 ) world.getNearbyEntities( strikeLoc, 1.5, 1.5, 1.5 )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach { victim -> .forEach { victim ->
victim.damage( LIGHTNING_DAMAGE, player ) victim.damage( capturedDamage, player )
victim.world.spawnParticle( victim.world.spawnParticle(
Particle.ELECTRIC_SPARK, Particle.ELECTRIC_SPARK,
victim.location.clone().add( 0.0, 1.0, 0.0 ), victim.location.clone().add( 0.0, 1.0, 0.0 ),
@@ -194,29 +254,31 @@ class TeslaKit : Kit() {
12, 0.3, 0.2, 0.3, 0.08 12, 0.3, 0.2, 0.3, 0.08
) )
}, index * BOLT_STAGGER_TICKS ) }, index * capturedStaggerTicks )
} }
player.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1f, 1.3f ) player.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1f, 1.3f )
player.sendActionBar(player.trans( "kits.tesla.messages.lightning_cast" )) player.sendActionBar( player.trans( "kits.tesla.messages.lightning_cast" ) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
// ========================================================================= // =========================================================================
// Passive Aura Rückschlag + Brandschaden im Umkreis (beide Playstyles) // Passive counter-shock aura (both playstyles)
// ========================================================================= // =========================================================================
class TeslaPassive( inner class TeslaPassive(
playstyle: Playstyle playstyle: Playstyle
) : PassiveAbility( playstyle ) { ) : PassiveAbility( playstyle )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val rng = Random() private val rng = Random()
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.description" )
@@ -225,23 +287,23 @@ class TeslaKit : Kit() {
attacker: Player, attacker: Player,
event: EntityDamageByEntityEvent event: EntityDamageByEntityEvent
) { ) {
if ( rng.nextDouble() > AURA_CHANCE ) // Snapshot at hit time so a config change mid-round is consistent
return if ( rng.nextDouble() > auraChance ) return
attacker.fireTicks = AURA_FIRE_TICKS val capturedFireTicks = auraFireTicks
attacker.fireTicks = capturedFireTicks
attacker.world.spawnParticle( attacker.world.spawnParticle(
Particle.ELECTRIC_SPARK, Particle.ELECTRIC_SPARK,
attacker.location.clone().add( 0.0, 1.0, 0.0 ), attacker.location.clone().add( 0.0, 1.0, 0.0 ),
10, 0.3, 0.4, 0.3, 0.06 10, 0.3, 0.4, 0.3, 0.06
) )
victim.world.spawnParticle( victim.world.spawnParticle(
Particle.ELECTRIC_SPARK, Particle.ELECTRIC_SPARK,
victim.location.clone().add( 0.0, 1.0, 0.0 ), victim.location.clone().add( 0.0, 1.0, 0.0 ),
6, 0.6, 0.6, 0.6, 0.02 6, 0.6, 0.6, 0.6, 0.02
) )
victim.world.playSound( victim.world.playSound(
victim.location, victim.location,
Sound.ENTITY_LIGHTNING_BOLT_IMPACT, Sound.ENTITY_LIGHTNING_BOLT_IMPACT,
@@ -251,15 +313,34 @@ class TeslaKit : Kit() {
} }
// ── Kein Active für Defensive ───────────────────────────────────────────── // =========================================================================
// Defensive no-active placeholder
// =========================================================================
private class NoActive(
playstyle: Playstyle
) : ActiveAbility( playstyle )
{
override val kitId: String
get() = "tesla"
override val name: String
get() = "None"
override val description: String
get() = "None"
override val hardcodedHitsRequired: Int
get() = 0
override val triggerMaterial: Material
get() = Material.BARRIER
override fun execute(
player: Player
): AbilityResult = AbilityResult.Success
private class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) {
override val kitId = "tesla"
override val name = "None"
override val description = "None"
override val hardcodedHitsRequired = 0
override val triggerMaterial = Material.BARRIER
override fun execute(player: Player) = AbilityResult.Success
} }
} }

View File

@@ -27,63 +27,124 @@ import java.util.concurrent.ConcurrentHashMap
/** /**
* ## TridentKit * ## TridentKit
* *
* | Playstyle | Fähigkeit | * | Playstyle | Active | Passive |
* |-------------|----------------------------------------------------------------------------| * |-------------|-----------------------------------------------------------------------------|------------------------------------------------|
* | AGGRESSIVE | **Dive**: 3 Charges Hochsprung, bei Landung schlägt Blitz ein | * | AGGRESSIVE | **Dive** [maxDiveCharges] charges; launches up, lightning strikes on land | |
* | DEFENSIVE | **Parry**: 20 % Chance Angreifer abprallen + Slowness I (2 s) | * | DEFENSIVE | | **Parry** [parryChance] % bounce + Slowness |
* *
* ### Dive-Mechanismus * ### Dive mechanic
* `hitsRequired = 0` → Fähigkeit ist immer READY; interne [diveCharges] verwalten * `hitsRequired = 0` → ability is always READY; internal [diveCharges] manage
* die 3 Sprünge einer Sequenz. Coodown [SEQUENCE_COOLDOWN_MS] gilt nur zwischen * the per-sequence uses. [sequenceCooldownMs] applies only between full sequences
* vollständigen Sequenzen (wenn alle Charges verbraucht wurden). * (when all charges are exhausted).
* *
* Jeder Charge-Verbrauch startet einen 1-Tick-Monitor: * Each charge consumption starts a 1-tick landing monitor:
* 1. Warte auf Velocity-Wechsel (aufwärts → abwärts) * 1. Wait for velocity to flip (ascending → descending).
* 2. Sobald Block unterhalb solid → [triggerLightningStrike] * 2. Once the block below is solid → [triggerLightningStrike].
* *
* ### Parry-Mechanismus * ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
* [onHitByEnemy] mit 20 % Chance + Dreizack-Check (Haupt- oder Offhand). *
* All values read from the `extras` map with companion-object defaults as fallback.
*
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |-------------------------|--------|------------|------------------------------------------------------|
* | `max_dive_charges` | Int | `3` | Number of dive charges per sequence |
* | `sequence_cooldown_ms` | Long | `25_000` | Cooldown between full sequences in milliseconds |
* | `lightning_radius` | Double | `3.5` | Radius (blocks) of the landing lightning strike |
* | `lightning_damage` | Double | `4.0` | HP damage dealt to enemies hit by the lightning |
* | `parry_chance` | Double | `0.20` | Probability (01) of the parry passive firing |
* | `parry_slowness_ticks` | Int | `40` | Slowness I ticks applied to the attacker on parry |
*/ */
class TridentKit : Kit() { class TridentKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val id: String override val id: String
get() = "trident" get() = "trident"
override val displayName: Component override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.trident.name", mapOf() ) get() = plugin.languageManager.getDefaultComponent( "kits.trident.name", mapOf() )
override val lore: List<String> override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.trident.lore" ) get() = plugin.languageManager.getDefaultRawMessageList( "kits.trident.lore" )
override val icon: Material override val icon: Material
get() = Material.TRIDENT get() = Material.TRIDENT
/** Verbleibende Dive-Charges: 0 = neue Sequenz erforderlich. */ companion object {
const val DEFAULT_MAX_DIVE_CHARGES = 3
const val DEFAULT_SEQUENCE_COOLDOWN_MS = 25_000L
const val DEFAULT_LIGHTNING_RADIUS = 3.5
const val DEFAULT_LIGHTNING_DAMAGE = 4.0
const val DEFAULT_PARRY_CHANCE = 0.20
const val DEFAULT_PARRY_SLOWNESS_TICKS = 40
}
// ── Live config accessors ─────────────────────────────────────────────────
/**
* Number of dive charges granted at the start of each sequence.
* JSON key: `max_dive_charges`
*/
private val maxDiveCharges: Int
get() = override().getInt( "max_dive_charges" ) ?: DEFAULT_MAX_DIVE_CHARGES
/**
* Cooldown in milliseconds between full dive sequences.
* JSON key: `sequence_cooldown_ms`
*/
private val sequenceCooldownMs: Long
get() = override().getLong( "sequence_cooldown_ms" ) ?: DEFAULT_SEQUENCE_COOLDOWN_MS
/**
* Radius in blocks of the lightning strike triggered on landing.
* JSON key: `lightning_radius`
*/
private val lightningRadius: Double
get() = override().getDouble( "lightning_radius" ) ?: DEFAULT_LIGHTNING_RADIUS
/**
* HP damage dealt to each player hit by the landing lightning strike.
* JSON key: `lightning_damage`
*/
private val lightningDamage: Double
get() = override().getDouble( "lightning_damage" ) ?: DEFAULT_LIGHTNING_DAMAGE
/**
* Probability (0.01.0) of the defensive parry passive firing on being hit.
* JSON key: `parry_chance`
*/
private val parryChance: Double
get() = override().getDouble( "parry_chance" ) ?: DEFAULT_PARRY_CHANCE
/**
* Duration in ticks of Slowness I applied to the attacker when parried.
* JSON key: `parry_slowness_ticks`
*/
private val parrySlownessTicks: Int
get() = override().getInt( "parry_slowness_ticks" ) ?: DEFAULT_PARRY_SLOWNESS_TICKS
// ── Kit-level state ───────────────────────────────────────────────────────
/** Remaining dive charges per player: 0 = new sequence required. */
internal val diveCharges: MutableMap<UUID, Int> = ConcurrentHashMap() internal val diveCharges: MutableMap<UUID, Int> = ConcurrentHashMap()
private val diveMonitors: MutableMap<UUID, BukkitTask> = ConcurrentHashMap() private val diveMonitors: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
private val lastSequenceTime: MutableMap<UUID, Long> = ConcurrentHashMap() private val lastSequenceTime: MutableMap<UUID, Long> = ConcurrentHashMap()
/** Players who have recently launched a dive and should not receive fall damage. */ /** Players who have recently launched a dive and must not receive fall damage. */
internal val noFallDamagePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet() internal val noFallDamagePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
companion object { // ── Cached ability instances (avoid allocating per event call) ────────────
const val MAX_DIVE_CHARGES = 3
const val SEQUENCE_COOLDOWN_MS = 25_000L // Cooldown zwischen vollst. Sequenzen
const val LIGHTNING_RADIUS = 3.5 // Blöcke um den Einschlagpunkt
const val LIGHTNING_DAMAGE = 4.0 // 2 Herzen
const val PARRY_CHANCE = 0.20 // 20 %
const val PARRY_SLOWNESS_TICKS = 40 // 2 Sekunden
}
// ── Gecachte Instanzen ────────────────────────────────────────────────────
private val aggressiveActive = DiveActive() private val aggressiveActive = DiveActive()
private val defensiveActive = NoActive( Playstyle.DEFENSIVE ) private val defensiveActive = NoActive( Playstyle.DEFENSIVE )
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE ) private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
private val defensivePassive = ParryPassive() private val defensivePassive = ParryPassive()
// ── Playstyle routing ─────────────────────────────────────────────────────
override fun getActiveAbility( override fun getActiveAbility(
playstyle: Playstyle playstyle: Playstyle
) = when( playstyle ) ): ActiveAbility = when( playstyle )
{ {
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
@@ -91,12 +152,14 @@ class TridentKit : Kit() {
override fun getPassiveAbility( override fun getPassiveAbility(
playstyle: Playstyle playstyle: Playstyle
) = when( playstyle ) ): PassiveAbility = when( playstyle )
{ {
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
// ── Item distribution ─────────────────────────────────────────────────────
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems( override fun giveItems(
@@ -109,7 +172,7 @@ class TridentKit : Kit() {
"kits.trident.item.trident.defensive.name" "kits.trident.item.trident.defensive.name"
val trident = ItemBuilder( Material.TRIDENT ) val trident = ItemBuilder( Material.TRIDENT )
.name(plugin.languageManager.getDefaultRawMessage( nameKey )) .name( plugin.languageManager.getDefaultRawMessage( nameKey ) )
.lore(listOf( .lore(listOf(
plugin.languageManager.getDefaultRawMessage( plugin.languageManager.getDefaultRawMessage(
if ( playstyle == Playstyle.AGGRESSIVE ) if ( playstyle == Playstyle.AGGRESSIVE )
@@ -126,6 +189,8 @@ class TridentKit : Kit() {
player.inventory.addItem( trident ) player.inventory.addItem( trident )
} }
// ── Lifecycle hooks ───────────────────────────────────────────────────────
override fun onRemove( override fun onRemove(
player: Player player: Player
) { ) {
@@ -137,7 +202,7 @@ class TridentKit : Kit() {
} }
// ========================================================================= // =========================================================================
// Dive: Landungs-Monitor // Dive: landing monitor
// ========================================================================= // =========================================================================
private fun startDiveMonitor( private fun startDiveMonitor(
@@ -151,9 +216,9 @@ class TridentKit : Kit() {
val task = Bukkit.getScheduler().runTaskTimer( plugin, { -> val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
elapsed++ elapsed++
// Safety-Timeout: 10 Sekunden // Safety timeout: 10 seconds
if ( elapsed > 200 || !player.isOnline || if ( elapsed > 200 || !player.isOnline ||
!plugin.gameManager.alivePlayers.contains( player.uniqueId )) !plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
{ {
diveMonitors.remove( player.uniqueId )?.cancel() diveMonitors.remove( player.uniqueId )?.cancel()
return@runTaskTimer return@runTaskTimer
@@ -180,7 +245,7 @@ class TridentKit : Kit() {
diveMonitors.remove( player.uniqueId )?.cancel() diveMonitors.remove( player.uniqueId )?.cancel()
} }
} }
}, 4L, 1L ) // 4 Ticks Anlauf (verhindert Sofort-Trigger auf dem Boden) }, 4L, 1L ) // 4-tick head-start prevents instant ground trigger
diveMonitors[ player.uniqueId ] = task diveMonitors[ player.uniqueId ] = task
} }
@@ -196,38 +261,47 @@ class TridentKit : Kit() {
world.spawnParticle( Particle.ELECTRIC_SPARK, loc, 45, 1.2, 0.5, 1.2, 0.2 ) world.spawnParticle( Particle.ELECTRIC_SPARK, loc, 45, 1.2, 0.5, 1.2, 0.2 )
world.spawnParticle( Particle.EXPLOSION, loc, 3, 0.4, 0.2, 0.4, 0.0 ) world.spawnParticle( Particle.EXPLOSION, loc, 3, 0.4, 0.2, 0.4, 0.0 )
world.getNearbyEntities( loc, LIGHTNING_RADIUS, LIGHTNING_RADIUS, LIGHTNING_RADIUS ) // Snapshot at landing time so a mid-sequence config change is consistent
val capturedRadius = lightningRadius
val capturedDamage = lightningDamage
world.getNearbyEntities( loc, capturedRadius, capturedRadius, capturedRadius )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach { enemy -> .forEach { enemy ->
enemy.damage( LIGHTNING_DAMAGE, player ) enemy.damage( capturedDamage, player )
enemy.addPotionEffect(PotionEffect( enemy.addPotionEffect(
PotionEffectType.SLOWNESS, 40, 0, false, false, true PotionEffect( PotionEffectType.SLOWNESS, 40, 0, false, false, true )
)) )
} }
val remaining = diveCharges.getOrDefault( player.uniqueId, 0 ) val remaining = diveCharges.getOrDefault( player.uniqueId, 0 )
val msgKey = if ( remaining > 0 ) "kits.trident.messages.charges_left" val msgKey = if ( remaining > 0 ) "kits.trident.messages.charges_left"
else "kits.trident.messages.sequence_done" else "kits.trident.messages.sequence_done"
player.sendActionBar(player.trans( msgKey, "charges" to remaining.toString() )) player.sendActionBar( player.trans( msgKey, "charges" to remaining.toString() ) )
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE active Dive-Charges // AGGRESSIVE active Dive charges
// ========================================================================= // =========================================================================
private inner class DiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { private inner class DiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val kitId: String override val kitId: String
get() = "trident" get() = "trident"
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.description" )
override val triggerMaterial: Material override val triggerMaterial: Material
get() = Material.TRIDENT get() = Material.TRIDENT
override val hardcodedHitsRequired: Int override val hardcodedHitsRequired: Int
get() = 0 get() = 0
@@ -238,16 +312,20 @@ class TridentKit : Kit() {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val charges = diveCharges.getOrDefault( player.uniqueId, 0 ) val charges = diveCharges.getOrDefault( player.uniqueId, 0 )
// Snapshot config values at activation time
val capturedMaxCharges = maxDiveCharges
val capturedCooldownMs = sequenceCooldownMs
if ( charges <= 0 ) if ( charges <= 0 )
{ {
val lastSeq = lastSequenceTime[ player.uniqueId ] ?: 0L val lastSeq = lastSequenceTime[ player.uniqueId ] ?: 0L
if ( now - lastSeq < SEQUENCE_COOLDOWN_MS ) if ( now - lastSeq < capturedCooldownMs )
{ {
val secLeft = ( SEQUENCE_COOLDOWN_MS - ( now - lastSeq )) / 1000 val secLeft = ( capturedCooldownMs - ( now - lastSeq ) ) / 1000
return AbilityResult.ConditionNotMet("Cooldown: ${secLeft}s") return AbilityResult.ConditionNotMet( "Cooldown: ${secLeft}s" )
} }
lastSequenceTime[ player.uniqueId ] = now lastSequenceTime[ player.uniqueId ] = now
diveCharges[ player.uniqueId ] = MAX_DIVE_CHARGES - 1 diveCharges[ player.uniqueId ] = capturedMaxCharges - 1
} }
else diveCharges[ player.uniqueId ] = charges - 1 else diveCharges[ player.uniqueId ] = charges - 1
@@ -255,19 +333,16 @@ class TridentKit : Kit() {
noFallDamagePlayers.add( player.uniqueId ) noFallDamagePlayers.add( player.uniqueId )
val remaining = diveCharges.getOrDefault( player.uniqueId, 0 ) val remaining = diveCharges.getOrDefault( player.uniqueId, 0 )
player.sendActionBar(player.trans( "kits.trident.messages.dive_launched", "charges" to remaining.toString() )) player.sendActionBar(
player.trans( "kits.trident.messages.dive_launched", "charges" to remaining.toString() )
)
player.world.spawnParticle( player.world.spawnParticle(
Particle.ELECTRIC_SPARK, Particle.ELECTRIC_SPARK,
player.location.clone().add( 0.0, 0.5, 0.0 ), player.location.clone().add( 0.0, 0.5, 0.0 ),
15, 0.3, 0.2, 0.3, 0.12 15, 0.3, 0.2, 0.3, 0.12
) )
player.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 0.7f, 1.6f )
player.playSound(
player.location,
Sound.ENTITY_LIGHTNING_BOLT_IMPACT,
0.7f, 1.6f
)
startDiveMonitor( player ) startDiveMonitor( player )
return AbilityResult.Success return AbilityResult.Success
@@ -276,69 +351,98 @@ class TridentKit : Kit() {
} }
// ========================================================================= // =========================================================================
// DEFENSIVE passive Parry (20 %) // DEFENSIVE passive Parry
// ========================================================================= // =========================================================================
private inner class ParryPassive : PassiveAbility( Playstyle.DEFENSIVE ) { private inner class ParryPassive : PassiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val rng = Random() private val rng = Random()
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.trident.passive.defensive.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.passive.defensive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.trident.passive.defensive.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.passive.defensive.description" )
override fun onHitByEnemy( override fun onHitByEnemy(
victim: Player, victim: Player,
attacker: Player, attacker: Player,
event: EntityDamageByEntityEvent event: EntityDamageByEntityEvent
) { ) {
if ( rng.nextDouble() >= PARRY_CHANCE ) return // Snapshot at hit time
if ( rng.nextDouble() >= parryChance ) return
val mainType = victim.inventory.itemInMainHand.type val mainType = victim.inventory.itemInMainHand.type
val offType = victim.inventory.itemInOffHand.type val offType = victim.inventory.itemInOffHand.type
if ( mainType != Material.TRIDENT && offType != Material.TRIDENT ) return if ( mainType != Material.TRIDENT && offType != Material.TRIDENT ) return
val capturedSlownessTicks = parrySlownessTicks
attacker.velocity = attacker.location.toVector() attacker.velocity = attacker.location.toVector()
.subtract( victim.location.toVector() ) .subtract( victim.location.toVector() )
.normalize() .normalize()
.multiply( 1.7 ) .multiply( 1.7 )
.setY( 0.45 ) .setY( 0.45 )
attacker.addPotionEffect(PotionEffect( attacker.addPotionEffect(
PotionEffectType.SLOWNESS, PARRY_SLOWNESS_TICKS, 0, false, false, true PotionEffect( PotionEffectType.SLOWNESS, capturedSlownessTicks, 0, false, false, true )
)) )
victim.world.spawnParticle( victim.world.spawnParticle(
Particle.SWEEP_ATTACK, Particle.SWEEP_ATTACK,
victim.location.clone().add( 0.0, 1.0, 0.0 ), victim.location.clone().add( 0.0, 1.0, 0.0 ),
6, 0.3, 0.3, 0.3, 0.0 6, 0.3, 0.3, 0.3, 0.0
) )
victim.world.playSound( victim.location, Sound.ITEM_SHIELD_BLOCK, 1f, 0.65f ) victim.world.playSound( victim.location, Sound.ITEM_SHIELD_BLOCK, 1f, 0.65f )
victim.world.playSound( victim.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 0.3f, 1.9f ) victim.world.playSound( victim.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 0.3f, 1.9f )
victim.sendActionBar(victim.trans( "kits.trident.messages.parry_success" )) victim.sendActionBar( victim.trans( "kits.trident.messages.parry_success" ) )
attacker.sendActionBar(attacker.trans( "kits.trident.messages.parried_by_victim" )) attacker.sendActionBar( attacker.trans( "kits.trident.messages.parried_by_victim" ) )
} }
} }
// ── Stubs ──────────────────────────────────────────────────────────────── // ── Stubs ────────────────────────────────────────────────────────────────
private class NoActive(
playstyle: Playstyle
) : ActiveAbility( playstyle )
{
override val kitId: String
get() = "trident"
override val name: String
get() = "None"
override val description: String
get() = "None"
override val hardcodedHitsRequired: Int
get() = 0
override val triggerMaterial: Material
get() = Material.BARRIER
override fun execute(
player: Player
): AbilityResult = AbilityResult.Success
private class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) {
override val kitId = "trident"
override val name = "None"
override val description = "None"
override val hardcodedHitsRequired = 0
override val triggerMaterial = Material.BARRIER
override fun execute(player: Player) = AbilityResult.Success
} }
private class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) { private class NoPassive(
override val name = "None" playstyle: Playstyle
override val description = "None" ) : PassiveAbility( playstyle )
{
override val name: String
get() = "None"
override val description: String
get() = "None"
} }
} }

View File

@@ -1,7 +1,6 @@
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
@@ -26,10 +25,29 @@ import java.util.concurrent.ConcurrentHashMap
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
class VenomKit : Kit() { /**
* ## VenomKit
*
* | Playstyle | Active | Passive |
* |-------------|--------------------------------------------------------------|-------------------------------------------------|
* | AGGRESSIVE | **Deafening Beam** dragon-breath raycast, Blindness + Wither | |
* | DEFENSIVE | **Shield of Darkness** absorbs [shieldCapacity] damage for [shieldDurationTicks] ticks | Absorb hits while shield is active |
*
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
*
* The two shield parameters are **typed fields** in [CustomGameSettings.KitOverride]
* and are therefore accessed directly rather than through `extras`.
*
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |-------------------------|--------|---------|-------------------------------------------------------|
* | `shield_duration_ticks` | Long | `160` | Ticks the shield stays active before expiring |
* | `shield_capacity` | Double | `15.0` | Total damage the shield can absorb before breaking |
*/
class VenomKit : Kit()
{
data class ActiveShield( data class ActiveShield(
var remainingCapacity: Double = 15.0, var remainingCapacity: Double,
val expireTask: BukkitTask, val expireTask: BukkitTask,
val particleTask: BukkitTask val particleTask: BukkitTask
) )
@@ -49,11 +67,27 @@ class VenomKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.SPIDER_EYE get() = Material.SPIDER_EYE
private val kitOverride: CustomGameSettings.KitOverride by lazy { companion object {
plugin.customGameManager.settings.kits.kits["venom"] const val DEFAULT_SHIELD_DURATION_TICKS = 160L
?: CustomGameSettings.KitOverride() const val DEFAULT_SHIELD_CAPACITY = 15.0
} }
// ── Live config accessors (typed KitOverride fields) ──────────────────────
/**
* Ticks the shield stays active before expiring automatically.
* Source: typed field `shield_duration_ticks`.
*/
private val shieldDurationTicks: Long
get() = override().shieldDurationTicks
/**
* Total damage the shield absorbs before it breaks.
* Source: typed field `shield_capacity`.
*/
private val shieldCapacity: Double
get() = override().shieldCapacity
// ── 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()
@@ -112,7 +146,7 @@ class VenomKit : Kit() {
} }
} }
// ── Optional lifecycle hooks ────────────────────────────────────────────── // ── Lifecycle hooks ───────────────────────────────────────────────────────
override fun onRemove( override fun onRemove(
player: Player player: Player
@@ -122,11 +156,15 @@ class VenomKit : Kit() {
shield.particleTask.cancel() shield.particleTask.cancel()
activeShields.remove( player.uniqueId ) activeShields.remove( player.uniqueId )
} }
val items = cachedItems.remove( player.uniqueId ) ?: return cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
items.forEach { player.inventory.remove( it ) }
} }
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { // =========================================================================
// AGGRESSIVE active Deafening Beam
// =========================================================================
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -166,13 +204,18 @@ class VenomKit : Kit() {
player.world.playSound( target.location, Sound.ENTITY_DRAGON_FIREBALL_EXPLODE, 1f, 0.8f ) player.world.playSound( target.location, Sound.ENTITY_DRAGON_FIREBALL_EXPLODE, 1f, 0.8f )
} }
player.sendActionBar(player.trans( "kits.venom.messages.wither_beam" )) player.sendActionBar( player.trans( "kits.venom.messages.wither_beam" ) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { // =========================================================================
// DEFENSIVE active Shield of Darkness
// =========================================================================
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -195,20 +238,25 @@ class VenomKit : Kit() {
player: Player player: Player
): AbilityResult ): AbilityResult
{ {
if (activeShields.containsKey( player.uniqueId )) if ( activeShields.containsKey( player.uniqueId ) )
return AbilityResult.ConditionNotMet( "Shield is already active!" ) return AbilityResult.ConditionNotMet( "Shield is already active!" )
// Snapshot config values at activation time so a mid-round change
// does not alter an already-running shield's duration or capacity.
val capturedDurationTicks = shieldDurationTicks
val capturedCapacity = shieldCapacity
player.playSound( player.location, Sound.ENTITY_BLAZE_AMBIENT, 1f, 0.5f ) player.playSound( player.location, Sound.ENTITY_BLAZE_AMBIENT, 1f, 0.5f )
val particleTask = object : BukkitRunnable() { val particleTask = object : BukkitRunnable()
{
var rotation = 0.0 var rotation = 0.0
override fun run() override fun run()
{ {
if ( !player.isOnline || if ( !player.isOnline ||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ) || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ||
!activeShields.containsKey( player.uniqueId )) !activeShields.containsKey( player.uniqueId ) )
{ {
this.cancel() this.cancel()
return return
@@ -233,27 +281,33 @@ class VenomKit : Kit() {
} }
}.runTaskTimer( plugin, 0L, 2L ) }.runTaskTimer( plugin, 0L, 2L )
val expireTask = object : BukkitRunnable() { val expireTask = object : BukkitRunnable()
{
override fun run() override fun run()
{ {
if (activeShields.containsKey( player.uniqueId )) if ( activeShields.containsKey( player.uniqueId ) )
breakShield( player ) breakShield( player )
} }
}.runTaskLater( plugin, kitOverride.shieldDurationTicks ) }.runTaskLater( plugin, capturedDurationTicks )
activeShields[ player.uniqueId ] = ActiveShield( activeShields[ player.uniqueId ] = ActiveShield(
remainingCapacity = kitOverride.shieldCapacity, remainingCapacity = capturedCapacity,
expireTask = expireTask, expireTask = expireTask,
particleTask = particleTask particleTask = particleTask
) )
player.sendActionBar(player.trans( "kits.venom.messages.shield_activate" )) player.sendActionBar( player.trans( "kits.venom.messages.shield_activate" ) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) { // =========================================================================
// DEFENSIVE passive absorb incoming damage while shield is active
// =========================================================================
private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -268,16 +322,21 @@ class VenomKit : Kit() {
attacker: Player, attacker: Player,
event: EntityDamageByEntityEvent event: EntityDamageByEntityEvent
) { ) {
activeShields[victim.uniqueId]?.apply { activeShields[ victim.uniqueId ]?.apply {
remainingCapacity -= event.damage remainingCapacity -= event.damage
event.damage = if (event.isCritical) 3.0 else 2.0 event.damage = if ( event.isCritical ) 3.0 else 2.0
if (remainingCapacity <= 0) breakShield(victim) if ( remainingCapacity <= 0 ) breakShield( victim )
} ?: return } ?: return
} }
} }
private class NoPassive : PassiveAbility( Playstyle.AGGRESSIVE ) { // =========================================================================
// AGGRESSIVE passive stub (no passive ability)
// =========================================================================
private class NoPassive : PassiveAbility( Playstyle.AGGRESSIVE )
{
override val name: String override val name: String
get() = "None" get() = "None"
@@ -287,7 +346,7 @@ class VenomKit : Kit() {
} }
// ── Helper methods ────────────────────────────────────────────── // ── Shared helpers ────────────────────────────────────────────────────────
private fun breakShield( private fun breakShield(
player: Player player: Player
@@ -298,7 +357,7 @@ class VenomKit : Kit() {
shield.particleTask.cancel() shield.particleTask.cancel()
player.world.playSound( player.location, Sound.ENTITY_WITHER_BREAK_BLOCK, 1f, 1f ) player.world.playSound( player.location, Sound.ENTITY_WITHER_BREAK_BLOCK, 1f, 1f )
player.sendActionBar(player.trans( "kits.venom.messages.shield_break" )) player.sendActionBar( player.trans( "kits.venom.messages.shield_break" ) )
} }
} }

View File

@@ -1,7 +1,6 @@
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
@@ -26,83 +25,130 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
/** /**
* ## Voodoo * ## VoodooKit
* *
* | Playstyle | Active | Passive | * | Playstyle | Active | Passive |
* |-------------|-----------------------------------------------------|---------------------------------------------| * |-------------|----------------------------------------------------------|------------------------------------------------|
* | AGGRESSIVE | Root enemy if HP < 50 % (5 s hold) | 20 % chance to apply Wither on hit | * | AGGRESSIVE | Root enemy if HP < 50 % (5 s hold) | 20 % chance to apply Wither on hit |
* | DEFENSIVE | Curse nearby enemies for 15 s (Slow + MiningFatigue)| Speed + Regen while cursed enemies are nearby| * | DEFENSIVE | Curse nearby enemies for [curseDurationMs] ms | Speed + Regen while cursed enemies are nearby |
* *
* ### Root mechanic (AGGRESSIVE) * ### Root mechanic (AGGRESSIVE)
* Zeros horizontal velocity every tick for 5 s + applies Slowness 127 so the * Zeroes horizontal velocity every tick for 5 s + applies Slowness 127 so the
* player cannot walk even if the velocity reset misses a frame. * player cannot walk even if the velocity reset misses a frame.
* *
* ### Curse mechanic (DEFENSIVE) * ### Curse mechanic (DEFENSIVE)
* Cursed players are stored in [cursedExpiry] (UUID → expiry timestamp). * Cursed players are stored in [cursedExpiry] (UUID → expiry timestamp ms).
* A per-player repeating task (started in [DefensivePassive.onActivate]) checks * A per-player repeating task checks every second: cleans expired curses,
* every second: cleans expired curses, applies debuffs to still-cursed enemies, * applies Slowness + MiningFatigue to still-cursed enemies, and grants
* and grants Speed + Regen to the Voodoo player if at least one cursed enemy * Speed + Regen to the Voodoo player if at least one cursed enemy is within
* is within 10 blocks. * 10 blocks.
*
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
*
* The curse duration is a **typed field** in [CustomGameSettings.KitOverride]
* and is therefore accessed directly rather than through `extras`.
*
* | JSON-Schlüssel | Typ | Default | Beschreibung |
* |---------------------|------|------------|-----------------------------------------------|
* | `curse_duration_ms` | Long | `15_000` | How long a curse lasts in milliseconds |
*/ */
class VoodooKit : Kit() { class VoodooKit : Kit()
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
override val id = "voodoo" override val id: String
get() = "voodoo"
override val displayName: Component override val displayName: Component
get() = plugin.languageManager.getDefaultComponent("kits.voodoo.name", mapOf()) get() = plugin.languageManager.getDefaultComponent( "kits.voodoo.name", mapOf() )
override val lore: List<String> override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList("kits.voodoo.lore") get() = plugin.languageManager.getDefaultRawMessageList( "kits.voodoo.lore" )
override val icon = Material.WITHER_ROSE
/** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */ override val icon: Material
internal val cursedExpiry: MutableMap<UUID, Long> = ConcurrentHashMap() get() = Material.WITHER_ROSE
private val kitOverride: CustomGameSettings.KitOverride by lazy { companion object {
plugin.customGameManager.settings.kits.kits["voodoo"] const val DEFAULT_CURSE_DURATION_MS = 15_000L
?: CustomGameSettings.KitOverride()
} }
// ── Cached ability instances ────────────────────────────────────────────── // ── Live config accessor (typed KitOverride field) ────────────────────────
/**
* Duration of each applied curse in milliseconds.
* Source: typed field `curse_duration_ms`.
*/
private val curseDurationMs: Long
get() = override().curseDurationMs
// ── Kit-level state ───────────────────────────────────────────────────────
/** Tracks active curses: victim UUID → expiry timestamp (ms). */
internal val cursedExpiry: MutableMap<UUID, Long> = ConcurrentHashMap()
// ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive() private val defensiveActive = DefensiveActive()
private val aggressivePassive = AggressivePassive() private val aggressivePassive = AggressivePassive()
private val defensivePassive = DefensivePassive() private val defensivePassive = DefensivePassive()
override fun getActiveAbility (playstyle: Playstyle) = when (playstyle) { // ── Playstyle routing ─────────────────────────────────────────────────────
override fun getActiveAbility(
playstyle: Playstyle
): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive Playstyle.DEFENSIVE -> defensiveActive
} }
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
override fun getPassiveAbility(
playstyle: Playstyle
): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive Playstyle.DEFENSIVE -> defensivePassive
} }
// ── Item distribution ─────────────────────────────────────────────────────
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>() override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(player: Player, playstyle: Playstyle) { override fun giveItems(
val (mat, active) = when (playstyle) { player: Player,
playstyle: Playstyle
) {
val ( mat, active ) = when( playstyle )
{
Playstyle.AGGRESSIVE -> Material.WITHER_ROSE to aggressiveActive Playstyle.AGGRESSIVE -> Material.WITHER_ROSE to aggressiveActive
Playstyle.DEFENSIVE -> Material.SOUL_TORCH to defensiveActive Playstyle.DEFENSIVE -> Material.SOUL_TORCH to defensiveActive
} }
val item = ItemBuilder(mat)
.name(active.name) val item = ItemBuilder( mat )
.lore(listOf(active.description)) .name( active.name )
.lore(listOf( active.description ))
.build() .build()
cachedItems[player.uniqueId] = listOf(item)
player.inventory.addItem(item) cachedItems[ player.uniqueId ] = listOf( item )
player.inventory.addItem( item )
} }
override fun onRemove(player: Player) { // ── Lifecycle hooks ───────────────────────────────────────────────────────
cursedExpiry.remove(player.uniqueId)
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } override fun onRemove(
player: Player
) {
cursedExpiry.remove( player.uniqueId )
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE active root enemy if below 50 % HP // AGGRESSIVE active root enemy if below 50 % HP
// ========================================================================= // =========================================================================
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -110,62 +156,78 @@ class VoodooKit : Kit() {
get() = "voodoo" 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 hardcodedHitsRequired: Int override val hardcodedHitsRequired: Int
get() = 15 get() = 15
override val triggerMaterial = Material.WITHER_ROSE
override fun execute(player: Player): AbilityResult { override val triggerMaterial: Material
val target = player.getTargetEntity(6) as? Player get() = Material.WITHER_ROSE
?: return AbilityResult.ConditionNotMet("No player in line of sight!")
if (!plugin.gameManager.alivePlayers.contains(target.uniqueId)) override fun execute(
return AbilityResult.ConditionNotMet("Target is not alive!") player: Player
): AbilityResult
{
val target = player.getTargetEntity( 6 ) as? Player
?: return AbilityResult.ConditionNotMet( "No player in line of sight!" )
val maxHp = target.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.value ?: 20.0 if ( !plugin.gameManager.alivePlayers.contains( target.uniqueId ) )
if (target.health > maxHp * 0.5) return AbilityResult.ConditionNotMet( "Target is not alive!" )
return AbilityResult.ConditionNotMet("Target must be below 50 % health!")
val maxHp = target.getAttribute( Attribute.GENERIC_MAX_HEALTH )?.value ?: 20.0
if ( target.health > maxHp * 0.5 )
return AbilityResult.ConditionNotMet( "Target must be below 50 % health!" )
// ── Immobilise ──────────────────────────────────────────────────── // ── Immobilise ────────────────────────────────────────────────────
target.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 5 * 20, 127, false, false, true)) target.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, 5 * 20, 127, false, false, true ) )
target.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 5 * 20, 127, false, false, false)) target.addPotionEffect( PotionEffect( PotionEffectType.MINING_FATIGUE, 5 * 20, 127, false, false, false ) )
target.addPotionEffect(PotionEffect(PotionEffectType.GLOWING, 5 * 20, 0, false, false, false)) target.addPotionEffect( PotionEffect( PotionEffectType.GLOWING, 5 * 20, 0, false, false, false ) )
// Zero horizontal velocity every tick for 5 seconds (100 ticks) // Zero horizontal velocity every tick for 5 seconds (100 ticks)
object : BukkitRunnable() { object : BukkitRunnable()
var ticks = 0
override fun run() {
if (ticks++ >= 100 || !target.isOnline ||
!plugin.gameManager.alivePlayers.contains(target.uniqueId))
{ {
target.removePotionEffect(PotionEffectType.GLOWING) var ticks = 0
cancel(); return
override fun run()
{
if ( ticks++ >= 100 || !target.isOnline ||
!plugin.gameManager.alivePlayers.contains( target.uniqueId ) )
{
target.removePotionEffect( PotionEffectType.GLOWING )
cancel()
return
} }
val v = target.velocity val v = target.velocity
target.velocity = v.clone().setX(0.0).setZ(0.0) target.velocity = v.clone().setX( 0.0 ).setZ( 0.0 )
.let { if (it.y > 0.0) it.setY(0.0) else it } .let { if ( it.y > 0.0 ) it.setY( 0.0 ) else it }
} }
}.runTaskTimer(plugin, 0L, 1L) }.runTaskTimer( plugin, 0L, 1L )
player.playSound(player.location, Sound.ENTITY_WITHER_SHOOT, 1f, 0.5f) player.playSound( player.location, Sound.ENTITY_WITHER_SHOOT, 1f, 0.5f )
target.playSound(target.location, Sound.ENTITY_WITHER_HURT, 1f, 0.5f) target.playSound( target.location, Sound.ENTITY_WITHER_HURT, 1f, 0.5f )
target.world.spawnParticle(Particle.SOUL, target.location.clone().add(0.0, 1.0, 0.0), target.world.spawnParticle(
20, 0.4, 0.6, 0.4, 0.02) Particle.SOUL,
target.location.clone().add( 0.0, 1.0, 0.0 ),
20, 0.4, 0.6, 0.4, 0.02
)
player.sendActionBar(player.trans("kits.voodoo.messages.root_activated")) player.sendActionBar( player.trans( "kits.voodoo.messages.root_activated" ) )
target.sendActionBar(target.trans("kits.voodoo.messages.root_received")) target.sendActionBar( target.trans( "kits.voodoo.messages.root_received" ) )
return AbilityResult.Success return AbilityResult.Success
} }
} }
// ========================================================================= // =========================================================================
// DEFENSIVE active curse nearby enemies // DEFENSIVE active curse nearby enemies
// ========================================================================= // =========================================================================
private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) { private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
@@ -173,119 +235,169 @@ class VoodooKit : Kit() {
get() = "voodoo" 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 hardcodedHitsRequired: Int override val hardcodedHitsRequired: Int
get() = 10 get() = 10
override val triggerMaterial = Material.SOUL_TORCH
override fun execute(player: Player): AbilityResult { override val triggerMaterial: Material
get() = Material.SOUL_TORCH
override fun execute(
player: Player
): AbilityResult
{
val targets = player.world val targets = player.world
.getNearbyEntities(player.location, 8.0, 8.0, 8.0) .getNearbyEntities( player.location, 8.0, 8.0, 8.0 )
.filterIsInstance<Player>() .filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) } .filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
if (targets.isEmpty()) if ( targets.isEmpty() )
return AbilityResult.ConditionNotMet("No enemies within 8 blocks!") return AbilityResult.ConditionNotMet( "No enemies within 8 blocks!" )
// Snapshot the curse duration at activation time
val capturedCurseDurationMs = curseDurationMs
val expiry = System.currentTimeMillis() + capturedCurseDurationMs
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(
t.sendActionBar(t.trans("kits.voodoo.messages.curse_received")) PotionEffect( PotionEffectType.GLOWING, 15 * 20, 0, false, true, false )
t.world.spawnParticle(Particle.SOUL_FIRE_FLAME, t.location.clone().add(0.0, 1.0, 0.0), )
10, 0.3, 0.4, 0.3, 0.05) t.sendActionBar( t.trans( "kits.voodoo.messages.curse_received" ) )
t.world.spawnParticle(
Particle.SOUL_FIRE_FLAME,
t.location.clone().add( 0.0, 1.0, 0.0 ),
10, 0.3, 0.4, 0.3, 0.05
)
} }
player.playSound(player.location, Sound.ENTITY_WITHER_AMBIENT, 1f, 0.3f) player.playSound( player.location, Sound.ENTITY_WITHER_AMBIENT, 1f, 0.3f )
player.sendActionBar(player.trans("kits.voodoo.messages.curse_cast", player.sendActionBar(
mapOf("count" to targets.size.toString()))) player.trans(
"kits.voodoo.messages.curse_cast",
mapOf( "count" to targets.size.toString() )
)
)
return AbilityResult.Success return AbilityResult.Success
} }
} }
// ========================================================================= // =========================================================================
// AGGRESSIVE passive 20 % Wither on hit // AGGRESSIVE passive 20 % Wither on hit
// ========================================================================= // =========================================================================
private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) { private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val rng = Random() private val rng = Random()
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.aggressive.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.aggressive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.aggressive.description") get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.aggressive.description" )
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) { override fun onHitEnemy(
if (rng.nextDouble() >= 0.20) return attacker: Player,
victim.addPotionEffect(PotionEffect(PotionEffectType.WITHER, 3 * 20, 0)) victim: Player,
attacker.playSound(attacker.location, Sound.ENTITY_WITHER_AMBIENT, 0.5f, 1.8f) event: EntityDamageByEntityEvent
victim.world.spawnParticle(Particle.SOUL, victim.location.clone().add(0.0, 1.0, 0.0), ) {
5, 0.2, 0.3, 0.2, 0.0) if ( rng.nextDouble() >= 0.20 ) return
victim.addPotionEffect( PotionEffect( PotionEffectType.WITHER, 3 * 20, 0 ) )
attacker.playSound( attacker.location, Sound.ENTITY_WITHER_AMBIENT, 0.5f, 1.8f )
victim.world.spawnParticle(
Particle.SOUL,
victim.location.clone().add( 0.0, 1.0, 0.0 ),
5, 0.2, 0.3, 0.2, 0.0
)
} }
} }
// ========================================================================= // =========================================================================
// DEFENSIVE passive buff while cursed enemies are nearby // DEFENSIVE passive Speed + Regen while cursed enemies are nearby
// ========================================================================= // =========================================================================
private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) { private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
{
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val tasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap() private val tasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.defensive.name") get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.defensive.name" )
override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.defensive.description")
override fun onActivate(player: Player) { override val description: String
val task = object : BukkitRunnable() { get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.defensive.description" )
override fun run() {
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) { override fun onActivate(
cancel(); return player: Player
) {
val task = object : BukkitRunnable()
{
override fun run()
{
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
{
cancel()
return
} }
runCatching { tickPassive( player ) } runCatching { tickPassive( player ) }
.onFailure { plugin.logger.severe( "[VoodooKit] tickPassive error: ${it.message}" ) } .onFailure { plugin.logger.severe( "[VoodooKit] tickPassive error: ${it.message}" ) }
} }
}.runTaskTimer(plugin, 0L, 20L) }.runTaskTimer( plugin, 0L, 20L )
tasks[player.uniqueId] = task
tasks[ player.uniqueId ] = task
} }
override fun onDeactivate(player: Player) { override fun onDeactivate(
tasks.remove(player.uniqueId)?.cancel() player: Player
) {
tasks.remove( player.uniqueId )?.cancel()
} }
private fun tickPassive(voodooPlayer: Player) { private fun tickPassive(
voodooPlayer: Player
) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
// ── Expire stale curses ─────────────────────────────────────────── // ── Expire stale curses ───────────────────────────────────────────
cursedExpiry.entries.removeIf { (uuid, expiry) -> cursedExpiry.entries.removeIf { ( uuid, expiry ) ->
if (now > expiry) { if ( now > expiry )
Bukkit.getPlayer(uuid)?.removePotionEffect(PotionEffectType.GLOWING) {
Bukkit.getPlayer( uuid )?.removePotionEffect( PotionEffectType.GLOWING )
true true
} else false }
else false
} }
// ── Apply debuffs to all still-cursed + alive players ───────────── // ── Apply debuffs to all still-cursed alive players ───────────────
val cursedNearby = cursedExpiry.keys val cursedNearby = cursedExpiry.keys
.mapNotNull { Bukkit.getPlayer(it) } .mapNotNull { Bukkit.getPlayer( it ) }
.filter { it.isOnline && plugin.gameManager.alivePlayers.contains(it.uniqueId) } .filter { it.isOnline && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.onEach { cursed -> .onEach { cursed ->
cursed.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 30, 0)) cursed.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, 30, 0 ) )
cursed.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 30, 0)) cursed.addPotionEffect( PotionEffect( PotionEffectType.MINING_FATIGUE, 30, 0 ) )
} }
.filter { it.location.distanceSquared(voodooPlayer.location) <= 100.0 } // ≤ 10 blocks .filter { it.location.distanceSquared( voodooPlayer.location ) <= 100.0 } // ≤ 10 blocks
// ── Buff voodoo player if cursed enemy nearby ───────────────────── // ── Buff voodoo player if a cursed enemy is nearby ────────────────
if (cursedNearby.isNotEmpty()) { if ( cursedNearby.isNotEmpty() )
voodooPlayer.addPotionEffect(PotionEffect(PotionEffectType.SPEED, 30, 0)) {
voodooPlayer.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 30, 0)) voodooPlayer.addPotionEffect( PotionEffect( PotionEffectType.SPEED, 30, 0 ) )
voodooPlayer.addPotionEffect( PotionEffect( PotionEffectType.REGENERATION, 30, 0 ) )
} }
} }
} }
} }

View File

@@ -37,7 +37,7 @@ class ItemBuilder(
name: String name: String
): ItemBuilder ): ItemBuilder
{ {
itemStack.editMeta { it.displayName(Component.text( name )) } itemStack.editMeta { it.displayName(mm.deserialize( name )) }
return this return this
} }