Add podium ceremony and rank enhancements
Introduce a PodiumManager to run an end-of-round podium ceremony and integrate it into the lifecycle. Key changes: - Add PodiumManager (new): builds podium columns, teleports and freezes top-3 players, spawns TextDisplay entities, posts staggered announcements, and runs a ranked firework show; includes cleanup to remove entities, blocks and cancel tasks. - SpeedHG: initialize podiumManager and call podiumManager.cleanup() on disable. - GameManager: trigger plugin.podiumManager.launch(winnerUUID) shortly after the win announcement. - RankingManager: track elimination order during the round and expose getEliminationOrder() to determine placements. - Rank: add sub-tier logic (I/II/III), firework color, and helper methods to produce formatted MiniMessage rank tags/components; improve cached component/prefix handling. - ScoreboardManager: use Rank.getFormattedRankName(...) to show the formatted rank with sub-tier on scoreboards. Purpose: provide a richer end-of-round experience (visuals, sounds, fireworks) and accurate top-3 determination using elimination order while adding rank sub-tiering and formatted rank output for UI elements.
This commit is contained in:
@@ -2,70 +2,115 @@ package club.mcscrims.speedhg.ranking
|
||||
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import org.bukkit.Color
|
||||
|
||||
/**
|
||||
* Tier-System für SpeedHG.
|
||||
* Rang-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.
|
||||
* ## Sub-Tier Berechnung
|
||||
* Jeder Rang (außer [UNRANKED] und [SCRIM_MASTER]) hat drei Unterränge I–III.
|
||||
* Die Spanne [minScore, nächsterRang.minScore) wird in drei gleichgroße Drittel
|
||||
* aufgeteilt — unterstes Drittel = III, mittleres = II, oberstes = I.
|
||||
*
|
||||
* @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.
|
||||
* Beispiel GOLD (1000–1499, Spanne 500):
|
||||
* Gold III → 1000–1165 (position < 166)
|
||||
* Gold II → 1166–1332 (position < 332)
|
||||
* Gold I → 1333–1499 (position ≥ 332)
|
||||
*
|
||||
* @param displayName Angezeigter Rang-Name.
|
||||
* @param colorTag MiniMessage-Farbtag ohne Inhalt (z. B. "<gold>").
|
||||
* @param minScore Untere Score-Grenze (inklusiv). UNRANKED = [Int.MIN_VALUE].
|
||||
* @param fireworkColor Feuerwerksfarbe für die Podium-Zeremonie.
|
||||
*/
|
||||
enum class Rank(
|
||||
val displayName: String,
|
||||
val colorTag: String,
|
||||
val minScore: Int
|
||||
val displayName: String,
|
||||
val colorTag: String,
|
||||
val minScore: Int,
|
||||
val fireworkColor: Color
|
||||
) {
|
||||
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 );
|
||||
UNRANKED ("Unranked", "<dark_gray>", Int.MIN_VALUE, Color.GRAY ),
|
||||
BRONZE ("Bronze", "<#CD7F32>", 0, Color.fromRGB(0xCD7F32) ),
|
||||
SILVER ("Silver", "<gray>", 500, Color.fromRGB(0xC0C0C0) ),
|
||||
GOLD ("Gold", "<gold>", 1000, Color.YELLOW ),
|
||||
PLATINUM ("Platinum", "<aqua>", 1500, Color.fromRGB(0x00FFFF) ),
|
||||
DIAMOND ("Diamond", "<#B9F2FF>", 2000, Color.fromRGB(0xB9F2FF) ),
|
||||
SCRIM_MASTER("Scrim-Master", "<gradient:red:gold>", 2500, Color.ORANGE );
|
||||
|
||||
// ── Vorgefertigte Strings (String-Konkatenation einmalig, nicht pro Frame) ──
|
||||
// ── Vorberechnete Strings & Components (einmal pro Rang, nie pro Frame) ──
|
||||
|
||||
/** MiniMessage-String: "<gold>Gold<reset>". Ideal für Platzhalter in Nachrichten. */
|
||||
/** MiniMessage-Tag: "<gold>Gold<reset>" */
|
||||
val tag: String = "$colorTag$displayName<reset>"
|
||||
|
||||
/** Klammer-Prefix für Chat/Scoreboard: "<gold>[Gold]<reset>". */
|
||||
/** Klammer-Prefix: "<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.
|
||||
* Deserialisiertes Adventure-Component des [tag].
|
||||
* [lazy] → einmalige Deserialisierung, danach gecacht.
|
||||
* Da Enums Singletons sind, passiert das genau einmal pro Rang.
|
||||
*/
|
||||
val component: Component by lazy { MiniMessage.miniMessage().deserialize(tag) }
|
||||
|
||||
/** Deserialisiertes Adventure-[Component] des [prefix]. */
|
||||
/** Deserialisiertes Adventure-Component des [prefix]. */
|
||||
val prefixComponent: Component by lazy { MiniMessage.miniMessage().deserialize(prefix) }
|
||||
|
||||
// ── Sub-Tier Logik ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Berechnet den Sub-Tier (1 = höchster, 3 = niedrigster) für diesen Rang.
|
||||
*
|
||||
* Gibt `null` zurück für [UNRANKED] und [SCRIM_MASTER] — sie haben keine Sub-Tiers.
|
||||
*
|
||||
* Algorithmus:
|
||||
* range = nächsterRang.minScore − this.minScore
|
||||
* position = (scrimScore − this.minScore).coerceIn(0, range − 1)
|
||||
* Drittel: position < range/3 → III, < 2*range/3 → II, sonst → I
|
||||
*/
|
||||
fun subTier(scrimScore: Int): Int? {
|
||||
if (this == UNRANKED || this == SCRIM_MASTER) return null
|
||||
|
||||
// Obere Grenze (exklusiv) = minScore des nächsthöheren Rangs
|
||||
val upperBound = entries
|
||||
.filter { it != UNRANKED && it.minScore > this.minScore }
|
||||
.minOfOrNull { it.minScore }
|
||||
?: return null // Sicherheitsnetz — sollte für reguläre Ränge nie eintreten
|
||||
|
||||
val range = upperBound - this.minScore
|
||||
val position = (scrimScore - this.minScore).coerceIn(0, range - 1)
|
||||
|
||||
return when {
|
||||
position < range / 3 -> 3 // Unteres Drittel → III
|
||||
position < 2 * range / 3 -> 2 // Mittleres Drittel → II
|
||||
else -> 1 // Oberes Drittel → I
|
||||
}
|
||||
}
|
||||
|
||||
/** Kurzform: "I", "II", "III" oder `null`. */
|
||||
fun subTierRoman(scrimScore: Int): String? = when (subTier(scrimScore)) {
|
||||
1 -> "I"; 2 -> "II"; 3 -> "III"; else -> null
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Companion
|
||||
// =========================================================================
|
||||
|
||||
companion object {
|
||||
private val mm = MiniMessage.miniMessage()
|
||||
|
||||
/**
|
||||
* Gibt den **sichtbaren** Rang zurück.
|
||||
* Sichtbarer Rang eines Spielers inklusive Placement-Phasen-Check.
|
||||
*
|
||||
* 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).
|
||||
* Spieler mit weniger als [RankingManager.PLACEMENT_GAMES] abgeschlossenen
|
||||
* Spielen erhalten immer [UNRANKED], unabhängig vom internen Score.
|
||||
*/
|
||||
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.
|
||||
* Reines Score → Tier Mapping (ignoriert Placement-Phase).
|
||||
* Wird intern für Rank-Change-Erkennung genutzt.
|
||||
* Iteriert maximal 6 Einträge → O(1) in der Praxis.
|
||||
*/
|
||||
fun fromScore(scrimScore: Int): Rank =
|
||||
entries
|
||||
@@ -74,5 +119,27 @@ enum class Rank(
|
||||
.sortedByDescending { it.minScore }
|
||||
.firstOrNull { scrimScore >= it.minScore }
|
||||
?: BRONZE
|
||||
|
||||
/**
|
||||
* Gibt ein vollständiges [Component] zurück, z. B. "Gold II" oder "Scrim-Master".
|
||||
*
|
||||
* ⚠ Für häufige Aufrufe (Scoreboard) empfiehlt es sich, das Ergebnis
|
||||
* pro Spieler zu cachen und nur bei Score-Änderungen zu invalidieren.
|
||||
*/
|
||||
fun getFormattedRankName(scrimScore: Int, gamesPlayed: Int): Component =
|
||||
mm.deserialize(getFormattedRankTag(scrimScore, gamesPlayed))
|
||||
|
||||
/**
|
||||
* Gibt den MiniMessage-String zurück, geeignet als Platzhalter in
|
||||
* anderen MiniMessage-Templates.
|
||||
*
|
||||
* Beispiele: `"<gold>Gold II<reset>"`, `"<dark_gray>Unranked<reset>"`.
|
||||
*/
|
||||
fun getFormattedRankTag(scrimScore: Int, gamesPlayed: Int): String {
|
||||
val rank = fromPlayer(scrimScore, gamesPlayed)
|
||||
val roman = rank.subTierRoman(scrimScore)
|
||||
return if (roman != null) "${rank.colorTag}${rank.displayName} $roman<reset>"
|
||||
else rank.tag
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user