feat(livestats): per-player killedBy + deathPosition + winner for live standings (#4593)

Enriches the admin-bot live-stats snapshot
(`/api/adminbot/game/:id/stats`) with the per-player fields a semi-live
standings board needs, so it can score kills + placement live instead of
waiting for the post-game record.

Per player in `liveStats.players[]`:
- **`killedBy`** — the eliminator's clientID (null while alive /
non-client killer). Invert → live kill points.
- **`deathPosition`** — finishing place at elimination = non-bot players
still standing + 1 (frozen at "last of the then-standing"). Null while
alive → live placement.

The **full roster** is reported (dead players are retained in the client
view), plus top-level **`winner`** (the decided winner's clientID, else
null).

Deterministic / consensus-safe: `killedBy` (stamped at `recordKill`) and
`deathPosition` (stamped in `PlayerExecution`) are pure sim values, so
in-sync clients agree on the majority vote; `winner` is added
server-side in `GameServer.liveStats()` from the winner vote (like the
existing publicID/username/connected enrichment).

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
This commit is contained in:
Zixer1
2026-07-16 10:39:55 -07:00
committed by evanpelle
parent 0a2a85423c
commit 92a0801298
18 changed files with 179 additions and 2 deletions
+7
View File
@@ -298,6 +298,11 @@ export class PlayerImpl implements Player {
}
}
// OFM live standings: elimination info is stored on the player's stats
// (set live in the sim via mg.stats()), surfaced here so it rides the live
// PlayerUpdate every tick rather than only appearing in the game-end record.
const deathStats = this.mg.stats().getPlayerStats(this);
return {
type: GameUpdateType.Player,
clientID: this.clientID(),
@@ -309,6 +314,8 @@ export class PlayerImpl implements Player {
playerType: this.type(),
isAlive: this.isAlive(),
isDisconnected: this.isDisconnected(),
killedBy: deathStats?.killedBy ?? null,
deathPosition: deathStats?.deathPosition ?? null,
tilesOwned: this.numTilesOwned(),
gold: this._gold,
troops: this.troops(),