Add stats & leaderboard menus and lobby items
Add StatsMenu and LeaderboardMenu GUI implementations and wire them into the lobby hotbar. LobbyItemManager now builds a tutorial written-book, a player-head Profile & Stats item, and a leaderboard gold-ingot item, tags them with PDC keys, and opens the appropriate menu or book on interact. Leaderboard loads top-10 asynchronously from the stats manager. Also add corresponding language keys for item names/lores and menu titles.
This commit is contained in:
@@ -5,8 +5,12 @@ 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_KITS
|
||||||
import club.mcscrims.speedhg.game.modules.LobbyItemManager.Companion.TAG_PERKS
|
import club.mcscrims.speedhg.game.modules.LobbyItemManager.Companion.TAG_PERKS
|
||||||
import club.mcscrims.speedhg.gui.menu.KitSelectorMenu
|
import club.mcscrims.speedhg.gui.menu.KitSelectorMenu
|
||||||
|
import club.mcscrims.speedhg.gui.menu.LeaderboardMenu
|
||||||
import club.mcscrims.speedhg.gui.menu.PerkSelectorMenu
|
import club.mcscrims.speedhg.gui.menu.PerkSelectorMenu
|
||||||
|
import club.mcscrims.speedhg.gui.menu.StatsMenu
|
||||||
import club.mcscrims.speedhg.util.ItemBuilder
|
import club.mcscrims.speedhg.util.ItemBuilder
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
import org.bukkit.Material
|
import org.bukkit.Material
|
||||||
import org.bukkit.NamespacedKey
|
import org.bukkit.NamespacedKey
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
@@ -20,14 +24,19 @@ import org.bukkit.event.player.PlayerInteractEvent
|
|||||||
import org.bukkit.event.player.PlayerJoinEvent
|
import org.bukkit.event.player.PlayerJoinEvent
|
||||||
import org.bukkit.inventory.EquipmentSlot
|
import org.bukkit.inventory.EquipmentSlot
|
||||||
import org.bukkit.inventory.ItemStack
|
import org.bukkit.inventory.ItemStack
|
||||||
|
import org.bukkit.inventory.meta.BookMeta
|
||||||
|
import org.bukkit.inventory.meta.SkullMeta
|
||||||
import org.bukkit.persistence.PersistentDataType
|
import org.bukkit.persistence.PersistentDataType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages interactive lobby hotbar items for the LOBBY and STARTING phases.
|
* Manages interactive lobby hotbar items for the LOBBY and STARTING phases.
|
||||||
*
|
*
|
||||||
* ## Items
|
* ## Items
|
||||||
* - Slot 0: Kit selector (CHEST)
|
* - Slot 0: Kit selector (CHEST)
|
||||||
* - Slot 1: Perk selector (ENDER_CHEST)
|
* - Slot 1: Perk selector (ENDER_CHEST)
|
||||||
|
* - Slot 4: Tutorial Book (WRITTEN_BOOK)
|
||||||
|
* - Slot 7: Profile & Stats (PLAYER_HEAD)
|
||||||
|
* - Slot 8: Leaderboard (GOLD_INGOT)
|
||||||
*
|
*
|
||||||
* ## PDC tagging
|
* ## PDC tagging
|
||||||
* Each item is tagged with [KEY_LOBBY_ITEM] → a String value identifying which
|
* Each item is tagged with [KEY_LOBBY_ITEM] → a String value identifying which
|
||||||
@@ -42,6 +51,8 @@ class LobbyItemManager(
|
|||||||
private val plugin: SpeedHG
|
private val plugin: SpeedHG
|
||||||
) : Listener {
|
) : Listener {
|
||||||
|
|
||||||
|
private val mm = MiniMessage.miniMessage()
|
||||||
|
|
||||||
// ── PDC keys ─────────────────────────────────────────────────────────────
|
// ── PDC keys ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Key stored on every lobby item; value identifies the target menu. */
|
/** Key stored on every lobby item; value identifies the target menu. */
|
||||||
@@ -50,6 +61,9 @@ class LobbyItemManager(
|
|||||||
companion object {
|
companion object {
|
||||||
const val TAG_KITS = "kits"
|
const val TAG_KITS = "kits"
|
||||||
const val TAG_PERKS = "perks"
|
const val TAG_PERKS = "perks"
|
||||||
|
const val TAG_TUTORIAL = "tutorial"
|
||||||
|
const val TAG_STATS = "stats"
|
||||||
|
const val TAG_LEADERBOARD = "leaderboard"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Item definitions ──────────────────────────────────────────────────────
|
// ── Item definitions ──────────────────────────────────────────────────────
|
||||||
@@ -82,6 +96,55 @@ class LobbyItemManager(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildTutorialItem(
|
||||||
|
player: Player
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
return ItemBuilder( Material.WRITTEN_BOOK )
|
||||||
|
.name(plugin.languageManager.getComponent( player, "game.lobby-items.tutorial.name", mapOf() ))
|
||||||
|
.lore(listOf(plugin.languageManager.getRawMessage( player, "game.lobby-items.tutorial.lore" )))
|
||||||
|
.pdc( key, PersistentDataType.STRING, TAG_TUTORIAL )
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the Profile & Stats lobby item.
|
||||||
|
* Uses [SkullMeta] to display the player's own skin, requiring manual
|
||||||
|
* PDC tagging outside of [ItemBuilder] which does not support skull meta.
|
||||||
|
*/
|
||||||
|
fun buildStatsItem(
|
||||||
|
player: Player
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
val item = ItemStack( Material.PLAYER_HEAD )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
if ( meta is SkullMeta )
|
||||||
|
meta.owningPlayer = player
|
||||||
|
|
||||||
|
meta.displayName(
|
||||||
|
plugin.languageManager.getComponent( player, "game.lobby-items.stats.name", mapOf() )
|
||||||
|
)
|
||||||
|
|
||||||
|
meta.lore(listOf(
|
||||||
|
mm.deserialize(plugin.languageManager.getRawMessage( player, "game.lobby-items.stats.lore" ))
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
))
|
||||||
|
meta.persistentDataContainer.set( key, PersistentDataType.STRING, TAG_STATS )
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildLeaderboardItem(
|
||||||
|
player: Player
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
return ItemBuilder( Material.GOLD_INGOT )
|
||||||
|
.name(plugin.languageManager.getComponent( player, "game.lobby-items.leaderboard.name", mapOf() ))
|
||||||
|
.lore(listOf(plugin.languageManager.getRawMessage( player, "game.lobby-items.leaderboard.lore" )))
|
||||||
|
.pdc( key, PersistentDataType.STRING, TAG_LEADERBOARD )
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────────────────
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,6 +159,9 @@ class LobbyItemManager(
|
|||||||
player.inventory.clear()
|
player.inventory.clear()
|
||||||
player.inventory.setItem( 0, buildKitItem( player ))
|
player.inventory.setItem( 0, buildKitItem( player ))
|
||||||
player.inventory.setItem( 1, buildPerkItem( player ))
|
player.inventory.setItem( 1, buildPerkItem( player ))
|
||||||
|
player.inventory.setItem( 4, buildTutorialItem( player ))
|
||||||
|
player.inventory.setItem( 7, buildStatsItem( player ))
|
||||||
|
player.inventory.setItem( 8, buildLeaderboardItem( player ))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,6 +210,41 @@ class LobbyItemManager(
|
|||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a written book UI for the player explaining core SpeedHG mechanincs.
|
||||||
|
* The book is never placed in the player's inventory - it is opened directly.
|
||||||
|
*/
|
||||||
|
private fun openTutorialBook(
|
||||||
|
player: Player
|
||||||
|
) {
|
||||||
|
val book = ItemStack( Material.WRITTEN_BOOK )
|
||||||
|
|
||||||
|
book.editMeta { meta ->
|
||||||
|
if ( meta !is BookMeta ) return@editMeta
|
||||||
|
|
||||||
|
meta.title(mm.deserialize( "<yellow><bold>SpeedHG - How to play</bold></yellow>" ))
|
||||||
|
meta.author(mm.deserialize( "<gold>McScrims Network</gold>" ))
|
||||||
|
|
||||||
|
meta.addPages(
|
||||||
|
mm.deserialize(
|
||||||
|
"<gold><bold>Welcome!</bold></gold>\n\n" +
|
||||||
|
"<black>SpeedHG is a fast-paced Hunger Games mode.\n\n" +
|
||||||
|
"Gather resources, pick a Kit, and be the last player standing. " +
|
||||||
|
"The border shrinks over time — stay alert!</black>"
|
||||||
|
),
|
||||||
|
mm.deserialize(
|
||||||
|
"<gold><bold>Kits & Perks</bold></gold>\n\n" +
|
||||||
|
"<black>Open the Kit selector (hotbar slot 1) to choose your Kit " +
|
||||||
|
"and Playstyle.\n\n" +
|
||||||
|
"Open the Perk selector (slot 2) to pick up to 2 passive Perks. " +
|
||||||
|
"Combine them wisely!\n\nGood luck!</black>"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.openBook( book )
|
||||||
|
}
|
||||||
|
|
||||||
// ── Event handlers ────────────────────────────────────────────────────────
|
// ── Event handlers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,8 +288,11 @@ class LobbyItemManager(
|
|||||||
|
|
||||||
when( tag )
|
when( tag )
|
||||||
{
|
{
|
||||||
TAG_KITS -> KitSelectorMenu( event.player ).open( event.player )
|
TAG_KITS -> KitSelectorMenu( event.player ).open( event.player )
|
||||||
TAG_PERKS -> PerkSelectorMenu( event.player ).open( event.player )
|
TAG_PERKS -> PerkSelectorMenu( event.player ).open( event.player )
|
||||||
|
TAG_TUTORIAL -> openTutorialBook( event.player )
|
||||||
|
TAG_STATS -> StatsMenu( event.player ).open( event.player )
|
||||||
|
TAG_LEADERBOARD -> LeaderboardMenu( event.player ).open( event.player )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.menu
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.database.PlayerStats
|
||||||
|
import club.mcscrims.speedhg.ranking.Rank
|
||||||
|
import club.mcscrims.speedhg.util.trans
|
||||||
|
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.Inventory
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the top 10 players by Scrim Score in a 5-row (45-slot) inventory.
|
||||||
|
*
|
||||||
|
* ## Layout (rows 0–4)
|
||||||
|
* ```
|
||||||
|
* [F][F][#1][#2][#3][#4][#5][F][F] ← Row 0: places 1-5 (slots 2-6)
|
||||||
|
* [F][F][#6][#7][#8][#9][#10][F][F] ← Row 1: places 6-10 (slots 11-15)
|
||||||
|
* [F][F][F][F][F][F][F][F][F] ← Row 2: filler
|
||||||
|
* [F][F][F][F][F][F][F][F][F] ← Row 3: filler
|
||||||
|
* [F][F][F][F][F][F][F][F][X] ← Row 4: close at slot 44
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Loading strategy
|
||||||
|
* The inventory opens immediately with animated "Loading…" placeholders,
|
||||||
|
* then [StatsManager.getLeaderboard] populates the real entries via its
|
||||||
|
* Main-Thread callback — no blocking, no perceivable delay.
|
||||||
|
*/
|
||||||
|
class LeaderboardMenu(
|
||||||
|
private val player: Player
|
||||||
|
) : Menu(
|
||||||
|
rows = 5,
|
||||||
|
title = player.trans( "gui.leaderboard_menu.title" )
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = SpeedHG.instance
|
||||||
|
private val mm = MiniMessage.miniMessage()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SLOT_CLOSE = 44
|
||||||
|
|
||||||
|
/** Slots occupied by the top-10 entries, in placement order. */
|
||||||
|
private val ENTRY_SLOTS = listOf( 2, 3, 4, 5, 6, 11, 12, 13, 14, 15 )
|
||||||
|
|
||||||
|
/** MiniMessage color tags per placement index (0-based). */
|
||||||
|
private val PLACEMENT_COLORS = listOf(
|
||||||
|
"<gold>", "<gray>", "<#CD7F32>",
|
||||||
|
"<white>", "<white>", "<white>",
|
||||||
|
"<white>", "<white>", "<white>", "<white>"
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Display prefixes per placement index (0-based). */
|
||||||
|
private val PLACEMENT_LABELS = listOf(
|
||||||
|
"🥇", "🥈", "🥉",
|
||||||
|
"4.", "5.", "6.", "7.", "8.", "9.", "10."
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Block material used as background per placement (1-based). */
|
||||||
|
private fun materialFor(
|
||||||
|
placement: Int
|
||||||
|
): Material = when( placement )
|
||||||
|
{
|
||||||
|
1 -> Material.GOLD_BLOCK
|
||||||
|
2 -> Material.IRON_BLOCK
|
||||||
|
3 -> Material.COPPER_BLOCK
|
||||||
|
else -> Material.STONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
override fun build(): Inventory = createInventory( title ).also { buildSkeleton( it ) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills the inventory with filler + loading placeholders, then immediately
|
||||||
|
* triggers an async leaderboard fetch whose Main-Thread callback populates
|
||||||
|
* the real entries into the already-open inventory.
|
||||||
|
*/
|
||||||
|
private fun buildSkeleton(
|
||||||
|
inv: Inventory
|
||||||
|
) {
|
||||||
|
inv.clear()
|
||||||
|
|
||||||
|
val filler = buildFiller()
|
||||||
|
repeat( 45 ) { inv.setItem( it, filler ) }
|
||||||
|
|
||||||
|
ENTRY_SLOTS.forEachIndexed { index, slot ->
|
||||||
|
inv.setItem( slot, buildLoadingItem( index + 1 ) )
|
||||||
|
}
|
||||||
|
|
||||||
|
inv.setItem( SLOT_CLOSE, buildCloseItem() )
|
||||||
|
|
||||||
|
// Fetch top-10 asynchronously; callback already dispatched to Main Thread
|
||||||
|
plugin.statsManager.getLeaderboard( limit = 10 ) { entries ->
|
||||||
|
populateEntries( inv, entries )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateEntries(
|
||||||
|
inv: Inventory,
|
||||||
|
entries: List<PlayerStats>
|
||||||
|
) {
|
||||||
|
ENTRY_SLOTS.forEachIndexed { index, slot ->
|
||||||
|
val stats = entries.getOrNull( index )
|
||||||
|
inv.setItem(
|
||||||
|
slot,
|
||||||
|
if ( stats != null ) buildEntryItem( index, stats )
|
||||||
|
else buildEmptySlot( index + 1 )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handling ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
override fun onClick(
|
||||||
|
event: InventoryClickEvent,
|
||||||
|
player: Player
|
||||||
|
) {
|
||||||
|
val slot = event.rawSlot
|
||||||
|
if ( slot !in 0 until size ) return
|
||||||
|
if ( slot == SLOT_CLOSE ) player.closeInventory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Item builders ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun buildEntryItem(
|
||||||
|
index: Int,
|
||||||
|
stats: PlayerStats
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
val placement = index + 1
|
||||||
|
val color = PLACEMENT_COLORS.getOrElse( index ) { "<white>" }
|
||||||
|
val label = PLACEMENT_LABELS.getOrElse( index ) { "${placement}." }
|
||||||
|
val score = stats.scrimScore
|
||||||
|
val games = stats.wins + stats.losses
|
||||||
|
val rankTag = Rank.getFormattedRankTag( score, games )
|
||||||
|
|
||||||
|
val item = ItemStack( materialFor( placement ) )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "$label $color<bold>${stats.name}</bold><reset>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
meta.lore(listOf(
|
||||||
|
Component.empty(),
|
||||||
|
mm.deserialize( "<gray>Rank: </gray>$rankTag" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
mm.deserialize( "<gray>Score: <white>${score} RR</white></gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
mm.deserialize( "<gray>Kills: <white>${stats.kills}</white></gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
mm.deserialize( "<gray>K/D: <white>${stats.formattedKD}</white></gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
mm.deserialize( "<gray>Wins: <white>${stats.wins}</white></gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
mm.deserialize( "<gray>Win Rate: <white>${stats.formattedWinRate}</white></gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
Component.empty()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildLoadingItem(
|
||||||
|
placement: Int
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
val item = ItemStack( Material.GRAY_STAINED_GLASS_PANE )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "<gray>#$placement — <italic>Loading…</italic></gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEmptySlot(
|
||||||
|
placement: Int
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
val item = ItemStack( Material.BARRIER )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "<gray>#$placement — <red>No Data</red></gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFiller(): ItemStack = ItemStack( Material.BLACK_STAINED_GLASS_PANE ).apply {
|
||||||
|
editMeta { it.displayName( Component.text( " " ).decoration( TextDecoration.ITALIC, false ) ) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCloseItem(): ItemStack = ItemStack( Material.DARK_OAK_DOOR ).apply {
|
||||||
|
editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "<red>✕ Close</red>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
187
src/main/kotlin/club/mcscrims/speedhg/gui/menu/StatsMenu.kt
Normal file
187
src/main/kotlin/club/mcscrims/speedhg/gui/menu/StatsMenu.kt
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package club.mcscrims.speedhg.gui.menu
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.ranking.Rank
|
||||||
|
import club.mcscrims.speedhg.util.trans
|
||||||
|
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.Inventory
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
import org.bukkit.inventory.meta.SkullMeta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a player's personal statistics in a 3-row (27-slot) inventory.
|
||||||
|
*
|
||||||
|
* ## Layout
|
||||||
|
* ```
|
||||||
|
* [F][F][F][F][HEAD][F][F][F][F] ← Row 0: Player head at slot 4
|
||||||
|
* [K][F][D][F][ W ][F][L][F][KD] ← Row 1: Kills / Deaths / Wins / Losses / K/D
|
||||||
|
* [F][F][SC][F][RK][F][WR][F][ X] ← Row 2: Score / Rank / Win Rate / Close
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* All stats are read from the in-memory [StatsManager] cache — no DB call.
|
||||||
|
*/
|
||||||
|
class StatsMenu(
|
||||||
|
private val player: Player
|
||||||
|
) : Menu(
|
||||||
|
rows = 3,
|
||||||
|
title = player.trans( "gui.stats_menu.title" )
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = SpeedHG.instance
|
||||||
|
private val mm = MiniMessage.miniMessage()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SLOT_HEAD = 4
|
||||||
|
|
||||||
|
private const val SLOT_KILLS = 9
|
||||||
|
private const val SLOT_DEATHS = 11
|
||||||
|
private const val SLOT_WINS = 13
|
||||||
|
private const val SLOT_LOSSES = 15
|
||||||
|
private const val SLOT_KD = 17
|
||||||
|
|
||||||
|
private const val SLOT_SCORE = 20
|
||||||
|
private const val SLOT_RANK = 22
|
||||||
|
private const val SLOT_WIN_RATE = 24
|
||||||
|
private const val SLOT_CLOSE = 26
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
override fun build(): Inventory = createInventory( title ).also { populate( it ) }
|
||||||
|
|
||||||
|
private fun populate(
|
||||||
|
inv: Inventory
|
||||||
|
) {
|
||||||
|
inv.clear()
|
||||||
|
|
||||||
|
val filler = buildFiller()
|
||||||
|
repeat( 27 ) { inv.setItem( it, filler ) }
|
||||||
|
|
||||||
|
// ── Fetch cached stats ────────────────────────────────────────────────
|
||||||
|
val stats = plugin.statsManager.getCachedStats( player.uniqueId )
|
||||||
|
val kills = stats?.kills ?: 0
|
||||||
|
val deaths = stats?.deaths ?: 0
|
||||||
|
val wins = stats?.wins ?: 0
|
||||||
|
val losses = stats?.losses ?: 0
|
||||||
|
val kd = stats?.formattedKD ?: "0.00"
|
||||||
|
val score = stats?.scrimScore ?: 0
|
||||||
|
val wr = stats?.formattedWinRate ?: "0.0%"
|
||||||
|
val games = wins + losses
|
||||||
|
val rank = Rank.fromPlayer( score, games )
|
||||||
|
val rankTag = Rank.getFormattedRankTag( score, games )
|
||||||
|
|
||||||
|
// ── Populate items ────────────────────────────────────────────────────
|
||||||
|
inv.setItem( SLOT_HEAD, buildPlayerHead() )
|
||||||
|
|
||||||
|
inv.setItem( SLOT_KILLS, buildStatItem( Material.IRON_SWORD, "<green>Kills", "$kills" ) )
|
||||||
|
inv.setItem( SLOT_DEATHS, buildStatItem( Material.SKELETON_SKULL, "<red>Deaths", "$deaths" ) )
|
||||||
|
inv.setItem( SLOT_WINS, buildStatItem( Material.GOLDEN_APPLE, "<gold>Wins", "$wins" ) )
|
||||||
|
inv.setItem( SLOT_LOSSES, buildStatItem( Material.BARRIER, "<red>Losses", "$losses" ) )
|
||||||
|
inv.setItem( SLOT_KD, buildStatItem( Material.BLAZE_ROD, "<yellow>K/D", "$kd" ) )
|
||||||
|
|
||||||
|
inv.setItem( SLOT_SCORE, buildStatItem( Material.NETHER_STAR, "<aqua>Scrim Score", "$score RR" ) )
|
||||||
|
inv.setItem( SLOT_RANK, buildRankItem( rank, rankTag ) )
|
||||||
|
inv.setItem( SLOT_WIN_RATE, buildStatItem( Material.EXPERIENCE_BOTTLE, "<green>Win Rate", "$wr" ) )
|
||||||
|
|
||||||
|
inv.setItem( SLOT_CLOSE, buildCloseItem() )
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handling ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
override fun onClick(
|
||||||
|
event: InventoryClickEvent,
|
||||||
|
player: Player
|
||||||
|
) {
|
||||||
|
val slot = event.rawSlot
|
||||||
|
if ( slot !in 0 until size ) return
|
||||||
|
if ( slot == SLOT_CLOSE ) player.closeInventory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Item builders ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun buildPlayerHead(): ItemStack
|
||||||
|
{
|
||||||
|
val item = ItemStack( Material.PLAYER_HEAD )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
if ( meta is SkullMeta )
|
||||||
|
meta.owningPlayer = player
|
||||||
|
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "<aqua><bold>${player.name}</bold></aqua>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
meta.lore(listOf(
|
||||||
|
Component.empty(),
|
||||||
|
mm.deserialize( "<gray>Your personal statistics.</gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
Component.empty()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a generic stat glass-pane item with a [label] and a [value] line in the lore.
|
||||||
|
*/
|
||||||
|
private fun buildStatItem(
|
||||||
|
material: Material,
|
||||||
|
label: String,
|
||||||
|
value: String
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
val item = ItemStack( material )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "<gray>$label</gray>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
meta.lore(listOf(
|
||||||
|
Component.empty(),
|
||||||
|
mm.deserialize( "<white> $value</white>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
Component.empty()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRankItem(
|
||||||
|
rank: Rank,
|
||||||
|
rankTag: String
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
val item = ItemStack( Material.DIAMOND )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "<light_purple>Rank</light_purple>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
meta.lore(listOf(
|
||||||
|
Component.empty(),
|
||||||
|
mm.deserialize( " $rankTag" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false ),
|
||||||
|
Component.empty()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFiller(): ItemStack = ItemStack( Material.GRAY_STAINED_GLASS_PANE ).apply {
|
||||||
|
editMeta { it.displayName( Component.text( " " ).decoration( TextDecoration.ITALIC, false ) ) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCloseItem(): ItemStack = ItemStack( Material.DARK_OAK_DOOR ).apply {
|
||||||
|
editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
mm.deserialize( "<red>✕ Close</red>" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,6 +23,15 @@ game:
|
|||||||
name: '<gold><bold>Kits</bold></gold> <gray>(right-click)</gray>'
|
name: '<gold><bold>Kits</bold></gold> <gray>(right-click)</gray>'
|
||||||
perks:
|
perks:
|
||||||
name: '<light_purple><bold>Perks</bold></light_purple> <gray>(right-click)</gray>'
|
name: '<light_purple><bold>Perks</bold></light_purple> <gray>(right-click)</gray>'
|
||||||
|
tutorial:
|
||||||
|
name: '<yellow><bold>How to Play</bold></yellow>'
|
||||||
|
lore: '<gray>Read me to understand the game!</gray>'
|
||||||
|
stats:
|
||||||
|
name: '<aqua><bold>Profile & Stats</bold></aqua>'
|
||||||
|
lore: '<gray>View your personal statistics.</gray>'
|
||||||
|
leaderboard:
|
||||||
|
name: '<gold><bold>Top 10 Leaderboard</bold></gold>'
|
||||||
|
lore: '<gray>Who are the best players?</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>'
|
||||||
@@ -218,6 +227,10 @@ gui:
|
|||||||
selected: '<green>Equipped: <perk><green>!'
|
selected: '<green>Equipped: <perk><green>!'
|
||||||
deselected: '<red>Unequipped: <perk><red>!'
|
deselected: '<red>Unequipped: <perk><red>!'
|
||||||
close: '<red>✕ Close</red>'
|
close: '<red>✕ Close</red>'
|
||||||
|
stats_menu:
|
||||||
|
title: '<aqua><bold>Profile & Stats</bold></aqua>'
|
||||||
|
leaderboard_menu:
|
||||||
|
title: '<gold><bold>Top 10 Leaderboard</bold></gold>'
|
||||||
|
|
||||||
perks:
|
perks:
|
||||||
oracle:
|
oracle:
|
||||||
|
|||||||
Reference in New Issue
Block a user