Files
OpenFrontIO/tests/client/graphics/layers/ActionableEventsAlliance.test.ts
T
Evan Pelle a7f992e9b0 refactor: convert to npm-workspaces monorepo (engine/core-public/shared/client/server)
Restructure the single src/ tree into an npm-workspaces monorepo under
packages/, rename core -> engine, extract a types-only core-public layer,
and break the pre-existing engine -> client dependency cycle.

Structure (packages/):
  core-public  public API/wire schemas + shared enums (clean leaf)
  shared       framework-agnostic helpers (clean leaf)
  engine       deterministic simulation (was src/core)
  client       rendering/UI (was src/client)
  server       coordination (was src/server)

Dependency DAG: engine -> {core-public, shared}; client -> {core-public,
shared, engine}; server -> {core-public, engine}.

- npm workspaces: root package.json workspaces + per-package package.json;
  tsconfig.base.json holds shared options + path aliases
  (core-public/* shared/* engine/* client/* server/*) resolved uniformly by
  tsc, Vite (resolve.tsconfigPaths), Vitest, and tsx. Lockfile regenerated.
- core-public: moved Schemas/ApiSchemas/CosmeticSchemas/StatsSchemas/
  ClanApiSchemas/WorkerSchemas/Base64/PatternDecoder; extracted the enums
  (GameTypes), GameEvent type, emoji table, and GraphicsOverrides schema.
  Engine re-exports the moved enums/types so existing imports keep working.
- Broke engine -> client cycle:
  - renderNumber/renderTroops -> shared/format
  - NameBoxCalculator moved into engine
  - username validation returns translation key + params; client translates
  - applyStateUpdate moved to client (operates on the render-only PlayerState)
  - Config/UnitGrid/execution-Util/GameImpl now use structural read
    interfaces (engine/game/ReadViews: PlayerLike/UnitLike/GameLike) instead
    of importing client view classes; client imports view classes from a new
    client/view barrel; deleted the engine/game/GameView re-export shim.
- Build/deploy updated: vite.config, index.html, eslint, Dockerfile
  (copies packages/ + tsconfig.base.json before npm ci), .vscode, tests.

Verified: tsc --noEmit clean; 1364 + 65 tests pass; production vite build
succeeds; engine has zero client/server imports; core-public and shared are
dependency leaves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:51:03 +00:00

212 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { GameUpdateType } from "engine/game/GameUpdates";
vi.mock("lit", () => ({
html: () => {},
LitElement: class {},
}));
vi.mock("lit/decorators.js", () => ({
customElement: () => (clazz: any) => clazz,
query: () => () => {},
state: () => () => {},
property: () => () => {},
}));
vi.mock("lit/directive.js", () => ({
DirectiveResult: class {},
}));
vi.mock("lit/directives/unsafe-html.js", () => ({
unsafeHTML: () => {},
UnsafeHTMLDirective: class {},
}));
import { ActionableEvents } from "client/hud/layers/ActionableEvents";
import { MessageType } from "engine/game/Game";
describe("ActionableEvents - alliance renewal cleanup (allianceID based)", () => {
function makeRenewal(
allianceID: number,
focusID: number,
description = "Alliance about to expire",
) {
return {
description,
type: MessageType.RENEW_ALLIANCE,
allianceID,
focusID,
createdAt: 0,
};
}
test("removes ONLY renewal events for the broken alliance", () => {
const display = new ActionableEvents();
const allianceAB = 1;
const allianceAC = 2;
const allianceBC = 3;
(display as any).events = [
makeRenewal(allianceAB, 1), // AB
makeRenewal(allianceAC, 1), // AC
makeRenewal(allianceBC, 2), // BC
];
// Break alliance AB
(display as any).removeAllianceRenewalEvents(allianceAB);
const remaining = (display as any).events;
// AB renewal removed
expect(remaining.some((e: any) => e.allianceID === allianceAB)).toBe(false);
// Other alliances untouched
expect(remaining.some((e: any) => e.allianceID === allianceAC)).toBe(true);
expect(remaining.some((e: any) => e.allianceID === allianceBC)).toBe(true);
});
test("does NOT remove renewals just because the same player is involved", () => {
const display = new ActionableEvents();
const allianceAB = 10;
const allianceAC = 11;
(display as any).events = [
makeRenewal(allianceAB, 1), // Player 1 involved
makeRenewal(allianceAC, 1), // Same player, different alliance
];
(display as any).removeAllianceRenewalEvents(allianceAB);
const remaining = (display as any).events;
expect(remaining.length).toBe(1);
expect(remaining[0].allianceID).toBe(allianceAC);
});
test("breaking one alliance does not affect renewals between other players", () => {
const display = new ActionableEvents();
const allianceAB = 100;
const allianceCD = 200;
(display as any).events = [
makeRenewal(allianceAB, 1), // AB
makeRenewal(allianceCD, 3), // CD
];
(display as any).removeAllianceRenewalEvents(allianceAB);
const remaining = (display as any).events;
expect(remaining.length).toBe(1);
expect(remaining[0].allianceID).toBe(allianceCD);
});
test("onAllianceExtensionEvent removes renewal when playerID matches myPlayer", () => {
const display = new ActionableEvents();
const allianceID = 42;
const mySmallID = 7;
(display as any).game = {
myPlayer: () => ({ smallID: () => mySmallID }),
};
(display as any).requestUpdate = () => {};
(display as any).events = [makeRenewal(allianceID, mySmallID)];
(display as any).onAllianceExtensionEvent({
type: GameUpdateType.AllianceExtension,
playerID: mySmallID,
allianceID,
});
const remaining = (display as any).events;
expect(remaining.some((e: any) => e.allianceID === allianceID)).toBe(false);
});
test("onAllianceExtensionEvent keeps renewal when playerID does not match myPlayer", () => {
const display = new ActionableEvents();
const allianceID = 42;
const mySmallID = 7;
const otherSmallID = 9;
(display as any).game = {
myPlayer: () => ({ smallID: () => mySmallID }),
};
(display as any).requestUpdate = () => {};
(display as any).events = [makeRenewal(allianceID, mySmallID)];
(display as any).onAllianceExtensionEvent({
type: "AllianceExtension",
playerID: otherSmallID,
allianceID,
});
const remaining = (display as any).events;
expect(remaining.some((e: any) => e.allianceID === allianceID)).toBe(true);
});
test("onAllianceExtensionEvent keeps renewal when myPlayer is null", () => {
const display = new ActionableEvents();
const allianceID = 42;
(display as any).game = {
myPlayer: () => null,
};
(display as any).requestUpdate = () => {};
(display as any).events = [makeRenewal(allianceID, 1)];
(display as any).onAllianceExtensionEvent({
type: "AllianceExtension",
playerID: 1,
allianceID,
});
const remaining = (display as any).events;
expect(remaining.some((e: any) => e.allianceID === allianceID)).toBe(true);
});
test("does not affect non-RENEW_ALLIANCE events", () => {
const display = new ActionableEvents();
(display as any).events = [
{
description: "Alliance broken",
type: MessageType.ALLIANCE_BROKEN,
createdAt: 0,
},
{
description: "Alliance accepted",
type: MessageType.ALLIANCE_ACCEPTED,
createdAt: 0,
},
{
description: "Renewal",
type: MessageType.RENEW_ALLIANCE,
allianceID: 999,
createdAt: 0,
},
];
(display as any).removeAllianceRenewalEvents(999);
const remaining = (display as any).events;
expect(
remaining.some((e: any) => e.type === MessageType.ALLIANCE_BROKEN),
).toBe(true);
expect(
remaining.some((e: any) => e.type === MessageType.ALLIANCE_ACCEPTED),
).toBe(true);
expect(
remaining.some((e: any) => e.type === MessageType.RENEW_ALLIANCE),
).toBe(false);
});
});