Merge pull request #1467 from asturksever/fix/remove-operator-hud-layout

Remove the Operator HUD layout
This commit is contained in:
Said Turksever authored and GitHub committed 2026-10-29 16:41:30 +00:00
commit 23a0cc0eb7
41 files changed
+126 -205

No files matched your search

@@ -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).
+11 -2
View File
@@ -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 changes preserve both settings; share links and scenes restore them. The
historical missing-`scf` fallback remains 35%; explicit 0% keeps the hard edge. 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 at 70% opacity behind telemetry, vertical metadata and scene chrome. Original
HUD text color, glow and opacity are preserved. Scope OFF brightens the tagline 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 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, `node scripts/qa-scope-hud.mjs` checks native input, range, layout persistence,
restoration and both Scope presentations. `QA_SCREENSHOT_DIR` saves screenshots. 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 ## Cyber HUD — September 30, 2026
Display > HUD > Layout includes Cyber, also available through the HUD voice 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, > corridor). Its minimal Display popover exposes Intel HUD,
> Detection, Parameters, and 3D aircraft. During Cockpit, those existing > Detection, Parameters, and 3D aircraft. During Cockpit, those existing
> standard Display controls move into the Cockpit popover and retain their normal > 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 > 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 > 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 > maintain a second control state. **Detection is owned by the CONTACTS
+4 -1
View File
@@ -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 | | 13 | thing | LEN | none | sub-message, see thing views |
| 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) | | 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) |
| 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | | 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 | | | 17 | `bloomIntensity` | uint | style | |
| 18 | `bloomVersion` | uint | 2 | | | 18 | `bloomVersion` | uint | 2 | |
| 19 | `sharpenIntensity` | uint | style | | | 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 defaults, while a decoder that does not know the style uses Normal's; adding a
style also extends the pinned style-defaults table. 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 Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3
`celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on). `celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on).
+1 -1
View File
@@ -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 | | briefing / panel / dock | briefing / pannello / dock |
| cockpit | cockpit | | cockpit | cockpit |
| preset / visual preset | preset / preset visivo | | 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 | | Scope (circular viewport mask) / keyhole | scope / foro |
| feather / fade | sfumatura / dissolvenza | | feather / fade | sfumatura / dissolvenza |
| detection overlay | overlay di rilevamento | | detection overlay | overlay di rilevamento |
+1 -1
View File
@@ -61,7 +61,7 @@ Translation source: abiyeenzo (#913).
| scene / shot | シーン / ショット | | scene / shot | シーン / ショット |
| briefing / panel / dock | ブリーフィング / パネル / ドック | | briefing / panel / dock | ブリーフィング / パネル / ドック |
| preset / visual preset | プリセット / ビジュアルプリセット | | preset / visual preset | プリセット / ビジュアルプリセット |
| Tactical / Operator / Minimal / Cyber | タクティカル / オペレーター / ミニマル / サイバー | | Tactical / Minimal / Cyber | タクティカル / ミニマル / サイバー |
| Scope (circular viewport mask) / keyhole | スコープ / キーホール | | Scope (circular viewport mask) / keyhole | スコープ / キーホール |
| Clean UI | クリーンUI | | Clean UI | クリーンUI |
| cockpit | コックピット | | cockpit | コックピット |
+1 -1
View File
@@ -43,7 +43,7 @@ Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979).
| briefing / panel / dock | 简报 / 面板 / 停靠栏 | | briefing / panel / dock | 简报 / 面板 / 停靠栏 |
| preset / visual preset | 预设 / 视觉预设 | | preset / visual preset | 预设 / 视觉预设 |
| Display / HUD layout | 显示 / HUD 布局 | | Display / HUD layout | 显示 / HUD 布局 |
| Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 | | Tactical / Minimal / Cyber | 战术 / 精简 / 赛博 |
| Scope (circular viewport mask) / keyhole | 视域 | | Scope (circular viewport mask) / keyhole | 视域 |
| Clean UI | 纯净界面 | | Clean UI | 纯净界面 |
| cockpit | 座舱 | | cockpit | 座舱 |
+2 -2
View File
@@ -1751,7 +1751,7 @@ try {
return state; return state;
}, { variantName: variant, kind: openKind }); }, { 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']) { for (const kind of ['display', 'radio']) {
const state = await desktopState(variant, kind); const state = await desktopState(variant, kind);
check( check(
@@ -2081,7 +2081,7 @@ try {
await page.screenshot({ path: path.join(shotsDir, `cyber-resize-${height}.png`) }); 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 signalTransition = await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager; const manager = window.__godsEyeView.styleManager;
+7 -5
View File
@@ -2158,11 +2158,13 @@ async function main() {
for (let index = 0; index < 5; index += 1) { for (let index = 0; index < 5; index += 1) {
recordLayoutStep(); recordLayoutStep();
if (index < 4) { if (index < 4) {
if (index < 2) { // Off and back on in Minimal, so the toggle must restore a
hudLayout.value = index === 0 ? 'operator' : 'minimal'; // non-default layout before the cycle returns to Tactical.
hudLayout.dispatchEvent(new Event('change', { bubbles: true })); if (index === 1 || index === 2) {
} else {
hudToggle.click(); hudToggle.click();
} else {
hudLayout.value = index === 0 ? 'minimal' : 'tactical';
hudLayout.dispatchEvent(new Event('change', { bubbles: true }));
} }
await waitForHudSettle(); await waitForHudSettle();
} }
@@ -2331,7 +2333,7 @@ async function main() {
scenesHidden, scenesHidden,
mapProviderUtilityStable, mapProviderUtilityStable,
hudInteractionsExact: layoutSteps.map((step) => step.hud).join('>') 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 // The accordion clears the topline, Cockpit cards, and Cesium credits
// across every HUD layout. Its list owns any overflow internally. // across every HUD layout. Its list owns any overflow internally.
leftLaneClearAcrossHudCycle: layoutSteps.every((step) => ( leftLaneClearAcrossHudCycle: layoutSteps.every((step) => (
+5 -12
View File
@@ -345,7 +345,7 @@ try {
(await readScope()).feather === 2, (await readScope()).feather === 2,
); );
await page.click('#scope-toggle'); 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); await page.select('#hud-layout-select', layout);
const state = await readScope(); const state = await readScope();
check( check(
@@ -395,7 +395,7 @@ try {
restored, restored,
); );
for (const layout of ['minimal', 'operator', 'tactical']) { for (const layout of ['minimal', 'tactical']) {
await page.select('#hud-layout-select', layout); await page.select('#hud-layout-select', layout);
await page.evaluate(() => await page.evaluate(() =>
window.__godsEyeView.styleManager.applyVisualState({ window.__godsEyeView.styleManager.applyVisualState({
@@ -429,20 +429,13 @@ try {
wrappersTransparent: wrappers.every( wrappersTransparent: wrappers.every(
(el) => getComputedStyle(el).display === 'contents', (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( check(
`${layout}: Scope ON retains the original presentation without new backplates`, `${layout}: Scope ON retains the original presentation without new backplates`,
original.markerAbsent && original.markerAbsent &&
original.unbacked && original.unbacked &&
original.wrappersTransparent && original.wrappersTransparent,
original.operatorFrame,
original, original,
); );
check( check(
@@ -571,7 +564,7 @@ try {
); );
}); });
const broadBackings = [ const broadBackings = [
...hud.querySelectorAll('.hud-corner, .hud-top-bar, .hud-bottom-bar'), ...hud.querySelectorAll('.hud-corner, .hud-bottom-bar'),
].filter( ].filter(
(el) => (el) =>
el.offsetWidth > 0 && el.offsetWidth > 0 &&
@@ -656,7 +649,7 @@ try {
}), }),
); );
check('narrow HUD text backings stay within the viewport', narrow); 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.select('#hud-layout-select', layout);
await page.evaluate(() => await page.evaluate(() =>
window.__godsEyeView.styleManager.applyVisualState({ window.__godsEyeView.styleManager.applyVisualState({
+2 -2
View File
@@ -48,13 +48,13 @@ function realtimeInstructions(
'For visual filter requests, call set_visual_style with one of the allowed style IDs.', '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.', '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.', '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.', '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.', '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.', '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 <something specific>" (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.', '"Track/follow <something specific>" (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.', '"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.', '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.', '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.', '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.',
+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') const hudLayout = legacyTools.find((tool) => tool.name === 'set_hud')
.parameters.properties.layout; .parameters.properties.layout;
assert.deepEqual(hudLayout.enum, [ assert.deepEqual(hudLayout.enum, ['tactical', 'minimal', 'cyber']);
'tactical', // Cyber deliberately adds one layout and Operator was retired; first-run
'operator', // missions still change no tools.
'minimal', 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');
const cameraMotion = legacyTools.find((tool) => tool.name === 'move_camera') const cameraMotion = legacyTools.find((tool) => tool.name === 'move_camera')
.parameters.properties.motion; .parameters.properties.motion;
assert.deepEqual(cameraMotion.enum, [ assert.deepEqual(cameraMotion.enum, [
+2 -8
View File
@@ -9,7 +9,7 @@ import { applicationServices } from './services/application.js';
* real-time at configurable cadences. * real-time at configurable cadences.
* *
* The HUD auto-activates when a military-style shader (NVG, FLIR, CRT) is * 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. * Color theming is driven by the active shader mode via CSS custom properties.
*/ */
@@ -287,12 +287,6 @@ export class IntelHUD {
this._el.innerHTML = ` this._el.innerHTML = `
<div class="hud-sonar" aria-hidden="true"></div> <div class="hud-sonar" aria-hidden="true"></div>
<div class="hud-top-bar">
<span class="hud-readout"><span class="hud-top-bar-left">TOP SECRET // SI-TK // NOFORN</span></span>
<span class="hud-readout"><span class="hud-top-bar-center">${this._missionId}</span></span>
<span class="hud-readout"><span class="hud-top-bar-right">PAGE 1/1</span></span>
</div>
<div class="hud-corner hud-top-left"> <div class="hud-corner hud-top-left">
<div class="hud-bracket">┌</div> <div class="hud-bracket">┌</div>
<div class="hud-content"> <div class="hud-content">
@@ -1062,7 +1056,7 @@ export class IntelHUD {
* Switch the HUD layout variant. Falls back to `'tactical'` if the * Switch the HUD layout variant. Falls back to `'tactical'` if the
* name is unrecognized. Cyber also applies a coordinated skin to the full * name is unrecognized. Cyber also applies a coordinated skin to the full
* application shell; leaving Cyber restores the standard shell tokens. * 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) { setVariant(variantName) {
this._variant = normalizeHudLayout(variantName); this._variant = normalizeHudLayout(variantName);
+22
View File
@@ -575,3 +575,25 @@ test('a summary interrupted while typing can retry unchanged context', async (t)
env.restore(); 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();
}
});
+1 -6
View File
@@ -1,7 +1,2 @@
/** Browser-free layout vocabulary shared by UI and server-side voice tools. */ /** Browser-free layout vocabulary shared by UI and server-side voice tools. */
export const HUD_LAYOUTS = Object.freeze([ export const HUD_LAYOUTS = Object.freeze(['tactical', 'minimal', 'cyber']);
'tactical',
'operator',
'minimal',
'cyber',
]);
-1
View File
@@ -792,7 +792,6 @@
"Layout": "Layout", "Layout": "Layout",
"HUD layout": "HUD-Layout", "HUD layout": "HUD-Layout",
"Tactical": "Taktisch", "Tactical": "Taktisch",
"Operator": "Operator",
"Minimal": "Minimal", "Minimal": "Minimal",
"Detection Overlay (D)": "Erkennungs-Overlay (D)", "Detection Overlay (D)": "Erkennungs-Overlay (D)",
"Detection overlay": "Erkennungs-Overlay", "Detection overlay": "Erkennungs-Overlay",
-1
View File
@@ -74,7 +74,6 @@
"Layout": "Disposición", "Layout": "Disposición",
"HUD layout": "Disposición del HUD", "HUD layout": "Disposición del HUD",
"Tactical": "Táctica", "Tactical": "Táctica",
"Operator": "De operador",
"Minimal": "Mínima", "Minimal": "Mínima",
"Detection Overlay (D)": "Superposición de detección (D)", "Detection Overlay (D)": "Superposición de detección (D)",
"Detection overlay": "Superposición de detección", "Detection overlay": "Superposición de detección",
-1
View File
@@ -2860,7 +2860,6 @@
"Layout": "Disposition", "Layout": "Disposition",
"HUD layout": "Disposition du HUD", "HUD layout": "Disposition du HUD",
"Tactical": "Tactique", "Tactical": "Tactique",
"Operator": "Opérateur",
"Minimal": "Minimal", "Minimal": "Minimal",
"Cyber": "Cyber", "Cyber": "Cyber",
"Sonar": "Sonar", "Sonar": "Sonar",
-1
View File
@@ -2860,7 +2860,6 @@
"Layout": "Layout", "Layout": "Layout",
"HUD layout": "Layout HUD", "HUD layout": "Layout HUD",
"Tactical": "Tattico", "Tactical": "Tattico",
"Operator": "Operatore",
"Minimal": "Minimale", "Minimal": "Minimale",
"Cyber": "Cyber", "Cyber": "Cyber",
"Sonar": "Sonar", "Sonar": "Sonar",
-1
View File
@@ -2720,7 +2720,6 @@
"Layout": "レイアウト", "Layout": "レイアウト",
"HUD layout": "HUDレイアウト", "HUD layout": "HUDレイアウト",
"Tactical": "タクティカル", "Tactical": "タクティカル",
"Operator": "オペレーター",
"Minimal": "ミニマル", "Minimal": "ミニマル",
"Cyber": "サイバー", "Cyber": "サイバー",
"Sonar": "ソナー", "Sonar": "ソナー",
-1
View File
@@ -2862,7 +2862,6 @@
"Layout": "Layout", "Layout": "Layout",
"HUD layout": "Layout do HUD", "HUD layout": "Layout do HUD",
"Tactical": "Tático", "Tactical": "Tático",
"Operator": "Operador",
"Minimal": "Mínimo", "Minimal": "Mínimo",
"Cyber": "Ciber", "Cyber": "Ciber",
"Sonar": "Sonar", "Sonar": "Sonar",
-1
View File
@@ -775,7 +775,6 @@
"Fade": "Затухание", "Fade": "Затухание",
"Outside": "Снаружи", "Outside": "Снаружи",
"Tactical": "Тактический", "Tactical": "Тактический",
"Operator": "Операторский",
"Minimal": "Минимальный", "Minimal": "Минимальный",
"HUD layout": "Макет HUD", "HUD layout": "Макет HUD",
"Layout": "Макет", "Layout": "Макет",
-1
View File
@@ -2929,7 +2929,6 @@
"Layout": "Компонування", "Layout": "Компонування",
"HUD layout": "Компонування HUD", "HUD layout": "Компонування HUD",
"Tactical": "Тактичне", "Tactical": "Тактичне",
"Operator": "Операторське",
"Minimal": "Мінімальне", "Minimal": "Мінімальне",
"Cyber": "Кібер", "Cyber": "Кібер",
"Sonar": "Сонар", "Sonar": "Сонар",
-1
View File
@@ -2616,7 +2616,6 @@
"Layout": "布局", "Layout": "布局",
"HUD layout": "HUD 布局", "HUD layout": "HUD 布局",
"Tactical": "战术", "Tactical": "战术",
"Operator": "操作员",
"Minimal": "精简", "Minimal": "精简",
"Cyber": "赛博", "Cyber": "赛博",
"Sonar": "声呐", "Sonar": "声呐",
-1
View File
@@ -2604,7 +2604,6 @@
"Layout": "佈局", "Layout": "佈局",
"HUD layout": "HUD 佈局", "HUD layout": "HUD 佈局",
"Tactical": "戰術", "Tactical": "戰術",
"Operator": "操作員",
"Minimal": "精簡", "Minimal": "精簡",
"Cyber": "賽博", "Cyber": "賽博",
"Sonar": "聲吶", "Sonar": "聲吶",
+1 -1
View File
@@ -48,7 +48,7 @@ test('setup placement moves the same button only in Cyber and disconnects on tea
chip, chip,
); );
assert.equal(chip.parentNode, home); 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; theme.dataset.uiTheme = variant;
notify(); notify();
assert.equal(chip.parentNode, variant === 'cyber' ? toolbar : home); assert.equal(chip.parentNode, variant === 'cyber' ? toolbar : home);
-1
View File
@@ -114,7 +114,6 @@ export const WORLD_OVERLAY_OCCLUDER_SELECTORS = Object.freeze([
'.hud-top-right', '.hud-top-right',
'.hud-bottom-left', '.hud-bottom-left',
'.hud-bottom-right', '.hud-bottom-right',
'.hud-top-bar',
'.hud-bottom-bar', '.hud-bottom-bar',
'#space-mission-panel', '#space-mission-panel',
'#space-mission-panel-host', '#space-mission-panel-host',
+4 -3
View File
@@ -206,10 +206,11 @@ test('no unchanged Realtime tool definition drifts silently', () => {
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map((tool) => structuredClone(tool)) .map((tool) => structuredClone(tool))
.filter((tool) => tool.name !== 'set_cyber_sonar'); .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; const hudLayout = unchanged.find((tool) => tool.name === 'set_hud').parameters.properties.layout;
assert.deepEqual(hudLayout.enum, ['tactical', 'operator', 'minimal', 'cyber']); assert.deepEqual(hudLayout.enum, ['tactical', 'minimal', 'cyber']);
hudLayout.enum = hudLayout.enum.filter((layout) => layout !== 'cyber'); hudLayout.enum = ['tactical', 'operator', 'minimal'];
// Globe rotation adds one motion and clarifies the two affected descriptions. // Globe rotation adds one motion and clarifies the two affected descriptions.
// Check those edits explicitly, then keep the legacy pin for every other field. // Check those edits explicitly, then keep the legacy pin for every other field.
const camera = unchanged.find((tool) => tool.name === 'move_camera'); const camera = unchanged.find((tool) => tool.name === 'move_camera');
+11 -2
View File
@@ -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.bloomIntensity, BLOOM_INTENSITY_DEFAULT);
assert.equal(defaults.bloomVersion, BLOOM_SCALE_VERSION); assert.equal(defaults.bloomVersion, BLOOM_SCALE_VERSION);
assert.equal(defaults.hudVariant, HUD_LAYOUTS[0]); assert.equal(defaults.hudVariant, HUD_LAYOUTS[0]);
assert.deepEqual(SHARE_VIEW_ENUMS.hudVariant.slice(0, HUD_LAYOUTS.length), [ // Codes are append-only: the retired Operator layout keeps code 1 so old
...HUD_LAYOUTS, // 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). // 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.style, [...VIEW_STYLES, 'snow']);
assert.deepEqual(SHARE_VIEW_ENUMS.mapStack, [...VIEW_MAPS]); assert.deepEqual(SHARE_VIEW_ENUMS.mapStack, [...VIEW_MAPS]);
+1 -1
View File
@@ -1337,7 +1337,7 @@ export class StyleManager extends ShellFacade {
/** /**
* Switches the HUD layout variant. * 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}} * @returns {{ok: boolean, layout?: string, visible?: boolean, error?: string}}
*/ */
setHudLayout(variantName) { 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', () => { 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; const f = fixture(theme), prior = globalThis.document;
globalThis.document = f.doc; globalThis.document = f.doc;
try { 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', () => { 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); const f = fixture(theme);
assert.equal(setCyberSonarControls(f.owner, { enabled: true }, f.doc).ok, false); assert.equal(setCyberSonarControls(f.owner, { enabled: true }, f.doc).ok, false);
assert.deepEqual(f.calls, []); assert.deepEqual(f.calls, []);
+22 -22
View File
@@ -54,8 +54,9 @@ function styleOwner(t, initialVariant = 'cyber') {
const hud = { const hud = {
getVariant: () => variant, getVariant: () => variant,
setVariant: (value) => { setVariant: (value) => {
variant = value; // Normalize like the real HUD so retired layouts fall back.
applyHudUiTheme(root, value); variant = normalizeHudLayout(value);
applyHudUiTheme(root, variant);
}, },
setMode: (value) => calls.push(['mode', value]), setMode: (value) => calls.push(['mode', value]),
onStyleChange: (value) => calls.push(['style', 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) => { test('non-Cyber style presets and explicit HUD choices remain authoritative', (t) => {
const { owner, hud, root } = styleOwner(t); const { owner, hud, root } = styleOwner(t);
for (const variant of ['tactical', 'operator', 'minimal']) { for (const variant of ['tactical', 'minimal']) {
owner._setHudVariant(variant); owner._setHudVariant(variant);
assert.equal(hud.getVariant(), variant); assert.equal(hud.getVariant(), variant);
assert.equal(root.dataset.uiTheme, undefined); 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._preCyberStyle, 'noir');
assert.equal(owner._cyberStyleUserSelected, false); assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true }); owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'operator'); assert.equal(hud.getVariant(), 'minimal');
assert.equal(owner.activeStyle, 'noir'); assert.equal(owner.activeStyle, 'noir');
assert.equal(owner._preCyberStyle, null); assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false); 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. // Reselecting the active mode must not erase the user's preset intent.
owner._setHudVariant('cyber', { applyVisualDefaults: true }); owner._setHudVariant('cyber', { applyVisualDefaults: true });
assert.equal(owner._preCyberStyle, 'noir'); assert.equal(owner._preCyberStyle, 'noir');
owner._setHudVariant('operator', { applyVisualDefaults: true }); owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(hud.getVariant(), 'operator'); assert.equal(hud.getVariant(), 'minimal');
assert.equal(owner.activeStyle, 'anime'); assert.equal(owner.activeStyle, 'anime');
assert.equal(owner._preCyberStyle, null); assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false); 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 }); owner.setStyle('thermal', { userInitiated: true });
assert.equal(owner.activeStyle, 'thermal'); assert.equal(owner.activeStyle, 'thermal');
assert.equal(owner._cyberStyleUserSelected, true); assert.equal(owner._cyberStyleUserSelected, true);
owner._setHudVariant('operator', { applyVisualDefaults: true }); owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'thermal'); 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.activeStyle, 'anime');
assert.equal(owner._preCyberStyle, null); assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false); assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true }); owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'anime'); 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'); assert.equal(display.owner.activeStyle, 'thermal');
animeButton.dispatchEvent(new Event('click')); animeButton.dispatchEvent(new Event('click'));
assert.equal(display.owner.activeStyle, 'anime'); assert.equal(display.owner.activeStyle, 'anime');
hudLayout.value = 'operator'; hudLayout.value = 'minimal';
hudLayout.dispatchEvent(new Event('change')); hudLayout.dispatchEvent(new Event('change'));
assert.equal(display.hud.getVariant(), 'operator'); assert.equal(display.hud.getVariant(), 'minimal');
assert.equal(display.owner.activeStyle, 'anime'); assert.equal(display.owner.activeStyle, 'anime');
controls.destroy(); 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' }); const selected = await runVoiceAction('set_visual_style', { style: 'anime' });
assert.equal(selected.ok, true); assert.equal(selected.ok, true);
assert.equal(voice.owner.activeStyle, 'anime'); 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(exited.ok, true);
assert.equal(voice.hud.getVariant(), 'operator'); assert.equal(voice.hud.getVariant(), 'minimal');
assert.equal(voice.owner.activeStyle, 'anime'); 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._scopeBtn.checked, false);
assert.equal(owner._scopeFeatherSlider.disabled, true); assert.equal(owner._scopeFeatherSlider.disabled, true);
assert.equal(owner._scopeFeatherSlider.value, '200'); 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 }); owner._setHudVariant(layout, { applyVisualDefaults: true });
assert.equal(isScopeMaskEnabled(), false, layout); assert.equal(isScopeMaskEnabled(), false, layout);
assert.equal(getScopeMaskFeather(), 2, 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.activeStyle, 'retro');
assert.equal(owner._preCyberStyle, null); assert.equal(owner._preCyberStyle, null);
assert.equal(owner._cyberStyleUserSelected, false); assert.equal(owner._cyberStyleUserSelected, false);
owner._setHudVariant('operator', { applyVisualDefaults: true }); owner._setHudVariant('minimal', { applyVisualDefaults: true });
assert.equal(owner.activeStyle, 'retro'); assert.equal(owner.activeStyle, 'retro');
owner.setStyle('surveillance'); owner.setStyle('surveillance');
@@ -348,7 +349,8 @@ test('scene and share restoration override Cyber and preserve restored Cyber par
style: 'surveillance', style: 'surveillance',
hudVariant: 'operator', 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({ await owner.restoreShareState({
style: 'thermal', style: 'thermal',
hudVariant: 'cyber', 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', () => { 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('CYBER'), 'cyber');
assert.equal(normalizeHudLayout('operator'), 'tactical');
assert.equal(normalizeHudLayout('unknown'), '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('tactical', 'cyber'), null);
assert.equal( assert.equal(
cyberVisualDefaultsForHudTransition('cyber', 'operator', { cyberVisualDefaultsForHudTransition('cyber', 'minimal', {
explicit: true, explicit: true,
}), }),
null, 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, /\.hud-top-left/);
assert.match(cyberStyles, /transparent 0 min\(40vw, 52vh\)/); assert.match(cyberStyles, /transparent 0 min\(40vw, 52vh\)/);
assert.match(cyberStyles, /\.hud-bottom-bar \{\s*display: none;/); assert.match(cyberStyles, /\.hud-bottom-bar \{\s*display: none;/);
assert.match( assert.doesNotMatch(cyberStyles, /\.hud-top-bar/);
cyberStyles,
/#intel-hud\[data-variant='cyber'\] \.hud-top-bar \{\s*display: none;/,
);
// Cyber shares the measured rail/credit keep-out rather than overriding it. // Cyber shares the measured rail/credit keep-out rather than overriding it.
assert.doesNotMatch( assert.doesNotMatch(
cyberStyles, cyberStyles,
-2
View File
@@ -20,7 +20,6 @@ const LEFT_STACK_OBSTACLE_SELECTOR = [
'#intel-hud .hud-top-right', '#intel-hud .hud-top-right',
'#intel-hud .hud-bottom-left', '#intel-hud .hud-bottom-left',
'#intel-hud .hud-bottom-right', '#intel-hud .hud-bottom-right',
'#intel-hud .hud-top-bar',
'#intel-hud .hud-bottom-bar', '#intel-hud .hud-bottom-bar',
'#intel-hud .hud-left-edge', '#intel-hud .hud-left-edge',
'#intel-hud .hud-right-edge', '#intel-hud .hud-right-edge',
@@ -50,7 +49,6 @@ const RIGHT_STACK_OBSTACLE_SELECTOR = [
'#intel-hud .hud-top-right', '#intel-hud .hud-top-right',
'#intel-hud .hud-bottom-left', '#intel-hud .hud-bottom-left',
'#intel-hud .hud-bottom-right', '#intel-hud .hud-bottom-right',
'#intel-hud .hud-top-bar',
'#intel-hud .hud-bottom-bar', '#intel-hud .hud-bottom-bar',
'#intel-hud .hud-left-edge', '#intel-hud .hud-left-edge',
'#intel-hud .hud-right-edge', '#intel-hud .hud-right-edge',
+1 -1
View File
@@ -39,7 +39,7 @@ test('Cyber restores one expanded owner, keeps launchers, and restores other the
true, true,
'layout does not reopen a peer', 'layout does not reopen a peer',
); );
f.options.hud.variant = 'operator'; f.options.hud.variant = 'minimal';
f.run(); f.run();
assert.equal(f.first.classList.contains('collapsed'), false); assert.equal(f.first.classList.contains('collapsed'), false);
assert.equal( 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-left,
#intel-hud[data-variant='tactical'] .hud-bottom-right, #intel-hud[data-variant='tactical'] .hud-bottom-right {
#intel-hud[data-variant='operator'] .hud-bottom-left,
#intel-hud[data-variant='operator'] .hud-bottom-right {
bottom: calc(2vh + 6.5rem); 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 { #intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 5.4rem); bottom: calc(2vh + 5.4rem);
} }
-5
View File
@@ -1552,10 +1552,6 @@
clip-path: polygon(12px 0, 100% 0, calc(100% - 12px) 100%, 0 100%); 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. */ /* 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-classification,
#intel-hud[data-variant='cyber'] .hud-top-left .hud-system { #intel-hud[data-variant='cyber'] .hud-top-left .hud-system {
@@ -1855,7 +1851,6 @@
background-size: 78px 78px; background-size: 78px 78px;
} }
#intel-hud[data-variant='cyber'] .hud-top-bar,
#intel-hud[data-variant='cyber'] .hud-bottom-bar { #intel-hud[data-variant='cyber'] .hud-bottom-bar {
display: none; display: none;
} }
-1
View File
@@ -107,7 +107,6 @@ body.mobile-shell #top-center-actions {
/* ── HUD: one compact readout under the top bar ────────────────────────── */ /* ── HUD: one compact readout under the top bar ────────────────────────── */
body.mobile-shell #intel-hud .hud-edge, body.mobile-shell #intel-hud .hud-edge,
body.mobile-shell #intel-hud .hud-corner, body.mobile-shell #intel-hud .hud-corner,
body.mobile-shell #intel-hud .hud-top-bar,
body.mobile-shell #intel-hud .hud-bottom-bar { body.mobile-shell #intel-hud .hud-bottom-bar {
display: none; display: none;
} }
+12 -80
View File
@@ -124,7 +124,6 @@
line-height: 1.6; line-height: 1.6;
} }
.hud-top-bar,
.hud-bottom-bar { .hud-bottom-bar {
display: none; display: none;
position: absolute; position: absolute;
@@ -141,24 +140,11 @@
pointer-events: none; pointer-events: none;
} }
.hud-top-bar {
top: 70px;
}
.hud-bottom-bar { .hud-bottom-bar {
bottom: 110px; bottom: 110px;
justify-content: center; justify-content: center;
} }
.hud-top-bar-center {
opacity: 0.84;
}
.hud-top-bar-left,
.hud-top-bar-right {
opacity: 0.6;
}
/* Corner blocks */ /* Corner blocks */
.hud-corner { .hud-corner {
position: absolute; position: absolute;
@@ -298,54 +284,15 @@
gap: 20px; gap: 20px;
} }
#intel-hud[data-variant='tactical'] .hud-top-bar,
#intel-hud[data-variant='tactical'] .hud-bottom-bar { #intel-hud[data-variant='tactical'] .hud-bottom-bar {
display: none; 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-left,
#intel-hud[data-variant='tactical'] .hud-top-right, #intel-hud[data-variant='tactical'] .hud-top-right {
#intel-hud[data-variant='operator'] .hud-top-left,
#intel-hud[data-variant='operator'] .hud-top-right {
top: max(14vh, 136px); 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-top-right,
#intel-hud[data-variant='minimal'] .hud-bottom-right, #intel-hud[data-variant='minimal'] .hud-bottom-right,
#intel-hud[data-variant='minimal'] .hud-left-edge, #intel-hud[data-variant='minimal'] .hud-left-edge,
@@ -378,11 +325,7 @@
} }
:root[data-scope-mask='off'] :root[data-scope-mask='off']
#intel-hud:is( #intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
.hud-readout, .hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) .scene-text-holdout { :root[data-scope-mask='off']:not([data-ui-theme='cyber']) .scene-text-holdout {
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
@@ -399,11 +342,7 @@
} }
:root[data-scope-mask='off'] :root[data-scope-mask='off']
#intel-hud:is( #intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
.hud-readout { .hud-readout {
display: block; display: block;
width: max-content; width: max-content;
@@ -413,11 +352,7 @@
} }
:root[data-scope-mask='off'] :root[data-scope-mask='off']
#intel-hud:is( #intel-hud:is([data-variant='minimal'], [data-variant='tactical'])
[data-variant='minimal'],
[data-variant='operator'],
[data-variant='tactical']
)
:is(.hud-top-right, .hud-bottom-right) :is(.hud-top-right, .hud-bottom-right)
.hud-readout, .hud-readout,
:root[data-scope-mask='off']:not([data-ui-theme='cyber']) :root[data-scope-mask='off']:not([data-ui-theme='cyber'])
@@ -427,38 +362,35 @@
} }
/* Preserve glyph opacity without dimming the vertical backplates. */ /* Preserve glyph opacity without dimming the vertical backplates. */
:root[data-scope-mask='off'] :root[data-scope-mask='off'] #intel-hud[data-variant='tactical'] .hud-edge {
#intel-hud:is([data-variant='operator'], [data-variant='tactical'])
.hud-edge {
opacity: 1; opacity: 1;
} }
:root[data-scope-mask='off'] :root[data-scope-mask='off']
#intel-hud:is([data-variant='operator'], [data-variant='tactical']) #intel-hud[data-variant='tactical']
.hud-edge .hud-edge
.hud-readout .hud-readout
> * { > * {
opacity: 0.5; opacity: 0.5;
} }
:root[data-scope-mask='off'] #intel-hud[data-variant='operator'] .hud-corner,
:root[data-scope-mask='off'] :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-bottom-bar {
background: transparent; background: transparent;
} }
:root[data-scope-mask='off'] :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-bottom-bar {
width: fit-content; width: fit-content;
max-width: calc(100vw - 120px); max-width: calc(100vw - 120px);
gap: 24px; gap: 24px;
} }
:root[data-scope-mask='off'] :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-bottom-bar
.hud-readout { .hud-readout {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
-13
View File
@@ -98,25 +98,12 @@
display: none; display: none;
} }
.hud-top-bar,
.hud-bottom-bar { .hud-bottom-bar {
width: calc(100vw - 24px); width: calc(100vw - 24px);
left: 12px; left: 12px;
transform: none; 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 { .hud-bottom-bar {
bottom: 90px; bottom: 90px;
font-size: 9px; font-size: 9px;
-1
View File
@@ -15,7 +15,6 @@
<span class="pp-slider-mini-label" data-i18n>Layout</span> <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> <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="tactical" data-i18n>Tactical</option>
<option value="operator" data-i18n>Operator</option>
<option value="minimal" data-i18n>Minimal</option> <option value="minimal" data-i18n>Minimal</option>
<option value="cyber" data-i18n>Cyber</option> <option value="cyber" data-i18n>Cyber</option>
</select> </select>
+5 -3
View File
@@ -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 // Panel adds named voice targets and timeout; shared Panel/Cockpit guidance
// is consolidated without changing either feature's arguments. // is consolidated without changing either feature's arguments.
// Snow leaves the set_visual_style enum. // 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].minimum;
delete center.properties[axis].maximum; 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; 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. // Snow is retired from set_visual_style; restore it to keep the legacy pin.
const visualStyle = legacy.find((tool) => tool.name === 'set_visual_style') const visualStyle = legacy.find((tool) => tool.name === 'set_visual_style')
.parameters.properties.style; .parameters.properties.style;