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.
145 lines
5.7 KiB
Kotlin
145 lines
5.7 KiB
Kotlin
package club.mcscrims.speedhg.ranking
|
||
|
||
import net.kyori.adventure.text.Component
|
||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||
import org.bukkit.Color
|
||
|
||
/**
|
||
* Rang-Tier System für SpeedHG.
|
||
*
|
||
* ## 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.
|
||
*
|
||
* 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 fireworkColor: Color
|
||
) {
|
||
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 );
|
||
|
||
// ── Vorberechnete Strings & Components (einmal pro Rang, nie pro Frame) ──
|
||
|
||
/** MiniMessage-Tag: "<gold>Gold<reset>" */
|
||
val tag: String = "$colorTag$displayName<reset>"
|
||
|
||
/** Klammer-Prefix: "<gold>[Gold]<reset>" */
|
||
val prefix: String = "$colorTag[$displayName]<reset>"
|
||
|
||
/**
|
||
* 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]. */
|
||
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()
|
||
|
||
/**
|
||
* Sichtbarer Rang eines Spielers inklusive Placement-Phasen-Check.
|
||
*
|
||
* 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 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
|
||
.asSequence()
|
||
.filter { it != UNRANKED }
|
||
.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
|
||
}
|
||
}
|
||
} |