mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
WebGL: show alliance request+duration icon, show ally and team mate targets too, some optimization (#3971)
## Description: Show nuke icons during replay too (when there's no localPlayer). Show alliance request envelope icon, and duration in alliance icon (weren't calculated yet). Show ally and team mates' targets too (weren't calculated yet). Remove unnecessary allocations. Nukes loop allocated two new sets, transitive targets was a new set and now uses predicate with fallback to localPlayer.targets, localPlayer.allies and localPlayer.embargoes were both put in new set instead of using .includes directly. ## Please complete the following: - [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 - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33
This commit is contained in:
@@ -14,31 +14,42 @@ 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;
|
||||
localPlayerSmallID?: number;
|
||||
/**
|
||||
* Local player string ID for matching outgoing alliance requests.
|
||||
*/
|
||||
localPlayerID?: string;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* Current game tick to evaluate alliance progress.
|
||||
*/
|
||||
tick?: number;
|
||||
/**
|
||||
* Static duration of an alliance to evaluate fraction.
|
||||
*/
|
||||
allianceDuration?: number;
|
||||
/**
|
||||
* Predicate testing if the local player considers `sid` a transitive target.
|
||||
*/
|
||||
isTransitiveTarget?: (sid: number) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute per-player status flags for the name/status-icon pass.
|
||||
*
|
||||
* Without `opts.localPlayerID`: replay-path mode. Crown/traitor/disconnected/
|
||||
* Without `opts.localPlayerSmallID`: 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
|
||||
* With `opts.localPlayerSmallID`: 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>,
|
||||
@@ -46,29 +57,11 @@ export function computePlayerStatus(
|
||||
opts: ComputePlayerStatusOptions = {},
|
||||
): Map<number, PlayerStatusData> {
|
||||
const result = new Map<number, PlayerStatusData>();
|
||||
const localPlayerID = opts.localPlayerID ?? 0;
|
||||
const tileState = opts.tileState;
|
||||
const localPlayerSmallID = opts.localPlayerSmallID ?? 0;
|
||||
const localPlayerID = opts.localPlayerID ?? "";
|
||||
const localPlayer =
|
||||
localPlayerID > 0 ? players.get(localPlayerID) : undefined;
|
||||
|
||||
// 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)) 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
localPlayerSmallID > 0 ? players.get(localPlayerSmallID) : undefined;
|
||||
const tileState = opts.tileState;
|
||||
|
||||
// Crown: alive player with most tiles owned.
|
||||
let crownSmallID = -1;
|
||||
@@ -81,13 +74,6 @@ 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 sid = ps.smallID;
|
||||
@@ -95,20 +81,70 @@ export function computePlayerStatus(
|
||||
const traitor = ps.isTraitor;
|
||||
const disconnected = ps.isDisconnected;
|
||||
const traitorRemainingTicks = ps.traitorRemainingTicks;
|
||||
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.
|
||||
// Relative flags
|
||||
let nukeActive = false;
|
||||
let nukeTargetsMe = false;
|
||||
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);
|
||||
let allianceReq = false;
|
||||
let allianceFraction = 0;
|
||||
|
||||
// Nukes: show during replay too, except the nukeTargetsMe flag
|
||||
for (const u of units.values()) {
|
||||
if (
|
||||
u.ownerID === sid &&
|
||||
u.isActive &&
|
||||
NUKE_ACTIVE_TYPES.has(u.unitType)
|
||||
) {
|
||||
nukeActive = true;
|
||||
if (
|
||||
localPlayerSmallID > 0 &&
|
||||
tileState !== undefined &&
|
||||
u.targetTile !== null &&
|
||||
(tileState[u.targetTile] & OWNER_MASK) === localPlayerSmallID
|
||||
) {
|
||||
nukeTargetsMe = true;
|
||||
}
|
||||
if (nukeTargetsMe) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Flags which are only meaningful when there's a local player,
|
||||
// and we're not looking at the local player itself.
|
||||
if (localPlayer !== undefined && sid !== localPlayerSmallID) {
|
||||
alliance = localPlayer.allies.includes(sid);
|
||||
allianceReq = ps.outgoingAllianceRequests.includes(localPlayerID);
|
||||
target = opts.isTransitiveTarget
|
||||
? opts.isTransitiveTarget(sid)
|
||||
: localPlayer.targets.includes(sid);
|
||||
// Embargo is bilateral: either side embargoes the other.
|
||||
embargo = myEmbargoes!.has(sid) || ps.embargoes.includes(localPlayerID);
|
||||
nukeTargetsMe = nukeAimedAtMe.has(sid);
|
||||
embargo =
|
||||
localPlayer.embargoes.includes(sid) ||
|
||||
ps.embargoes.includes(localPlayerSmallID);
|
||||
|
||||
if (
|
||||
alliance &&
|
||||
opts.tick !== undefined &&
|
||||
opts.allianceDuration !== undefined &&
|
||||
opts.localPlayerID
|
||||
) {
|
||||
const foundAlliance = ps.alliances.find(
|
||||
(a) => a.other === opts.localPlayerID,
|
||||
);
|
||||
if (foundAlliance) {
|
||||
// e.g. expiresAt = 100, tick = 60, diff = 40. duration = 100. fraction = 0.4.
|
||||
const remainingTicks = Math.max(
|
||||
0,
|
||||
foundAlliance.expiresAt - opts.tick,
|
||||
);
|
||||
allianceFraction = Math.max(
|
||||
0,
|
||||
Math.min(1, remainingTicks / Math.max(1, opts.allianceDuration)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -118,6 +154,7 @@ export function computePlayerStatus(
|
||||
traitorRemainingTicks > 0 ||
|
||||
nukeActive ||
|
||||
alliance ||
|
||||
allianceReq ||
|
||||
target ||
|
||||
embargo ||
|
||||
nukeTargetsMe
|
||||
@@ -127,13 +164,13 @@ export function computePlayerStatus(
|
||||
traitor,
|
||||
disconnected,
|
||||
alliance,
|
||||
allianceReq: false,
|
||||
allianceReq,
|
||||
target,
|
||||
embargo,
|
||||
nukeActive,
|
||||
nukeTargetsMe,
|
||||
traitorRemainingTicks,
|
||||
allianceFraction: 0,
|
||||
allianceFraction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,9 +474,15 @@ 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, {
|
||||
localPlayerID: this._myPlayer?.smallID() ?? 0,
|
||||
localPlayerSmallID: this._myPlayer?.smallID() ?? 0,
|
||||
localPlayerID: this._myPlayer?.id() ?? "",
|
||||
tileState: this._map.tileStateBuffer(),
|
||||
tick: gu.tick,
|
||||
allianceDuration: this._config.allianceDuration(),
|
||||
isTransitiveTarget: (sid) =>
|
||||
this._myPlayer?.hasTransitiveTarget(sid) ?? false,
|
||||
});
|
||||
const rel = buildRelationMatrix(this._playerStates);
|
||||
f.relationMatrix = rel.matrix;
|
||||
|
||||
@@ -522,6 +522,32 @@ export class PlayerView {
|
||||
return result;
|
||||
}
|
||||
|
||||
hasTransitiveTarget(sid: number): boolean {
|
||||
if (this.state.targets.includes(sid)) return true;
|
||||
|
||||
for (const allyID of this.state.allies) {
|
||||
const ally = this.game.playerBySmallID(allyID) as PlayerView;
|
||||
if (ally && ally.state.targets.includes(sid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const myTeam = this.static.team;
|
||||
if (myTeam !== null) {
|
||||
for (const p of this.game.playerViews()) {
|
||||
if (
|
||||
p !== this &&
|
||||
p.static.team === myTeam &&
|
||||
p.state.targets.includes(sid)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isTraitor(): boolean {
|
||||
return this.state.isTraitor;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* computePlayerStatus has two modes:
|
||||
*
|
||||
* - Replay mode (no localPlayerID): only crown / traitor / disconnected /
|
||||
* - Replay mode (no localPlayerSmallID): only crown / traitor / disconnected /
|
||||
* nukeActive flags are populated. All relative flags are false.
|
||||
* - Live mode (localPlayerID set): also fills alliance / target / embargo,
|
||||
* - Live mode (localPlayerSmallID set): also fills alliance / target / embargo,
|
||||
* and nukeTargetsMe if a tileState buffer is supplied.
|
||||
*
|
||||
* The function only emits an entry per player when at least one flag is true
|
||||
@@ -83,7 +83,7 @@ function unitsMap(...us: UnitState[]): Map<number, UnitState> {
|
||||
return new Map(us.map((u) => [u.id, u]));
|
||||
}
|
||||
|
||||
describe("computePlayerStatus — replay mode (no localPlayerID)", () => {
|
||||
describe("computePlayerStatus — replay mode (no localPlayerSmallID)", () => {
|
||||
it("returns empty map when no flags are set", () => {
|
||||
const players = playersMap(ps({ smallID: 1 }));
|
||||
const status = computePlayerStatus(players, unitsMap());
|
||||
@@ -160,14 +160,14 @@ describe("computePlayerStatus — replay mode (no localPlayerID)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
describe("computePlayerStatus — live mode (localPlayerSmallID set)", () => {
|
||||
it("alliance: local has them as ally → alliance true", () => {
|
||||
const players = playersMap(
|
||||
ps({ smallID: 1, allies: [2] }), // me
|
||||
ps({ smallID: 2 }),
|
||||
);
|
||||
const status = computePlayerStatus(players, unitsMap(), {
|
||||
localPlayerID: 1,
|
||||
localPlayerSmallID: 1,
|
||||
});
|
||||
expect(status.get(2)?.alliance).toBe(true);
|
||||
});
|
||||
@@ -178,7 +178,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
ps({ smallID: 2 }),
|
||||
);
|
||||
const status = computePlayerStatus(players, unitsMap(), {
|
||||
localPlayerID: 1,
|
||||
localPlayerSmallID: 1,
|
||||
});
|
||||
expect(status.get(2)?.target).toBe(true);
|
||||
});
|
||||
@@ -188,7 +188,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
let status = computePlayerStatus(
|
||||
playersMap(ps({ smallID: 1, embargoes: [2] }), ps({ smallID: 2 })),
|
||||
unitsMap(),
|
||||
{ localPlayerID: 1 },
|
||||
{ localPlayerSmallID: 1 },
|
||||
);
|
||||
expect(status.get(2)?.embargo).toBe(true);
|
||||
|
||||
@@ -196,7 +196,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
status = computePlayerStatus(
|
||||
playersMap(ps({ smallID: 1 }), ps({ smallID: 2, embargoes: [1] })),
|
||||
unitsMap(),
|
||||
{ localPlayerID: 1 },
|
||||
{ localPlayerSmallID: 1 },
|
||||
);
|
||||
expect(status.get(2)?.embargo).toBe(true);
|
||||
|
||||
@@ -204,7 +204,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
status = computePlayerStatus(
|
||||
playersMap(ps({ smallID: 1 }), ps({ smallID: 2, tilesOwned: 1 })),
|
||||
unitsMap(),
|
||||
{ localPlayerID: 1 },
|
||||
{ localPlayerSmallID: 1 },
|
||||
);
|
||||
// Player 2 only has crown — embargo should be false.
|
||||
expect(status.get(2)?.embargo).toBe(false);
|
||||
@@ -222,7 +222,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
ps({ smallID: 2 }),
|
||||
);
|
||||
const status = computePlayerStatus(players, unitsMap(), {
|
||||
localPlayerID: 1,
|
||||
localPlayerSmallID: 1,
|
||||
});
|
||||
// Player 1 (local) gets crown but no relative flags vs. self.
|
||||
expect(status.get(1)?.crown).toBe(true);
|
||||
@@ -242,7 +242,9 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
targetTile: 5,
|
||||
}),
|
||||
);
|
||||
const status = computePlayerStatus(players, units, { localPlayerID: 1 });
|
||||
const status = computePlayerStatus(players, units, {
|
||||
localPlayerSmallID: 1,
|
||||
});
|
||||
expect(status.get(2)?.nukeActive).toBe(true);
|
||||
expect(status.get(2)?.nukeTargetsMe).toBe(false);
|
||||
});
|
||||
@@ -263,7 +265,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
tileState[5] = 1;
|
||||
|
||||
const status = computePlayerStatus(players, units, {
|
||||
localPlayerID: 1,
|
||||
localPlayerSmallID: 1,
|
||||
tileState,
|
||||
});
|
||||
expect(status.get(2)?.nukeTargetsMe).toBe(true);
|
||||
@@ -289,7 +291,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
tileState[5] = 3;
|
||||
|
||||
const status = computePlayerStatus(players, units, {
|
||||
localPlayerID: 1,
|
||||
localPlayerSmallID: 1,
|
||||
tileState,
|
||||
});
|
||||
expect(status.get(2)?.nukeTargetsMe).toBe(false);
|
||||
@@ -301,7 +303,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
ps({ smallID: 2 }), // no other flags
|
||||
);
|
||||
const status = computePlayerStatus(players, unitsMap(), {
|
||||
localPlayerID: 1,
|
||||
localPlayerSmallID: 1,
|
||||
});
|
||||
// Without local-mode, player 2 wouldn't get an entry — alliance is the
|
||||
// only reason it shows up here.
|
||||
@@ -309,13 +311,13 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
expect(status.get(2)?.alliance).toBe(true);
|
||||
});
|
||||
|
||||
it("localPlayerID = 0 (no local player) behaves like replay mode", () => {
|
||||
it("localPlayerSmallID = 0 (no local player) behaves like replay mode", () => {
|
||||
const players = playersMap(
|
||||
ps({ smallID: 1, allies: [2] }),
|
||||
ps({ smallID: 2, tilesOwned: 1 }),
|
||||
);
|
||||
const status = computePlayerStatus(players, unitsMap(), {
|
||||
localPlayerID: 0,
|
||||
localPlayerSmallID: 0,
|
||||
});
|
||||
expect(status.get(2)?.alliance).toBe(false);
|
||||
});
|
||||
@@ -326,7 +328,7 @@ describe("computePlayerStatus — live mode (localPlayerID set)", () => {
|
||||
ps({ smallID: 2 }),
|
||||
);
|
||||
const status = computePlayerStatus(players, unitsMap(), {
|
||||
localPlayerID: 1,
|
||||
localPlayerSmallID: 1,
|
||||
});
|
||||
expect(status.get(2)?.allianceReq).toBe(false);
|
||||
expect(status.get(2)?.allianceFraction).toBe(0);
|
||||
|
||||
Reference in New Issue
Block a user