Add IceMage kit and playstyle support

Introduce a new IceMage kit and wire playstyle support across the plugin. Changes include:

- Add IceMageKit with aggressive/defensive active & passive abilities, item distribution, and lifecycle hooks; caches ability instances and given items to reduce allocations.
- Register IceMageKit in SpeedHG on plugin startup.
- Update KitCommand to require a playstyle argument, validate it, select the kit's playstyle, and provide tab-completion for playstyles.
- Extend KitEventDispatcher with IceMage-specific handlers: spawn a circular volley of snowballs on ability use (cancelling the original projectile), mark spawned snowballs with persistent data, and apply freeze/slow effects on hit.
- Adjust GoblinKit to use a plugin getter and make inner ability classes static to avoid capturing the outer instance.
- Update language lines and plugin.yml usage to reflect the new /kit <kitName> <playstyle> usage.

These changes implement the IceMage feature and ensure proper playstyle selection and event handling while keeping allocations low and behavior consistent.
This commit is contained in:
TDSTOS
2026-03-25 05:02:59 +01:00
parent 19bd708b59
commit ea86272c01
7 changed files with 294 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ import club.mcscrims.speedhg.game.GameManager
import club.mcscrims.speedhg.game.modules.AntiRunningManager
import club.mcscrims.speedhg.kit.KitManager
import club.mcscrims.speedhg.kit.impl.GoblinKit
import club.mcscrims.speedhg.kit.impl.IceMageKit
import club.mcscrims.speedhg.kit.listener.KitEventDispatcher
import club.mcscrims.speedhg.listener.ConnectListener
import club.mcscrims.speedhg.listener.GameStateListener
@@ -53,6 +54,7 @@ class SpeedHG : JavaPlugin() {
kitManager = KitManager( this )
// Register kits
kitManager.registerKit( GoblinKit() )
kitManager.registerKit( IceMageKit() )
registerCommands()
registerListener()

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg.command
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.util.legacySerializer
import club.mcscrims.speedhg.util.sendMsg
import org.bukkit.command.Command
@@ -28,7 +29,7 @@ class KitCommand : CommandExecutor, TabCompleter {
return true
}
if ( args.isNullOrEmpty() )
if ( args.isNullOrEmpty() || args.size < 2 )
{
player.sendMsg( "commands.kit.usage" )
return true
@@ -43,8 +44,18 @@ class KitCommand : CommandExecutor, TabCompleter {
return true
}
val playstyle = Playstyle.entries.firstOrNull { it.name.equals( args[1], true ) }
if ( playstyle == null )
{
player.sendMsg( "commands.kit.playstyleNotFound", "playstyle" to args[1] )
return true
}
plugin.kitManager.selectKit( player, kit )
player.sendMsg( "commands.kit.selected", "kit" to legacySerializer.serialize( kit.displayName ))
plugin.kitManager.selectPlaystyle( player, playstyle )
player.sendMsg( "commands.kit.selected", "playstyle" to playstyle.displayName, "kit" to legacySerializer.serialize( kit.displayName ))
return true
}
@@ -61,6 +72,9 @@ class KitCommand : CommandExecutor, TabCompleter {
if ( args.size == 1 )
return plugin.kitManager.getRegisteredKits().map { it.id }
if ( args.size == 2 )
return Playstyle.entries.map { it.name.lowercase() }
return listOf()
}

View File

@@ -22,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap
class GoblinKit : Kit() {
private val plugin = SpeedHG.instance
private val plugin get() = SpeedHG.instance
override val id: String
get() = "goblin"
@@ -104,7 +104,7 @@ class GoblinKit : Kit() {
items.forEach { player.inventory.remove( it ) }
}
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
private val plugin get() = SpeedHG.instance
@@ -175,7 +175,7 @@ class GoblinKit : Kit() {
}
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
private val plugin get() = SpeedHG.instance

View File

@@ -0,0 +1,203 @@
package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult
import club.mcscrims.speedhg.kit.ability.ActiveAbility
import club.mcscrims.speedhg.kit.ability.PassiveAbility
import club.mcscrims.speedhg.util.ItemBuilder
import club.mcscrims.speedhg.util.sendMsg
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Material
import org.bukkit.Sound
import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.player.PlayerMoveEvent
import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import java.util.Random
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
class IceMageKit : Kit() {
private val plugin get() = SpeedHG.instance
override val id: String
get() = "icemage"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.icemage.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.icemage.lore" )
override val icon: Material
get() = Material.SNOWBALL
// ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive()
private val defensiveActive = DefensiveActive()
private val aggressivePassive = AggressivePassive()
private val defensivePassive = DefensivePassive()
// ── Playstyle routing ─────────────────────────────────────────────────────
override fun getActiveAbility(
playstyle: Playstyle
): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive
}
override fun getPassiveAbility(
playstyle: Playstyle
): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive
}
// ── Item distribution ─────────────────────────────────────────────────────
private val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(
player: Player,
playstyle: Playstyle
) {
if ( playstyle != Playstyle.DEFENSIVE )
return
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 ──────────────────────────────────────────────
override fun onRemove(
player: Player
) {
val items = cachedItems.remove( player.uniqueId ) ?: return
items.forEach { player.inventory.remove( it ) }
}
private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
override val name: String
get() = "None"
override val description: String
get() = "None"
override val hitsRequired: Int
get() = 0
override val triggerMaterial: Material
get() = Material.BARRIER
override fun execute(
player: Player
): AbilityResult
{
return AbilityResult.Success
}
}
private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
private val plugin get() = SpeedHG.instance
override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.name" )
override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.description" )
override val hitsRequired: Int
get() = 15
override val triggerMaterial: Material
get() = Material.SNOWBALL
override fun execute(
player: Player
): AbilityResult
{
player.playSound( player.location, Sound.ENTITY_PLAYER_HURT_FREEZE, 1f, 1.5f )
player.sendActionBar(player.trans( "kits.icemage.messages.shoot_snowballs" ))
return AbilityResult.Success
}
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f )
player.sendActionBar(player.trans( "kits.icemage.messages.ability_charged" ))
}
}
private class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE ) {
private val plugin get() = SpeedHG.instance
private val random = Random()
private val biomeList = listOf(
"taiga_cold",
"snowy_tundra",
"ice_spikes",
"snowy_beach",
"grove",
"snowy_slopes",
"jagged_peaks",
"frozen_peaks"
)
override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.name" )
override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.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 ))
}
override fun onHitEnemy(
attacker: Player,
victim: Player,
event: EntityDamageByEntityEvent
) {
if ( random.nextBoolean() )
victim.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 60, 0 ))
}
}
private class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) {
override val name: String
get() = "None"
override val description: String
get() = "None"
}
}

View File

@@ -4,16 +4,28 @@ import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.speedhg.kit.KitManager
import club.mcscrims.speedhg.kit.ability.AbilityResult
import club.mcscrims.speedhg.kit.impl.IceMageKit
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.NamespacedKey
import org.bukkit.entity.LivingEntity
import org.bukkit.entity.Player
import org.bukkit.entity.Snowball
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.entity.ProjectileHitEvent
import org.bukkit.event.entity.ProjectileLaunchEvent
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.event.player.PlayerMoveEvent
import org.bukkit.inventory.EquipmentSlot
import org.bukkit.persistence.PersistentDataType
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.util.Vector
import kotlin.math.cos
import kotlin.math.sin
/**
* The *single* Bukkit [Listener] responsible for all kit-related event handling.
@@ -171,6 +183,60 @@ class KitEventDispatcher(
kit.getPassiveAbility(kitManager.getSelectedPlaystyle( player )).onMove( player, event )
}
// =========================================================================
// IceMage Listener
// =========================================================================
private val iceMageKey = NamespacedKey( plugin, "icemage_snowball" )
@EventHandler
fun onSnowballThrow(
event: ProjectileLaunchEvent
) {
val projectile = event.entity as? Snowball ?: return
val shooter = projectile.shooter as? Player ?: return
if (kitManager.getSelectedKit( shooter ) !is IceMageKit ) return
if (kitManager.getChargeData( shooter )?.isReady == false ) return
val amountOfSnowballs = 16
val playerLocation = shooter.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 = shooter.world.spawn( playerLocation, Snowball::class.java )
snowBall.shooter = shooter
snowBall.velocity = direction
snowBall.persistentDataContainer.set( iceMageKey, PersistentDataType.BYTE, 1.toByte() )
}
event.isCancelled = true
}
@EventHandler
fun onSnowballHit(
event: ProjectileHitEvent
) {
val projectile = event.entity as? Snowball ?: return
if (!projectile.persistentDataContainer.has( iceMageKey, PersistentDataType.BYTE )) return
val hitEntity = event.hitEntity
if ( hitEntity is LivingEntity && hitEntity != projectile.shooter )
{
hitEntity.freezeTicks = 60
hitEntity.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 40, 1 ))
}
}
// =========================================================================
// Helpers
// =========================================================================

View File

@@ -45,9 +45,10 @@ craft:
commands:
kit:
usage: '<red>Usage: /kit <kitName></red>'
usage: '<red>Usage: /kit <kitName> <playstyle></red>'
kitNotFound: '<red><kit> is not a registered kit!</red>'
selected: '<green>You have selected <kit> as your Kit!</green>'
playstyleNotFound: '<red><playstyle> is not an available playstyle!</red>'
selected: '<green>You have selected <kit> as your Kit with playstyle <playstyle>!</green>'
scoreboard:
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'

View File

@@ -9,4 +9,4 @@ depend:
commands:
kit:
description: 'Select kits via command'
usage: '/kit <kitName>'
usage: '/kit <kitName> <playstyle>'