Add LobbyItemManager; PDC support; misc fixes

Introduce LobbyItemManager to provide interactive lobby hotbar items (kits & perks) tagged via PersistentDataContainer and open the appropriate GUIs. Wire the manager into SpeedHG (field, init, event registration) and clear lobby items at game start in GameManager. Add ItemBuilder.pdc helper and necessary imports for setting PDC. Fix potion duration calculation to use invincibilityTime*20. Change AnchorKit icon/trigger material to ANVIL. Update config (border start/end/shrink and disable anti-runner) and update en_US language entries (lobby item names, leaderboard format, kit lore/formatting and small text fixes).
This commit is contained in:
TDSTOS
2026-04-11 01:18:38 +02:00
parent cd8e3e37a7
commit 0896bc85a5
7 changed files with 275 additions and 27 deletions

View File

@@ -16,6 +16,7 @@ import club.mcscrims.speedhg.disaster.DisasterManager
import club.mcscrims.speedhg.game.GameManager import club.mcscrims.speedhg.game.GameManager
import club.mcscrims.speedhg.game.PodiumManager import club.mcscrims.speedhg.game.PodiumManager
import club.mcscrims.speedhg.game.modules.AntiRunningManager 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.gui.listener.MenuListener
import club.mcscrims.speedhg.kit.KitManager import club.mcscrims.speedhg.kit.KitManager
import club.mcscrims.speedhg.kit.impl.* import club.mcscrims.speedhg.kit.impl.*
@@ -110,6 +111,9 @@ class SpeedHG : JavaPlugin() {
lateinit var lunarClientManager: LunarClientManager lateinit var lunarClientManager: LunarClientManager
private set private set
lateinit var lobbyItemManager: LobbyItemManager
private set
override fun onLoad() override fun onLoad()
{ {
instance = this instance = this
@@ -164,6 +168,7 @@ class SpeedHG : JavaPlugin() {
kitManager = KitManager( this ) kitManager = KitManager( this )
discordWebhookManager = DiscordWebhookManager( this ) discordWebhookManager = DiscordWebhookManager( this )
lunarClientManager = LunarClientManager( this ) lunarClientManager = LunarClientManager( this )
lobbyItemManager = LobbyItemManager( this )
perkManager = PerkManager( this ) perkManager = PerkManager( this )
perkManager.initialize() perkManager.initialize()
@@ -271,6 +276,7 @@ class SpeedHG : JavaPlugin() {
pm.registerEvents( MenuListener(), this ) pm.registerEvents( MenuListener(), this )
pm.registerEvents(PerkEventDispatcher( this, perkManager ), this ) pm.registerEvents(PerkEventDispatcher( this, perkManager ), this )
pm.registerEvents( TeamListener(), this ) pm.registerEvents( TeamListener(), this )
pm.registerEvents( lobbyItemManager, this )
} }
private fun registerRecipes() private fun registerRecipes()

View File

@@ -154,6 +154,8 @@ class GameManager(
feastManager.reset() feastManager.reset()
pitManager.reset() pitManager.reset()
plugin.lobbyItemManager.clearAll()
setGameState( GameState.INVINCIBILITY ) setGameState( GameState.INVINCIBILITY )
timer = invincibilityTime timer = invincibilityTime
@@ -170,7 +172,7 @@ class GameManager(
val speedEffect = PotionEffect( val speedEffect = PotionEffect(
PotionEffectType.SPEED, PotionEffectType.SPEED,
timer, invincibilityTime * 20,
0, 0,
false, false,
false, false,
@@ -179,7 +181,7 @@ class GameManager(
val hasteEffect = PotionEffect( val hasteEffect = PotionEffect(
PotionEffectType.HASTE, PotionEffectType.HASTE,
timer, invincibilityTime * 20,
0, 0,
false, false,
false, false,

View File

@@ -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
}
}

View File

@@ -64,7 +64,7 @@ class AnchorKit : Kit() {
get() = plugin.languageManager.getDefaultComponent("kits.anchor.name", mapOf()) get() = plugin.languageManager.getDefaultComponent("kits.anchor.name", mapOf())
override val lore: List<String> override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList("kits.anchor.lore") get() = plugin.languageManager.getDefaultRawMessageList("kits.anchor.lore")
override val icon = Material.CHAIN override val icon = Material.ANVIL
companion object { companion object {
const val PARTIAL_RESISTANCE = 0.4 // 40 % immer aktiv const val PARTIAL_RESISTANCE = 0.4 // 40 % immer aktiv
@@ -141,7 +141,7 @@ class AnchorKit : Kit() {
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.anchor.items.chain.description") get() = plugin.languageManager.getDefaultRawMessage("kits.anchor.items.chain.description")
override val hardcodedHitsRequired = 15 override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.CHAIN override val triggerMaterial = Material.ANVIL
override fun execute(player: Player): AbilityResult { override fun execute(player: Player): AbilityResult {
// Alten Anker entfernen (kein Todesklang Spieler beschwört neuen) // Alten Anker entfernen (kein Todesklang Spieler beschwört neuen)

View File

@@ -5,9 +5,11 @@ import net.kyori.adventure.text.format.TextDecoration
import net.kyori.adventure.text.minimessage.MiniMessage import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.ChatColor import org.bukkit.ChatColor
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.enchantments.Enchantment import org.bukkit.enchantments.Enchantment
import org.bukkit.inventory.ItemFlag import org.bukkit.inventory.ItemFlag
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataType
class ItemBuilder( class ItemBuilder(
private val itemStack: ItemStack private val itemStack: ItemStack
@@ -76,6 +78,18 @@ class ItemBuilder(
return this return this
} }
fun <T, Z> pdc(
key: NamespacedKey,
dataType: PersistentDataType<T, Z>,
value: Z
): ItemBuilder
{
itemStack.editMeta {
it.persistentDataContainer.set( key, dataType, value!! )
}
return this
}
fun enchant( fun enchant(
ench: Enchantment ench: Enchantment
): ItemBuilder ): ItemBuilder

View File

@@ -7,13 +7,13 @@ game:
min-players: 2 min-players: 2
lobby-time: 60 lobby-time: 60
invincibility-time: 60 invincibility-time: 60
border-start: 300.0 border-start: 600.0
border-end: 20.0 border-end: 100.0
border-shrink-time: 600 # 10 Minuten border-shrink-time: 900 # 10 Minuten
ranked: false ranked: false
anti-runner: anti-runner:
enabled: true enabled: false
check-radius: 20.0 check-radius: 20.0
warn-time: 15 warn-time: 15
punish-time: 25 punish-time: 25

View File

@@ -18,6 +18,11 @@ game:
death-killed: '<prefix><yellow><player> has died whilst fighting <killer>! There are <left> players left.</yellow>' death-killed: '<prefix><yellow><player> has died whilst fighting <killer>! There are <left> players left.</yellow>'
death-pve: '<prefix><yellow><player> has died! There are <left> players left.</yellow>' death-pve: '<prefix><yellow><player> has died! There are <left> players left.</yellow>'
win-chat: '<prefix><green><winner> has won the game! Thanks for playing!</green>' win-chat: '<prefix><green><winner> has won the game! Thanks for playing!</green>'
lobby-items:
kits:
name: '<gold><bold>Kits</bold></gold> <gray>(right-click)</gray>'
perks:
name: '<light_purple><bold>Perks</bold></light_purple> <gray>(right-click)</gray>'
ranking: ranking:
placement_progress: '<prefix><gray>Placement <aqua><current>/<total></aqua> — Placed <aqua>#<placement></aqua> · <aqua><kills></aqua> Kill(s)</gray>' placement_progress: '<prefix><gray>Placement <aqua><current>/<total></aqua> — Placed <aqua>#<placement></aqua> · <aqua><kills></aqua> Kill(s)</gray>'
@@ -139,7 +144,7 @@ commands:
leaderboard: leaderboard:
header: '<gray>====== <gold>Leaderboard</gold> ======</gray>' header: '<gray>====== <gold>Leaderboard</gold> ======</gray>'
empty: '<red>There are currently no stats</red>' empty: '<red>There are currently no stats</red>'
line: '#<rank> - <green><name></green> - <aqua><score></aqua>' line: '#<rank> - <green><name></green> <dark_gray>[<white><playerrank></white>]</dark_gray> - <aqua><score></aqua>'
footer: '<gray>====== <gold>Leaderboard</gold> ======</gray>' footer: '<gray>====== <gold>Leaderboard</gold> ======</gray>'
timer: timer:
usage: '<red>Usage: /timer <seconds></red>' usage: '<red>Usage: /timer <seconds></red>'
@@ -311,11 +316,8 @@ kits:
name: '<gradient:dark_green:gray><bold>Goblin</bold></gradient>' name: '<gradient:dark_green:gray><bold>Goblin</bold></gradient>'
lore: lore:
- ' ' - ' '
- 'AGGRESSIVE:' - 'AGGRESSIVE: Copy your enemies kit'
- 'Copy your enemies kit' - 'DEFENSIVE: Summon a bunker for protection'
- ' '
- 'DEFENSIVE:'
- 'Summon a bunker for protection'
items: items:
steal: steal:
name: '§cSteal Kit' name: '§cSteal Kit'
@@ -331,11 +333,8 @@ kits:
name: '<gradient:dark_aqua:aqua><bold>IceMage</bold></gradient>' name: '<gradient:dark_aqua:aqua><bold>IceMage</bold></gradient>'
lore: lore:
- ' ' - ' '
- 'AGGRESSIVE:' - 'AGGRESSIVE: Gain speed in ice biomes and give slowness'
- 'Gain speed in ice biomes and give slowness' - 'DEFENSIVE: Summon snowballs and freeze enemies'
- ' '
- 'DEFENSIVE:'
- 'Summon snowballs and freeze enemies'
items: items:
snowball: snowball:
name: '§bFreeze' name: '§bFreeze'
@@ -350,11 +349,8 @@ kits:
name: '<gradient:dark_red:red><bold>Venom</bold></gradient>' name: '<gradient:dark_red:red><bold>Venom</bold></gradient>'
lore: lore:
- ' ' - ' '
- 'AGGRESSIVE:' - 'AGGRESSIVE: Summon a deafening beam'
- 'Summon a deafening beam' - 'DEFENSIVE: Create a shield for protection'
- ' '
- 'DEFENSIVE:'
- 'Create a shield for protection'
items: items:
wither: wither:
name: '§8Deafening Beam' name: '§8Deafening Beam'
@@ -371,7 +367,7 @@ kits:
shield_break: '<red>Your shield of darkness has broken!</red>' shield_break: '<red>Your shield of darkness has broken!</red>'
ability_charged: '<yellow>Your ability has been recharged</yellow>' ability_charged: '<yellow>Your ability has been recharged</yellow>'
rattlesnake: rattlesnake:
name: '<gradient:green:lime><bold>Rattlesnake</bold></gradient>' name: '<gradient:green:dark_green><bold>Rattlesnake</bold></gradient>'
lore: lore:
- ' ' - ' '
- 'AGGRESSIVE: Sneak-charged pounce' - 'AGGRESSIVE: Sneak-charged pounce'
@@ -508,10 +504,10 @@ kits:
- 'DEFENSIVE: Blindness + Slowness III (4 s)' - 'DEFENSIVE: Blindness + Slowness III (4 s)'
items: items:
drain: drain:
name: '<dark_purple>Life Drain' name: '<dark_purple>Life Drain</dark_purple>'
description: 'Drain life from nearby enemies. Sneak to cancel.' description: 'Drain life from nearby enemies. Sneak to cancel.'
fear: fear:
name: '<dark_purple>Puppeteer''s Fear' name: '<dark_purple>Puppeteer''s Fear</dark_purple>'
description: 'Apply Blindness + Slowness to nearby enemies' description: 'Apply Blindness + Slowness to nearby enemies'
messages: messages:
drain_start: '<dark_purple>Draining life...' drain_start: '<dark_purple>Draining life...'