diff --git a/changelog.d/1467-remove-operator-hud-layout.md b/changelog.d/1467-remove-operator-hud-layout.md new file mode 100644 index 000000000..556d551ee --- /dev/null +++ b/changelog.d/1467-remove-operator-hud-layout.md @@ -0,0 +1,3 @@ +- Remove the Operator HUD layout. Display > HUD > Layout and the HUD voice + action now offer Tactical, Minimal and Cyber; share links, scenes and saved + views that name Operator open in Tactical (asturksever, #1467). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index e854d2c3c..05a33e0b9 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -221,7 +221,7 @@ from 0–200%. OFF disables the slider while retaining its value. HUD layout changes preserve both settings; share links and scenes restore them. The historical missing-`scf` fallback remains 35%; explicit 0% keeps the hard edge. -With Scope OFF, Tactical, Operator and Minimal add text-sized black backplates +With Scope OFF, Tactical and Minimal add text-sized black backplates at 70% opacity behind telemetry, vertical metadata and scene chrome. Original HUD text color, glow and opacity are preserved. Scope OFF brightens the tagline and Active Style label with the shared secondary text color. The title plate @@ -231,6 +231,15 @@ the original HUD presentation; Cyber retains its existing skin. `node scripts/qa-scope-hud.mjs` checks native input, range, layout persistence, restoration and both Scope presentations. `QA_SCREENSHOT_DIR` saves screenshots. +## HUD layouts — October 29, 2026 + +Display > HUD > Layout offers Tactical, Minimal and Cyber; the `set_hud` voice +action accepts the same three. The Operator layout is retired. Share links +keep its enum code so older links still parse, and a link, scene or saved state +that names Operator restores the default Tactical layout. Operator was the +only layout that showed the HUD's top banner bar, so its markup and styles are +removed too. + ## Cyber HUD — September 30, 2026 Display > HUD > Layout includes Cyber, also available through the HUD voice @@ -2929,7 +2938,7 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with > corridor). Its minimal Display popover exposes Intel HUD, > Detection, Parameters, and 3D aircraft. During Cockpit, those existing > standard Display controls move into the Cockpit popover and retain their normal -> nested interaction: HUD plus Tactical/Operator/Minimal layout, Detection plus +> nested interaction: HUD plus Tactical/Minimal/Cyber layout, Detection plus > Density/Allocation/Fade/Outside tuning, and 3D plus Proximity/All mode. The > same nodes and state return to the map Display on exit, so Cockpit does not > maintain a second control state. **Detection is owned by the CONTACTS diff --git a/docs/SHARE-LINK-FORMAT.md b/docs/SHARE-LINK-FORMAT.md index ea5636f39..5728d1ec3 100644 --- a/docs/SHARE-LINK-FORMAT.md +++ b/docs/SHARE-LINK-FORMAT.md @@ -107,7 +107,7 @@ probability of about 255 in 256 (many of them already fail as base64url). | 13 | thing | LEN | none | sub-message, see thing views | | 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) | | 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | -| 16 | `hudVariant` | enum | style | tactical, operator, minimal, cyber | +| 16 | `hudVariant` | enum | style | tactical, operator (retired), minimal, cyber | | 17 | `bloomIntensity` | uint | style | | | 18 | `bloomVersion` | uint | 2 | | | 19 | `sharpenIntensity` | uint | style | | @@ -129,6 +129,9 @@ table learns its code. A new style's settings are encoded against its own defaults, while a decoder that does not know the style uses Normal's; adding a style also extends the pinned style-defaults table. +`hudVariant` code 1 (`operator`) is retired: the app no longer offers that +layout and restores it as `tactical`. The code stays reserved. + Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3 `celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on). diff --git a/docs/translations/it.md b/docs/translations/it.md index 81854d6fe..722916101 100644 --- a/docs/translations/it.md +++ b/docs/translations/it.md @@ -79,7 +79,7 @@ The `many` form is the `other` text with «di» after the number when a noun fol | briefing / panel / dock | briefing / pannello / dock | | cockpit | cockpit | | preset / visual preset | preset / preset visivo | -| Tactical / Operator / Minimal / Cyber | Tattico / Operatore / Minimale / Cyber | +| Tactical / Minimal / Cyber | Tattico / Minimale / Cyber | | Scope (circular viewport mask) / keyhole | scope / foro | | feather / fade | sfumatura / dissolvenza | | detection overlay | overlay di rilevamento | diff --git a/docs/translations/ja.md b/docs/translations/ja.md index 2253bdc45..9e612e8e4 100644 --- a/docs/translations/ja.md +++ b/docs/translations/ja.md @@ -61,7 +61,7 @@ Translation source: abiyeenzo (#913). | scene / shot | シーン / ショット | | briefing / panel / dock | ブリーフィング / パネル / ドック | | preset / visual preset | プリセット / ビジュアルプリセット | -| Tactical / Operator / Minimal / Cyber | タクティカル / オペレーター / ミニマル / サイバー | +| Tactical / Minimal / Cyber | タクティカル / ミニマル / サイバー | | Scope (circular viewport mask) / keyhole | スコープ / キーホール | | Clean UI | クリーンUI | | cockpit | コックピット | diff --git a/docs/translations/zh-CN.md b/docs/translations/zh-CN.md index e3b7bcad1..b3a3f973a 100644 --- a/docs/translations/zh-CN.md +++ b/docs/translations/zh-CN.md @@ -43,7 +43,7 @@ Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979). | briefing / panel / dock | 简报 / 面板 / 停靠栏 | | preset / visual preset | 预设 / 视觉预设 | | Display / HUD layout | 显示 / HUD 布局 | -| Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 | +| Tactical / Minimal / Cyber | 战术 / 精简 / 赛博 | | Scope (circular viewport mask) / keyhole | 视域 | | Clean UI | 纯净界面 | | cockpit | 座舱 | diff --git a/scripts/qa-cockpit-utility.mjs b/scripts/qa-cockpit-utility.mjs index 2bfc1ca41..e62df75f4 100644 --- a/scripts/qa-cockpit-utility.mjs +++ b/scripts/qa-cockpit-utility.mjs @@ -1751,7 +1751,7 @@ try { return state; }, { variantName: variant, kind: openKind }); - for (const variant of ['off', 'minimal', 'tactical', 'operator']) { + for (const variant of ['off', 'minimal', 'tactical']) { for (const kind of ['display', 'radio']) { const state = await desktopState(variant, kind); check( @@ -2081,7 +2081,7 @@ try { await page.screenshot({ path: path.join(shotsDir, `cyber-resize-${height}.png`) }); } } - await page.evaluate(() => window.__godsEyeView.styleManager._setHudVariant('operator')); + await page.evaluate(() => window.__godsEyeView.styleManager._setHudVariant('tactical')); const signalTransition = await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; diff --git a/scripts/qa-radio.mjs b/scripts/qa-radio.mjs index d78c3dbcc..ec95046db 100644 --- a/scripts/qa-radio.mjs +++ b/scripts/qa-radio.mjs @@ -2158,11 +2158,13 @@ async function main() { for (let index = 0; index < 5; index += 1) { recordLayoutStep(); if (index < 4) { - if (index < 2) { - hudLayout.value = index === 0 ? 'operator' : 'minimal'; - hudLayout.dispatchEvent(new Event('change', { bubbles: true })); - } else { + // Off and back on in Minimal, so the toggle must restore a + // non-default layout before the cycle returns to Tactical. + if (index === 1 || index === 2) { hudToggle.click(); + } else { + hudLayout.value = index === 0 ? 'minimal' : 'tactical'; + hudLayout.dispatchEvent(new Event('change', { bubbles: true })); } await waitForHudSettle(); } @@ -2331,7 +2333,7 @@ async function main() { scenesHidden, mapProviderUtilityStable, hudInteractionsExact: layoutSteps.map((step) => step.hud).join('>') - === 'TACTICAL>OPERATOR>MINIMAL>OFF>MINIMAL', + === 'TACTICAL>MINIMAL>OFF>MINIMAL>TACTICAL', // The accordion clears the topline, Cockpit cards, and Cesium credits // across every HUD layout. Its list owns any overflow internally. leftLaneClearAcrossHudCycle: layoutSteps.every((step) => ( diff --git a/scripts/qa-scope-hud.mjs b/scripts/qa-scope-hud.mjs index 7fece45ee..1607b2631 100644 --- a/scripts/qa-scope-hud.mjs +++ b/scripts/qa-scope-hud.mjs @@ -345,7 +345,7 @@ try { (await readScope()).feather === 2, ); await page.click('#scope-toggle'); - for (const layout of ['minimal', 'operator', 'cyber', 'tactical']) { + for (const layout of ['minimal', 'cyber', 'tactical']) { await page.select('#hud-layout-select', layout); const state = await readScope(); check( @@ -395,7 +395,7 @@ try { restored, ); - for (const layout of ['minimal', 'operator', 'tactical']) { + for (const layout of ['minimal', 'tactical']) { await page.select('#hud-layout-select', layout); await page.evaluate(() => window.__godsEyeView.styleManager.applyVisualState({ @@ -429,20 +429,13 @@ try { wrappersTransparent: wrappers.every( (el) => getComputedStyle(el).display === 'contents', ), - operatorFrame: - hud.dataset.variant !== 'operator' || - (getComputedStyle(hud.querySelector('.hud-corner')) - .backgroundColor === 'rgba(0, 0, 0, 0.36)' && - getComputedStyle(hud.querySelector('.hud-top-bar')) - .backgroundImage !== 'none'), }; }); check( `${layout}: Scope ON retains the original presentation without new backplates`, original.markerAbsent && original.unbacked && - original.wrappersTransparent && - original.operatorFrame, + original.wrappersTransparent, original, ); check( @@ -571,7 +564,7 @@ try { ); }); const broadBackings = [ - ...hud.querySelectorAll('.hud-corner, .hud-top-bar, .hud-bottom-bar'), + ...hud.querySelectorAll('.hud-corner, .hud-bottom-bar'), ].filter( (el) => el.offsetWidth > 0 && @@ -656,7 +649,7 @@ try { }), ); check('narrow HUD text backings stay within the viewport', narrow); - for (const layout of ['minimal', 'operator', 'tactical']) { + for (const layout of ['minimal', 'tactical']) { await page.select('#hud-layout-select', layout); await page.evaluate(() => window.__godsEyeView.styleManager.applyVisualState({ diff --git a/server/providers/openai/instructions.js b/server/providers/openai/instructions.js index 2841df78a..acf576d13 100644 --- a/server/providers/openai/instructions.js +++ b/server/providers/openai/instructions.js @@ -48,13 +48,13 @@ function realtimeInstructions( 'For visual filter requests, call set_visual_style with one of the allowed style IDs.', 'SHIPS vs STYLES: "vessels" is easily heard as "visuals". "Show me vessels", "turn on vessels", "the vessels", "visuals" or "vestibules" said WITHOUT a style name means the ships layer: set_layer_visibility{layerId:"ais-live-vessels"}, exactly as "ships" or "boats" does. Call set_visual_style only when the user names a style (retro/CRT, surveillance/night vision, thermal, anime, noir, normal) or says filter, effect or look. Never turn on a different data layer (such as fires) for a ships request.', 'Disambiguation table — basemap vs layer vs style: basemap switching requires an explicit stack name — "Bing aerial" means set_map_stack bing-aerial, "aerial with labels" means bing-labels, "OSM"/"road map" means osm, "Esri"/"Esri imagery" means esri-imagery, "Google 3D"/"photorealistic" means photoreal. Any other mention of "satellite" or "satellites" means the satellites DATA LAYER via set_layer_visibility, never a basemap — except "recent/latest satellite imagery" (the recent-imagery layer) and "weather satellite"/clouds (weather-satellite). "surveillance"/"night vision"/"thermal" are visual STYLES via set_visual_style.', - 'HUD requests ("hud on/off", "switch to operator/minimal/tactical/cyber layout") use set_hud. Detection requests ("detection on", "dense mode", "balanced mode", "sparse mode", "set density to 25", "use weighted allocation") use set_detection. Density snaps to 0/25/50/75/100 and derives Sparse/Balanced/Dense; panoptic is a legacy alias for Dense.', + 'HUD requests ("hud on/off", "switch to minimal/tactical/cyber layout") use set_hud. Detection requests ("detection on", "dense mode", "balanced mode", "sparse mode", "set density to 25", "use weighted allocation") use set_detection. Density snaps to 0/25/50/75/100 and derives Sparse/Balanced/Dense; panoptic is a legacy alias for Dense.', 'Cyber sonar requests use set_cyber_sonar: enabled, rings, rangePct, intensityPct (Power), opacityPct, sectorDeg. For settings questions read get_current_view_state.controls.hud.sonar. Sonar is not detection. Do not silently switch themes, reveal the HUD or enable sonar when changing a slider. If the user explicitly requests Cyber plus sonar settings, call set_hud first, wait for success, then set_cyber_sonar. Confirm returned values; mapSweepActive and contactSweepActive distinguish saved settings from active map effects, including in Cockpit or reduced motion. Do not claim visible contacts solely because enabled is true.', 'Bloom/sharpen requests use set_post_processing. Scene requests ("play orbital watch", "stop the scene", "what scenes are there") use control_scene. CCTV camera requests ("next camera", "nearest camera", "select the Congress camera", "show coverage") use control_cctv — the CCTV layer must be enabled first.', 'Radio playback requests use control_radio. "Turn on/start the radio" means action=play; action=enable only reveals Radio markers and must be reserved for explicit "show/enable the Radio layer/markers" requests. After a prepared playback result, briefly confirm any other completed actions and say "Turning on the radio"—never claim it is already playing. The client keeps Radio muted until playback is verified, then closes voice before restoring Radio volume. Examples: "play news near Austin" → select category=news locationId=austin; "play US news" → select category=news country=US; "Radio volume 30" → volume; pause/resume/stop/next/previous use the matching action. Radio selection never moves the camera.', '"Track/follow " (a callsign, ship name, satellite name) uses track_entity. "Take me to the biggest fire" uses track_entity with query "biggest fire" (the fires layer must be enabled). Bare "orbit" means camera orbit of the current landmark. "Stop following/tracking" uses stop_tracking.', '"Show me which planes are overhead"/"frame the ships"/"show me the satellites above" use frame_overhead with the matching target.', - 'Confirmations echo the RESULTING state, never the request: "HUD operator layout", "Density twenty-five percent", "Bing aerial imagery", "Tracking UAL428", "Framed fourteen aircraft". On ok=false, state the failure plainly: "Nothing matched UAL999", "No ships within 120 kilometers". Never claim an action without ok=true in the tool result.', + 'Confirmations echo the RESULTING state, never the request: "HUD minimal layout", "Density twenty-five percent", "Bing aerial imagery", "Tracking UAL428", "Framed fourteen aircraft". On ok=false, state the failure plainly: "Nothing matched UAL999", "No ships within 120 kilometers". Never claim an action without ok=true in the tool result.', 'For destination requests such as "take me to Italy", "go to NYC", or "show me the Eiffel Tower", call fly_to_location. Prefer known city IDs when available; otherwise pass the plain place query.', 'Navigation-only requests ("take me to X", "go to X", "fly to X") are NOT descriptions: call fly_to_location alone and do NOT also call annotate_map, unless the user explicitly asks to mark the place or you go on to explain specific places there. Never drop a point pin on a region-scale natural feature (a mountain range, desert, sea, or forest) — a single point in the middle of the Rockies is meaningless. If the user explicitly asks to mark such a region, prefer type=area.', 'For country and city destinations, omit rangeM so GEV frames the whole country or city in view. For landmarks and buildings, omit rangeM so GEV chooses a close landmark view.', diff --git a/src/firstRunExperience.test.mjs b/src/firstRunExperience.test.mjs index 6629536e2..622fc710e 100644 --- a/src/firstRunExperience.test.mjs +++ b/src/firstRunExperience.test.mjs @@ -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, [ diff --git a/src/hud.js b/src/hud.js index 023d617dc..6eff65df7 100644 --- a/src/hud.js +++ b/src/hud.js @@ -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. */ @@ -287,12 +287,6 @@ export class IntelHUD { this._el.innerHTML = ` -
- TOP SECRET // SI-TK // NOFORN - ${this._missionId} - PAGE 1/1 -
-
┌
@@ -1062,7 +1056,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); diff --git a/src/hudAltitudeDatum.test.mjs b/src/hudAltitudeDatum.test.mjs index ca14569e5..18ed56f92 100644 --- a/src/hudAltitudeDatum.test.mjs +++ b/src/hudAltitudeDatum.test.mjs @@ -575,3 +575,25 @@ test('a summary interrupted while typing can retry unchanged context', async (t) env.restore(); } }); + +// ── Retired layouts ───────────────────────────────────────────────────────── +// +// Share links keep the retired Operator code, so old links still name it. The +// real HUD, not a caller, is what turns it into the default Tactical layout. + +test('a retired Operator layout falls back to Tactical in the live HUD', () => { + const env = installHudEnvironment(); + let hud; + try { + hud = new IntelHUD(env.viewer); + hud._el = { dataset: {} }; + hud.setVariant('minimal'); + assert.equal(hud.getVariant(), 'minimal'); + hud.setVariant('operator'); + assert.equal(hud.getVariant(), 'tactical'); + assert.equal(hud._el.dataset.variant, 'tactical'); + } finally { + hud?.destroy(); + env.restore(); + } +}); diff --git a/src/hudLayoutPolicy.js b/src/hudLayoutPolicy.js index 6b3e2287b..81a64b782 100644 --- a/src/hudLayoutPolicy.js +++ b/src/hudLayoutPolicy.js @@ -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']); diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 143788867..84306d25c 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -792,7 +792,6 @@ "Layout": "Layout", "HUD layout": "HUD-Layout", "Tactical": "Taktisch", - "Operator": "Operator", "Minimal": "Minimal", "Detection Overlay (D)": "Erkennungs-Overlay (D)", "Detection overlay": "Erkennungs-Overlay", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 8cbdf6ab5..a5b34b259 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -74,7 +74,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", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index f7c362161..1487c7f5b 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -2860,7 +2860,6 @@ "Layout": "Disposition", "HUD layout": "Disposition du HUD", "Tactical": "Tactique", - "Operator": "Opérateur", "Minimal": "Minimal", "Cyber": "Cyber", "Sonar": "Sonar", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 96c319129..308ad6c17 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -2860,7 +2860,6 @@ "Layout": "Layout", "HUD layout": "Layout HUD", "Tactical": "Tattico", - "Operator": "Operatore", "Minimal": "Minimale", "Cyber": "Cyber", "Sonar": "Sonar", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 6844a967e..0e33ea4a4 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -2720,7 +2720,6 @@ "Layout": "レイアウト", "HUD layout": "HUDレイアウト", "Tactical": "タクティカル", - "Operator": "オペレーター", "Minimal": "ミニマル", "Cyber": "サイバー", "Sonar": "ソナー", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index f139e517a..f3c84c7cf 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -2862,7 +2862,6 @@ "Layout": "Layout", "HUD layout": "Layout do HUD", "Tactical": "Tático", - "Operator": "Operador", "Minimal": "Mínimo", "Cyber": "Ciber", "Sonar": "Sonar", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 0e8632bf3..e46874b58 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -775,7 +775,6 @@ "Fade": "Затухание", "Outside": "Снаружи", "Tactical": "Тактический", - "Operator": "Операторский", "Minimal": "Минимальный", "HUD layout": "Макет HUD", "Layout": "Макет", diff --git a/src/i18n/locales/uk.json b/src/i18n/locales/uk.json index 2e6fa95c0..b2d01c1ce 100644 --- a/src/i18n/locales/uk.json +++ b/src/i18n/locales/uk.json @@ -2929,7 +2929,6 @@ "Layout": "Компонування", "HUD layout": "Компонування HUD", "Tactical": "Тактичне", - "Operator": "Операторське", "Minimal": "Мінімальне", "Cyber": "Кібер", "Sonar": "Сонар", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 06fa38a18..6d52a3167 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -2616,7 +2616,6 @@ "Layout": "布局", "HUD layout": "HUD 布局", "Tactical": "战术", - "Operator": "操作员", "Minimal": "精简", "Cyber": "赛博", "Sonar": "声呐", diff --git a/src/i18n/locales/zh-Hant.json b/src/i18n/locales/zh-Hant.json index d594f0212..33eedb487 100644 --- a/src/i18n/locales/zh-Hant.json +++ b/src/i18n/locales/zh-Hant.json @@ -2604,7 +2604,6 @@ "Layout": "佈局", "HUD layout": "HUD 佈局", "Tactical": "戰術", - "Operator": "操作員", "Minimal": "精簡", "Cyber": "賽博", "Sonar": "聲吶", diff --git a/src/keySetup.test.mjs b/src/keySetup.test.mjs index e7e6c70ae..013dbbd87 100644 --- a/src/keySetup.test.mjs +++ b/src/keySetup.test.mjs @@ -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); diff --git a/src/overlays/worldOverlay.js b/src/overlays/worldOverlay.js index ccf3cbd59..1fcfdd8dc 100644 --- a/src/overlays/worldOverlay.js +++ b/src/overlays/worldOverlay.js @@ -114,7 +114,6 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([ '.hud-top-right', '.hud-bottom-left', '.hud-bottom-right', - '.hud-top-bar', '.hud-bottom-bar', '#space-mission-panel', '#space-mission-panel-host', diff --git a/src/radioMarkup.test.mjs b/src/radioMarkup.test.mjs index 9d9ae45ed..2fb4add44 100644 --- a/src/radioMarkup.test.mjs +++ b/src/radioMarkup.test.mjs @@ -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'); diff --git a/src/share/viewCodec.test.mjs b/src/share/viewCodec.test.mjs index 4449ceb19..6cc6fd9b1 100644 --- a/src/share/viewCodec.test.mjs +++ b/src/share/viewCodec.test.mjs @@ -438,9 +438,18 @@ 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 code 1 so old + // links still parse, and the app restores them as Tactical. + assert.deepEqual(SHARE_VIEW_ENUMS.hudVariant, [ + 'tactical', + 'operator', + 'minimal', + 'cyber', ]); + assert.deepEqual( + SHARE_VIEW_ENUMS.hudVariant.filter((variant) => variant !== 'operator'), + [...HUD_LAYOUTS], + ); // Snow is retired; its code stays reserved (see the retired-style test). assert.deepEqual(SHARE_VIEW_ENUMS.style, [...VIEW_STYLES, 'snow']); assert.deepEqual(SHARE_VIEW_ENUMS.mapStack, [...VIEW_MAPS]); diff --git a/src/ui/applicationShell.js b/src/ui/applicationShell.js index 1ff3b45ac..b5b2be018 100644 --- a/src/ui/applicationShell.js +++ b/src/ui/applicationShell.js @@ -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) { diff --git a/src/ui/cyberPanelDisclosure.test.mjs b/src/ui/cyberPanelDisclosure.test.mjs index ae0256d4c..e2a666ba5 100644 --- a/src/ui/cyberPanelDisclosure.test.mjs +++ b/src/ui/cyberPanelDisclosure.test.mjs @@ -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 { diff --git a/src/ui/cyberSonarControls.test.mjs b/src/ui/cyberSonarControls.test.mjs index b819d6f3d..7636d76b7 100644 --- a/src/ui/cyberSonarControls.test.mjs +++ b/src/ui/cyberSonarControls.test.mjs @@ -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, []); diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index f316d219c..ec9795046 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -54,8 +54,9 @@ function styleOwner(t, initialVariant = 'cyber') { const hud = { getVariant: () => variant, setVariant: (value) => { - variant = value; - applyHudUiTheme(root, value); + // Normalize like the real HUD so retired layouts fall back. + variant = normalizeHudLayout(value); + applyHudUiTheme(root, variant); }, setMode: (value) => calls.push(['mode', value]), onStyleChange: (value) => calls.push(['style', value]), @@ -130,7 +131,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); @@ -149,8 +150,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); @@ -169,8 +170,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, 'anime'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); @@ -191,7 +192,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'); }); @@ -206,7 +207,7 @@ test('an authoritative same-Cyber reset clears local preset ownership', (t) => { assert.equal(owner.activeStyle, 'anime'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); - owner._setHudVariant('operator', { applyVisualDefaults: true }); + owner._setHudVariant('minimal', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'anime'); }); @@ -240,9 +241,9 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author assert.equal(display.owner.activeStyle, 'thermal'); animeButton.dispatchEvent(new Event('click')); assert.equal(display.owner.activeStyle, 'anime'); - 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, 'anime'); controls.destroy(); @@ -265,9 +266,9 @@ test('Display and voice HUD routes preserve explicit Cyber entry and exit author const selected = await runVoiceAction('set_visual_style', { style: 'anime' }); assert.equal(selected.ok, true); assert.equal(voice.owner.activeStyle, 'anime'); - 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, 'anime'); }); @@ -283,7 +284,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); @@ -319,7 +320,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'); @@ -348,7 +349,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', @@ -570,8 +572,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'); }); @@ -603,7 +606,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, @@ -720,10 +723,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', () assert.match(cyberStyles, /\.hud-top-left/); assert.match(cyberStyles, /transparent 0 min\(40vw, 52vh\)/); assert.match(cyberStyles, /\.hud-bottom-bar \{\s*display: none;/); - assert.match( - cyberStyles, - /#intel-hud\[data-variant='cyber'\] \.hud-top-bar \{\s*display: none;/, - ); + assert.doesNotMatch(cyberStyles, /\.hud-top-bar/); // Cyber shares the measured rail/credit keep-out rather than overriding it. assert.doesNotMatch( cyberStyles, diff --git a/src/ui/panelLayoutController.js b/src/ui/panelLayoutController.js index 25b4b3910..0c2a2ad3a 100644 --- a/src/ui/panelLayoutController.js +++ b/src/ui/panelLayoutController.js @@ -20,7 +20,6 @@ const LEFT_STACK_OBSTACLE_SELECTOR = [ '#intel-hud .hud-top-right', '#intel-hud .hud-bottom-left', '#intel-hud .hud-bottom-right', - '#intel-hud .hud-top-bar', '#intel-hud .hud-bottom-bar', '#intel-hud .hud-left-edge', '#intel-hud .hud-right-edge', @@ -50,7 +49,6 @@ const RIGHT_STACK_OBSTACLE_SELECTOR = [ '#intel-hud .hud-top-right', '#intel-hud .hud-bottom-left', '#intel-hud .hud-bottom-right', - '#intel-hud .hud-top-bar', '#intel-hud .hud-bottom-bar', '#intel-hud .hud-left-edge', '#intel-hud .hud-right-edge', diff --git a/src/ui/panelRails.test.mjs b/src/ui/panelRails.test.mjs index 270ba61f5..f144ec0e7 100644 --- a/src/ui/panelRails.test.mjs +++ b/src/ui/panelRails.test.mjs @@ -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( diff --git a/src/ui/styles/command-dock-trays.css b/src/ui/styles/command-dock-trays.css index 547037de7..e3607a8ce 100644 --- a/src/ui/styles/command-dock-trays.css +++ b/src/ui/styles/command-dock-trays.css @@ -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); } diff --git a/src/ui/styles/cyber.css b/src/ui/styles/cyber.css index f19bb3315..cb2281ed9 100644 --- a/src/ui/styles/cyber.css +++ b/src/ui/styles/cyber.css @@ -1552,10 +1552,6 @@ clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%); } -#intel-hud[data-variant='cyber'] .hud-top-bar { - display: none; -} - /* Cyber keeps the useful style and locality readouts, not fictional IDs. */ #intel-hud[data-variant='cyber'] .hud-top-left .hud-classification, #intel-hud[data-variant='cyber'] .hud-top-left .hud-system { @@ -1855,7 +1851,6 @@ background-size: 78px 78px; } - #intel-hud[data-variant='cyber'] .hud-top-bar, #intel-hud[data-variant='cyber'] .hud-bottom-bar { display: none; } diff --git a/src/ui/styles/mobile-shell.css b/src/ui/styles/mobile-shell.css index aecfdf8d4..a406b8f3b 100644 --- a/src/ui/styles/mobile-shell.css +++ b/src/ui/styles/mobile-shell.css @@ -107,7 +107,6 @@ body.mobile-shell #top-center-actions { /* ── HUD: one compact readout under the top bar ────────────────────────── */ body.mobile-shell #intel-hud .hud-edge, body.mobile-shell #intel-hud .hud-corner, -body.mobile-shell #intel-hud .hud-top-bar, body.mobile-shell #intel-hud .hud-bottom-bar { display: none; } diff --git a/src/ui/styles/overlays.css b/src/ui/styles/overlays.css index 7f491439b..85aaafa06 100644 --- a/src/ui/styles/overlays.css +++ b/src/ui/styles/overlays.css @@ -124,7 +124,6 @@ line-height: 1.6; } -.hud-top-bar, .hud-bottom-bar { display: none; position: absolute; @@ -141,24 +140,11 @@ pointer-events: none; } -.hud-top-bar { - top: 70px; -} - .hud-bottom-bar { bottom: 110px; justify-content: center; } -.hud-top-bar-center { - opacity: 0.84; -} - -.hud-top-bar-left, -.hud-top-bar-right { - opacity: 0.6; -} - /* Corner blocks */ .hud-corner { position: absolute; @@ -298,54 +284,15 @@ gap: 20px; } -#intel-hud[data-variant='tactical'] .hud-top-bar, #intel-hud[data-variant='tactical'] .hud-bottom-bar { 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 +325,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 +342,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 +352,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,38 +362,35 @@ } /* 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']) - :is(.hud-top-bar, .hud-bottom-bar) { + #intel-hud[data-variant='minimal'] + .hud-bottom-bar { background: transparent; } :root[data-scope-mask='off'] - #intel-hud:is([data-variant='minimal'], [data-variant='operator']) - :is(.hud-top-bar, .hud-bottom-bar) { + #intel-hud[data-variant='minimal'] + .hud-bottom-bar { width: fit-content; max-width: calc(100vw - 120px); gap: 24px; } :root[data-scope-mask='off'] - #intel-hud:is([data-variant='minimal'], [data-variant='operator']) - :is(.hud-top-bar, .hud-bottom-bar) + #intel-hud[data-variant='minimal'] + .hud-bottom-bar .hud-readout { overflow: hidden; white-space: nowrap; diff --git a/src/ui/styles/responsive.css b/src/ui/styles/responsive.css index 8278b51a7..7a77eb665 100644 --- a/src/ui/styles/responsive.css +++ b/src/ui/styles/responsive.css @@ -98,25 +98,12 @@ display: none; } - .hud-top-bar, .hud-bottom-bar { width: calc(100vw - 24px); left: 12px; transform: none; } - .hud-top-bar { - top: 54px; - 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; diff --git a/src/ui/templates/display-controls.html b/src/ui/templates/display-controls.html index f6af31127..458a2fcd8 100644 --- a/src/ui/templates/display-controls.html +++ b/src/ui/templates/display-controls.html @@ -15,7 +15,6 @@ Layout diff --git a/src/voice/actionSchemas.test.mjs b/src/voice/actionSchemas.test.mjs index 3bf9cfcd9..4e6586729 100644 --- a/src/voice/actionSchemas.test.mjs +++ b/src/voice/actionSchemas.test.mjs @@ -46,7 +46,8 @@ test('the complete Realtime tool payload pins the manifest-generated layer relea // Panel adds named voice targets and timeout; shared Panel/Cockpit guidance // is consolidated without changing either feature's arguments. // Snow leaves the set_visual_style enum. - '724a82b538a12a994401fc941e175625b032bfb96109934e091d3ef5b9600909', + // The Operator HUD layout leaves set_hud's layout enum. + 'f47a7f78f13b1f79978fa524fad2618e32bd813e1a5dfd746bf35d29909fc91b', ); }); @@ -167,9 +168,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']; // Snow is retired from set_visual_style; restore it to keep the legacy pin. const visualStyle = legacy.find((tool) => tool.name === 'set_visual_style') .parameters.properties.style;