# Scenario schemas

The library accepts two kinds of scenario file through **Scenario library → Import scenario JSON**:

| | Schema 1 — step module | Schema 2 — content pack |
|---|---|---|
| Runtime | `xr/session.html` (`ScenarioEngine`) | `xr/mission.html` (`ScenarioMachine`, engine2) |
| Shape | linear `steps[]` with one 3D goal each | 15-state `phases[]` with role-gated `tasks[]` on a spatial tablet + 3D goals |
| Players | solo or live lockstep | multiplayer roles (Technician, Second Technician, Team Leader, QC, Instructor) |
| Scoring | sequence / time / hints / tools / safety / quiz | safety 25 · diagnostic 25 · reference 20 · execution 15 · documentation 10 · time 5 + critical rules |
| Validator | `validateScenario` (`xr/scenario.js`) | `validatePack` (`xr/engine2.js`) |
| Example | `RUNTIME_CHECK` in `xr/scenario.js` | `scenarios/apache-phantom-fault/pack.json` |

The portal routes by the `schema` field: sessions on a schema-2 scenario open `mission.html`, everything else opens `session.html`.

## How to add a scenario

1. Copy its GLB files into `assets/` (Quest-optimised: ≤ 150k tris, ≤ 2048² textures, ≤ 8 materials). In the JSON, reference them as `../assets/<file>.glb` (paths resolve from `xr/`) or as absolute `https://` URLs.
2. **Import (any machine):** Scenario library → *Import scenario JSON* → pick the file. Validation errors show under the drop zone; fix and re-import. Re-importing a file with the same `id` replaces that scenario (bump `version`).
3. **Ship with the build (optional):** save the file as `scenarios/<id>/pack.json` and add its path to `PACKS` in `xr/store.js` — it is then seeded into every fresh install and refreshed on load.
4. Create a session from the library card, assign roles in the lobby, run it on desktop emulation first, then Quest 3.

Authoring in another Claude session: hand over `docs/SCENARIO_AUTHORING_PROMPT.md`.

# Schema 2 — content pack

```jsonc
{
  "schema": 2,
  "id": "uh60-fuel-boost-pump",                 // kebab-case, unique in the library
  "version": "0.1.0",
  "techRefVersion": "[[PLACEHOLDER:TM_REV]]",
  "title": {"en":"","ar":""}, "summary": {"en":"","ar":""}, "disclaimer": {"en":"","ar":""},
  "type": "MR", "aircraft": "UH-60 — training model", "anchor": "floor",   // floor = origin on the printed floor marker, y-up, metres
  "tags": [], "durationMin": { "executive": 9, "full": 25 },
  "modes":      { "executive": { "label": {..}, "virtualQC": true, "highlight": true, "narrationSubtitles": true, "minParticipants": 2, "maxParticipants": 4 },
                  "full":      { "label": {..}, "virtualQC": false, "highlight": false, "timePressure": true, "humanFactors": true, "includeD3": false } },
  "difficulty": { "guided":     { "label": {..}, "hints": true,  "preventUnsafe": true,  "revealCorrectness": true },
                  "assessment": { "label": {..}, "hints": false, "preventUnsafe": false, "revealCorrectness": false, "criticalRules": true } },
  "roles": [ /* exactly these ids */ {"id":"technician",..}, {"id":"technician2",..}, {"id":"teamLeader",..}, {"id":"qc",..}, {"id":"instructor","max":2,..} ],  // each: label {en,ar}, color, max
  "scoring": { "pass": 80, "categories": { "safety": 25, "diagnostic": 25, "reference": 20, "execution": 15, "documentation": 10, "time": 5 } },
  "critical": [
    { "id": "work_before_isolation",  "when": "interactBeforeFlag", "flag": "energyIsolated", "targets": ["access-panel","lru"], "label": {..} },
    { "id": "advance_without_isolation", "when": "advanceWithoutFlag", "phase": "SAFETY_SETUP", "flag": "energyIsolated", "label": {..} }
  ],
  "world": {                                   // same vocabulary as schema 1 (assets / objects / zones / panels) plus the spatial UI anchors
    "assets": [ { "id": "pumpModel", "src": "../assets/fuel_pump_q.glb", "position": [0,1.0,-0.5], "rotation": [0,0,0], "scale": 0.2 } ],
    "objects": [ { "id": "pump", "label": {..}, "asset": "pumpModel", "inspectable": true, "pokeable": true },
                 { "id": "lockout-tag", "label": {..}, "grabbable": true, "primitive": {..}, "position": [..], "snapTo": ["cb-lock"], "home": "bench-c" } ],
    "zones":   [ { "id": "cb-lock", "kind": "snap", "position": [..], "radius": 0.08, "accepts": ["lockout-tag"], "label": {..} } ],
    "panels":  [ { "id": "bay-panel", "position": [..], "rotation": [..], "width": 0.3, "height": 0.2, "title": {..},
                   "buttons": [ { "id": "btn-status", "kind": "button", "label": {..} }, { "id": "cb-main", "kind": "toggle", "state": true, "label": {..} } ] } ],
    "tablet":  { "position": [-0.78,1.22,-0.05], "rotation": [-0.1,0.55,0], "width": 0.52, "height": 0.38 },   // required
    "hud":     { "position": [0.05,1.82,-0.35], "rotation": [0.25,0,0] },
    "console": { "position": [1.0,1.32,0.02],  "rotation": [-0.15,-0.6,0] },
    "footprint": { "center": [0.15,0,-0.1], "size": [2.6,1.8] }                                            // area-clear check, metres
  },
  "phases": [
    { "id": "BRIEFING", "lmsStart": "viewed-briefing", "lmsComplete": "initialized", "title": {..}, "objective": {..},
      "tabs": [ { "id": "workorder", "title": {..}, "body": {..} } ],
      "tasks": [
        { "id": "view_workorder", "kind": "view", "tab": "workorder", "label": {..}, "lms": "viewed-briefing" },
        { "id": "risk_level", "kind": "choice", "label": {..}, "options": [ { "id": "low", "label": {..} }, { "id": "medium", "label": {..} } ], "correct": "medium", "feedbackOk": {..}, "feedbackBad": {..}, "lms": "identified-hazard" },
        { "id": "start_task", "kind": "confirm", "label": {..}, "requiresRole": ["teamLeader"], "fallbackRole": "technician", "requiresTask": "view_workorder", "completesPhase": true, "lms": "initialized" }
      ],
      "advance": { "requiresRole": ["teamLeader"], "fallbackRole": "technician" } },
    { "id": "DOCUMENT_CHECK", "documents": [ { "id": "tm_b", "title": {..}, "revision": "..", "applicability": {..}, "applicable": true, "feedbackOk": {..} }, { "id": "tm_a", "applicable": false, "feedback": {..}, .. } ],
      "tasks": [ { "id": "select_ref", "kind": "selectDocument", .. }, { "id": "open_fip", "kind": "confirm", "requiresTask": "select_ref", "completesPhase": true, .. } ], .. },
    { "id": "SAFETY_SETUP", "tasks": [
        { "id": "ppe", "kind": "choice", "mustBeCorrect": true, .. },
        { "id": "work_area", "kind": "check", .. },
        { "id": "energy_isolate", "kind": "goal", "goal": { "type": "toggle", "target": "cb-main", "state": false }, "flag": "energyIsolated", .. },
        { "id": "lockout", "kind": "goal", "goal": { "type": "place", "target": "lockout-tag", "zone": "cb-lock" }, .. },
        { "id": "calibration", "kind": "choice", "correct": "cal_expired", "critical": "invalid_calibration_used", "criticalLabel": {..}, .. },
        { "id": "safety_confirm", "kind": "confirm", "requiresRole": ["teamLeader"], "fallbackRole": "technician", "requiresFlag": "energyIsolated", "completesPhase": true, .. } ], .. },
    { "id": "EVIDENCE_REVIEW", "status": "D3", "title": {..} }   // stub: runtime stops here with a boundary event
  ],
  "placeholders": ["TM_REV", ..]                 // every [[PLACEHOLDER:NAME]] used in the text
}
```

Phase ids (fixed state machine, use in this order, omit none you deliver later — stub them with `"status": "D3"`):
`BRIEFING · DOCUMENT_CHECK · SAFETY_SETUP · EVIDENCE_REVIEW · HYPOTHESIS_SELECTION · TEST_PLANNING · COMPONENT_INSPECTION · DIAGNOSIS · CORRECTIVE_ACTION · POST_MAINTENANCE_TEST · DOCUMENTATION · QC_REVIEW` (`DEBRIEF`, `COMPLETED`, `FAILED_CRITICAL` are engine states).

Task kinds and what completes them
* `view` — the trainee opens `tab` on the tablet.
* `check` — checkbox; unchecking clears its `flag`.
* `choice` — `options[].id`; `correct` marks the right one. `mustBeCorrect` blocks until right; a wrong pick with `critical` ends the run as a critical failure (in Guided it becomes a warning). `feedbackOk` / `feedbackBad` (or per-option `feedback`) show when `revealCorrectness` is on; an option may carry a `flag`.
* `selectDocument` — picks from the phase `documents[]`; a non-`applicable` pick is a `wrongRevision` procedural error.
* `confirm` — button; `requiresTask` / `requiresFlag` gate it; `completesPhase` advances.
* `goal` — 3D interaction `{ type: poke | grab | place | toggle | inspect, target, zone?, state? }`; `target` is an object id or a panel button id.

Every task: `label` `{en, ar}`, optional `requiresRole` (array of role ids; instructors always pass; `fallbackRole` acts when no Team Leader is present), `optional`, `flag` (set when done), `lms` (xAPI verb slug).

Flags the engine and scoring read: `energyIsolated`, `calibrationValid` (D2 safety gates); `rootCauseCorrect`, `opCheckComplete`, `documentationComplete` (pass conditions — set them from DIAGNOSIS, POST_MAINTENANCE_TEST and DOCUMENTATION tasks). The diagnostic category reads BRIEFING task ids `review_history`, `view_flightreport`, `risk_level`; keep those ids.

Text rules: every string bilingual `{en, ar}`; unverified technical values wrapped as `[[PLACEHOLDER:NAME]]` and listed in `placeholders`; `disclaimer` states the data is fictional training data. No real maintenance-manual content.

## Learn → Do additions (schema 2, runtime v2)

Every `goal` task can carry a **Learn** demonstration: a translucent ghost of the part or tool performs the step in place, looping with a subtitle, until the trainee starts doing it. In **Guided** the demo of the next open step plays automatically and the tablet's ▶ LEARN button replays it for free; in **Assessment** nothing auto-plays and ▶ LEARN is a paid hint (`hintUsed`, −5 % execution per use).

```jsonc
{ "id": "unthread", "kind": "goal", "goal": { "type": "turn", "target": "bnut", "tool": "wrench", "turns": 3, "dir": "ccw" },
  "requiresTask": "cut_wire",                       // goals now honour sequencing — out-of-order attempts toast + log proceduralError 'sequence'
  "learn": { "en": "Seat the wrench jaw on the B-nut and rotate anticlockwise — three turns.", "ar": "…" },   // subtitle; falls back to label
  "label": {..}, "lms": "loosened-fastener", "requiresRole": ["technician"] }
```

New goal vocabulary
* `turn` — hold `tool` on `target` and rotate the wrist about `world.fx.nut.axis`: `turns` × 360° in `dir` (`ccw` | `cw`), ratchet haptics every 1/8 turn; `torque: true` ends with a CLICK flash. On desktop, tool contact alone advances the turn (no wrist rotation with a mouse).
* `inspect` — completes after 2 s of gaze/ray dwell on the part (green glow ramps up), or immediately on an explicit trigger: pinch/click while pointing at it, or a fingertip touch.
* `poke` + `tool` — the step completes only when the held tool touches the target (e.g. cutters on the lockwire). Finger pokes still count as *touching the joint* for `interactBeforeFlag` criticals.

3D controls — objects may declare `control` instead of `primitive`/`asset`; they are poke-toggled, animated and synced (`ctrl` messages):
```jsonc
{ "id": "fuel-selector", "label": {..}, "control": { "type": "rotary", "state": true, "on": {..}, "off": {..} }, "position": [..], "rotation": [..] }
{ "id": "cb-boost", "control": { "type": "breaker", "state": true } }          // push-pull breaker, white collar when open
{ "id": "gauge",    "control": { "type": "gauge", "label": {..}, "value": 0 } }   // read-only needle, driven by world.fx.pressure
```

Assets: `hidden: true` loads but hides a GLB (x-ray variant); `play: true | <speed>` runs its animation clips.

`world.fx` binds the effects layer to the pack (all ids are objects / zones / `PHASE:task` keys):
```jsonc
"fx": {
  "leak":     { "at": "bnut", "dir": [0.35,-0.45,0.82], "pressureFlag": "energyIsolated", "stopTask": "SAFETY_SETUP:verify_zero", "repairedTask": "CORRECTIVE_ACTION:seat_hose" },
  "catcher":  { "object": "basin", "zone": "basin-under-leak", "radius": 0.2, "height": 0.08 },   // drips land in the basin once it occupies the zone
  "pressure": 0.72, "pressurisedTask": "POST_MAINTENANCE_TEST:leak_test",                         // gauge value 0..1; live again after the leak test
  "wire":     { "object": "lockwire", "cutTask": "CORRECTIVE_ACTION:cut_wire" },                     // ring vanishes, two tails fall
  "nut":      { "object": "bnut", "axis": [1,0,0], "turns": 3, "travel": 0.012, "openTask": "CORRECTIVE_ACTION:unthread", "torqueTask": "CORRECTIVE_ACTION:torque" },
  "pin":      { "object": "rigging-pin", "task": "CORRECTIVE_ACTION:rig_pin", "removeTask": "POST_MAINTENANCE_TEST:remove_pin", "depth": [0,0,-0.03] },
  "drips":    { "object": "hose-damaged", "afterTask": "CORRECTIVE_ACTION:unthread", "seconds": 5, "offset": [-0.16,0,0] },
  "xray":     { "show": "engineCut", "hide": "engineModel", "path": [[0.66,0.9,-0.25], [0.34,0.9,-0.25], [0.05,1.02,-0.36]] }   // cutaway swap + animated fuel path
}
```
Leak logic (derived on every client from machine state, so it is identical for all participants): pressurised spray while `pressureFlag` is unset → decaying drips after isolation → stops at `stopTask` → gone once `repairedTask` is done. Spray landing on the floor grows an iridescent puddle; vapour haze hangs over it. Phase completion fires a burst at the tablet; `place` / `inspect` completions flash green at the zone or part; a spotlight and pulsing ring follow the trainee's next target.

Scoring v2 (complete): safety 25 (SAFETY_SETUP items, violations, critical → 0) · diagnostic 25 (briefing 20 % + first-try `hypothesis` 30 % / `test_method` 20 % / `root_cause` 30 %) · reference 20 · execution 15 (CORRECTIVE_ACTION + POST_MAINTENANCE_TEST goals done, −5 % per paid demo) · documentation 10 (DOCUMENTATION + QC_REVIEW items) · time 5. Categories whose phases a pack stubs as D3 stay `null` (partial result).

Emulation shortcut: `xr/mission.html?dev=<scenarioId>&mode=executive|full&difficulty=guided|assessment` creates an instructor-hosted local session and opens it (desktop: left-drag tools, hold a tool on the nut to turn).

## Spatial window, workshop bench, engine run (runtime v2.1)
* **One window** (`ui: 'window'`, set by the mission runtime): glass panel with Steps · Reference · Team tabs plus X-RAY / CUTAWAY / ENGINE buttons; pinch its bar to move it (ray or hand), ◉ pins it, – minimises it to a pill; unpinned it lazily follows the user's head at 0.95 m. Dragging a window pins it. The old `hud` / `tablet` / `console` positions are ignored for schema-2 packs.
* **TAKE**: every hands-on step with a tool or part has a ✋ TAKE button — the item flies to the user's hand (or in front of the camera on desktop); a ray-pinch on a distant grabbable also pulls it into the hand.
* **Sequencing feedback**: a goal attempted before its `requiresTask` is logged as a procedural error and the trainee sees “Not yet — first: <prerequisite label>”. Inspect targets, the placement guide, tool auto-actions and control confirmations are only armed once their prerequisite is done, so the Learn eye / guide arc never points at a step that cannot yet be completed.
* **Already-satisfied steps**: a `place` goal whose part is already in the target zone, or a `toggle` goal whose control is already in the required state, completes on a tap/poke/DO of that part or control (confirming the state) instead of dead-locking.
* **Catch tray physics**: `world.fx.catcher = { object, zone, radius, floorY, fill: { w, h, area, depth } }` — the tray is the catcher wherever it is carried (not only when snapped). Every stream blob / drop that lands inside adds its volume; the liquid surface inside rises as volume ÷ inner area (0.3 × 0.21 m tray → ≈ 1 mm per 63 ml), sloshes, ripples at each hit point and caps at the rim depth. No floor film forms under the tray and any film beneath it is wiped when it is set down. After the repair, moving the tray away from the leak slowly empties it (disposal).
* **Valve control**: `control: { type: 'valve', on, off }` — petcock with a red T-handle that turns a quarter turn; opening it plays a drain burst. Control-based steps (`poke`/`toggle` on a 3D control or a canvas-panel button) get a ✋ **DO** button on the window and side panel, so a hard-to-reach control can always be actioned from the UI (logged like a physical actuation). The window lists open steps first, so the current step is always on page 1.
* **Composite primitives**: `primitive.type` also accepts `hose` (`size: [radius, length]`, `chafed: true` for the worn variant — braided tube with ferrules and B-nuts), `pin` (`size: [radius, length]` T-handle) and `cutters` (`size: [length]`).
* **Ghost name tag**: every Learn demonstration shows the item's name over the ghost and, when the item lives in a labelled slot, a yellow "take from: <slot>" line plus a dotted fetch arc from the slot to the work point.
* **Guided placement**: for the current `place` step an animated dotted guide arcs from the part to its target ring (turns green when close), the target's magnetic radius widens to 14 cm so the part snaps home as it approaches, and a **double pinch / double click** on the part while pointing at it auto-flies it into position and records the step. Applies to the current step only. **Tools too**: double-tapping the current step's tool (cutters, wrench) flies it to the part in working pose, performs the cut / the ratchet turns (nut animates, torque click), records the step and returns the tool to its bench slot.
* **Labelled slots**: snap zones with `"showLabel": true` render their `label` above the ring (`labelHeight` optional) — the workshop bench in Scenario 02 lists every tool and spare this way.
* **Engine run**: `world.fx.engine = { control, asset, speed }` — the control's ON state plays the GLB animation clips (and the cutaway's), adds vibration and boosts the leak while the joint is unrepaired. Criticals accept `fromPhase` (rule applies once that phase has started) and `state` (only that toggle state), e.g. `{ when: 'interactBeforeFlag', flag: 'repairComplete', targets: ['engine-start'], state: true, fromPhase: 'SAFETY_SETUP' }`.
* **Safety zones** render nothing but their label (`"visible": false` hides the label too) — they act as invisible hazard triggers; the runtime toasts and logs the violation when a hand or held object enters.
* **Leak realism** (runtime v2.4): fuel is red-dyed (pink thin film → deep red body). Pressurised state = a particle fluid rendered as ONE surface: blobs accumulate a screen-space thickness field in an offscreen target, a composite pass thresholds it into a smooth connected liquid and shades it as glass (thickness tint, fresnel, specular, soft edge). Physics is real: exit ≈ 0.7 m/s from the side of the fitting, free fall at 9.81 m/s², the stream thins as it accelerates (continuity) and breaks into a drop train ~0.3–0.5 m below the exit (Plateau–Rayleigh); sub-step emission advances age and velocity so the stream stays continuous at any frame rate. The launch velocity is solved so the stream lands on the catcher zone (`world.fx.catcher.zone`, the basin spot), i.e. the basin ring always marks the real impact point; isolated state = a pendant droplet swells under the fitting, necks and detaches every 0.7–1.2 s (2× with the engine running), falls as a shaped drop with a specular highlight, and lands with a splash ring + spatter. The floor film is a 256² thickness map: every landing paints a drop; each step the film spreads (surface-tension front), creeps down the floor slope (`world.fx.floorSlope`, default toward +x/+z) and slowly evaporates — so a leak left running forms a pool that runs off toward the drain side. Drops caught by the basin do not reach the floor. A wet run-down streak stays on the fitting until the repair. Drop material is a glossy standard material (no transmission pass) to stay within the Quest budget.
* **X-RAY vs CUTAWAY**: X-RAY turns the engine skins into fresnel glass (parts stay in place, fuel path visible); CUTAWAY swaps in the cross-section GLB (`world.fx.xray.show`).

# Schema 1 — step module

A schema-1 scenario is one JSON file. Import it from **Scenario library → Import scenario JSON** (validated by `validateScenario`).
Every user-facing string is bilingual: `{ "en": "...", "ar": "..." }`. Units: metres, y-up, origin = marker centre on the bench top, +z toward the trainee.

```jsonc
{
  "schema": 1,
  "id": "uh60-fuel-boost-pump",              // unique, kebab-case
  "title": { "en": "...", "ar": "..." },
  "summary": { "en": "...", "ar": "..." },
  "type": "MR" | "AR",
  "aircraft": "generic" | "UH-60" | "AH-64",
  "durationMin": 10,
  "tags": ["fuel", "sop"],
  "scoring": { "pass": 70, "weights": { "sequence": 25, "time": 15, "hints": 10, "tools": 15, "safety": 20, "quiz": 15 } },

  "assets": [                                  // GLB files (Quest-optimised: ≤ 150k tris, ≤ 2048² textures, ≤ 8 materials)
    { "id": "pump", "src": "assets/fuel_pump.glb", "position": [0,0,0], "rotation": [0,0,0], "scale": 1 }
  ],

  "objects": [
    // procedural primitive
    { "id": "wrench", "label": {"en":"Torque wrench","ar":"مفتاح العزم"}, "tool": true, "grabbable": true,
      "primitive": { "type": "box" | "cylinder" | "sphere" | "torus" | "capsule", "size": [0.2,0.018,0.03], "color": "#9aa4b4", "metal": 0.8, "rough": 0.3, "emissive": "#000", "emissiveIntensity": 1 },
      "position": [-0.45,0.03,0.16], "rotation": [0,0,0], "home": "tray-2" },
    // node inside a GLB asset
    { "id": "cap", "label": {...}, "asset": "pump", "node": "Cap_01", "grabbable": true, "snapTo": ["socket"],
      "animation": { "type": "spin" | "oscillate" | "clip", "axis": "y", "speed": 6, "dir": [0,1,0], "amp": 0.02, "clip": "CapOpen", "trigger": "always" | "step" } }
  ],

  "zones": [
    { "id": "socket", "kind": "snap",   "position": [0.3,0.215,-0.05], "radius": 0.07, "accepts": ["cap"], "label": {...} },
    { "id": "hot",    "kind": "safety", "position": [0.04,0.09,-0.17],  "radius": 0.10, "label": {"en":"HOT — keep hands clear","ar":"..."} }
  ],

  "panels": [
    { "id": "bench-panel", "position": [-0.42,0.16,-0.22], "rotation": [-0.45,0.25,0], "width": 0.3, "height": 0.19,
      "title": {...}, "buttons": [ { "id": "btn-start", "kind": "button", "label": {...} }, { "id": "switch-run", "kind": "toggle", "label": {...} } ] }
  ],

  "steps": [
    { "id": "s1", "title": {...}, "instruction": {...}, "hint": {...},
      "goal": { "type": "poke", "target": "btn-start" },                     // poke | toggle → button id or pokeable object id
      "highlight": ["bench-panel"], "timeTargetSec": 20, "points": 10 },
    { "id": "s2", "goal": { "type": "grab", "target": "torch" }, "tool": "torch" },       // tool: grabbing another tool = toolError
    { "id": "s3", "goal": { "type": "inspect", "target": "bearing", "seconds": 2 } },    // ray/gaze dwell
    { "id": "s4", "goal": { "type": "place", "target": "torch", "zone": "tray-1" } },
    { "id": "s7", "goal": { "type": "toggle", "target": "switch-run", "state": true },
      "onComplete": [ { "action": "animate", "target": "shaft", "state": "on" } ] },
    { "id": "s8", "goal": { "type": "quiz" } }                                            // completes when all quiz[] answered
  ],

  "quiz": [
    { "id": "q1", "question": {...}, "options": [ {...}, {...}, {...} ], "answer": 0, "points": 5 }
  ]
}
```

Rules the engine enforces
* Acting on a **future** step's target (same action type) → `sequenceError`. Grabbing tools is free unless the step declares `tool`.
* Hands or held objects inside a `safety` zone → `safetyViolation` (1.5 s debounce).
* `hint()` → `hintUsed`; instructor prev/next → `instructorSkip`; instructor ± → `instructorMark {delta, note}`.

Asset sourcing order: 1) linked project folder (Quest-optimised GLBs: `UH60_Trainer.glb`, `Engine_Animated_Quest*.glb`, `Meshy_Dual_Tank_Quest3_*.glb`, `parts/optimized/`), 2) request from the client with a searchable name + suggested store links.


## Test console (world.station)
A **procedurally built** fuel-system test console (`xr/console.js`) — cabinet, sloped fascia, instrument head, dials, engraved legends and the plant model all authored in one coordinate system, so nothing is pasted onto a borrowed model and nothing floats. No external asset, no licensing question.

`world.station`:
* geometry — `position`, `rotation`, `width`, `depth`, `bodyHeight`, `fasciaHeight`, `headHeight`, `tilt` (fascia slope, rad), `title` (engraved on the body front).
* `dials[]` — `{ id: "psi"|"ng"|"bus", name, unit, range:[lo,hi], band:[0..1,0..1] }`. Recessed in the head with steel case, bezel and glass; canvas faces redraw only when the needle actually moves.
* `controls[]` — `{ id, u, v, off }` in fascia-local metres (`u` across, `v` up-slope, `off` along the outward normal). The runtime re-parents each control onto the console and gives it a contrast mounting boss + trim ring. Desk-mounted controls set `control.plate: false` and `control.labels: false`; the naming comes from the legend plates.
* `legends[]` — `{ id, u, v, width, name, fn }` engraved bilingual plates (name + what the control does), authored above their control.
* `screen` — `{ panel: <existing panel id>, mount: "head"|"fascia", u, v, off, width, height }`. The named pack panel is re-parented into the console and re-drawn as the live readout (line pressure / flow mismatch / pump amps, six annunciator lamps, fault codes, the two test buttons); its button ids, goals, ✋ DO buttons and network sync are unchanged. The button column mirrors in RTL, so the readouts shift to keep clear of it.
* `brand` — `{ left, right, width, height, y }` etched steel maker's plate on the body front (Holostrata + Beyond Universe marks, four rivets, hairline divider). Logos are transparent PNGs in `assets/`; the Holostrata mark was keyed off its white background and cropped. Deliberately small and low on the cabinet — visible when you walk up to the console, never in the way of a task.
* `bind` — `{ selector, breaker, generator, engine, bleed }` control ids feeding the plant model; GPU connection and repair state come from the mission page via `rt.setPlant()`, step gating via `rt.setGates()`.

### Plant model (Instruments, in xr/console.js)
First-order sim updated every frame: DC bus (GPU + generator, 0.45 s slew), boost-pump current, line pressure (pump head − leak loss, flutter while leaking, dumped by the bleed valve, ~6 s trapped-pressure decay), supply/return flow with the leak as an unaccounted mismatch, Ng and TGT with a rich-start overshoot. Digits sample-and-hold at 5 Hz with instrument jitter. Training values, not AMM data.

**Undo:** delete `world.station` (the console is additive) or restore `pack.posts-2.5.2.json`, which holds the post-mounted control layout used before the console.
