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:
TDSTOS
2026-04-03 22:28:56 +02:00
parent ab976cc2a4
commit 7b6557122b
7 changed files with 339 additions and 18 deletions

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