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]>
This commit is contained in:
authored and GitHub committed 2026-09-23 20:31:56 -07:00
1 parent f913066cf2
commit 513dc87a0e
74 files changed
+7320 -90

No files matched your search

+15 -5
View File
@@ -25,11 +25,11 @@ const css = readStylesheet(new URL('../style.css', import.meta.url));
function realtimeTools() { return GEV_REALTIME_TOOLS; }
test('Realtime schema exposes the authoritative 29-tool inventory', () => {
test('Realtime schema exposes the authoritative 30-tool inventory', () => {
const tools = realtimeTools();
assert.equal(tools.length, 29);
assert.equal(tools.length, 30);
const names = tools.map((tool) => tool.name);
assert.equal(new Set(names).size, 29, 'tool names are unique');
assert.equal(new Set(names).size, 30, 'tool names are unique');
assert.ok(names.includes('set_context_mode'));
assert.ok(names.includes('control_cockpit'));
assert.ok(names.includes('select_nearest_aircraft'));
@@ -173,6 +173,7 @@ test('no unchanged Realtime tool definition drifts silently', () => {
// in the mic-test brief which tools moved — the session cache busts on any
// schema change.
const TOUCHED = new Set([
'set_cyber_sonar',
'set_context_mode',
'control_cockpit',
'set_panel_open',
@@ -188,7 +189,13 @@ test('no unchanged Realtime tool definition drifts silently', () => {
]);
const unchanged = realtimeTools()
.filter((tool) => !TOUCHED.has(tool.name))
.sort((a, b) => a.name.localeCompare(b.name));
.sort((a, b) => a.name.localeCompare(b.name))
.map((tool) => structuredClone(tool))
.filter((tool) => tool.name !== 'set_cyber_sonar');
// Cyber adds one HUD choice and the sonar tool; retain the existing pin for every legacy field.
const hudLayout = unchanged.find((tool) => tool.name === 'set_hud').parameters.properties.layout;
assert.deepEqual(hudLayout.enum, ['tactical', 'operator', 'minimal', 'cyber']);
hudLayout.enum = hudLayout.enum.filter((layout) => layout !== 'cyber');
assert.equal(unchanged.length, 18);
const digest = createHash('sha256')
.update(JSON.stringify(unchanged))
@@ -274,7 +281,10 @@ test('Radio is nested inside Context with separate disclosure and power controls
assert.match(radioControlsSource, /classList\.remove\('radio-broadcasting'\)/);
assert.match(radioBindings, /this\.radio\.getTunerStations\(750\)/);
assert.match(radioBindings, /radioTunerPointerPosition\(/);
assert.doesNotMatch(css, /#right-context-rail\s*>\s*#radio-panel/);
// Only the Cyber skin promotes Radio to a peer panel. Other themes retain
// the embedded Context layout; theme round-trip behavior has separate tests.
const baseCss = css.replace(readFileSync(new URL('./ui/styles/cyber.css', import.meta.url), 'utf8'), '');
assert.doesNotMatch(baseCss, /#right-context-rail\s*>\s*#radio-panel/);
assert.match(css, /#global-context-panel #radio-panel\.collapsed/);
assert.doesNotMatch(css, /\.context-radio-dock\.active:hover \.context-radio-mini/);
assert.doesNotMatch(css, /\.context-radio-dock\.active:focus-within \.context-radio-mini/);