orbit/dev/star-art.test.mjs

166 lines
6.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* dev/star-art.test.mjs — the GALAXY tab's zoom-bloom star art (pure
* geometry in js/galaxy/GalaxyChart.js). Bare Node, no Phaser:
*
* node dev/star-art.test.mjs
*/
import { strict as assert } from 'node:assert';
import { pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const file = (p) => pathToFileURL(join(__dirname, '../js', p)).href;
// --- Load the real config (data/*.json) into the config singleton --------
const { config } = await import(file('config/Config.js'));
const fs = await import('node:fs');
const dataDir = join(__dirname, '../data');
const configData = {};
for (const f of fs.readdirSync(dataDir)) {
if (!f.endsWith('.json') || f === 'manifest.json') continue;
configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8'));
}
config.init(configData);
const {
mixHex,
starDotPx,
layerFade,
starArtSpec,
} = await import(file('galaxy/GalaxyChart.js'));
const ART = config.get('map.galaxy.stars.art', {});
const TYPES = ['main', 'redDwarf', 'binary', 'habitable', 'nebula', 'void'];
/* — dot size — */
{
const d1 = starDotPx(1, ART);
const d2 = starDotPx(2, ART);
const d10 = starDotPx(10, ART);
assert.ok(d1 >= 3, `dot at 1× should be a real dot (got ${d1})`);
assert.ok(d2 > d1, 'dot grows with zoom');
assert.ok(d10 > d2, 'dot keeps growing to max zoom');
assert.equal(starDotPx(0.5, ART), starDotPx(1, ART), 'below 1× the dot holds its floor');
console.log(' ok dot size: floor + zoom growth');
}
/* — layer fade — */
{
assert.equal(layerFade(1.0, 1.6, 0.45), 0, 'below the threshold: 0');
assert.ok(layerFade(1.61, 1.6, 0.45) > 0, 'just past the threshold: rising');
assert.equal(layerFade(3.0, 1.6, 0.45), 1, 'past the fade span: 1');
const a = layerFade(1.7, 1.6, 0.45);
const b = layerFade(1.8, 1.6, 0.45);
assert.ok(a < b, 'fade is monotonic');
console.log(' ok layer fade: 0 → 1 across the fade span');
}
/* — mixHex — */
{
assert.equal(mixHex('#000000', '#ffffff', 0), '#000000');
assert.equal(mixHex('#000000', '#ffffff', 1), '#ffffff');
const mid = mixHex('#000000', '#ffffff', 0.5);
assert.match(mid, /^#[0-9a-f]{6}$/);
assert.ok(mid !== '#000000' && mid !== '#ffffff', 'mid mix is between the ends');
console.log(' ok mixHex: endpoints + midpoint');
}
/* — below the flare threshold: a plain dot, no art — */
{
for (const t of TYPES) {
const spec = starArtSpec(t, { z: 1, time: 0, dot: 4, pulse: 0.5, cfg: ART });
assert.equal(spec.prims.length, 0, `${t}: no art below flareAt`);
}
console.log(' ok all types: plain dot below flareAt');
}
/* — every type blooms: spikes (all but the void) + a crown — */
{
const atCrown = { z: 4, time: 0, dot: 8, pulse: 0.5, cfg: ART };
for (const t of TYPES) {
const spec = starArtSpec(t, atCrown);
assert.ok(spec.prims.length > 0, `${t}: crown present at z=4`);
const tc = ART[t] ?? {};
const hasSpikes = (tc.spikes ?? 4) > 0;
assert.equal(spec.prims.some((p) => p.k === 'tri'), hasSpikes, `${t}: spike presence follows config`);
if (t === 'void') assert.ok(spec.prims.some((p) => p.k === 'darkdot'), 'void: dark horizon present');
if (t === 'binary') assert.ok(spec.prims.some((p) => p.k === 'ellipse'), 'binary: orbit ellipse present');
if (t === 'nebula') assert.equal(spec.prims.filter((p) => p.k === 'ellipse').length, 3, 'nebula: 3 disc layers');
if (t === 'habitable') assert.ok(spec.prims.filter((p) => p.k === 'dot' && p.x !== 0 || (p.k === 'dot' && p.y !== 0)).length >= 1, 'habitable: orbiting world(s) present');
}
console.log(' ok crowns: spikes + the per-type signature');
}
/* — binary: the companion rides its ellipse (and moves over time) — */
{
const base = { z: 5, dot: 8, pulse: 0.5, cfg: ART };
const s1 = starArtSpec('binary', { ...base, time: 0, seed: { compPhase: 0.3 } });
const s2 = starArtSpec('binary', { ...base, time: 7000, seed: { compPhase: 0.3 } });
const comp = (spec) => spec.prims.filter((p) => p.k === 'dot' && p.a > 0.9);
const c1 = comp(s1);
const c2 = comp(s2);
assert.ok(c1.length >= 1 && c2.length >= 1, 'companion dot present at both times');
const [a] = c1;
const [b] = c2;
assert.ok(
Math.hypot(a.x - b.x, a.y - b.y) > 1,
'the companion moves over time',
);
// on the ellipse: (x')²/rx² + (y')²/ry² ≈ 1 after undoing the rotation
const tc = ART.binary.companion;
const dot = 8;
const rx = tc.dist * dot;
const ry = rx * tc.tilt;
const onEllipse = (pt) => {
// rotation defaults: seed.orbitRot missing → 0.5 in the fallback
const rot = 0.5;
const cs = Math.cos(rot);
const sn = Math.sin(rot);
const lx = pt.x * cs + pt.y * sn;
const ly = -pt.x * sn + pt.y * cs;
const v = (lx * lx) / (rx * rx) + (ly * ly) / (ry * ry);
return Math.abs(v - 1) < 0.01;
};
assert.ok(onEllipse(a), 'companion sits on the orbit ellipse');
assert.ok(onEllipse(b), 'companion stays on the orbit ellipse over time');
console.log(' ok binary: companion orbits on its ellipse');
}
/* — habitable: the world(s) sit on their orbit radii — */
{
const spec = starArtSpec('habitable', { z: 5, time: 1000, dot: 8, pulse: 0.5, cfg: ART });
const dots = spec.prims.filter((p) => p.k === 'dot' && p.a > 0.9 && p.r >= 1.3 && p.r < 8);
assert.ok(dots.length >= 1, 'orbiting world present');
for (const d of dots) {
const rr = Math.hypot(d.x, d.y);
assert.ok(Math.abs(rr - 8 * 2.2) < 0.01 || Math.abs(rr - 8 * 1.7) < 0.01, `world on an orbit ring (r=${rr.toFixed(2)})`);
}
console.log(' ok habitable: worlds ride their zone rings');
}
/* — determinism: same input → same spec — */
{
const o = { z: 5, time: 4200, dot: 9, pulse: 0.6, cfg: ART, seed: { spikeAngle: 1.1, compPhase: 2.2 } };
const a = JSON.stringify(starArtSpec('binary', o));
const b = JSON.stringify(starArtSpec('binary', { ...o }));
assert.equal(a, b, 'specs are deterministic');
console.log(' ok deterministic specs');
}
/* — every primitive is finite + sane — */
{
for (const t of TYPES) {
const spec = starArtSpec(t, { z: 8, time: 12345, dot: 12, pulse: 0.7, cfg: ART });
for (const p of spec.prims) {
for (const v of Object.values(p)) {
if (typeof v === 'number') assert.ok(Number.isFinite(v), `${t}: finite ${JSON.stringify(p)}`);
}
if (p.a !== undefined) assert.ok(p.a >= 0 && p.a <= 1.01, `${t}: alpha in range`);
}
}
console.log(' ok all primitives finite, alphas in range');
}
console.log('star-art: all tests passed ✓');