mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-28 14:14:15 +00:00
make computePlayerStatus live-aware so status icons render
The replay-path computePlayerStatus left alliance/target/embargo/ nukeTargetsMe at false, which meant the WebGL NamePass had no data for those status icons after we switched names off canvas2D — they just stopped appearing. Add an opts param taking localPlayerID + tileState. When localPlayerID is set, fill the relative flags by checking the local player's allies/targets/embargoes against each other player's smallID; embargo is bilateral (either side). nukeTargetsMe walks active nukes and checks their targetTile's owner via the tileState buffer. Plumb localPlayerID = myPlayer?.smallID() and tileState from populateFrame so the live path uses the new mode. Emit an entry when only a relative flag is true (previously could be dropped if no base flag was set). allianceReq and allianceFraction stay deferred (need local PlayerID string for outgoing requests and current tick for fraction). 18 new tests covering both modes — replay (relative flags forced false), and live (alliance one-way, target one-way, embargo bilateral, self-flags suppressed, nukeTargetsMe with/without tileState, relative-flag-alone emits, localPlayerID=0 falls back to replay, allianceReq/allianceFraction stay deferred).
This commit is contained in:
@@ -7,29 +7,70 @@ const NUKE_ACTIVE_TYPES: ReadonlySet<string> = new Set([
|
||||
UT_MIRV_WARHEAD,
|
||||
]);
|
||||
|
||||
const OWNER_MASK = 0xfff;
|
||||
|
||||
export interface ComputePlayerStatusOptions {
|
||||
/**
|
||||
* Local player smallID for computing relative flags. Omit (or set to 0)
|
||||
* for replay mode — relative flags will all be false.
|
||||
*/
|
||||
localPlayerID?: number;
|
||||
/**
|
||||
* Tile state buffer (the same Uint16Array exposed via FrameData.tileState).
|
||||
* Used to determine if a nuke's target tile is owned by the local player
|
||||
* for the `nukeTargetsMe` flag. If omitted, `nukeTargetsMe` stays false.
|
||||
*/
|
||||
tileState?: Uint16Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute per-player status flags for the name/status-icon pass.
|
||||
*
|
||||
* This is the replay-path version — no local player concept.
|
||||
* All relative flags (alliance, allianceReq, target, embargo, nukeTargetsMe)
|
||||
* are always false. The live path uses the shim's own computePlayerStatus
|
||||
* which has local-player awareness.
|
||||
* Without `opts.localPlayerID`: replay-path mode. Crown/traitor/disconnected/
|
||||
* nukeActive are populated; relative flags (alliance/target/embargo/
|
||||
* nukeTargetsMe) are all false.
|
||||
*
|
||||
* With `opts.localPlayerID`: live mode. Relative flags compare each player
|
||||
* against the local player's state to determine alliance/target/embargo;
|
||||
* if `opts.tileState` is also given, `nukeTargetsMe` is set for players
|
||||
* whose in-flight nuke is targeting one of the local player's tiles.
|
||||
*
|
||||
* `allianceReq` and `allianceFraction` are not computed yet — they need
|
||||
* additional context (the local player's PlayerID string for outgoing
|
||||
* requests, and the current tick for fraction). Left as `false`/`0` until
|
||||
* those use cases need them.
|
||||
*/
|
||||
export function computePlayerStatus(
|
||||
players: ReadonlyMap<number, PlayerState>,
|
||||
units: ReadonlyMap<number, UnitState>,
|
||||
opts: ComputePlayerStatusOptions = {},
|
||||
): Map<number, PlayerStatusData> {
|
||||
const result = new Map<number, PlayerStatusData>();
|
||||
const localPlayerID = opts.localPlayerID ?? 0;
|
||||
const tileState = opts.tileState;
|
||||
const localPlayer =
|
||||
localPlayerID > 0 ? players.get(localPlayerID) : undefined;
|
||||
|
||||
// Nuke owners: players who have an active nuke in flight
|
||||
// Nuke owners: players who have an active nuke in flight.
|
||||
// Also collect which of those nukes target a tile owned by the local player.
|
||||
const nukeOwners = new Set<number>();
|
||||
const nukeAimedAtMe = new Set<number>();
|
||||
for (const u of units.values()) {
|
||||
if (u.isActive && NUKE_ACTIVE_TYPES.has(u.unitType)) {
|
||||
nukeOwners.add(u.ownerID);
|
||||
if (!u.isActive || !NUKE_ACTIVE_TYPES.has(u.unitType)) continue;
|
||||
nukeOwners.add(u.ownerID);
|
||||
if (
|
||||
localPlayer !== undefined &&
|
||||
tileState !== undefined &&
|
||||
u.targetTile !== null
|
||||
) {
|
||||
const tileOwner = tileState[u.targetTile] & OWNER_MASK;
|
||||
if (tileOwner === localPlayerID) {
|
||||
nukeAimedAtMe.add(u.ownerID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Crown: alive player with most tiles
|
||||
// Crown: alive player with most tiles owned.
|
||||
let crownSmallID = -1;
|
||||
let maxTiles = 0;
|
||||
for (const ps of players.values()) {
|
||||
@@ -40,31 +81,57 @@ export function computePlayerStatus(
|
||||
}
|
||||
}
|
||||
|
||||
// Relative-flag sets seeded from the local player's state. Looking them
|
||||
// up once outside the per-player loop is O(1) per player rather than O(n)
|
||||
// per .includes(); doesn't matter at small scale but keeps the loop tidy.
|
||||
const allySet = localPlayer ? new Set(localPlayer.allies) : null;
|
||||
const targetSet = localPlayer ? new Set(localPlayer.targets) : null;
|
||||
const myEmbargoes = localPlayer ? new Set(localPlayer.embargoes) : null;
|
||||
|
||||
for (const ps of players.values()) {
|
||||
if (!ps.isAlive) continue;
|
||||
const crown = ps.smallID === crownSmallID;
|
||||
const sid = ps.smallID;
|
||||
const crown = sid === crownSmallID;
|
||||
const traitor = ps.isTraitor;
|
||||
const disconnected = ps.isDisconnected;
|
||||
const traitorRemainingTicks = ps.traitorRemainingTicks;
|
||||
const nukeActive = nukeOwners.has(ps.smallID);
|
||||
const nukeActive = nukeOwners.has(sid);
|
||||
|
||||
// Relative flags — only meaningful when there's a local player AND we're
|
||||
// not looking at the local player itself.
|
||||
let alliance = false;
|
||||
let target = false;
|
||||
let embargo = false;
|
||||
let nukeTargetsMe = false;
|
||||
if (localPlayer !== undefined && sid !== localPlayerID) {
|
||||
alliance = allySet!.has(sid);
|
||||
target = targetSet!.has(sid);
|
||||
// Embargo is bilateral: either side embargoes the other.
|
||||
embargo = myEmbargoes!.has(sid) || ps.embargoes.includes(localPlayerID);
|
||||
nukeTargetsMe = nukeAimedAtMe.has(sid);
|
||||
}
|
||||
|
||||
if (
|
||||
crown ||
|
||||
traitor ||
|
||||
disconnected ||
|
||||
traitorRemainingTicks > 0 ||
|
||||
nukeActive
|
||||
nukeActive ||
|
||||
alliance ||
|
||||
target ||
|
||||
embargo ||
|
||||
nukeTargetsMe
|
||||
) {
|
||||
result.set(ps.smallID, {
|
||||
result.set(sid, {
|
||||
crown,
|
||||
traitor,
|
||||
disconnected,
|
||||
alliance: false,
|
||||
alliance,
|
||||
allianceReq: false,
|
||||
target: false,
|
||||
embargo: false,
|
||||
target,
|
||||
embargo,
|
||||
nukeActive,
|
||||
nukeTargetsMe: false,
|
||||
nukeTargetsMe,
|
||||
traitorRemainingTicks,
|
||||
allianceFraction: 0,
|
||||
});
|
||||
|
||||
@@ -465,7 +465,10 @@ export class GameView implements GameMap {
|
||||
f.railroadDirty = this.railroadCache.railroadDirty;
|
||||
f.trailDirtyRowMin = this.trailManager.dirtyRowMin;
|
||||
f.trailDirtyRowMax = this.trailManager.dirtyRowMax;
|
||||
f.playerStatus = computePlayerStatus(this._playerStates, this._unitStates);
|
||||
f.playerStatus = computePlayerStatus(this._playerStates, this._unitStates, {
|
||||
localPlayerID: this._myPlayer?.smallID() ?? 0,
|
||||
tileState: this._map.tileStateBuffer(),
|
||||
});
|
||||
const rel = buildRelationMatrix(this._playerStates);
|
||||
f.relationMatrix = rel.matrix;
|
||||
f.relationSize = rel.size;
|
||||
|
||||
Reference in New Issue
Block a user