From b2edcff44700d700f8fa983c1fc5431143451e2a Mon Sep 17 00:00:00 2001 From: TDSTOS Date: Thu, 26 Mar 2026 04:13:26 +0100 Subject: [PATCH] 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. --- .../kotlin/club/mcscrims/speedhg/SpeedHG.kt | 2 + .../club/mcscrims/speedhg/kit/KitMetaData.kt | 12 + .../mcscrims/speedhg/kit/impl/GladiatorKit.kt | 303 ++++++++++++++++++ .../kit/listener/KitEventDispatcher.kt | 70 ++++ .../speedhg/listener/GameStateListener.kt | 6 - .../mcscrims/speedhg/util/WorldEditUtils.kt | 25 ++ src/main/resources/languages/en_US.yml | 23 +- 7 files changed, 432 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/club/mcscrims/speedhg/kit/KitMetaData.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index 4f7bbb7..45b2137 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -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() ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/KitMetaData.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/KitMetaData.kt new file mode 100644 index 0000000..52957ee --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/KitMetaData.kt @@ -0,0 +1,12 @@ +package club.mcscrims.speedhg.kit + +enum class KitMetaData { + IN_GLADIATOR, + GLADIATOR_BLOCK; + + fun getKey(): String + { + return name + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt new file mode 100644 index 0000000..d9b7cfe --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt @@ -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 + 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>() + + 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() + } + + } + +} \ 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 index 1f9c1ba..c278323 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt @@ -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 diff --git a/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt b/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt index eab5fd2..c1b4109 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt @@ -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 } } \ 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 index 847dfea..ec145a9 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/util/WorldEditUtils.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/util/WorldEditUtils.kt @@ -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() + } + } \ No newline at end of file diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index a929184..5ac2395 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -81,7 +81,7 @@ scoreboard: kits: backup: - name: 'Backup' + name: 'Backup' 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: 'Gladiator' + 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: 'Cage' + description: 'Fight an enemy in a 1v1 above the skies' + messages: + ability_charged: 'Your ability has been recharged!' goblin: name: 'Goblin' lore: @@ -117,7 +134,7 @@ kits: spawn_bunker: 'You have created a bunker around yourself!' ability_charged: 'Your ability has been recharged!' icemage: - name: 'IceMage' + name: 'IceMage' lore: - ' ' - 'AGGRESSIVE:' @@ -141,7 +158,7 @@ kits: shoot_snowballs: 'You have shot frozen snowballs in all directions!' ability_charged: 'Your ability has been recharged!' venom: - name: 'Venom' + name: 'Venom' lore: - ' ' - 'AGGRESSIVE:'