Fix alt-view coloring teammates as enemies in team games (#4247)

## Problem

In team games, alternate view (space-hold) colored teammates' units red
(enemy color) instead of yellow (ally color). Teammates' territory
borders had the same problem.

## Root cause

`buildRelationMatrix()` in
`src/client/render/frame/derive/RelationMatrix.ts` already supports an
optional `teams` map that marks same-team pairs as `RELATION_FRIENDLY`,
but the call site in `GameView.populateFrame()` never passed it (the
companion `buildTeamMap` helper was dead code). Only explicit alliances
were marked friendly, so a teammate without a formal alliance read as
neutral — and the alt-view unit palette maps neutral to the enemy color.

## Fix

- `GameView` now tracks a `smallID → team` map, populated when each
`PlayerView` is first created (team is a static field, so once per
player is enough).
- The map is passed through to `buildRelationMatrix()`, which feeds both
the `AffiliationPalette` (unit colors) and `BorderComputePass` (border
colors).

## Testing

- New regression test in `tests/client/view/GameView.test.ts`: same-team
players are `RELATION_FRIENDLY` in `frame.relationMatrix`, cross-team
players stay neutral.
- All 36 GameView tests pass; typecheck clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-06-12 15:05:37 -07:00
committed by GitHub
parent 9e2648f80c
commit b85d1fc372
2 changed files with 27 additions and 1 deletions
+7 -1
View File
@@ -76,6 +76,8 @@ export class GameView implements GameMap {
import("../render/types").PlayerState
>();
private _unitStates = new Map<number, import("../render/types").UnitState>();
/** smallID → team, for the renderer's relation matrix (team games). */
private _teams = new Map<number, string>();
private updatedTiles: TileRef[] = [];
private updatedTerrainTiles: TileRef[] = [];
@@ -327,6 +329,10 @@ export class GameView implements GameMap {
);
this._players.set(pu.id, player);
this._playerStates.set(pu.smallID!, player.state);
const team = player.team();
if (team !== null) {
this._teams.set(pu.smallID!, team);
}
}
});
@@ -472,7 +478,7 @@ export class GameView implements GameMap {
isTransitiveTarget: (sid) =>
this._myPlayer?.hasTransitiveTarget(sid) ?? false,
});
const rel = buildRelationMatrix(this._playerStates);
const rel = buildRelationMatrix(this._playerStates, this._teams);
f.relationMatrix = rel.matrix;
f.relationSize = rel.size;
f.allianceClusters = computeAllianceClusters(this._playerStates);
+20
View File
@@ -466,4 +466,24 @@ describe("GameView.frameData() — renderer contract", () => {
game.update(makeEmptyGu(2));
expect(game.frameData().structuresDirty).toBe(false);
});
it("frame.relationMatrix marks same-team players as friendly (team games)", () => {
const RELATION_FRIENDLY = 1;
const RELATION_NEUTRAL = 0;
const game = makeGameView();
game.update(
withPlayers(1, [
makePlayerUpdate({ id: "alice", smallID: 1, team: "red" }),
makePlayerUpdate({ id: "bob", smallID: 2, team: "red" }),
makePlayerUpdate({ id: "carol", smallID: 3, team: "blue" }),
]),
);
const { relationMatrix, relationSize } = game.frameData();
// Teammates (no explicit alliance) are friendly both ways.
expect(relationMatrix[1 * relationSize + 2]).toBe(RELATION_FRIENDLY);
expect(relationMatrix[2 * relationSize + 1]).toBe(RELATION_FRIENDLY);
// Cross-team players stay neutral.
expect(relationMatrix[1 * relationSize + 3]).toBe(RELATION_NEUTRAL);
expect(relationMatrix[3 * relationSize + 1]).toBe(RELATION_NEUTRAL);
});
});