# Adding Maps to the Desert Nomad Cinematic Lab

This is the authoritative extension procedure for `https://orbie-desert-nomad-cinematic.pages.dev/`. It is written for coding agents operating from the cinematic project root.

A map is not just a selector label. It is a builder, runtime profile, descriptive contract, asset package, camera treatment, cinematic hierarchy, full motion pass, mobile pass, and deployed verification.

## Non-negotiable rules

- Add a new environment. Do not silently overwrite an accepted map unless replacement is explicit.
- Preserve the approved Desert Nomad, Idle, Boast, grounding, playback controls, unlocked mouse/touch contract, post stack, mobile layout, and QA hooks.
- Keep foreground and midground as actual 3D geometry. A plate may be distant atmosphere only; free orbit should use an equirectangular HDRI or procedural sky.
- Preserve a central hero lane of at least 1.6 m. No architecture, prop, flag, particle wall, or light fixture may intersect it.
- First pointer interaction must keep taking ownership from cinematic auto-drift. Do not add pointer lock or an azimuth clamp.
- Provider success is not acceptance. Inspect actual GLB bytes and multi-angle renders.
- Asset-backed builders must await all required assets before the environment change completes.
- Dispose environment geometry/materials/textures on map switch. Dispose `light.shadow.map` / `light.shadow.mapPass` for removed shadow-casting lights as well; otherwise repeated procedural-map switching leaks GPU textures even when geometry counts stay flat. Mark cached shared environment textures with `texture.userData.sharedEnvironmentAsset = true` so disposal does not invalidate the cache.
- Do not deploy until all environments × both motions pass locally, including the new map in off-axis orbit and exact mobile.
- Never expose API keys, signed URLs, provider task secrets, or deployment credentials.

## Extension points

A map touches these authoritative files:

1. `src/main.js`
   - add `buildNewMap(profile)`;
   - add one `PROFILES.new_map_id` row.
2. `scene_profiles.json`
   - add the descriptive/asset/lighting/cinematic contract.
3. `scripts/build.mjs`
   - increment `environment_count`.
4. `public/`
   - add optimized models/backgrounds when the map is asset-backed.
5. `src/*.generated.js`
   - add/import a generated asset manifest when models are not procedural.
6. `guides/validate_map_extension.py`
   - run it; do not edit it for a normal map.

The environment selector and test matrix are generated from `PROFILES`; do not hand-edit selector HTML.

### Character-aware map profiles

This lab can preserve the original Desert Nomad on existing maps while assigning an accepted roster character to a new map. Add the character once to `CHARACTER_PROFILES` with its exact prompt, scale, and validated Idle/Boast GLBs, then set `character: 'character_slug'` on the runtime profile. Copy only already-optimized, content-hashed web GLBs into `public/models/`; do not rerig or regenerate a character as part of a map extension. The full matrix must assert the expected character ID and embedded clip for every environment × motion pair, so a map switch cannot silently leave the previous character active.

If the character itself is new or its source/rig/animation acceptance is incomplete, stop and follow the roster project’s `ADDING_CHARACTERS.md` first. Map acceptance never substitutes for character four-view, Idle, or Boast acceptance.

Start the design contract from `guides/map_contract.template.json`.

## Builder contract

`setEnvironment()` awaits `profile.builder(profile)`. A builder may be synchronous or `async`, but it must return:

```js
{
  group,                         // THREE.Group; required
  updates,                       // array of (elapsed, delta) => void; required
  environmentTexture,           // optional IBL texture
  environmentIntensity,         // optional IBL intensity
  backgroundTexture,            // optional equirectangular background
  backgroundBlurriness,         // optional 0..1 cinematic defocus
  backgroundIntensity,          // optional
  badge,                         // optional HUD label
  diagnostics                   // optional machine-readable QA fields
}
```

A minimal procedural builder can reuse existing helpers:

```js
function buildNewMap(profile) {
  const world = baseWorld(profile);
  const { group, heightAt, updates } = world;

  const structure = addBox(
    group,
    [4.0, 2.4, 1.5],
    [-4.2, heightAt(-4.2, -6.0) + 1.2, -6.0],
    mat(0x3d454d, 0.82, 0.18),
  );
  structure.castShadow = true;
  structure.receiveShadow = true;

  return {
    ...world,
    updates,
    diagnostics: {
      kind: 'procedural-3d',
      protectedHeroLane: true,
    },
  };
}
```

Do not put a large object at `x≈0` in front of or behind the character without measured clearance.

## Runtime profile contract

Every profile needs the fields consumed by `setEnvironment()`, `frameMotion()`, `profilePost()`, and the UI:

```js
new_map_id: {
  id: 'new_map_id',
  label: 'New Map Display Name',
  short: 'selector subtitle',
  badge: 'short HUD badge',
  description: 'What is actually rendered and why the character remains dominant.',
  builder: buildNewMap,

  fog: 0x28333d,
  fogDensity: 0.025,
  environmentIntensity: 0.7,
  portraitKeyIntensity: 19,
  portraitRimIntensity: 12,

  camera: {
    idle: { position: [0.5, 1.7, 4.1], target: [0, 0.9, -0.1] },
    boast: { position: [0.7, 1.9, 4.8], target: [0, 1.02, -0.15] },
  },
  orbit: { minPolar: 0.86, maxPolar: 1.64 },

  post: {
    exposure: 1.05,
    bloom: 0.4,
    threshold: 0.82,
    grain: 0.012,
    vignette: 0.32,
    aberration: 0.0007,
    contrast: 1.05,
    saturation: 0.9,
    grade: 0.15,
    shadow: [0.86, 0.96, 1.05],
    highlight: [1.08, 1.0, 0.88],
  },
}
```

Do not set `minAzimuth` or `maxAzimuth` unless the user explicitly requests a camera restriction. Do not set `boundedDrift`; it intentionally makes manual orbit fight a cinematic camera.

## Procedure

### 1. Define the map before building it

Create a filled map contract from `guides/map_contract.template.json`. Specify:

- unique snake-case ID;
- time/weather;
- actual foreground, midground, distant-background layers;
- practical and portrait lights;
- Idle/Boast camera intent;
- hero-lane clearance;
- asset source and poly strategy;
- visual hierarchy and cinematic trick set;
- acceptance shots.

Run the current non-billable contract check before edits:

```bash
python3 guides/validate_map_extension.py
```

Completion criterion: the existing project reports `PASS`; the proposed ID does not collide.

### 2. Choose procedural or asset-backed construction

Use procedural geometry when the map depends on repeated simple forms, terrain, cables, panels, tents, road pieces, particles, or abstract architecture. Use actual generated GLBs when the object identity and parallax matter.

For generated architecture, prefer a modular pack over one entire-city model:

- left architecture;
- right architecture;
- central landmark with intentional opening;
- one or two prop clusters.

Inspect every actual model from front, three-quarter, side, and rear. Reject cutaway floors, flat cards, floating levels, missing backs, malformed wheels, or unrelated content.

Completion criterion: each accepted asset has source metadata, four-view evidence, measured bounds, byte count, SHA-256, and a placement role.

### 3. Optimize asset-backed maps

Keep sources outside `public/`; place only web copies in `public/models/<map-id>/`.

A typical optimization command is:

```bash
npx --yes @gltf-transform/cli optimize \
  source.glb public/models/new_map_id/model.tmp.glb \
  --compress draco --texture-compress webp \
  --texture-size 1024 --simplify false
```

Rename the output with a SHA-256 prefix, then validate:

```bash
npx --yes @gltf-transform/cli validate public/models/new_map_id/model-<hash>.glb
python3 -c "from pathlib import Path; p=Path('public/models/new_map_id/model-<hash>.glb'); assert p.read_bytes()[:4] == b'glTF' and p.stat().st_size > 1024"
```

If testing LOD, hold camera, placement, texture cap, lights, fog, post, and motion time fixed. Compare high/balanced/low in the actual scene. Extreme simplification may preserve the front silhouette while shredding side/rear geometry.

Completion criterion: optimized GLBs re-import successfully and pass the same structural/multi-angle review as sources.

### 4. Add the builder

Add `buildNewMap(profile)` near the existing builders in `src/main.js`.

For external assets:

- import a generated manifest module;
- load required GLBs with the existing `loadGltf()`/Draco path;
- use `Promise.all` so the map does not appear half-loaded;
- normalize from measured world bounds;
- ground each model;
- set reviewed position/yaw/target dimensions;
- apply background material treatment intentionally;
- return diagnostics: loaded count, triangles, world bounds, and minimum hero-lane distance.

Register animated particles/lights as functions in `updates`; do not create an extra render loop.

Completion criterion: switching repeatedly to/from the map does not leak objects or emit errors.

### 5. Add the profile and descriptive contract

Add the runtime row to `PROFILES` and the matching row to `scene_profiles.json`. The validator requires the ID sets to match exactly.

Increment `environment_count` in `scripts/build.mjs`.

Run:

```bash
python3 guides/validate_map_extension.py --id new_map_id
npm run build
python3 guides/validate_map_extension.py --id new_map_id --check-dist
```

Completion criterion: both validator runs and the build exit `0`; `dist/build-manifest.json.environment_count` equals the number of runtime profiles; the guide is present in `dist/guides/`.

### 6. Apply cinematic hierarchy

Use the smallest set of techniques that serves the shot:

- atmospheric perspective through Exp2 fog;
- equirectangular HDRI background blur;
- lower background brightness, saturation, contrast, and IBL;
- higher background roughness;
- warmer/cooler portrait key and rim on the character;
- motivated practicals near story objects;
- foreground edge framing that remains valid off-axis;
- restrained bloom, vignette, grain, and chromatic aberration;
- lower-poly and lower-detail geometry with distance;
- protected negative space behind the head, hands, backpack, and legs.

Do not use fog to hide broken geometry. Do not add untextured placeholder cubes that only look acceptable from the hero camera.

Completion criterion: character identity and silhouette read first in Idle and at the Boast apex; the environment reads second without becoming empty.

### 7. Run local browser QA

Serve:

```bash
npm run serve
```

Open:

- direct Idle: `http://127.0.0.1:8792/?env=new_map_id&motion=idle`
- direct Boast: `http://127.0.0.1:8792/?env=new_map_id&motion=boast`
- exhaustive matrix: `http://127.0.0.1:8792/?qa=1`

Required visual passes:

1. desktop Idle front camera;
2. desktop Boast apex;
3. off-axis orbit left/right and rearward enough to expose cards, ground edges, blank blocks, or floating props;
4. camera drift on, then real pointer interaction — drift must turn off;
5. exact `390×844@2x` touch emulation, full page;
6. repeated switching between every map.

Runtime gates currently require:

- draw calls `< 260`;
- rendered triangles `< 300000`;
- geometries `< 240`;
- textures `< 64`;
- programs `< 80`.

Do not raise a budget just to make an accidental leak pass. If a purposeful textured asset pack crosses one narrow cap while all other costs remain controlled, document the reason and rerun the whole matrix.

Completion criterion: matrix reports `PASS` for every environment × Idle/Boast plus pause, restart, FX, layout, and budgets; browser errors are empty.

### 8. Deploy and verify

Deploy only the built `dist/` directory:

```bash
npx --yes wrangler@latest pages deploy dist \
  --project-name orbie-desert-nomad-cinematic \
  --branch main --commit-dirty=true
```

Record the immutable URL. Verify canonical plus immutable bytes, magic, MIME, cache and security headers:

```bash
python3 verify_deployment.py \
  --base https://orbie-desert-nomad-cinematic.pages.dev \
  --base https://<deployment-prefix>.orbie-desert-nomad-cinematic.pages.dev
```

Run `?qa=1` on canonical production and capture a clean live Idle frame for the new map.

Completion criterion: exact hosted checks and canonical matrix pass; `/guides/ADDING_MAPS.md` returns the current guide; canonical direct map URL returns `200`.

## Acceptance checklist

- [ ] Unique map contract
- [ ] Builder returns required group/updates contract
- [ ] Runtime and descriptive profile IDs match
- [ ] Build count incremented
- [ ] Actual foreground/midground 3D geometry
- [ ] Source and optimized asset inspection pass
- [ ] Hero lane ≥ 1.6 m
- [ ] Idle front visual pass
- [ ] Boast apex visual pass
- [ ] Off-axis unlocked-orbit pass
- [ ] Real pointer handoff disables drift
- [ ] Exact 390×844 mobile pass
- [ ] Full local matrix and budgets pass
- [ ] Canonical and immutable verification pass
- [ ] Canonical live matrix pass
- [ ] No secrets or signed URLs

## Common failure modes

- **Selector missing:** the profile was not added to `PROFILES` or source failed before button creation.
- **Validator ID mismatch:** add the same ID to `scene_profiles.json`; do not rename only one side.
- **Map switches but old objects remain:** every environment object must be under the returned `group`.
- **HDRI disappears after switching:** cached shared texture was disposed; mark it `sharedEnvironmentAsset`.
- **Manual camera snaps back:** remove `boundedDrift`; first controls interaction must call `setDrift(false)`.
- **Looks good only from front:** perform off-axis orbit; reject cards, ground edges, floating pieces, and blank cuboids.
- **Character becomes secondary:** reduce background contrast/detail/IBL before adding more bloom.
- **Matrix intermittently fails in a background tab:** rerun in a clean foreground browser with other heavy WebGL pages closed before changing thresholds.
- **Dirty parent repository:** do not reset or commit unrelated parent-tree changes; a manual Pages preview may be the honest release mode.
