mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-11 22:48:12 +00:00
update
This commit is contained in:
@@ -51,7 +51,6 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
|
||||
this.game.config().isReplay();
|
||||
this._isVisible = true;
|
||||
this.game.inSpawnPhase();
|
||||
|
||||
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
|
||||
this.spawnBarVisible = e.visible;
|
||||
@@ -64,7 +63,7 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
|
||||
this.eventBus.on(SendWinnerEvent, () => {
|
||||
this.hasWinner = true;
|
||||
this.requestUpdate();
|
||||
this.updateTimer();
|
||||
});
|
||||
|
||||
this.requestUpdate();
|
||||
@@ -75,28 +74,27 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
// Timer logic
|
||||
// Check if the player is the lobby creator
|
||||
if (!this.isLobbyCreator && this.game.myPlayer()?.isLobbyCreator()) {
|
||||
this.isLobbyCreator = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
if (this.hasWinner) return;
|
||||
|
||||
this.updateTimer();
|
||||
}
|
||||
|
||||
private updateTimer() {
|
||||
const maxTimerValue = this.game.config().gameConfig().maxTimerValue;
|
||||
const spawnPhaseTurns = this.game.config().numSpawnPhaseTurns();
|
||||
const ticks = this.game.ticks();
|
||||
const gameTicks = Math.max(0, ticks - spawnPhaseTurns);
|
||||
const elapsedSeconds = Math.floor(gameTicks / 10); // 10 ticks per second
|
||||
const gameTicks = Math.max(0, this.game.ticks() - spawnPhaseTurns);
|
||||
const elapsedSeconds = Math.floor(gameTicks / 10);
|
||||
|
||||
if (this.game.inSpawnPhase()) {
|
||||
this.timer = maxTimerValue !== undefined ? maxTimerValue * 60 : 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasWinner) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxTimerValue !== undefined) {
|
||||
this.timer = Math.max(0, maxTimerValue * 60 - elapsedSeconds);
|
||||
} else {
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
Team,
|
||||
UnitType,
|
||||
} from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { SendWinnerEvent } from "../../Transport";
|
||||
import {
|
||||
formatPercentage,
|
||||
renderNumber,
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
} from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
function formatCrownTime(seconds: number): string {
|
||||
function formatCrownTime(ticks: number): string {
|
||||
const seconds = Math.floor(ticks / 10);
|
||||
if (seconds <= 0) return "0:00";
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
@@ -38,7 +39,7 @@ interface TeamEntry {
|
||||
totalWarShips: string;
|
||||
totalCities: string;
|
||||
totalScoreSort: number;
|
||||
crownSeconds: number;
|
||||
crownTicks: number;
|
||||
players: PlayerView[];
|
||||
}
|
||||
|
||||
@@ -52,20 +53,30 @@ export class TeamStats extends LitElement implements Layer {
|
||||
private _shownOnInit = false;
|
||||
private viewMode: ViewMode = "control";
|
||||
private _myTeam: Team | null = null;
|
||||
/** Crown time in game ticks accumulated per team (client-side tracking). */
|
||||
private _crownTicks: Map<Team, number> = new Map();
|
||||
/** Peak tile count per team (client-side tracking). */
|
||||
private _peakTiles: Map<Team, number> = new Map();
|
||||
/** Last game tick we processed metrics for. */
|
||||
private _lastMetricsTick: number = 0;
|
||||
/** Whether the game has ended (win detected). */
|
||||
private _gameOver: boolean = false;
|
||||
/** Frozen current % per team at game end. */
|
||||
private _frozenCurrentPercent: Map<Team, number> = new Map();
|
||||
/** Frozen peak % per team at game end. */
|
||||
private _frozenPeakPercent: Map<Team, number> = new Map();
|
||||
/** Game tick when game ended (for freezing elapsed time). */
|
||||
private _endTick: number | null = null;
|
||||
/** All teams we've ever seen (so eliminated teams still appear). */
|
||||
private _knownTeams: Set<Team> = new Set();
|
||||
|
||||
createRenderRoot() {
|
||||
return this; // use light DOM for Tailwind
|
||||
}
|
||||
|
||||
init() {}
|
||||
init() {
|
||||
this.eventBus.on(SendWinnerEvent, () => {
|
||||
this._gameOver = true;
|
||||
this._endTick = this.game.ticks();
|
||||
this.freezeScores();
|
||||
});
|
||||
}
|
||||
|
||||
getTickIntervalMs() {
|
||||
return 100;
|
||||
@@ -76,7 +87,10 @@ export class TeamStats extends LitElement implements Layer {
|
||||
|
||||
if (!this._shownOnInit && !this.game.inSpawnPhase()) {
|
||||
this._shownOnInit = true;
|
||||
this._lastMetricsTick = this.game.ticks();
|
||||
if (this.game.config().gameConfig().competitiveScoring) {
|
||||
this.viewMode = "competitive";
|
||||
this.visible = true;
|
||||
}
|
||||
this.updateTeamStats();
|
||||
}
|
||||
|
||||
@@ -91,90 +105,39 @@ export class TeamStats extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
private trackMetrics() {
|
||||
const currentTick = this.game.ticks();
|
||||
const tickDelta = currentTick - this._lastMetricsTick;
|
||||
this._lastMetricsTick = currentTick;
|
||||
if (tickDelta <= 0) return;
|
||||
|
||||
const players = this.game.playerViews();
|
||||
const teamToTiles = new Map<Team, number>();
|
||||
for (const player of players) {
|
||||
for (const player of this.game.playerViews()) {
|
||||
const team = player.team();
|
||||
if (team === null || team === ColoredTeams.Bot) continue;
|
||||
this._knownTeams.add(team);
|
||||
teamToTiles.set(
|
||||
team,
|
||||
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
||||
);
|
||||
}
|
||||
|
||||
const hasWinUpdate = this.hasWinUpdate();
|
||||
const winConditionMet = this.isTeamWinConditionMet(
|
||||
teamToTiles,
|
||||
currentTick,
|
||||
);
|
||||
|
||||
// Fallback for missed WinUpdate polling: stop immediately once we detect
|
||||
// the board already satisfies the team win condition.
|
||||
if (!hasWinUpdate && winConditionMet) {
|
||||
this._gameOver = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Track peak tiles
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
const prev = this._peakTiles.get(team) ?? 0;
|
||||
if (tiles > prev) {
|
||||
this._peakTiles.set(team, tiles);
|
||||
}
|
||||
}
|
||||
|
||||
// Track crown time (in game ticks)
|
||||
let maxTiles = 0;
|
||||
let crownTeam: Team | null = null;
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
if (tiles > maxTiles) {
|
||||
maxTiles = tiles;
|
||||
crownTeam = team;
|
||||
}
|
||||
}
|
||||
if (crownTeam !== null && maxTiles > 0) {
|
||||
this._crownTicks.set(
|
||||
crownTeam,
|
||||
(this._crownTicks.get(crownTeam) ?? 0) + tickDelta,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasWinUpdate || winConditionMet) {
|
||||
this._gameOver = true;
|
||||
}
|
||||
}
|
||||
|
||||
private hasWinUpdate(): boolean {
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
const winUpdates = updates !== null ? updates[GameUpdateType.Win] : [];
|
||||
return winUpdates.length > 0;
|
||||
}
|
||||
|
||||
private isTeamWinConditionMet(
|
||||
teamToTiles: Map<Team, number>,
|
||||
currentTick: number,
|
||||
): boolean {
|
||||
private freezeScores() {
|
||||
const numTilesWithoutFallout =
|
||||
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
||||
if (numTilesWithoutFallout <= 0 || teamToTiles.size === 0) return false;
|
||||
|
||||
const maxTiles = Math.max(...Array.from(teamToTiles.values()));
|
||||
const percentage = (maxTiles / numTilesWithoutFallout) * 100;
|
||||
const territoryWin =
|
||||
percentage > this.game.config().percentageTilesOwnedToWin();
|
||||
|
||||
const maxTimer = this.game.config().gameConfig().maxTimerValue;
|
||||
const timeElapsedSeconds =
|
||||
(currentTick - this.game.config().numSpawnPhaseTurns()) / 10;
|
||||
const timerWin =
|
||||
maxTimer !== undefined && timeElapsedSeconds - maxTimer * 60 >= 0;
|
||||
|
||||
return territoryWin || timerWin;
|
||||
for (const player of this.game.playerViews()) {
|
||||
const team = player.team();
|
||||
if (team === null || team === ColoredTeams.Bot) continue;
|
||||
this._frozenCurrentPercent.set(
|
||||
team,
|
||||
(this._frozenCurrentPercent.get(team) ?? 0) +
|
||||
player.numTilesOwned() / numTilesWithoutFallout,
|
||||
);
|
||||
}
|
||||
for (const [team, peakTiles] of this._peakTiles) {
|
||||
this._frozenPeakPercent.set(team, peakTiles / numTilesWithoutFallout);
|
||||
}
|
||||
}
|
||||
|
||||
private updateTeamStats() {
|
||||
@@ -193,9 +156,54 @@ export class TeamStats extends LitElement implements Layer {
|
||||
grouped[team].push(player);
|
||||
}
|
||||
|
||||
// In competitive view, ensure eliminated teams still appear
|
||||
if (this.viewMode === "competitive") {
|
||||
for (const team of this._knownTeams) {
|
||||
if (!(team in grouped)) {
|
||||
grouped[team] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const numTilesWithoutFallout =
|
||||
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
||||
|
||||
// Read per-team crown ticks from the server (authoritative source).
|
||||
// Normalize so displayed seconds sum to the sidebar elapsed seconds.
|
||||
const currentTick = this._endTick ?? this.game.ticks();
|
||||
const elapsedGameTicks = Math.max(
|
||||
0,
|
||||
currentTick - this.game.config().numSpawnPhaseTurns(),
|
||||
);
|
||||
const elapsedSeconds = Math.floor(elapsedGameTicks / 10);
|
||||
const serverCrownTicks = this.game.teamCrownTicks() ?? {};
|
||||
// Crown holder = team with most tiles (same logic as WinCheckExecution)
|
||||
let crownHolder: string | null = null;
|
||||
let maxTilesNow = 0;
|
||||
for (const [team, players] of Object.entries(grouped)) {
|
||||
const teamTotal = players.reduce((sum, p) => sum + p.numTilesOwned(), 0);
|
||||
if (teamTotal > maxTilesNow) {
|
||||
maxTilesNow = teamTotal;
|
||||
crownHolder = team;
|
||||
}
|
||||
}
|
||||
// Non-holder teams get floor(ticks/10), holder gets remainder
|
||||
const normalizedCrownTicks = new Map<string, number>();
|
||||
let othersSeconds = 0;
|
||||
for (const [team, ticks] of Object.entries(serverCrownTicks)) {
|
||||
if (team !== crownHolder) {
|
||||
const secs = Math.floor(ticks / 10);
|
||||
normalizedCrownTicks.set(team, secs * 10);
|
||||
othersSeconds += secs;
|
||||
}
|
||||
}
|
||||
if (crownHolder !== null) {
|
||||
normalizedCrownTicks.set(
|
||||
crownHolder,
|
||||
Math.max(0, elapsedSeconds - othersSeconds) * 10,
|
||||
);
|
||||
}
|
||||
|
||||
this.teams = Object.entries(grouped)
|
||||
.map(([teamStr, teamPlayers]) => {
|
||||
let totalGold = 0n;
|
||||
@@ -218,9 +226,14 @@ export class TeamStats extends LitElement implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
const totalScorePercent = totalScoreSort / numTilesWithoutFallout;
|
||||
const peakTiles = this._peakTiles.get(teamStr) ?? 0;
|
||||
const peakPercent = peakTiles / numTilesWithoutFallout;
|
||||
const totalScorePercent = this._gameOver
|
||||
? (this._frozenCurrentPercent.get(teamStr) ??
|
||||
totalScoreSort / numTilesWithoutFallout)
|
||||
: totalScoreSort / numTilesWithoutFallout;
|
||||
const rawPeakPercent = this._gameOver
|
||||
? (this._frozenPeakPercent.get(teamStr) ?? 0)
|
||||
: (this._peakTiles.get(teamStr) ?? 0) / numTilesWithoutFallout;
|
||||
const peakPercent = Math.max(rawPeakPercent, totalScorePercent);
|
||||
|
||||
return {
|
||||
teamName: teamStr,
|
||||
@@ -231,7 +244,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
totalGold: renderNumber(totalGold),
|
||||
totalMaxTroops: renderTroops(totalMaxTroops),
|
||||
players: teamPlayers,
|
||||
crownSeconds: Math.floor((this._crownTicks.get(teamStr) ?? 0) / 10),
|
||||
crownTicks: normalizedCrownTicks.get(teamStr) ?? 0,
|
||||
|
||||
totalLaunchers: renderNumber(totalLaunchers),
|
||||
totalSAMs: renderNumber(totalSAMs),
|
||||
@@ -272,7 +285,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
const cell = (text: string, title?: string) => html`
|
||||
<div
|
||||
class="p-1.5 md:p-2.5 text-center border-b border-slate-500"
|
||||
title=${title ?? ""}
|
||||
title="${title ?? ""}"
|
||||
>
|
||||
${text}
|
||||
</div>
|
||||
@@ -296,14 +309,10 @@ export class TeamStats extends LitElement implements Layer {
|
||||
`;
|
||||
case "competitive":
|
||||
return html`
|
||||
${cell(translateText("leaderboard.team"))} ${cell("Current %")}
|
||||
${cell("Peak %")}
|
||||
<div
|
||||
class="p-1.5 md:p-2.5 text-center border-b border-slate-500"
|
||||
title="Crown time (time holding most territory)"
|
||||
>
|
||||
👑
|
||||
</div>
|
||||
${cell(translateText("leaderboard.team"))}
|
||||
${cell("Current %", "Percentage of land currently controlled")}
|
||||
${cell("Peak %", "Highest percentage of land ever controlled")}
|
||||
${cell("👑", "Total time spent holding the most territory")}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -333,7 +342,7 @@ export class TeamStats extends LitElement implements Layer {
|
||||
return html`
|
||||
<div class="${rowClass}">
|
||||
${td(team.teamName)} ${td(team.totalScoreStr)}
|
||||
${td(team.peakScoreStr)} ${td(formatCrownTime(team.crownSeconds))}
|
||||
${td(team.peakScoreStr)} ${td(formatCrownTime(team.crownTicks))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,43 @@ export class WinModal extends LitElement implements Layer {
|
||||
`;
|
||||
}
|
||||
|
||||
private formatTime(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${m}m${String(s).padStart(2, "0")}s`;
|
||||
}
|
||||
|
||||
private _tooltip: HTMLDivElement | null = null;
|
||||
|
||||
private _showTooltip(e: MouseEvent, text: string) {
|
||||
if (!this._tooltip) {
|
||||
this._tooltip = document.createElement("div");
|
||||
this._tooltip.className =
|
||||
"fixed bg-black text-white text-xs rounded px-2 py-1.5 z-[99999] max-w-[250px] pointer-events-none";
|
||||
document.body.appendChild(this._tooltip);
|
||||
}
|
||||
this._tooltip.textContent = text;
|
||||
this._tooltip.style.display = "block";
|
||||
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
||||
this._tooltip.style.left = `${rect.left + rect.width / 2 - 125}px`;
|
||||
this._tooltip.style.top = `${rect.top - this._tooltip.offsetHeight - 4}px`;
|
||||
}
|
||||
|
||||
private _hideTooltip() {
|
||||
if (this._tooltip) {
|
||||
this._tooltip.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
private renderInfoIcon(tooltip: string) {
|
||||
return html`<span
|
||||
class="inline-flex items-center justify-center w-4 h-4 rounded-full bg-slate-600 text-slate-300 text-[10px] font-bold leading-none cursor-help shrink-0 ml-0.5"
|
||||
@mouseenter=${(e: MouseEvent) => this._showTooltip(e, tooltip)}
|
||||
@mouseleave=${() => this._hideTooltip()}
|
||||
>?</span
|
||||
>`;
|
||||
}
|
||||
|
||||
private renderCompetitiveScores() {
|
||||
if (!this.competitiveScores) return html``;
|
||||
return html`
|
||||
@@ -120,9 +157,24 @@ export class WinModal extends LitElement implements Layer {
|
||||
<tr class="text-slate-300 border-b border-slate-600">
|
||||
<th class="py-1.5 px-1">#</th>
|
||||
<th class="py-1.5 px-1 text-left">Team</th>
|
||||
<th class="py-1.5 px-1">Tiles</th>
|
||||
<th class="py-1.5 px-1">Crown</th>
|
||||
<th class="py-1.5 px-1">Place</th>
|
||||
<th class="py-1.5 px-1">
|
||||
Tiles
|
||||
${this.renderInfoIcon(
|
||||
"Points for peak territory % (1st: 60, 2nd: 54, 3rd: 48...)",
|
||||
)}
|
||||
</th>
|
||||
<th class="py-1.5 px-1">
|
||||
Crown
|
||||
${this.renderInfoIcon(
|
||||
"Points for time holding the most territory (1st: 30, 2nd: 27, 3rd: 24...)",
|
||||
)}
|
||||
</th>
|
||||
<th class="py-1.5 px-1">
|
||||
Place
|
||||
${this.renderInfoIcon(
|
||||
"Points for survival placement (1st: 10, 2nd: 8, 3rd: 6...)",
|
||||
)}
|
||||
</th>
|
||||
<th class="py-1.5 px-1 font-bold">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -136,9 +188,24 @@ export class WinModal extends LitElement implements Layer {
|
||||
>
|
||||
<td class="py-1.5 px-1">${i + 1}</td>
|
||||
<td class="py-1.5 px-1 text-left">${s.team}</td>
|
||||
<td class="py-1.5 px-1">${s.maxTilesPoints}</td>
|
||||
<td class="py-1.5 px-1">${s.crownTimePoints}</td>
|
||||
<td class="py-1.5 px-1">${s.placementPoints}</td>
|
||||
<td class="py-1.5 px-1">
|
||||
${s.maxTilesPoints}
|
||||
${this.renderInfoIcon(
|
||||
`${s.team} ranked #${s.maxTilesRank} in peak territory (${s.peakTilePercentage.toFixed(1)}%)`,
|
||||
)}
|
||||
</td>
|
||||
<td class="py-1.5 px-1">
|
||||
${s.crownTimePoints}
|
||||
${this.renderInfoIcon(
|
||||
`${s.team} ranked #${s.crownTimeRank} in crown time (${this.formatTime(s.crownTimeSeconds)})`,
|
||||
)}
|
||||
</td>
|
||||
<td class="py-1.5 px-1">
|
||||
${s.placementPoints}
|
||||
${this.renderInfoIcon(
|
||||
`${s.team} placed #${s.placementRank} in survival`,
|
||||
)}
|
||||
</td>
|
||||
<td class="py-1.5 px-1 font-bold text-yellow-300">
|
||||
${s.totalScore}
|
||||
</td>
|
||||
|
||||
@@ -174,12 +174,20 @@ export class GameRunner {
|
||||
const packedTileUpdates = this.game.drainPackedTileUpdates();
|
||||
const packedMotionPlans = this.game.drainPackedMotionPlans();
|
||||
|
||||
// Build per-team crown ticks for competitive mode
|
||||
const crownTicks = this.game.allTeamCrownTicks();
|
||||
const teamCrownTicks: Record<string, number> | undefined =
|
||||
crownTicks.size > 0
|
||||
? Object.fromEntries(crownTicks.entries())
|
||||
: undefined;
|
||||
|
||||
this.callBack({
|
||||
tick: this.game.ticks(),
|
||||
packedTileUpdates,
|
||||
...(packedMotionPlans ? { packedMotionPlans } : {}),
|
||||
updates: updates,
|
||||
playerNameViewData: this.playerViewData,
|
||||
teamCrownTicks,
|
||||
tickExecutionDuration: tickExecutionDuration,
|
||||
pendingTurns: pendingTurns ?? 0,
|
||||
});
|
||||
|
||||
@@ -109,7 +109,7 @@ export class WinCheckExecution implements Execution {
|
||||
checkWinnerTeam(): void {
|
||||
if (this.mg === null) throw new Error("Not initialized");
|
||||
const teamToTiles = new Map<Team, number>();
|
||||
for (const player of this.mg.players()) {
|
||||
for (const player of this.mg.allPlayers()) {
|
||||
const team = player.team();
|
||||
// Sanity check, team should not be null here
|
||||
if (team === null) continue;
|
||||
@@ -167,10 +167,39 @@ export class WinCheckExecution implements Execution {
|
||||
(a, b) => (teamToTiles.get(b) ?? 0) - (teamToTiles.get(a) ?? 0),
|
||||
);
|
||||
|
||||
// Normalize crown seconds: non-crown teams get floor(ticks/10),
|
||||
// crown holder gets the remainder so the sum matches the game timer.
|
||||
const elapsedSeconds = Math.floor(Math.max(0, totalGameTicks) / 10);
|
||||
let crownHolder: Team | null = null;
|
||||
let maxTiles = 0;
|
||||
for (const [team, tiles] of teamToTiles) {
|
||||
if (team !== ColoredTeams.Bot && tiles > maxTiles) {
|
||||
maxTiles = tiles;
|
||||
crownHolder = team;
|
||||
}
|
||||
}
|
||||
const allCrownTicks = this.mg!.allTeamCrownTicks();
|
||||
const teamCrownSeconds = new Map<Team, number>();
|
||||
let othersSum = 0;
|
||||
for (const team of allTeams) {
|
||||
const ticks = allCrownTicks.get(team) ?? 0;
|
||||
if (team !== crownHolder) {
|
||||
const secs = Math.floor(ticks / 10);
|
||||
teamCrownSeconds.set(team, secs);
|
||||
othersSum += secs;
|
||||
}
|
||||
}
|
||||
if (crownHolder !== null) {
|
||||
teamCrownSeconds.set(
|
||||
crownHolder,
|
||||
Math.max(0, elapsedSeconds - othersSum),
|
||||
);
|
||||
}
|
||||
|
||||
const metrics: TeamRawMetrics[] = allTeams.map((team) => {
|
||||
const peakTiles = this.mg!.teamPeakTiles(team);
|
||||
const peakTilePercentage = (peakTiles / numTilesWithoutFallout) * 100;
|
||||
const crownTicks = this.mg!.teamCrownTicks(team);
|
||||
const crownTicks = allCrownTicks.get(team) ?? 0;
|
||||
const crownRatio = totalGameTicks > 0 ? crownTicks / totalGameTicks : 0;
|
||||
|
||||
const elimIndex = eliminationOrder.indexOf(team);
|
||||
@@ -185,7 +214,14 @@ export class WinCheckExecution implements Execution {
|
||||
placementRank = elimIndex + 1;
|
||||
}
|
||||
|
||||
return { team, peakTilePercentage, crownRatio, placementRank };
|
||||
const crownTimeSeconds = teamCrownSeconds.get(team) ?? 0;
|
||||
return {
|
||||
team,
|
||||
peakTilePercentage,
|
||||
crownRatio,
|
||||
crownTimeSeconds,
|
||||
placementRank,
|
||||
};
|
||||
});
|
||||
|
||||
return computeCompetitiveScores(metrics);
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface TeamRawMetrics {
|
||||
team: Team;
|
||||
peakTilePercentage: number;
|
||||
crownRatio: number;
|
||||
crownTimeSeconds: number;
|
||||
placementRank: number;
|
||||
}
|
||||
|
||||
@@ -11,8 +12,10 @@ export interface TeamScoreBreakdown {
|
||||
team: Team;
|
||||
maxTilesRank: number;
|
||||
maxTilesPoints: number;
|
||||
peakTilePercentage: number;
|
||||
crownTimeRank: number;
|
||||
crownTimePoints: number;
|
||||
crownTimeSeconds: number;
|
||||
placementRank: number;
|
||||
placementPoints: number;
|
||||
totalScore: number;
|
||||
@@ -74,8 +77,10 @@ export function computeCompetitiveScores(
|
||||
team: m.team,
|
||||
maxTilesRank,
|
||||
maxTilesPoints,
|
||||
peakTilePercentage: m.peakTilePercentage,
|
||||
crownTimeRank,
|
||||
crownTimePoints,
|
||||
crownTimeSeconds: m.crownTimeSeconds,
|
||||
placementRank,
|
||||
placementPoints,
|
||||
totalScore: maxTilesPoints + crownTimePoints + placementPoints,
|
||||
|
||||
@@ -862,7 +862,7 @@ export interface Game extends GameMap {
|
||||
|
||||
// Crown tracking (team-based)
|
||||
crownTeam(): Team | null;
|
||||
teamCrownTicks(team: Team): number;
|
||||
allTeamCrownTicks(): ReadonlyMap<Team, number>;
|
||||
addCrownTick(team: Team, amount: number): void;
|
||||
|
||||
// Peak tile tracking (team-based)
|
||||
|
||||
@@ -1276,8 +1276,8 @@ export class GameImpl implements Game {
|
||||
return crown;
|
||||
}
|
||||
|
||||
teamCrownTicks(team: Team): number {
|
||||
return this._teamCrownTicks.get(team) ?? 0;
|
||||
allTeamCrownTicks(): ReadonlyMap<Team, number> {
|
||||
return this._teamCrownTicks;
|
||||
}
|
||||
|
||||
addCrownTick(team: Team, amount: number): void {
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface GameUpdateViewData {
|
||||
*/
|
||||
packedMotionPlans?: Uint32Array;
|
||||
playerNameViewData: Record<string, NameViewData>;
|
||||
/** Per-team crown ticks from the server (authoritative). */
|
||||
teamCrownTicks?: Record<string, number>;
|
||||
tickExecutionDuration?: number;
|
||||
pendingTurns?: number;
|
||||
}
|
||||
|
||||
@@ -674,6 +674,11 @@ export class GameView implements GameMap {
|
||||
return this.lastUpdate?.updates ?? null;
|
||||
}
|
||||
|
||||
/** Per-team crown ticks from the server (authoritative). */
|
||||
public teamCrownTicks(): Record<string, number> | undefined {
|
||||
return this.lastUpdate?.teamCrownTicks;
|
||||
}
|
||||
|
||||
public motionPlans(): ReadonlyMap<
|
||||
number,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user