149 lines
6.8 KiB
JavaScript
149 lines
6.8 KiB
JavaScript
/**
|
||
* 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);
|