mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 21:54:15 +00:00
a7f992e9b0
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>
212 lines
5.6 KiB
TypeScript
212 lines
5.6 KiB
TypeScript
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), // A–B
|
||
makeRenewal(allianceAC, 1), // A–C
|
||
makeRenewal(allianceBC, 2), // B–C
|
||
];
|
||
|
||
// Break alliance A–B
|
||
(display as any).removeAllianceRenewalEvents(allianceAB);
|
||
|
||
const remaining = (display as any).events;
|
||
|
||
// A–B 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), // A–B
|
||
makeRenewal(allianceCD, 3), // C–D
|
||
];
|
||
|
||
(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);
|
||
});
|
||
});
|