Add ability feedback/particles; refactor kits
Introduce centralized AbilityFeedback and AbilityParticles utilities and update language strings. Refactor IceMage: replace old snowball mechanics with an aggressive Ice Spike Burst (cone projectiles, cooldown tracking, config keys) and a defensive snowball barrage with metadata; reorganize config accessors and passive behaviors. Refactor Ninja: add layered sounds/particles for teleport, rewrite smoke aura as an animated BukkitRunnable with visual rotation and enemy effects, and improve lifecycle/scheduling handling. Update en_US.yml with new kit item names, passive labels, and messages.
This commit is contained in:
@@ -10,6 +10,7 @@ import club.mcscrims.speedhg.util.ItemBuilder
|
||||
import club.mcscrims.speedhg.util.trans
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Particle
|
||||
import org.bukkit.Sound
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||
@@ -17,28 +18,31 @@ import org.bukkit.event.player.PlayerMoveEvent
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.potion.PotionEffect
|
||||
import org.bukkit.potion.PotionEffectType
|
||||
import org.bukkit.util.Vector
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
/**
|
||||
* ## IceMageKit
|
||||
*
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|-------------------------------------------------------------------|-----------------------------------------------------|
|
||||
* | AGGRESSIVE | – | Speed I in ice biomes; [slowChance] Slowness on hit |
|
||||
* | DEFENSIVE | **Snowball** – throws a 360° ring of frozen snowballs | – |
|
||||
* | Playstyle | Aktive Fähigkeit | Passive |
|
||||
* |-------------|------------------------------------------------------------|-----------------------------------------|
|
||||
* | AGGRESSIVE | **Ice Spike Burst** – Cone of freezing projectiles (3×) | Speed II in snowy biomes |
|
||||
* | DEFENSIVE | **Snowball Barrage** – 16 projectiles in circle pattern | Slowness proc (33%) on hits |
|
||||
*
|
||||
* ## 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 |
|
||||
* | JSON Key | Type | Default | Description |
|
||||
* |----------------------------|--------|---------|----------------------------------------|
|
||||
* | `spike_cooldown_ms` | Long | 8000 | Cooldown between spike bursts |
|
||||
* | `spike_range_blocks` | Double | 12.0 | How far spike cone extends |
|
||||
* | `spike_cone_width` | Double | 60.0 | Cone angle in degrees (wider = spread) |
|
||||
* | `spike_freeze_ticks` | Int | 60 | Duration of freeze effect (ticks) |
|
||||
* | `snowball_slowness_chance` | Double | 0.33 | Chance to apply Slowness on hit |
|
||||
*/
|
||||
class IceMageKit : Kit()
|
||||
{
|
||||
@@ -58,50 +62,33 @@ class IceMageKit : Kit()
|
||||
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
|
||||
const val DEFAULT_SPIKE_COOLDOWN_MS = 8000L
|
||||
const val DEFAULT_SPIKE_RANGE_BLOCKS = 12.0
|
||||
const val DEFAULT_SPIKE_CONE_WIDTH = 60.0 // degrees
|
||||
const val DEFAULT_SPIKE_FREEZE_TICKS = 60
|
||||
const val DEFAULT_SNOWBALL_SLOWNESS_CHANCE = 0.33
|
||||
}
|
||||
|
||||
// ── 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
|
||||
private val spikeCooldownMs: Long
|
||||
get() = override().getLong( "spike_cooldown_ms" ) ?: DEFAULT_SPIKE_COOLDOWN_MS
|
||||
|
||||
/**
|
||||
* 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
|
||||
private val spikeRangeBlocks: Double
|
||||
get() = override().getDouble( "spike_range_blocks" ) ?: DEFAULT_SPIKE_RANGE_BLOCKS
|
||||
|
||||
/**
|
||||
* Launch speed of each snowball in the ring.
|
||||
* JSON key: `snowball_speed`
|
||||
*/
|
||||
private val snowballSpeed: Double
|
||||
get() = override().getDouble( "snowball_speed" ) ?: DEFAULT_SNOWBALL_SPEED
|
||||
private val spikeConeWidth: Double
|
||||
get() = override().getDouble( "spike_cone_width" ) ?: DEFAULT_SPIKE_CONE_WIDTH
|
||||
|
||||
/**
|
||||
* 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
|
||||
private val spikeFreezeTicks: Int
|
||||
get() = override().getInt( "spike_freeze_ticks" ) ?: DEFAULT_SPIKE_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
|
||||
private val snowballSlowChance: Double
|
||||
get() = override().getDouble( "snowball_slowness_chance" ) ?: DEFAULT_SNOWBALL_SLOWNESS_CHANCE
|
||||
|
||||
// ── State tracking ────────────────────────────────────────────────────────
|
||||
|
||||
internal val spikeCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
|
||||
|
||||
// ── Cached ability instances (avoid allocating per event call) ────────────
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
@@ -135,9 +122,21 @@ class IceMageKit : Kit()
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
if ( playstyle != Playstyle.DEFENSIVE )
|
||||
return
|
||||
when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE ->
|
||||
{
|
||||
val spikeItem = ItemBuilder( Material.BLUE_ICE )
|
||||
.name( aggressiveActive.name )
|
||||
.lore(listOf( aggressiveActive.description ))
|
||||
.build()
|
||||
|
||||
cachedItems[ player.uniqueId ] = listOf( spikeItem )
|
||||
player.inventory.addItem( spikeItem )
|
||||
}
|
||||
|
||||
Playstyle.DEFENSIVE ->
|
||||
{
|
||||
val snowBall = ItemBuilder( Material.SNOWBALL )
|
||||
.name( defensiveActive.name )
|
||||
.lore(listOf( defensiveActive.description ))
|
||||
@@ -146,45 +145,150 @@ class IceMageKit : Kit()
|
||||
cachedItems[ player.uniqueId ] = listOf( snowBall )
|
||||
player.inventory.addItem( snowBall )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Optional lifecycle hooks ──────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
spikeCooldowns.remove( player.uniqueId )
|
||||
val items = cachedItems.remove( player.uniqueId ) ?: return
|
||||
items.forEach { player.inventory.remove( it ) }
|
||||
}
|
||||
|
||||
private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
// =========================================================================
|
||||
// AGGRESSIVE active – Ice Spike Burst
|
||||
// =========================================================================
|
||||
|
||||
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val kitId: String
|
||||
get() = "icemage"
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.ice_spike.name" )
|
||||
|
||||
override val description: String
|
||||
get() = "None"
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.ice_spike.description" )
|
||||
|
||||
override val hardcodedHitsRequired: Int
|
||||
get() = 15
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.BARRIER
|
||||
get() = Material.BLUE_ICE
|
||||
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
val now = System.currentTimeMillis()
|
||||
val lastUse = spikeCooldowns[ player.uniqueId ] ?: 0L
|
||||
|
||||
if ( now - lastUse < spikeCooldownMs )
|
||||
{
|
||||
val secLeft = ( spikeCooldownMs - ( now - lastUse )) / 1000
|
||||
player.sendActionBar(player.trans( "kits.icemage.messages.spike_cooldown", "time" to secLeft.toString() ))
|
||||
return AbilityResult.ConditionNotMet( "On cooldown" )
|
||||
}
|
||||
|
||||
// Snapshot config at execution time
|
||||
val capturedRange = spikeRangeBlocks
|
||||
val capturedConeWidth = spikeConeWidth
|
||||
val capturedFreezeTicks = spikeFreezeTicks
|
||||
|
||||
// Play charge-up sound
|
||||
player.playSound( player.location, Sound.ENTITY_FIREWORK_ROCKET_LAUNCH, 1f, 0.7f )
|
||||
|
||||
// Spawn cone of spikes
|
||||
val direction = player.eyeLocation.direction.normalize()
|
||||
val right = direction.clone().crossProduct(Vector( 0.0, 1.0, 0.0 )).normalize()
|
||||
val up = right.crossProduct( direction ).normalize()
|
||||
|
||||
val coneHalfAngle = capturedConeWidth / 2.0
|
||||
|
||||
val spikeCount = 8
|
||||
for ( i in 0 until spikeCount )
|
||||
{
|
||||
val verticalAngle = ( ( i / spikeCount.toDouble() ) - 0.5 ) * coneHalfAngle
|
||||
val horizontalAngle = ( ( i % 4 ) / 4.0 - 0.5 ) * coneHalfAngle
|
||||
|
||||
val vertRad = Math.toRadians( verticalAngle )
|
||||
val horizRad = Math.toRadians( horizontalAngle )
|
||||
|
||||
val cosV = cos( vertRad )
|
||||
val sinV = sin( vertRad )
|
||||
val cosH = cos( horizRad )
|
||||
val sinH = sin( horizRad )
|
||||
|
||||
val spikeDir = direction.clone()
|
||||
.multiply( cosV * cosH )
|
||||
.add( right.clone().multiply( sinH * cosV ) )
|
||||
.add( up.clone().multiply( sinV ) )
|
||||
.normalize()
|
||||
|
||||
val startLoc = player.eyeLocation.clone().add( spikeDir.clone().multiply( 1.5 ) )
|
||||
|
||||
// Spawn particles along spike path
|
||||
var currentLoc = startLoc.clone()
|
||||
while ( currentLoc.distance( startLoc ) < capturedRange )
|
||||
{
|
||||
currentLoc.world.spawnParticle(
|
||||
Particle.SNOWFLAKE,
|
||||
currentLoc,
|
||||
3, 0.1, 0.1, 0.1, 0.0
|
||||
)
|
||||
|
||||
// Check for entity hits
|
||||
val nearbyEnemies = currentLoc.world
|
||||
.getNearbyEntities( currentLoc, 0.5, 0.5, 0.5 )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
|
||||
for ( enemy in nearbyEnemies )
|
||||
{
|
||||
enemy.freezeTicks = capturedFreezeTicks
|
||||
enemy.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, capturedFreezeTicks, 1 ))
|
||||
|
||||
// Impact feedback
|
||||
enemy.world.spawnParticle(
|
||||
Particle.LARGE_SMOKE,
|
||||
enemy.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
12, 0.3, 0.3, 0.3, 0.05
|
||||
)
|
||||
enemy.world.playSound( enemy.location, Sound.BLOCK_GLASS_BREAK, 1f, 1.5f )
|
||||
currentLoc = currentLoc.add( spikeDir.clone().multiply( 50.0 ) ) // Exit ray
|
||||
break
|
||||
}
|
||||
|
||||
currentLoc.add( spikeDir.clone().multiply( 0.5 ) )
|
||||
}
|
||||
}
|
||||
|
||||
// Blast sound + particle burst
|
||||
player.world.playSound( player.location, Sound.ENTITY_BLAZE_SHOOT, 1f, 1.8f )
|
||||
player.world.spawnParticle(
|
||||
Particle.SNOWFLAKE,
|
||||
player.eyeLocation,
|
||||
30, 0.5, 0.5, 0.5, 0.1
|
||||
)
|
||||
|
||||
spikeCooldowns[ player.uniqueId ] = now
|
||||
player.sendActionBar(player.trans( "kits.icemage.messages.spike_fired" ))
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE )
|
||||
{
|
||||
// =========================================================================
|
||||
// DEFENSIVE active – Snowball Barrage
|
||||
// =========================================================================
|
||||
|
||||
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
@@ -207,15 +311,44 @@ class IceMageKit : Kit()
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
player.playSound( player.location, Sound.ENTITY_PLAYER_HURT_FREEZE, 1f, 1.5f )
|
||||
player.sendActionBar( player.trans( "kits.icemage.messages.shoot_snowballs" ) )
|
||||
// Multi-shot sound
|
||||
player.playSound( player.location, Sound.ENTITY_BLAZE_SHOOT, 0.8f, 1.2f )
|
||||
player.sendActionBar(player.trans( "kits.icemage.messages.shoot_snowballs" ))
|
||||
|
||||
val amountOfSnowballs = 16
|
||||
val playerLocation = player.location
|
||||
val baseSpeed = 1.5
|
||||
|
||||
for ( i in 0 until amountOfSnowballs )
|
||||
{
|
||||
val angle = i * ( 2 * Math.PI / amountOfSnowballs )
|
||||
|
||||
val x = cos( angle )
|
||||
val z = sin( angle )
|
||||
|
||||
val direction = Vector( x, 0.0, z ).normalize().multiply( baseSpeed )
|
||||
|
||||
val snowBall = player.world.spawn( playerLocation, org.bukkit.entity.Snowball::class.java )
|
||||
snowBall.shooter = player
|
||||
snowBall.velocity = direction
|
||||
|
||||
snowBall.persistentDataContainer.set(
|
||||
org.bukkit.NamespacedKey( plugin, "icemage_snowball" ),
|
||||
org.bukkit.persistence.PersistentDataType.BYTE,
|
||||
1.toByte()
|
||||
)
|
||||
}
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE )
|
||||
{
|
||||
// =========================================================================
|
||||
// AGGRESSIVE passive – Biome Speed Boost
|
||||
// =========================================================================
|
||||
|
||||
private class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val random = Random()
|
||||
@@ -232,18 +365,18 @@ class IceMageKit : Kit()
|
||||
)
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.name" )
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.aggressive.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.description" )
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.aggressive.description" )
|
||||
|
||||
override fun onMove(
|
||||
player: Player,
|
||||
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(
|
||||
@@ -251,23 +384,53 @@ class IceMageKit : Kit()
|
||||
victim: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
// 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 )
|
||||
if (random.nextInt( 3 ) < 1 )
|
||||
{
|
||||
victim.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 60, 0 ))
|
||||
|
||||
// Feedback: brief ice particle burst on victim
|
||||
victim.world.spawnParticle(
|
||||
Particle.SNOWFLAKE,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
8, 0.2, 0.2, 0.2, 0.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE passive – Slowness Proc
|
||||
// =========================================================================
|
||||
|
||||
private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val random = Random()
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.defensive.name" )
|
||||
|
||||
override val description: String
|
||||
get() = "None"
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.defensive.description" )
|
||||
|
||||
override fun onHitEnemy(
|
||||
attacker: Player,
|
||||
victim: Player,
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
if (random.nextDouble() >= snowballSlowChance) return
|
||||
|
||||
victim.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 80, 1 ))
|
||||
|
||||
// Feedback: particle puff + sound
|
||||
victim.world.spawnParticle(
|
||||
Particle.LARGE_SMOKE,
|
||||
victim.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
6, 0.2, 0.3, 0.2, 0.0
|
||||
)
|
||||
victim.world.playSound( victim.location, Sound.BLOCK_GLASS_BREAK, 0.6f, 0.9f )
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -258,22 +258,42 @@ class NinjaKit : Kit() {
|
||||
dest.yaw = enemy.location.yaw
|
||||
dest.pitch = 0f
|
||||
|
||||
// ── Departure Particles + Sound ───────────────────────────────────────
|
||||
|
||||
player.world.spawnParticle(
|
||||
Particle.LARGE_SMOKE,
|
||||
player.location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
25, 0.3, 0.5, 0.3, 0.05
|
||||
)
|
||||
|
||||
// Layered sounds: departure "whoosh" + teleport
|
||||
player.playSound( player.location, Sound.ENTITY_ENDER_PEARL_THROW, 0.8f, 1.5f )
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
player.playSound( player.location, Sound.ENTITY_ENDERMAN_TELEPORT, 0.7f, 1.8f )
|
||||
}, 2L )
|
||||
|
||||
// ── Teleport ──────────────────────────────────────────────────────────
|
||||
|
||||
player.teleport( dest )
|
||||
|
||||
player.world.spawnParticle(
|
||||
// ── Arrival Particles + Sound ─────────────────────────────────────────
|
||||
|
||||
dest.world.spawnParticle(
|
||||
Particle.LARGE_SMOKE,
|
||||
dest.clone().add( 0.0, 1.0, 0.0 ),
|
||||
25, 0.3, 0.5, 0.3, 0.05
|
||||
)
|
||||
|
||||
player.playSound( player.location, Sound.ENTITY_ENDERMAN_TELEPORT, 0.7f, 1.8f )
|
||||
// Brief particle ring around destination (visual confirmation)
|
||||
dest.world.spawnParticle(
|
||||
Particle.FLAME,
|
||||
dest.clone().add( 0.0, 0.5, 0.0 ),
|
||||
12, 0.5, 0.1, 0.5, 0.0
|
||||
)
|
||||
|
||||
player.playSound( dest, Sound.ENTITY_ENDERMAN_TELEPORT, 0.4f, 0.7f )
|
||||
enemy.playSound( enemy.location, Sound.ENTITY_ENDERMAN_TELEPORT, 0.4f, 0.7f )
|
||||
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.teleported" ))
|
||||
}
|
||||
|
||||
@@ -305,80 +325,98 @@ class NinjaKit : Kit() {
|
||||
|
||||
// Snapshot the config values at activation time so mid-round changes
|
||||
// don't alter an already-running aura unexpectedly.
|
||||
val capturedRefreshTicks = smokeRefreshTicks
|
||||
val capturedRadius = smokeRadius
|
||||
val capturedDurationTicks = smokeDurationTicks
|
||||
val capturedRefreshTicks = smokeRefreshTicks
|
||||
val capturedEffectTicks = smokeEffectTicks
|
||||
|
||||
val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
|
||||
if ( !player.isOnline ||
|
||||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
|
||||
// Play activation sound (magical, layered)
|
||||
player.playSound( player.location, Sound.ENTITY_WARDEN_SONIC_CHARGE, 0.8f, 1.2f )
|
||||
player.playSound( player.location, Sound.BLOCK_RESPAWN_ANCHOR_CHARGE, 0.6f, 0.9f )
|
||||
|
||||
val auraTask = object : org.bukkit.scheduler.BukkitRunnable() {
|
||||
|
||||
var ticksElapsed = 0L
|
||||
var rotation = 0.0
|
||||
|
||||
override fun run()
|
||||
{
|
||||
smokeTasks.remove( player.uniqueId )?.cancel()
|
||||
return@runTaskTimer
|
||||
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
|
||||
{
|
||||
this.cancel()
|
||||
smokeTasks.remove( player.uniqueId )
|
||||
return
|
||||
}
|
||||
|
||||
spawnSmokeRing( player, capturedRadius )
|
||||
applyEffectsToEnemies( player, capturedRadius, capturedEffectTicks )
|
||||
ticksElapsed += capturedRefreshTicks
|
||||
|
||||
}, 0L, capturedRefreshTicks )
|
||||
if ( ticksElapsed >= capturedDurationTicks )
|
||||
{
|
||||
this.cancel()
|
||||
smokeTasks.remove( player.uniqueId )
|
||||
|
||||
smokeTasks[ player.uniqueId ] = task
|
||||
|
||||
// Schedule automatic aura expiry
|
||||
val capturedDurationTicks = smokeDurationTicks
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
smokeTasks.remove( player.uniqueId )?.cancel()
|
||||
}, capturedDurationTicks )
|
||||
|
||||
player.playSound( player.location, Sound.ENTITY_ENDERMAN_AMBIENT, 0.7f, 1.8f )
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.smoke_activated" ))
|
||||
return AbilityResult.Success
|
||||
// Deactivation sound & feedback
|
||||
player.world.playSound( player.location, Sound.BLOCK_FIRE_EXTINGUISH, 1f, 0.8f )
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.smoke_expired" ))
|
||||
return
|
||||
}
|
||||
|
||||
// ── Rendering helpers (private to this inner class) ───────────────────
|
||||
// ── Spawn smoke ring particles (animated circle) ──────────────────
|
||||
|
||||
private fun spawnSmokeRing(
|
||||
player: Player,
|
||||
radius: Double
|
||||
) {
|
||||
val center = player.location
|
||||
val steps = 10
|
||||
val loc = player.location
|
||||
|
||||
for ( i in 0 until steps )
|
||||
for ( i in 0 until 16 )
|
||||
{
|
||||
val angle = i * ( 2.0 * Math.PI / steps )
|
||||
center.world.spawnParticle(
|
||||
Particle.CAMPFIRE_COSY_SMOKE,
|
||||
center.clone().add(
|
||||
cos( angle ) * radius,
|
||||
0.8,
|
||||
sin( angle ) * radius
|
||||
),
|
||||
1, 0.05, 0.12, 0.05, 0.004
|
||||
val angle = ( 2 * Math.PI * i / 16 ) + rotation
|
||||
val x = cos( angle ) * capturedRadius
|
||||
val z = sin( angle ) * capturedRadius
|
||||
|
||||
loc.world.spawnParticle(
|
||||
Particle.LARGE_SMOKE,
|
||||
loc.clone().add( x, 0.8, z ),
|
||||
2, 0.1, 0.1, 0.1, 0.05
|
||||
)
|
||||
}
|
||||
|
||||
// Rotate ring for visual animation
|
||||
rotation += 0.2
|
||||
|
||||
// ── Apply Blindness + Slowness to nearby enemies ──────────────────
|
||||
|
||||
loc.world.getNearbyEntities( loc, capturedRadius, capturedRadius, capturedRadius )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { enemy ->
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.BLINDNESS,
|
||||
capturedEffectTicks,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
))
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.SLOWNESS,
|
||||
capturedEffectTicks,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
))
|
||||
|
||||
// Brief particle flash on hit (visual feedback)
|
||||
enemy.world.spawnParticle(
|
||||
Particle.SMOKE,
|
||||
enemy.location.clone().add( 0.0, 0.8, 0.0 ),
|
||||
4, 0.2, 0.2, 0.2, 0.0
|
||||
)
|
||||
}
|
||||
}
|
||||
}.runTaskTimer( plugin, 0L, capturedRefreshTicks )
|
||||
|
||||
private fun applyEffectsToEnemies(
|
||||
player: Player,
|
||||
radius: Double,
|
||||
effectTicks: Int
|
||||
) {
|
||||
player.location.world
|
||||
.getNearbyEntities( player.location, radius, 2.0, radius )
|
||||
.filterIsInstance<Player>()
|
||||
.filter { it != player &&
|
||||
plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
|
||||
.forEach { enemy ->
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.BLINDNESS, effectTicks, 0,
|
||||
false, false, true
|
||||
))
|
||||
enemy.addPotionEffect(PotionEffect(
|
||||
PotionEffectType.SLOWNESS, effectTicks, 0,
|
||||
false, false, true
|
||||
))
|
||||
}
|
||||
smokeTasks[ player.uniqueId ] = auraTask
|
||||
player.sendActionBar(player.trans( "kits.ninja.messages.smoke_activated" ))
|
||||
return AbilityResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package club.mcscrims.speedhg.util
|
||||
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Sound
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
object AbilityFeedback {
|
||||
|
||||
// ── Charge Feedback ────────────────────────────────────────────────────
|
||||
|
||||
fun playChargeReady( player: Player ) {
|
||||
player.playSound( player.location, Sound.BLOCK_BEACON_POWER_SELECT, 1f, 1.6f )
|
||||
}
|
||||
|
||||
fun playCharging( player: Player ) {
|
||||
player.playSound( player.location, Sound.BLOCK_COMPARATOR_CLICK, 0.6f, 1.2f )
|
||||
}
|
||||
|
||||
// ── Activation Feedback ────────────────────────────────────────────────
|
||||
|
||||
fun playActivation( player: Player ) {
|
||||
player.playSound( player.location, Sound.ENTITY_FIREWORK_ROCKET_LAUNCH, 0.8f, 1.0f )
|
||||
}
|
||||
|
||||
// ── Impact Feedback ───────────────────────────────────────────────────
|
||||
|
||||
fun playImpact( location: Location ) {
|
||||
location.world?.playSound( location, Sound.BLOCK_ANVIL_LAND, 1f, 1.2f )
|
||||
}
|
||||
|
||||
// ── Cooldown Feedback ──────────────────────────────────────────────────
|
||||
|
||||
fun playCooldownExpired( player: Player ) {
|
||||
player.playSound( player.location, Sound.BLOCK_NOTE_BLOCK_PLING, 1f, 1.8f )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package club.mcscrims.speedhg.util
|
||||
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Particle
|
||||
|
||||
object AbilityParticles {
|
||||
|
||||
// ── Directional Impact Ring ────────────────────────────────────────────
|
||||
|
||||
fun spawnImpactRing( center: Location, radius: Double = 1.5 ) {
|
||||
val particleCount = 12
|
||||
for ( i in 0 until particleCount ) {
|
||||
val angle = ( 2 * Math.PI * i / particleCount )
|
||||
val x = kotlin.math.cos( angle ) * radius
|
||||
val z = kotlin.math.sin( angle ) * radius
|
||||
|
||||
center.world?.spawnParticle(
|
||||
Particle.FLAME,
|
||||
center.clone().add( x, 0.5, z ),
|
||||
1, 0.0, 0.0, 0.0, 0.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Charge Build Indicator ────────────────────────────────────────────
|
||||
|
||||
fun spawnChargeRing( center: Location, scale: Double ) {
|
||||
val radius = 0.5 + ( scale * 1.5 )
|
||||
center.world?.spawnParticle(
|
||||
Particle.ELECTRIC_SPARK,
|
||||
center,
|
||||
8,
|
||||
radius, radius, radius,
|
||||
0.1
|
||||
)
|
||||
}
|
||||
|
||||
// ── Cooldown Indicator ────────────────────────────────────────────────
|
||||
|
||||
fun spawnCooldownIndicator( location: Location ) {
|
||||
location.world?.spawnParticle(
|
||||
Particle.FLAME,
|
||||
location.clone().add( 0.0, 1.0, 0.0 ),
|
||||
6,
|
||||
0.3, 0.3, 0.3,
|
||||
0.0
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -315,6 +315,7 @@ kits:
|
||||
- ' '
|
||||
- 'Select a kit mid-round at any time.'
|
||||
- 'All kits are available to pick.'
|
||||
|
||||
gladiator:
|
||||
name: '<gradient:dark_gray:gray><bold>Gladiator</bold></gradient>'
|
||||
lore:
|
||||
@@ -327,6 +328,7 @@ kits:
|
||||
description: 'Fight an enemy in a 1v1 above the skies'
|
||||
messages:
|
||||
ability_charged: '<yellow>Your ability has been recharged!</yellow>'
|
||||
|
||||
goblin:
|
||||
name: '<gradient:dark_green:gray><bold>Goblin</bold></gradient>'
|
||||
lore:
|
||||
@@ -344,6 +346,7 @@ kits:
|
||||
stole_kit: '<green>You have stolen the kit of your opponent (Kit: <kit>)!</green>'
|
||||
spawn_bunker: '<green>You have created a bunker around yourself!'
|
||||
ability_charged: '<yellow>Your ability has been recharged!</yellow>'
|
||||
|
||||
icemage:
|
||||
name: '<gradient:dark_aqua:aqua><bold>IceMage</bold></gradient>'
|
||||
lore:
|
||||
@@ -352,14 +355,24 @@ kits:
|
||||
- 'DEFENSIVE: Summon snowballs and freeze enemies'
|
||||
items:
|
||||
snowball:
|
||||
name: '§bFreeze'
|
||||
name: '<aqua>❄️ Freeze</aqua>'
|
||||
description: 'Freeze your enemies by throwing snowballs in all directions'
|
||||
ice_spike:
|
||||
name: '<aqua>❄️ Ice Spike Burst</aqua>'
|
||||
description: 'Right-click: Fire cone of freezing projectiles'
|
||||
passive:
|
||||
name: '§bIceStorm'
|
||||
aggressive:
|
||||
name: '<aqua>IceStorm</aqua>'
|
||||
description: 'Gain speed in cold biomes and give enemies slowness'
|
||||
defensive:
|
||||
name: '<aqua>Slowness Proc</aqua>'
|
||||
description: '33% chance to slow enemies when you hit them'
|
||||
messages:
|
||||
shoot_snowballs: '<aqua>You have shot frozen snowballs in all directions!</aqua>'
|
||||
ability_charged: '<yellow>Your ability has been recharged!</yellow>'
|
||||
spike_fired: '<aqua>❄️ Spikes fired!</aqua>'
|
||||
spike_cooldown: '<red>Cooldown: <time>s remaining'
|
||||
|
||||
venom:
|
||||
name: '<gradient:dark_red:red><bold>Venom</bold></gradient>'
|
||||
lore:
|
||||
@@ -381,6 +394,7 @@ kits:
|
||||
shield_activate: '<gray>Your shield of darkness has been activated!</gray>'
|
||||
shield_break: '<red>Your shield of darkness has broken!</red>'
|
||||
ability_charged: '<yellow>Your ability has been recharged</yellow>'
|
||||
|
||||
rattlesnake:
|
||||
name: '<gradient:green:dark_green><bold>Rattlesnake</bold></gradient>'
|
||||
lore:
|
||||
@@ -402,6 +416,7 @@ kits:
|
||||
pounce_hit: '<green>Pounce hit <count> target(s)! Poison II applied!</green>'
|
||||
pounce_miss: '<red>Pounce missed! Nearby enemies were disoriented.</red>'
|
||||
venom_proc: '<green>Counter Venom triggered!</green>'
|
||||
|
||||
armorer:
|
||||
name: '<gradient:gray:white><bold>Armorer</bold></gradient>'
|
||||
lore:
|
||||
@@ -418,6 +433,7 @@ kits:
|
||||
messages:
|
||||
armor_upgraded: '<gold>Armor upgraded to tier <tier>!</gold>'
|
||||
armor_replaced: '<yellow>Broken armor replaced automatically.</yellow>'
|
||||
|
||||
voodoo:
|
||||
name: '<gradient:dark_purple:light_purple><bold>Voodoo</bold></gradient>'
|
||||
lore:
|
||||
@@ -444,6 +460,7 @@ kits:
|
||||
curse_cast: '<light_purple>Cursed <count> enemy(s) for 15 seconds!</light_purple>'
|
||||
curse_received: '<red>You have been cursed by a Voodoo player!</red>'
|
||||
ability_charged: '<yellow>Ability recharged!</yellow>'
|
||||
|
||||
blackpanther:
|
||||
name: '<gradient:dark_gray:white><bold>Black Panther</bold></gradient>'
|
||||
lore:
|
||||
@@ -465,6 +482,7 @@ kits:
|
||||
fist_mode_active: '<gray>⚡ Vibranium Fists active for 12 seconds!</gray>'
|
||||
wakanda_impact: '<white>Wakanda Forever! Hit <count> enemy(s)!</white>'
|
||||
ability_charged: '<yellow>Ability recharged!</yellow>'
|
||||
|
||||
theworld:
|
||||
name: '<gradient:dark_gray:white><bold>The World</bold></gradient>'
|
||||
lore:
|
||||
@@ -492,6 +510,7 @@ kits:
|
||||
frozen_expired: '<gray>The freeze has worn off.</gray>'
|
||||
freeze_broken: '<gold>Freeze broken — 5 hits reached!</gold>'
|
||||
freeze_hits_left: '<aqua>Frozen enemy — <hits> hit(s) remaining.</aqua>'
|
||||
|
||||
tesla:
|
||||
name: '<gradient:yellow:aqua><bold>Tesla</bold></gradient>'
|
||||
lore:
|
||||
@@ -551,6 +570,7 @@ kits:
|
||||
anchor_placed: '<gray>Anchor deployed! Radius: <radius> blocks.'
|
||||
anchor_destroyed: '<red>⚓ Your anchor was destroyed!'
|
||||
ability_charged: '<gray>Anchor ready to deploy!'
|
||||
|
||||
ninja:
|
||||
name: '<gradient:dark_gray:white><bold>Ninja</bold></gradient>'
|
||||
lore:
|
||||
@@ -562,10 +582,12 @@ kits:
|
||||
name: '<dark_gray>Smoke Bomb</dark_gray>'
|
||||
description: 'Enemies in 3-block radius get Blindness + Slowness'
|
||||
messages:
|
||||
teleported: '<gray>Teleported behind enemy!'
|
||||
cooldown: '<red>Teleport on cooldown – <time>s'
|
||||
teleport_cooldown: '<dark_red>Teleport on cooldown – <time>s'
|
||||
no_target: '<red>No recent target! Hit an enemy first.'
|
||||
target_expired: '<red>Target expired (10 s window).'
|
||||
target_expired: '<red>Target expired (10s window).'
|
||||
teleported: '<dark_gray>◆ Teleported behind target'
|
||||
smoke_activated: '<dark_gray>◆ Smoke aura activated!'
|
||||
smoke_expired: '<gray>◆ Smoke expired.'
|
||||
|
||||
trident:
|
||||
name: '<gradient:aqua:blue><bold>Trident</bold></gradient>'
|
||||
|
||||
Reference in New Issue
Block a user