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:
@@ -6,6 +6,7 @@ import club.mcscrims.speedhg.game.GameManager
|
|||||||
import club.mcscrims.speedhg.game.modules.AntiRunningManager
|
import club.mcscrims.speedhg.game.modules.AntiRunningManager
|
||||||
import club.mcscrims.speedhg.kit.KitManager
|
import club.mcscrims.speedhg.kit.KitManager
|
||||||
import club.mcscrims.speedhg.kit.impl.GoblinKit
|
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.kit.listener.KitEventDispatcher
|
||||||
import club.mcscrims.speedhg.listener.ConnectListener
|
import club.mcscrims.speedhg.listener.ConnectListener
|
||||||
import club.mcscrims.speedhg.listener.GameStateListener
|
import club.mcscrims.speedhg.listener.GameStateListener
|
||||||
@@ -53,6 +54,7 @@ class SpeedHG : JavaPlugin() {
|
|||||||
kitManager = KitManager( this )
|
kitManager = KitManager( this )
|
||||||
// Register kits
|
// Register kits
|
||||||
kitManager.registerKit( GoblinKit() )
|
kitManager.registerKit( GoblinKit() )
|
||||||
|
kitManager.registerKit( IceMageKit() )
|
||||||
|
|
||||||
registerCommands()
|
registerCommands()
|
||||||
registerListener()
|
registerListener()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package club.mcscrims.speedhg.command
|
package club.mcscrims.speedhg.command
|
||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.kit.Playstyle
|
||||||
import club.mcscrims.speedhg.util.legacySerializer
|
import club.mcscrims.speedhg.util.legacySerializer
|
||||||
import club.mcscrims.speedhg.util.sendMsg
|
import club.mcscrims.speedhg.util.sendMsg
|
||||||
import org.bukkit.command.Command
|
import org.bukkit.command.Command
|
||||||
@@ -28,7 +29,7 @@ class KitCommand : CommandExecutor, TabCompleter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( args.isNullOrEmpty() )
|
if ( args.isNullOrEmpty() || args.size < 2 )
|
||||||
{
|
{
|
||||||
player.sendMsg( "commands.kit.usage" )
|
player.sendMsg( "commands.kit.usage" )
|
||||||
return true
|
return true
|
||||||
@@ -43,8 +44,18 @@ class KitCommand : CommandExecutor, TabCompleter {
|
|||||||
return true
|
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 )
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +72,9 @@ class KitCommand : CommandExecutor, TabCompleter {
|
|||||||
if ( args.size == 1 )
|
if ( args.size == 1 )
|
||||||
return plugin.kitManager.getRegisteredKits().map { it.id }
|
return plugin.kitManager.getRegisteredKits().map { it.id }
|
||||||
|
|
||||||
|
if ( args.size == 2 )
|
||||||
|
return Playstyle.entries.map { it.name.lowercase() }
|
||||||
|
|
||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
|
|
||||||
class GoblinKit : Kit() {
|
class GoblinKit : Kit() {
|
||||||
|
|
||||||
private val plugin = SpeedHG.instance
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
override val id: String
|
override val id: String
|
||||||
get() = "goblin"
|
get() = "goblin"
|
||||||
@@ -104,7 +104,7 @@ class GoblinKit : Kit() {
|
|||||||
items.forEach { player.inventory.remove( it ) }
|
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
|
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
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
|
|||||||
203
src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt
Normal file
203
src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt
Normal 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"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,16 +4,28 @@ import club.mcscrims.speedhg.SpeedHG
|
|||||||
import club.mcscrims.speedhg.game.GameState
|
import club.mcscrims.speedhg.game.GameState
|
||||||
import club.mcscrims.speedhg.kit.KitManager
|
import club.mcscrims.speedhg.kit.KitManager
|
||||||
import club.mcscrims.speedhg.kit.ability.AbilityResult
|
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.Component
|
||||||
import net.kyori.adventure.text.format.NamedTextColor
|
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.Player
|
||||||
|
import org.bukkit.entity.Snowball
|
||||||
import org.bukkit.event.EventHandler
|
import org.bukkit.event.EventHandler
|
||||||
import org.bukkit.event.EventPriority
|
import org.bukkit.event.EventPriority
|
||||||
import org.bukkit.event.Listener
|
import org.bukkit.event.Listener
|
||||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
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.PlayerInteractEvent
|
||||||
import org.bukkit.event.player.PlayerMoveEvent
|
import org.bukkit.event.player.PlayerMoveEvent
|
||||||
import org.bukkit.inventory.EquipmentSlot
|
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.
|
* 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 )
|
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
|
// Helpers
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -45,9 +45,10 @@ craft:
|
|||||||
|
|
||||||
commands:
|
commands:
|
||||||
kit:
|
kit:
|
||||||
usage: '<red>Usage: /kit <kitName></red>'
|
usage: '<red>Usage: /kit <kitName> <playstyle></red>'
|
||||||
kitNotFound: '<red><kit> is not a registered kit!</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:
|
scoreboard:
|
||||||
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'
|
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ depend:
|
|||||||
commands:
|
commands:
|
||||||
kit:
|
kit:
|
||||||
description: 'Select kits via command'
|
description: 'Select kits via command'
|
||||||
usage: '/kit <kitName>'
|
usage: '/kit <kitName> <playstyle>'
|
||||||
Reference in New Issue
Block a user