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.game.GameManager
|
||||
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.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
|
||||
import club.mcscrims.speedhg.kit.impl.*
|
||||
import club.mcscrims.speedhg.kit.listener.KitEventDispatcher
|
||||
import club.mcscrims.speedhg.listener.ConnectListener
|
||||
import club.mcscrims.speedhg.listener.GameStateListener
|
||||
@@ -118,6 +115,7 @@ class SpeedHG : JavaPlugin() {
|
||||
pm.registerEvents( SoupListener(), this )
|
||||
pm.registerEvents(KitEventDispatcher( this, kitManager ), 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.game.GameState
|
||||
import club.mcscrims.speedhg.gui.menu.KitSelectorMenu
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
import club.mcscrims.speedhg.kit.impl.BackupKit
|
||||
import club.mcscrims.speedhg.util.sendMsg
|
||||
@@ -30,9 +31,22 @@ class KitCommand : CommandExecutor, TabCompleter {
|
||||
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 )
|
||||
{
|
||||
player.sendMsg( "commands.kit.usage" )
|
||||
KitSelectorMenu( player ).open( player )
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -53,15 +67,7 @@ class KitCommand : CommandExecutor, TabCompleter {
|
||||
return true
|
||||
}
|
||||
|
||||
val playerKit = plugin.kitManager.getSelectedKit( player )
|
||||
val isBackup = playerKit != null && playerKit is BackupKit
|
||||
|
||||
if ( !isBackup && isIngame() )
|
||||
{
|
||||
player.sendMsg("commands.kit.gameHasStarted")
|
||||
return true
|
||||
}
|
||||
else if ( isBackup && isIngame() )
|
||||
if ( isBackup && ingame )
|
||||
{
|
||||
if ( kit is BackupKit )
|
||||
{
|
||||
@@ -69,6 +75,7 @@ class KitCommand : CommandExecutor, TabCompleter {
|
||||
return true
|
||||
}
|
||||
|
||||
plugin.kitManager.removeKit( player )
|
||||
plugin.kitManager.selectKit( player, kit )
|
||||
plugin.kitManager.selectPlaystyle( player, playstyle )
|
||||
plugin.kitManager.applyKit( player )
|
||||
@@ -103,10 +110,4 @@ class KitCommand : CommandExecutor, TabCompleter {
|
||||
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
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Hält die rohen Spieler-Statistiken im Arbeitsspeicher.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package club.mcscrims.speedhg.database
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 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 kotlinx.coroutines.*
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
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.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class BackupKit : Kit() {
|
||||
|
||||
@@ -15,19 +15,14 @@ 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.*
|
||||
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.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class GladiatorKit : Kit() {
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.potion.PotionEffect
|
||||
import org.bukkit.potion.PotionEffectType
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,12 +14,7 @@ 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.entity.*
|
||||
import org.bukkit.event.Cancellable
|
||||
import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.EventPriority
|
||||
|
||||
@@ -23,9 +23,13 @@ class ConnectListener : Listener {
|
||||
val player = event.player
|
||||
event.joinMessage( null )
|
||||
|
||||
if ( plugin.gameManager.currentState == GameState.INGAME ||
|
||||
plugin.gameManager.currentState == GameState.INVINCIBILITY )
|
||||
if (( plugin.gameManager.currentState == GameState.INGAME ||
|
||||
plugin.gameManager.currentState == GameState.INVINCIBILITY ) &&
|
||||
!player.hasPermission( "speedhg.bypass" ))
|
||||
{
|
||||
player.kick(Component.text("§cThe game has already started!"))
|
||||
return
|
||||
}
|
||||
|
||||
Bukkit.getOnlinePlayers().forEach { p ->
|
||||
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.ItemDespawnEvent
|
||||
import org.bukkit.event.inventory.*
|
||||
import org.bukkit.event.player.PlayerAttemptPickupItemEvent
|
||||
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.event.player.*
|
||||
import org.bukkit.inventory.EnchantingInventory
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.inventory.meta.Damageable
|
||||
|
||||
@@ -86,22 +86,12 @@ kits:
|
||||
- ' '
|
||||
- 'Select a kit mid-round at any time.'
|
||||
- 'All kits are available to pick.'
|
||||
- ' '
|
||||
- 'PlayStyle: §e%playstyle%'
|
||||
- ' '
|
||||
- 'Left-click to select'
|
||||
- 'Right-click to change playstyle'
|
||||
gladiator:
|
||||
name: '<gradient:dark_gray:gray><bold>Gladiator</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- 'Use your ability to fight enemies'
|
||||
- 'in a 1v1 above the skies.'
|
||||
- ' '
|
||||
- 'PlayStyle: §e%playstyle%'
|
||||
- ' '
|
||||
- 'Left-click to select'
|
||||
- 'Right-click to change playstyle'
|
||||
items:
|
||||
ironBars:
|
||||
name: '<gray>Cage</gray>'
|
||||
@@ -117,11 +107,6 @@ kits:
|
||||
- ' '
|
||||
- 'DEFENSIVE:'
|
||||
- 'Summon a bunker for protection'
|
||||
- ' '
|
||||
- 'PlayStyle: §e%playstyle%'
|
||||
- ' '
|
||||
- 'Left-click to select'
|
||||
- 'Right-click to change playstyle'
|
||||
items:
|
||||
steal:
|
||||
name: '§cSteal Kit'
|
||||
@@ -142,11 +127,6 @@ kits:
|
||||
- ' '
|
||||
- 'DEFENSIVE:'
|
||||
- 'Summon snowballs and freeze enemies'
|
||||
- ' '
|
||||
- 'PlayStyle: §e%playstyle%'
|
||||
- ' '
|
||||
- 'Left-click to select'
|
||||
- 'Right-click to change playstyle'
|
||||
items:
|
||||
snowball:
|
||||
name: '§bFreeze'
|
||||
@@ -166,11 +146,6 @@ kits:
|
||||
- ' '
|
||||
- 'DEFENSIVE:'
|
||||
- 'Create a shield for protection'
|
||||
- ' '
|
||||
- 'PlayStyle: §e%playstyle%'
|
||||
- ' '
|
||||
- 'Left-click to select'
|
||||
- 'Right-click to change playstyle'
|
||||
items:
|
||||
wither:
|
||||
name: '§8Deafening Beam'
|
||||
|
||||
@@ -6,6 +6,11 @@ api-version: '1.21'
|
||||
depend:
|
||||
- "WorldEdit"
|
||||
|
||||
permissions:
|
||||
speedhg.bypass:
|
||||
description: 'Allows joining the server while its running'
|
||||
default: false
|
||||
|
||||
commands:
|
||||
kit:
|
||||
description: 'Select kits via command'
|
||||
|
||||
Reference in New Issue
Block a user