Operational Handoff · For Claude Code · v0.1

The Turnover

Everything that's built, everything that isn't, and exactly where to pick up. Pair with the design doc — that's the vision; this is the keys.

Project
Dispatch · Nightshift
Build state
Playable M1
Next milestone
M2 · Save · Heat · Resting
Engine
three.js · React · CSS
One-line pitch

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.

TURNOVER.md · paste-ready

Same content, plain Markdown. Drop into Claude Code's context window directly.

DOWNLOAD ▸

01 How to run it

Static HTML. No build step. All scripts come from unpkg with pinned versions + SRI hashes.

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

Serve over http:// (not file://) so the Babel inline transform works. Any static file server is fine — python -m http.server, Vite preview, whatever.

02 File map

Single flat directory. No bundler. No package.json yet. Eleven files, ~2,500 lines total.

PathPurposeLines
index.htmlLanding card · links to all artifacts~180
Nightshift.htmlDesktop shell (loads game-data + scene + ui.jsx)~30
Nightshift Mobile.htmlMobile shell (loads game-data + scene + mobile-ui.jsx)~30
design-doc.htmlFull vision doc · self-contained long-form~900
turnover.html / TURNOVER.mdThis doc · operational handoff~800
scene.jsthree.js world · exposes window.NightshiftScene · fully headless~720
game-data.jsROSTER · CALL_TEMPLATES · evaluateMatch · travelTime · pure data + fns~210
ui.jsxDesktop React app + game loop~600
mobile-ui.jsxMobile React app + game loop~500
styles.cssDesktop HUD chrome + :root tokens~640
mobile-styles.cssMobile chrome + :root tokens (duplicated intentionally)~580

No shared game-loop module yet. Both UIs implement their own loop over the same data + scene. That's intentional for the prototype — see §06 for the refactor target.

03 What the prototype already does Playable

04 The NightshiftScene API

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

Lifecycle
init(hostElement)Mount canvas, start render loop.
Coordinates
cellToWorld(gx, gz)grid cell → world units {x, z}
worldToScreen(x, y, z)world coords → screen px (for HTML overlays)
gridToScreen(gx, gz, yLift=1.5)cell → screen px, convenience
gridDistance(a, b)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, dur)L-shape route, dur in seconds
setOperativeColor(id, color)change dot + halo
setOperativeVisible(id, bool)show/hide
removeOperative(id)tear down
Constants
GRID_RADIUS · HQ3 → 7×7 grid · HQ at {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().

05 Game state shape

Both UIs use the same state machine. The whole thing lives in useState hooks at the top of the App component.

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: [],
}

// Status state machines:
call.status:  'open''en-route''on-scene''resolved' | 'failed'
op.status:    'available''enroute''onscene''returning''available'
              // also: 'resting', 'down' — supported in UI, NOT YET WIRED

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

Tuning constants (top of each UI file)

06 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. Add autosave on call resolution + every 15s.
  2. Heat consequences Heat meter advances + shows visually, but doesn't yet shorten timers (>30), spawn 10-99 lockouts (>60), or trigger early-sunrise (>85). Curve is in design doc §06.
  3. Resting / down states Op status enum supports them, UI shows them, but no code transitions ops into them. Hook into resolution: PRIORITY failure → resting 30s; two priority failures → down for the shift.
  4. Chapter selection Game starts at Chapter 1 implicitly. Add a 3-card chapter picker + per-chapter rule modifiers.
  5. Between-shift store Hire / equip / train screens. No UI exists.

Refactors before scaling

  1. Extract the game loop into game-loop.js (or a useGameLoop hook). The two UI files duplicate ~200 lines of identical state-machine code.
  2. Speaker/log abstraction. pushLog is fine for now, but Milestone 3's radio chatter audio needs a separate event bus.
  3. 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

Known visual quirks Acceptable for prototype

07 Conventions worth preserving

Naming

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 — React batches them.

Time

All game time is in seconds. No mixing of ms / sec.

Coordinates

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.

08 Recommended next session M2 · Save · Heat · Resting

If I were taking this to Claude Code, I'd ask for Milestone 2A 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 Schema from design-doc §10. Save on call resolution + every 15s autosave. Intro screen gets "continue shift" vs. "new shift."
  3. Wire heat consequences Above 30: c.timer ticks at 1.15× speed (timer bar still uses real elapsed). Above 60: 25% chance per new call to spawn as a "10-99" lockout consuming a random available op. Above 85: shift can end early — sunrise modal triggers at 03:00 or heat 100.
  4. Resting / down On PRIORITY failure, the assigned op enters resting for 30s on return. Two priority failures → down until end-of-shift.
  5. Chapter picker Intro screen gets a 3-card chapter select. Ch.1 (current behavior); Ch.2 (timers –15%, +1 op slot); Ch.3 (negotiation-heavy calls only).

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

09 Do / don't

Do
  • Keep the scene headless. Pure visual API.
  • Use functional state updaters.
  • Stay on pinned unpkg scripts until M2 ships.
  • Build new features as Tweaks in game-data.js templates when possible.
  • Use the oklch() color space for any new accent colors.
Don't
  • Replace operative dots with 3D models. The abstraction is the visual language.
  • Add PBR materials or shadows. Wrecks performance + noir mood.
  • Move to a heavy bundler before M2 ships.
  • Bolt on a full 3D city. The 7×7 grid is the contract.
  • Introduce a third font, emoji, or icon set.

10 Open questions for the product owner

  1. Monetization model? Nothing in the build 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?