Files
OpenFrontIO/tests/GraphicsOverrides.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

229 lines
7.7 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
GraphicsOverrides,
GraphicsOverridesSchema,
} from "core-public/GraphicsOverrides";
import { applyGraphicsOverrides } from "client/render/gl/RenderOverrides";
import { createRenderSettings } from "client/render/gl/RenderSettings";
function gen(overrides: GraphicsOverrides) {
const settings = createRenderSettings();
applyGraphicsOverrides(settings, overrides);
return settings;
}
describe("GraphicsOverridesSchema", () => {
test("accepts empty object", () => {
expect(GraphicsOverridesSchema.safeParse({}).success).toBe(true);
});
test("accepts partial name overrides", () => {
const cases = [
{ name: {} },
{ name: { nameScaleFactor: 0.8 } },
{ name: { cullThreshold: 0.02 } },
{ name: { darkNames: true } },
{ name: { nameScaleFactor: 1.2, cullThreshold: 0, darkNames: false } },
];
for (const c of cases) {
expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
}
});
test("accepts partial structure overrides", () => {
const cases = [
{ structure: {} },
{ structure: { classicIcons: true } },
{ structure: { classicIcons: false } },
{ name: { darkNames: true }, structure: { classicIcons: true } },
];
for (const c of cases) {
expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
}
});
test("accepts partial mapOverlay overrides", () => {
const cases = [
{ mapOverlay: {} },
{ mapOverlay: { territorySaturation: 0.5 } },
{ mapOverlay: { territoryAlpha: 0.8 } },
{ mapOverlay: { territorySaturation: 0, territoryAlpha: 1 } },
];
for (const c of cases) {
expect(GraphicsOverridesSchema.safeParse(c).success).toBe(true);
}
});
test("rejects wrong field types", () => {
expect(
GraphicsOverridesSchema.safeParse({ name: { nameScaleFactor: "big" } })
.success,
).toBe(false);
expect(
GraphicsOverridesSchema.safeParse({ name: { darkNames: "yes" } }).success,
).toBe(false);
expect(
GraphicsOverridesSchema.safeParse({
structure: { classicIcons: "yes" },
}).success,
).toBe(false);
expect(
GraphicsOverridesSchema.safeParse({
mapOverlay: { territorySaturation: "full" },
}).success,
).toBe(false);
});
});
describe("applyGraphicsOverrides", () => {
test("with empty overrides matches createRenderSettings defaults", () => {
const fromGen = gen({});
const fromCreate = createRenderSettings();
expect(fromGen).toEqual(fromCreate);
});
test("returns a fresh object each call (no shared mutation)", () => {
const a = gen({});
const b = gen({});
expect(a).not.toBe(b);
expect(a.name).not.toBe(b.name);
a.name.nameScaleFactor = 999;
expect(b.name.nameScaleFactor).not.toBe(999);
});
test("does not mutate the overrides input", () => {
const overrides = { name: { darkNames: true as const } };
const snapshot = JSON.parse(JSON.stringify(overrides));
gen(overrides);
expect(overrides).toEqual(snapshot);
});
test("applies nameScaleFactor override", () => {
const settings = gen({ name: { nameScaleFactor: 1.3 } });
expect(settings.name.nameScaleFactor).toBe(1.3);
});
test("applies cullThreshold override (including 0)", () => {
expect(gen({ name: { cullThreshold: 0.03 } }).name.cullThreshold).toBe(
0.03,
);
expect(gen({ name: { cullThreshold: 0 } }).name.cullThreshold).toBe(0);
});
test("darkNames=true → black fill + player-colored outline + outline RGB 0", () => {
const s = gen({ name: { darkNames: true } }).name;
expect(s.fillUsePlayerColor).toBe(false);
expect(s.outlineUsePlayerColor).toBe(true);
expect(s.outlineR).toBe(0);
expect(s.outlineG).toBe(0);
expect(s.outlineB).toBe(0);
});
test("darkNames=false → player-colored fill + white outline + outline RGB 1", () => {
const s = gen({ name: { darkNames: false } }).name;
expect(s.fillUsePlayerColor).toBe(true);
expect(s.outlineUsePlayerColor).toBe(false);
expect(s.outlineR).toBe(1);
expect(s.outlineG).toBe(1);
expect(s.outlineB).toBe(1);
});
test("only-darkNames override leaves nameScale/cull at defaults", () => {
const defaults = createRenderSettings().name;
const s = gen({ name: { darkNames: true } }).name;
expect(s.nameScaleFactor).toBe(defaults.nameScaleFactor);
expect(s.cullThreshold).toBe(defaults.cullThreshold);
});
test("combined overrides all apply together", () => {
const s = gen({
name: { nameScaleFactor: 0.9, cullThreshold: 0.01, darkNames: true },
}).name;
expect(s.nameScaleFactor).toBe(0.9);
expect(s.cullThreshold).toBe(0.01);
expect(s.fillUsePlayerColor).toBe(false);
expect(s.outlineUsePlayerColor).toBe(true);
expect(s.outlineR).toBe(0);
});
test("settings outside the name slice are untouched by name overrides", () => {
const defaults = createRenderSettings();
const s = gen({
name: { nameScaleFactor: 0.6, darkNames: true },
});
expect(s.passEnabled).toEqual(defaults.passEnabled);
expect(s.lighting).toEqual(defaults.lighting);
expect(s.structure).toEqual(defaults.structure);
});
test("classicIcons=true → light shape + dark icon + 0.75 alpha", () => {
const s = gen({
structure: { classicIcons: true },
}).structure;
// Shape (circle behind) is mostly player color, lightly darkened.
expect(s.fillDarken).toBe(1.0);
expect(s.borderDarken).toBe(0.7);
// Icon glyph itself is black.
expect(s.iconR).toBe(0);
expect(s.iconG).toBe(0);
expect(s.iconB).toBe(0);
// Slightly translucent in classic mode.
expect(s.iconAlpha).toBe(0.75);
});
test("classicIcons=false or absent → keeps render-settings.json defaults (fully opaque)", () => {
const defaults = createRenderSettings().structure;
const off = gen({
structure: { classicIcons: false },
}).structure;
expect(off.borderDarken).toBe(defaults.borderDarken);
expect(off.fillDarken).toBe(defaults.fillDarken);
expect(off.iconR).toBe(defaults.iconR);
expect(off.iconAlpha).toBe(1);
const absent = gen({ structure: {} }).structure;
expect(absent.borderDarken).toBe(defaults.borderDarken);
expect(absent.fillDarken).toBe(defaults.fillDarken);
expect(absent.iconR).toBe(defaults.iconR);
expect(absent.iconAlpha).toBe(1);
});
test("applies territorySaturation override (including 0)", () => {
expect(
gen({ mapOverlay: { territorySaturation: 0.4 } }).mapOverlay
.territorySaturation,
).toBe(0.4);
expect(
gen({ mapOverlay: { territorySaturation: 0 } }).mapOverlay
.territorySaturation,
).toBe(0);
});
test("applies territoryAlpha override (including 0)", () => {
expect(
gen({ mapOverlay: { territoryAlpha: 0.3 } }).mapOverlay.territoryAlpha,
).toBe(0.3);
expect(
gen({ mapOverlay: { territoryAlpha: 0 } }).mapOverlay.territoryAlpha,
).toBe(0);
});
test("mapOverlay override leaves other mapOverlay fields at defaults", () => {
const defaults = createRenderSettings().mapOverlay;
const mo = gen({ mapOverlay: { territorySaturation: 0.2 } }).mapOverlay;
expect(mo.territoryAlpha).toBe(defaults.territoryAlpha);
expect(mo.territoryDefenseDarken).toBe(defaults.territoryDefenseDarken);
});
test("classicIcons + name overrides compose independently", () => {
const s = gen({
name: { darkNames: true, nameScaleFactor: 0.9 },
structure: { classicIcons: true },
});
expect(s.name.fillUsePlayerColor).toBe(false);
expect(s.name.nameScaleFactor).toBe(0.9);
expect(s.structure.borderDarken).toBe(0.7);
expect(s.structure.fillDarken).toBe(1.0);
expect(s.structure.iconAlpha).toBe(0.75);
});
});