diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts index c011d421f..ab66b3d10 100644 --- a/src/client/graphics/layers/TeamStats.ts +++ b/src/client/graphics/layers/TeamStats.ts @@ -32,7 +32,7 @@ interface TeamEntry { totalWarShips: string; totalCities: string; totalScoreSort: number; - crownTicks: number; + crownSeconds: number; players: PlayerView[]; } @@ -168,7 +168,11 @@ export class TeamStats extends LitElement implements Layer { 0, currentTick - this.game.config().numSpawnPhaseTurns(), ); - const elapsedSeconds = Math.floor(elapsedGameTicks / 10); + const maxTimerValue = this.game.config().gameConfig().maxTimerValue; + const elapsedSeconds = + maxTimerValue !== undefined + ? Math.min(Math.floor(elapsedGameTicks / 10), maxTimerValue * 60) + : Math.floor(elapsedGameTicks / 10); const serverCrownTicks = this.game.teamCrownTicks() ?? {}; // Crown holder = team with most tiles (same logic as WinCheckExecution) let crownHolder: string | null = null; @@ -237,7 +241,11 @@ export class TeamStats extends LitElement implements Layer { totalGold: renderNumber(totalGold), totalMaxTroops: renderTroops(totalMaxTroops), players: teamPlayers, - crownTicks: normalizedCrownTicks.get(teamStr) ?? 0, + crownSeconds: + this._gameOver && this.game.competitiveScores() + ? (this.game.competitiveScores()!.find((s) => s.team === teamStr) + ?.crownTimeSeconds ?? 0) + : Math.floor((normalizedCrownTicks.get(teamStr) ?? 0) / 10), totalLaunchers: renderNumber(totalLaunchers), totalSAMs: renderNumber(totalSAMs), @@ -335,8 +343,7 @@ export class TeamStats extends LitElement implements Layer { return html`
${td(team.teamName)} ${td(team.totalScoreStr)} - ${td(team.peakScoreStr)} - ${td(secondsToHms(Math.floor(team.crownTicks / 10)))} + ${td(team.peakScoreStr)} ${td(secondsToHms(team.crownSeconds))}
`; } diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index a82b9afc0..f86d824f5 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -9,7 +9,6 @@ import { } from "../../../client/Utils"; import { ColorPalette, Pattern } from "../../../core/CosmeticSchemas"; import { EventBus } from "../../../core/EventBus"; -import { TeamScoreBreakdown } from "../../../core/game/CompetitiveScoring"; import { RankedType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView } from "../../../core/game/GameView"; @@ -46,8 +45,9 @@ export class WinModal extends LitElement implements Layer { @state() private patternContent: TemplateResult | null = null; - @state() - private competitiveScores: TeamScoreBreakdown[] | null = null; + private get competitiveScores() { + return this.game.competitiveScores(); + } private _title: string; @@ -409,9 +409,6 @@ export class WinModal extends LitElement implements Layer { // ... } else if (wu.winner[0] === "team") { this.eventBus.emit(new SendWinnerEvent(wu.winner, wu.allPlayersStats)); - if (wu.competitiveScores) { - this.competitiveScores = wu.competitiveScores; - } if (wu.winner[1] === this.game.myPlayer()?.team()) { this._title = translateText("win_modal.your_team"); this.isWin = true; diff --git a/src/core/execution/TeamMetricsExecution.ts b/src/core/execution/TeamMetricsExecution.ts index 33ddedb93..aa25362bc 100644 --- a/src/core/execution/TeamMetricsExecution.ts +++ b/src/core/execution/TeamMetricsExecution.ts @@ -22,6 +22,14 @@ export class TeamMetricsExecution implements Execution { if (ticks % 10 !== 0) return; if (this.mg === null) throw new Error("Not initialized"); + // Stop tracking after the game timer expires. + const maxTimerValue = this.mg.config().gameConfig().maxTimerValue; + if (maxTimerValue !== undefined) { + const elapsedSeconds = + (ticks - this.mg.config().numSpawnPhaseTurns()) / 10; + if (elapsedSeconds >= maxTimerValue * 60) return; + } + const teamToTiles = new Map(); for (const player of this.mg.players()) { const team = player.team(); diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 7c98da8a6..075e57479 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -6,6 +6,7 @@ import { PatternDecoder } from "../PatternDecoder"; import { ClientID, GameID, Player, PlayerCosmetics } from "../Schemas"; import { createRandomName } from "../Util"; import { WorkerClient } from "../worker/WorkerClient"; +import { TeamScoreBreakdown } from "./CompetitiveScoring"; import { Cell, EmojiMessage, @@ -33,6 +34,7 @@ import { GameUpdateViewData, PlayerUpdate, UnitUpdate, + WinUpdate, } from "./GameUpdates"; import { MotionPlanRecord, unpackMotionPlans } from "./MotionPlans"; import { TerrainMapData } from "./TerrainMapLoader"; @@ -631,6 +633,7 @@ export class GameView implements GameMap { private trainUnitToEngine = new Map(); private toDelete = new Set(); + private _competitiveScores: TeamScoreBreakdown[] | null = null; private _cosmetics: Map = new Map(); @@ -679,6 +682,11 @@ export class GameView implements GameMap { return this.lastUpdate?.teamCrownTicks; } + /** Competitive scores set once at game end (authoritative). */ + public competitiveScores(): TeamScoreBreakdown[] | null { + return this._competitiveScores; + } + public motionPlans(): ReadonlyMap< number, { @@ -803,6 +811,17 @@ export class GameView implements GameMap { } }); + // Capture competitive scores from WinUpdate (once) + if (this._competitiveScores === null && gu.updates) { + const winUpdates = gu.updates[GameUpdateType.Win] as WinUpdate[]; + for (const wu of winUpdates) { + if (wu.competitiveScores) { + this._competitiveScores = wu.competitiveScores; + break; + } + } + } + this.advanceMotionPlannedUnits(gu.tick); this.rebuildMotionPlannedUnitIdsCacheIfDirty(); }