/** * 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 ✓');