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:
@@ -19,9 +19,9 @@ import java.util.*
|
||||
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 |
|
||||
* |------|-------|-----------|
|
||||
@@ -32,22 +32,31 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* When a piece breaks ([onItemBreak]), it is immediately replaced with the
|
||||
* current tier so the player is never left without armor.
|
||||
*
|
||||
* - **AGGRESSIVE passive**: +Strength I (5 s) for every kill.
|
||||
* - **DEFENSIVE passive**: +Protection I enchant on iron/diamond pieces.
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* 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
|
||||
|
||||
override val id = "armorer"
|
||||
override val displayName: Component
|
||||
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
|
||||
override val id: String
|
||||
get() = "armorer"
|
||||
|
||||
// ── Kill tracking ─────────────────────────────────────────────────────────
|
||||
internal val killCounts: MutableMap<UUID, Int> = ConcurrentHashMap()
|
||||
override val displayName: Component
|
||||
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 {
|
||||
private val CHESTPLATE_TIERS = listOf(
|
||||
@@ -65,87 +74,156 @@ class ArmorerKit : Kit() {
|
||||
Sound.ITEM_ARMOR_EQUIP_IRON,
|
||||
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 ──────────────────────────────────────────────
|
||||
private val aggressiveActive = NoActive(Playstyle.AGGRESSIVE)
|
||||
private val defensiveActive = NoActive(Playstyle.DEFENSIVE)
|
||||
private val aggressiveActive = NoActive( Playstyle.AGGRESSIVE )
|
||||
private val defensiveActive = NoActive( Playstyle.DEFENSIVE )
|
||||
private val aggressivePassive = AggressivePassive()
|
||||
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.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(player: Player, playstyle: Playstyle) { /* armor is given in onAssign */ }
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) { /* armor is given in onAssign */ }
|
||||
|
||||
override fun onAssign(player: Player, playstyle: Playstyle) {
|
||||
killCounts[player.uniqueId] = 0
|
||||
setArmorTier(player, tier = 0, playstyle)
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
override fun onAssign(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
killCounts[ player.uniqueId ] = 0
|
||||
setArmorTier( player, tier = 0, playstyle )
|
||||
}
|
||||
|
||||
override fun onRemove(player: Player) {
|
||||
killCounts.remove(player.uniqueId)
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
killCounts.remove( player.uniqueId )
|
||||
}
|
||||
|
||||
// ── Kit-level kill hook (upgrades armor) ──────────────────────────────────
|
||||
|
||||
override fun onKillEnemy(killer: Player, victim: Player) {
|
||||
val newKills = killCounts.compute(killer.uniqueId) { _, v -> (v ?: 0) + 1 } ?: return
|
||||
val tier = (newKills / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex)
|
||||
val prevTier = ((newKills - 1) / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex)
|
||||
override fun onKillEnemy(
|
||||
killer: Player,
|
||||
victim: Player
|
||||
) {
|
||||
val newKills = killCounts.compute( killer.uniqueId ) { _, v -> ( v ?: 0 ) + 1 } ?: return
|
||||
|
||||
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())))
|
||||
// Snapshot the tier threshold at kill time
|
||||
val capturedKillsPerTier = killsPerTier
|
||||
|
||||
val tier = ( newKills / capturedKillsPerTier ).coerceAtMost( CHESTPLATE_TIERS.lastIndex )
|
||||
val prevTier = ( ( newKills - 1 ) / capturedKillsPerTier ).coerceAtMost( CHESTPLATE_TIERS.lastIndex )
|
||||
|
||||
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 ───────────────────────────────────────────
|
||||
|
||||
override fun onItemBreak(player: Player, brokenItem: ItemStack) {
|
||||
val kills = killCounts[player.uniqueId] ?: return
|
||||
val tier = (kills / 2).coerceAtMost(CHESTPLATE_TIERS.lastIndex)
|
||||
val playstyle = plugin.kitManager.getSelectedPlaystyle(player)
|
||||
override fun onItemBreak(
|
||||
player: Player,
|
||||
brokenItem: ItemStack
|
||||
) {
|
||||
val kills = killCounts[ player.uniqueId ] ?: return
|
||||
val tier = ( kills / killsPerTier ).coerceAtMost( CHESTPLATE_TIERS.lastIndex )
|
||||
val playstyle = plugin.kitManager.getSelectedPlaystyle( player )
|
||||
|
||||
when {
|
||||
brokenItem.type.name.endsWith("_CHESTPLATE") -> {
|
||||
player.inventory.chestplate = buildPiece(CHESTPLATE_TIERS[tier], tier, playstyle)
|
||||
player.sendActionBar(player.trans("kits.armorer.messages.armor_replaced"))
|
||||
when
|
||||
{
|
||||
brokenItem.type.name.endsWith( "_CHESTPLATE" ) ->
|
||||
{
|
||||
player.inventory.chestplate = buildPiece( CHESTPLATE_TIERS[ tier ], tier, playstyle )
|
||||
player.sendActionBar( player.trans( "kits.armorer.messages.armor_replaced" ) )
|
||||
}
|
||||
brokenItem.type.name.endsWith("_BOOTS") -> {
|
||||
player.inventory.boots = buildPiece(BOOT_TIERS[tier], tier, playstyle)
|
||||
player.sendActionBar(player.trans("kits.armorer.messages.armor_replaced"))
|
||||
brokenItem.type.name.endsWith( "_BOOTS" ) ->
|
||||
{
|
||||
player.inventory.boots = buildPiece( BOOT_TIERS[ tier ], tier, playstyle )
|
||||
player.sendActionBar( player.trans( "kits.armorer.messages.armor_replaced" ) )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private fun setArmorTier(player: Player, tier: Int, playstyle: Playstyle) {
|
||||
player.inventory.chestplate = buildPiece(CHESTPLATE_TIERS[tier], tier, playstyle)
|
||||
player.inventory.boots = buildPiece(BOOT_TIERS[tier], tier, playstyle)
|
||||
private fun setArmorTier(
|
||||
player: Player,
|
||||
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).
|
||||
*/
|
||||
private fun buildPiece(material: Material, tier: Int, playstyle: Playstyle): ItemStack {
|
||||
val item = ItemStack(material)
|
||||
if (playstyle == Playstyle.DEFENSIVE && tier >= 1) {
|
||||
item.editMeta { it.addEnchant(Enchantment.PROTECTION, 1, true) }
|
||||
}
|
||||
private fun buildPiece(
|
||||
material: Material,
|
||||
tier: Int,
|
||||
playstyle: Playstyle
|
||||
): ItemStack
|
||||
{
|
||||
val item = ItemStack( material )
|
||||
if ( playstyle == Playstyle.DEFENSIVE && tier >= 1 )
|
||||
item.editMeta { it.addEnchant( Enchantment.PROTECTION, 1, true ) }
|
||||
return item
|
||||
}
|
||||
|
||||
@@ -153,45 +231,75 @@ class ArmorerKit : Kit() {
|
||||
// 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
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.aggressive.name")
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.armorer.passive.aggressive.description")
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.armorer.passive.aggressive.name" )
|
||||
|
||||
override fun onKillEnemy(killer: Player, victim: Player) {
|
||||
killer.addPotionEffect(PotionEffect(PotionEffectType.STRENGTH, 5 * 20, 0))
|
||||
killer.playSound(killer.location, Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.4f)
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.armorer.passive.aggressive.description" )
|
||||
|
||||
override fun onKillEnemy(
|
||||
killer: Player,
|
||||
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
|
||||
|
||||
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
|
||||
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) {
|
||||
override val kitId: String = "armorer"
|
||||
override val name = "None"
|
||||
override val description = "None"
|
||||
override val triggerMaterial = Material.BARRIER
|
||||
override val hardcodedHitsRequired: Int = 0
|
||||
override fun execute(player: Player) = AbilityResult.Success
|
||||
inner class NoActive(
|
||||
playstyle: Playstyle
|
||||
) : ActiveAbility( playstyle )
|
||||
{
|
||||
|
||||
override val kitId: String
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,7 +13,20 @@ import org.bukkit.inventory.ItemStack
|
||||
import java.util.*
|
||||
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
|
||||
|
||||
@@ -57,9 +70,15 @@ class BackupKit : Kit() {
|
||||
|
||||
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
|
||||
get() = "backup"
|
||||
@@ -76,11 +95,16 @@ class BackupKit : Kit() {
|
||||
override val triggerMaterial: Material
|
||||
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
|
||||
get() = "None"
|
||||
|
||||
@@ -25,7 +25,24 @@ import org.bukkit.scheduler.BukkitRunnable
|
||||
import java.util.*
|
||||
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
|
||||
|
||||
@@ -41,11 +58,35 @@ class GladiatorKit : Kit() {
|
||||
override val icon: Material
|
||||
get() = Material.IRON_BARS
|
||||
|
||||
private val kitOverride: CustomGameSettings.KitOverride by lazy {
|
||||
plugin.customGameManager.settings.kits.kits["gladiator"]
|
||||
?: CustomGameSettings.KitOverride()
|
||||
companion object {
|
||||
const val DEFAULT_ARENA_RADIUS = 11
|
||||
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) ────────────
|
||||
private val aggressiveActive = AllActive( Playstyle.AGGRESSIVE )
|
||||
private val defensiveActive = AllActive( Playstyle.DEFENSIVE )
|
||||
@@ -96,7 +137,10 @@ class GladiatorKit : Kit() {
|
||||
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
|
||||
|
||||
@@ -110,7 +154,7 @@ class GladiatorKit : Kit() {
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.description" )
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 15
|
||||
get() = DEFAULT_WITHER_AFTER_SECONDS
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.IRON_BARS
|
||||
@@ -122,40 +166,54 @@ class GladiatorKit : Kit() {
|
||||
val lineOfSight = player.getTargetEntity( 3 ) as? Player
|
||||
?: return AbilityResult.ConditionNotMet( "No player in line of sight" )
|
||||
|
||||
if (player.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ) ||
|
||||
lineOfSight.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ))
|
||||
if ( player.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ) ||
|
||||
lineOfSight.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ) )
|
||||
return AbilityResult.ConditionNotMet( "Already in gladiator fight" )
|
||||
|
||||
val radius = kitOverride.arenaRadius
|
||||
val height = kitOverride.arenaHeight
|
||||
// Snapshot config values at activation time
|
||||
val capturedRadius = arenaRadius
|
||||
val capturedHeight = arenaHeight
|
||||
|
||||
player.setMetadata( KitMetaData.IN_GLADIATOR.getKey(), FixedMetadataValue( plugin, true ))
|
||||
lineOfSight.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 ) )
|
||||
|
||||
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 )
|
||||
|
||||
WorldEditUtils.createCylinder( player.world, center, radius - 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.clone().add( 0.0, height - 1.0, 0.0 ), 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, capturedRadius - 1, false, capturedHeight, 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, { ->
|
||||
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
|
||||
block.setMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), FixedMetadataValue( plugin, true ))
|
||||
block.setMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), FixedMetadataValue( plugin, true ) )
|
||||
}
|
||||
}, 5L )
|
||||
|
||||
val gladiatorFight = GladiatorFight( gladiatorRegion, player, lineOfSight, radius, height )
|
||||
val gladiatorFight = GladiatorFight(
|
||||
gladiatorRegion,
|
||||
player,
|
||||
lineOfSight,
|
||||
capturedRadius,
|
||||
capturedHeight
|
||||
)
|
||||
gladiatorFight.runTaskTimer( plugin, 0, 20 )
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) {
|
||||
private class NoPassive(
|
||||
playstyle: Playstyle
|
||||
) : PassiveAbility( playstyle )
|
||||
{
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
@@ -182,8 +240,16 @@ class GladiatorKit : Kit() {
|
||||
location.blockY, location.blockY + height
|
||||
)
|
||||
|
||||
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 )
|
||||
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
|
||||
)
|
||||
else region
|
||||
}
|
||||
|
||||
@@ -201,10 +267,10 @@ class GladiatorKit : Kit() {
|
||||
{
|
||||
val adapt = BukkitAdapter.adapt( world, vector3 )
|
||||
|
||||
if (!world.worldBorder.isInside( adapt ))
|
||||
if ( !world.worldBorder.isInside( adapt ) )
|
||||
return false
|
||||
|
||||
if (!world.getBlockAt( adapt ).type.isAir)
|
||||
if ( !world.getBlockAt( adapt ).type.isAir )
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -216,7 +282,8 @@ class GladiatorKit : Kit() {
|
||||
val enemy: Player,
|
||||
val radius: Int,
|
||||
val height: Int
|
||||
) : BukkitRunnable() {
|
||||
) : BukkitRunnable()
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
@@ -230,10 +297,10 @@ class GladiatorKit : Kit() {
|
||||
|
||||
fun init()
|
||||
{
|
||||
gladiator.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 ))
|
||||
enemy.teleport(Location( world, center.x - radius / 2, center.y + 1, center.z, -90f, 0f ))
|
||||
gladiator.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 ) )
|
||||
enemy.teleport( Location( world, center.x - radius / 2, center.y + 1, center.z, -90f, 0f ) )
|
||||
}
|
||||
|
||||
private var ended = false
|
||||
@@ -252,15 +319,18 @@ class GladiatorKit : Kit() {
|
||||
return
|
||||
}
|
||||
|
||||
if (region.contains(BukkitAdapter.asBlockVector( gladiator.location )) &&
|
||||
region.contains(BukkitAdapter.asBlockVector( enemy.location )))
|
||||
if ( region.contains( BukkitAdapter.asBlockVector( gladiator.location ) ) &&
|
||||
region.contains( BukkitAdapter.asBlockVector( enemy.location ) ) )
|
||||
{
|
||||
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 ))
|
||||
enemy.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 ) )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,25 +343,25 @@ class GladiatorKit : Kit() {
|
||||
gladiator.apply {
|
||||
removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin )
|
||||
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 )
|
||||
}
|
||||
|
||||
enemy.apply {
|
||||
removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin )
|
||||
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 )
|
||||
}
|
||||
|
||||
for ( vector3 in region )
|
||||
{
|
||||
val block = world.getBlockAt(BukkitAdapter.adapt( world, vector3 ))
|
||||
if (!block.hasMetadata( KitMetaData.GLADIATOR_BLOCK.getKey() )) continue
|
||||
val block = world.getBlockAt( BukkitAdapter.adapt( world, vector3 ) )
|
||||
if ( !block.hasMetadata( KitMetaData.GLADIATOR_BLOCK.getKey() ) ) continue
|
||||
block.removeMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), plugin )
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,25 @@ import org.bukkit.scheduler.BukkitTask
|
||||
import java.util.*
|
||||
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
|
||||
|
||||
@@ -37,11 +55,27 @@ class GoblinKit : Kit() {
|
||||
override val icon: Material
|
||||
get() = Material.MOSSY_COBBLESTONE
|
||||
|
||||
private val kitOverride: CustomGameSettings.KitOverride by lazy {
|
||||
plugin.customGameManager.settings.kits.kits["goblin"]
|
||||
?: CustomGameSettings.KitOverride()
|
||||
companion object {
|
||||
const val DEFAULT_STEAL_DURATION_SECONDS = 60
|
||||
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) ────────────
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
private val defensiveActive = DefensiveActive()
|
||||
@@ -110,7 +144,8 @@ class GoblinKit : Kit() {
|
||||
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
|
||||
|
||||
@@ -138,10 +173,15 @@ class GoblinKit : Kit() {
|
||||
val lineOfSight = player.getTargetEntity( 3 ) as? Player
|
||||
?: 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 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 )
|
||||
|
||||
activeStealTasks.remove( player.uniqueId )
|
||||
@@ -151,10 +191,13 @@ class GoblinKit : Kit() {
|
||||
plugin.kitManager.selectPlaystyle( player, targetPlaystyle )
|
||||
plugin.kitManager.applyKit( player )
|
||||
|
||||
// Snapshot the duration at activation time
|
||||
val capturedStealDuration = stealDuration
|
||||
|
||||
val task = Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
activeStealTasks.remove( player.uniqueId )
|
||||
// Nur wiederherstellen, wenn Spieler noch alive und Spiel läuft
|
||||
if (plugin.gameManager.alivePlayers.contains( player.uniqueId ) &&
|
||||
// Only restore if player is still alive and game is running
|
||||
if ( plugin.gameManager.alivePlayers.contains( player.uniqueId ) &&
|
||||
plugin.gameManager.currentState == GameState.INGAME )
|
||||
{
|
||||
plugin.kitManager.removeKit( player )
|
||||
@@ -162,12 +205,14 @@ class GoblinKit : Kit() {
|
||||
plugin.kitManager.selectPlaystyle( player, currentPlaystyle )
|
||||
plugin.kitManager.applyKit( player )
|
||||
}
|
||||
}, 20L * kitOverride.stealDuration)
|
||||
}, 20L * capturedStealDuration )
|
||||
|
||||
activeStealTasks[ player.uniqueId ] = task
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -206,10 +252,13 @@ class GoblinKit : Kit() {
|
||||
val world = player.world
|
||||
val location = player.location
|
||||
|
||||
// Snapshot the radius at activation time
|
||||
val capturedBunkerRadius = bunkerRadius
|
||||
|
||||
WorldEditUtils.createSphere(
|
||||
world,
|
||||
location,
|
||||
kitOverride.bunkerRadius,
|
||||
capturedBunkerRadius,
|
||||
false,
|
||||
Material.MOSSY_COBBLESTONE
|
||||
)
|
||||
@@ -218,21 +267,22 @@ class GoblinKit : Kit() {
|
||||
WorldEditUtils.createSphere(
|
||||
world,
|
||||
location,
|
||||
kitOverride.bunkerRadius,
|
||||
capturedBunkerRadius,
|
||||
false,
|
||||
Material.AIR
|
||||
)
|
||||
}, 20L * 15 )
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class AggressiveNoPassive : PassiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
private class AggressiveNoPassive : PassiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
override val name: String
|
||||
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
|
||||
get() = "None"
|
||||
|
||||
@@ -20,7 +20,28 @@ import org.bukkit.potion.PotionEffectType
|
||||
import java.util.*
|
||||
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
|
||||
|
||||
@@ -36,6 +57,52 @@ class IceMageKit : Kit() {
|
||||
override val icon: Material
|
||||
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) ────────────
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
private val defensiveActive = DefensiveActive()
|
||||
@@ -89,7 +156,8 @@ class IceMageKit : Kit() {
|
||||
items.forEach { player.inventory.remove( it ) }
|
||||
}
|
||||
|
||||
private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
override val kitId: String
|
||||
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
|
||||
|
||||
@@ -139,13 +208,14 @@ class IceMageKit : Kit() {
|
||||
): AbilityResult
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val random = Random()
|
||||
@@ -172,8 +242,8 @@ class IceMageKit : Kit() {
|
||||
event: PlayerMoveEvent
|
||||
) {
|
||||
val biome = player.world.getBiome( player.location )
|
||||
if (!biomeList.contains( biome.name.lowercase() )) return
|
||||
player.addPotionEffect(PotionEffect( PotionEffectType.SPEED, 20, 0 ))
|
||||
if ( !biomeList.contains( biome.name.lowercase() ) ) return
|
||||
player.addPotionEffect( PotionEffect( PotionEffectType.SPEED, 20, 0 ) )
|
||||
}
|
||||
|
||||
override fun onHitEnemy(
|
||||
@@ -181,13 +251,17 @@ class IceMageKit : Kit() {
|
||||
victim: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if (random.nextInt( 3 ) < 1 )
|
||||
victim.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 60, 0 ))
|
||||
// Snapshot at hit time for consistency
|
||||
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
|
||||
get() = "None"
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.potion.PotionEffect
|
||||
import org.bukkit.potion.PotionEffectType
|
||||
import org.bukkit.scheduler.BukkitTask
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
@@ -27,243 +27,353 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
*
|
||||
* | Playstyle | Fähigkeit |
|
||||
* |-------------|----------------------------------------------------------------------------------------|
|
||||
* | AGGRESSIVE | **Life Drain** – saugt 4 ♥/s pro Gegner in der Nähe (max. 8 ♥, 2 s). Sneak: Cancel. |
|
||||
* | DEFENSIVE | **Puppeteer's Fear** – Blindness + Slowness III an alle Nahkämpfer für 4 Sekunden. |
|
||||
* | 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 in [fearRadius] Blöcken für [fearDurationTicks] Ticks. |
|
||||
*
|
||||
* ### Cancel-Mechanismus (Aggressive):
|
||||
* `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.
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* ### Drain-Timing:
|
||||
* 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.
|
||||
* Healing: Direkt über `player.health = (player.health + healAmount).coerceAtMost(maxHp)`.
|
||||
* Drain: Jeder Gegner nimmt `DRAIN_HP_PER_ENEMY_PER_SECOND` Schaden.
|
||||
* All values read from the `extras` map with companion-object defaults as fallback.
|
||||
*
|
||||
* | JSON-Schlüssel | Typ | Default | Beschreibung |
|
||||
* |-----------------------------|--------|---------|------------------------------------------------------------|
|
||||
* | `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
|
||||
|
||||
override val id = "puppet"
|
||||
override val displayName: Component
|
||||
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
|
||||
override val id: String
|
||||
get() = "puppet"
|
||||
|
||||
// Laufende Drain-Tasks: PlayerUUID → BukkitTask
|
||||
internal val activeDrainTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
||||
override val displayName: Component
|
||||
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 {
|
||||
const val DRAIN_RADIUS = 7.0
|
||||
const val DRAIN_DURATION_TICKS = 40L // 2 Sekunden
|
||||
const val DRAIN_TICK_INTERVAL = 20L // pro Sekunde einmal
|
||||
const val HEAL_PER_ENEMY_PER_S_HP = 8.0 // 4 Herzen = 8 HP
|
||||
const val MAX_TOTAL_HEAL_HP = 16.0 // 8 Herzen = 16 HP
|
||||
const val DRAIN_DMG_PER_ENEMY_PER_S = 4.0 // Gegner verlieren 2 Herzen/s
|
||||
const val FEAR_RADIUS = 7.0
|
||||
const val FEAR_DURATION_TICKS = 80 // 4 Sekunden
|
||||
const val DEFAULT_DRAIN_RADIUS = 7.0
|
||||
const val DEFAULT_DRAIN_DURATION_TICKS = 40L
|
||||
const val DEFAULT_DRAIN_TICK_INTERVAL = 20L
|
||||
const val DEFAULT_HEAL_PER_ENEMY_PER_S_HP = 8.0
|
||||
const val DEFAULT_MAX_TOTAL_HEAL_HP = 16.0
|
||||
const val DEFAULT_DRAIN_DMG_PER_ENEMY_PER_S = 4.0
|
||||
const val DEFAULT_FEAR_RADIUS = 7.0
|
||||
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 defensiveActive = DefensiveActive()
|
||||
private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE)
|
||||
private val defensivePassive = NoPassive(Playstyle.DEFENSIVE)
|
||||
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
|
||||
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.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(player: Player, playstyle: Playstyle) {
|
||||
val (mat, active) = when (playstyle) {
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
val ( mat, active ) = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> Material.PHANTOM_MEMBRANE to aggressiveActive
|
||||
Playstyle.DEFENSIVE -> Material.BLAZE_ROD to defensiveActive
|
||||
}
|
||||
val item = ItemBuilder(mat)
|
||||
.name(active.name)
|
||||
.lore(listOf(active.description))
|
||||
|
||||
val item = ItemBuilder( mat )
|
||||
.name( active.name )
|
||||
.lore(listOf( active.description ))
|
||||
.build()
|
||||
cachedItems[player.uniqueId] = listOf(item)
|
||||
player.inventory.addItem(item)
|
||||
|
||||
cachedItems[ player.uniqueId ] = listOf( item )
|
||||
player.inventory.addItem( item )
|
||||
}
|
||||
|
||||
override fun onRemove(player: Player) {
|
||||
// Laufenden Drain abbrechen (z.B. bei Spielende)
|
||||
activeDrainTasks.remove(player.uniqueId)?.cancel()
|
||||
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) }
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
activeDrainTasks.remove( player.uniqueId )?.cancel()
|
||||
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sneak → bricht einen laufenden Drain ab.
|
||||
* Wird von [KitEventDispatcher.onPlayerToggleSneak] aufgerufen.
|
||||
* Sneak → cancels a running drain.
|
||||
* Dispatched by [KitEventDispatcher.onPlayerToggleSneak].
|
||||
*/
|
||||
override fun onToggleSneak(player: Player, isSneaking: Boolean) {
|
||||
if (!isSneaking) return
|
||||
val task = activeDrainTasks.remove(player.uniqueId) ?: return
|
||||
override fun onToggleSneak(
|
||||
player: Player,
|
||||
isSneaking: Boolean
|
||||
) {
|
||||
if ( !isSneaking ) return
|
||||
val task = activeDrainTasks.remove( player.uniqueId ) ?: return
|
||||
task.cancel()
|
||||
player.playSound(player.location, Sound.ENTITY_VEX_HURT, 0.6f, 1.8f)
|
||||
player.sendActionBar(player.trans("kits.puppet.messages.drain_cancelled"))
|
||||
player.playSound( player.location, Sound.ENTITY_VEX_HURT, 0.6f, 1.8f )
|
||||
player.sendActionBar( player.trans( "kits.puppet.messages.drain_cancelled" ) )
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AGGRESSIVE active – Life Drain
|
||||
// =========================================================================
|
||||
|
||||
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) {
|
||||
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val kitId = "puppet"
|
||||
override val kitId: String
|
||||
get() = "puppet"
|
||||
|
||||
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
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.puppet.items.drain.description")
|
||||
override val hardcodedHitsRequired = 15
|
||||
override val triggerMaterial = Material.PHANTOM_MEMBRANE
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.puppet.items.drain.description" )
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
// Sicherheit: kein doppelter Drain (kann eigentlich nicht passieren, da
|
||||
// Charge in CHARGING-State ist, aber defensiv trotzdem prüfen)
|
||||
if (activeDrainTasks.containsKey(player.uniqueId))
|
||||
return AbilityResult.ConditionNotMet("Drain already active!")
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 15
|
||||
|
||||
// Sofort prüfen ob Gegner in der Nähe sind
|
||||
val initialEnemies = player.world
|
||||
.getNearbyEntities(player.location, DRAIN_RADIUS, DRAIN_RADIUS, DRAIN_RADIUS)
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.PHANTOM_MEMBRANE
|
||||
|
||||
if (initialEnemies.isEmpty())
|
||||
return AbilityResult.ConditionNotMet(
|
||||
plugin.languageManager.getDefaultRawMessage("kits.puppet.messages.no_enemies")
|
||||
)
|
||||
override fun execute(
|
||||
player: Player
|
||||
): 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!" )
|
||||
|
||||
var totalHealedHp = 0.0
|
||||
var ticksFired = 0
|
||||
// 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
|
||||
|
||||
val task = Bukkit.getScheduler().runTaskTimer(plugin, { ->
|
||||
var totalHealedHp = 0.0
|
||||
var ticksElapsed = 0L
|
||||
|
||||
ticksFired++
|
||||
|
||||
// Task selbst beenden wenn: offline, tot, max Heilung erreicht, Zeit abgelaufen
|
||||
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()
|
||||
val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
|
||||
if ( ticksElapsed >= capturedDrainDurationTicks ||
|
||||
totalHealedHp >= capturedMaxTotalHeal )
|
||||
{
|
||||
activeDrainTasks.remove( player.uniqueId )?.cancel()
|
||||
return@runTaskTimer
|
||||
}
|
||||
|
||||
val currentEnemies = player.world
|
||||
.getNearbyEntities(player.location, DRAIN_RADIUS, DRAIN_RADIUS, DRAIN_RADIUS)
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
ticksElapsed += capturedDrainTickInterval
|
||||
|
||||
if (currentEnemies.isEmpty()) {
|
||||
activeDrainTasks.remove(player.uniqueId)?.cancel()
|
||||
return@runTaskTimer
|
||||
}
|
||||
|
||||
// Heilmenge: 4♥ pro Gegner, gedeckelt auf verbleibendes Maximum
|
||||
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
|
||||
val enemies = player.world
|
||||
.getNearbyEntities(
|
||||
player.location,
|
||||
capturedDrainRadius,
|
||||
capturedDrainRadius,
|
||||
capturedDrainRadius
|
||||
)
|
||||
.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 maxHp = player.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.value ?: 20.0
|
||||
player.health = (player.health + actualHeal).coerceAtMost(maxHp)
|
||||
val remainingHealBudget = capturedMaxTotalHeal - totalHealedHp
|
||||
val rawHeal = capturedHealPerEnemy * enemies.size
|
||||
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
|
||||
|
||||
// Audio-Visual Feedback
|
||||
// Audio-visual feedback
|
||||
player.world.spawnParticle(
|
||||
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
|
||||
)
|
||||
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.trans(
|
||||
"kits.puppet.messages.draining",
|
||||
"healed" to "%.1f".format(totalHealedHp / 2.0), // in Herzen
|
||||
"max" to (MAX_TOTAL_HEAL_HP / 2.0).toInt().toString()
|
||||
"healed" to "%.1f".format( totalHealedHp / 2.0 ),
|
||||
"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.sendActionBar(player.trans("kits.puppet.messages.drain_start"))
|
||||
player.playSound( player.location, Sound.ENTITY_VEX_AMBIENT, 1f, 0.4f )
|
||||
player.sendActionBar( player.trans( "kits.puppet.messages.drain_start" ) )
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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
|
||||
|
||||
override val kitId = "puppet"
|
||||
override val kitId: String
|
||||
get() = "puppet"
|
||||
|
||||
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
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.puppet.items.fear.description")
|
||||
override val hardcodedHitsRequired = 15
|
||||
override val triggerMaterial = Material.BLAZE_ROD
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.puppet.items.fear.description" )
|
||||
|
||||
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
|
||||
.getNearbyEntities(player.location, FEAR_RADIUS, FEAR_RADIUS, FEAR_RADIUS)
|
||||
.getNearbyEntities(
|
||||
player.location,
|
||||
capturedFearRadius,
|
||||
capturedFearRadius,
|
||||
capturedFearRadius
|
||||
)
|
||||
.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(
|
||||
plugin.languageManager.getDefaultRawMessage("kits.puppet.messages.no_enemies")
|
||||
plugin.languageManager.getDefaultRawMessage( "kits.puppet.messages.no_enemies" )
|
||||
)
|
||||
|
||||
targets.forEach { target ->
|
||||
target.addPotionEffect(
|
||||
PotionEffect(PotionEffectType.BLINDNESS, FEAR_DURATION_TICKS, 0, false, false, true)
|
||||
PotionEffect( PotionEffectType.BLINDNESS, capturedFearDurationTicks, 0, false, false, true )
|
||||
)
|
||||
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(
|
||||
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
|
||||
)
|
||||
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.trans(
|
||||
"kits.puppet.messages.fear_cast",
|
||||
@@ -272,10 +382,24 @@ class PuppetKit : Kit() {
|
||||
)
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) {
|
||||
override val name = "None"
|
||||
override val description = "None"
|
||||
// =========================================================================
|
||||
// Shared no-passive placeholder
|
||||
// =========================================================================
|
||||
|
||||
class NoPassive(
|
||||
playstyle: Playstyle
|
||||
) : PassiveAbility( playstyle )
|
||||
{
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
|
||||
override val description: String
|
||||
get() = "None"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package club.mcscrims.speedhg.kit.impl
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.config.CustomGameSettings
|
||||
import club.mcscrims.speedhg.kit.Kit
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
||||
@@ -24,100 +23,174 @@ import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* ## Rattlesnake
|
||||
* ## RattlesnakeKit
|
||||
*
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|------------------------------------------------|--------------------------------------|
|
||||
* | AGGRESSIVE | Sneak-charged pounce (3–10 blocks) | Poison II on pounce-hit |
|
||||
* | DEFENSIVE | – | 25 % counter-venom on being hit |
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|-------------------------------------------------|-----------------------------------|
|
||||
* | AGGRESSIVE | Sneak-charged pounce ([pounceMinRange]–[pounceMaxRange] blocks) | Poison II on pounce-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
|
||||
* 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.
|
||||
* 3. If [onHitEnemy] fires while the player is marked as *pouncing*: apply Poison II
|
||||
* to 1 target (before Feast) or 3 targets (after Feast) and clear the flag.
|
||||
* 4. If the timeout fires first (miss): apply Nausea + Slowness to nearby enemies.
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* The five pounce-related values are stored as **typed fields** in
|
||||
* [CustomGameSettings.KitOverride] and are therefore accessed directly as
|
||||
* properties rather than through `extras`.
|
||||
*
|
||||
* | 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
|
||||
|
||||
override val id = "rattlesnake"
|
||||
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.SLIME_BALL
|
||||
override val id: String
|
||||
get() = "rattlesnake"
|
||||
|
||||
// ── Shared state (accessed by inner ability classes via outer-class reference) ─
|
||||
internal val sneakStartTimes: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
internal val pouncingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
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 {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// ── Cached ability instances ──────────────────────────────────────────────
|
||||
// ── 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 pouncingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
// ── Cached ability instances (avoid allocating per event call) ────────────
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
private val defensiveActive = DefensiveActive()
|
||||
private val aggressivePassive = AggressivePassive()
|
||||
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.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(player: Player, playstyle: Playstyle) {
|
||||
if (playstyle != Playstyle.AGGRESSIVE) return
|
||||
val item = ItemBuilder(Material.SLIME_BALL)
|
||||
.name(aggressiveActive.name)
|
||||
.lore(listOf(aggressiveActive.description))
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
if ( playstyle != Playstyle.AGGRESSIVE ) return
|
||||
|
||||
val item = ItemBuilder( Material.SLIME_BALL )
|
||||
.name( aggressiveActive.name )
|
||||
.lore(listOf( aggressiveActive.description ))
|
||||
.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(
|
||||
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) {
|
||||
player.removePotionEffect(PotionEffectType.SPEED)
|
||||
sneakStartTimes.remove(player.uniqueId)
|
||||
pouncingPlayers.remove(player.uniqueId)
|
||||
lastPounceUse.remove(player.uniqueId)
|
||||
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) }
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
player.removePotionEffect( PotionEffectType.SPEED )
|
||||
sneakStartTimes.remove( player.uniqueId )
|
||||
pouncingPlayers.remove( player.uniqueId )
|
||||
lastPounceUse.remove( player.uniqueId )
|
||||
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
|
||||
}
|
||||
|
||||
override fun onToggleSneak(player: Player, isSneaking: Boolean) {
|
||||
if (plugin.kitManager.getSelectedPlaystyle(player) != Playstyle.AGGRESSIVE) return
|
||||
if (isSneaking) sneakStartTimes[player.uniqueId] = System.currentTimeMillis()
|
||||
override fun onToggleSneak(
|
||||
player: Player,
|
||||
isSneaking: Boolean
|
||||
) {
|
||||
if ( plugin.kitManager.getSelectedPlaystyle( player ) != Playstyle.AGGRESSIVE ) return
|
||||
if ( isSneaking ) sneakStartTimes[ player.uniqueId ] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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
|
||||
|
||||
@@ -125,151 +198,205 @@ class RattlesnakeKit : Kit() {
|
||||
get() = "rattlesnake"
|
||||
|
||||
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
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.rattlesnake.items.pounce.description")
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.rattlesnake.items.pounce.description" )
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 0
|
||||
override val triggerMaterial = Material.SLIME_BALL
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
if (!player.isSneaking)
|
||||
return AbilityResult.ConditionNotMet("Sneak while activating the pounce!")
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.SLIME_BALL
|
||||
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
if ( !player.isSneaking )
|
||||
return AbilityResult.ConditionNotMet( "Sneak while activating the pounce!" )
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - (lastPounceUse[player.uniqueId] ?: 0L) < POUNCE_COOLDOWN_MS) {
|
||||
val remaining = ((POUNCE_COOLDOWN_MS - (now - (lastPounceUse[player.uniqueId] ?: 0L))) / 1000)
|
||||
return AbilityResult.ConditionNotMet("Cooldown: ${remaining}s remaining")
|
||||
|
||||
// Snapshot all config values at activation time
|
||||
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)
|
||||
val sneakDuration = (now - (sneakStartTimes[player.uniqueId] ?: now))
|
||||
.coerceIn(0L, MAX_SNEAK_MS)
|
||||
val range = MIN_RANGE + (sneakDuration.toDouble() / MAX_SNEAK_MS) * (MAX_RANGE - MIN_RANGE)
|
||||
// Sneak duration → range (min–max blocks)
|
||||
val sneakDuration = ( now - ( sneakStartTimes[ player.uniqueId ] ?: now ) )
|
||||
.coerceIn( 0L, capturedMaxSneakMs )
|
||||
val range = capturedPounceMinRange +
|
||||
( sneakDuration.toDouble() / capturedMaxSneakMs ) * ( capturedPounceMaxRange - capturedPounceMinRange )
|
||||
|
||||
val target = player.world
|
||||
.getNearbyEntities(player.location, range, range, range)
|
||||
.getNearbyEntities( player.location, range, range, range )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
.minByOrNull { it.location.distanceSquared(player.location) }
|
||||
?: return AbilityResult.ConditionNotMet("No enemies within ${range.toInt()} blocks!")
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.minByOrNull { it.location.distanceSquared( player.location ) }
|
||||
?: return AbilityResult.ConditionNotMet( "No enemies within ${range.toInt()} blocks!" )
|
||||
|
||||
// ── Launch ───────────────────────────────────────────────────────
|
||||
// ── Launch ────────────────────────────────────────────────────────
|
||||
val launchVec: Vector = target.location.toVector()
|
||||
.subtract(player.location.toVector())
|
||||
.subtract( player.location.toVector() )
|
||||
.normalize()
|
||||
.multiply(1.9)
|
||||
.setY(0.55)
|
||||
.multiply( 1.9 )
|
||||
.setY( 0.55 )
|
||||
|
||||
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)
|
||||
lastPounceUse[player.uniqueId] = now
|
||||
pouncingPlayers.add( player.uniqueId )
|
||||
lastPounceUse[ player.uniqueId ] = now
|
||||
|
||||
// ── Miss timeout ──────────────────────────────────────────────────
|
||||
Bukkit.getScheduler().runTaskLater(plugin, { ->
|
||||
if (!pouncingPlayers.remove(player.uniqueId)) return@runTaskLater // already hit
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
if ( !pouncingPlayers.remove( player.uniqueId ) ) return@runTaskLater // already hit
|
||||
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>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { enemy ->
|
||||
enemy.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 3 * 20, 0))
|
||||
enemy.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 3 * 20, 0))
|
||||
enemy.addPotionEffect( PotionEffect( PotionEffectType.NAUSEA, 3 * 20, 0 ) )
|
||||
enemy.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, 3 * 20, 0 ) )
|
||||
}
|
||||
player.sendActionBar(player.trans("kits.rattlesnake.messages.pounce_miss"))
|
||||
}, POUNCE_TIMEOUT_TICKS)
|
||||
player.sendActionBar( player.trans( "kits.rattlesnake.messages.pounce_miss" ) )
|
||||
}, capturedPounceTimeoutTicks )
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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
|
||||
|
||||
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
|
||||
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
|
||||
* to 1 target (before Feast) or up to 3 targets (after Feast).
|
||||
*/
|
||||
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) {
|
||||
if (!pouncingPlayers.remove(attacker.uniqueId)) return // not a pounce-hit
|
||||
override fun onHitEnemy(
|
||||
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 {
|
||||
add(victim)
|
||||
if (maxTargets > 1) {
|
||||
add( victim )
|
||||
if ( maxTargets > 1 )
|
||||
{
|
||||
victim.world
|
||||
.getNearbyEntities(victim.location, 4.0, 4.0, 4.0)
|
||||
.getNearbyEntities( victim.location, 4.0, 4.0, 4.0 )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != victim && it != attacker &&
|
||||
plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
.take(maxTargets - 1)
|
||||
.forEach { add(it) }
|
||||
plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.take( maxTargets - 1 )
|
||||
.forEach { add( it ) }
|
||||
}
|
||||
}
|
||||
|
||||
targets.forEach { t ->
|
||||
t.addPotionEffect(PotionEffect(PotionEffectType.POISON, 8 * 20, 1)) // Poison II
|
||||
t.world.spawnParticle(Particle.ITEM_SLIME, t.location.clone().add(0.0, 1.0, 0.0),
|
||||
12, 0.4, 0.4, 0.4, 0.0)
|
||||
t.addPotionEffect( PotionEffect( PotionEffectType.POISON, 8 * 20, 1 ) )
|
||||
t.world.spawnParticle(
|
||||
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.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.trans(
|
||||
"kits.rattlesnake.messages.pounce_hit",
|
||||
mapOf( "count" to targets.size.toString() )
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE active – no active ability
|
||||
// =========================================================================
|
||||
|
||||
private class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) {
|
||||
override val kitId: String = "rattlesnake"
|
||||
override val name = "None"
|
||||
override val description = "None"
|
||||
override val hardcodedHitsRequired: Int = 0
|
||||
override val triggerMaterial = Material.BARRIER
|
||||
override fun execute(player: Player) = AbilityResult.Success
|
||||
private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
|
||||
{
|
||||
|
||||
override val kitId: String
|
||||
get() = "rattlesnake"
|
||||
|
||||
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)
|
||||
// =========================================================================
|
||||
|
||||
private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) {
|
||||
private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val rng = Random()
|
||||
|
||||
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
|
||||
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) {
|
||||
if (rng.nextDouble() >= 0.25) return
|
||||
override fun onHitByEnemy(
|
||||
victim: Player,
|
||||
attacker: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if ( rng.nextDouble() >= 0.25 ) return
|
||||
|
||||
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.playSound(victim.location, Sound.ENTITY_SLIME_HURT, 0.8f, 1.8f)
|
||||
victim.sendActionBar(victim.trans("kits.rattlesnake.messages.venom_proc"))
|
||||
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.playSound( victim.location, Sound.ENTITY_SLIME_HURT, 0.8f, 1.8f )
|
||||
victim.sendActionBar( victim.trans( "kits.rattlesnake.messages.venom_proc" ) )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,8 +38,7 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* | Playstyle | Beschreibung |
|
||||
* |-------------|---------------------------------------------------------------------------------|
|
||||
* | 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: |
|
||||
* | | keine Dia-Armor, kein Instant-Death-Outcome |
|
||||
* | DEFENSIVE | Öffnet eine Slot-Maschinen-GUI (nur wenn kein Feind in [safeRadius] Blöcken) |
|
||||
*
|
||||
* ### Aggressive – Outcome-Wahrscheinlichkeiten
|
||||
* | 5 % | Instant Death |
|
||||
@@ -48,162 +47,233 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
* | 20 % | Neutrale Items |
|
||||
* | 50 % | Positive Items (inkl. möglicher Dia-Armor) |
|
||||
*
|
||||
* ### Defensive – Slot-Maschinen-GUI
|
||||
* Ö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.
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* ### Integration
|
||||
* Die [SlotMachineGui] nutzt einen eigenen [InventoryHolder]. Der Click-Dispatch
|
||||
* läuft über den zentralen [MenuListener] – dafür muss in [MenuListener.onInventoryClick]
|
||||
* ein zusätzlicher Branch ergänzt werden:
|
||||
* ```kotlin
|
||||
* val spieloHolder = event.inventory.holder as? SpieloKit.SlotMachineGui ?: ...
|
||||
* spieloHolder.onClick(event)
|
||||
* ```
|
||||
* All values read from the `extras` map with companion-object defaults as fallback.
|
||||
*
|
||||
* | JSON-Schlüssel | Typ | Default | Beschreibung |
|
||||
* |----------------------|--------|------------|----------------------------------------------------------|
|
||||
* | `active_cooldown_ms` | Long | `12_000` | Cooldown between aggressive gamble uses in milliseconds |
|
||||
* | `safe_radius` | Double | `12.0` | Radius (blocks) checked for enemies before opening GUI |
|
||||
*/
|
||||
class SpieloKit : Kit() {
|
||||
class SpieloKit : Kit()
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val rng = Random()
|
||||
private val mm = MiniMessage.miniMessage()
|
||||
private val mm = MiniMessage.miniMessage()
|
||||
|
||||
override val id: String
|
||||
get() = "spielo"
|
||||
|
||||
override val id = "spielo"
|
||||
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>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList("kits.spielo.lore")
|
||||
override val icon = Material.GOLD_NUGGET
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "kits.spielo.lore" )
|
||||
|
||||
// Blockiert Doppel-Trigger während eine Animation läuft
|
||||
internal val gamblingPlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
// Cooldowns für den Aggressive-Automaten
|
||||
private val activeCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
override val icon: Material
|
||||
get() = Material.GOLD_NUGGET
|
||||
|
||||
companion object {
|
||||
const val ACTIVE_COOLDOWN_MS = 12_000L // 12 s zwischen Aggressive-Uses
|
||||
const val SAFE_RADIUS = 12.0 // Feind-Radius für Defensive-GUI-Sperrung
|
||||
const val DEFAULT_ACTIVE_COOLDOWN_MS = 12_000L
|
||||
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 defensiveActive = DefensiveActive()
|
||||
private val aggressivePassive = NoPassive(Playstyle.AGGRESSIVE)
|
||||
private val defensivePassive = NoPassive(Playstyle.DEFENSIVE)
|
||||
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
|
||||
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.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(player: Player, playstyle: Playstyle) {
|
||||
val (mat, active) = when (playstyle) {
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
val ( mat, active ) = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> Material.GOLD_NUGGET to aggressiveActive
|
||||
Playstyle.DEFENSIVE -> Material.GOLD_BLOCK to defensiveActive
|
||||
}
|
||||
val item = ItemBuilder(mat)
|
||||
.name(active.name)
|
||||
.lore(listOf(active.description))
|
||||
|
||||
val item = ItemBuilder( mat )
|
||||
.name( active.name )
|
||||
.lore(listOf( active.description ))
|
||||
.build()
|
||||
cachedItems[player.uniqueId] = listOf(item)
|
||||
player.inventory.addItem(item)
|
||||
|
||||
cachedItems[ player.uniqueId ] = listOf( item )
|
||||
player.inventory.addItem( item )
|
||||
}
|
||||
|
||||
override fun onRemove(player: Player) {
|
||||
gamblingPlayers.remove(player.uniqueId)
|
||||
activeCooldowns.remove(player.uniqueId)
|
||||
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) }
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
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)
|
||||
// =========================================================================
|
||||
|
||||
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) {
|
||||
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val kitId = "spielo"
|
||||
override val kitId: String
|
||||
get() = "spielo"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.automat.name")
|
||||
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
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.spielo.items.automat.description" )
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
if (gamblingPlayers.contains(player.uniqueId))
|
||||
return AbilityResult.ConditionNotMet("Slotmachine is already running!")
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 12
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val lastUse = activeCooldowns[player.uniqueId] ?: 0L
|
||||
if (now - lastUse < ACTIVE_COOLDOWN_MS) {
|
||||
val secLeft = (ACTIVE_COOLDOWN_MS - (now - lastUse)) / 1000
|
||||
return AbilityResult.ConditionNotMet("Cooldown: ${secLeft}s")
|
||||
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 lastUse = activeCooldowns[ player.uniqueId ] ?: 0L
|
||||
|
||||
// Snapshot the cooldown at activation time
|
||||
val capturedCooldownMs = activeCooldownMs
|
||||
|
||||
if ( now - lastUse < capturedCooldownMs )
|
||||
{
|
||||
val secLeft = ( capturedCooldownMs - ( now - lastUse ) ) / 1000
|
||||
return AbilityResult.ConditionNotMet( "Cooldown: ${secLeft}s" )
|
||||
}
|
||||
|
||||
activeCooldowns[player.uniqueId] = now
|
||||
gamblingPlayers.add(player.uniqueId)
|
||||
activeCooldowns[ player.uniqueId ] = now
|
||||
gamblingPlayers.add( player.uniqueId )
|
||||
|
||||
// Kurze Sound-Animation (0,8 s) → dann Ergebnis
|
||||
playQuickAnimation(player) {
|
||||
gamblingPlayers.remove(player.uniqueId)
|
||||
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@playQuickAnimation
|
||||
resolveOutcome(player, allowInstantDeath = true, allowDiamondArmor = true)
|
||||
// Short sound animation (≈ 0.9 s) → then resolve
|
||||
playQuickAnimation( player ) {
|
||||
gamblingPlayers.remove( player.uniqueId )
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@playQuickAnimation
|
||||
resolveOutcome( player, allowInstantDeath = true, allowDiamondArmor = true )
|
||||
}
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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
|
||||
|
||||
override val kitId = "spielo"
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.slotautomat.name")
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.spielo.items.slotautomat.description")
|
||||
override val hardcodedHitsRequired = 15
|
||||
override val triggerMaterial = Material.GOLD_BLOCK
|
||||
override val kitId: String
|
||||
get() = "spielo"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.spielo.items.slotautomat.name" )
|
||||
|
||||
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
|
||||
.asSequence()
|
||||
.filter { it != player.uniqueId }
|
||||
.mapNotNull { Bukkit.getPlayer(it) }
|
||||
.any { it.location.distanceSquared(player.location) <= SAFE_RADIUS * SAFE_RADIUS }
|
||||
.mapNotNull { Bukkit.getPlayer( it ) }
|
||||
.any { it.location.distanceSquared( player.location ) <= capturedSafeRadius * capturedSafeRadius }
|
||||
|
||||
if (gamblingPlayers.contains(player.uniqueId))
|
||||
return AbilityResult.ConditionNotMet("Slotmachine is already running!")
|
||||
|
||||
if (enemyNearby)
|
||||
if ( enemyNearby )
|
||||
{
|
||||
playQuickAnimation(player) {
|
||||
gamblingPlayers.remove(player.uniqueId)
|
||||
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@playQuickAnimation
|
||||
resolveOutcome(player, allowInstantDeath = false, allowDiamondArmor = false)
|
||||
playQuickAnimation( player ) {
|
||||
gamblingPlayers.remove( player.uniqueId )
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@playQuickAnimation
|
||||
resolveOutcome( player, allowInstantDeath = false, allowDiamondArmor = false )
|
||||
}
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
SlotMachineGui(player).open()
|
||||
SlotMachineGui( player ).open()
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -225,108 +295,114 @@ class SpieloKit : Kit() {
|
||||
* 2. Spieler klickt Slot 22 ("Drehen") → Animation startet, Button wird gelb.
|
||||
* 3. Walzen stoppen gestaffelt (Walze 1 → 2 → 3).
|
||||
* 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(
|
||||
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 spinButton = 22
|
||||
private val reelSlots = intArrayOf( 11, 13, 15 )
|
||||
private val spinButton = 22
|
||||
|
||||
// Symbole die auf den Walzen erscheinen (nur visuell – kein Einfluss auf Outcome)
|
||||
private val reelSymbols = listOf(
|
||||
Material.GOLD_NUGGET, Material.EMERALD, Material.IRON_INGOT,
|
||||
Material.GOLDEN_APPLE, Material.MUSHROOM_STEW, Material.EXPERIENCE_BOTTLE,
|
||||
Material.TNT, Material.BARRIER, Material.NETHER_STAR, Material.LAPIS_LAZULI
|
||||
)
|
||||
|
||||
private var isSpinning = false
|
||||
private var isSpinning = false
|
||||
private var lastAnimTask: BukkitTask? = null
|
||||
|
||||
override fun getInventory(): Inventory = inv
|
||||
|
||||
fun open() {
|
||||
fun open()
|
||||
{
|
||||
drawLayout()
|
||||
player.openInventory(inv)
|
||||
player.openInventory( inv )
|
||||
}
|
||||
|
||||
private fun drawLayout() {
|
||||
private fun drawLayout()
|
||||
{
|
||||
val filler = buildFiller()
|
||||
repeat(27) { inv.setItem(it, filler) }
|
||||
reelSlots.forEach { inv.setItem(it, buildReelItem(reelSymbols.random())) }
|
||||
inv.setItem(spinButton, buildSpinButton(spinning = false))
|
||||
repeat( 27 ) { inv.setItem( it, filler ) }
|
||||
reelSlots.forEach { inv.setItem( it, buildReelItem( reelSymbols.random() ) ) }
|
||||
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
|
||||
if (isSpinning) return
|
||||
if (event.rawSlot != spinButton) return
|
||||
if ( isSpinning ) return
|
||||
if ( event.rawSlot != spinButton ) return
|
||||
|
||||
isSpinning = true
|
||||
gamblingPlayers.add(player.uniqueId)
|
||||
inv.setItem(spinButton, buildSpinButton(spinning = true))
|
||||
gamblingPlayers.add( player.uniqueId )
|
||||
inv.setItem( spinButton, buildSpinButton( spinning = true ) )
|
||||
|
||||
startSpinAnimation()
|
||||
}
|
||||
|
||||
/** Aufgerufen wenn Inventar geschlossen wird (z.B. ESC). */
|
||||
fun onClose() {
|
||||
/** Called when the inventory is closed (e.g. ESC). */
|
||||
fun onClose()
|
||||
{
|
||||
lastAnimTask?.cancel()
|
||||
// Charge nur zurückgeben wenn noch nicht gedreht wurde
|
||||
if (!isSpinning) {
|
||||
gamblingPlayers.remove(player.uniqueId)
|
||||
}
|
||||
// Wenn isSpinning == true läuft die Animation noch – Cleanup in onAllReelsStopped
|
||||
// Only refund the lock if the player never actually spun
|
||||
if ( !isSpinning ) gamblingPlayers.remove( player.uniqueId )
|
||||
// If isSpinning == true the animation is still running — cleanup in onAllReelsStopped
|
||||
}
|
||||
|
||||
// ── Animation ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Startet die gestaffelte Walzen-Animation.
|
||||
* Walze 1 stoppt nach 8 Frames, Walze 2 nach 12, Walze 3 nach 16.
|
||||
* Jeder Frame dauert 2 Ticks (0,1 s). Starts sind versetzt (+5 Ticks pro Walze).
|
||||
* Starts the staggered reel animation.
|
||||
* Reel 1 stops after 8 frames, reel 2 after 12, reel 3 after 16.
|
||||
* Each frame takes 2 ticks (0.1 s). Starts are staggered (+5 ticks per reel).
|
||||
*/
|
||||
private fun startSpinAnimation() {
|
||||
val framesPerReel = intArrayOf(8, 12, 16)
|
||||
val startDelays = longArrayOf(0L, 5L, 10L)
|
||||
val ticksPerFrame = 2L
|
||||
var stoppedReels = 0
|
||||
private fun startSpinAnimation()
|
||||
{
|
||||
val framesPerReel = intArrayOf( 8, 12, 16 )
|
||||
val startDelays = longArrayOf( 0L, 5L, 10L )
|
||||
val ticksPerFrame = 2L
|
||||
var stoppedReels = 0
|
||||
|
||||
for (reelIdx in 0..2) {
|
||||
val slot = reelSlots[reelIdx]
|
||||
val maxFrames = framesPerReel[reelIdx]
|
||||
var frame = 0
|
||||
for ( reelIdx in 0..2 )
|
||||
{
|
||||
val slot = reelSlots[ reelIdx ]
|
||||
val maxFrames = framesPerReel[ reelIdx ]
|
||||
var frame = 0
|
||||
|
||||
val task = object : BukkitRunnable() {
|
||||
val task = object : BukkitRunnable()
|
||||
{
|
||||
override fun run()
|
||||
{
|
||||
if (!player.isOnline) {
|
||||
this.cancel(); return
|
||||
}
|
||||
if ( !player.isOnline ) { this.cancel(); return }
|
||||
frame++
|
||||
|
||||
if (frame <= maxFrames) {
|
||||
// Zufälliges Walzen-Symbol während Rotation
|
||||
inv.setItem(slot, buildReelItem(reelSymbols.random()))
|
||||
val pitch = (0.6f + frame * 0.07f).coerceAtMost(2.0f)
|
||||
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.4f, pitch)
|
||||
} else {
|
||||
// Einrasten – finales Symbol (zufällig, rein visuell)
|
||||
inv.setItem(slot, buildReelItem(reelSymbols.random()))
|
||||
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_CHIME, 0.9f, 1.1f + reelIdx * 0.2f)
|
||||
if ( frame <= maxFrames )
|
||||
{
|
||||
inv.setItem( slot, buildReelItem( reelSymbols.random() ) )
|
||||
val pitch = ( 0.6f + frame * 0.07f ).coerceAtMost( 2.0f )
|
||||
player.playSound( player.location, Sound.BLOCK_NOTE_BLOCK_HAT, 0.4f, pitch )
|
||||
}
|
||||
else
|
||||
{
|
||||
inv.setItem( slot, buildReelItem( reelSymbols.random() ) )
|
||||
player.playSound(
|
||||
player.location,
|
||||
Sound.BLOCK_NOTE_BLOCK_CHIME,
|
||||
0.9f, 1.1f + reelIdx * 0.2f
|
||||
)
|
||||
|
||||
stoppedReels++
|
||||
if (stoppedReels == 3) onAllReelsStopped()
|
||||
if ( stoppedReels == 3 ) onAllReelsStopped()
|
||||
|
||||
this.cancel()
|
||||
}
|
||||
@@ -337,42 +413,47 @@ class SpieloKit : Kit() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAllReelsStopped() {
|
||||
player.playSound(player.location, Sound.ENTITY_PLAYER_LEVELUP, 0.7f, 1.5f)
|
||||
private fun onAllReelsStopped()
|
||||
{
|
||||
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, { ->
|
||||
gamblingPlayers.remove(player.uniqueId)
|
||||
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
gamblingPlayers.remove( player.uniqueId )
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@runTaskLater
|
||||
player.closeInventory()
|
||||
// Defensive: kein Instant-Death, keine Dia-Armor
|
||||
resolveOutcome(player, allowInstantDeath = false, allowDiamondArmor = false)
|
||||
}, 20L)
|
||||
resolveOutcome( player, allowInstantDeath = false, allowDiamondArmor = false )
|
||||
}, 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 ->
|
||||
meta.displayName(Component.text(" ").decoration(TextDecoration.ITALIC, false))
|
||||
meta.displayName( Component.text( " " ).decoration( TextDecoration.ITALIC, false ) )
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSpinButton(spinning: Boolean): ItemStack {
|
||||
val mat = if (spinning) Material.YELLOW_CONCRETE else Material.LIME_CONCRETE
|
||||
val name = if (spinning)
|
||||
mm.deserialize("<yellow><bold>⟳ Spins...</bold></yellow>")
|
||||
private fun buildSpinButton(
|
||||
spinning: Boolean
|
||||
): ItemStack
|
||||
{
|
||||
val mat = if ( spinning ) Material.YELLOW_CONCRETE else Material.LIME_CONCRETE
|
||||
val name = if ( spinning )
|
||||
mm.deserialize( "<yellow><bold>⟳ Spins...</bold></yellow>" )
|
||||
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 ->
|
||||
meta.displayName(name.decoration(TextDecoration.ITALIC, false))
|
||||
if (!spinning) {
|
||||
meta.displayName( name.decoration( TextDecoration.ITALIC, false ) )
|
||||
if ( !spinning )
|
||||
{
|
||||
meta.lore(listOf(
|
||||
Component.empty(),
|
||||
mm.deserialize("<gray>Click to spin the reels.")
|
||||
.decoration(TextDecoration.ITALIC, false),
|
||||
mm.deserialize( "<gray>Click to spin the reels." )
|
||||
.decoration( TextDecoration.ITALIC, false ),
|
||||
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 ->
|
||||
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.
|
||||
* @param allowInstantDeath true = Aggressive (5 % Instant Death möglich)
|
||||
* @param allowDiamondArmor true = Aggressive (Dia-Armor in Loot möglich)
|
||||
* Resolves the gamble result.
|
||||
* @param allowInstantDeath `true` = aggressive (5 % instant death possible)
|
||||
* @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()
|
||||
|
||||
when {
|
||||
allowInstantDeath && roll < 0.05 -> triggerInstantDeath(player)
|
||||
allowInstantDeath && roll < 0.20 -> triggerRandomDisaster(player)
|
||||
roll < (if (allowInstantDeath) 0.30 else 0.10) -> applyNegativeEffect(player)
|
||||
roll < (if (allowInstantDeath) 0.50 else 0.30) -> giveNeutralItems(player)
|
||||
else -> givePositiveItems(player, allowDiamondArmor)
|
||||
when
|
||||
{
|
||||
allowInstantDeath && roll < 0.05 -> triggerInstantDeath( player )
|
||||
allowInstantDeath && roll < 0.20 -> triggerRandomDisaster( player )
|
||||
roll < ( if ( allowInstantDeath ) 0.30 else 0.10 ) -> applyNegativeEffect( player )
|
||||
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) {
|
||||
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"))
|
||||
private fun triggerInstantDeath(
|
||||
player: Player
|
||||
) {
|
||||
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, { ->
|
||||
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@runTaskLater
|
||||
player.health = 0.0
|
||||
}, 3L)
|
||||
}, 3L )
|
||||
}
|
||||
|
||||
private fun triggerRandomDisaster(player: Player) {
|
||||
private fun triggerRandomDisaster(
|
||||
player: Player
|
||||
) {
|
||||
val disaster = listOf(
|
||||
MeteorDisaster(), TornadoDisaster(), EarthquakeDisaster(), ThunderDisaster()
|
||||
).random()
|
||||
|
||||
disaster.warn(player)
|
||||
Bukkit.getScheduler().runTaskLater(plugin, { ->
|
||||
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) return@runTaskLater
|
||||
disaster.trigger(plugin, player)
|
||||
}, disaster.warningDelayTicks)
|
||||
disaster.warn( player )
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) ) return@runTaskLater
|
||||
disaster.trigger( plugin, player )
|
||||
}, disaster.warningDelayTicks )
|
||||
|
||||
player.world.playSound(player.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 0.6f)
|
||||
player.sendActionBar(player.trans("kits.spielo.messages.gamble_event"))
|
||||
player.world.playSound( player.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 0.6f )
|
||||
player.sendActionBar( player.trans( "kits.spielo.messages.gamble_event" ) )
|
||||
}
|
||||
|
||||
private fun applyNegativeEffect(player: Player) {
|
||||
private fun applyNegativeEffect(
|
||||
player: Player
|
||||
) {
|
||||
val outcomes: List<() -> Unit> = listOf(
|
||||
{ player.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 6 * 20, 1)) },
|
||||
{ player.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 6 * 20, 1)) },
|
||||
{ player.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 5 * 20, 0)) },
|
||||
{ player.addPotionEffect(PotionEffect(PotionEffectType.WEAKNESS, 8 * 20, 0)) },
|
||||
{ player.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, 6 * 20, 1 ) ) },
|
||||
{ player.addPotionEffect( PotionEffect( PotionEffectType.MINING_FATIGUE, 6 * 20, 1 ) ) },
|
||||
{ player.addPotionEffect( PotionEffect( PotionEffectType.NAUSEA, 5 * 20, 0 ) ) },
|
||||
{ player.addPotionEffect( PotionEffect( PotionEffectType.WEAKNESS, 8 * 20, 0 ) ) },
|
||||
{ player.fireTicks = 4 * 20 }
|
||||
)
|
||||
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(
|
||||
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
|
||||
)
|
||||
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(
|
||||
ItemStack(Material.ARROW, rng.nextInt(5) + 3),
|
||||
ItemStack(Material.BREAD, rng.nextInt(4) + 2),
|
||||
ItemStack(Material.IRON_INGOT, rng.nextInt(3) + 1),
|
||||
ItemStack(Material.COBBLESTONE, rng.nextInt(8) + 4),
|
||||
ItemStack( Material.ARROW, rng.nextInt( 5 ) + 3 ),
|
||||
ItemStack( Material.BREAD, rng.nextInt( 4 ) + 2 ),
|
||||
ItemStack( Material.IRON_INGOT, rng.nextInt( 3 ) + 1 ),
|
||||
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.sendActionBar(player.trans("kits.spielo.messages.gamble_neutral"))
|
||||
player.playSound( player.location, Sound.ENTITY_ITEM_PICKUP, 0.8f, 1.0f )
|
||||
player.sendActionBar( player.trans( "kits.spielo.messages.gamble_neutral" ) )
|
||||
}
|
||||
|
||||
private fun givePositiveItems(player: Player, allowDiamondArmor: Boolean) {
|
||||
data class LootEntry(val item: ItemStack, val weight: Int)
|
||||
private fun givePositiveItems(
|
||||
player: Player,
|
||||
allowDiamondArmor: Boolean
|
||||
) {
|
||||
data class LootEntry( val item: ItemStack, val weight: Int )
|
||||
|
||||
val pool = buildList {
|
||||
add(LootEntry(ItemStack(Material.MUSHROOM_STEW, 3), 30))
|
||||
add(LootEntry(ItemStack(Material.MUSHROOM_STEW, 5), 15))
|
||||
add(LootEntry(ItemStack(Material.GOLDEN_APPLE), 20))
|
||||
add(LootEntry(ItemStack(Material.ENCHANTED_GOLDEN_APPLE), 3))
|
||||
add(LootEntry(ItemStack(Material.EXPERIENCE_BOTTLE, 5), 12))
|
||||
add(LootEntry(buildSplashPotion(PotionEffectType.STRENGTH, 200, 0), 8))
|
||||
add(LootEntry(buildSplashPotion(PotionEffectType.SPEED, 400, 0), 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_HELMET), 4))
|
||||
// Dia-Rüstung: nur Aggressive
|
||||
if (allowDiamondArmor) {
|
||||
add(LootEntry(ItemStack(Material.DIAMOND_CHESTPLATE), 2))
|
||||
add(LootEntry(ItemStack(Material.DIAMOND_HELMET), 2))
|
||||
add( LootEntry( ItemStack( Material.MUSHROOM_STEW, 3 ), 30 ) )
|
||||
add( LootEntry( ItemStack( Material.MUSHROOM_STEW, 5 ), 15 ) )
|
||||
add( LootEntry( ItemStack( Material.GOLDEN_APPLE ), 20 ) )
|
||||
add( LootEntry( ItemStack( Material.ENCHANTED_GOLDEN_APPLE ), 3 ) )
|
||||
add( LootEntry( ItemStack( Material.EXPERIENCE_BOTTLE, 5 ), 12 ) )
|
||||
add( LootEntry( buildSplashPotion( PotionEffectType.STRENGTH, 200, 0 ), 8 ) )
|
||||
add( LootEntry( buildSplashPotion( PotionEffectType.SPEED, 400, 0 ), 8 ) )
|
||||
add( LootEntry( buildSplashPotion( PotionEffectType.REGENERATION, 160, 1 ), 8 ) )
|
||||
add( LootEntry( ItemStack( Material.IRON_CHESTPLATE ), 4 ) )
|
||||
add( LootEntry( ItemStack( Material.IRON_HELMET ), 4 ) )
|
||||
if ( allowDiamondArmor )
|
||||
{
|
||||
add( LootEntry( ItemStack( Material.DIAMOND_CHESTPLATE ), 2 ) )
|
||||
add( LootEntry( ItemStack( Material.DIAMOND_HELMET ), 2 ) )
|
||||
}
|
||||
}
|
||||
|
||||
val totalWeight = pool.sumOf { it.weight }
|
||||
var roll = rng.nextInt(totalWeight)
|
||||
val chosen = pool.first { entry -> roll -= entry.weight; roll < 0 }
|
||||
player.inventory.addItem(chosen.item.clone())
|
||||
var roll = rng.nextInt( totalWeight )
|
||||
val chosen = pool.first { entry -> roll -= entry.weight; roll < 0 }
|
||||
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(
|
||||
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
|
||||
)
|
||||
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) {
|
||||
override val name = "None"
|
||||
override val description = "None"
|
||||
class NoPassive(
|
||||
playstyle: Playstyle
|
||||
) : PassiveAbility( playstyle )
|
||||
{
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
|
||||
override val description: String
|
||||
get() = "None"
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// Shared helpers
|
||||
// =========================================================================
|
||||
|
||||
/** Klicker-Sounds mit steigendem Pitch, danach Callback. */
|
||||
private fun playQuickAnimation(player: Player, onFinish: () -> Unit) {
|
||||
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)
|
||||
/** Click-sounds with rising pitch, then fires [onFinish] callback. */
|
||||
private fun playQuickAnimation(
|
||||
player: Player,
|
||||
onFinish: () -> Unit
|
||||
) {
|
||||
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(
|
||||
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
|
||||
)
|
||||
}, 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) =
|
||||
ItemStack(Material.SPLASH_POTION).also { potion ->
|
||||
potion.editMeta { meta ->
|
||||
if (meta is org.bukkit.inventory.meta.PotionMeta)
|
||||
meta.addCustomEffect(PotionEffect(type, duration, amplifier), true)
|
||||
}
|
||||
private fun buildSplashPotion(
|
||||
type: PotionEffectType,
|
||||
duration: Int,
|
||||
amplifier: Int
|
||||
) = ItemStack( Material.SPLASH_POTION ).also { potion ->
|
||||
potion.editMeta { meta ->
|
||||
if ( meta is org.bukkit.inventory.meta.PotionMeta )
|
||||
meta.addCustomEffect( PotionEffect( type, duration, amplifier ), true )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,8 +16,6 @@ import org.bukkit.Sound
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.scheduler.BukkitTask
|
||||
import org.bukkit.util.Vector
|
||||
import java.util.Random
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@@ -27,88 +25,138 @@ import kotlin.math.sin
|
||||
/**
|
||||
* ## TeslaKit
|
||||
*
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|---------------------------------------------------|------------------------------------------------|
|
||||
* | AGGRESSIVE | 5 Blitze im 5-Block-Radius (1.5 ♥ pro Treffer) | Rückschlag + Brandschaden-Aura alle 3 s (klein)|
|
||||
* | DEFENSIVE | – | Rückschlag + Brandschaden-Aura alle 3 s (groß) |
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|---------------------------------------------------|--------------------------------------------------|
|
||||
* | AGGRESSIVE | [lightningBoltCount] bolts in [lightningRadius] blocks ([lightningDamage] HP each) | [auraChance] % counter-shock on hit |
|
||||
* | DEFENSIVE | – | [auraChance] % counter-shock on hit |
|
||||
*
|
||||
* **Höhen-Einschränkung**: Beide Mechaniken deaktivieren sich ab Y > [MAX_HEIGHT_Y]
|
||||
* (~50 Blöcke über Meeresspiegel). Tesla braucht Erdkontakt.
|
||||
* **Height restriction**: Both mechanics deactivate above Y > [MAX_HEIGHT_Y].
|
||||
* Tesla needs ground contact.
|
||||
*
|
||||
* ### Technische Lösung – „Visueller Blitz + manueller Schaden":
|
||||
* `world.strikeLightningEffect()` erzeugt nur Partikel/Sound – keinen Block-/Entity-Schaden.
|
||||
* Direkt danach werden Spieler im 1,5-Block-Radius per `entity.damage()` manuell getroffen.
|
||||
* Das verhindert ungewollte Nebeneffekte (Feuer, Dorfbewohner-Schaden, eigener Tod durch
|
||||
* zufälligen Blitzschlag).
|
||||
* ### Technical note – "Visual lightning + manual damage":
|
||||
* `world.strikeLightningEffect()` produces only particles/sound — no block or entity damage.
|
||||
* Players within 1.5 blocks of the strike are then damaged manually via `entity.damage()`.
|
||||
* This prevents unwanted side-effects (fire, villager conversion, self-death from random bolts).
|
||||
*
|
||||
* ### Passive Aura:
|
||||
* 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
|
||||
* nach außen + `fireTicks`. Aggressive-Playstyle hat schwächeren Rückschlag, Defensive stärkeren.
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* All values read from the `extras` map with companion-object defaults as fallback.
|
||||
*
|
||||
* | 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 (0–1) 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
|
||||
|
||||
override val id: String
|
||||
get() = "tesla"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "kits.tesla.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "kits.tesla.lore" )
|
||||
|
||||
override val icon: Material
|
||||
get() = Material.LIGHTNING_ROD
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* ~50 Blöcke über Meeresspiegel ( Y ≈ 63 + 50 = 113 )
|
||||
* Oberhalb dieser Grenze sind beide Fähigkeiten deaktiviert.
|
||||
*/
|
||||
/** ~50 blocks above sea level (Y ≈ 63 + 50 = 113). Above this both abilities deactivate. */
|
||||
const val MAX_HEIGHT_Y = 113.0
|
||||
|
||||
// Aggressive Active
|
||||
const val LIGHTNING_RADIUS = 5.0
|
||||
const val LIGHTNING_DAMAGE = 3.0
|
||||
const val LIGHTNING_BOLT_COUNT = 5
|
||||
const val BOLT_STAGGER_TICKS = 8L
|
||||
|
||||
// Passive Aura
|
||||
const val AURA_CHANCE = 0.05
|
||||
const val AURA_FIRE_TICKS = 60
|
||||
const val DEFAULT_LIGHTNING_RADIUS = 5.0
|
||||
const val DEFAULT_LIGHTNING_DAMAGE = 3.0
|
||||
const val DEFAULT_LIGHTNING_BOLT_COUNT = 5
|
||||
const val DEFAULT_BOLT_STAGGER_TICKS = 8L
|
||||
const val DEFAULT_AURA_CHANCE = 0.05
|
||||
const val DEFAULT_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.0–1.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 defensiveActive = NoActive(Playstyle.DEFENSIVE)
|
||||
private val aggressivePassive = TeslaPassive(
|
||||
playstyle = Playstyle.AGGRESSIVE
|
||||
)
|
||||
private val defensivePassive = TeslaPassive(
|
||||
playstyle = Playstyle.DEFENSIVE
|
||||
)
|
||||
private val defensiveActive = NoActive( Playstyle.DEFENSIVE )
|
||||
private val aggressivePassive = TeslaPassive( Playstyle.AGGRESSIVE )
|
||||
private val defensivePassive = TeslaPassive( Playstyle.DEFENSIVE )
|
||||
|
||||
// ── Playstyle routing ─────────────────────────────────────────────────────
|
||||
|
||||
override fun getActiveAbility(
|
||||
playstyle: Playstyle
|
||||
) = when (playstyle) {
|
||||
): ActiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressiveActive
|
||||
Playstyle.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
) = when (playstyle) {
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
if ( playstyle != Playstyle.AGGRESSIVE )
|
||||
return
|
||||
if ( playstyle != Playstyle.AGGRESSIVE ) return
|
||||
|
||||
val item = ItemBuilder( Material.LIGHTNING_ROD )
|
||||
.name( aggressiveActive.name )
|
||||
@@ -119,6 +167,8 @@ class TeslaKit : Kit() {
|
||||
player.inventory.addItem( item )
|
||||
}
|
||||
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
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 rng = Random()
|
||||
|
||||
override val kitId: String
|
||||
get() = "tesla"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.items.rod.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.items.rod.description" )
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.LIGHTNING_ROD
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 15
|
||||
|
||||
@@ -156,32 +211,37 @@ class TeslaKit : Kit() {
|
||||
|
||||
val world = player.world
|
||||
|
||||
repeat( LIGHTNING_BOLT_COUNT ) { index ->
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
if ( !player.isOnline )
|
||||
return@runTaskLater
|
||||
// Snapshot all config values at activation time so a mid-round
|
||||
// config change cannot alter a bolt sequence already in flight.
|
||||
val capturedRadius = lightningRadius
|
||||
val capturedDamage = lightningDamage
|
||||
val capturedBoltCount = lightningBoltCount
|
||||
val capturedStaggerTicks = boltStaggerTicks
|
||||
|
||||
// Zufällige Position innerhalb des Radius
|
||||
val angle = rng.nextDouble() * 2.0 * Math.PI
|
||||
val dist = rng.nextDouble() * LIGHTNING_RADIUS
|
||||
repeat( capturedBoltCount ) { index ->
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
if ( !player.isOnline ) return@runTaskLater
|
||||
|
||||
val angle = rng.nextDouble() * 2.0 * Math.PI
|
||||
val dist = rng.nextDouble() * capturedRadius
|
||||
val strikeLoc = player.location.clone().add(
|
||||
cos( angle ) * dist,
|
||||
0.0,
|
||||
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
|
||||
|
||||
// Nur visueller Effekt – KEIN Block-/Feuer-Schaden
|
||||
// Visual only — no block/fire damage
|
||||
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 )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { victim ->
|
||||
victim.damage( LIGHTNING_DAMAGE, player )
|
||||
victim.damage( capturedDamage, player )
|
||||
victim.world.spawnParticle(
|
||||
Particle.ELECTRIC_SPARK,
|
||||
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
|
||||
)
|
||||
|
||||
}, index * BOLT_STAGGER_TICKS )
|
||||
}, index * capturedStaggerTicks )
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Passive Aura – Rückschlag + Brandschaden im Umkreis (beide Playstyles)
|
||||
// Passive – counter-shock aura (both playstyles)
|
||||
// =========================================================================
|
||||
|
||||
class TeslaPassive(
|
||||
inner class TeslaPassive(
|
||||
playstyle: Playstyle
|
||||
) : PassiveAbility( playstyle ) {
|
||||
) : PassiveAbility( playstyle )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val rng = Random()
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.description" )
|
||||
|
||||
@@ -225,23 +287,23 @@ class TeslaKit : Kit() {
|
||||
attacker: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if ( rng.nextDouble() > AURA_CHANCE )
|
||||
return
|
||||
// Snapshot at hit time so a config change mid-round is consistent
|
||||
if ( rng.nextDouble() > auraChance ) return
|
||||
|
||||
attacker.fireTicks = AURA_FIRE_TICKS
|
||||
val capturedFireTicks = auraFireTicks
|
||||
|
||||
attacker.fireTicks = capturedFireTicks
|
||||
|
||||
attacker.world.spawnParticle(
|
||||
Particle.ELECTRIC_SPARK,
|
||||
attacker.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
10, 0.3, 0.4, 0.3, 0.06
|
||||
)
|
||||
|
||||
victim.world.spawnParticle(
|
||||
Particle.ELECTRIC_SPARK,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
6, 0.6, 0.6, 0.6, 0.02
|
||||
)
|
||||
|
||||
victim.world.playSound(
|
||||
victim.location,
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,63 +27,124 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
/**
|
||||
* ## TridentKit
|
||||
*
|
||||
* | Playstyle | Fähigkeit |
|
||||
* |-------------|----------------------------------------------------------------------------|
|
||||
* | AGGRESSIVE | **Dive**: 3 Charges – Hochsprung, bei Landung schlägt Blitz ein |
|
||||
* | DEFENSIVE | **Parry**: 20 % Chance – Angreifer abprallen + Slowness I (2 s) |
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|-----------------------------------------------------------------------------|------------------------------------------------|
|
||||
* | AGGRESSIVE | **Dive** – [maxDiveCharges] charges; launches up, lightning strikes on land | – |
|
||||
* | DEFENSIVE | – | **Parry** – [parryChance] % bounce + Slowness |
|
||||
*
|
||||
* ### Dive-Mechanismus
|
||||
* `hitsRequired = 0` → Fähigkeit ist immer READY; interne [diveCharges] verwalten
|
||||
* die 3 Sprünge einer Sequenz. Coodown [SEQUENCE_COOLDOWN_MS] gilt nur zwischen
|
||||
* vollständigen Sequenzen (wenn alle Charges verbraucht wurden).
|
||||
* ### Dive mechanic
|
||||
* `hitsRequired = 0` → ability is always READY; internal [diveCharges] manage
|
||||
* the per-sequence uses. [sequenceCooldownMs] applies only between full sequences
|
||||
* (when all charges are exhausted).
|
||||
*
|
||||
* Jeder Charge-Verbrauch startet einen 1-Tick-Monitor:
|
||||
* 1. Warte auf Velocity-Wechsel (aufwärts → abwärts)
|
||||
* 2. Sobald Block unterhalb solid → [triggerLightningStrike]
|
||||
* Each charge consumption starts a 1-tick landing monitor:
|
||||
* 1. Wait for velocity to flip (ascending → descending).
|
||||
* 2. Once the block below is solid → [triggerLightningStrike].
|
||||
*
|
||||
* ### Parry-Mechanismus
|
||||
* [onHitByEnemy] mit 20 % Chance + Dreizack-Check (Haupt- oder Offhand).
|
||||
* ## Konfiguration (via `SPEEDHG_CUSTOM_SETTINGS`)
|
||||
*
|
||||
* 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 (0–1) 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
|
||||
|
||||
override val id: String
|
||||
get() = "trident"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "kits.trident.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "kits.trident.lore" )
|
||||
|
||||
override val icon: Material
|
||||
get() = Material.TRIDENT
|
||||
|
||||
/** Verbleibende Dive-Charges: 0 = neue Sequenz erforderlich. */
|
||||
internal val diveCharges: MutableMap<UUID, Int> = ConcurrentHashMap()
|
||||
private val diveMonitors: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
||||
private val lastSequenceTime: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
/** Players who have recently launched a dive and should not receive fall damage. */
|
||||
internal val noFallDamagePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
companion object {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// ── Gecachte Instanzen ────────────────────────────────────────────────────
|
||||
// ── 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.0–1.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()
|
||||
private val diveMonitors: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
||||
private val lastSequenceTime: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
/** Players who have recently launched a dive and must not receive fall damage. */
|
||||
internal val noFallDamagePlayers: MutableSet<UUID> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
// ── Cached ability instances (avoid allocating per event call) ────────────
|
||||
private val aggressiveActive = DiveActive()
|
||||
private val defensiveActive = NoActive( Playstyle.DEFENSIVE )
|
||||
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
|
||||
private val defensivePassive = ParryPassive()
|
||||
|
||||
// ── Playstyle routing ─────────────────────────────────────────────────────
|
||||
|
||||
override fun getActiveAbility(
|
||||
playstyle: Playstyle
|
||||
) = when( playstyle )
|
||||
): ActiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressiveActive
|
||||
Playstyle.DEFENSIVE -> defensiveActive
|
||||
@@ -91,12 +152,14 @@ class TridentKit : Kit() {
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
) = when( playstyle )
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(
|
||||
@@ -109,7 +172,7 @@ class TridentKit : Kit() {
|
||||
"kits.trident.item.trident.defensive.name"
|
||||
|
||||
val trident = ItemBuilder( Material.TRIDENT )
|
||||
.name(plugin.languageManager.getDefaultRawMessage( nameKey ))
|
||||
.name( plugin.languageManager.getDefaultRawMessage( nameKey ) )
|
||||
.lore(listOf(
|
||||
plugin.languageManager.getDefaultRawMessage(
|
||||
if ( playstyle == Playstyle.AGGRESSIVE )
|
||||
@@ -126,6 +189,8 @@ class TridentKit : Kit() {
|
||||
player.inventory.addItem( trident )
|
||||
}
|
||||
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
@@ -137,7 +202,7 @@ class TridentKit : Kit() {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Dive: Landungs-Monitor
|
||||
// Dive: landing monitor
|
||||
// =========================================================================
|
||||
|
||||
private fun startDiveMonitor(
|
||||
@@ -146,14 +211,14 @@ class TridentKit : Kit() {
|
||||
diveMonitors.remove( player.uniqueId )?.cancel()
|
||||
|
||||
var wasAscending = true
|
||||
var elapsed = 0
|
||||
var elapsed = 0
|
||||
|
||||
val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
|
||||
elapsed++
|
||||
|
||||
// Safety-Timeout: 10 Sekunden
|
||||
// Safety timeout: 10 seconds
|
||||
if ( elapsed > 200 || !player.isOnline ||
|
||||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ))
|
||||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
|
||||
{
|
||||
diveMonitors.remove( player.uniqueId )?.cancel()
|
||||
return@runTaskTimer
|
||||
@@ -180,7 +245,7 @@ class TridentKit : Kit() {
|
||||
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
|
||||
}
|
||||
@@ -188,46 +253,55 @@ class TridentKit : Kit() {
|
||||
private fun triggerLightningStrike(
|
||||
player: Player
|
||||
) {
|
||||
val loc = player.location
|
||||
val loc = player.location
|
||||
val world = loc.world ?: return
|
||||
|
||||
world.strikeLightningEffect( loc )
|
||||
world.playSound( loc, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1f, 0.75f )
|
||||
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>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { enemy ->
|
||||
enemy.damage( LIGHTNING_DAMAGE, player )
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.SLOWNESS, 40, 0, false, false, true
|
||||
))
|
||||
enemy.damage( capturedDamage, player )
|
||||
enemy.addPotionEffect(
|
||||
PotionEffect( PotionEffectType.SLOWNESS, 40, 0, false, false, true )
|
||||
)
|
||||
}
|
||||
|
||||
val remaining = diveCharges.getOrDefault( player.uniqueId, 0 )
|
||||
val msgKey = if ( remaining > 0 ) "kits.trident.messages.charges_left"
|
||||
else "kits.trident.messages.sequence_done"
|
||||
player.sendActionBar(player.trans( msgKey, "charges" to remaining.toString() ))
|
||||
val msgKey = if ( remaining > 0 ) "kits.trident.messages.charges_left"
|
||||
else "kits.trident.messages.sequence_done"
|
||||
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
|
||||
|
||||
override val kitId: String
|
||||
get() = "trident"
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.description" )
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.TRIDENT
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 0
|
||||
|
||||
@@ -235,19 +309,23 @@ class TridentKit : Kit() {
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
val now = System.currentTimeMillis()
|
||||
val now = System.currentTimeMillis()
|
||||
val charges = diveCharges.getOrDefault( player.uniqueId, 0 )
|
||||
|
||||
// Snapshot config values at activation time
|
||||
val capturedMaxCharges = maxDiveCharges
|
||||
val capturedCooldownMs = sequenceCooldownMs
|
||||
|
||||
if ( charges <= 0 )
|
||||
{
|
||||
val lastSeq = lastSequenceTime[ player.uniqueId ] ?: 0L
|
||||
if ( now - lastSeq < SEQUENCE_COOLDOWN_MS )
|
||||
if ( now - lastSeq < capturedCooldownMs )
|
||||
{
|
||||
val secLeft = ( SEQUENCE_COOLDOWN_MS - ( now - lastSeq )) / 1000
|
||||
return AbilityResult.ConditionNotMet("Cooldown: ${secLeft}s")
|
||||
val secLeft = ( capturedCooldownMs - ( now - lastSeq ) ) / 1000
|
||||
return AbilityResult.ConditionNotMet( "Cooldown: ${secLeft}s" )
|
||||
}
|
||||
lastSequenceTime[ player.uniqueId ] = now
|
||||
diveCharges[ player.uniqueId ] = MAX_DIVE_CHARGES - 1
|
||||
diveCharges[ player.uniqueId ] = capturedMaxCharges - 1
|
||||
}
|
||||
else diveCharges[ player.uniqueId ] = charges - 1
|
||||
|
||||
@@ -255,19 +333,16 @@ class TridentKit : Kit() {
|
||||
noFallDamagePlayers.add( player.uniqueId )
|
||||
|
||||
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(
|
||||
Particle.ELECTRIC_SPARK,
|
||||
player.location.clone().add( 0.0, 0.5, 0.0 ),
|
||||
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 )
|
||||
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 rng = Random()
|
||||
|
||||
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
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.trident.passive.defensive.description")
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.passive.defensive.description" )
|
||||
|
||||
override fun onHitByEnemy(
|
||||
victim: Player,
|
||||
attacker: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if ( rng.nextDouble() >= PARRY_CHANCE ) return
|
||||
// Snapshot at hit time
|
||||
if ( rng.nextDouble() >= parryChance ) return
|
||||
|
||||
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
|
||||
|
||||
val capturedSlownessTicks = parrySlownessTicks
|
||||
|
||||
attacker.velocity = attacker.location.toVector()
|
||||
.subtract( victim.location.toVector() )
|
||||
.normalize()
|
||||
.multiply( 1.7 )
|
||||
.setY( 0.45 )
|
||||
|
||||
attacker.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.SLOWNESS, PARRY_SLOWNESS_TICKS, 0, false, false, true
|
||||
))
|
||||
attacker.addPotionEffect(
|
||||
PotionEffect( PotionEffectType.SLOWNESS, capturedSlownessTicks, 0, false, false, true )
|
||||
)
|
||||
|
||||
victim.world.spawnParticle(
|
||||
Particle.SWEEP_ATTACK,
|
||||
victim.location.clone().add( 0.0, 1.0, 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.ENTITY_LIGHTNING_BOLT_IMPACT, 0.3f, 1.9f )
|
||||
|
||||
victim.sendActionBar(victim.trans( "kits.trident.messages.parry_success" ))
|
||||
attacker.sendActionBar(attacker.trans( "kits.trident.messages.parried_by_victim" ))
|
||||
victim.sendActionBar( victim.trans( "kits.trident.messages.parry_success" ) )
|
||||
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) {
|
||||
override val name = "None"
|
||||
override val description = "None"
|
||||
private class NoPassive(
|
||||
playstyle: Playstyle
|
||||
) : PassiveAbility( playstyle )
|
||||
{
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
|
||||
override val description: String
|
||||
get() = "None"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package club.mcscrims.speedhg.kit.impl
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.config.CustomGameSettings
|
||||
import club.mcscrims.speedhg.kit.Kit
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
||||
@@ -26,10 +25,29 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.cos
|
||||
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(
|
||||
var remainingCapacity: Double = 15.0,
|
||||
var remainingCapacity: Double,
|
||||
val expireTask: BukkitTask,
|
||||
val particleTask: BukkitTask
|
||||
)
|
||||
@@ -49,11 +67,27 @@ class VenomKit : Kit() {
|
||||
override val icon: Material
|
||||
get() = Material.SPIDER_EYE
|
||||
|
||||
private val kitOverride: CustomGameSettings.KitOverride by lazy {
|
||||
plugin.customGameManager.settings.kits.kits["venom"]
|
||||
?: CustomGameSettings.KitOverride()
|
||||
companion object {
|
||||
const val DEFAULT_SHIELD_DURATION_TICKS = 160L
|
||||
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) ────────────
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
private val defensiveActive = DefensiveActive()
|
||||
@@ -112,7 +146,7 @@ class VenomKit : Kit() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Optional lifecycle hooks ──────────────────────────────────────────────
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
@@ -122,11 +156,15 @@ class VenomKit : Kit() {
|
||||
shield.particleTask.cancel()
|
||||
activeShields.remove( player.uniqueId )
|
||||
}
|
||||
val items = cachedItems.remove( player.uniqueId ) ?: return
|
||||
items.forEach { player.inventory.remove( it ) }
|
||||
cachedItems.remove( player.uniqueId )?.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
|
||||
|
||||
@@ -160,19 +198,24 @@ class VenomKit : Kit() {
|
||||
) { target ->
|
||||
target.addPotionEffects(listOf(
|
||||
PotionEffect( PotionEffectType.BLINDNESS, 100, 0 ),
|
||||
PotionEffect( PotionEffectType.WITHER, 100, 0 )
|
||||
PotionEffect( PotionEffectType.WITHER, 100, 0 )
|
||||
))
|
||||
target.damage( 4.0, player )
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -195,33 +238,38 @@ class VenomKit : Kit() {
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
if (activeShields.containsKey( player.uniqueId ))
|
||||
if ( activeShields.containsKey( player.uniqueId ) )
|
||||
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 )
|
||||
|
||||
val particleTask = object : BukkitRunnable() {
|
||||
|
||||
val particleTask = object : BukkitRunnable()
|
||||
{
|
||||
var rotation = 0.0
|
||||
|
||||
override fun run()
|
||||
{
|
||||
if ( !player.isOnline ||
|
||||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ) ||
|
||||
!activeShields.containsKey( player.uniqueId ))
|
||||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ) ||
|
||||
!activeShields.containsKey( player.uniqueId ) )
|
||||
{
|
||||
this.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
val loc = player.location
|
||||
val loc = player.location
|
||||
val radius = 1.2
|
||||
|
||||
for ( i in 0 until 8 )
|
||||
{
|
||||
val angle = ( 2 * Math.PI * i / 8 ) + rotation
|
||||
val x = cos( angle ) * radius
|
||||
val z = sin( angle ) * radius
|
||||
val x = cos( angle ) * radius
|
||||
val z = sin( angle ) * radius
|
||||
|
||||
loc.world.spawnParticle(
|
||||
Particle.LARGE_SMOKE,
|
||||
@@ -233,27 +281,33 @@ class VenomKit : Kit() {
|
||||
}
|
||||
}.runTaskTimer( plugin, 0L, 2L )
|
||||
|
||||
val expireTask = object : BukkitRunnable() {
|
||||
val expireTask = object : BukkitRunnable()
|
||||
{
|
||||
override fun run()
|
||||
{
|
||||
if (activeShields.containsKey( player.uniqueId ))
|
||||
if ( activeShields.containsKey( player.uniqueId ) )
|
||||
breakShield( player )
|
||||
}
|
||||
}.runTaskLater( plugin, kitOverride.shieldDurationTicks )
|
||||
}.runTaskLater( plugin, capturedDurationTicks )
|
||||
|
||||
activeShields[ player.uniqueId ] = ActiveShield(
|
||||
remainingCapacity = kitOverride.shieldCapacity,
|
||||
expireTask = expireTask,
|
||||
particleTask = particleTask
|
||||
remainingCapacity = capturedCapacity,
|
||||
expireTask = expireTask,
|
||||
particleTask = particleTask
|
||||
)
|
||||
|
||||
player.sendActionBar(player.trans( "kits.venom.messages.shield_activate" ))
|
||||
player.sendActionBar( player.trans( "kits.venom.messages.shield_activate" ) )
|
||||
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
|
||||
|
||||
@@ -268,16 +322,21 @@ class VenomKit : Kit() {
|
||||
attacker: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
activeShields[victim.uniqueId]?.apply {
|
||||
activeShields[ victim.uniqueId ]?.apply {
|
||||
remainingCapacity -= event.damage
|
||||
event.damage = if (event.isCritical) 3.0 else 2.0
|
||||
if (remainingCapacity <= 0) breakShield(victim)
|
||||
event.damage = if ( event.isCritical ) 3.0 else 2.0
|
||||
if ( remainingCapacity <= 0 ) breakShield( victim )
|
||||
} ?: return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class NoPassive : PassiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
// =========================================================================
|
||||
// AGGRESSIVE passive – stub (no passive ability)
|
||||
// =========================================================================
|
||||
|
||||
private class NoPassive : PassiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
@@ -287,7 +346,7 @@ class VenomKit : Kit() {
|
||||
|
||||
}
|
||||
|
||||
// ── Helper methods ──────────────────────────────────────────────
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────
|
||||
|
||||
private fun breakShield(
|
||||
player: Player
|
||||
@@ -298,7 +357,7 @@ class VenomKit : Kit() {
|
||||
shield.particleTask.cancel()
|
||||
|
||||
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" ) )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package club.mcscrims.speedhg.kit.impl
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.config.CustomGameSettings
|
||||
import club.mcscrims.speedhg.kit.Kit
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
||||
@@ -26,83 +25,130 @@ import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* ## Voodoo
|
||||
* ## VoodooKit
|
||||
*
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|-----------------------------------------------------|---------------------------------------------|
|
||||
* | 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|
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|----------------------------------------------------------|------------------------------------------------|
|
||||
* | AGGRESSIVE | Root enemy if HP < 50 % (5 s hold) | 20 % chance to apply Wither on hit |
|
||||
* | DEFENSIVE | Curse nearby enemies for [curseDurationMs] ms | Speed + Regen while cursed enemies are nearby |
|
||||
*
|
||||
* ### 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.
|
||||
*
|
||||
* ### Curse mechanic (DEFENSIVE)
|
||||
* Cursed players are stored in [cursedExpiry] (UUID → expiry timestamp).
|
||||
* A per-player repeating task (started in [DefensivePassive.onActivate]) checks
|
||||
* every second: cleans expired curses, applies debuffs to still-cursed enemies,
|
||||
* and grants Speed + Regen to the Voodoo player if at least one cursed enemy
|
||||
* is within 10 blocks.
|
||||
* Cursed players are stored in [cursedExpiry] (UUID → expiry timestamp ms).
|
||||
* A per-player repeating task checks every second: cleans expired curses,
|
||||
* applies Slowness + MiningFatigue to still-cursed enemies, and grants
|
||||
* Speed + Regen to the Voodoo player if at least one cursed enemy is within
|
||||
* 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
|
||||
|
||||
override val id = "voodoo"
|
||||
override val id: String
|
||||
get() = "voodoo"
|
||||
|
||||
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>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList("kits.voodoo.lore")
|
||||
override val icon = Material.WITHER_ROSE
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "kits.voodoo.lore" )
|
||||
|
||||
/** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */
|
||||
internal val cursedExpiry: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
override val icon: Material
|
||||
get() = Material.WITHER_ROSE
|
||||
|
||||
private val kitOverride: CustomGameSettings.KitOverride by lazy {
|
||||
plugin.customGameManager.settings.kits.kits["voodoo"]
|
||||
?: CustomGameSettings.KitOverride()
|
||||
companion object {
|
||||
const val DEFAULT_CURSE_DURATION_MS = 15_000L
|
||||
}
|
||||
|
||||
// ── 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 defensiveActive = DefensiveActive()
|
||||
private val aggressivePassive = AggressivePassive()
|
||||
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.DEFENSIVE -> defensiveActive
|
||||
}
|
||||
override fun getPassiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||
|
||||
override fun getPassiveAbility(
|
||||
playstyle: Playstyle
|
||||
): PassiveAbility = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> aggressivePassive
|
||||
Playstyle.DEFENSIVE -> defensivePassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(player: Player, playstyle: Playstyle) {
|
||||
val (mat, active) = when (playstyle) {
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
val ( mat, active ) = when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE -> Material.WITHER_ROSE to aggressiveActive
|
||||
Playstyle.DEFENSIVE -> Material.SOUL_TORCH to defensiveActive
|
||||
}
|
||||
val item = ItemBuilder(mat)
|
||||
.name(active.name)
|
||||
.lore(listOf(active.description))
|
||||
|
||||
val item = ItemBuilder( mat )
|
||||
.name( active.name )
|
||||
.lore(listOf( active.description ))
|
||||
.build()
|
||||
cachedItems[player.uniqueId] = listOf(item)
|
||||
player.inventory.addItem(item)
|
||||
|
||||
cachedItems[ player.uniqueId ] = listOf( item )
|
||||
player.inventory.addItem( item )
|
||||
}
|
||||
|
||||
override fun onRemove(player: Player) {
|
||||
cursedExpiry.remove(player.uniqueId)
|
||||
cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) }
|
||||
// ── Lifecycle hooks ───────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
// =========================================================================
|
||||
|
||||
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) {
|
||||
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
@@ -110,62 +156,78 @@ class VoodooKit : Kit() {
|
||||
get() = "voodoo"
|
||||
|
||||
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
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.root.description")
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.items.root.description" )
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 15
|
||||
override val triggerMaterial = Material.WITHER_ROSE
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
val target = player.getTargetEntity(6) as? Player
|
||||
?: return AbilityResult.ConditionNotMet("No player in line of sight!")
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.WITHER_ROSE
|
||||
|
||||
if (!plugin.gameManager.alivePlayers.contains(target.uniqueId))
|
||||
return AbilityResult.ConditionNotMet("Target is not alive!")
|
||||
override fun execute(
|
||||
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 (target.health > maxHp * 0.5)
|
||||
return AbilityResult.ConditionNotMet("Target must be below 50 % health!")
|
||||
if ( !plugin.gameManager.alivePlayers.contains( target.uniqueId ) )
|
||||
return AbilityResult.ConditionNotMet( "Target is not alive!" )
|
||||
|
||||
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 ────────────────────────────────────────────────────
|
||||
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.GLOWING, 5 * 20, 0, false, false, false))
|
||||
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.GLOWING, 5 * 20, 0, false, false, false ) )
|
||||
|
||||
// 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))
|
||||
|
||||
override fun run()
|
||||
{
|
||||
if ( ticks++ >= 100 || !target.isOnline ||
|
||||
!plugin.gameManager.alivePlayers.contains( target.uniqueId ) )
|
||||
{
|
||||
target.removePotionEffect(PotionEffectType.GLOWING)
|
||||
cancel(); return
|
||||
target.removePotionEffect( PotionEffectType.GLOWING )
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
val v = target.velocity
|
||||
target.velocity = v.clone().setX(0.0).setZ(0.0)
|
||||
.let { if (it.y > 0.0) it.setY(0.0) else it }
|
||||
target.velocity = v.clone().setX( 0.0 ).setZ( 0.0 )
|
||||
.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)
|
||||
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),
|
||||
20, 0.4, 0.6, 0.4, 0.02)
|
||||
player.playSound( player.location, Sound.ENTITY_WITHER_SHOOT, 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 ),
|
||||
20, 0.4, 0.6, 0.4, 0.02
|
||||
)
|
||||
|
||||
player.sendActionBar(player.trans("kits.voodoo.messages.root_activated"))
|
||||
target.sendActionBar(target.trans("kits.voodoo.messages.root_received"))
|
||||
player.sendActionBar( player.trans( "kits.voodoo.messages.root_activated" ) )
|
||||
target.sendActionBar( target.trans( "kits.voodoo.messages.root_received" ) )
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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
|
||||
|
||||
@@ -173,119 +235,169 @@ class VoodooKit : Kit() {
|
||||
get() = "voodoo"
|
||||
|
||||
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
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.items.curse.description")
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.items.curse.description" )
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
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
|
||||
.getNearbyEntities(player.location, 8.0, 8.0, 8.0)
|
||||
.getNearbyEntities( player.location, 8.0, 8.0, 8.0 )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
|
||||
if (targets.isEmpty())
|
||||
return AbilityResult.ConditionNotMet("No enemies within 8 blocks!")
|
||||
if ( targets.isEmpty() )
|
||||
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 ->
|
||||
cursedExpiry[t.uniqueId] = expiry
|
||||
t.addPotionEffect(PotionEffect(PotionEffectType.GLOWING, 15 * 20, 0, false, true, false))
|
||||
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)
|
||||
cursedExpiry[ t.uniqueId ] = expiry
|
||||
t.addPotionEffect(
|
||||
PotionEffect( PotionEffectType.GLOWING, 15 * 20, 0, false, true, false )
|
||||
)
|
||||
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.sendActionBar(player.trans("kits.voodoo.messages.curse_cast",
|
||||
mapOf("count" to targets.size.toString())))
|
||||
player.playSound( player.location, Sound.ENTITY_WITHER_AMBIENT, 1f, 0.3f )
|
||||
player.sendActionBar(
|
||||
player.trans(
|
||||
"kits.voodoo.messages.curse_cast",
|
||||
mapOf( "count" to targets.size.toString() )
|
||||
)
|
||||
)
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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 rng = Random()
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.aggressive.name")
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.aggressive.description")
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.aggressive.name" )
|
||||
|
||||
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) {
|
||||
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)
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.aggressive.description" )
|
||||
|
||||
override fun onHitEnemy(
|
||||
attacker: Player,
|
||||
victim: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
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 tasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.defensive.name")
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage("kits.voodoo.passive.defensive.description")
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.defensive.name" )
|
||||
|
||||
override fun onActivate(player: Player) {
|
||||
val task = object : BukkitRunnable() {
|
||||
override fun run() {
|
||||
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) {
|
||||
cancel(); return
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.voodoo.passive.defensive.description" )
|
||||
|
||||
override fun onActivate(
|
||||
player: Player
|
||||
) {
|
||||
val task = object : BukkitRunnable()
|
||||
{
|
||||
override fun run()
|
||||
{
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
|
||||
{
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
runCatching { tickPassive( player ) }
|
||||
.onFailure { plugin.logger.severe( "[VoodooKit] tickPassive error: ${it.message}" ) }
|
||||
}
|
||||
}.runTaskTimer(plugin, 0L, 20L)
|
||||
tasks[player.uniqueId] = task
|
||||
}.runTaskTimer( plugin, 0L, 20L )
|
||||
|
||||
tasks[ player.uniqueId ] = task
|
||||
}
|
||||
|
||||
override fun onDeactivate(player: Player) {
|
||||
tasks.remove(player.uniqueId)?.cancel()
|
||||
override fun onDeactivate(
|
||||
player: Player
|
||||
) {
|
||||
tasks.remove( player.uniqueId )?.cancel()
|
||||
}
|
||||
|
||||
private fun tickPassive(voodooPlayer: Player) {
|
||||
private fun tickPassive(
|
||||
voodooPlayer: Player
|
||||
) {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// ── Expire stale curses ───────────────────────────────────────────
|
||||
cursedExpiry.entries.removeIf { (uuid, expiry) ->
|
||||
if (now > expiry) {
|
||||
Bukkit.getPlayer(uuid)?.removePotionEffect(PotionEffectType.GLOWING)
|
||||
cursedExpiry.entries.removeIf { ( uuid, expiry ) ->
|
||||
if ( now > expiry )
|
||||
{
|
||||
Bukkit.getPlayer( uuid )?.removePotionEffect( PotionEffectType.GLOWING )
|
||||
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
|
||||
.mapNotNull { Bukkit.getPlayer(it) }
|
||||
.filter { it.isOnline && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
|
||||
.mapNotNull { Bukkit.getPlayer( it ) }
|
||||
.filter { it.isOnline && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.onEach { cursed ->
|
||||
cursed.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 30, 0))
|
||||
cursed.addPotionEffect(PotionEffect(PotionEffectType.MINING_FATIGUE, 30, 0))
|
||||
cursed.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, 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 ─────────────────────
|
||||
if (cursedNearby.isNotEmpty()) {
|
||||
voodooPlayer.addPotionEffect(PotionEffect(PotionEffectType.SPEED, 30, 0))
|
||||
voodooPlayer.addPotionEffect(PotionEffect(PotionEffectType.REGENERATION, 30, 0))
|
||||
// ── Buff voodoo player if a cursed enemy is nearby ────────────────
|
||||
if ( cursedNearby.isNotEmpty() )
|
||||
{
|
||||
voodooPlayer.addPotionEffect( PotionEffect( PotionEffectType.SPEED, 30, 0 ) )
|
||||
voodooPlayer.addPotionEffect( PotionEffect( PotionEffectType.REGENERATION, 30, 0 ) )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class ItemBuilder(
|
||||
name: String
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.editMeta { it.displayName(Component.text( name )) }
|
||||
itemStack.editMeta { it.displayName(mm.deserialize( name )) }
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user