// Phase 29 unit coverage for the Phase 29 fog overlay's layered // overpaint logic. `fogPaintOps` lives in `src/map/render.ts` next // to its sole consumer (`RendererHandle.setVisibilityFog`) — the // renderer dispatches each op straight onto its own Pixi `Graphics` // (one per shape) inside a per-copy `Container`, so the unit test // exercises the public ordering contract: a single fog-coloured // rectangle followed by one background-coloured circle per // visibility entry (multiplied by the torus wrap offsets when the // renderer is in torus mode). The natural rendering order unions // overlapping circles for free, replacing the earlier `cut()` // implementation that produced disconnected arc segments. import { describe, expect, test } from "vitest"; import { FOG_COLOR, fogPaintOps } from "../src/map/render"; const BG_COLOR = 0x0a0e1a; const WORLD = { width: 1000, height: 800 }; describe("fogPaintOps — no-wrap mode", () => { test("empty input returns no ops", () => { expect(fogPaintOps(WORLD, [], FOG_COLOR, BG_COLOR, "no-wrap")).toEqual([]); }); test("single circle emits fog rect + one bg circle in that order", () => { const ops = fogPaintOps( WORLD, [{ x: 100, y: 200, radius: 50 }], FOG_COLOR, BG_COLOR, "no-wrap", ); expect(ops).toEqual([ { kind: "fillRect", x: 0, y: 0, width: 1000, height: 800, color: FOG_COLOR, alpha: 1, }, { kind: "fillCircle", x: 100, y: 200, radius: 50, color: BG_COLOR, alpha: 1, }, ]); }); test("multiple circles produce one fog rect followed by N bg circles", () => { const ops = fogPaintOps( WORLD, [ { x: 100, y: 100, radius: 50 }, { x: 300, y: 200, radius: 80 }, { x: 500, y: 600, radius: 30 }, ], FOG_COLOR, BG_COLOR, "no-wrap", ); expect(ops.length).toBe(4); expect(ops[0].kind).toBe("fillRect"); for (let i = 1; i < ops.length; i++) { expect(ops[i].kind).toBe("fillCircle"); const op = ops[i]; if (op.kind === "fillCircle") { expect(op.color).toBe(BG_COLOR); expect(op.alpha).toBe(1); } } }); test("zero or negative world dimensions return no ops", () => { expect( fogPaintOps( { width: 0, height: 800 }, [{ x: 0, y: 0, radius: 10 }], FOG_COLOR, BG_COLOR, "no-wrap", ), ).toEqual([]); expect( fogPaintOps( { width: 1000, height: -1 }, [{ x: 0, y: 0, radius: 10 }], FOG_COLOR, BG_COLOR, "no-wrap", ), ).toEqual([]); }); }); describe("fogPaintOps — torus mode", () => { test("each circle is emitted at nine wrapped positions", () => { const ops = fogPaintOps( WORLD, [{ x: 100, y: 200, radius: 50 }], FOG_COLOR, BG_COLOR, "torus", ); // 1 fog rect + 9 wrapped circles. expect(ops.length).toBe(10); expect(ops[0].kind).toBe("fillRect"); const positions = ops .slice(1) .map((op) => (op.kind === "fillCircle" ? `${op.x},${op.y}` : "")) .sort(); // Every neighbour offset is emitted with width=1000 / height=800. const expected: string[] = []; for (const dx of [-1, 0, 1]) { for (const dy of [-1, 0, 1]) { expected.push(`${100 + dx * 1000},${200 + dy * 800}`); } } expected.sort(); expect(positions).toEqual(expected); }); test("multiple circles produce 9 × N wrapped circles after the fog rect", () => { const ops = fogPaintOps( WORLD, [ { x: 100, y: 100, radius: 50 }, { x: 700, y: 600, radius: 30 }, ], FOG_COLOR, BG_COLOR, "torus", ); // 1 fog rect + (9 wraps × 2 circles) = 19 ops. expect(ops.length).toBe(19); expect(ops[0].kind).toBe("fillRect"); // Each circle keeps its own radius across every wrap. const radii = ops .slice(1) .map((op) => (op.kind === "fillCircle" ? op.radius : 0)) .filter((r) => r > 0); expect(radii.filter((r) => r === 50).length).toBe(9); expect(radii.filter((r) => r === 30).length).toBe(9); }); test("a circle near the right edge produces a wrapped copy past the seam", () => { // Planet at (950, 400) with radius 300 — the painted area // extends to x = 1250 in the central tile. In torus mode the // renderer also draws a wrapped circle at (950 - 1000, 400) = // (-50, 400) so the next tile (with its own fog rect) keeps a // matching unfogged hole at the seam — this is the fix for // the "sector" artifact at the wrap boundary. const ops = fogPaintOps( WORLD, [{ x: 950, y: 400, radius: 300 }], FOG_COLOR, BG_COLOR, "torus", ); const xs = ops .slice(1) .map((op) => (op.kind === "fillCircle" ? op.x : 0)); expect(xs).toContain(-50); expect(xs).toContain(950); expect(xs).toContain(1950); }); test("empty input still returns no ops in torus mode", () => { expect(fogPaintOps(WORLD, [], FOG_COLOR, BG_COLOR, "torus")).toEqual([]); }); });