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:
TDSTOS
2026-04-12 05:38:22 +02:00
parent 8ff4dee6bf
commit 18ee9937f8
5 changed files with 467 additions and 157 deletions

View File

@@ -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,16 +122,30 @@ 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()
val snowBall = ItemBuilder( Material.SNOWBALL )
.name( defensiveActive.name )
.lore(listOf( defensiveActive.description ))
.build()
cachedItems[ player.uniqueId ] = listOf( spikeItem )
player.inventory.addItem( spikeItem )
}
cachedItems[ player.uniqueId ] = listOf( snowBall )
player.inventory.addItem( snowBall )
Playstyle.DEFENSIVE ->
{
val snowBall = ItemBuilder( Material.SNOWBALL )
.name( defensiveActive.name )
.lore(listOf( defensiveActive.description ))
.build()
cachedItems[ player.uniqueId ] = listOf( snowBall )
player.inventory.addItem( snowBall )
}
}
}
// ── Optional lifecycle hooks ──────────────────────────────────────────────
@@ -152,39 +153,142 @@ class IceMageKit : Kit()
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( 3 ) < 1 )
{
victim.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 60, 0 ))
if ( random.nextInt( capturedDenom ) < 1 )
victim.addPotionEffect( PotionEffect( PotionEffectType.SLOWNESS, slowTicks, 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
)
}
}
}
private class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE )
{
// =========================================================================
// 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 )
}
}

View File

@@ -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 )
enemy.playSound( enemy.location, Sound.ENTITY_ENDERMAN_TELEPORT, 0.4f, 0.7f )
// 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,81 +325,99 @@ 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 capturedEffectTicks = smokeEffectTicks
val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
if ( !player.isOnline ||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
{
smokeTasks.remove( player.uniqueId )?.cancel()
return@runTaskTimer
}
spawnSmokeRing( player, capturedRadius )
applyEffectsToEnemies( player, capturedRadius, capturedEffectTicks )
}, 0L, capturedRefreshTicks )
smokeTasks[ player.uniqueId ] = task
// Schedule automatic aura expiry
val capturedRadius = smokeRadius
val capturedDurationTicks = smokeDurationTicks
Bukkit.getScheduler().runTaskLater( plugin, { ->
smokeTasks.remove( player.uniqueId )?.cancel()
}, capturedDurationTicks )
val capturedRefreshTicks = smokeRefreshTicks
val capturedEffectTicks = smokeEffectTicks
player.playSound( player.location, Sound.ENTITY_ENDERMAN_AMBIENT, 0.7f, 1.8f )
// 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()
{
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ) )
{
this.cancel()
smokeTasks.remove( player.uniqueId )
return
}
ticksElapsed += capturedRefreshTicks
if ( ticksElapsed >= capturedDurationTicks )
{
this.cancel()
smokeTasks.remove( player.uniqueId )
// 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
}
// ── Spawn smoke ring particles (animated circle) ──────────────────
val loc = player.location
for ( i in 0 until 16 )
{
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 )
smokeTasks[ player.uniqueId ] = auraTask
player.sendActionBar(player.trans( "kits.ninja.messages.smoke_activated" ))
return AbilityResult.Success
}
// ── Rendering helpers (private to this inner class) ───────────────────
private fun spawnSmokeRing(
player: Player,
radius: Double
) {
val center = player.location
val steps = 10
for ( i in 0 until steps )
{
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
)
}
}
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
))
}
}
}
// =========================================================================

View File

@@ -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 )
}
}

View File

@@ -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
)
}
}

View File

@@ -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'
description: 'Gain speed in cold biomes and give enemies slowness'
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>'