mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 22:14:16 +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>
162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
import { TileRef } from "engine/game/GameMap.js";
|
|
import { PathFinding } from "engine/pathfinding/PathFinder.js";
|
|
import { SpatialQuery } from "engine/pathfinding/spatial/SpatialQuery.js";
|
|
import { DebugSpan } from "engine/utilities/DebugSpan.js";
|
|
import { loadMap } from "./maps.js";
|
|
|
|
export interface SpatialQueryResult {
|
|
selectedShore: [number, number] | null;
|
|
path: Array<[number, number]> | null;
|
|
shores: Array<[number, number]>;
|
|
debug: {
|
|
candidates: Array<[number, number]> | null;
|
|
refinedPath: Array<[number, number]> | null;
|
|
originalBestTile: [number, number] | null;
|
|
newBestTile: [number, number] | null;
|
|
timings: Record<string, number>;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract timings from DebugSpan hierarchy
|
|
*/
|
|
function extractTimings(span: {
|
|
name: string;
|
|
duration?: number;
|
|
children: any[];
|
|
}): Record<string, number> {
|
|
const timings: Record<string, number> = {};
|
|
|
|
if (span.duration !== undefined) {
|
|
timings[span.name] = span.duration;
|
|
}
|
|
|
|
for (const child of span.children) {
|
|
Object.assign(timings, extractTimings(child));
|
|
}
|
|
|
|
return timings;
|
|
}
|
|
|
|
/**
|
|
* Convert TileRef to coordinate tuple
|
|
*/
|
|
function tileToCoord(tile: TileRef, game: any): [number, number] {
|
|
return [game.x(tile), game.y(tile)];
|
|
}
|
|
|
|
/**
|
|
* Convert TileRef array to coordinate array
|
|
*/
|
|
function tilesToCoords(
|
|
tiles: TileRef[] | null | undefined,
|
|
game: any,
|
|
): Array<[number, number]> | null {
|
|
if (!tiles) return null;
|
|
return tiles.map((tile) => tileToCoord(tile, game));
|
|
}
|
|
|
|
/**
|
|
* Compute spatial query for transport ship launch
|
|
*/
|
|
export async function computeSpatialQuery(
|
|
mapName: string,
|
|
ownedTiles: number[],
|
|
target: [number, number],
|
|
): Promise<SpatialQueryResult> {
|
|
const { game } = await loadMap(mapName);
|
|
|
|
const targetRef = game.ref(target[0], target[1]) as TileRef;
|
|
|
|
// Validate target is water or shore
|
|
if (!game.isWater(targetRef) && !game.isShore(targetRef)) {
|
|
throw new Error(
|
|
`Target (${target[0]}, ${target[1]}) must be water or shore`,
|
|
);
|
|
}
|
|
|
|
// Convert owned tile indices to TileRefs
|
|
const ownedRefs = ownedTiles.map((idx) => {
|
|
const x = idx % game.width();
|
|
const y = Math.floor(idx / game.width());
|
|
return game.ref(x, y) as TileRef;
|
|
});
|
|
|
|
// Create mock player that returns owned tiles as border tiles
|
|
// The SpatialQuery will filter to actual shore tiles
|
|
const mockPlayer = {
|
|
isPlayer: () => true,
|
|
smallID: () => 999, // Arbitrary ID for visualization
|
|
borderTiles: function* () {
|
|
for (const tile of ownedRefs) {
|
|
yield tile;
|
|
}
|
|
},
|
|
};
|
|
|
|
// Get target water component for filtering
|
|
const targetComponent = game.getWaterComponent(targetRef);
|
|
|
|
// Pre-compute all valid shore tiles for visualization
|
|
const allShores: TileRef[] = [];
|
|
for (const tile of ownedRefs) {
|
|
if (game.isShore(tile) && game.isLand(tile)) {
|
|
const tComponent = game.getWaterComponent(tile);
|
|
if (tComponent === targetComponent) {
|
|
allShores.push(tile);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enable DebugSpan to capture internal state
|
|
DebugSpan.enable();
|
|
|
|
// Run spatial query
|
|
const spatialQuery = new SpatialQuery(game);
|
|
const selectedShore = spatialQuery.closestShoreByWater(
|
|
mockPlayer as any,
|
|
targetRef,
|
|
);
|
|
|
|
// Get span data
|
|
const span = DebugSpan.getLastSpan();
|
|
DebugSpan.disable();
|
|
|
|
// Extract debug info from span
|
|
let candidates: TileRef[] | null = null;
|
|
let refinedPath: TileRef[] | null = null;
|
|
let originalBestTile: TileRef | null = null;
|
|
let newBestTile: TileRef | null = null;
|
|
|
|
if (span?.data) {
|
|
candidates = (span.data.$candidates as TileRef[] | undefined) ?? null;
|
|
refinedPath = (span.data.$refinedPath as TileRef[] | undefined) ?? null;
|
|
originalBestTile =
|
|
(span.data.$originalBestTile as TileRef | undefined) ?? null;
|
|
newBestTile = (span.data.$newBestTile as TileRef | undefined) ?? null;
|
|
}
|
|
|
|
// Compute full path if we have a selected shore
|
|
let path: TileRef[] | null = null;
|
|
if (selectedShore) {
|
|
path = PathFinding.Water(game).findPath(selectedShore, targetRef);
|
|
}
|
|
|
|
const timings = span ? extractTimings(span) : {};
|
|
|
|
return {
|
|
selectedShore: selectedShore ? tileToCoord(selectedShore, game) : null,
|
|
path: tilesToCoords(path, game),
|
|
shores: allShores.map((t) => tileToCoord(t, game)),
|
|
debug: {
|
|
candidates: tilesToCoords(candidates, game),
|
|
refinedPath: tilesToCoords(refinedPath, game),
|
|
originalBestTile: originalBestTile
|
|
? tileToCoord(originalBestTile, game)
|
|
: null,
|
|
newBestTile: newBestTile ? tileToCoord(newBestTile, game) : null,
|
|
timings,
|
|
},
|
|
};
|
|
}
|