Files
gods-eye-view/src/cyberSonar.test.mjs
T
513dc87a0e feat(hud): Cyber HUD theme with GPU contact sonar (#706)
Add the opt-in Cyber HUD layout with coordinated map and cockpit panel
styling, GPU-only contact sonar (rings, range, power, opacity, sector) with
native-rendering fallback, and set_cyber_sonar voice controls. Sonar visual
style inspired by kk376's tactical naval sonar (#214).

Co-authored-by: Sameh Khamis <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-23 20:31:56 -07:00

371 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile } from 'node:fs/promises';
import { transformWithEsbuild } from 'vite';
import { BillboardCollection, Color, PointPrimitive } from 'cesium';
import {
syncCyberSonarAnimation,
applyCyberSonarPrimitive,
cyberSonarBaseAlpha,
applyCyberSonarThemePhase,
cyberSonarAngleDeg,
cyberSonarAnimationDelay,
cyberSonarFrameDelay,
cyberSonarIntensityAtScreenPoint,
cyberSonarLabelIntensity,
cyberSonarTintColor,
applyCyberSonarSettings,
CYBER_SONAR_DIM_FACTOR,
CYBER_SONAR_LABEL_DIM_FACTOR,
CYBER_SONAR_PERIOD_MS,
CYBER_SONAR_RENDER_INTERVAL_MS,
isCyberSonarEnabled,
isCyberSonarActive,
setCyberSonarEnabled,
} from './cyberSonar.js';
test('production-transformed sonar defaults retain the document method receiver', async () => {
const source = await readFile(
new URL('./cyberSonar.js', import.meta.url),
'utf8',
);
const { code } = await transformWithEsbuild(source, 'cyberSonar.js', {
target: 'es2019',
minify: true,
});
const compiled = await import(
`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`
);
const previous = Object.getOwnPropertyDescriptor(globalThis, 'document');
const hud = { classList: { contains: (name) => name === 'active' } };
const document = {
documentElement: { dataset: { uiTheme: 'cyber' } },
body: { classList: { contains: () => false } },
getElementById(id) {
assert.equal(this, document);
assert.equal(id, 'intel-hud');
return hud;
},
};
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: document,
});
try {
assert.equal(compiled.isCyberSonarActive(), true);
assert.equal(compiled.isCyberMapThemeActive(), true);
assert.ok(
Number.isFinite(compiled.createCyberSonarSampler(800, 600).at(400, 300)),
);
document.documentElement.dataset.uiTheme = 'tactical';
assert.equal(compiled.isCyberSonarActive(), false);
assert.equal(compiled.isCyberMapThemeActive(), false);
} finally {
if (previous) Object.defineProperty(globalThis, 'document', previous);
else delete globalThis.document;
}
});
test('hue-only aircraft restyles retain owner alpha across sonar opacity changes', () => {
const collection = new BillboardCollection();
const visual = collection.add({ color: Color.WHITE.withAlpha(0.8) });
try {
for (const opacity of [0.84, 0.35, 0.81, 1, 0.84]) {
for (let i = 0; i < 8; i++) {
visual.color = Color.CYAN.withAlpha(cyberSonarBaseAlpha(visual));
applyCyberSonarPrimitive(visual, opacity);
assert.equal(visual.color.alpha, 0.8 * opacity);
}
}
// A true owner freshness update must not be mistaken for our old alpha.
visual.color = Color.YELLOW.withAlpha(0.4);
assert.equal(cyberSonarBaseAlpha(visual), 0.4);
applyCyberSonarPrimitive(visual, 0.35);
visual.color = Color.CYAN.withAlpha(cyberSonarBaseAlpha(visual));
applyCyberSonarPrimitive(visual, 0.84);
applyCyberSonarPrimitive(visual, 1, false);
assert.ok(Color.equals(visual.color, Color.CYAN.withAlpha(0.4)));
} finally {
collection.destroy();
}
});
test('the Cyber sonar clock advances clockwise and wraps cleanly', () => {
assert.equal(cyberSonarAngleDeg(0), 0);
assert.equal(cyberSonarAngleDeg(CYBER_SONAR_PERIOD_MS / 4), 90);
assert.equal(cyberSonarAngleDeg(CYBER_SONAR_PERIOD_MS), 0);
assert.equal(cyberSonarAnimationDelay(CYBER_SONAR_PERIOD_MS / 2), '-2600ms');
});
test('contact refresh cadence waits only until the next bounded sonar frame', () => {
assert.equal(CYBER_SONAR_RENDER_INTERVAL_MS, 80);
assert.equal(cyberSonarFrameDelay(1_000, 1_000), 80);
assert.equal(cyberSonarFrameDelay(1_000, 1_035), 45);
assert.equal(cyberSonarFrameDelay(1_000, 1_080), 0);
assert.equal(cyberSonarFrameDelay(Number.NEGATIVE_INFINITY, 1_000), 0);
});
test('the sweep brightens contacts inside its sector and dims the remainder', () => {
const width = 1_000;
const height = 800;
const centerX = width / 2;
const centerY = height / 2;
assert.equal(
cyberSonarIntensityAtScreenPoint(centerX, centerY - 200, width, height, 0),
1,
);
assert.equal(
cyberSonarIntensityAtScreenPoint(centerX + 200, centerY, width, height, 0),
CYBER_SONAR_DIM_FACTOR,
);
assert.equal(
cyberSonarIntensityAtScreenPoint(
centerX + 200,
centerY,
width,
height,
CYBER_SONAR_PERIOD_MS / 4,
),
1,
);
assert.equal(
cyberSonarIntensityAtScreenPoint(centerX, centerY - 401, width, height, 0),
CYBER_SONAR_DIM_FACTOR,
);
assert.equal(
cyberSonarIntensityAtScreenPoint(Number.NaN, 0, width, height, 0),
1,
);
});
test('the sonar activates only for a visible Cyber map HUD with motion enabled', () => {
const root = { dataset: { uiTheme: 'cyber' } };
const classes = (...values) => ({
contains: (value) => values.includes(value),
});
assert.equal(
isCyberSonarActive({
root,
body: { classList: classes() },
hud: { classList: classes('active') },
reducedMotion: false,
}),
true,
);
assert.equal(
isCyberSonarActive({
root,
body: { classList: classes('cockpit-mode') },
hud: { classList: classes('active') },
reducedMotion: false,
}),
false,
);
assert.equal(
isCyberSonarActive({
root,
body: { classList: classes() },
hud: { classList: classes('active') },
reducedMotion: true,
}),
false,
);
root.dataset.cyberSonar = 'off';
assert.equal(
isCyberSonarActive({
root,
body: { classList: classes() },
hud: { classList: classes('active') },
reducedMotion: false,
}),
false,
);
});
test('labels keep a more readable baseline while tracking the same sweep', () => {
assert.equal(CYBER_SONAR_DIM_FACTOR, 0.84);
assert.equal(CYBER_SONAR_LABEL_DIM_FACTOR, 0.92);
assert.equal(
cyberSonarLabelIntensity(CYBER_SONAR_DIM_FACTOR),
CYBER_SONAR_LABEL_DIM_FACTOR,
);
assert.equal(cyberSonarLabelIntensity(1), 1);
assert.ok(
cyberSonarLabelIntensity(CYBER_SONAR_DIM_FACTOR) > CYBER_SONAR_DIM_FACTOR,
);
});
test('the opacity setting controls every contact floor and preserves brighter labels', () => {
const values = new Map();
const root = {
dataset: {},
style: { setProperty: (name, value) => values.set(name, value) },
};
const settings = applyCyberSonarSettings({ opacity: 62 }, root);
assert.equal(settings.opacity, 62);
assert.equal(root.dataset.cyberSonarOpacity, '62');
assert.equal(
cyberSonarIntensityAtScreenPoint(900, 400, 1_000, 800, 0, root),
0.62,
);
assert.equal(cyberSonarLabelIntensity(0.62, root), 0.81);
assert.equal(cyberSonarLabelIntensity(1, root), 1);
applyCyberSonarSettings({ opacity: 100 }, root);
assert.equal(cyberSonarLabelIntensity(1, root), 1);
assert.deepEqual(
cyberSonarTintColor({ red: 1, green: 1, blue: 1, alpha: 1 }, 1, root),
{ red: 1, green: 0.8848, blue: 0.8848, alpha: 1 },
);
});
test('the sonar control defaults on and records explicit operator state', () => {
const root = { dataset: {} };
assert.equal(isCyberSonarEnabled(root), true);
assert.equal(setCyberSonarEnabled(false, root), false);
assert.equal(root.dataset.cyberSonar, 'off');
assert.equal(isCyberSonarEnabled(root), false);
assert.equal(setCyberSonarEnabled(true, root), true);
assert.equal(root.dataset.cyberSonar, 'on');
});
test('real Cesium points and billboards recover after repeated low/high opacity cycles', () => {
const collection = new BillboardCollection();
const base = new Color(0.7, 0.5, 0.25, 0.8);
const visuals = [
new PointPrimitive({ color: base }),
collection.add({ color: base }),
];
try {
for (const visual of visuals) {
for (const factor of [0.35, 1, 0.5, 1, 0.35]) {
for (let frame = 0; frame < 60; frame++) {
applyCyberSonarPrimitive(visual, factor);
}
assert.equal(visual.color.alpha, base.alpha * factor);
}
// Owner setters mutate the same Cesium Color object, even when the new
// alpha equals the last sonar output. Its new RGB must become the base.
const refreshed = new Color(0.2, 0.8, 0.4, visual.color.alpha);
visual.color = refreshed;
applyCyberSonarPrimitive(visual, 0.5);
assert.equal(visual.color.alpha, refreshed.alpha * 0.5);
applyCyberSonarPrimitive(visual, 1, false);
assert.ok(Color.equals(visual.color, refreshed));
}
} finally {
collection.destroy();
}
});
test('generic Cesium primitives dim, accept owner style refreshes, and restore', () => {
const makeColor = (alpha, red = 1) => ({
red,
green: 0.5,
blue: 0.25,
alpha,
clone() {
return makeColor(this.alpha, this.red);
},
});
const visual = { color: makeColor(0.8) };
applyCyberSonarPrimitive(visual, 0.25, true);
assert.equal(visual.color.alpha, 0.2);
const paintedColor = visual.color;
applyCyberSonarPrimitive(visual, 0.25, true);
assert.equal(visual.color, paintedColor);
visual.color = makeColor(0.6, 0.7);
applyCyberSonarPrimitive(visual, 0.5, true);
assert.equal(visual.color.alpha, 0.3);
assert.notEqual(visual.color.red, 0.7);
applyCyberSonarPrimitive(visual, 1, false);
assert.equal(visual.color.alpha, 0.6);
assert.equal(visual.color.red, 0.7);
});
test('Cyber contact tint moves from cool-muted to warm-bright with the sector', () => {
const root = { dataset: { cyberSonarIntensity: '70' } };
const base = { red: 0.2, green: 0.8, blue: 1, alpha: 0.75 };
const dim = cyberSonarTintColor(base, CYBER_SONAR_DIM_FACTOR, root);
const bright = cyberSonarTintColor(base, 1, root);
assert.ok(bright.red > dim.red);
assert.ok(bright.green > dim.green);
assert.equal(bright.alpha, 0.75);
});
test('unswept white map symbols retain a bright readable Cyber tint', () => {
const root = { dataset: { cyberSonarIntensity: '70' } };
const dim = cyberSonarTintColor(
{ red: 1, green: 1, blue: 1, alpha: 1 },
CYBER_SONAR_DIM_FACTOR,
root,
);
assert.ok(dim.red >= 0.72);
assert.ok(dim.green >= 0.76);
assert.ok(dim.blue >= 0.79);
assert.equal(dim.alpha, 1);
});
test('the sweep corrects delayed starts and reacquires restarted animations without per-frame writes', () => {
let queries = 0,
writes = 0;
const makeAnimation = () => ({
animationName: 'cyber-sonar-sweep',
playState: 'running',
startTime: 1800,
effect: {
updateTiming(value) {
assert.deepEqual(value, { delay: 0 });
writes++;
},
},
});
let animation = makeAnimation();
let element = {
isConnected: true,
getAnimations() {
queries++;
return [animation];
},
};
const root = { querySelector: () => element };
syncCyberSonarAnimation(root);
assert.equal(animation.startTime, 0);
for (let i = 0; i < 100; i++) syncCyberSonarAnimation(root);
assert.equal(queries, 1);
assert.equal(writes, 1);
animation.playState = 'idle';
animation = makeAnimation();
syncCyberSonarAnimation(root);
assert.equal(animation.startTime, 0);
assert.equal(writes, 2);
element.isConnected = false;
animation = makeAnimation();
element = { isConnected: true, getAnimations: () => [animation] };
syncCyberSonarAnimation(root);
assert.equal(animation.startTime, 0);
assert.equal(writes, 3);
assert.doesNotThrow(() => syncCyberSonarAnimation({}));
});
test('the selected Cyber theme seeds and releases the compositor sweep phase', () => {
const values = new Map();
const root = {
dataset: {},
style: {
setProperty: (name, value) => values.set(name, value),
removeProperty: (name) => values.delete(name),
getPropertyValue: (name) => values.get(name) || '',
},
};
applyCyberSonarThemePhase(root, true, CYBER_SONAR_PERIOD_MS / 4);
assert.equal(values.has('--cyber-sonar-angle'), false);
assert.equal(values.get('--cyber-sonar-phase-delay'), '-1300ms');
applyCyberSonarThemePhase(root, true, CYBER_SONAR_PERIOD_MS / 2);
assert.equal(values.get('--cyber-sonar-phase-delay'), '-1300ms');
applyCyberSonarThemePhase(root, false, 0);
assert.equal(values.has('--cyber-sonar-angle'), false);
assert.equal(values.has('--cyber-sonar-phase-delay'), false);
});