Add Gladiator kit and WorldEdit support

Introduce the Gladiator kit feature and supporting infrastructure: add KitMetaData enum and new GladiatorKit implementation (items, abilities, fight lifecycle and region logic), register the kit in SpeedHG, and add language entries for the kit and some bolded kit names. Add WorldEditUtils.createCylinder helper to build/tear down arenas. Extend KitEventDispatcher with listeners to handle gladiator arena block interactions and explosion handling (with a helper to step glass color -> air). Also add ingame guards in several projectile handlers and remove a small TODO/comment in GameStateListener.
This commit is contained in:
TDSTOS
2026-03-26 04:13:26 +01:00
parent 0043a3e82c
commit b2edcff447
7 changed files with 432 additions and 9 deletions

View File

@@ -9,6 +9,7 @@ import club.mcscrims.speedhg.game.GameManager
import club.mcscrims.speedhg.game.modules.AntiRunningManager
import club.mcscrims.speedhg.kit.KitManager
import club.mcscrims.speedhg.kit.impl.BackupKit
import club.mcscrims.speedhg.kit.impl.GladiatorKit
import club.mcscrims.speedhg.kit.impl.GoblinKit
import club.mcscrims.speedhg.kit.impl.IceMageKit
import club.mcscrims.speedhg.kit.impl.VenomKit
@@ -93,6 +94,7 @@ class SpeedHG : JavaPlugin() {
private fun registerKits()
{
kitManager.registerKit( BackupKit() )
kitManager.registerKit( GladiatorKit() )
kitManager.registerKit( GoblinKit() )
kitManager.registerKit( IceMageKit() )
kitManager.registerKit( VenomKit() )

View File

@@ -0,0 +1,12 @@
package club.mcscrims.speedhg.kit
enum class KitMetaData {
IN_GLADIATOR,
GLADIATOR_BLOCK;
fun getKey(): String
{
return name
}
}

View File

@@ -0,0 +1,303 @@
package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.KitMetaData
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.trans
import com.sk89q.worldedit.bukkit.BukkitAdapter
import com.sk89q.worldedit.math.Vector2
import com.sk89q.worldedit.regions.CylinderRegion
import com.sk89q.worldedit.regions.Region
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.Sound
import org.bukkit.World
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.metadata.FixedMetadataValue
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.scheduler.BukkitRunnable
import java.util.Random
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
class GladiatorKit : Kit() {
private val plugin get() = SpeedHG.instance
override val id: String
get() = "gladiator"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.gladiator.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.gladiator.lore" )
override val icon: Material
get() = Material.IRON_BARS
// ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AllActive( Playstyle.AGGRESSIVE )
private val defensiveActive = AllActive( Playstyle.DEFENSIVE )
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
private val defensivePassive = NoPassive( Playstyle.DEFENSIVE )
// ── 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 val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(
player: Player,
playstyle: Playstyle
) {
val ironBars = ItemBuilder( Material.IRON_BARS )
.name( aggressiveActive.name )
.lore(listOf( aggressiveActive.description ))
.build()
cachedItems[ player.uniqueId ] = listOf( ironBars )
player.inventory.addItem( ironBars )
}
// ── Optional lifecycle hooks ──────────────────────────────────────────────
override fun onRemove(
player: Player
) {
val items = cachedItems.remove( player.uniqueId ) ?: return
items.forEach { player.inventory.remove( it ) }
}
private inner class AllActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) {
private val plugin get() = SpeedHG.instance
override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.name" )
override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.gladiator.items.ironBars.description" )
override val hitsRequired: Int
get() = 15
override val triggerMaterial: Material
get() = Material.IRON_BARS
override fun execute(
player: Player
): AbilityResult
{
val lineOfSight = player.getTargetEntity( 3 ) as? Player
?: return AbilityResult.ConditionNotMet( "No player in line of sight" )
if (player.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ) ||
lineOfSight.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ))
return AbilityResult.ConditionNotMet( "Already in gladiator fight" )
val radius = 23
val height = 10
player.setMetadata( KitMetaData.IN_GLADIATOR.getKey(), FixedMetadataValue( plugin, true ))
lineOfSight.setMetadata( KitMetaData.IN_GLADIATOR.getKey(), FixedMetadataValue( plugin, true ))
val gladiatorRegion = getGladiatorLocation(player.location.clone().add( 0.0, 64.0, 0.0 ), radius, height + 2 )
val center = BukkitAdapter.adapt( player.world, gladiatorRegion.center )
WorldEditUtils.createCylinder( player.world, center, radius - 1, true, 1, Material.WHITE_STAINED_GLASS )
WorldEditUtils.createCylinder( player.world, center, radius - 1, false, height, Material.WHITE_STAINED_GLASS )
WorldEditUtils.createCylinder( player.world, center.clone().add( 0.0, height - 1.0, 0.0 ), radius -1, true, 1, Material.WHITE_STAINED_GLASS )
Bukkit.getScheduler().runTaskLater( plugin, { ->
for ( vector3 in gladiatorRegion )
{
val block = player.world.getBlockAt(BukkitAdapter.adapt( player.world, vector3 ))
if ( block.type.isAir ) continue
block.setMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), FixedMetadataValue( plugin, true ))
}
}, 5L )
val gladiatorFight = GladiatorFight( gladiatorRegion, player, lineOfSight, radius, height )
gladiatorFight.runTaskTimer( plugin, 0, 20 )
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.gladiator.messages.ability_charged" ))
}
}
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) {
override val name: String
get() = "None"
override val description: String
get() = "None"
}
// ── Helper methods ────────────────────────────────────────────────────────
private fun getGladiatorLocation(
location: Location,
radius: Int,
height: Int
): Region
{
val random = Random()
val region = CylinderRegion(
BukkitAdapter.adapt( location.world ),
BukkitAdapter.asBlockVector( location ),
Vector2.at( radius.toDouble(), radius.toDouble() ),
location.blockY, location.blockY + height
)
return if (!hasEnoughSpace( region ))
getGladiatorLocation(location.add(if ( random.nextBoolean() ) -10.0 else 10.0, 5.0, if ( random.nextBoolean() ) -10.0 else 10.0 ), radius, height )
else region
}
private fun hasEnoughSpace(
region: Region
): Boolean
{
val world: World
if ( region.world != null )
world = BukkitAdapter.adapt( region.world )
else return true
for ( vector3 in region )
{
val adapt = BukkitAdapter.adapt( world, vector3 )
if (!world.worldBorder.isInside( adapt ))
return false
if (!world.getBlockAt( adapt ).type.isAir)
return false
}
return true
}
private class GladiatorFight(
val region: Region,
val gladiator: Player,
val enemy: Player,
val radius: Int,
val height: Int
) : BukkitRunnable() {
private val plugin get() = SpeedHG.instance
val world: World = BukkitAdapter.adapt( region.world )
val center: Location = BukkitAdapter.adapt( world, region.center )
private val oldLocationGladiator: Location = gladiator.location
private val oldLocationEnemy: Location = enemy.location
var timer = 0
init { init() }
fun init()
{
gladiator.addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ))
enemy.addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ))
gladiator.teleport(Location( world, center.x + radius / 2, center.y + 1, center.z, 90f, 0f ))
enemy.teleport(Location( world, center.x - radius / 2, center.y + 1, center.z, -90f, 0f ))
}
override fun run()
{
if ( !gladiator.isOnline || !enemy.isOnline )
{
endFight()
return
}
if ( gladiator.location.y < center.y || enemy.location.y < center.y )
{
endFight()
return
}
if (region.contains(BukkitAdapter.asBlockVector( gladiator.location )) &&
region.contains(BukkitAdapter.asBlockVector( enemy.location )))
{
timer++
if ( timer > 180 )
{
gladiator.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 ))
enemy.addPotionEffect(PotionEffect( PotionEffectType.WITHER, Int.MAX_VALUE, 2 ))
}
}
}
private fun endFight()
{
gladiator.apply {
removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin )
removePotionEffect( PotionEffectType.WITHER )
addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ))
if ( isOnline && plugin.gameManager.alivePlayers.contains( uniqueId ))
teleport( oldLocationGladiator )
}
enemy.apply {
removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin )
removePotionEffect( PotionEffectType.WITHER )
addPotionEffect(PotionEffect( PotionEffectType.RESISTANCE, 40, 20 ))
if ( isOnline && plugin.gameManager.alivePlayers.contains( uniqueId ))
teleport( oldLocationEnemy )
}
for ( vector3 in region )
{
val block = world.getBlockAt(BukkitAdapter.adapt( world, vector3 ))
if (!block.hasMetadata( KitMetaData.GLADIATOR_BLOCK.getKey() )) continue
block.removeMetadata( KitMetaData.GLADIATOR_BLOCK.getKey(), plugin )
}
WorldEditUtils.createCylinder( world, center, radius, true, height, Material.AIR )
cancel()
}
}
}

View File

@@ -3,24 +3,30 @@ 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.KitMetaData
import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult
import club.mcscrims.speedhg.kit.impl.IceMageKit
import club.mcscrims.speedhg.kit.impl.VenomKit
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.Sound
import org.bukkit.block.Block
import org.bukkit.entity.Arrow
import org.bukkit.entity.Egg
import org.bukkit.entity.LivingEntity
import org.bukkit.entity.Player
import org.bukkit.entity.Snowball
import org.bukkit.entity.ThrownPotion
import org.bukkit.event.Cancellable
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.entity.EntityExplodeEvent
import org.bukkit.event.entity.ProjectileHitEvent
import org.bukkit.event.entity.ProjectileLaunchEvent
import org.bukkit.event.player.PlayerInteractEvent
@@ -199,6 +205,9 @@ class KitEventDispatcher(
fun onSnowballThrow(
event: ProjectileLaunchEvent
) {
if ( !isIngame() )
return
val projectile = event.entity as? Snowball ?: return
val shooter = projectile.shooter as? Player ?: return
if (kitManager.getSelectedKit( shooter ) !is IceMageKit ) return
@@ -231,6 +240,9 @@ class KitEventDispatcher(
fun onSnowballHit(
event: ProjectileHitEvent
) {
if ( !isIngame() )
return
val projectile = event.entity as? Snowball ?: return
if (!projectile.persistentDataContainer.has( iceMageKey, PersistentDataType.BYTE )) return
@@ -251,6 +263,9 @@ class KitEventDispatcher(
fun onProjectileHit(
event: ProjectileHitEvent
) {
if ( !isIngame() )
return
val victim = event.hitEntity as? Player ?: return
val projectile = event.entity
@@ -277,10 +292,65 @@ class KitEventDispatcher(
}
}
// =========================================================================
// Gladiator Listener
// =========================================================================
@EventHandler
fun onBlockBreak(
event: BlockBreakEvent
) {
if ( !isIngame() )
return
val player = event.player
if (!player.hasMetadata( KitMetaData.IN_GLADIATOR.getKey() ))
return
val block = event.block
if (!block.hasMetadata( KitMetaData.GLADIATOR_BLOCK.getKey() ))
return
changeGladiatorBlock( event, block )
}
@EventHandler
fun onExplode(
event: EntityExplodeEvent
) {
if ( !isIngame() )
{
event.isCancelled = true
return
}
event.blockList().stream()
.filter { block -> block.hasMetadata( KitMetaData.GLADIATOR_BLOCK.getKey() ) }
.forEach { block -> changeGladiatorBlock( event, block ) }
}
// =========================================================================
// Helpers
// =========================================================================
private fun changeGladiatorBlock(
event: Cancellable,
block: Block
) {
event.isCancelled = true
when( block.type )
{
Material.WHITE_STAINED_GLASS -> block.type = Material.YELLOW_STAINED_GLASS
Material.YELLOW_STAINED_GLASS -> block.type = Material.ORANGE_STAINED_GLASS
Material.ORANGE_STAINED_GLASS -> block.type = Material.RED_STAINED_GLASS
Material.RED_STAINED_GLASS -> block.type = Material.AIR
else -> return
}
}
private fun isIngame(): Boolean = when ( plugin.gameManager.currentState )
{
GameState.INGAME, GameState.INVINCIBILITY -> true

View File

@@ -123,10 +123,6 @@ class GameStateListener : Listener {
player.sendMsg("build.no_iron_before_feast")
player.playSound(player.location, Sound.ENTITY_VILLAGER_NO, 1f, 1f)
}
else if ( block.type == Material.IRON_ORE && feastStarted )
{
// TODO: Database-Aufruf
}
}
private fun pickupBlock(
@@ -354,8 +350,6 @@ class GameStateListener : Listener {
item.editMeta { meta -> ( meta as Damageable ).damage /= 2 }
player.sendMsg( "craft.iron_nerf" )
}
// TODO: add 0.1 to ironFarmed in database
}
}

View File

@@ -34,4 +34,29 @@ object WorldEditUtils {
e.printStackTrace()
}
fun createCylinder(
world: World,
startLocation: Location,
radius: Int,
filled: Boolean,
height: Int,
block: Material
) = try {
val editSession = WorldEdit.getInstance().newEditSessionBuilder()
.world(BukkitAdapter.adapt( world )).maxBlocks( -1 ).build()
editSession.sideEffectApplier = SideEffectSet.defaults()
editSession.makeCylinder(
BukkitAdapter.asBlockVector( startLocation ),
BukkitAdapter.asBlockState(ItemStack( block )),
radius.toDouble(), height, filled
)
editSession.commit()
editSession.close()
} catch ( e: Exception ) {
e.printStackTrace()
}
}

View File

@@ -81,7 +81,7 @@ scoreboard:
kits:
backup:
name: '<gradient:gold:#ff841f>Backup</gradient>'
name: '<gradient:gold:#ff841f><bold>Backup</bold></gradient>'
lore:
- ' '
- 'Select a kit mid-round at any time.'
@@ -91,6 +91,23 @@ kits:
- ' '
- 'Left-click to select'
- 'Right-click to change playstyle'
gladiator:
name: '<gradient:dark_gray:gray><bold>Gladiator</bold></gradient>'
lore:
- ' '
- 'Use your ability to fight enemies'
- 'in a 1v1 above the skies.'
- ' '
- 'PlayStyle: §e%playstyle%'
- ' '
- 'Left-click to select'
- 'Right-click to change playstyle'
items:
ironBars:
name: '<gray>Cage</gray>'
description: 'Fight an enemy in a 1v1 above the skies'
messages:
ability_charged: '<yellow>Your ability has been recharged!</yellow>'
goblin:
name: '<gradient:dark_green:gray><bold>Goblin</bold></gradient>'
lore:
@@ -117,7 +134,7 @@ kits:
spawn_bunker: '<green>You have created a bunker around yourself!'
ability_charged: '<yellow>Your ability has been recharged!</yellow>'
icemage:
name: '<gradient:dark_aqua:aqua>IceMage</gradient>'
name: '<gradient:dark_aqua:aqua><bold>IceMage</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE:'
@@ -141,7 +158,7 @@ kits:
shoot_snowballs: '<aqua>You have shot frozen snowballs in all directions!</aqua>'
ability_charged: '<yellow>Your ability has been recharged!</yellow>'
venom:
name: '<gradient:dark_red:red>Venom</gradient>'
name: '<gradient:dark_red:red><bold>Venom</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE:'