// ============ rendercheck.mjs — regression test for terrain culling ============ let fails = 0; const ok = (cond, msg) => { console.log((cond ? '✔ ' : '✘ ') + msg); if (!cond) fails++; }; globalThis.window = globalThis; globalThis.requestAnimationFrame = () => 0; globalThis.document = { getElementById: () => null, createElement: () => ({ style: {}, classList: { add() {}, remove() {} }, addEventListener() {}, appendChild() {} }), addEventListener() {}, body: { appendChild() {} }, }; globalThis.localStorage = { getItem: () => null, setItem() {}, removeItem() {} }; const R = await import('../js/render/renderer.js'); const { visibleBounds, worldToScreen, screenToWorld } = R; ok(typeof visibleBounds === 'function', 'visibleBounds exported'); // --- camera translation regression: panning MUST shift screen coords --- const camA = { x: 20, y: 20, zoom: 1 }; const camB = { x: 30, y: 26, zoom: 1 }; const [ax] = worldToScreen(camA, 1280, 800, 10, 10); const [bx] = worldToScreen(camB, 1280, 800, 10, 10); ok(Math.abs(bx - ax) > 50, `worldToScreen shifts with cam.x (${ax.toFixed(0)} → ${bx.toFixed(0)})`); const [, ay2] = worldToScreen(camA, 1280, 800, 10, 10); const camC = { x: 20, y: 30, zoom: 1 }; const [, cy2] = worldToScreen(camC, 1280, 800, 10, 10); ok(Math.abs(cy2 - ay2) > 50, `worldToScreen shifts with cam.y (${ay2.toFixed(0)} → ${cy2.toFixed(0)})`); // --- roundtrip screen→world→screen identity --- for (const cz of [0.5, 1, 1.7]) { const c = { x: 25.3, y: 41.8, zoom: cz }; const [sx, sy] = worldToScreen(c, 1280, 800, 12.4, 33.9, 0); // ground plane const [wx, wy] = screenToWorld(c, 1280, 800, sx, sy); ok(wx === 12 && wy === 33, `roundtrip zoom ${cz}: (${sx.toFixed(0)},${sy.toFixed(0)}) → (${wx},${wy}) expect (12,33)`); } // --- visibleBounds must follow the camera --- const bA = visibleBounds({ x: 5, y: 5, zoom: 1 }, 1280, 800, 80); const bB = visibleBounds({ x: 40, y: 40, zoom: 1 }, 1280, 800, 80); ok(bB.x0 > bA.x0 + 10 && bB.y0 > bA.y0 + 10, `bounds track camera (x0 ${bA.x0}→${bB.x0}, y0 ${bA.y0}→${bB.y0})`); const cases = [ { name: 'center of 52-map, zoom 1', cam: { x: 26, y: 26, zoom: 1 }, cw: 1280, ch: 800 }, { name: 'entrance area, zoom 1', cam: { x: 28, y: 44, zoom: 1 }, cw: 1280, ch: 800 }, { name: 'zoomed out 0.5', cam: { x: 26, y: 26, zoom: 0.5 }, cw: 1280, ch: 800 }, { name: 'zoomed in 2', cam: { x: 26, y: 26, zoom: 2 }, cw: 1920, ch: 1080 }, ]; for (const c of cases) { const b = visibleBounds(c.cam, c.cw, c.ch, 80); const spanX = b.x1 - b.x0, spanY = b.y1 - b.y0; // exact iso-diamond coverage: ((cw+2p)/(TW2·z) + (ch+2p)/(TH2·z)) / 2 per axis const p = 80; const expectSpan = ((c.cw + 2 * p) / (32 * c.cam.zoom) + (c.ch + 2 * p) / (16 * c.cam.zoom)) / 2; ok(spanX >= expectSpan - 2 && spanY >= expectSpan - 2, `${c.name}: span ${spanX}×${spanY} tiles (expect ≈${Math.round(expectSpan)})`); } // the old bug produced a y-span of roughly 10 rows on a 1280×800 @zoom1 view const b = visibleBounds({ x: 26, y: 26, zoom: 1 }, 1280, 800, 80); ok(b.y1 - b.y0 > 40, `vertical coverage fixed (${b.y1 - b.y0} rows, was ~10 before)`); console.log(fails ? `\n${fails} RENDER FAILURES` : '\nRENDER CULLING OK'); process.exit(fails ? 1 : 0);