Remove the Operator HUD layout

Display > HUD > Layout and the set_hud voice action now offer Tactical,
Minimal and Cyber. The Operator option, its CSS and its QA coverage are
removed, and the unused "Operator" translations are pruned.

Share-link codes are append-only, so operator keeps its hudVariant code.
A link, scene or saved state that names it restores the default Tactical
layout. The voice schema pins keep their legacy digests by restoring the
retired value before comparing; the full-payload digest is re-derived.
This commit is contained in:
Said Turksever committed 2026-10-29 10:41:58 +00:00
1 parent aaa3ff46cf
commit 8b2c699370
26 files changed
+78 -147

No files matched your search

+4 -8
View File
@@ -1022,14 +1022,10 @@ test('the voice TOOL SCHEMA matches the pinned contract — the mission mapping
);
const hudLayout = legacyTools.find((tool) => tool.name === 'set_hud')
.parameters.properties.layout;
assert.deepEqual(hudLayout.enum, [
'tactical',
'operator',
'minimal',
'cyber',
]);
// Cyber deliberately adds one layout; first-run missions still change no tools.
hudLayout.enum = hudLayout.enum.filter((layout) => layout !== 'cyber');
assert.deepEqual(hudLayout.enum, ['tactical', 'minimal', 'cyber']);
// Cyber deliberately adds one layout and Operator was retired; first-run
// missions still change no tools.
hudLayout.enum = ['tactical', 'operator', 'minimal'];
const cameraMotion = legacyTools.find((tool) => tool.name === 'move_camera')
.parameters.properties.motion;
assert.deepEqual(cameraMotion.enum, [
+2 -2
View File
@@ -9,7 +9,7 @@ import { applicationServices } from './services/application.js';
* real-time at configurable cadences.
*
* The HUD auto-activates when a military-style shader (NVG, FLIR, CRT) is
* selected and supports three layout variants: tactical, operator, minimal.
* selected and supports the layout variants: tactical, minimal, cyber.
*
* Color theming is driven by the active shader mode via CSS custom properties.
*/
@@ -1063,7 +1063,7 @@ export class IntelHUD {
* Switch the HUD layout variant. Falls back to `'tactical'` if the
* name is unrecognized. Cyber also applies a coordinated skin to the full
* application shell; leaving Cyber restores the standard shell tokens.
* @param {string} variantName - One of `'tactical'`, `'operator'`, `'minimal'`, `'cyber'`.
* @param {string} variantName - One of `'tactical'`, `'minimal'`, `'cyber'`.
*/
setVariant(variantName) {
this._variant = normalizeHudLayout(variantName);
+1 -6
View File
@@ -1,7 +1,2 @@
/** Browser-free layout vocabulary shared by UI and server-side voice tools. */
export const HUD_LAYOUTS = Object.freeze([
'tactical',
'operator',
'minimal',
'cyber',
]);
export const HUD_LAYOUTS = Object.freeze(['tactical', 'minimal', 'cyber']);
-1
View File
@@ -76,7 +76,6 @@
"Layout": "Disposición",
"HUD layout": "Disposición del HUD",
"Tactical": "Táctica",
"Operator": "De operador",
"Minimal": "Mínima",
"Detection Overlay (D)": "Superposición de detección (D)",
"Detection overlay": "Superposición de detección",
-1
View File
@@ -777,7 +777,6 @@
"Fade": "Затухание",
"Outside": "Снаружи",
"Tactical": "Тактический",
"Operator": "Операторский",
"Minimal": "Минимальный",
"HUD layout": "Макет HUD",
"Layout": "Макет",
-1
View File
@@ -2616,7 +2616,6 @@
"Layout": "布局",
"HUD layout": "HUD 布局",
"Tactical": "战术",
"Operator": "操作员",
"Minimal": "精简",
"Cyber": "赛博",
"Sonar": "声呐",
-1
View File
@@ -2604,7 +2604,6 @@
"Layout": "佈局",
"HUD layout": "HUD 佈局",
"Tactical": "戰術",
"Operator": "操作員",
"Minimal": "精簡",
"Cyber": "賽博",
"Sonar": "聲吶",
+1 -1
View File
@@ -48,7 +48,7 @@ test('setup placement moves the same button only in Cyber and disconnects on tea
chip,
);
assert.equal(chip.parentNode, home);
for (const variant of ['cyber', 'operator', 'cyber', 'minimal']) {
for (const variant of ['cyber', 'tactical', 'cyber', 'minimal']) {
theme.dataset.uiTheme = variant;
notify();
assert.equal(chip.parentNode, variant === 'cyber' ? toolbar : home);
+4 -3
View File
@@ -206,10 +206,11 @@ test('no unchanged Realtime tool definition drifts silently', () => {
.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.
// Cyber adds one HUD choice and the sonar tool, and Operator was retired;
// 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.deepEqual(hudLayout.enum, ['tactical', 'minimal', 'cyber']);
hudLayout.enum = ['tactical', 'operator', 'minimal'];
// Globe rotation adds one motion and clarifies the two affected descriptions.
// Check those edits explicitly, then keep the legacy pin for every other field.
const camera = unchanged.find((tool) => tool.name === 'move_camera');
+6 -3
View File
@@ -437,9 +437,12 @@ test('defaults travel as frozen meanings that match today’s app defaults', ()
assert.equal(defaults.bloomIntensity, BLOOM_INTENSITY_DEFAULT);
assert.equal(defaults.bloomVersion, BLOOM_SCALE_VERSION);
assert.equal(defaults.hudVariant, HUD_LAYOUTS[0]);
assert.deepEqual(SHARE_VIEW_ENUMS.hudVariant.slice(0, HUD_LAYOUTS.length), [
...HUD_LAYOUTS,
]);
// Codes are append-only: the retired Operator layout keeps its code so old
// links still parse, and the app restores them as Tactical.
assert.deepEqual(
SHARE_VIEW_ENUMS.hudVariant.filter((variant) => variant !== 'operator'),
[...HUD_LAYOUTS],
);
assert.deepEqual(SHARE_VIEW_ENUMS.style, [...VIEW_STYLES]);
assert.deepEqual(SHARE_VIEW_ENUMS.mapStack, [...VIEW_MAPS]);
assert.deepEqual(SHARE_VIEW_ENUMS.detectionAllocation, [
+1 -1
View File
@@ -1337,7 +1337,7 @@ export class StyleManager extends ShellFacade {
/**
* Switches the HUD layout variant.
* @param {'tactical'|'operator'|'minimal'|'cyber'} variantName - Layout variant.
* @param {'tactical'|'minimal'|'cyber'} variantName - Layout variant.
* @returns {{ok: boolean, layout?: string, visible?: boolean, error?: string}}
*/
setHudLayout(variantName) {
+1 -1
View File
@@ -72,7 +72,7 @@ test('opening already-expanded Radio reveals Context and closes Display', () =>
});
test('non-Cyber explicit panels and restored preferences retain their existing policy', () => {
for (const theme of ['tactical', 'operator', 'minimal', 'cyber']) {
for (const theme of ['tactical', 'minimal', 'cyber']) {
const f = fixture(theme), prior = globalThis.document;
globalThis.document = f.doc;
try {
+1 -1
View File
@@ -132,7 +132,7 @@ test('rejects the whole invalid request before any mutation', () => {
});
test('requires Cyber and preserves off state and every omitted setting', () => {
for (const theme of ['tactical', 'operator', 'minimal']) {
for (const theme of ['tactical', 'minimal']) {
const f = fixture(theme);
assert.equal(setCyberSonarControls(f.owner, { enabled: true }, f.doc).ok, false);
assert.deepEqual(f.calls, []);
+20 -17
View File
@@ -54,7 +54,8 @@ function styleOwner(t, initialVariant = 'cyber') {
const hud = {
getVariant: () => variant,
setVariant: (value) => {
variant = value;
// Normalize like the real HUD so retired layouts fall back.
variant = normalizeHudLayout(value);
applyHudUiTheme(root, value);
},
setMode: (value) => calls.push(['mode', value]),
@@ -131,7 +132,7 @@ test('visual styles retain Cyber through real preset application without droppin
test('non-Cyber style presets and explicit HUD choices remain authoritative', (t) => {
const { owner, hud, root } = styleOwner(t);
for (const variant of ['tactical', 'operator', 'minimal']) {
for (const variant of ['tactical', 'minimal']) {
owner._setHudVariant(variant);
assert.equal(hud.getVariant(), variant);
assert.equal(root.dataset.uiTheme, undefined);
@@ -150,8 +151,8 @@ test('leaving Cyber without a preset choice restores the prior visual preset', (
assert.equal(owner._preCyberStyle, 'noir');
assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'operator');
owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'minimal');
assert.equal(owner.activeStyle, 'noir');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
@@ -170,8 +171,8 @@ test('a preset explicitly selected in Cyber survives exit and resets for re-entr
// Reselecting the active mode must not erase the user's preset intent.
owner._setHudVariant('cyber', { applyVisualDefaults: true });
assert.equal(owner._preCyberStyle, 'noir');
owner._setHudVariant('operator', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'operator');
owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'minimal');
assert.equal(owner.activeStyle, 'snow');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
@@ -192,7 +193,7 @@ test('an explicit same-value FLIR choice is preserved on Cyber exit', (t) => {
owner.setStyle('thermal', { userInitiated: true });
assert.equal(owner.activeStyle, 'thermal');
assert.equal(owner._cyberStyleUserSelected, true);
owner._setHudVariant('operator', { applyVisualDefaults: true });
owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'thermal');
});
@@ -207,7 +208,7 @@ test('an authoritative same-Cyber reset clears local preset ownership', (t) => {
assert.equal(owner.activeStyle, 'snow');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true });
owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'snow');
});
@@ -241,9 +242,9 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author
assert.equal(display.owner.activeStyle, 'thermal');
snowButton.dispatchEvent(new Event('click'));
assert.equal(display.owner.activeStyle, 'snow');
hudLayout.value = 'operator';
hudLayout.value = 'minimal';
hudLayout.dispatchEvent(new Event('change'));
assert.equal(display.hud.getVariant(), 'operator');
assert.equal(display.hud.getVariant(), 'minimal');
assert.equal(display.owner.activeStyle, 'snow');
controls.destroy();
@@ -266,9 +267,9 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author
const selected = await runVoiceAction('set_visual_style', { style: 'snow' });
assert.equal(selected.ok, true);
assert.equal(voice.owner.activeStyle, 'snow');
const exited = await runVoiceAction('set_hud', { layout: 'operator' });
const exited = await runVoiceAction('set_hud', { layout: 'minimal' });
assert.equal(exited.ok, true);
assert.equal(voice.hud.getVariant(), 'operator');
assert.equal(voice.hud.getVariant(), 'minimal');
assert.equal(voice.owner.activeStyle, 'snow');
});
@@ -284,7 +285,7 @@ test('scope choice and expanded feather survive every HUD layout and both restor
assert.equal(owner._scopeBtn.checked, false);
assert.equal(owner._scopeFeatherSlider.disabled, true);
assert.equal(owner._scopeFeatherSlider.value, '200');
for (const layout of ['minimal', 'operator', 'cyber', 'tactical']) {
for (const layout of ['minimal', 'cyber', 'tactical']) {
owner._setHudVariant(layout, { applyVisualDefaults: true });
assert.equal(isScopeMaskEnabled(), false, layout);
assert.equal(getScopeMaskFeather(), 2, layout);
@@ -313,7 +314,7 @@ test('scene and share HUD transitions do not restore a local pre-Cyber preset',
assert.equal(owner.activeStyle, 'retro');
assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true });
owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'retro');
owner.setStyle('surveillance');
@@ -342,7 +343,8 @@ test('scene and share restoration override Cyber and preserve restored Cyber par
style: 'surveillance',
hudVariant: 'operator',
});
assert.equal(hud.getVariant(), 'operator');
// A link saved with the retired Operator layout restores as Tactical.
assert.equal(hud.getVariant(), 'tactical');
await owner.restoreShareState({
style: 'thermal',
hudVariant: 'cyber',
@@ -564,8 +566,9 @@ test('Cyber side panels share one width and one framed surface material', () =>
});
test('Cyber is a canonical HUD layout with a safe Tactical fallback', () => {
assert.deepEqual(HUD_LAYOUTS, ['tactical', 'operator', 'minimal', 'cyber']);
assert.deepEqual(HUD_LAYOUTS, ['tactical', 'minimal', 'cyber']);
assert.equal(normalizeHudLayout('CYBER'), 'cyber');
assert.equal(normalizeHudLayout('operator'), 'tactical');
assert.equal(normalizeHudLayout('unknown'), 'tactical');
});
@@ -597,7 +600,7 @@ test('an explicit transition into Cyber selects FLIR with Ironbow 0.42 once', ()
);
assert.equal(cyberVisualDefaultsForHudTransition('tactical', 'cyber'), null);
assert.equal(
cyberVisualDefaultsForHudTransition('cyber', 'operator', {
cyberVisualDefaultsForHudTransition('cyber', 'minimal', {
explicit: true,
}),
null,
+1 -1
View File
@@ -39,7 +39,7 @@ test('Cyber restores one expanded owner, keeps launchers, and restores other the
true,
'layout does not reopen a peer',
);
f.options.hud.variant = 'operator';
f.options.hud.variant = 'minimal';
f.run();
assert.equal(f.first.classList.contains('collapsed'), false);
assert.equal(
+1 -8
View File
@@ -463,17 +463,10 @@ body:not(.ui-clean-view):not(.recording-mode):has(
}
#intel-hud[data-variant='tactical'] .hud-bottom-left,
#intel-hud[data-variant='tactical'] .hud-bottom-right,
#intel-hud[data-variant='operator'] .hud-bottom-left,
#intel-hud[data-variant='operator'] .hud-bottom-right {
#intel-hud[data-variant='tactical'] .hud-bottom-right {
bottom: calc(2vh + 6.5rem);
}
#intel-hud[data-variant='operator'] .hud-bottom-bar {
bottom: calc(2vh + 9.8rem);
width: min(48vw, 680px);
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 5.4rem);
}
+9 -62
View File
@@ -303,49 +303,11 @@
display: none;
}
#intel-hud[data-variant='operator'] .hud-top-bar,
#intel-hud[data-variant='operator'] .hud-bottom-bar {
display: flex;
}
#intel-hud[data-variant='operator'] .hud-top-bar {
top: max(10vh, 90px);
left: 50%;
right: auto;
width: min(52vw, 720px);
transform: translateX(-50%);
border-color: rgba(255, 255, 255, 0.24);
background: linear-gradient(90deg, rgba(0, 0, 0, 0.56), rgba(8, 26, 34, 0.5));
box-shadow: 0 0 24px rgba(0, 255, 255, 0.08);
}
#intel-hud[data-variant='tactical'] .hud-top-left,
#intel-hud[data-variant='tactical'] .hud-top-right,
#intel-hud[data-variant='operator'] .hud-top-left,
#intel-hud[data-variant='operator'] .hud-top-right {
#intel-hud[data-variant='tactical'] .hud-top-right {
top: max(14vh, 136px);
}
#intel-hud[data-variant='operator'] .hud-bottom-bar {
border-color: rgba(255, 255, 255, 0.2);
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.52),
rgba(8, 22, 18, 0.52)
);
}
#intel-hud[data-variant='operator'] .hud-corner {
padding: 8px 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 8px;
background: rgba(0, 0, 0, 0.36);
}
#intel-hud[data-variant='operator'] .hud-bracket {
display: none;
}
#intel-hud[data-variant='minimal'] .hud-top-right,
#intel-hud[data-variant='minimal'] .hud-bottom-right,
#intel-hud[data-variant='minimal'] .hud-left-edge,
@@ -378,11 +340,7 @@
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
#intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
.hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) .scene-text-holdout {
background: rgba(0, 0, 0, 0.7);
@@ -399,11 +357,7 @@
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
#intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
.hud-readout {
display: block;
width: max-content;
@@ -413,11 +367,7 @@
}
:root[data-scope-mask='off']
#intel-hud:is(
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
#intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
:is(.hud-top-right, .hud-bottom-right)
.hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber'])
@@ -427,29 +377,26 @@
}
/* Preserve glyph opacity without dimming the vertical backplates. */
:root[data-scope-mask='off']
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
.hud-edge {
:root[data-scope-mask='off'] #intel-hud[data-variant='tactical'] .hud-edge {
opacity: 1;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
#intel-hud[data-variant='tactical']
.hud-edge
.hud-readout
> * {
opacity: 0.5;
}
:root[data-scope-mask='off'] #intel-hud[data-variant='operator'] .hud-corner,
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
#intel-hud[data-variant='minimal']
:is(.hud-top-bar, .hud-bottom-bar) {
background: transparent;
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
#intel-hud[data-variant='minimal']
:is(.hud-top-bar, .hud-bottom-bar) {
width: fit-content;
max-width: calc(100vw - 120px);
@@ -457,7 +404,7 @@
}
:root[data-scope-mask='off']
#intel-hud:is([data-variant='minimal'], [data-variant='operator'])
#intel-hud[data-variant='minimal']
:is(.hud-top-bar, .hud-bottom-bar)
.hud-readout {
overflow: hidden;
-7
View File
@@ -110,13 +110,6 @@
font-size: 9px;
}
#intel-hud[data-variant='operator'] .hud-top-bar {
left: 12px;
right: auto;
width: calc(100vw - 24px);
transform: none;
}
.hud-bottom-bar {
bottom: 90px;
font-size: 9px;
-1
View File
@@ -15,7 +15,6 @@
<span class="pp-slider-mini-label" data-i18n>Layout</span>
<select id="hud-layout-select" class="pp-select gev-field gev-field--compact" aria-label="HUD layout" data-i18n-aria-label>
<option value="tactical" data-i18n>Tactical</option>
<option value="operator" data-i18n>Operator</option>
<option value="minimal" data-i18n>Minimal</option>
<option value="cyber" data-i18n>Cyber</option>
</select>
+5 -3
View File
@@ -45,7 +45,8 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea
// S2 GhostMaps extends the generated layer enums.
// Panel adds named voice targets and timeout; shared Panel/Cockpit guidance
// is consolidated without changing either feature's arguments.
'9ee82c4214499280c208c93751b19d7b724c81b7727a2d0c6dab0184749fbe15',
// The Operator HUD layout leaves set_hud's layout enum.
'8c3c26336e4820b4b0828ab2bedf70050eca91ea3b0517a62cd8b69ea4fefc92',
);
});
@@ -166,9 +167,10 @@ test('all legacy action arguments are byte-identical after removing the delibera
delete center.properties[axis].minimum;
delete center.properties[axis].maximum;
}
// Cyber adds one HUD layout.
// Cyber adds one HUD layout and Operator was retired.
const hud = property('set_hud').layout;
hud.enum = hud.enum.filter((layout) => layout !== 'cyber');
assert.deepEqual(hud.enum, ['tactical', 'minimal', 'cyber']);
hud.enum = ['tactical', 'operator', 'minimal'];
// Derived by applying the same removals to the 4b56d0e9 actionSchemas.
assert.equal(
createHash('sha256').update(JSON.stringify(legacy)).digest('hex'),