Add Kit GUI (menu/anvil) and integrate MenuListener
Introduce a full chest-based kit selection UI and anvil search: add Menu, MenuHolder, KitSelectorMenu, KitItemFactory, AnvilSearchMenu (with AnvilSearchTracker) and a central MenuListener to dispatch menu and anvil interactions. Integrate the GUI into KitCommand (opens selector on no args, enforces mid-game selection rules, remove/replace backup kits when switching) and register MenuListener in the main plugin. Add permission speedhg.bypass and update ConnectListener to kick joiners when a game is running unless they have the bypass permission. Remove static playstyle lore from language file (now rendered dynamically), and apply small import/refactor cleanups (use wildcard imports for kit impls, java.util.* replacements).
This commit is contained in:
@@ -7,12 +7,9 @@ import club.mcscrims.speedhg.database.DatabaseManager
|
|||||||
import club.mcscrims.speedhg.database.StatsManager
|
import club.mcscrims.speedhg.database.StatsManager
|
||||||
import club.mcscrims.speedhg.game.GameManager
|
import club.mcscrims.speedhg.game.GameManager
|
||||||
import club.mcscrims.speedhg.game.modules.AntiRunningManager
|
import club.mcscrims.speedhg.game.modules.AntiRunningManager
|
||||||
|
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.BackupKit
|
import club.mcscrims.speedhg.kit.impl.*
|
||||||
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
|
|
||||||
import club.mcscrims.speedhg.kit.listener.KitEventDispatcher
|
import club.mcscrims.speedhg.kit.listener.KitEventDispatcher
|
||||||
import club.mcscrims.speedhg.listener.ConnectListener
|
import club.mcscrims.speedhg.listener.ConnectListener
|
||||||
import club.mcscrims.speedhg.listener.GameStateListener
|
import club.mcscrims.speedhg.listener.GameStateListener
|
||||||
@@ -118,6 +115,7 @@ class SpeedHG : JavaPlugin() {
|
|||||||
pm.registerEvents( SoupListener(), this )
|
pm.registerEvents( SoupListener(), this )
|
||||||
pm.registerEvents(KitEventDispatcher( this, kitManager ), this )
|
pm.registerEvents(KitEventDispatcher( this, kitManager ), this )
|
||||||
pm.registerEvents( StatsListener(), this )
|
pm.registerEvents( StatsListener(), this )
|
||||||
|
pm.registerEvents( MenuListener(), this )
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package club.mcscrims.speedhg.command
|
|||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
import club.mcscrims.speedhg.game.GameState
|
import club.mcscrims.speedhg.game.GameState
|
||||||
|
import club.mcscrims.speedhg.gui.menu.KitSelectorMenu
|
||||||
import club.mcscrims.speedhg.kit.Playstyle
|
import club.mcscrims.speedhg.kit.Playstyle
|
||||||
import club.mcscrims.speedhg.kit.impl.BackupKit
|
import club.mcscrims.speedhg.kit.impl.BackupKit
|
||||||
import club.mcscrims.speedhg.util.sendMsg
|
import club.mcscrims.speedhg.util.sendMsg
|
||||||
@@ -30,9 +31,22 @@ class KitCommand : CommandExecutor, TabCompleter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val state = plugin.gameManager.currentState
|
||||||
|
val selectedKit = plugin.kitManager.getSelectedKit( player )
|
||||||
|
val isBackup = selectedKit is BackupKit
|
||||||
|
|
||||||
|
val ingame = state == GameState.INVINCIBILITY ||
|
||||||
|
state == GameState.INGAME
|
||||||
|
|
||||||
|
if ( ingame && !isBackup )
|
||||||
|
{
|
||||||
|
player.sendMsg( "commands.kit.gameHasStarted" )
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if ( args.isNullOrEmpty() || args.size < 2 )
|
if ( args.isNullOrEmpty() || args.size < 2 )
|
||||||
{
|
{
|
||||||
player.sendMsg( "commands.kit.usage" )
|
KitSelectorMenu( player ).open( player )
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,15 +67,7 @@ class KitCommand : CommandExecutor, TabCompleter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
val playerKit = plugin.kitManager.getSelectedKit( player )
|
if ( isBackup && ingame )
|
||||||
val isBackup = playerKit != null && playerKit is BackupKit
|
|
||||||
|
|
||||||
if ( !isBackup && isIngame() )
|
|
||||||
{
|
|
||||||
player.sendMsg("commands.kit.gameHasStarted")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else if ( isBackup && isIngame() )
|
|
||||||
{
|
{
|
||||||
if ( kit is BackupKit )
|
if ( kit is BackupKit )
|
||||||
{
|
{
|
||||||
@@ -69,6 +75,7 @@ class KitCommand : CommandExecutor, TabCompleter {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugin.kitManager.removeKit( player )
|
||||||
plugin.kitManager.selectKit( player, kit )
|
plugin.kitManager.selectKit( player, kit )
|
||||||
plugin.kitManager.selectPlaystyle( player, playstyle )
|
plugin.kitManager.selectPlaystyle( player, playstyle )
|
||||||
plugin.kitManager.applyKit( player )
|
plugin.kitManager.applyKit( player )
|
||||||
@@ -103,10 +110,4 @@ class KitCommand : CommandExecutor, TabCompleter {
|
|||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isIngame(): Boolean = when ( plugin.gameManager.currentState )
|
|
||||||
{
|
|
||||||
GameState.INGAME, GameState.INVINCIBILITY -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package club.mcscrims.speedhg.database
|
package club.mcscrims.speedhg.database
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hält die rohen Spieler-Statistiken im Arbeitsspeicher.
|
* Hält die rohen Spieler-Statistiken im Arbeitsspeicher.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package club.mcscrims.speedhg.database
|
package club.mcscrims.speedhg.database
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kapselt alle SQL-Operationen für die `player_stats`-Tabelle.
|
* Kapselt alle SQL-Operationen für die `player_stats`-Tabelle.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package club.mcscrims.speedhg.database
|
|||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.anvil
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.gui.menu.KitSelectorMenu
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
|
import org.bukkit.inventory.AnvilInventory
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
import org.bukkit.inventory.view.AnvilView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suchmenü auf Basis der **Paper Anvil-API** (kein NMS!).
|
||||||
|
*
|
||||||
|
* ## Wie Paper's Anvil-API funktioniert
|
||||||
|
*
|
||||||
|
* Paper stellt `player.openAnvil(location, force)` bereit. Diese Methode
|
||||||
|
* gibt eine `InventoryView` zurück. Da wir uns nicht für die physische Position
|
||||||
|
* interessieren (nur das Inventar-UI), übergeben wir `null` als Ort und
|
||||||
|
* `true` als Force-Flag (öffnet auch ohne physischen Amboss in der Nähe).
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* Slot 0 (INPUT_LEFT) → Das "Such-Icon" mit dem Placeholder-Text
|
||||||
|
* Slot 1 (INPUT_RIGHT) → Leer (wird vom Spieler nicht benutzt)
|
||||||
|
* Slot 2 (OUTPUT) → Das Ergebnis-Item → Klick bestätigt die Suche
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Der Spieler schreibt in das Umbenennen-Feld des Amboss. Wir lesen den
|
||||||
|
* eingegebenen Text über `AnvilView.renameText` aus. Paper aktualisiert
|
||||||
|
* diesen Wert in Echtzeit. Beim Klick auf Slot 2 (Output) schließen wir
|
||||||
|
* den Amboss und übergeben den Text an [KitSelectorMenu.applySearch].
|
||||||
|
*
|
||||||
|
* **Warum kein `PrepareAnvilEvent` + Fake-Item?**
|
||||||
|
* Für unsere reine Text-Eingabe brauchen wir kein Live-Feedback im Output-Slot.
|
||||||
|
* Wir setzen ein statisches Bestätigungs-Item und lesen den Rename-Text beim
|
||||||
|
* Klick aus. Das ist einfacher und stabiler.
|
||||||
|
*
|
||||||
|
* @param player Der suchende Spieler.
|
||||||
|
* @param returnMenu Das [KitSelectorMenu] zu dem wir nach der Suche zurückspringen.
|
||||||
|
* @param initialText Vorausgefüllter Text (wird als Rename-Text gesetzt).
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
class AnvilSearchMenu(
|
||||||
|
private val player: Player,
|
||||||
|
private val returnMenu: KitSelectorMenu,
|
||||||
|
private val initialText: String = ""
|
||||||
|
) {
|
||||||
|
private val plugin = SpeedHG.instance
|
||||||
|
private val mm = MiniMessage.miniMessage()
|
||||||
|
|
||||||
|
/** Öffnet das Anvil-Inventar. */
|
||||||
|
fun open(player: Player) {
|
||||||
|
// Paper API: öffnet einen Amboss ohne physischen Block
|
||||||
|
val view = player.openAnvil(null, true) ?: return
|
||||||
|
val anvilInv = view.topInventory as? AnvilInventory ?: return
|
||||||
|
|
||||||
|
// ── Slot 0: Such-Icon (Input) ──────────────────────────────────────
|
||||||
|
anvilInv.setItem(0, buildInputItem())
|
||||||
|
|
||||||
|
// ── Slot 2: Bestätigungs-Icon (Output) ────────────────────────────
|
||||||
|
anvilInv.setItem(2, buildConfirmItem())
|
||||||
|
|
||||||
|
// Kosten auf 0 setzen — Spieler soll kein XP zahlen
|
||||||
|
anvilInv.repairCost = 0
|
||||||
|
|
||||||
|
// Klick-Listener merken uns über die AnvilTracker-Map
|
||||||
|
AnvilSearchTracker.register(player, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet einen Klick im Anvil-Inventory.
|
||||||
|
* Wird vom [MenuListener] über [AnvilSearchTracker] weitergeleitet.
|
||||||
|
*
|
||||||
|
* @param event Das Click-Event (bereits gecancelt).
|
||||||
|
* @param view Die geöffnete [AnvilView] mit dem Eingabetext.
|
||||||
|
*/
|
||||||
|
fun onClick(event: InventoryClickEvent, view: AnvilView) {
|
||||||
|
event.isCancelled = true
|
||||||
|
|
||||||
|
// Nur Klick auf Output-Slot (Slot 2) bestätigt die Suche
|
||||||
|
if (event.rawSlot != 2) return
|
||||||
|
|
||||||
|
val query = view.renameText ?: ""
|
||||||
|
|
||||||
|
// Amboss schließen und zurück zum Kit-Menü
|
||||||
|
player.closeInventory()
|
||||||
|
AnvilSearchTracker.unregister(player)
|
||||||
|
|
||||||
|
// Suchbegriff anwenden → öffnet KitSelectorMenu neu
|
||||||
|
returnMenu.applySearch(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cleanup wenn Spieler Amboss schließt ohne zu bestätigen. */
|
||||||
|
fun onClose() {
|
||||||
|
AnvilSearchTracker.unregister(player)
|
||||||
|
// Zurück zum Kit-Menü ohne Suchbegriff zu ändern
|
||||||
|
// Einen Tick warten, damit das aktuelle Close-Event fertig ist
|
||||||
|
plugin.server.scheduler.runTask(plugin) { ->
|
||||||
|
returnMenu.open(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Item-Builder
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private fun buildInputItem(): ItemStack {
|
||||||
|
val item = ItemStack(Material.NAME_TAG)
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
// Der Name des Input-Items wird als Placeholder-Text im Umbenennungsfeld angezeigt
|
||||||
|
mm.deserialize(initialText.ifEmpty { "<gray>Suchbegriff eingeben...</gray>" })
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildConfirmItem(): ItemStack {
|
||||||
|
val item = ItemStack(Material.NAME_TAG)
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize("<green>✔ Suche bestätigen</green>")
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
)
|
||||||
|
val lore = listOf(
|
||||||
|
Component.empty(),
|
||||||
|
mm.deserialize("<gray>Klicken um zu suchen</gray>")
|
||||||
|
.decoration(TextDecoration.ITALIC, false),
|
||||||
|
Component.empty()
|
||||||
|
)
|
||||||
|
meta.lore(lore)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hält die Zuordnung `Player → AnvilSearchMenu` für alle aktuell offenen
|
||||||
|
* Anvil-Suchen. Wird benötigt, weil Anvil-Inventories keinen eigenen
|
||||||
|
* [MenuHolder] haben — sie werden direkt von Bukkit verwaltet.
|
||||||
|
*
|
||||||
|
* Die Map wird im [MenuListener] abgefragt.
|
||||||
|
*/
|
||||||
|
object AnvilSearchTracker {
|
||||||
|
|
||||||
|
private val openSearches = java.util.concurrent.ConcurrentHashMap<java.util.UUID, AnvilSearchMenu>()
|
||||||
|
|
||||||
|
fun register(player: Player, menu: AnvilSearchMenu) {
|
||||||
|
openSearches[player.uniqueId] = menu
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister(player: Player) {
|
||||||
|
openSearches.remove(player.uniqueId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMenu(player: Player): AnvilSearchMenu? = openSearches[player.uniqueId]
|
||||||
|
|
||||||
|
fun isSearchOpen(player: Player): Boolean = openSearches.containsKey(player.uniqueId)
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.factory
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.kit.Kit
|
||||||
|
import club.mcscrims.speedhg.kit.Playstyle
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
|
import org.bukkit.enchantments.Enchantment
|
||||||
|
import org.bukkit.inventory.ItemFlag
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt dynamisch die [ItemStack]s für das Kit-Auswahl-Menü.
|
||||||
|
*
|
||||||
|
* ## Warum eine eigene Factory?
|
||||||
|
*
|
||||||
|
* Die Item-Generierung (Icon, Name, Lore, Enchants) ist von [KitSelectorMenu]
|
||||||
|
* trennbar und testbar. Außerdem kann sie wiederverwendet werden (z.B. für
|
||||||
|
* Hotbar-Items oder Chat-Vorschauen).
|
||||||
|
*
|
||||||
|
* ## Lore-Aufbau
|
||||||
|
* ```
|
||||||
|
* ─────────────────────────
|
||||||
|
* <Zeile 1 aus Kit.lore>
|
||||||
|
* <Zeile 2 aus Kit.lore>
|
||||||
|
*
|
||||||
|
* Playstyle: ► Aggressive
|
||||||
|
*
|
||||||
|
* [L-Klick] Kit auswählen
|
||||||
|
* [R-Klick] Playstyle wechseln
|
||||||
|
* ─────────────────────────
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
object KitItemFactory {
|
||||||
|
|
||||||
|
private val mm = MiniMessage.miniMessage()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut das vollständige Kit-Item inklusive dynamischer Lore.
|
||||||
|
*
|
||||||
|
* @param kit Das Kit, das dargestellt werden soll.
|
||||||
|
* @param currentStyle Der aktuell gewählte [Playstyle] des Spielers.
|
||||||
|
* @param isSelected Ob dieses Kit gerade das aktiv gewählte ist.
|
||||||
|
*/
|
||||||
|
fun buildKitItem(
|
||||||
|
kit: Kit,
|
||||||
|
currentStyle: Playstyle,
|
||||||
|
isSelected: Boolean
|
||||||
|
): ItemStack {
|
||||||
|
val item = ItemStack(kit.icon)
|
||||||
|
|
||||||
|
item.editMeta { meta ->
|
||||||
|
// ── Name ──────────────────────────────────────────────────────────
|
||||||
|
meta.displayName(
|
||||||
|
kit.displayName
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Lore ──────────────────────────────────────────────────────────
|
||||||
|
val lore = mutableListOf<Component>()
|
||||||
|
|
||||||
|
// Trennlinie
|
||||||
|
lore += separator()
|
||||||
|
|
||||||
|
// Kit-eigene Lore-Zeilen
|
||||||
|
kit.lore.forEach { line ->
|
||||||
|
lore += mm.deserialize(line)
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
lore += Component.empty()
|
||||||
|
|
||||||
|
// Playstyle-Anzeige
|
||||||
|
lore += mm.deserialize(
|
||||||
|
"<gray>Playstyle: <white>► ${currentStyle.displayName}"
|
||||||
|
).decoration(TextDecoration.ITALIC, false)
|
||||||
|
|
||||||
|
lore += Component.empty()
|
||||||
|
|
||||||
|
// Interaktions-Hints
|
||||||
|
lore += mm.deserialize("<yellow>[L-Click]</yellow> <gray>Select Kit</gray>")
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
lore += mm.deserialize("<gold>[R-Click]</gold> <gray>Change playstyle</gray>")
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
|
||||||
|
lore += separator()
|
||||||
|
|
||||||
|
meta.lore(lore)
|
||||||
|
|
||||||
|
// ── Ausgewählt-Effekt ──────────────────────────────────────────────
|
||||||
|
if (isSelected) {
|
||||||
|
meta.addEnchant(Enchantment.UNBREAKING, 1, true)
|
||||||
|
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.addItemFlags(
|
||||||
|
ItemFlag.HIDE_ATTRIBUTES,
|
||||||
|
ItemFlag.HIDE_ADDITIONAL_TOOLTIP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dekorations-Items ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun buildFillerItem(): ItemStack {
|
||||||
|
val item = ItemStack(org.bukkit.Material.GRAY_STAINED_GLASS_PANE)
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(Component.text(" ").decoration(TextDecoration.ITALIC, false))
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildPrevPageItem(): ItemStack = buildNavItem(
|
||||||
|
org.bukkit.Material.ARROW,
|
||||||
|
"<red>◄ Previous"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun buildNextPageItem(): ItemStack = buildNavItem(
|
||||||
|
org.bukkit.Material.ARROW,
|
||||||
|
"<green>Next ►"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun buildSearchItem(currentQuery: String): ItemStack {
|
||||||
|
val item = ItemStack(org.bukkit.Material.NAME_TAG)
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize("<yellow>🔍 Search")
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
)
|
||||||
|
val lore = mutableListOf<Component>()
|
||||||
|
lore += separator()
|
||||||
|
if (currentQuery.isNotEmpty()) {
|
||||||
|
lore += mm.deserialize("<gray>Current: <white>\"$currentQuery\"")
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
lore += Component.empty()
|
||||||
|
}
|
||||||
|
lore += mm.deserialize("<gray>Click to search</gray>")
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
lore += separator()
|
||||||
|
meta.lore(lore)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildClearSearchItem(): ItemStack = buildNavItem(
|
||||||
|
org.bukkit.Material.BARRIER,
|
||||||
|
"<red>✕ Reset search"
|
||||||
|
)
|
||||||
|
|
||||||
|
fun buildCloseItem(): ItemStack = buildNavItem(
|
||||||
|
org.bukkit.Material.DARK_OAK_DOOR,
|
||||||
|
"<red>✕ Close"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── Interne Hilfsmittel ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun buildNavItem(material: org.bukkit.Material, title: String): ItemStack {
|
||||||
|
val item = ItemStack(material)
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize(title)
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
)
|
||||||
|
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ADDITIONAL_TOOLTIP)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun separator(): Component =
|
||||||
|
Component.text("────────────────────", NamedTextColor.DARK_GRAY)
|
||||||
|
.decoration(TextDecoration.ITALIC, false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.listener
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.gui.anvil.AnvilSearchMenu
|
||||||
|
import club.mcscrims.speedhg.gui.anvil.AnvilSearchTracker
|
||||||
|
import club.mcscrims.speedhg.gui.menu.MenuHolder
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.EventHandler
|
||||||
|
import org.bukkit.event.EventPriority
|
||||||
|
import org.bukkit.event.Listener
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent
|
||||||
|
import org.bukkit.event.inventory.InventoryType
|
||||||
|
import org.bukkit.inventory.view.AnvilView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentraler, einmalig registrierter Listener für alle Menü-Interaktionen.
|
||||||
|
*
|
||||||
|
* ## Dispatch-Strategie
|
||||||
|
*
|
||||||
|
* ### Chest-Menüs (KitSelectorMenu und andere Menu-Subklassen)
|
||||||
|
* ```
|
||||||
|
* InventoryClickEvent
|
||||||
|
* → event.inventory.holder as? MenuHolder (O(1), kein Map-Lookup)
|
||||||
|
* → holder.menu.onClick(event, player)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Anvil-Menüs (AnvilSearchMenu)
|
||||||
|
* Anvil-Inventories haben keinen Custom-Holder (Bukkit setzt intern `CraftAnvil`).
|
||||||
|
* Deshalb nutzen wir den [AnvilSearchTracker], der `Player → AnvilSearchMenu`
|
||||||
|
* speichert. Der Lookup ist UUID-basiert und ebenfalls O(1).
|
||||||
|
* ```
|
||||||
|
* InventoryClickEvent (Typ = ANVIL)
|
||||||
|
* → AnvilSearchTracker.getMenu(player)
|
||||||
|
* → menu.onClick(event, anvilView)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ### Close-Events
|
||||||
|
* Beim Schließen eines Anvil-Menüs ohne Bestätigung navigiert
|
||||||
|
* [AnvilSearchMenu.onClose] automatisch zurück zum Kit-Menü.
|
||||||
|
*/
|
||||||
|
class MenuListener : Listener {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Click-Handling
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false)
|
||||||
|
fun onInventoryClick(event: InventoryClickEvent) {
|
||||||
|
val player = event.whoClicked as? Player ?: return
|
||||||
|
|
||||||
|
// ── Anvil-Suche ────────────────────────────────────────────────────────
|
||||||
|
if (event.inventory.type == InventoryType.ANVIL) {
|
||||||
|
val searchMenu = AnvilSearchTracker.getMenu(player) ?: return
|
||||||
|
val anvilView = event.view as? AnvilView ?: return
|
||||||
|
searchMenu.onClick(event, anvilView)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chest-Menü (MenuHolder-Dispatch) ───────────────────────────────────
|
||||||
|
val holder = event.inventory.holder as? MenuHolder ?: return
|
||||||
|
val menu = holder.menu
|
||||||
|
|
||||||
|
// Alle Klicks in Custom-Menüs standardmäßig canceln
|
||||||
|
event.isCancelled = true
|
||||||
|
|
||||||
|
// Nur Klicks im oberen Inventory (das Menü selbst) verarbeiten,
|
||||||
|
// nicht Klicks im Spieler-Inventory (unterer Teil)
|
||||||
|
if (event.clickedInventory != event.inventory) return
|
||||||
|
|
||||||
|
menu.onClick(event, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Close-Handling
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
fun onInventoryClose(event: InventoryCloseEvent) {
|
||||||
|
val player = event.player as? Player ?: return
|
||||||
|
|
||||||
|
// ── Anvil geschlossen → zurück zum Kit-Menü ────────────────────────────
|
||||||
|
if (event.inventory.type == InventoryType.ANVIL && AnvilSearchTracker.isSearchOpen(player)) {
|
||||||
|
val searchMenu = AnvilSearchTracker.getMenu(player) ?: return
|
||||||
|
|
||||||
|
// onClose leitet selbst zurück zum Kit-Menü.
|
||||||
|
// Wir prüfen hier, ob das Close NICHT durch onClick ausgelöst wurde.
|
||||||
|
// AnvilSearchMenu.onClick ruft unregister() vor player.closeInventory() auf,
|
||||||
|
// sodass isSearchOpen() bei dem Close-Event danach false zurückgibt.
|
||||||
|
// Kommt der Close-Event aber zuerst (ESC-Druck), ist der Tracker noch gefüllt.
|
||||||
|
searchMenu.onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chest-Menü: onClose-Hook aufrufen ─────────────────────────────────
|
||||||
|
val holder = event.inventory.holder as? MenuHolder ?: return
|
||||||
|
holder.menu.onClose(event, player)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.menu
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.gui.anvil.AnvilSearchMenu
|
||||||
|
import club.mcscrims.speedhg.gui.factory.KitItemFactory
|
||||||
|
import club.mcscrims.speedhg.kit.Kit
|
||||||
|
import club.mcscrims.speedhg.kit.Playstyle
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.inventory.ClickType
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
|
import org.bukkit.inventory.Inventory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Haupt-Kit-Auswahl-Menü mit Pagination und Suchfunktion.
|
||||||
|
*
|
||||||
|
* ## Slot-Layout (6 Reihen × 9 Spalten = 54 Slots)
|
||||||
|
* ```
|
||||||
|
* [ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ] ← Kits (Seite 1)
|
||||||
|
* [ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ]
|
||||||
|
* [ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ]
|
||||||
|
* [ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ][ K ]
|
||||||
|
* [ F ][ F ][ F ][ F ][ F ][ F ][ F ][ F ][ F ] ← Filler
|
||||||
|
* [◄] [ F ][ F ][SRCH][F ][ F ][CLR][ F ][►] ← Navigation
|
||||||
|
* ```
|
||||||
|
* - Slots 0–44: 45 Kit-Plätze
|
||||||
|
* - Slots 45–53: Navigationsleiste
|
||||||
|
*
|
||||||
|
* @param player Der Spieler, für den das Menü geöffnet wird.
|
||||||
|
* @param searchQuery Optionaler Suchbegriff; filtert die angezeigten Kits.
|
||||||
|
*/
|
||||||
|
class KitSelectorMenu(
|
||||||
|
private val player: Player,
|
||||||
|
private var searchQuery: String = ""
|
||||||
|
) : Menu(rows = 6, title = "<gradient:red:gold><bold>Kit-Auswahl</bold></gradient>") {
|
||||||
|
|
||||||
|
private val plugin = SpeedHG.instance
|
||||||
|
private val mm = MiniMessage.miniMessage()
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
private val kitSlots = 45 // Slots 0–44 für Kits
|
||||||
|
private var currentPage = 0
|
||||||
|
|
||||||
|
// ── Slot-Konstanten ────────────────────────────────────────────────────────
|
||||||
|
private val slotPrevPage = 45
|
||||||
|
private val slotSearch = 48
|
||||||
|
private val slotClearSearch = 50
|
||||||
|
private val slotClose = 53
|
||||||
|
private val slotNextPage = 53 // wird durch slotClose ersetzt wenn letzte Seite
|
||||||
|
|
||||||
|
// ── Navigation-Slots ──────────────────────────────────────────────────────
|
||||||
|
private val navSlotPrev = 45
|
||||||
|
private val navSlotSearch = 48
|
||||||
|
private val navSlotClear = 50
|
||||||
|
private val navSlotNext = 53
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Aufbau
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
override fun build(): Inventory {
|
||||||
|
val inv = createInventory(mm.deserialize(title))
|
||||||
|
populate(inv)
|
||||||
|
return inv
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populate(inv: Inventory) {
|
||||||
|
inv.clear()
|
||||||
|
|
||||||
|
// ── Filler ────────────────────────────────────────────────────────────
|
||||||
|
val filler = KitItemFactory.buildFillerItem()
|
||||||
|
(45 until 54).forEach { inv.setItem(it, filler) }
|
||||||
|
|
||||||
|
// ── Kits ──────────────────────────────────────────────────────────────
|
||||||
|
val filteredKits = getFilteredKits()
|
||||||
|
val totalPages = ((filteredKits.size - 1) / kitSlots + 1).coerceAtLeast(1)
|
||||||
|
|
||||||
|
// Seiten-Clamp: wenn Suche Kits reduziert, nicht auf nicht-existenter Seite bleiben
|
||||||
|
currentPage = currentPage.coerceIn(0, totalPages - 1)
|
||||||
|
|
||||||
|
val pageStart = currentPage * kitSlots
|
||||||
|
val pageKits = filteredKits.subList(
|
||||||
|
pageStart.coerceAtMost(filteredKits.size),
|
||||||
|
(pageStart + kitSlots).coerceAtMost(filteredKits.size)
|
||||||
|
)
|
||||||
|
|
||||||
|
pageKits.forEachIndexed { slotIndex, kit ->
|
||||||
|
val currentStyle = plugin.kitManager.getSelectedPlaystyle(player)
|
||||||
|
val selectedKit = plugin.kitManager.getSelectedKit(player)
|
||||||
|
inv.setItem(slotIndex, KitItemFactory.buildKitItem(kit, currentStyle, selectedKit?.id == kit.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Navigation ────────────────────────────────────────────────────────
|
||||||
|
if (currentPage > 0)
|
||||||
|
inv.setItem(navSlotPrev, KitItemFactory.buildPrevPageItem())
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 1)
|
||||||
|
inv.setItem(navSlotNext, KitItemFactory.buildNextPageItem())
|
||||||
|
|
||||||
|
inv.setItem(navSlotSearch, KitItemFactory.buildSearchItem(searchQuery))
|
||||||
|
|
||||||
|
if (searchQuery.isNotEmpty())
|
||||||
|
inv.setItem(navSlotClear, KitItemFactory.buildClearSearchItem())
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Klick-Handling
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
override fun onClick(event: InventoryClickEvent, player: Player) {
|
||||||
|
val slot = event.rawSlot
|
||||||
|
if (slot !in 0..<size) return
|
||||||
|
|
||||||
|
when (slot) {
|
||||||
|
navSlotPrev -> handlePrevPage()
|
||||||
|
navSlotNext -> handleNextPage()
|
||||||
|
navSlotSearch -> handleSearchClick()
|
||||||
|
navSlotClear -> handleClearSearch()
|
||||||
|
in 0 until kitSlots -> handleKitClick(slot, event.click)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Kit-Interaktionen
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private fun handleKitClick(slot: Int, clickType: ClickType) {
|
||||||
|
val kit = getKitAtSlot(slot) ?: return
|
||||||
|
|
||||||
|
when {
|
||||||
|
// Linksklick → Kit auswählen
|
||||||
|
clickType.isLeftClick -> selectKit(kit)
|
||||||
|
// Rechtsklick → Playstyle toggeln (und Menü refreshen)
|
||||||
|
clickType.isRightClick -> togglePlaystyle(kit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectKit(kit: Kit) {
|
||||||
|
plugin.kitManager.selectKit(player, kit)
|
||||||
|
player.sendActionBar(
|
||||||
|
MiniMessage.miniMessage().deserialize(
|
||||||
|
"<green>Kit <white>${kit.displayName}</white> gewählt!</green>"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Menü aktualisieren um Auswahl-Glanz zu zeigen
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun togglePlaystyle(kit: Kit) {
|
||||||
|
val current = plugin.kitManager.getSelectedPlaystyle(player)
|
||||||
|
val next = when (current) {
|
||||||
|
Playstyle.AGGRESSIVE -> Playstyle.DEFENSIVE
|
||||||
|
Playstyle.DEFENSIVE -> Playstyle.AGGRESSIVE
|
||||||
|
}
|
||||||
|
plugin.kitManager.selectPlaystyle(player, next)
|
||||||
|
|
||||||
|
// Wenn dieses Kit gerade ausgewählt ist, Playstyle-Änderung übernehmen
|
||||||
|
if (plugin.kitManager.getSelectedKit(player)?.id == kit.id) {
|
||||||
|
player.sendActionBar(
|
||||||
|
MiniMessage.miniMessage().deserialize(
|
||||||
|
"<gold>Playstyle: <white>► ${next.displayName}</white>"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Navigation
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private fun handlePrevPage() {
|
||||||
|
if (currentPage > 0) {
|
||||||
|
currentPage--
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNextPage() {
|
||||||
|
val totalPages = ((getFilteredKits().size - 1) / kitSlots + 1).coerceAtLeast(1)
|
||||||
|
if (currentPage < totalPages - 1) {
|
||||||
|
currentPage++
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSearchClick() {
|
||||||
|
// Öffne das Anvil-Suchmenü; schließt dieses Menü temporär
|
||||||
|
AnvilSearchMenu(
|
||||||
|
player = player,
|
||||||
|
returnMenu = this,
|
||||||
|
initialText = searchQuery
|
||||||
|
).open(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleClearSearch() {
|
||||||
|
searchQuery = ""
|
||||||
|
currentPage = 0
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Hilfsmittel
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Aktualisiert das geöffnete Inventory in-place ohne es neu zu öffnen. */
|
||||||
|
fun refresh() {
|
||||||
|
populate(inventory)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert den Suchbegriff und baut das Menü neu auf.
|
||||||
|
* Wird vom [AnvilSearchMenu] aufgerufen nachdem der Spieler einen Begriff eingegeben hat.
|
||||||
|
*/
|
||||||
|
fun applySearch(query: String) {
|
||||||
|
searchQuery = query.trim()
|
||||||
|
currentPage = 0
|
||||||
|
open(player) // neu öffnen (Anvil hat das vorherige Inventory geschlossen)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFilteredKits(): List<Kit> {
|
||||||
|
val allKits = plugin.kitManager.getRegisteredKits().toList()
|
||||||
|
if (searchQuery.isEmpty()) return allKits
|
||||||
|
|
||||||
|
val q = searchQuery.lowercase()
|
||||||
|
return allKits.filter { kit ->
|
||||||
|
// Suche im Plain-Text des Adventure-Components
|
||||||
|
kit.id.lowercase().contains(q) ||
|
||||||
|
net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
|
||||||
|
.plainText()
|
||||||
|
.serialize(kit.displayName)
|
||||||
|
.lowercase()
|
||||||
|
.contains(q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKitAtSlot(slot: Int): Kit? {
|
||||||
|
val filteredKits = getFilteredKits()
|
||||||
|
val absoluteIndex = currentPage * kitSlots + slot
|
||||||
|
return filteredKits.getOrNull(absoluteIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/main/kotlin/club/mcscrims/speedhg/gui/menu/Menu.kt
Normal file
65
src/main/kotlin/club/mcscrims/speedhg/gui/menu/Menu.kt
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.menu
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent
|
||||||
|
import org.bukkit.inventory.Inventory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstrakte Basisklasse für alle Chest-basierenden Menüs.
|
||||||
|
*
|
||||||
|
* ## Verantwortlichkeiten
|
||||||
|
* - Erstellt und befüllt das Inventory via [build].
|
||||||
|
* - Stellt [onClick] und [onClose] als override-bare Hooks bereit.
|
||||||
|
* - Öffnet sich selbst via [open].
|
||||||
|
*
|
||||||
|
* ## Designprinzip
|
||||||
|
* Jede [Menu]-Unterklasse ist zustandsbehaftet (enthält ihren eigenen
|
||||||
|
* Seiten-Index, Suchbegriff etc.) und ist genau einem Spieler zugeordnet.
|
||||||
|
* Zwei verschiedene Spieler, die das Kit-Menü öffnen, erhalten je eine
|
||||||
|
* eigene [KitSelectorMenu]-Instanz.
|
||||||
|
*
|
||||||
|
* @param rows Anzahl der Reihen (1–6).
|
||||||
|
* @param title Inventar-Titel als MiniMessage-String.
|
||||||
|
*/
|
||||||
|
abstract class Menu(
|
||||||
|
protected val rows: Int,
|
||||||
|
protected val title: String
|
||||||
|
) {
|
||||||
|
protected val size: Int = rows * 9
|
||||||
|
internal lateinit var inventory: Inventory
|
||||||
|
private set
|
||||||
|
|
||||||
|
/** Erstellt das Inventory und befüllt es initial. Wird in [open] aufgerufen. */
|
||||||
|
protected abstract fun build(): Inventory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet einen Klick auf einen Slot.
|
||||||
|
* Wird vom [MenuListener] aufgerufen.
|
||||||
|
* Das Event ist zu diesem Zeitpunkt bereits gecancelt.
|
||||||
|
*/
|
||||||
|
abstract fun onClick(event: InventoryClickEvent, player: Player)
|
||||||
|
|
||||||
|
/** Optionaler Hook wenn das Inventar geschlossen wird. */
|
||||||
|
open fun onClose(event: InventoryCloseEvent, player: Player) {}
|
||||||
|
|
||||||
|
/** Erstellt, befüllt und öffnet das Menü für den Spieler. */
|
||||||
|
fun open(player: Player) {
|
||||||
|
inventory = build()
|
||||||
|
player.openInventory(inventory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmittel ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein Inventory mit dem korrekten [MenuHolder].
|
||||||
|
* Immer statt `Bukkit.createInventory(null, ...)` verwenden.
|
||||||
|
*/
|
||||||
|
protected fun createInventory(parsedTitle: net.kyori.adventure.text.Component): Inventory {
|
||||||
|
val holder = MenuHolder(this)
|
||||||
|
val inv = Bukkit.createInventory(holder, size, parsedTitle)
|
||||||
|
holder.bind(inv)
|
||||||
|
return inv
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/kotlin/club/mcscrims/speedhg/gui/menu/MenuHolder.kt
Normal file
35
src/main/kotlin/club/mcscrims/speedhg/gui/menu/MenuHolder.kt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.menu
|
||||||
|
|
||||||
|
import org.bukkit.inventory.Inventory
|
||||||
|
import org.bukkit.inventory.InventoryHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbindet ein Bukkit [Inventory] mit seiner zugehörigen [Menu]-Instanz.
|
||||||
|
*
|
||||||
|
* ## Warum kein globales HashMap?
|
||||||
|
*
|
||||||
|
* Das naive Muster ist: `openMenus: Map<UUID, Menu>`, das beim Öffnen
|
||||||
|
* befüllt und beim Schließen geleert wird — mit dem Risiko, bei Crashes
|
||||||
|
* oder unerwarteten Schließ-Events Einträge zu leaken.
|
||||||
|
*
|
||||||
|
* Der sauberere Ansatz: Bukkit erlaubt jedem Inventory einen [InventoryHolder].
|
||||||
|
* `MenuHolder` ist dieser Holder. Wann immer der [MenuListener] ein
|
||||||
|
* `InventoryClickEvent` empfängt, prüft er einfach:
|
||||||
|
* ```kotlin
|
||||||
|
* val menu = (event.inventory.holder as? MenuHolder)?.menu ?: return
|
||||||
|
* ```
|
||||||
|
* Kein Map-Lookup, kein UUID-Tracking — der Holder lebt und stirbt
|
||||||
|
* zusammen mit dem Inventory-Objekt.
|
||||||
|
*/
|
||||||
|
class MenuHolder(val menu: Menu) : InventoryHolder {
|
||||||
|
|
||||||
|
/** Wird von Bukkit gesetzt nachdem das Inventory erstellt wurde. */
|
||||||
|
private lateinit var inventory: Inventory
|
||||||
|
|
||||||
|
override fun getInventory(): Inventory = inventory
|
||||||
|
|
||||||
|
/** Wird vom Menu direkt nach der Erstellung aufgerufen. */
|
||||||
|
internal fun bind(inv: Inventory) {
|
||||||
|
this.inventory = inv
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import net.kyori.adventure.text.Component
|
|||||||
import org.bukkit.Material
|
import org.bukkit.Material
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import net.kyori.adventure.text.Component
|
|||||||
import org.bukkit.Material
|
import org.bukkit.Material
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class BackupKit : Kit() {
|
class BackupKit : Kit() {
|
||||||
|
|||||||
@@ -15,19 +15,14 @@ import com.sk89q.worldedit.math.Vector2
|
|||||||
import com.sk89q.worldedit.regions.CylinderRegion
|
import com.sk89q.worldedit.regions.CylinderRegion
|
||||||
import com.sk89q.worldedit.regions.Region
|
import com.sk89q.worldedit.regions.Region
|
||||||
import net.kyori.adventure.text.Component
|
import net.kyori.adventure.text.Component
|
||||||
import org.bukkit.Bukkit
|
import org.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.entity.Player
|
||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
import org.bukkit.metadata.FixedMetadataValue
|
import org.bukkit.metadata.FixedMetadataValue
|
||||||
import org.bukkit.potion.PotionEffect
|
import org.bukkit.potion.PotionEffect
|
||||||
import org.bukkit.potion.PotionEffectType
|
import org.bukkit.potion.PotionEffectType
|
||||||
import org.bukkit.scheduler.BukkitRunnable
|
import org.bukkit.scheduler.BukkitRunnable
|
||||||
import java.util.Random
|
import java.util.*
|
||||||
import java.util.UUID
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class GladiatorKit : Kit() {
|
class GladiatorKit : Kit() {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent
|
|||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
import org.bukkit.potion.PotionEffect
|
import org.bukkit.potion.PotionEffect
|
||||||
import org.bukkit.potion.PotionEffectType
|
import org.bukkit.potion.PotionEffectType
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,12 +14,7 @@ import org.bukkit.Material
|
|||||||
import org.bukkit.NamespacedKey
|
import org.bukkit.NamespacedKey
|
||||||
import org.bukkit.Sound
|
import org.bukkit.Sound
|
||||||
import org.bukkit.block.Block
|
import org.bukkit.block.Block
|
||||||
import org.bukkit.entity.Arrow
|
import org.bukkit.entity.*
|
||||||
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.Cancellable
|
||||||
import org.bukkit.event.EventHandler
|
import org.bukkit.event.EventHandler
|
||||||
import org.bukkit.event.EventPriority
|
import org.bukkit.event.EventPriority
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ class ConnectListener : Listener {
|
|||||||
val player = event.player
|
val player = event.player
|
||||||
event.joinMessage( null )
|
event.joinMessage( null )
|
||||||
|
|
||||||
if ( plugin.gameManager.currentState == GameState.INGAME ||
|
if (( plugin.gameManager.currentState == GameState.INGAME ||
|
||||||
plugin.gameManager.currentState == GameState.INVINCIBILITY )
|
plugin.gameManager.currentState == GameState.INVINCIBILITY ) &&
|
||||||
|
!player.hasPermission( "speedhg.bypass" ))
|
||||||
|
{
|
||||||
|
player.kick(Component.text("§cThe game has already started!"))
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Bukkit.getOnlinePlayers().forEach { p ->
|
Bukkit.getOnlinePlayers().forEach { p ->
|
||||||
p.sendMsg( "game.join", "name" to player.name )
|
p.sendMsg( "game.join", "name" to player.name )
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent
|
|||||||
import org.bukkit.event.entity.FoodLevelChangeEvent
|
import org.bukkit.event.entity.FoodLevelChangeEvent
|
||||||
import org.bukkit.event.entity.ItemDespawnEvent
|
import org.bukkit.event.entity.ItemDespawnEvent
|
||||||
import org.bukkit.event.inventory.*
|
import org.bukkit.event.inventory.*
|
||||||
import org.bukkit.event.player.PlayerAttemptPickupItemEvent
|
import org.bukkit.event.player.*
|
||||||
import org.bukkit.event.player.PlayerDropItemEvent
|
|
||||||
import org.bukkit.event.player.PlayerItemDamageEvent
|
|
||||||
import org.bukkit.event.player.PlayerJoinEvent
|
|
||||||
import org.bukkit.event.player.PlayerQuitEvent
|
|
||||||
import org.bukkit.inventory.EnchantingInventory
|
import org.bukkit.inventory.EnchantingInventory
|
||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
import org.bukkit.inventory.meta.Damageable
|
import org.bukkit.inventory.meta.Damageable
|
||||||
|
|||||||
@@ -86,22 +86,12 @@ kits:
|
|||||||
- ' '
|
- ' '
|
||||||
- 'Select a kit mid-round at any time.'
|
- 'Select a kit mid-round at any time.'
|
||||||
- 'All kits are available to pick.'
|
- 'All kits are available to pick.'
|
||||||
- ' '
|
|
||||||
- 'PlayStyle: §e%playstyle%'
|
|
||||||
- ' '
|
|
||||||
- 'Left-click to select'
|
|
||||||
- 'Right-click to change playstyle'
|
|
||||||
gladiator:
|
gladiator:
|
||||||
name: '<gradient:dark_gray:gray><bold>Gladiator</bold></gradient>'
|
name: '<gradient:dark_gray:gray><bold>Gladiator</bold></gradient>'
|
||||||
lore:
|
lore:
|
||||||
- ' '
|
- ' '
|
||||||
- 'Use your ability to fight enemies'
|
- 'Use your ability to fight enemies'
|
||||||
- 'in a 1v1 above the skies.'
|
- 'in a 1v1 above the skies.'
|
||||||
- ' '
|
|
||||||
- 'PlayStyle: §e%playstyle%'
|
|
||||||
- ' '
|
|
||||||
- 'Left-click to select'
|
|
||||||
- 'Right-click to change playstyle'
|
|
||||||
items:
|
items:
|
||||||
ironBars:
|
ironBars:
|
||||||
name: '<gray>Cage</gray>'
|
name: '<gray>Cage</gray>'
|
||||||
@@ -117,11 +107,6 @@ kits:
|
|||||||
- ' '
|
- ' '
|
||||||
- 'DEFENSIVE:'
|
- 'DEFENSIVE:'
|
||||||
- 'Summon a bunker for protection'
|
- 'Summon a bunker for protection'
|
||||||
- ' '
|
|
||||||
- 'PlayStyle: §e%playstyle%'
|
|
||||||
- ' '
|
|
||||||
- 'Left-click to select'
|
|
||||||
- 'Right-click to change playstyle'
|
|
||||||
items:
|
items:
|
||||||
steal:
|
steal:
|
||||||
name: '§cSteal Kit'
|
name: '§cSteal Kit'
|
||||||
@@ -142,11 +127,6 @@ kits:
|
|||||||
- ' '
|
- ' '
|
||||||
- 'DEFENSIVE:'
|
- 'DEFENSIVE:'
|
||||||
- 'Summon snowballs and freeze enemies'
|
- 'Summon snowballs and freeze enemies'
|
||||||
- ' '
|
|
||||||
- 'PlayStyle: §e%playstyle%'
|
|
||||||
- ' '
|
|
||||||
- 'Left-click to select'
|
|
||||||
- 'Right-click to change playstyle'
|
|
||||||
items:
|
items:
|
||||||
snowball:
|
snowball:
|
||||||
name: '§bFreeze'
|
name: '§bFreeze'
|
||||||
@@ -166,11 +146,6 @@ kits:
|
|||||||
- ' '
|
- ' '
|
||||||
- 'DEFENSIVE:'
|
- 'DEFENSIVE:'
|
||||||
- 'Create a shield for protection'
|
- 'Create a shield for protection'
|
||||||
- ' '
|
|
||||||
- 'PlayStyle: §e%playstyle%'
|
|
||||||
- ' '
|
|
||||||
- 'Left-click to select'
|
|
||||||
- 'Right-click to change playstyle'
|
|
||||||
items:
|
items:
|
||||||
wither:
|
wither:
|
||||||
name: '§8Deafening Beam'
|
name: '§8Deafening Beam'
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ api-version: '1.21'
|
|||||||
depend:
|
depend:
|
||||||
- "WorldEdit"
|
- "WorldEdit"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
speedhg.bypass:
|
||||||
|
description: 'Allows joining the server while its running'
|
||||||
|
default: false
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
kit:
|
kit:
|
||||||
description: 'Select kits via command'
|
description: 'Select kits via command'
|
||||||
|
|||||||
Reference in New Issue
Block a user