mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
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]>
371 lines
12 KiB
JavaScript
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);
|
|
});
|