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

+58
View File
@@ -9,6 +9,19 @@ function element(value = '') {
return el;
}
test('sonar on/off control cannot fire after disposal', () => {
const sonarButton = element();
const calls = [];
const control = bindDisplayControls({
elements: { sonarButton },
actions: { toggleSonar: () => calls.push('toggle') },
});
sonarButton.dispatchEvent(new Event('click'));
control.destroy();
sonarButton.dispatchEvent(new Event('click'));
assert.deepEqual(calls, ['toggle']);
});
test('controls read current values without preventing native input behavior', () => {
const bloomSlider = element('24');
const hudLayout = element('tactical');
@@ -54,6 +67,51 @@ test('destroyed and replaced controls cannot issue stale actions', () => {
assert.equal(newCalls, 1);
});
test('the Cyber sonar control is an optional owned display action', () => {
const sonarButton = element();
const sonarRingsSlider = element('9');
const sonarRangeSlider = element('110');
const sonarIntensitySlider = element('65');
const sonarOpacitySlider = element('84');
const sonarSectorSlider = element('32');
const calls = [];
const control = bindDisplayControls({
elements: {
sonarButton,
sonarRingsSlider,
sonarRangeSlider,
sonarIntensitySlider,
sonarOpacitySlider,
sonarSectorSlider,
},
actions: {
toggleSonar: () => calls.push('toggle'),
setSonarRings: (value) => calls.push(['rings', value]),
setSonarRange: (value) => calls.push(['range', value]),
setSonarIntensity: (value) => calls.push(['intensity', value]),
setSonarOpacity: (value) => calls.push(['opacity', value]),
setSonarSector: (value) => calls.push(['sector', value]),
},
});
sonarButton.dispatchEvent(new Event('click'));
sonarRingsSlider.dispatchEvent(new Event('input'));
sonarRangeSlider.dispatchEvent(new Event('input'));
sonarIntensitySlider.dispatchEvent(new Event('input'));
sonarOpacitySlider.dispatchEvent(new Event('input'));
sonarSectorSlider.dispatchEvent(new Event('input'));
assert.deepEqual(calls, [
'toggle',
['rings', 9],
['range', 110],
['intensity', 65],
['opacity', 84],
['sector', 32],
]);
control.destroy();
sonarButton.dispatchEvent(new Event('click'));
assert.equal(calls.length, 6);
});
test('style, allocation and model choices retain their current data attributes', () => {
const style = element();
const allocation = element();