Add ranking system and /ranking command
Introduce a ranking system: add Rank enum and RankingManager enhancements (isRankingEnabled toggle, getRank(), RR change handling, rank-up/down detection and player notifications with titles/sounds). Add a new /ranking admin command (toggle, status, rank) with tab completion and register it in plugin startup. Surface player rank on the scoreboard (ScoreboardManager) and add related language entries and plugin.yml permission/command metadata. Logging updated to include ranked state.
This commit is contained in:
@@ -2,6 +2,7 @@ package club.mcscrims.speedhg
|
|||||||
|
|
||||||
import club.mcscrims.speedhg.command.KitCommand
|
import club.mcscrims.speedhg.command.KitCommand
|
||||||
import club.mcscrims.speedhg.command.LeaderboardCommand
|
import club.mcscrims.speedhg.command.LeaderboardCommand
|
||||||
|
import club.mcscrims.speedhg.command.RankingCommand
|
||||||
import club.mcscrims.speedhg.command.TimerCommand
|
import club.mcscrims.speedhg.command.TimerCommand
|
||||||
import club.mcscrims.speedhg.config.CustomGameManager
|
import club.mcscrims.speedhg.config.CustomGameManager
|
||||||
import club.mcscrims.speedhg.config.CustomGameSettings
|
import club.mcscrims.speedhg.config.CustomGameSettings
|
||||||
@@ -149,6 +150,12 @@ class SpeedHG : JavaPlugin() {
|
|||||||
tabCompleter = timerCommand
|
tabCompleter = timerCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val rankingCommand = RankingCommand( this )
|
||||||
|
getCommand( "ranking" )?.apply {
|
||||||
|
setExecutor( rankingCommand )
|
||||||
|
tabCompleter = rankingCommand
|
||||||
|
}
|
||||||
|
|
||||||
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
|
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
109
src/main/kotlin/club/mcscrims/speedhg/command/RankingCommand.kt
Normal file
109
src/main/kotlin/club/mcscrims/speedhg/command/RankingCommand.kt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package club.mcscrims.speedhg.command
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.util.sendMsg
|
||||||
|
import org.bukkit.command.Command
|
||||||
|
import org.bukkit.command.CommandExecutor
|
||||||
|
import org.bukkit.command.CommandSender
|
||||||
|
import org.bukkit.command.TabCompleter
|
||||||
|
|
||||||
|
class RankingCommand(
|
||||||
|
private val plugin: SpeedHG
|
||||||
|
) : CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
override fun onCommand(
|
||||||
|
sender: CommandSender,
|
||||||
|
command: Command,
|
||||||
|
label: String,
|
||||||
|
args: Array<out String>
|
||||||
|
): Boolean
|
||||||
|
{
|
||||||
|
if (!sender.hasPermission( "speedhg.admin.ranking" ))
|
||||||
|
{
|
||||||
|
sender.sendMsg( "default.no_permission" )
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
when( args.firstOrNull()?.lowercase() )
|
||||||
|
{
|
||||||
|
"toggle" -> handleToggle( sender )
|
||||||
|
"status" -> handleStatus( sender )
|
||||||
|
"rank" -> handleRank( sender, args )
|
||||||
|
else -> sender.sendMsg( "commands.ranking.usage" )
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sub-Commands ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun handleToggle(
|
||||||
|
sender: CommandSender
|
||||||
|
) {
|
||||||
|
val rm = plugin.rankingManager
|
||||||
|
rm.isRankingEnabled = !rm.isRankingEnabled
|
||||||
|
|
||||||
|
val key = if ( rm.isRankingEnabled ) "commands.ranking.enabled"
|
||||||
|
else "commands.ranking.disabled"
|
||||||
|
sender.sendMsg( key )
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleStatus(
|
||||||
|
sender: CommandSender
|
||||||
|
) {
|
||||||
|
val enabled = plugin.rankingManager.isRankingEnabled
|
||||||
|
val key = if ( enabled ) "commands.ranking.status_enabled"
|
||||||
|
else "commands.ranking.status_disabled"
|
||||||
|
sender.sendMsg( key )
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRank(
|
||||||
|
sender: CommandSender,
|
||||||
|
args: Array<out String>
|
||||||
|
) {
|
||||||
|
val targetName = args.getOrNull( 1 ) ?: run {
|
||||||
|
sender.sendMsg( "commands.ranking.usage" )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val target = plugin.server.getPlayer( targetName ) ?: run {
|
||||||
|
sender.sendMsg( "commands.ranking.player_not_found", "name" to targetName )
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val stats = plugin.statsManager.getCachedStats( target.uniqueId )
|
||||||
|
val rank = plugin.rankingManager.getRank( target )
|
||||||
|
val score = stats?.scrimScore ?: 0
|
||||||
|
val games = ( stats?.wins ?: 0 ) + ( stats?.losses ?: 0 )
|
||||||
|
|
||||||
|
sender.sendMsg(
|
||||||
|
"commands.ranking.rank_info",
|
||||||
|
"name" to target.name,
|
||||||
|
"rank" to rank.tag,
|
||||||
|
"score" to score.toString(),
|
||||||
|
"games" to games.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabComplete(
|
||||||
|
sender: CommandSender,
|
||||||
|
command: Command,
|
||||||
|
label: String,
|
||||||
|
args: Array<out String>
|
||||||
|
): List<String?>
|
||||||
|
{
|
||||||
|
if (!sender.hasPermission( "speedhg.admin.ranking" ))
|
||||||
|
return emptyList()
|
||||||
|
|
||||||
|
return when( args.size )
|
||||||
|
{
|
||||||
|
1 -> listOf( "toggle", "status", "rank" )
|
||||||
|
.filter { it.startsWith( args[0], true ) }
|
||||||
|
2 -> if (args[ 0 ].equals( "rank", true ))
|
||||||
|
plugin.server.onlinePlayers.map { it.name }
|
||||||
|
.filter { it.startsWith( args[1], true ) }
|
||||||
|
else emptyList()
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
78
src/main/kotlin/club/mcscrims/speedhg/ranking/Rank.kt
Normal file
78
src/main/kotlin/club/mcscrims/speedhg/ranking/Rank.kt
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package club.mcscrims.speedhg.ranking
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tier-System für SpeedHG.
|
||||||
|
*
|
||||||
|
* Reihenfolge der Enum-Einträge ist bewusst aufsteigend (niedrig → hoch),
|
||||||
|
* da [ordinal] für den Rank-Up/Down-Vergleich in [RankingManager] genutzt wird.
|
||||||
|
*
|
||||||
|
* @param displayName Angezeigter Name (z. B. "Gold").
|
||||||
|
* @param colorTag MiniMessage-Farbtag ohne Inhalt (z. B. "<gold>").
|
||||||
|
* @param minScore Mindest-scrimScore für diesen Rang (inklusive). UNRANKED
|
||||||
|
* bekommt [Int.MIN_VALUE] — er wird nie per Score gewählt,
|
||||||
|
* sondern nur wenn der Spieler in der Placement-Phase ist.
|
||||||
|
*/
|
||||||
|
enum class Rank(
|
||||||
|
val displayName: String,
|
||||||
|
val colorTag: String,
|
||||||
|
val minScore: Int
|
||||||
|
) {
|
||||||
|
UNRANKED ("Unranked", "<dark_gray>", Int.MIN_VALUE),
|
||||||
|
BRONZE ("Bronze", "<#CD7F32>", 0 ),
|
||||||
|
SILVER ("Silver", "<silver>", 500 ),
|
||||||
|
GOLD ("Gold", "<gold>", 1000 ),
|
||||||
|
PLATINUM ("Platinum", "<aqua>", 1500 ),
|
||||||
|
DIAMOND ("Diamond", "<#B9F2FF>", 2000 ),
|
||||||
|
SCRIM_MASTER("Scrim-Master", "<gradient:red:gold>", 2500 );
|
||||||
|
|
||||||
|
// ── Vorgefertigte Strings (String-Konkatenation einmalig, nicht pro Frame) ──
|
||||||
|
|
||||||
|
/** MiniMessage-String: "<gold>Gold<reset>". Ideal für Platzhalter in Nachrichten. */
|
||||||
|
val tag: String = "$colorTag$displayName<reset>"
|
||||||
|
|
||||||
|
/** Klammer-Prefix für Chat/Scoreboard: "<gold>[Gold]<reset>". */
|
||||||
|
val prefix: String = "$colorTag[$displayName]<reset>"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialisiertes Adventure-[Component] des [tag].
|
||||||
|
* [lazy] → wird nur beim ersten Zugriff gebaut und dann gecacht.
|
||||||
|
* Da Enum-Instanzen Singletons sind, passiert das genau einmal pro Rang.
|
||||||
|
*/
|
||||||
|
val component: Component by lazy { MiniMessage.miniMessage().deserialize(tag) }
|
||||||
|
|
||||||
|
/** Deserialisiertes Adventure-[Component] des [prefix]. */
|
||||||
|
val prefixComponent: Component by lazy { MiniMessage.miniMessage().deserialize(prefix) }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den **sichtbaren** Rang zurück.
|
||||||
|
*
|
||||||
|
* Spieler mit < [RankingManager.PLACEMENT_GAMES] abgeschlossenen Spielen
|
||||||
|
* bekommen immer [UNRANKED], egal wie hoch ihr interner Score ist.
|
||||||
|
*
|
||||||
|
* @param scrimScore Aktueller Scrim-Score.
|
||||||
|
* @param gamesPlayed Abgeschlossene Spiele (wins + losses zum Zeitpunkt der Abfrage).
|
||||||
|
*/
|
||||||
|
fun fromPlayer(scrimScore: Int, gamesPlayed: Int): Rank =
|
||||||
|
if (gamesPlayed < RankingManager.PLACEMENT_GAMES) UNRANKED
|
||||||
|
else fromScore(scrimScore)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reines Score → Tier Mapping, ignoriert die Placement-Phase.
|
||||||
|
* Wird intern für Rank-Change-Erkennung (Pre/Post Adjustment) genutzt.
|
||||||
|
*
|
||||||
|
* Iteriert 6 Einträge in absteigender Score-Reihenfolge → O(1) für die Praxis.
|
||||||
|
*/
|
||||||
|
fun fromScore(scrimScore: Int): Rank =
|
||||||
|
entries
|
||||||
|
.asSequence()
|
||||||
|
.filter { it != UNRANKED }
|
||||||
|
.sortedByDescending { it.minScore }
|
||||||
|
.firstOrNull { scrimScore >= it.minScore }
|
||||||
|
?: BRONZE
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
package club.mcscrims.speedhg.ranking
|
package club.mcscrims.speedhg.ranking
|
||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.ranking.RankingManager.Companion.KILL_CAP
|
||||||
|
import club.mcscrims.speedhg.ranking.RankingManager.Companion.MAX_RR_GAIN
|
||||||
|
import club.mcscrims.speedhg.ranking.RankingManager.Companion.MAX_RR_LOSS
|
||||||
|
import club.mcscrims.speedhg.ranking.RankingManager.Companion.MIN_RR_GAIN
|
||||||
|
import club.mcscrims.speedhg.ranking.RankingManager.Companion.MIN_RR_LOSS
|
||||||
|
import club.mcscrims.speedhg.ranking.RankingManager.Companion.PLACEMENT_GAMES
|
||||||
import club.mcscrims.speedhg.util.sendMsg
|
import club.mcscrims.speedhg.util.sendMsg
|
||||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
import club.mcscrims.speedhg.util.trans
|
||||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
|
import net.kyori.adventure.title.Title
|
||||||
|
import org.bukkit.Sound
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
import java.util.UUID
|
import java.time.Duration
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -68,7 +76,15 @@ class RankingManager(
|
|||||||
const val KILL_CAP = 8
|
const val KILL_CAP = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mm = MiniMessage.miniMessage()
|
// ── Ranking-Modus ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wenn `false`: Keine RR-Änderungen am Ende der Runde.
|
||||||
|
* Kills/Deaths werden weiterhin geloggt.
|
||||||
|
* Per `/ranking toggle` umschaltbar.
|
||||||
|
*/
|
||||||
|
@Volatile
|
||||||
|
var isRankingEnabled: Boolean = true
|
||||||
|
|
||||||
// ── Round State (wird in startRound() zurückgesetzt) ─────────────────────
|
// ── Round State (wird in startRound() zurückgesetzt) ─────────────────────
|
||||||
|
|
||||||
@@ -102,7 +118,7 @@ class RankingManager(
|
|||||||
eliminationIndex.set( 0 )
|
eliminationIndex.set( 0 )
|
||||||
totalPlayersThisRound = players.size
|
totalPlayersThisRound = players.size
|
||||||
players.forEach { roundKills[ it.uniqueId ] = 0 }
|
players.forEach { roundKills[ it.uniqueId ] = 0 }
|
||||||
plugin.logger.info("[RankingManager] Runde gestartet mit ${players.size} Spielern.")
|
plugin.logger.info("[RankingManager] Runde gestartet mit ${players.size} Spielern, Ranked: $isRankingEnabled.")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,7 +168,7 @@ class RankingManager(
|
|||||||
if ( gamesPlayed < PLACEMENT_GAMES ) {
|
if ( gamesPlayed < PLACEMENT_GAMES ) {
|
||||||
handlePlacementMatch( player, gamesPlayed, kills, placement )
|
handlePlacementMatch( player, gamesPlayed, kills, placement )
|
||||||
} else {
|
} else {
|
||||||
handleRankedMatch( player, kills, placement )
|
handleRankedMatch( player, kills, placement, gamesPlayed )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +184,26 @@ class RankingManager(
|
|||||||
totalPlayersThisRound = 0
|
totalPlayersThisRound = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Rank-Abfrage (für Scoreboard, Chat-Prefix etc.)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den aktuell sichtbaren Rang des Spielers zurück.
|
||||||
|
* Berücksichtigt die Placement-Phase — Spieler in Placement bekommen [Rank.UNRANKED].
|
||||||
|
*
|
||||||
|
* **Performant**: Greift nur auf den In-Memory-Cache zu, kein DB-Call.
|
||||||
|
* Sicher für häufige Aufrufe (Scoreboard-Updates alle 10 Ticks).
|
||||||
|
*/
|
||||||
|
fun getRank(
|
||||||
|
player: Player
|
||||||
|
): Rank
|
||||||
|
{
|
||||||
|
val stats = plugin.statsManager.getCachedStats( player.uniqueId )
|
||||||
|
?: return Rank.UNRANKED
|
||||||
|
return Rank.fromPlayer( stats.scrimScore, stats.wins + stats.losses )
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Core RR Algorithm
|
// Core RR Algorithm
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -239,9 +275,12 @@ class RankingManager(
|
|||||||
kills: Int,
|
kills: Int,
|
||||||
placement: Int
|
placement: Int
|
||||||
) {
|
) {
|
||||||
// Internen Score still anpassen (für die spätere Einrankung relevant)
|
if ( isRankingEnabled )
|
||||||
val internalChange = calculateRRChange( placement, totalPlayersThisRound, kills )
|
{
|
||||||
plugin.statsManager.adjustScrimScore( player.uniqueId, internalChange )
|
// Internen Score still anpassen (für die spätere Einrankung relevant)
|
||||||
|
val internalChange = calculateRRChange( placement, totalPlayersThisRound, kills )
|
||||||
|
plugin.statsManager.adjustScrimScore( player.uniqueId, internalChange )
|
||||||
|
}
|
||||||
|
|
||||||
// Spielnummer im Display: min mit PLACEMENT_GAMES, damit kein "6/5" erscheint
|
// Spielnummer im Display: min mit PLACEMENT_GAMES, damit kein "6/5" erscheint
|
||||||
val currentGameNumber = ( completedGames + 1 ).coerceAtMost( PLACEMENT_GAMES )
|
val currentGameNumber = ( completedGames + 1 ).coerceAtMost( PLACEMENT_GAMES )
|
||||||
@@ -258,16 +297,29 @@ class RankingManager(
|
|||||||
private fun handleRankedMatch(
|
private fun handleRankedMatch(
|
||||||
player: Player,
|
player: Player,
|
||||||
kills: Int,
|
kills: Int,
|
||||||
placement: Int
|
placement: Int,
|
||||||
|
gamesPlayed: Int
|
||||||
) {
|
) {
|
||||||
val rrChange = calculateRRChange( placement, totalPlayersThisRound, kills )
|
val rrChange = calculateRRChange( placement, totalPlayersThisRound, kills )
|
||||||
|
val rrTag: String
|
||||||
|
|
||||||
// scrimScore clamp auf >= 0 ist bereits in adjustScrimScore() via coerceAtLest(0) gesichert
|
if ( isRankingEnabled )
|
||||||
plugin.statsManager.adjustScrimScore( player.uniqueId, rrChange )
|
{
|
||||||
|
// ── Rank-Change-Erkennung: Score VOR dem Anpassen lesen ───────────
|
||||||
|
val stats = plugin.statsManager.getCachedStats( player.uniqueId )
|
||||||
|
val scoreBefore = stats?.scrimScore ?: 0
|
||||||
|
val rankBefore = Rank.fromScore( scoreBefore )
|
||||||
|
|
||||||
// RR-Tag vorformatieren
|
plugin.statsManager.adjustScrimScore( player.uniqueId, rrChange )
|
||||||
val rrTag = if ( rrChange >= 0 ) "<green>+${rrChange} RR</green>"
|
val rankAfter = Rank.fromScore( stats?.scrimScore ?: 0 )
|
||||||
else "<red>${rrChange} RR</red>"
|
|
||||||
|
if ( rankBefore != rankAfter && gamesPlayed >= PLACEMENT_GAMES )
|
||||||
|
notifyRankChange( player, rankBefore, rankAfter )
|
||||||
|
|
||||||
|
rrTag = if ( rrChange >= 0 ) "<green>+${rrChange} RR</green>"
|
||||||
|
else "<red>${rrChange} RR</red>"
|
||||||
|
}
|
||||||
|
else rrTag = "<gray><italic>(Unranked - no RR)</italic></gray>"
|
||||||
|
|
||||||
val msgKey = if ( placement == 1 ) "ranking.result_win"
|
val msgKey = if ( placement == 1 ) "ranking.result_win"
|
||||||
else "ranking.result_loss"
|
else "ranking.result_loss"
|
||||||
@@ -280,6 +332,54 @@ class RankingManager(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private: Rank-Change Benachrichtigung
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Benachrichtigt den Spieler über einen Rang-Aufstieg oder -Abstieg.
|
||||||
|
* Promotion: Title + Fanfare-Sound.
|
||||||
|
* Demotion: Subtile Nachricht + leiser Sound.
|
||||||
|
*/
|
||||||
|
private fun notifyRankChange(
|
||||||
|
player: Player,
|
||||||
|
from: Rank,
|
||||||
|
to: Rank
|
||||||
|
) {
|
||||||
|
val isPromotion = to.ordinal > from.ordinal
|
||||||
|
|
||||||
|
if ( isPromotion )
|
||||||
|
{
|
||||||
|
player.showTitle(Title.title(
|
||||||
|
player.trans( "ranking.promote-main" ),
|
||||||
|
player.trans( "ranking.promote-sub", "color" to to.colorTag, "name" to to.displayName ),
|
||||||
|
Title.Times.times(
|
||||||
|
Duration.ofMillis( 300 ),
|
||||||
|
Duration.ofSeconds( 3 ),
|
||||||
|
Duration.ofMillis( 700 )
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
player.playSound( player.location, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1f )
|
||||||
|
player.playSound( player.location, Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.5f )
|
||||||
|
|
||||||
|
player.sendMsg(
|
||||||
|
"ranking.rank_up",
|
||||||
|
"from" to from.tag,
|
||||||
|
"to" to to.tag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
player.playSound( player.location, Sound.ENTITY_WITHER_HURT, 0.4f, 1.6f )
|
||||||
|
player.sendMsg(
|
||||||
|
"ranking.rank_down",
|
||||||
|
"from" to from.tag,
|
||||||
|
"to" to to.tag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Private: Helpers
|
// Private: Helpers
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ class ScoreboardManager(
|
|||||||
val max = Bukkit.getMaxPlayers().toString()
|
val max = Bukkit.getMaxPlayers().toString()
|
||||||
val kitName = plugin.kitManager.getSelectedKit( player )?.displayName ?: Component.text( "None" )
|
val kitName = plugin.kitManager.getSelectedKit( player )?.displayName ?: Component.text( "None" )
|
||||||
|
|
||||||
|
val rank = plugin.rankingManager.getRank( player )
|
||||||
|
val rankComponent = rank.component
|
||||||
|
|
||||||
val lines: List<Component>
|
val lines: List<Component>
|
||||||
|
|
||||||
if ( state == GameState.LOBBY || state == GameState.STARTING )
|
if ( state == GameState.LOBBY || state == GameState.STARTING )
|
||||||
@@ -75,7 +78,7 @@ class ScoreboardManager(
|
|||||||
lines = plugin.languageManager.getMessageList(
|
lines = plugin.languageManager.getMessageList(
|
||||||
player, "scoreboard.lobby",
|
player, "scoreboard.lobby",
|
||||||
mapOf( "online" to online, "max" to max, "time" to timeString ),
|
mapOf( "online" to online, "max" to max, "time" to timeString ),
|
||||||
mapOf( "kit" to kitName )
|
mapOf( "kit" to kitName, "rank" to rankComponent )
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -88,7 +91,7 @@ class ScoreboardManager(
|
|||||||
lines = plugin.languageManager.getMessageList(
|
lines = plugin.languageManager.getMessageList(
|
||||||
player, "scoreboard.ingame",
|
player, "scoreboard.ingame",
|
||||||
mapOf( "timer" to timeString, "alive" to alive, "kills" to kills, "border" to border ),
|
mapOf( "timer" to timeString, "alive" to alive, "kills" to kills, "border" to border ),
|
||||||
mapOf( "kit" to kitName )
|
mapOf( "kit" to kitName, "rank" to rankComponent )
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ ranking:
|
|||||||
result_win: '<prefix><gold>🏆 Victory!</gold> <gray>Kills: <aqua><kills></aqua></gray> · <rr>'
|
result_win: '<prefix><gold>🏆 Victory!</gold> <gray>Kills: <aqua><kills></aqua></gray> · <rr>'
|
||||||
result_loss: '<prefix><gray>You placed <aqua><placement></aqua> · Kills: <aqua><kills></aqua></gray> · <rr>'
|
result_loss: '<prefix><gray>You placed <aqua><placement></aqua> · Kills: <aqua><kills></aqua></gray> · <rr>'
|
||||||
|
|
||||||
|
promote-main: '<gradient:gold:yellow><bold>RANK UP!</bold></gradient>'
|
||||||
|
promote-sub: '<color><name><reset>'
|
||||||
|
|
||||||
|
rank_up: '<prefix><green>⬆ Rank Up!</green> <gray><from> → <to>'
|
||||||
|
rank_down: '<prefix><red>⬇ Rank Down.</red> <gray><from> → <to>'
|
||||||
|
|
||||||
title:
|
title:
|
||||||
fight-main: '<red>The battle has begun!</red>'
|
fight-main: '<red>The battle has begun!</red>'
|
||||||
fight-sub: '<red>Try not to die!</red>'
|
fight-sub: '<red>Try not to die!</red>'
|
||||||
@@ -79,6 +85,15 @@ commands:
|
|||||||
positiveNumber: '<red>Invalid time format! Use, for example, 10m, 30s, or 600.</red>'
|
positiveNumber: '<red>Invalid time format! Use, for example, 10m, 30s, or 600.</red>'
|
||||||
onlyIngame: '<red>Timer can only be set in game.</red>'
|
onlyIngame: '<red>Timer can only be set in game.</red>'
|
||||||
set: '<green>The game timer has been set to <time>!</green>'
|
set: '<green>The game timer has been set to <time>!</green>'
|
||||||
|
ranking:
|
||||||
|
usage: '<red>Usage: /ranking <toggle|status|rank [player]></red>'
|
||||||
|
enabled: '<prefix><green>✔ Ranked mode is now <bold>ACTIVE</bold>. RR will be awarded next round.</green>'
|
||||||
|
disabled: '<prefix><yellow>⚠ Unranked mode is now <bold>ACTIVE</bold>. No RR changes next round.</yellow>'
|
||||||
|
status_enabled: '<prefix><gray>Ranking: <green><bold>ACTIVE</bold></green></gray>'
|
||||||
|
status_disabled: '<prefix><gray>Ranking: <yellow><bold>DISABLED (Unranked)</bold></yellow></gray>'
|
||||||
|
rank_usage: '<red>Usage: /ranking rank <player></red>'
|
||||||
|
player_not_found: '<red>Player <name> is not online.</red>'
|
||||||
|
rank_info: '<prefix><gray>Player <white><name></white> — <rank> <gray>(<score> RR · <games> games)</gray>'
|
||||||
|
|
||||||
scoreboard:
|
scoreboard:
|
||||||
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'
|
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'
|
||||||
@@ -86,6 +101,7 @@ scoreboard:
|
|||||||
- "<gray><st> "
|
- "<gray><st> "
|
||||||
- "Players: <green><online>/<max>"
|
- "Players: <green><online>/<max>"
|
||||||
- "Kit: <yellow><kit>"
|
- "Kit: <yellow><kit>"
|
||||||
|
- "Rank: <rank>"
|
||||||
- ""
|
- ""
|
||||||
- "<gray>Waiting for start..."
|
- "<gray>Waiting for start..."
|
||||||
- ""
|
- ""
|
||||||
@@ -95,6 +111,7 @@ scoreboard:
|
|||||||
- "Time: <green><timer>"
|
- "Time: <green><timer>"
|
||||||
- "Players: <red><alive>"
|
- "Players: <red><alive>"
|
||||||
- "Kills: <green><kills>"
|
- "Kills: <green><kills>"
|
||||||
|
- "Rank: <rank>"
|
||||||
- ""
|
- ""
|
||||||
- "Border: <red><border>"
|
- "Border: <red><border>"
|
||||||
- ""
|
- ""
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ permissions:
|
|||||||
speedhg.admin.timer:
|
speedhg.admin.timer:
|
||||||
description: 'Change the current game time'
|
description: 'Change the current game time'
|
||||||
default: false
|
default: false
|
||||||
|
speedhg.admin.ranking:
|
||||||
|
description: 'Manage the ranking system (toggle unranked mode, inspect ranks)'
|
||||||
|
default: false
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
kit:
|
kit:
|
||||||
@@ -24,4 +27,8 @@ commands:
|
|||||||
timer:
|
timer:
|
||||||
description: 'Change the current game time (Admin Command)'
|
description: 'Change the current game time (Admin Command)'
|
||||||
usage: '/timer <seconds>'
|
usage: '/timer <seconds>'
|
||||||
permission: speedhg.admin.timer
|
permission: speedhg.admin.timer
|
||||||
|
ranking:
|
||||||
|
description: 'Manage the SpeedHG ranking system'
|
||||||
|
usage: '/ranking <toggle|status|rank [player]>'
|
||||||
|
permission: speedhg.admin.ranking
|
||||||
Reference in New Issue
Block a user