/** * SystemChart test (dev tool, run with Node — no browser needed): * * node dev/system-chart.test.mjs * * Asserts the pure geometry + stats behind the deck's MAP console * (js/galaxy/SystemChart.js): * - chartBounds: the furthest X/Y extents of the object set (object * EDGE = center ± radius), symmetric padding on every edge, the * degenerate empty-set case (a 2×padding box about the origin); * - fitToRect: uniform scale that fits the frame into the plate, * centred — toX/toY land the bounds centre on the plate centre, the * whole frame inside the plate, aspect preserved; * - navDiscoveryStats: the SYSTEM DISCOVERY share over the system's NAV * points (planets + space stations + gates) MINUS the central body * ('home') — found/total + pct; 0 total → pct 0; * - resourceStats: the SYSTEM RESOURCES share over the asteroid fields * (content.asteroids) — found/total + pct; missing list → 0/0. */ import { chartBounds, fitToRect, navDiscoveryStats, resourceStats } from '../js/galaxy/SystemChart.js'; let passed = 0; let failed = 0; function ok(cond, label) { if (cond) { passed++; console.log(` ok ${label}`); } else { failed++; console.error(`FAIL ${label}`); } } function near(a, b, eps = 1e-9) { return Math.abs(a - b) <= eps; } // ── chartBounds ──────────────────────────────────────────────────────────── console.log('chartBounds'); { const b = chartBounds( [ { x: 100, y: -50, radius: 30 }, { x: -200, y: 40, radius: 10 }, { x: 0, y: 500, radius: 200 }, ], 1000 ); ok(near(b.minX, -210 - 1000), 'minX = leftmost edge − padding'); ok(near(b.maxX, 0 + 200 + 1000), 'maxX = rightmost edge (the 200-radius object) + padding'); ok(near(b.minY, -50 - 30 - 1000), 'minY = top edge − padding'); ok(near(b.maxY, 500 + 200 + 1000), 'maxY = bottom edge + padding'); ok(near(b.w, b.maxX - b.minX) && near(b.h, b.maxY - b.minY), 'w/h consistent'); ok(near(b.cx, (b.minX + b.maxX) / 2) && near(b.cy, (b.minY + b.maxY) / 2), 'centre = midpoint'); } { // default padding (the design rule: ~1024 px on every edge) const b = chartBounds([{ x: 0, y: 0, radius: 500 }], 1024); ok(near(b.minX, -1524) && near(b.maxX, 1524), '±(radius + 1024) about the origin'); } { const b = chartBounds([], 512); ok(near(b.minX, -512) && near(b.maxX, 512) && near(b.minY, -512) && near(b.maxY, 512), 'empty set → a 2×padding box about the origin'); } { const b = chartBounds([{ x: 0, y: 0, radius: 10 }, { bogus: true }], 100); ok(near(b.minX, -110), 'non-object entries ignored'); } // ── fitToRect ────────────────────────────────────────────────────────────── console.log('fitToRect'); { const bounds = { minX: -1000, minY: -500, maxX: 3000, maxY: 1500, w: 4000, h: 2000, cx: 1000, cy: 500 }; const tf = fitToRect(bounds, 800, 600); // uniform scale = min(800,600)/max(4000,2000) = 600/4000 = 0.15 ok(near(tf.scale, 0.15), 'scale = min(w,h) / max(bounds w,h)'); ok(near(tf.toX(1000), 400), 'toX: bounds centre → plate centre x'); ok(near(tf.toY(500), 300), 'toY: bounds centre → plate centre y'); ok(tf.toX(-1000) >= 0 && tf.toX(3000) <= 800, 'frame x inside plate'); ok(tf.toY(-500) >= 0 && tf.toY(1500) <= 600, 'frame y inside plate'); // aspect preserved: a 100×100 world square maps to a 15×15 px square const x0 = tf.toX(0); const y0 = tf.toY(0); ok(near(tf.toX(100) - x0, tf.toY(100) - y0, 1e-9), 'uniform scale — x and y move equally'); } { // wider-than-tall plate: the frame is limited by WIDTH const bounds = { minX: -100, minY: -100, maxX: 100, maxY: 100, w: 200, h: 200, cx: 0, cy: 0 }; const tf = fitToRect(bounds, 1000, 200); ok(near(tf.scale, 1), 'width-constrained: scale = h / bounds.h'); ok(near(tf.toX(0), 500) && near(tf.toY(0), 100), 'centred'); ok(near(tf.toX(100) - tf.toX(-100), 200), 'frame width preserved'); } // ── navDiscoveryStats ────────────────────────────────────────────────────── console.log('navDiscoveryStats'); { const content = { planets: [{ name: 'A' }, { name: 'B' }, { name: 'C' }], settlements: [ { id: 's1', anchor: { type: 'space' } }, { id: 's2', anchor: { type: 'surface' } }, // not a NAV point (planet-anchored) ], jumps: [{ id: 'g1' }], }; const foundSet = new Set(['A', 'g1']); const discovery = { isDiscovered: (sysId, id) => foundSet.has(id) }; const st = navDiscoveryStats(discovery, 'sys1', content); ok(st.total === 5, 'total = 3 planets + 1 space station + 1 gate (central body excluded, surface settlement excluded)'); ok(st.found === 2, 'found counts the discovered NAV points'); ok(near(st.pct, 2 / 5), 'pct = found/total'); } { const st = navDiscoveryStats({ isDiscovered: () => true }, 'sys', { planets: [], settlements: [], jumps: [] }); ok(st.total === 0 && st.found === 0 && st.pct === 0, 'no NAV points → 0/0, pct 0'); } { // the central body is discoverable ('home') but NOT counted — the // readout rule: planets + stations + gates only. const content = { planets: [{ name: 'A' }], settlements: [], jumps: [] }; const st = navDiscoveryStats({ isDiscovered: () => true }, 'sys', content); ok(st.total === 1, 'home body never enters the total'); } { const st = navDiscoveryStats(null, 'sys', { planets: [{ name: 'A' }] }); ok(st.total === 1 && st.found === 0 && st.pct === 0, 'null discovery state → nothing found, no crash'); } // ── resourceStats ────────────────────────────────────────────────────────── console.log('resourceStats'); { const content = { asteroids: [{ id: 'r1' }, { id: 'r2' }, { id: 'r3' }] }; const foundSet = new Set(['r2']); const st = resourceStats({ isDiscovered: (sysId, id) => foundSet.has(id) }, 'sys', content); ok(st.total === 3, 'total = asteroid cluster count'); ok(st.found === 1 && near(st.pct, 1 / 3), 'found/pct over the clusters'); } { const st = resourceStats({ isDiscovered: () => true }, 'sys', {}); ok(st.total === 0 && st.pct === 0, 'no asteroid fields → 0/0'); } { const st = resourceStats(null, 'sys', { asteroids: [{ id: 'r1' }] }); ok(st.total === 1 && st.found === 0, 'null discovery state → nothing found'); } // ── result ───────────────────────────────────────────────────────────────── console.log(`\n${passed} passed, ${failed} failed`); if (failed > 0) process.exit(1);