diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index e5b3020..97a1aab 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -16,6 +16,7 @@ import club.mcscrims.speedhg.disaster.DisasterManager import club.mcscrims.speedhg.game.GameManager import club.mcscrims.speedhg.game.PodiumManager import club.mcscrims.speedhg.game.modules.AntiRunningManager +import club.mcscrims.speedhg.game.modules.LobbyItemManager import club.mcscrims.speedhg.gui.listener.MenuListener import club.mcscrims.speedhg.kit.KitManager import club.mcscrims.speedhg.kit.impl.* @@ -110,6 +111,9 @@ class SpeedHG : JavaPlugin() { lateinit var lunarClientManager: LunarClientManager private set + lateinit var lobbyItemManager: LobbyItemManager + private set + override fun onLoad() { instance = this @@ -164,6 +168,7 @@ class SpeedHG : JavaPlugin() { kitManager = KitManager( this ) discordWebhookManager = DiscordWebhookManager( this ) lunarClientManager = LunarClientManager( this ) + lobbyItemManager = LobbyItemManager( this ) perkManager = PerkManager( this ) perkManager.initialize() @@ -271,6 +276,7 @@ class SpeedHG : JavaPlugin() { pm.registerEvents( MenuListener(), this ) pm.registerEvents(PerkEventDispatcher( this, perkManager ), this ) pm.registerEvents( TeamListener(), this ) + pm.registerEvents( lobbyItemManager, this ) } private fun registerRecipes() diff --git a/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt b/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt index 72ca911..ddb17f4 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/game/GameManager.kt @@ -154,6 +154,8 @@ class GameManager( feastManager.reset() pitManager.reset() + plugin.lobbyItemManager.clearAll() + setGameState( GameState.INVINCIBILITY ) timer = invincibilityTime @@ -170,7 +172,7 @@ class GameManager( val speedEffect = PotionEffect( PotionEffectType.SPEED, - timer, + invincibilityTime * 20, 0, false, false, @@ -179,7 +181,7 @@ class GameManager( val hasteEffect = PotionEffect( PotionEffectType.HASTE, - timer, + invincibilityTime * 20, 0, false, false, diff --git a/src/main/kotlin/club/mcscrims/speedhg/game/modules/LobbyItemManager.kt b/src/main/kotlin/club/mcscrims/speedhg/game/modules/LobbyItemManager.kt new file mode 100644 index 0000000..f1d05ee --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/game/modules/LobbyItemManager.kt @@ -0,0 +1,230 @@ +package club.mcscrims.speedhg.game.modules + +import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.game.GameState +import club.mcscrims.speedhg.game.modules.LobbyItemManager.Companion.TAG_KITS +import club.mcscrims.speedhg.game.modules.LobbyItemManager.Companion.TAG_PERKS +import club.mcscrims.speedhg.gui.menu.KitSelectorMenu +import club.mcscrims.speedhg.gui.menu.PerkSelectorMenu +import club.mcscrims.speedhg.util.ItemBuilder +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.block.Action +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.player.PlayerDropItemEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataType + +/** + * Manages interactive lobby hotbar items for the LOBBY and STARTING phases. + * + * ## Items + * - Slot 0: Kit selector (CHEST) + * - Slot 1: Perk selector (ENDER_CHEST) + * + * ## PDC tagging + * Each item is tagged with [KEY_LOBBY_ITEM] → a String value identifying which + * menu to open ([TAG_KITS] or [TAG_PERKS]). This avoids brittle display-name + * comparisons and survives MiniMessage colour changes cleanly. + * + * ## Lifecycle + * Call [giveItems] on join (handled internally via [onJoin]). + * Call [clearAll] when transitioning to INVINCIBILITY — see [SpeedHG.startGame]. + */ +class LobbyItemManager( + private val plugin: SpeedHG +) : Listener { + + // ── PDC keys ───────────────────────────────────────────────────────────── + + /** Key stored on every lobby item; value identifies the target menu. */ + val key: NamespacedKey = NamespacedKey( plugin, "lobby_item" ) + + companion object { + const val TAG_KITS = "kits" + const val TAG_PERKS = "perks" + } + + // ── Item definitions ────────────────────────────────────────────────────── + + /** + * Creates a fresh kit-selector item. + * Always call this factory rather than caching a singleton, because + * [ItemStack] is mutable and Paper may modify the stack object in-place. + */ + fun buildKitItem( + player: Player + ): ItemStack + { + return ItemBuilder( Material.CHEST ) + .name(plugin.languageManager.getComponent( player, "game.lobby-items.kits.name", mapOf() )) + .pdc( key, PersistentDataType.STRING, TAG_KITS ) + .build() + } + + /** + * Creates a fresh perk-selector item. + */ + fun buildPerkItem( + player: Player + ): ItemStack + { + return ItemBuilder( Material.ENDER_CHEST ) + .name(plugin.languageManager.getComponent( player, "game.lobby-items.perks.name", mapOf() )) + .pdc( key, PersistentDataType.STRING, TAG_PERKS ) + .build() + } + + // ── Public API ──────────────────────────────────────────────────────────── + + /** + * Clears the player's inventory and places the two lobby items into + * hotbar slots 0 and 1. + * + * Only call this during [GameState.LOBBY] or [GameState.STARTING]. + */ + fun giveItems( + player: Player + ) { + player.inventory.clear() + player.inventory.setItem( 0, buildKitItem( player )) + player.inventory.setItem( 1, buildPerkItem( player )) + } + + /** + * Removes lobby items from every online player. + * + * Call this at the start of [GameState.INVINCIBILITY] (inside + * [GameManager.startGame], just before kit items are distributed): + * + * ```kotlin + * // Inside startGame(), before applyKit() loop: + * plugin.lobbyItemManager.clearAll() + * ``` + */ + fun clearAll() + { + plugin.server.onlinePlayers.forEach { + removeFromPlayer( it ) + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + /** + * Returns the PDC tag value ([TAG_KITS] / [TAG_PERKS]) if the given + * [ItemStack] is a lobby item, or `null` otherwise. + */ + private fun ItemStack?.lobbyTag(): String? + { + this ?: return null + if ( type.isAir ) return null + return itemMeta?.persistentDataContainer?.get( key, PersistentDataType.STRING ) + } + + /** Removes only the lobby-tagged items from the player's inventory. */ + private fun removeFromPlayer( + player: Player + ) { + player.inventory.forEachIndexed { index, stack -> + if ( stack.lobbyTag() != null ) player.inventory.setItem( index, null ) + } + } + + private fun isLobbyPhase(): Boolean = when( plugin.gameManager.currentState ) + { + GameState.LOBBY, GameState.STARTING -> true + else -> false + } + + // ── Event handlers ──────────────────────────────────────────────────────── + + /** + * Give items to players who join while the server is in the lobby phase. + */ + @EventHandler(priority = EventPriority.MONITOR) + fun onJoin( + event: PlayerJoinEvent + ) { + if ( !isLobbyPhase() ) return + // Defer by one tick so ConnectListener's join logic (kick check etc.) + // completes first and we don't give items to a player about to be kicked. + plugin.server.scheduler.runTask( plugin ) { -> + if ( event.player.isOnline && isLobbyPhase() ) + { + giveItems( event.player ) + } + } + } + + /** + * Right-click handler — opens the appropriate GUI. + * + * Priority HIGH so we run before other interact listeners and can cancel + * early without them seeing the event at all. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false) + fun onInteract( + event: PlayerInteractEvent + ) { + // Fast-exit: only main-hand right-clicks + if ( event.hand != EquipmentSlot.HAND ) return + if ( event.action != Action.RIGHT_CLICK_AIR && + event.action != Action.RIGHT_CLICK_BLOCK ) return + if ( !isLobbyPhase() ) return + + val tag = event.player.inventory.itemInMainHand.lobbyTag() ?: return + + // Cancel unconditionally — prevents chest placement, block interaction, etc. + event.isCancelled = true + + when( tag ) + { + TAG_KITS -> KitSelectorMenu( event.player ).open( event.player ) + TAG_PERKS -> PerkSelectorMenu( event.player ).open( event.player ) + } + } + + /** + * Prevents lobby items from being dropped. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false) + fun onDrop( + event: PlayerDropItemEvent + ) { + if ( !isLobbyPhase() ) return + if ( event.itemDrop.itemStack.lobbyTag() != null ) event.isCancelled = true + } + + /** + * Prevents lobby items from being moved within the inventory. + * + * We only listen on the player's own inventory to avoid interfering + * with chest/crafting interactions — the cursor item and the clicked + * item are both checked. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false) + fun onInventoryClick( + event: InventoryClickEvent + ) { + if ( !isLobbyPhase() ) return + val player = event.whoClicked as? Player ?: return + + // Only guard the player's own inventory (bottom section or hotbar) + val isOwnInventory = event.clickedInventory == player.inventory + if ( !isOwnInventory ) return + + val clickedIsLobby = event.currentItem.lobbyTag() != null + val cursorIsLobby = event.cursor.lobbyTag() != null + + if ( clickedIsLobby || cursorIsLobby ) event.isCancelled = true + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt index c1f9f4b..9d2cdcc 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/AnchorKit.kt @@ -64,7 +64,7 @@ class AnchorKit : Kit() { get() = plugin.languageManager.getDefaultComponent("kits.anchor.name", mapOf()) override val lore: List get() = plugin.languageManager.getDefaultRawMessageList("kits.anchor.lore") - override val icon = Material.CHAIN + override val icon = Material.ANVIL companion object { const val PARTIAL_RESISTANCE = 0.4 // 40 % – immer aktiv @@ -141,7 +141,7 @@ class AnchorKit : Kit() { override val description: String get() = plugin.languageManager.getDefaultRawMessage("kits.anchor.items.chain.description") override val hardcodedHitsRequired = 15 - override val triggerMaterial = Material.CHAIN + override val triggerMaterial = Material.ANVIL override fun execute(player: Player): AbilityResult { // Alten Anker entfernen (kein Todesklang – Spieler beschwört neuen) diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt b/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt index cbc04ad..f736b6f 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt @@ -5,9 +5,11 @@ import net.kyori.adventure.text.format.TextDecoration import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.ChatColor import org.bukkit.Material +import org.bukkit.NamespacedKey import org.bukkit.enchantments.Enchantment import org.bukkit.inventory.ItemFlag import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataType class ItemBuilder( private val itemStack: ItemStack @@ -76,6 +78,18 @@ class ItemBuilder( return this } + fun pdc( + key: NamespacedKey, + dataType: PersistentDataType, + value: Z + ): ItemBuilder + { + itemStack.editMeta { + it.persistentDataContainer.set( key, dataType, value!! ) + } + return this + } + fun enchant( ench: Enchantment ): ItemBuilder diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 27c926b..ff990ec 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -7,13 +7,13 @@ game: min-players: 2 lobby-time: 60 invincibility-time: 60 - border-start: 300.0 - border-end: 20.0 - border-shrink-time: 600 # 10 Minuten + border-start: 600.0 + border-end: 100.0 + border-shrink-time: 900 # 10 Minuten ranked: false anti-runner: - enabled: true + enabled: false check-radius: 20.0 warn-time: 15 punish-time: 25 diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index 8a59847..580857d 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -18,6 +18,11 @@ game: death-killed: ' has died whilst fighting ! There are players left.' death-pve: ' has died! There are players left.' win-chat: ' has won the game! Thanks for playing!' + lobby-items: + kits: + name: 'Kits (right-click)' + perks: + name: 'Perks (right-click)' ranking: placement_progress: 'Placement / — Placed # · Kill(s)' @@ -139,7 +144,7 @@ commands: leaderboard: header: '====== Leaderboard ======' empty: 'There are currently no stats' - line: '# - - ' + line: '# - [] - ' footer: '====== Leaderboard ======' timer: usage: 'Usage: /timer ' @@ -311,11 +316,8 @@ kits: name: 'Goblin' lore: - ' ' - - 'AGGRESSIVE:' - - 'Copy your enemies kit' - - ' ' - - 'DEFENSIVE:' - - 'Summon a bunker for protection' + - 'AGGRESSIVE: Copy your enemies kit' + - 'DEFENSIVE: Summon a bunker for protection' items: steal: name: '§cSteal Kit' @@ -331,11 +333,8 @@ kits: name: 'IceMage' lore: - ' ' - - 'AGGRESSIVE:' - - 'Gain speed in ice biomes and give slowness' - - ' ' - - 'DEFENSIVE:' - - 'Summon snowballs and freeze enemies' + - 'AGGRESSIVE: Gain speed in ice biomes and give slowness' + - 'DEFENSIVE: Summon snowballs and freeze enemies' items: snowball: name: '§bFreeze' @@ -350,11 +349,8 @@ kits: name: 'Venom' lore: - ' ' - - 'AGGRESSIVE:' - - 'Summon a deafening beam' - - ' ' - - 'DEFENSIVE:' - - 'Create a shield for protection' + - 'AGGRESSIVE: Summon a deafening beam' + - 'DEFENSIVE: Create a shield for protection' items: wither: name: '§8Deafening Beam' @@ -371,7 +367,7 @@ kits: shield_break: 'Your shield of darkness has broken!' ability_charged: 'Your ability has been recharged' rattlesnake: - name: 'Rattlesnake' + name: 'Rattlesnake' lore: - ' ' - 'AGGRESSIVE: Sneak-charged pounce' @@ -508,10 +504,10 @@ kits: - 'DEFENSIVE: Blindness + Slowness III (4 s)' items: drain: - name: 'Life Drain' + name: 'Life Drain' description: 'Drain life from nearby enemies. Sneak to cancel.' fear: - name: 'Puppeteer''s Fear' + name: 'Puppeteer''s Fear' description: 'Apply Blindness + Slowness to nearby enemies' messages: drain_start: 'Draining life...'