From 18ee9937f8c859934a1c0b3d426ba5a794a9990e Mon Sep 17 00:00:00 2001 From: TDSTOS Date: Sun, 12 Apr 2026 05:38:22 +0200 Subject: [PATCH] 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. --- .../mcscrims/speedhg/kit/impl/IceMageKit.kt | 319 +++++++++++++----- .../mcscrims/speedhg/kit/impl/NinjaKit.kt | 184 ++++++---- .../mcscrims/speedhg/util/AbilityFeedback.kt | 37 ++ .../mcscrims/speedhg/util/AbilityParticles.kt | 50 +++ src/main/resources/languages/en_US.yml | 34 +- 5 files changed, 467 insertions(+), 157 deletions(-) create mode 100644 src/main/kotlin/club/mcscrims/speedhg/util/AbilityFeedback.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/util/AbilityParticles.kt diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt index 579dd89..223c1d1 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt @@ -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 = 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() + .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 ) + } } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt index 2d85366..eb4151b 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt @@ -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() + .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() - .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 - )) - } - } } // ========================================================================= diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/AbilityFeedback.kt b/src/main/kotlin/club/mcscrims/speedhg/util/AbilityFeedback.kt new file mode 100644 index 0000000..e6475ac --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/util/AbilityFeedback.kt @@ -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 ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/AbilityParticles.kt b/src/main/kotlin/club/mcscrims/speedhg/util/AbilityParticles.kt new file mode 100644 index 0000000..26324c9 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/util/AbilityParticles.kt @@ -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 + ) + } + +} \ No newline at end of file diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index 5b85735..57d5bc1 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -315,6 +315,7 @@ kits: - ' ' - 'Select a kit mid-round at any time.' - 'All kits are available to pick.' + gladiator: name: 'Gladiator' lore: @@ -327,6 +328,7 @@ kits: description: 'Fight an enemy in a 1v1 above the skies' messages: ability_charged: 'Your ability has been recharged!' + goblin: name: 'Goblin' lore: @@ -344,6 +346,7 @@ kits: stole_kit: 'You have stolen the kit of your opponent (Kit: )!' spawn_bunker: 'You have created a bunker around yourself!' ability_charged: 'Your ability has been recharged!' + icemage: name: 'IceMage' lore: @@ -352,14 +355,24 @@ kits: - 'DEFENSIVE: Summon snowballs and freeze enemies' items: snowball: - name: '§bFreeze' + name: '❄️ Freeze' description: 'Freeze your enemies by throwing snowballs in all directions' + ice_spike: + name: '❄️ Ice Spike Burst' + description: 'Right-click: Fire cone of freezing projectiles' passive: - name: '§bIceStorm' - description: 'Gain speed in cold biomes and give enemies slowness' + aggressive: + name: 'IceStorm' + description: 'Gain speed in cold biomes and give enemies slowness' + defensive: + name: 'Slowness Proc' + description: '33% chance to slow enemies when you hit them' messages: shoot_snowballs: 'You have shot frozen snowballs in all directions!' ability_charged: 'Your ability has been recharged!' + spike_fired: '❄️ Spikes fired!' + spike_cooldown: 'Cooldown: