# NIGHTSHIFT · Turnover for Claude Code

**Project:** Dispatch (Three.js noir-dispatcher web game)
**Working title:** Nightshift
**Status:** Playable prototype + design doc · Ready for Milestone 2 work
**Engine:** Three.js r160 · React 18 (Babel standalone, no build) · vanilla CSS
**Repo state:** Single flat directory. No bundler. No package.json yet.

> **The pitch in one line.** You're the graveyard-shift dispatcher at a small PI agency in a noir city. Calls come in over the wire. You match operatives' skills against case requirements under time pressure, balancing payout against "city heat."

See `design-doc.html` for the full vision (chapters 1–7, progression, audio plan, narrative threads). This document is what's *actually built* and how to continue.

---

## 0 · How to run it

It's static HTML. No build step.

```
open index.html                # landing card → links to all 3 artifacts
open Nightshift.html           # desktop prototype
open "Nightshift Mobile.html"  # mobile prototype
open design-doc.html           # full design spec
```

All scripts come from unpkg with pinned versions + SRI hashes. The page must be served over `http://` (not `file://`) for the Babel inline transform to work cleanly — any static file server is fine.

---

## 1 · File map (what's actually in this project)

```
index.html                # Landing card (3 links). No game logic.
Nightshift.html           # Desktop shell.   Loads game-data.js + scene.js + ui.jsx
Nightshift Mobile.html    # Mobile shell.    Loads game-data.js + scene.js + mobile-ui.jsx
design-doc.html           # Full design doc (self-contained, long-form)

styles.css                # Desktop HUD styles + global noir tokens (CSS variables)
mobile-styles.css         # Mobile-only styles. Duplicates the :root tokens so the
                          # mobile shell does NOT need styles.css. Intentional.

scene.js                  # Three.js world. Exposes window.NightshiftScene.
                          # FULLY HEADLESS — knows nothing about React or game state.
                          # See §3 for its public API.

game-data.js              # ROSTER, CALL_TEMPLATES, SKILLS, evaluateMatch, travelTime.
                          # Exposes window.NSData. Pure data + pure functions.

ui.jsx                    # Desktop React app + game loop.   ~600 lines.
mobile-ui.jsx             # Mobile React app + game loop.    ~500 lines.
                          # The two UIs duplicate the loop on purpose — see §5.
```

There is **no shared game-loop module** — both UIs implement their own loop over the same data + scene. This is intentional for the prototype: the loop is short and easier to read inline. Refactor target is §6.

---

## 2 · What the prototype already does

✓ **Top-down isometric noir city.** 7×7 block grid, lit windows (warm + cool, some flicker), neon signs (additive blend, pulse), streetlamps with halos, rain (1,400 points, sloped fall), passing cars with headlight cones, drifting clouds, fog, scanline + vignette overlays.
✓ **Six-minute shift** with shift clock animated from 19:00 → 03:00.
✓ **Six operatives** with 5-skill ratings (Stealth, Force, Negotiation, Tech, Medical, 0–3).
✓ **Ten call templates** generating cases with code, type, location, required skills, bonus skills, timer, resolve time, payout, and urgency probability that ramps through the shift.
✓ **Dispatch flow.** Click call → compatible ops glow amber → click op → operative dot leaves HQ, routes L-shape through streets, arrives, resolves, returns home.
✓ **Outcome system.** Skill matching with bonus for ≥2 (expert) ratings; missing requirements → 35%-per-miss "scrape by" chance; failures add city heat.
✓ **Radio log.** Time-stamped chatter with category tags (dispatch / unit / alert / resolve).
✓ **End-of-shift modal** with closed / lost / take.
✓ **Mobile build.** Portrait-first layout, tabbed bottom sheet (CALLS / UNITS / RADIO), map labels floating over the 3D scene, dispatch banner, landscape rotation prompt.

---

## 3 · The `NightshiftScene` API (scene.js)

The scene is intentionally headless and imperative. Game logic owns time, decisions, and state; scene owns visuals only. **Do not put game state in scene.js.**

```js
window.NightshiftScene = {
  // Lifecycle
  init(hostElement),                        // mount canvas, start render loop

  // Coords
  cellToWorld(gx, gz)  → {x, z}            // grid cell → world units
  worldToScreen(x,y,z) → {x, y}            // world → screen px (for HTML overlays)
  gridToScreen(gx, gz, yLift=1.5) → {x,y}  // convenience: cell → screen px
  gridDistance(a, b)   → number             // manhattan distance in cells

  // Call markers (visual only — UI removes them on resolve)
  setCallMarker(id, cell, urgent)           // beacon at cell, red if urgent
  updateCallMarker(id, urgent)              // change color in place
  removeCallMarker(id)                      // tear down
  flashResolve(cell, success)               // expanding ring effect

  // Operatives (dots that route along streets)
  spawnOperative(id, cell, color)           // create dot at HQ (hidden by default)
  moveOperative(id, targetCell, duration)   // L-shape route, duration in seconds
  setOperativeColor(id, color)
  setOperativeVisible(id, bool)
  removeOperative(id)

  // Constants
  GRID_RADIUS,                              // 3 → 7x7 grid
  HQ: { x: 0, z: 0 },
};
```

**Camera.** Orthographic. `viewSize = aspect < 1 ? max(38, 26/aspect) : 38` — auto-fits portrait. Subtle Lissajous breath. Look-at always `(0, 0, 0)` (HQ).

**Performance.** ~1,200 meshes total. 60 FPS verified at 1440×900 and on iPhone-class portrait. Rain count is the easiest knob — change `count = 1400` in `buildRain()`.

---

## 4 · Game data shape (game-data.js)

```js
window.NSData = {
  SKILLS:      ['Stealth', 'Force', 'Negotiation', 'Tech', 'Medical'],
  SKILL_ABBR:  { Stealth: 'STH', Force: 'FRC', ... },

  ROSTER: [{
    id: 'wraith',
    name: 'Maya Okonkwo',
    codename: 'Wraith',
    role: 'Field Operative',
    bio: '…',
    color: 0x4cd9e8,           // hex int (three.js convention)
    skills: { Stealth: 3, Tech: 2, Force: 1, Negotiation: 1, Medical: 0 },
  }, …],   // 6 entries

  CALL_TEMPLATES: [{
    code: '10-22', type: 'Tail Subject',
    locations: ['Drexler & 6th', …],
    req: ['Stealth'],          // ALL must be >= 1 to clear cleanly
    bonus: ['Tech'],           // >=1 grants partial bonus
    timer: 75,                 // seconds before failure if unassigned
    resolveTime: 11,           // seconds on-scene before outcome
    payout: 120,
    detail: '…',
  }, …],   // 10 templates

  generateCall(tickSec) → call,            // call.id, .code, .type, .location, .cell,
                                            //  .req, .bonus, .timer, .timerMax,
                                            //  .resolveTime, .payout, .urgent, .status
  travelTime(fromCell, toCell) → seconds,
  evaluateMatch(op, call) → { ok, bonus, miss },
  HQ_CELL: { x: 0, z: 0 },
};
```

`generateCall` returns calls already wired with grid cells from a curated `CALL_CELLS` pool (so beacons land on streets, not inside buildings). Urgency probability ramps from 18% → 53% across the shift.

---

## 5 · Game loop state (ui.jsx & mobile-ui.jsx)

Both UIs use the same state machine. Pseudocode:

```
state = {
  tick: 0,                                  // shift seconds elapsed
  calls: [],                                // active + recently-resolved
  selectedCallId: null,
  ops: { [opId]: { status, cell, eta, workLeft, callId } },
  score: { closed, failed, cash },
  heat: 0,
  log: [],
}

call.status:  'open' → 'en-route' → 'on-scene' → 'resolved' | 'failed'
op.status:    'available' → 'enroute' → 'onscene' → 'returning' → 'available'
              (also: 'resting', 'down' — NOT YET WIRED)
```

Resolved/failed calls linger 6s for the player to see the outcome, then `_purge`.

**Tuning constants** at top of each UI file:
- `SHIFT_LEN = 360` (seconds)
- `CALL_INTERVAL = [9, 16]` desktop / `[10, 17]` mobile (random seconds between spawns)
- `MAX_HEAT = 100`
- Payout multiplier: `1 + bonus * 0.25`
- Heat hit: `8` normal / `14` priority failure; `-3` per clean clearance

---

## 6 · Known gaps / TODO (priority ordered)

These are intentional cuts from the prototype. None are bugs; all are spec'd in the design doc but not yet implemented.

### Must-haves for Milestone 2

1. **Save / load** (`localStorage`). Schema sketched in design-doc.html §10. Nothing persists today.
2. **Heat consequences.** Heat meter advances and shows visually, but doesn't yet shorten timers (>30) or spawn 10-99 lockouts (>60). Design doc §06 has the curve.
3. **Resting / down states.** `op.status` enum supports them, UI shows them, but no code transitions ops into them. Hook into call resolution: PRIORITY failures → resting 30s, two priority failures in a row → down for the shift.
4. **Chapter selection.** Game starts at Chapter 1 implicitly. Add a chapter picker + per-chapter rule modifiers (timer multipliers, mole rules, blackout events).
5. **Between-shift store.** Hire / equip / train screens. No UI exists.

### Refactors before scaling

6. **Extract the game loop** into `game-loop.js` (or a `useGameLoop` hook). The two UI files currently duplicate ~200 lines of identical state-machine code. A small hook-based extraction would keep both views thin.
7. **Speaker/log abstraction.** The `pushLog` function is fine for now, but Milestone 3's radio chatter audio needs a separate event bus.
8. **Two-op calls.** Templates `10-66` and `10-12` already declare two REQ skills. The UI handles them as a single-op match against both. Spec calls for true co-dispatch in Chapter 4 — extend `assignedOpId` to `assignedOpIds: []`.

### Polish backlog

9. Drag-from-call-to-op affordance (alternative to two-tap, behind a setting).
10. Hover callout on map markers (the styles exist as `.callout`; not wired).
11. WASD camera nudge on desktop.
12. Sounds: rain bed, radio squelch, jazz needle drop at start/mid/end. No audio bundled.
13. Visual: water reflection on streets (PBR-light, optional pass).
14. Accessibility: full keyboard nav, reduced-motion mode, color-blind-safe urgent indicator beyond red.

### Known visual quirks (acceptable for prototype)

- Operatives sometimes appear inside a building briefly during the L-shape route — they walk through walls because routing is naive. Replace with proper A* on the street grid in Milestone 2.
- The Babel inline transform takes 2-3s on first load. Switch to a build step (Vite) before shipping.
- Mobile scene re-layout on orientation change works but doesn't reposition the map labels until the next animation frame (~16ms blip). Cosmetic.

---

## 7 · Conventions worth preserving

**Naming.**
- Operative `id`s are kebab-case lowercase (`wraith`, `hannigan`).
- Call `id`s are `C-{seq}` (e.g. `C-1043`).
- Call codes follow noir-radio convention (`10-22`, `10-91`).

**State updates.** All React state mutations use the functional updater form (`setState(prev => ...)`). The game loop fires `setTick`, `setCalls`, `setOps` independently from a single `requestAnimationFrame` cycle. Don't combine them — React batches.

**Time.** All game time is in seconds (`tick`, `timer`, `resolveTime`, `eta`). No mixing of ms/sec.

**Coordinate spaces:**
- Grid cell: `{x, z}` integers in [-GRID_RADIUS, GRID_RADIUS].
- World: `{x, y, z}` three.js units (1 unit ≈ 1m).
- Screen: `{x, y}` CSS pixels.
Use the scene helpers, don't re-derive these.

**Colors.** Define new colors in `oklch()` or pull from `:root` CSS variables. Don't add a third typeface — Instrument Serif + JetBrains Mono is the system.

---

## 8 · Recommended next session

If I were taking this to Claude Code, I'd ask for **Milestone 2A (Save + Heat + Resting)** in this order:

1. **Refactor:** extract `useGameLoop()` into `game-loop.js`. Pass scene + log callbacks in. Both UI files should drop to ~250 lines.
2. **Implement** `localStorage` save/load with the schema from design-doc §10. Save on every call resolution + every 15s autosave. Add "continue shift" vs. "new shift" on the intro screen.
3. **Wire heat consequences:**
   - Above 30: all `c.timer` ticks at 1.15× (rendered timer bar still uses real elapsed).
   - Above 60: 25% chance per new call to spawn as a "10-99" lockout that consumes a random available op for the case's resolve duration before the player can intervene.
   - Above 85: shift can end early — sunrise modal triggers at 03:00 *or* heat 100.
4. **Resting/down:** on any PRIORITY failure, the assigned op enters `resting` for 30s on return. Two PRIORITY failures across the shift → `down` until end-of-shift.
5. **Chapter picker:** intro screen gets a 3-card chapter select. Chapter 1 (current behavior); Chapter 2 (timers –15%, +1 op slot); Chapter 3 (negotiation-heavy calls only).

Verify by running a full shift, intentionally failing 3 priority cases in a row, and confirming: heat ≥ 85, two ops in resting/down, save survives reload, intro shows "CONTINUE SHIFT."

---

## 9 · Things NOT to do

- **Don't replace the operative dots with 3D models.** The abstraction is the visual language. Game reads at a glance because all ops look identical except color.
- **Don't add PBR materials or shadows.** The flat lambert + emissive-window look is the brand. Shadows wreck the performance budget and the noir mood.
- **Don't move to a heavy bundler before Milestone 2 ships.** The pinned-script approach in the HTML shells is part of why the prototype is so portable.
- **Don't bolt on a full 3D city.** The fixed 7×7 grid with seeded variety is the contract. Adding more districts is a Chapter 4+ scope thing (Pier 9 unlock).
- **Don't introduce a third font, emoji, or icon set.** All UI affordances use type, color, and rule-line composition.

---

## 10 · Open questions for the product owner

1. **Monetization model?** Nothing assumes free/paid/ad-supported. The Chapter 7 leaderboard implies social.
2. **Audio licensing.** Design doc calls for "sparse jazz needle drops" — commission vs. royalty-free.
3. **Accessibility floor.** What WCAG level are we targeting?
4. **Localization.** Call codes and noir flavor text would need a translator with genre sense.
5. **Platform.** Web-only forever, or wrap-for-Steam/mobile-app later?

---

## Appendix A · One-page asset index

| Artifact                | Path                       | Purpose                                     |
|-------------------------|----------------------------|---------------------------------------------|
| Landing card            | `index.html`               | Project hub, links to all three artifacts   |
| Desktop prototype       | `Nightshift.html`          | Full HUD, three-column layout               |
| Mobile prototype        | `Nightshift Mobile.html`   | Portrait, tabbed bottom sheet               |
| Design doc              | `design-doc.html`          | Vision, mechanics, art, audio, progression  |
| Turnover (this doc)     | `turnover.html` / `.md`    | Operational handoff                         |
| Scene engine            | `scene.js`                 | three.js world + imperative API             |
| Game data               | `game-data.js`             | Roster, templates, pure functions           |
| Desktop UI              | `ui.jsx`                   | React HUD + desktop loop                    |
| Mobile UI               | `mobile-ui.jsx`            | React HUD + mobile loop                     |
| Desktop styles          | `styles.css`               | Tokens + desktop chrome                     |
| Mobile styles           | `mobile-styles.css`        | Tokens + mobile chrome                      |

---

*End of turnover. Ping the dispatcher if anything's unclear.*
