mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 12:54:37 +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>
210 lines
6.6 KiB
TypeScript
210 lines
6.6 KiB
TypeScript
import { NukeExecution } from "engine/execution/NukeExecution";
|
|
import {
|
|
Game,
|
|
Player,
|
|
PlayerInfo,
|
|
PlayerType,
|
|
UnitType,
|
|
} from "engine/game/Game";
|
|
import { TileRef } from "engine/game/GameMap";
|
|
import { setup } from "../util/Setup";
|
|
import { constructionExecution } from "../util/utils";
|
|
|
|
function launchNukeAt(game: Game, player: Player, target: TileRef): void {
|
|
game.addExecution(new NukeExecution(UnitType.AtomBomb, player, target, null));
|
|
// init + build
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
}
|
|
|
|
function tickUntilNukeLands(game: Game, maxTicks = 50): void {
|
|
for (let i = 0; i < maxTicks; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
}
|
|
|
|
describe("Water Nukes", () => {
|
|
let game: Game;
|
|
let player: Player;
|
|
const info = new PlayerInfo("p", PlayerType.Human, null, "p");
|
|
|
|
describe("when waterNukes is enabled", () => {
|
|
beforeEach(async () => {
|
|
game = await setup(
|
|
"plains",
|
|
{
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: true,
|
|
},
|
|
[info],
|
|
);
|
|
player = game.player(info.id);
|
|
player.conquer(game.ref(1, 1));
|
|
|
|
// Build a missile silo
|
|
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
|
});
|
|
|
|
test("nuke converts land tiles to water instead of fallout", () => {
|
|
const target = game.ref(10, 10);
|
|
// Confirm target is land before nuke
|
|
expect(game.isLand(target)).toBe(true);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
// Target should now be water, not land
|
|
expect(game.isLand(target)).toBe(false);
|
|
expect(game.isWater(target)).toBe(true);
|
|
// Should NOT have fallout
|
|
expect(game.hasFallout(target)).toBe(false);
|
|
});
|
|
|
|
test("converted tiles get shoreline bits updated", () => {
|
|
const target = game.ref(10, 10);
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
// With nukeMagnitudes { inner: 1, outer: 1 }, the target and its
|
|
// cardinal neighbors (dist² <= 1) are all converted to water.
|
|
// Shoreline tiles are the land tiles just outside the blast radius.
|
|
const x = game.x(target);
|
|
const y = game.y(target);
|
|
|
|
// 2 tiles away should still be land and now be shoreline
|
|
const outerNeighbors: TileRef[] = [];
|
|
if (game.isValidCoord(x - 2, y)) outerNeighbors.push(game.ref(x - 2, y));
|
|
if (game.isValidCoord(x + 2, y)) outerNeighbors.push(game.ref(x + 2, y));
|
|
if (game.isValidCoord(x, y - 2)) outerNeighbors.push(game.ref(x, y - 2));
|
|
if (game.isValidCoord(x, y + 2)) outerNeighbors.push(game.ref(x, y + 2));
|
|
|
|
for (const n of outerNeighbors) {
|
|
expect(game.isLand(n)).toBe(true);
|
|
expect(game.isShoreline(n)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("queueWaterConversion skips tiles conquered before flush", () => {
|
|
// Pick an unowned land tile and queue it for water conversion directly
|
|
const target = game.ref(10, 10);
|
|
expect(game.isLand(target)).toBe(true);
|
|
expect(game.hasOwner(target)).toBe(false);
|
|
|
|
// Queue the tile for water conversion (simulates nuke queueing)
|
|
game.queueWaterConversion(target);
|
|
|
|
// Another actor conquers the tile before the tick flushes the queue
|
|
player.conquer(target);
|
|
expect(game.hasOwner(target)).toBe(true);
|
|
|
|
// Flush: the pending conversion should be skipped because the tile is now owned
|
|
game.executeNextTick();
|
|
|
|
// Tile should remain land and owned
|
|
expect(game.isLand(target)).toBe(true);
|
|
expect(game.hasOwner(target)).toBe(true);
|
|
expect(game.isWater(target)).toBe(false);
|
|
});
|
|
|
|
test("waterGraphVersion increments after water conversion", async () => {
|
|
// Need a game with nav mesh enabled for graph rebuilds
|
|
const navGame = await setup(
|
|
"plains",
|
|
{
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: true,
|
|
disableNavMesh: false,
|
|
},
|
|
[info],
|
|
);
|
|
const player2 = navGame.player(info.id);
|
|
player2.conquer(navGame.ref(1, 1));
|
|
constructionExecution(navGame, player2, 1, 1, UnitType.MissileSilo);
|
|
|
|
const versionBefore = navGame.waterGraphVersion();
|
|
|
|
// Launch multiple nukes in a cluster to ensure enough tiles convert
|
|
// for at least one minimap tile to flip (need >= 3 of 4 source tiles)
|
|
const target = navGame.ref(50, 50);
|
|
navGame.addExecution(
|
|
new NukeExecution(UnitType.AtomBomb, player2, target, null),
|
|
);
|
|
// Tick enough for nuke to land + graph rebuild throttle (20 ticks)
|
|
for (let i = 0; i < 80; i++) navGame.executeNextTick();
|
|
|
|
expect(navGame.waterGraphVersion()).toBeGreaterThan(versionBefore);
|
|
});
|
|
});
|
|
|
|
describe("when waterNukes is disabled (default)", () => {
|
|
beforeEach(async () => {
|
|
game = await setup(
|
|
"plains",
|
|
{
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: false,
|
|
},
|
|
[info],
|
|
);
|
|
player = game.player(info.id);
|
|
player.conquer(game.ref(1, 1));
|
|
|
|
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
|
});
|
|
|
|
test("nuke applies fallout instead of converting to water", () => {
|
|
const target = game.ref(10, 10);
|
|
expect(game.isLand(target)).toBe(true);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
// Should remain land with fallout
|
|
expect(game.isLand(target)).toBe(true);
|
|
expect(game.hasFallout(target)).toBe(true);
|
|
});
|
|
|
|
test("waterGraphVersion does not change", () => {
|
|
const versionBefore = game.waterGraphVersion();
|
|
const target = game.ref(10, 10);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
expect(game.waterGraphVersion()).toBe(versionBefore);
|
|
});
|
|
});
|
|
|
|
describe("updateTile terrain byte round-trip", () => {
|
|
test("terrain byte is packed and unpacked correctly", async () => {
|
|
game = await setup(
|
|
"plains",
|
|
{
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
waterNukes: true,
|
|
},
|
|
[info],
|
|
);
|
|
player = game.player(info.id);
|
|
player.conquer(game.ref(1, 1));
|
|
constructionExecution(game, player, 1, 1, UnitType.MissileSilo);
|
|
|
|
const target = game.ref(10, 10);
|
|
const terrainBefore = game.terrainByte(target);
|
|
expect(game.isLand(target)).toBe(true);
|
|
|
|
launchNukeAt(game, player, target);
|
|
tickUntilNukeLands(game);
|
|
|
|
const terrainAfter = game.terrainByte(target);
|
|
// Terrain should have changed (was land, now water)
|
|
expect(terrainAfter).not.toBe(terrainBefore);
|
|
expect(game.isWater(target)).toBe(true);
|
|
});
|
|
});
|
|
});
|