Add kit system, KitManager and Goblin kit
Introduce a full kit framework and supporting utilities. - Add core kit API: Kit, Playstyle, ActiveAbility, PassiveAbility, AbilityResult, charge state and PlayerChargeData. - Implement KitManager for registration, lobby selections, apply/remove/clear lifecycle and charge tracking. - Add KitEventDispatcher listener to centralize kit event handling (interact, hits, move) and integrate passive/active hooks. - Provide example kit implementations: GoblinKit (functional abilities) and TemplateKit (reference). - Add utilities: ItemBuilder and WorldEditUtils (WorldEdit-based sphere creation). - Integrate into plugin: SpeedHG now initialises KitManager, registers kits and KitEventDispatcher, applies kits at game start and clears on end. - LanguageManager: add default message/list/component helpers. - Build changes: bump Kotlin plugin to 2.2.0 and add WorldEdit compileOnly deps; also expose legacySerializer in Extensions. These changes implement the kit feature set (items, abilities, charge/recharge flow) and wire it into the game lifecycle.
This commit is contained in:
@@ -2,8 +2,8 @@ plugins {
|
||||
id("java")
|
||||
id("maven-publish")
|
||||
id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||
kotlin("jvm") version libs.versions.kotlin
|
||||
kotlin("kapt") version libs.versions.kotlin
|
||||
kotlin("jvm") version "2.2.0"
|
||||
kotlin("kapt") version "2.2.0"
|
||||
}
|
||||
|
||||
group = "club.mcscrims"
|
||||
@@ -17,11 +17,16 @@ repositories {
|
||||
maven("https://libraries.minecraft.net/")
|
||||
maven("https://repo.codemc.io/repository/maven-public/")
|
||||
maven("https://repo.lunarclient.dev")
|
||||
maven("https://maven.enginehub.org/repo/")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("fr.mrmicky:fastboard:2.1.3")
|
||||
|
||||
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
|
||||
|
||||
compileOnly("com.sk89q.worldedit:worldedit-core:7.2.17-SNAPSHOT")
|
||||
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.2.17-SNAPSHOT")
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -3,7 +3,12 @@ package club.mcscrims.speedhg
|
||||
import club.mcscrims.speedhg.config.LanguageManager
|
||||
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.listener.KitEventDispatcher
|
||||
import club.mcscrims.speedhg.listener.ConnectListener
|
||||
import club.mcscrims.speedhg.listener.GameStateListener
|
||||
import club.mcscrims.speedhg.listener.SoupListener
|
||||
import club.mcscrims.speedhg.scoreboard.ScoreboardManager
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
@@ -29,6 +34,9 @@ class SpeedHG : JavaPlugin() {
|
||||
lateinit var scoreboardManager: ScoreboardManager
|
||||
private set
|
||||
|
||||
lateinit var kitManager: KitManager
|
||||
private set
|
||||
|
||||
override fun onEnable()
|
||||
{
|
||||
instance = this
|
||||
@@ -41,6 +49,11 @@ class SpeedHG : JavaPlugin() {
|
||||
|
||||
scoreboardManager = ScoreboardManager( this )
|
||||
|
||||
kitManager = KitManager( this )
|
||||
|
||||
// Register kits
|
||||
kitManager.registerKit( GoblinKit() )
|
||||
|
||||
registerListener()
|
||||
|
||||
logger.info("SpeedHG wurde geladen!")
|
||||
@@ -48,6 +61,7 @@ class SpeedHG : JavaPlugin() {
|
||||
|
||||
override fun onDisable()
|
||||
{
|
||||
kitManager.clearAll()
|
||||
super.onDisable()
|
||||
}
|
||||
|
||||
@@ -56,6 +70,9 @@ class SpeedHG : JavaPlugin() {
|
||||
val pm = Bukkit.getPluginManager()
|
||||
|
||||
pm.registerEvents( ConnectListener(), this )
|
||||
pm.registerEvents( GameStateListener(), this )
|
||||
pm.registerEvents( SoupListener(), this )
|
||||
pm.registerEvents(KitEventDispatcher( this, kitManager ), this )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,6 +65,13 @@ class LanguageManager(
|
||||
return langMap?.get( key ) ?: "<red>Missing Key: $key</red>"
|
||||
}
|
||||
|
||||
fun getDefaultRawMessage(
|
||||
key: String
|
||||
): String
|
||||
{
|
||||
return languages[ defaultLanguage ]?.get( key ) ?: "<red>Missing Key: $key</red>"
|
||||
}
|
||||
|
||||
fun getRawMessageList(
|
||||
player: Player,
|
||||
key: String
|
||||
@@ -82,6 +89,15 @@ class LanguageManager(
|
||||
return if (config.contains( key )) config.getStringList( key ) else listOf( "<red>Missing List: $key</red>" )
|
||||
}
|
||||
|
||||
fun getDefaultRawMessageList(
|
||||
key: String
|
||||
): List<String>
|
||||
{
|
||||
val file = File( plugin.dataFolder, "languages/$defaultLanguage.yml" )
|
||||
val config = YamlConfiguration.loadConfiguration( file )
|
||||
return if (config.contains( key )) config.getStringList( key ) else listOf( "<red>Missing List: $key</red>" )
|
||||
}
|
||||
|
||||
fun getComponent(
|
||||
player: Player,
|
||||
key: String,
|
||||
@@ -93,4 +109,14 @@ class LanguageManager(
|
||||
return miniMessage.deserialize( raw, *tags.toTypedArray() )
|
||||
}
|
||||
|
||||
fun getDefaultComponent(
|
||||
key: String,
|
||||
placeholders: Map<String, String>
|
||||
): Component
|
||||
{
|
||||
val raw = languages[ defaultLanguage ]?.get( key ) ?: "<red>Missing Key: $key</red>"
|
||||
val tags = placeholders.map { (k, v) -> Placeholder.parsed( k, v ) }
|
||||
return miniMessage.deserialize( raw, *tags.toTypedArray() )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -171,8 +171,7 @@ class GameManager(
|
||||
|
||||
teleportRandomly( player, world, startBorder / 2 )
|
||||
|
||||
// TODO: Kit Items geben
|
||||
// plugin.kitManager.giveKit( player )
|
||||
plugin.kitManager.applyKit( player ) // verteilt Items + ruft onAssign + passive.onActivate
|
||||
|
||||
player.inventory.addItem(ItemStack( Material.COMPASS ))
|
||||
player.sendMsg( "game.started" )
|
||||
@@ -244,6 +243,7 @@ class GameManager(
|
||||
) {
|
||||
setGameState( GameState.ENDING )
|
||||
timer = 15
|
||||
plugin.kitManager.clearAll()
|
||||
|
||||
Bukkit.getOnlinePlayers().forEach { p ->
|
||||
p.showTitle(Title.title(
|
||||
|
||||
87
src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt
Normal file
87
src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt
Normal file
@@ -0,0 +1,87 @@
|
||||
package club.mcscrims.speedhg.kit
|
||||
|
||||
import club.mcscrims.speedhg.kit.ability.ActiveAbility
|
||||
import club.mcscrims.speedhg.kit.ability.PassiveAbility
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
/**
|
||||
* Base class for every kit in SpeedHG.
|
||||
*
|
||||
* ## How to create a new kit
|
||||
* 1. Extend this class.
|
||||
* 2. Implement all abstract members.
|
||||
* 3. For each [Playstyle], create a private inner class extending [ActiveAbility]
|
||||
* and another extending [PassiveAbility].
|
||||
* 4. Return the correct instance from [getActiveAbility] / [getPassiveAbility]
|
||||
* using a `when(playstyle)` expression.
|
||||
* 5. Register the kit via `plugin.kitManager.registerKit(YourKit())` in [SpeedHG.onEnable].
|
||||
*
|
||||
* See [TemplateKit] for a fully annotated example.
|
||||
*/
|
||||
abstract class Kit {
|
||||
|
||||
/** Unique snake_case identifier (e.g. `"warrior"`, `"archer"`). */
|
||||
abstract val id: String
|
||||
|
||||
/** Coloured display name shown in the kit selection GUI. */
|
||||
abstract val displayName: Component
|
||||
|
||||
/** Short lore lines shown on the item in the kit GUI. */
|
||||
abstract val lore: List<String>
|
||||
|
||||
/** Icon used in the kit selection GUI. */
|
||||
abstract val icon: Material
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Playstyle-specific abilities — implement with a `when` expression
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return the [ActiveAbility] for the given [playstyle].
|
||||
*
|
||||
* ```kotlin
|
||||
* override fun getActiveAbility(playstyle: Playstyle) = when (playstyle) {
|
||||
* Playstyle.AGGRESSIVE -> AggressiveActive()
|
||||
* Playstyle.DEFENSIVE -> DefensiveActive()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* **Performance note:** This is called frequently by [KitEventDispatcher].
|
||||
* Prefer returning a cached singleton (`private val aggressiveActive = AggressiveActive()`)
|
||||
* over allocating a new instance on each call.
|
||||
*/
|
||||
abstract fun getActiveAbility( playstyle: Playstyle ): ActiveAbility
|
||||
|
||||
/** Return the [PassiveAbility] for the given [playstyle]. Same caching advice applies. */
|
||||
abstract fun getPassiveAbility( playstyle: Playstyle ): PassiveAbility
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Item distribution
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Give the player their kit-specific items at game start (after teleportation).
|
||||
* The standard HG items (soup, compass, etc.) are already given by [GameManager].
|
||||
* Only add kit-exclusive items here.
|
||||
*/
|
||||
abstract fun giveItems( player: Player, playstyle: Playstyle )
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle hooks (optional)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called once per round when the kit is applied to [player].
|
||||
* Use for permanent potion effects, scoreboard objectives, repeating tasks, etc.
|
||||
* The matching [PassiveAbility.onActivate] is called immediately after this.
|
||||
*/
|
||||
open fun onAssign( player: Player, playstyle: Playstyle ) {}
|
||||
|
||||
/**
|
||||
* Called when the kit is removed (game over / round reset).
|
||||
* The matching [PassiveAbility.onDeactivate] is called immediately before this.
|
||||
*/
|
||||
open fun onRemove( player: Player ) {}
|
||||
}
|
||||
144
src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt
Normal file
144
src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
package club.mcscrims.speedhg.kit
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.kit.charge.PlayerChargeData
|
||||
import org.bukkit.entity.Player
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Manages kit registration, player selections, and the charge state for all online players.
|
||||
*
|
||||
* ## Typical usage flow
|
||||
* ```
|
||||
* // During plugin startup
|
||||
* kitManager.registerKit(WarriorKit())
|
||||
* kitManager.registerKit(ArcherKit())
|
||||
*
|
||||
* // During lobby (e.g. GUI click)
|
||||
* kitManager.selectKit(player, kitManager.getKit("warrior")!!)
|
||||
* kitManager.selectPlaystyle(player, Playstyle.AGGRESSIVE)
|
||||
*
|
||||
* // When the game round starts (replace the TODO in GameManager.startGame)
|
||||
* kitManager.applyKit(player) // gives items, runs onAssign / passive.onActivate
|
||||
*
|
||||
* // When the round ends
|
||||
* kitManager.clearAll() // runs onRemove / passive.onDeactivate for every player
|
||||
* ```
|
||||
*/
|
||||
class KitManager(
|
||||
private val plugin: SpeedHG
|
||||
) {
|
||||
|
||||
// All kits available this session, keyed by Kit.id
|
||||
private val registeredKits = ConcurrentHashMap<String, Kit>()
|
||||
|
||||
// Lobby selections — set before the round starts
|
||||
private val selectedKits = ConcurrentHashMap<UUID, Kit>()
|
||||
private val selectedPlaystyles = ConcurrentHashMap<UUID, Playstyle>()
|
||||
|
||||
// Live charge state — populated by applyKit, cleared by removeKit/clearAll
|
||||
private val chargeData = ConcurrentHashMap<UUID, PlayerChargeData>()
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Kit registration
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
fun registerKit(
|
||||
kit: Kit
|
||||
) {
|
||||
registeredKits[kit.id] = kit
|
||||
plugin.logger.info("[KitManager] Registered kit: ${kit.id}")
|
||||
}
|
||||
|
||||
fun getRegisteredKits(): Collection<Kit> = registeredKits.values
|
||||
|
||||
fun getKit(id: String): Kit? = registeredKits[id]
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Player selections (lobby phase)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
fun selectKit(
|
||||
player: Player,
|
||||
kit: Kit
|
||||
) {
|
||||
selectedKits[player.uniqueId] = kit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the playstyle for a player.
|
||||
* Defaults to [Playstyle.DEFENSIVE] if never called.
|
||||
*/
|
||||
fun selectPlaystyle(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
selectedPlaystyles[player.uniqueId] = playstyle
|
||||
}
|
||||
|
||||
fun getSelectedKit( player: Player ): Kit? = selectedKits[player.uniqueId]
|
||||
|
||||
fun getSelectedPlaystyle( player: Player ): Playstyle =
|
||||
selectedPlaystyles[player.uniqueId] ?: Playstyle.DEFENSIVE
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Game lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Apply the player's selected kit at game start.
|
||||
*
|
||||
* Call this from [GameManager.startGame] for every online player, **after** teleportation.
|
||||
* If the player has not selected a kit, this is a no-op.
|
||||
*/
|
||||
fun applyKit(
|
||||
player: Player
|
||||
) {
|
||||
val kit = selectedKits[player.uniqueId] ?: return
|
||||
val playstyle = getSelectedPlaystyle( player )
|
||||
val active = kit.getActiveAbility( playstyle )
|
||||
|
||||
chargeData[player.uniqueId] = PlayerChargeData( active.hitsRequired )
|
||||
|
||||
kit.onAssign( player, playstyle )
|
||||
kit.getPassiveAbility( playstyle ).onActivate( player )
|
||||
kit.giveItems( player, playstyle )
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove and clean up a player's kit (round over, player eliminated, etc.).
|
||||
* Safe to call even if the player has no kit assigned.
|
||||
*/
|
||||
fun removeKit(
|
||||
player: Player
|
||||
) {
|
||||
val kit = selectedKits[player.uniqueId] ?: return
|
||||
val playstyle = getSelectedPlaystyle( player )
|
||||
|
||||
kit.getPassiveAbility( playstyle ).onDeactivate( player )
|
||||
kit.onRemove( player )
|
||||
|
||||
chargeData.remove( player.uniqueId )
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all kits and clear every selection.
|
||||
* Call this at the end of each round, **before** resetting other game state.
|
||||
*/
|
||||
fun clearAll()
|
||||
{
|
||||
selectedKits.keys.toList().forEach { uuid ->
|
||||
plugin.server.getPlayer( uuid )?.let { removeKit( it ) }
|
||||
}
|
||||
selectedKits.clear()
|
||||
selectedPlaystyles.clear()
|
||||
chargeData.clear()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Charge access (used by KitEventDispatcher)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
fun getChargeData( player: Player ): PlayerChargeData? = chargeData[player.uniqueId]
|
||||
}
|
||||
8
src/main/kotlin/club/mcscrims/speedhg/kit/Playstyle.kt
Normal file
8
src/main/kotlin/club/mcscrims/speedhg/kit/Playstyle.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package club.mcscrims.speedhg.kit
|
||||
|
||||
enum class Playstyle(
|
||||
val displayName: String
|
||||
) {
|
||||
AGGRESSIVE( "Aggressive" ),
|
||||
DEFENSIVE( "Defensive" )
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package club.mcscrims.speedhg.kit.ability
|
||||
|
||||
/**
|
||||
* Return type for [ActiveAbility.execute].
|
||||
*
|
||||
* - [Success] → Ability fired, KitEventDispatcher keeps the charge consumed.
|
||||
* - [ConditionNotMet] → Ability couldn't fire (e.g. no targets nearby).
|
||||
* KitEventDispatcher *refunds* the charge automatically.
|
||||
*/
|
||||
sealed class AbilityResult {
|
||||
|
||||
/** The ability executed successfully. */
|
||||
object Success : AbilityResult()
|
||||
|
||||
/**
|
||||
* A runtime condition was not satisfied (e.g. no enemy in range).
|
||||
* The dispatcher refunds the charge so the player can try again.
|
||||
*
|
||||
* @param reason Short message shown to the player via ActionBar.
|
||||
*/
|
||||
data class ConditionNotMet( val reason: String ) : AbilityResult()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package club.mcscrims.speedhg.kit.ability
|
||||
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
/**
|
||||
* An ability that the player actively triggers by right-clicking [triggerMaterial].
|
||||
*
|
||||
* ## Charge flow
|
||||
* 1. Game starts → charge state is READY (one free use).
|
||||
* 2. Player right-clicks → [KitEventDispatcher] checks if READY, then calls [execute].
|
||||
* 3. Charge transitions to CHARGING; the player must land [hitsRequired] melee hits.
|
||||
* 4. Each qualifying hit calls [PlayerChargeData.registerHit].
|
||||
* 5. When full → [onFullyCharged] is called and state returns to READY.
|
||||
*
|
||||
* If [execute] returns [AbilityResult.ConditionNotMet] the dispatcher automatically
|
||||
* refunds the charge.
|
||||
*/
|
||||
abstract class ActiveAbility(
|
||||
val playstyle: Playstyle
|
||||
) {
|
||||
|
||||
abstract val name: String
|
||||
abstract val description: String
|
||||
|
||||
/**
|
||||
* Melee hits required to recharge after one use.
|
||||
* Set to 0 for an unlimited / always-ready ability (e.g. in debug kits).
|
||||
*/
|
||||
abstract val hitsRequired: Int
|
||||
|
||||
/**
|
||||
* The item material that triggers this ability on right-click.
|
||||
* The item must be in the player's main hand.
|
||||
*/
|
||||
abstract val triggerMaterial: Material
|
||||
|
||||
/**
|
||||
* Execute the ability. Called only when [PlayerChargeData.isReady] is true.
|
||||
* The dispatcher has already called [PlayerChargeData.consume] before this runs.
|
||||
*
|
||||
* Return [AbilityResult.Success] to keep the charge consumed.
|
||||
* Return [AbilityResult.ConditionNotMet] to trigger an automatic charge refund.
|
||||
*/
|
||||
abstract fun execute( player: Player ): AbilityResult
|
||||
|
||||
/**
|
||||
* Called when the ability's charge completes ([hitsRequired] hits landed).
|
||||
* Override for sounds, particles, ActionBar feedback, etc.
|
||||
*/
|
||||
open fun onFullyCharged( player: Player ) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package club.mcscrims.speedhg.kit.ability
|
||||
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||
import org.bukkit.event.player.PlayerInteractEvent
|
||||
import org.bukkit.event.player.PlayerMoveEvent
|
||||
|
||||
/**
|
||||
* A passive ability that runs silently in the background.
|
||||
*
|
||||
* [KitEventDispatcher] is the single registered Bukkit [Listener].
|
||||
* It calls the appropriate hook on the relevant player's PassiveAbility
|
||||
* for every event, so you never register your own listeners here.
|
||||
*
|
||||
* Override **only** the hooks you need — all defaults are no-ops.
|
||||
*
|
||||
* ## Adding new event hooks
|
||||
* 1. Add an `open fun on<Event>` stub here.
|
||||
* 2. Add the corresponding `@EventHandler` in [KitEventDispatcher] that
|
||||
* looks up the player's kit and calls this stub.
|
||||
*/
|
||||
abstract class PassiveAbility(
|
||||
val playstyle: Playstyle
|
||||
) {
|
||||
|
||||
abstract val name: String
|
||||
abstract val description: String
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Called once when a game round starts and this kit is applied to [player]. */
|
||||
open fun onActivate( player: Player ) {}
|
||||
|
||||
/** Called when the round ends or the kit is removed. Cancel tasks here. */
|
||||
open fun onDeactivate( player: Player ) {}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Combat hooks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* [attacker] (this player) just hit [victim] with a melee attack.
|
||||
* Called AFTER [PlayerChargeData.registerHit] has already been incremented.
|
||||
*/
|
||||
open fun onHitEnemy( attacker: Player, victim: Player, event: EntityDamageByEntityEvent ) {}
|
||||
|
||||
/** [victim] (this player) just received a melee hit from [attacker]. */
|
||||
open fun onHitByEnemy( victim: Player, attacker: Player, event: EntityDamageByEntityEvent ) {}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Interaction hooks (called for non-trigger-material right-clicks)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
open fun onInteract( player: Player, event: PlayerInteractEvent ) {}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Movement hook
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
open fun onMove( player: Player, event: PlayerMoveEvent ) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package club.mcscrims.speedhg.kit.charge
|
||||
|
||||
enum class ChargeState {
|
||||
/**
|
||||
* Ability is ready. The next right-click will fire it.
|
||||
*/
|
||||
READY,
|
||||
|
||||
/**
|
||||
* Ability was used. Waiting for [PlayerChargeData.hitsRequired]
|
||||
* melee hits before returning to [READY]
|
||||
*/
|
||||
CHARGING
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package club.mcscrims.speedhg.kit.charge
|
||||
|
||||
/**
|
||||
* Tracks the ability charge for a single player.
|
||||
*
|
||||
* One instance is created per player in [KitManager.applyKit] and destroyed in [removeKit]
|
||||
*
|
||||
* ## State machine
|
||||
* ```
|
||||
* READY ──[consume()]──► CHARGING ──[registerHit() × hitsRequired]──► READY
|
||||
* ```
|
||||
*
|
||||
* @param hitsRequired Number of melee hits needed to recharge. 0 = alway READY.
|
||||
*/
|
||||
class PlayerChargeData(
|
||||
val hitsRequired: Int
|
||||
) {
|
||||
|
||||
var state: ChargeState = ChargeState.READY
|
||||
private set
|
||||
|
||||
private var currentHits: Int = 0
|
||||
|
||||
val isReady: Boolean
|
||||
get() = state == ChargeState.READY
|
||||
|
||||
val hitsRemaining: Int
|
||||
get() = ( hitsRequired - currentHits ).coerceAtLeast( 0 )
|
||||
|
||||
/**
|
||||
* Progress towards the next charge as a value in [0f, 1f].
|
||||
* Always 1f when [hitsRequired] is 0.
|
||||
*/
|
||||
val progress: Float
|
||||
get() = if ( hitsRequired == 0 ) 1f
|
||||
else ( currentHits.toFloat() / hitsRequired.toFloat() ).coerceIn( 0f, 1f )
|
||||
|
||||
/**
|
||||
* Consume a ready charge (ability is about to fire).
|
||||
*
|
||||
* @return `true` if the charge was consumed, `false` if already [ChargeState.CHARGING]
|
||||
*/
|
||||
fun consume(): Boolean
|
||||
{
|
||||
if ( state != ChargeState.READY ) return false
|
||||
// Edge case: ability has no charge cost → never leave READY state
|
||||
if ( hitsRequired == 0 ) return true
|
||||
state = ChargeState.CHARGING
|
||||
currentHits = 0
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Register one qualifying melee hit.
|
||||
*
|
||||
* @return `true` if this hit completed the charge (state is now READY again).
|
||||
* `false` if more hits are needed or the ability wasn't in CHARGING state.
|
||||
*/
|
||||
fun registerHit(): Boolean
|
||||
{
|
||||
if ( state != ChargeState.CHARGING ) return false
|
||||
currentHits++
|
||||
return if ( currentHits >= hitsRequired )
|
||||
{
|
||||
currentHits = 0
|
||||
state = ChargeState.READY
|
||||
true
|
||||
}
|
||||
else false
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund a charge — used by [KitEventDispatcher] when [AbilityResult.ConditionNotMet]
|
||||
* is returned, so the player doesn't lose their charge for a failed use.
|
||||
*/
|
||||
fun refund()
|
||||
{
|
||||
state = ChargeState.READY
|
||||
currentHits = 0
|
||||
}
|
||||
|
||||
/** Full reset to initial state (game start / round reset). */
|
||||
fun reset()
|
||||
{
|
||||
state = ChargeState.READY
|
||||
currentHits = 0
|
||||
}
|
||||
|
||||
}
|
||||
231
src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt
Normal file
231
src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt
Normal file
@@ -0,0 +1,231 @@
|
||||
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.WorldEditUtils
|
||||
import club.mcscrims.speedhg.util.legacySerializer
|
||||
import club.mcscrims.speedhg.util.trans
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Sound
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class GoblinKit : Kit() {
|
||||
|
||||
private val plugin = SpeedHG.instance
|
||||
|
||||
override val id: String
|
||||
get() = "goblin"
|
||||
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent( "kits.goblin.name", mapOf() )
|
||||
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList( "kits.goblin.lore" )
|
||||
|
||||
override val icon: Material
|
||||
get() = Material.MOSSY_COBBLESTONE
|
||||
|
||||
// ── Cached ability instances (avoid allocating per event call) ────────────
|
||||
private val aggressiveActive = AggressiveActive()
|
||||
private val defensiveActive = DefensiveActive()
|
||||
private val aggressiveNoPassive = AggressiveNoPassive()
|
||||
private val defensiveNoPassive = DefensiveNoPassive()
|
||||
|
||||
// ── 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 -> aggressiveNoPassive
|
||||
Playstyle.DEFENSIVE -> defensiveNoPassive
|
||||
}
|
||||
|
||||
// ── Item distribution ─────────────────────────────────────────────────────
|
||||
|
||||
private val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
|
||||
|
||||
override fun giveItems(
|
||||
player: Player,
|
||||
playstyle: Playstyle
|
||||
) {
|
||||
when( playstyle )
|
||||
{
|
||||
Playstyle.AGGRESSIVE ->
|
||||
{
|
||||
val stealItem = ItemBuilder( Material.GLASS )
|
||||
.name( aggressiveActive.name )
|
||||
.lore(listOf( aggressiveActive.description ))
|
||||
.build()
|
||||
|
||||
cachedItems[ player.uniqueId ] = listOf( stealItem )
|
||||
player.inventory.addItem( stealItem )
|
||||
}
|
||||
|
||||
Playstyle.DEFENSIVE ->
|
||||
{
|
||||
val bunkerItem = ItemBuilder( Material.MOSSY_COBBLESTONE )
|
||||
.name( defensiveActive.name )
|
||||
.lore(listOf( defensiveActive.description ))
|
||||
.build()
|
||||
|
||||
cachedItems[ player.uniqueId ] = listOf( bunkerItem )
|
||||
player.inventory.addItem( bunkerItem )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Optional lifecycle hooks ──────────────────────────────────────────────
|
||||
|
||||
override fun onRemove(
|
||||
player: Player
|
||||
) {
|
||||
val items = cachedItems[ player.uniqueId ] ?: return
|
||||
player.inventory.removeAll { items.contains( it ) }
|
||||
}
|
||||
|
||||
private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.steal.description" )
|
||||
|
||||
override val hitsRequired: Int
|
||||
get() = 15
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.GLASS
|
||||
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
val lineOfSight = player.getTargetEntity( 3 ) as? Player
|
||||
?: return AbilityResult.ConditionNotMet( "No player in line of sight" )
|
||||
|
||||
val targetKit = plugin.kitManager.getSelectedKit( lineOfSight )
|
||||
?: return AbilityResult.ConditionNotMet( "Target has no kit" )
|
||||
|
||||
val currentKit = plugin.kitManager.getSelectedKit( player )
|
||||
?: return AbilityResult.ConditionNotMet( "Error while copying kit" )
|
||||
|
||||
plugin.kitManager.removeKit( player )
|
||||
plugin.kitManager.selectKit( player, targetKit )
|
||||
plugin.kitManager.applyKit( player )
|
||||
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
plugin.kitManager.removeKit( player )
|
||||
plugin.kitManager.selectKit( player, currentKit )
|
||||
plugin.kitManager.applyKit( player )
|
||||
}, 20L * 60)
|
||||
|
||||
player.playSound( player.location, Sound.ENTITY_EVOKER_CAST_SPELL, 1f, 1.5f )
|
||||
player.sendActionBar(player.trans( "kits.goblin.messages.stole_kit", "kit" to legacySerializer.serialize( targetKit.displayName )))
|
||||
|
||||
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.goblin.messages.steal_kit_charged" ))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
|
||||
|
||||
override val name: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.name" )
|
||||
|
||||
override val description: String
|
||||
get() = plugin.languageManager.getDefaultRawMessage( "kits.goblin.items.bunker.description" )
|
||||
|
||||
override val hitsRequired: Int
|
||||
get() = 15
|
||||
|
||||
override val triggerMaterial: Material
|
||||
get() = Material.MOSSY_COBBLESTONE
|
||||
|
||||
override fun execute(
|
||||
player: Player
|
||||
): AbilityResult
|
||||
{
|
||||
val world = player.world
|
||||
val location = player.location
|
||||
|
||||
WorldEditUtils.createSphere(
|
||||
world,
|
||||
location,
|
||||
10.0,
|
||||
false,
|
||||
Material.MOSSY_COBBLESTONE
|
||||
)
|
||||
|
||||
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
||||
WorldEditUtils.createSphere(
|
||||
world,
|
||||
location,
|
||||
10.0,
|
||||
false,
|
||||
Material.AIR
|
||||
)
|
||||
}, 20L * 15 )
|
||||
|
||||
player.playSound( player.location, Sound.BLOCK_PISTON_EXTEND, 1f, 0.8f )
|
||||
player.sendActionBar(player.trans( "kits.goblin.messages.spawn_bunker" ))
|
||||
|
||||
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.goblin.messages.bunker_charged" ))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class AggressiveNoPassive : PassiveAbility( Playstyle.AGGRESSIVE ) {
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
|
||||
override val description: String
|
||||
get() = "None"
|
||||
|
||||
}
|
||||
|
||||
private class DefensiveNoPassive : PassiveAbility( Playstyle.DEFENSIVE ) {
|
||||
|
||||
override val name: String
|
||||
get() = "None"
|
||||
|
||||
override val description: String
|
||||
get() = "None"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
227
src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt
Normal file
227
src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt
Normal file
@@ -0,0 +1,227 @@
|
||||
package club.mcscrims.speedhg.kit.impl
|
||||
|
||||
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 net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.format.NamedTextColor
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Sound
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.potion.PotionEffect
|
||||
import org.bukkit.potion.PotionEffectType
|
||||
|
||||
/**
|
||||
* ──────────────────────────────────────────────────────────────────────────────
|
||||
* TEMPLATE KIT — reference implementation, do not use in production
|
||||
* ──────────────────────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Copy this file, rename the class, change the abilities, and register your kit:
|
||||
* ```kotlin
|
||||
* // In SpeedHG.onEnable():
|
||||
* kitManager.registerKit(YourKit())
|
||||
* ```
|
||||
*
|
||||
* ## Playstyle overview (Template)
|
||||
*
|
||||
* | Playstyle | Active | Passive |
|
||||
* |-------------|----------------------------|----------------------------|
|
||||
* | AGGRESSIVE | Power Strike (10 hits) | Bloodlust – speed on hit |
|
||||
* | DEFENSIVE | Iron Skin (5 hits) | Fortitude – 10% dmg reduc. |
|
||||
*/
|
||||
class TemplateKit : Kit() {
|
||||
|
||||
override val id = "template"
|
||||
override val displayName: Component = Component.text("Template Kit", NamedTextColor.YELLOW)
|
||||
override val lore = listOf("An example kit.", "Replace with your own.")
|
||||
override val icon = Material.NETHER_STAR
|
||||
|
||||
// ── 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 ─────────────────────────────────────────────────────
|
||||
|
||||
override fun giveItems(player: Player, playstyle: Playstyle) {
|
||||
// Slot 8 = ability trigger item (always present)
|
||||
player.inventory.setItem(8, ItemStack(Material.BLAZE_ROD))
|
||||
|
||||
when (playstyle) {
|
||||
Playstyle.AGGRESSIVE -> {
|
||||
// e.g. extra offensive item
|
||||
}
|
||||
Playstyle.DEFENSIVE -> {
|
||||
// e.g. extra defensive item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Optional lifecycle hooks ──────────────────────────────────────────────
|
||||
|
||||
override fun onAssign(player: Player, playstyle: Playstyle) {
|
||||
// Example: a kit that always gives permanent speed I
|
||||
// player.addPotionEffect(PotionEffect(PotionEffectType.SPEED, Int.MAX_VALUE, 0, false, false, true))
|
||||
}
|
||||
|
||||
override fun onRemove(player: Player) {
|
||||
// Undo anything done in onAssign
|
||||
// player.removePotionEffect(PotionEffectType.SPEED)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AGGRESSIVE active ability
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Power Strike — marks the player so their next hit deals 1.5× damage.
|
||||
*
|
||||
* Demonstrates:
|
||||
* - Returning [AbilityResult.Success]
|
||||
* - Returning [AbilityResult.ConditionNotMet] (no example here, but shown below)
|
||||
* - [onFullyCharged] for charge-complete feedback
|
||||
*/
|
||||
private inner class AggressiveActive : ActiveAbility(Playstyle.AGGRESSIVE) {
|
||||
|
||||
override val name = "Power Strike"
|
||||
override val description = "Your next melee attack deals 1.5× damage."
|
||||
override val hitsRequired = 10
|
||||
override val triggerMaterial = Material.BLAZE_ROD
|
||||
|
||||
// In a real kit, store pending-strike players in a companion Set<UUID>
|
||||
// and apply the bonus in onHitEnemy / a damage event listener.
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
// Example: guard clause returning ConditionNotMet
|
||||
// val nearbyEnemies = player.getNearbyEntities(10.0, 10.0, 10.0).filterIsInstance<Player>()
|
||||
// if (nearbyEnemies.isEmpty()) return AbilityResult.ConditionNotMet("No enemies nearby!")
|
||||
|
||||
// TODO: add player.uniqueId to a "powerStrikePending" set
|
||||
|
||||
player.playSound(player.location, Sound.ENTITY_BLAZE_SHOOT, 1f, 1.2f)
|
||||
player.sendActionBar(Component.text("⚔ Power Strike ready!", NamedTextColor.RED))
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
override fun onFullyCharged(player: Player) {
|
||||
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f)
|
||||
player.sendActionBar(Component.text("⚡ Ability recharged!", NamedTextColor.GREEN))
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE active ability
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Iron Skin — grants Resistance I for 4 seconds.
|
||||
*
|
||||
* Demonstrates a simpler ability with fewer required hits (5 vs 10).
|
||||
*/
|
||||
private inner class DefensiveActive : ActiveAbility(Playstyle.DEFENSIVE) {
|
||||
|
||||
override val name = "Iron Skin"
|
||||
override val description = "Gain Resistance I for 4 seconds."
|
||||
override val hitsRequired = 5
|
||||
override val triggerMaterial = Material.BLAZE_ROD
|
||||
|
||||
override fun execute(player: Player): AbilityResult {
|
||||
player.addPotionEffect(
|
||||
PotionEffect(
|
||||
PotionEffectType.RESISTANCE,
|
||||
/* duration */ 4 * 20,
|
||||
/* amplifier */ 0,
|
||||
/* ambient */ false,
|
||||
/* particles */ false,
|
||||
/* icon */ true
|
||||
)
|
||||
)
|
||||
player.playSound(player.location, Sound.ITEM_TOTEM_USE, 0.8f, 1.5f)
|
||||
player.sendActionBar(Component.text("🛡 Iron Skin active!", NamedTextColor.AQUA))
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
override fun onFullyCharged(player: Player) {
|
||||
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f)
|
||||
player.sendActionBar(Component.text("⚡ Ability recharged!", NamedTextColor.GREEN))
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AGGRESSIVE passive — Bloodlust
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Bloodlust — grants Speed I for 2 seconds after landing a hit.
|
||||
*
|
||||
* Demonstrates [onHitEnemy] and [onActivate] / [onDeactivate] usage.
|
||||
*/
|
||||
private inner class AggressivePassive : PassiveAbility(Playstyle.AGGRESSIVE) {
|
||||
|
||||
override val name = "Bloodlust"
|
||||
override val description = "Gain Speed I for 2 s after hitting an enemy."
|
||||
|
||||
override fun onActivate(player: Player) {
|
||||
// Called once at game start.
|
||||
// Start any repeating BukkitTasks here and store the returned BukkitTask
|
||||
// so you can cancel it in onDeactivate. Example:
|
||||
// task = Bukkit.getScheduler().runTaskTimer(plugin, { checkCooldowns(player) }, 0L, 10L)
|
||||
}
|
||||
|
||||
override fun onDeactivate(player: Player) {
|
||||
// task?.cancel()
|
||||
}
|
||||
|
||||
// NOTE: Called AFTER the charge counter has already been incremented.
|
||||
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) {
|
||||
attacker.addPotionEffect(
|
||||
PotionEffect(
|
||||
PotionEffectType.SPEED,
|
||||
/* duration */ 2 * 20,
|
||||
/* amplifier */ 0,
|
||||
/* ambient */ false,
|
||||
/* particles */ false,
|
||||
/* icon */ false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DEFENSIVE passive — Fortitude
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Fortitude — reduces all incoming melee damage by 10%.
|
||||
*
|
||||
* Demonstrates [onHitByEnemy] — the simplest passive pattern.
|
||||
*/
|
||||
private inner class DefensivePassive : PassiveAbility(Playstyle.DEFENSIVE) {
|
||||
|
||||
override val name = "Fortitude"
|
||||
override val description = "Take 10% less damage from melee attacks."
|
||||
|
||||
// onActivate / onDeactivate are no-ops for this passive (default impl. is fine)
|
||||
|
||||
override fun onHitByEnemy(victim: Player, attacker: Player, event: EntityDamageByEntityEvent) {
|
||||
event.damage *= 0.90
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package club.mcscrims.speedhg.kit.listener
|
||||
|
||||
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 net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.format.NamedTextColor
|
||||
import org.bukkit.entity.Player
|
||||
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.player.PlayerInteractEvent
|
||||
import org.bukkit.event.player.PlayerMoveEvent
|
||||
import org.bukkit.inventory.EquipmentSlot
|
||||
|
||||
/**
|
||||
* The *single* Bukkit [Listener] responsible for all kit-related event handling.
|
||||
*
|
||||
* ## Design rationale
|
||||
* Instead of letting each [PassiveAbility] register its own Listener (which creates
|
||||
* unpredictable ordering and hard-to-debug registration leaks), this dispatcher is
|
||||
* registered once and delegates to the correct kit ability per-player.
|
||||
*
|
||||
* ## Extending with new event types
|
||||
* 1. Add a new `@EventHandler` here.
|
||||
* 2. Add the corresponding `open fun on<Event>` stub to [PassiveAbility].
|
||||
* 3. Call the stub from the new handler.
|
||||
* No other files need to change.
|
||||
*
|
||||
* Register this in [SpeedHG.onEnable]:
|
||||
* ```kotlin
|
||||
* server.pluginManager.registerEvents(KitEventDispatcher(this, kitManager), this)
|
||||
* ```
|
||||
*/
|
||||
class KitEventDispatcher(
|
||||
private val plugin: SpeedHG,
|
||||
private val kitManager: KitManager,
|
||||
) : Listener {
|
||||
|
||||
// =========================================================================
|
||||
// Hit tracking + charge system + passive combat hook
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* MONITOR priority — runs after all other damage modifiers are applied,
|
||||
* so passive abilities that read [EntityDamageByEntityEvent.damage] see the
|
||||
* final value.
|
||||
*/
|
||||
@EventHandler(
|
||||
priority = EventPriority.MONITOR,
|
||||
ignoreCancelled = true
|
||||
)
|
||||
fun onMeleeHit(
|
||||
event: EntityDamageByEntityEvent
|
||||
) {
|
||||
val attacker = event.damager as? Player ?: return
|
||||
val victim = event.entity as? Player ?: return
|
||||
|
||||
if ( !isIngame() ) return
|
||||
|
||||
val attackerKit = kitManager.getSelectedKit( attacker ) ?: return
|
||||
val attackerPlaystyle = kitManager.getSelectedPlaystyle( attacker )
|
||||
val chargeData = kitManager.getChargeData( attacker ) ?: return
|
||||
|
||||
// ── 1. Increment charge counter ──────────────────────────────────────
|
||||
val justFullyCharged = chargeData.registerHit()
|
||||
if ( justFullyCharged ) {
|
||||
attackerKit.getActiveAbility( attackerPlaystyle ).onFullyCharged( attacker )
|
||||
}
|
||||
|
||||
// ── 2. Attacker passive hook ─────────────────────────────────────────
|
||||
attackerKit.getPassiveAbility( attackerPlaystyle )
|
||||
.onHitEnemy( attacker, victim, event )
|
||||
|
||||
// ── 3. Victim passive hook ────────────────────────────────────────────
|
||||
val victimKit = kitManager.getSelectedKit( victim ) ?: return
|
||||
victimKit.getPassiveAbility(kitManager.getSelectedPlaystyle( victim ))
|
||||
.onHitByEnemy( victim, attacker, event )
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Active ability trigger + passive interact hook
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* HIGH priority so we cancel the event (suppressing block/item interactions)
|
||||
* before HIGHEST or MONITOR listeners see it.
|
||||
*/
|
||||
@EventHandler(
|
||||
priority = EventPriority.HIGH,
|
||||
ignoreCancelled = true
|
||||
)
|
||||
fun onInteract(
|
||||
event: PlayerInteractEvent
|
||||
) {
|
||||
val player = event.player
|
||||
|
||||
// Only main-hand right-clicks — ignore left-click and off-hand duplicates
|
||||
if ( event.hand != EquipmentSlot.HAND ) return
|
||||
if ( !event.action.isRightClick ) return
|
||||
if ( !isIngame() ) return
|
||||
|
||||
val kit = kitManager.getSelectedKit( player ) ?: return
|
||||
val playstyle = kitManager.getSelectedPlaystyle( player )
|
||||
|
||||
// ── Always call the passive interact hook first ───────────────────────
|
||||
kit.getPassiveAbility( playstyle ).onInteract( player, event )
|
||||
|
||||
// ── Check whether the item in hand is the ability trigger ─────────────
|
||||
val itemInHand = player.inventory.itemInMainHand
|
||||
val active = kit.getActiveAbility( playstyle )
|
||||
|
||||
if ( itemInHand.type != active.triggerMaterial ) return
|
||||
|
||||
event.isCancelled = true // prevent vanilla block interaction on ability item
|
||||
|
||||
val chargeData = kitManager.getChargeData( player ) ?: return
|
||||
|
||||
if ( !chargeData.isReady ) {
|
||||
player.sendActionBar(
|
||||
Component.text(
|
||||
"⚡ ${chargeData.hitsRemaining} hits to recharge!",
|
||||
NamedTextColor.RED
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// ── Consume the charge, then execute ─────────────────────────────────
|
||||
chargeData.consume()
|
||||
|
||||
when ( val result = active.execute( player ))
|
||||
{
|
||||
is AbilityResult.Success -> { /* ability provides its own feedback */ }
|
||||
is AbilityResult.ConditionNotMet -> {
|
||||
chargeData.refund() // player keeps their charge
|
||||
player.sendActionBar(
|
||||
Component.text(result.reason, NamedTextColor.YELLOW)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Passive movement hook
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* MONITOR priority — reads the final resolved movement.
|
||||
* Call [event.isCancelled] = true inside [PassiveAbility.onMove] if needed.
|
||||
* If you do, switch this handler to a lower priority.
|
||||
*/
|
||||
@EventHandler(
|
||||
priority = EventPriority.MONITOR,
|
||||
ignoreCancelled = true
|
||||
)
|
||||
fun onMove(
|
||||
event: PlayerMoveEvent
|
||||
) {
|
||||
if ( !isIngame() ) return
|
||||
|
||||
val player = event.player
|
||||
val kit = kitManager.getSelectedKit( player ) ?: return
|
||||
|
||||
kit.getPassiveAbility(kitManager.getSelectedPlaystyle( player ))
|
||||
.onMove( player, event )
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
private fun isIngame(): Boolean = when ( plugin.gameManager.currentState )
|
||||
{
|
||||
GameState.INGAME, GameState.INVINCIBILITY -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import org.bukkit.entity.Player
|
||||
|
||||
private val langManager get() = SpeedHG.instance.languageManager
|
||||
|
||||
private val legacySerializer = LegacyComponentSerializer.builder()
|
||||
val legacySerializer = LegacyComponentSerializer.builder()
|
||||
.character('§')
|
||||
.hexColors()
|
||||
.useUnusualXRepeatedCharacterHexFormat()
|
||||
|
||||
116
src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt
Normal file
116
src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt
Normal file
@@ -0,0 +1,116 @@
|
||||
package club.mcscrims.speedhg.util
|
||||
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.ChatColor
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.enchantments.Enchantment
|
||||
import org.bukkit.inventory.ItemFlag
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
class ItemBuilder(
|
||||
private val itemStack: ItemStack
|
||||
) {
|
||||
|
||||
constructor(
|
||||
type: Material
|
||||
) : this(
|
||||
ItemStack( type )
|
||||
)
|
||||
|
||||
constructor(
|
||||
type: Material,
|
||||
amount: Int
|
||||
) : this(
|
||||
ItemStack( type, amount )
|
||||
)
|
||||
|
||||
constructor() : this(
|
||||
ItemStack( Material.STONE )
|
||||
)
|
||||
|
||||
fun name(
|
||||
name: String
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.editMeta { it.displayName(Component.text( name )) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun name(
|
||||
name: Component
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.editMeta { it.displayName( name ) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun lore(
|
||||
lore: List<String>
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.editMeta {
|
||||
val cLore = lore.stream()
|
||||
.map( this::color )
|
||||
.map( Component::text )
|
||||
.toList()
|
||||
|
||||
it.lore( cLore as List<Component> )
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun unbreakable(
|
||||
unbreakable: Boolean
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.editMeta { it.isUnbreakable = unbreakable }
|
||||
return this
|
||||
}
|
||||
|
||||
fun amount(
|
||||
amount: Int
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.amount = amount
|
||||
return this
|
||||
}
|
||||
|
||||
fun enchant(
|
||||
ench: Enchantment
|
||||
): ItemBuilder
|
||||
{
|
||||
enchant( ench, 1 )
|
||||
return this
|
||||
}
|
||||
|
||||
fun enchant(
|
||||
ench: Enchantment,
|
||||
level: Int
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.editMeta { it.addEnchant( ench, level, true ) }
|
||||
itemFlag( ItemFlag.HIDE_ENCHANTS )
|
||||
return this
|
||||
}
|
||||
|
||||
fun itemFlag(
|
||||
flag: ItemFlag
|
||||
): ItemBuilder
|
||||
{
|
||||
itemStack.editMeta { it.addItemFlags( flag ) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): ItemStack
|
||||
{
|
||||
return itemStack
|
||||
}
|
||||
|
||||
private fun color(
|
||||
string: String
|
||||
): String
|
||||
{
|
||||
return ChatColor.translateAlternateColorCodes( '&', string )
|
||||
}
|
||||
|
||||
}
|
||||
37
src/main/kotlin/club/mcscrims/speedhg/util/WorldEditUtils.kt
Normal file
37
src/main/kotlin/club/mcscrims/speedhg/util/WorldEditUtils.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package club.mcscrims.speedhg.util
|
||||
|
||||
import com.sk89q.worldedit.WorldEdit
|
||||
import com.sk89q.worldedit.bukkit.BukkitAdapter
|
||||
import com.sk89q.worldedit.util.SideEffectSet
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.World
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
object WorldEditUtils {
|
||||
|
||||
fun createSphere(
|
||||
world: World,
|
||||
startLocation: Location,
|
||||
radius: Double,
|
||||
filled: Boolean,
|
||||
block: Material
|
||||
) = try {
|
||||
val editSession = WorldEdit.getInstance().newEditSessionBuilder()
|
||||
.world(BukkitAdapter.adapt( world )).maxBlocks( -1 ).build()
|
||||
|
||||
editSession.sideEffectApplier = SideEffectSet.defaults()
|
||||
|
||||
editSession.makeSphere(
|
||||
BukkitAdapter.asBlockVector( startLocation ),
|
||||
BukkitAdapter.asBlockState(ItemStack( block )),
|
||||
radius, filled
|
||||
)
|
||||
|
||||
editSession.commit()
|
||||
editSession.close()
|
||||
} catch ( e: Exception ) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user