diff --git a/build.gradle.kts b/build.gradle.kts index 0481fb6..4f8a550 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index ec89680..ce6240e 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -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 ) } } \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/config/LanguageManager.kt b/src/main/kotlin/club/mcscrims/speedhg/config/LanguageManager.kt index 6d22a9e..3c75b8e 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/config/LanguageManager.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/config/LanguageManager.kt @@ -65,6 +65,13 @@ class LanguageManager( return langMap?.get( key ) ?: "Missing Key: $key" } + fun getDefaultRawMessage( + key: String + ): String + { + return languages[ defaultLanguage ]?.get( key ) ?: "Missing Key: $key" + } + fun getRawMessageList( player: Player, key: String @@ -79,7 +86,16 @@ class LanguageManager( val file = File( plugin.dataFolder, "languages/$locale.yml" ) val config = YamlConfiguration.loadConfiguration( file ) - return if (config.contains( key )) config.getStringList( key ) else listOf("Missing List: $key") + return if (config.contains( key )) config.getStringList( key ) else listOf( "Missing List: $key" ) + } + + fun getDefaultRawMessageList( + key: String + ): List + { + val file = File( plugin.dataFolder, "languages/$defaultLanguage.yml" ) + val config = YamlConfiguration.loadConfiguration( file ) + return if (config.contains( key )) config.getStringList( key ) else listOf( "Missing List: $key" ) } fun getComponent( @@ -93,4 +109,14 @@ class LanguageManager( return miniMessage.deserialize( raw, *tags.toTypedArray() ) } + fun getDefaultComponent( + key: String, + placeholders: Map + ): Component + { + val raw = languages[ defaultLanguage ]?.get( key ) ?: "Missing Key: $key" + val tags = placeholders.map { (k, v) -> Placeholder.parsed( k, v ) } + return miniMessage.deserialize( raw, *tags.toTypedArray() ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt b/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt index 8080bb5..c29963e 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt @@ -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( diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt new file mode 100644 index 0000000..80f5b64 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/Kit.kt @@ -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 + + /** 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 ) {} +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt new file mode 100644 index 0000000..76c4ebb --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/KitManager.kt @@ -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() + + // Lobby selections — set before the round starts + private val selectedKits = ConcurrentHashMap() + private val selectedPlaystyles = ConcurrentHashMap() + + // Live charge state — populated by applyKit, cleared by removeKit/clearAll + private val chargeData = ConcurrentHashMap() + + // ------------------------------------------------------------------------- + // Kit registration + // ------------------------------------------------------------------------- + + fun registerKit( + kit: Kit + ) { + registeredKits[kit.id] = kit + plugin.logger.info("[KitManager] Registered kit: ${kit.id}") + } + + fun getRegisteredKits(): Collection = 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] +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/Playstyle.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/Playstyle.kt new file mode 100644 index 0000000..6a567be --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/Playstyle.kt @@ -0,0 +1,8 @@ +package club.mcscrims.speedhg.kit + +enum class Playstyle( + val displayName: String +) { + AGGRESSIVE( "Aggressive" ), + DEFENSIVE( "Defensive" ) +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/AbilityResult.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/AbilityResult.kt new file mode 100644 index 0000000..38c0779 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/AbilityResult.kt @@ -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() + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt new file mode 100644 index 0000000..b80d993 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt @@ -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 ) {} + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/PassiveAbility.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/PassiveAbility.kt new file mode 100644 index 0000000..3646f0d --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/PassiveAbility.kt @@ -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` 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 ) {} + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/charge/ChargeState.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/charge/ChargeState.kt new file mode 100644 index 0000000..78c8c54 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/charge/ChargeState.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/charge/PlayerChargeData.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/charge/PlayerChargeData.kt new file mode 100644 index 0000000..bbbde1a --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/charge/PlayerChargeData.kt @@ -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 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt new file mode 100644 index 0000000..d557b9f --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt @@ -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 + 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>() + + 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" + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt new file mode 100644 index 0000000..d76850b --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TemplateKit.kt @@ -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 + // 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() + // 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 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt new file mode 100644 index 0000000..ba76445 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt @@ -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` 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 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/Extensions.kt b/src/main/kotlin/club/mcscrims/speedhg/util/Extensions.kt index 4d7bc6c..3cdd8bf 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/util/Extensions.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/util/Extensions.kt @@ -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() diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt b/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt new file mode 100644 index 0000000..3bbe954 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt @@ -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 + ): ItemBuilder + { + itemStack.editMeta { + val cLore = lore.stream() + .map( this::color ) + .map( Component::text ) + .toList() + + it.lore( cLore as List ) + } + 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 ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/WorldEditUtils.kt b/src/main/kotlin/club/mcscrims/speedhg/util/WorldEditUtils.kt new file mode 100644 index 0000000..847dfea --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/util/WorldEditUtils.kt @@ -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() + } + +} \ No newline at end of file