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.
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.
| Path | Purpose | Lines |
| index.html | Landing card · links to all artifacts | ~180 |
| Nightshift.html | Desktop shell (loads game-data + scene + ui.jsx) | ~30 |
| Nightshift Mobile.html | Mobile shell (loads game-data + scene + mobile-ui.jsx) | ~30 |
| design-doc.html | Full vision doc · self-contained long-form | ~900 |
| turnover.html / TURNOVER.md | This doc · operational handoff | ~800 |
| scene.js | three.js world · exposes window.NightshiftScene · fully headless | ~720 |
| game-data.js | ROSTER · CALL_TEMPLATES · evaluateMatch · travelTime · pure data + fns | ~210 |
| ui.jsx | Desktop React app + game loop | ~600 |
| mobile-ui.jsx | Mobile React app + game loop | ~500 |
| styles.css | Desktop HUD chrome + :root tokens | ~640 |
| mobile-styles.css | Mobile 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
- Top-down isometric noir city — 7×7 block grid, lit windows (warm + cool, some flicker), neon signs (additive pulse), streetlamps with halos, 1,400-point rain with slant, passing cars with headlight cones, drifting clouds, fog, scanline + vignette overlays.
- Six-minute shift with clock animated from 19:00 → 03:00.
- Six operatives with 5-skill ratings (STH / FRC / NEG / TCH / MED, 0–3).
- Ten call templates generating cases with code, type, location, REQ skills, BONUS skills, timer, resolve time, payout, urgency probability that ramps through the shift.
- Dispatch flow — click call → compatible ops glow amber → click op → operative routes L-shape through streets → arrives → resolves → returns home.
- Outcome system — skill match with bonus for ≥2 (expert) ratings; missing requirements → 35%-per-miss "scrape by" chance; failures add city heat.
- Radio log with time-stamped chatter and category tags (dispatch / unit / alert / resolve).
- End-of-shift modal with closed / lost / take.
- Mobile build — portrait-first, tabbed bottom sheet (CALLS / UNITS / RADIO), map labels floating over the 3D scene, dispatch banner, landscape rotation prompt.
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)
SHIFT_LEN = 360 (seconds)
CALL_INTERVAL = [9, 16] desktop / [10, 17] mobile
MAX_HEAT = 100
- Payout multiplier:
1 + bonus * 0.25
- Heat hit:
8 normal, 14 priority failure; -3 per clean clearance
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
- Save / load (localStorage)
Schema sketched in
design-doc.html §10. Nothing persists today. Add autosave on call resolution + every 15s.
- 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.
- 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.
- Chapter selection
Game starts at Chapter 1 implicitly. Add a 3-card chapter picker + per-chapter rule modifiers.
- Between-shift store
Hire / equip / train screens. No UI exists.
Refactors before scaling
- Extract the game loop into
game-loop.js (or a useGameLoop hook). The two UI files duplicate ~200 lines of identical state-machine code.
- Speaker/log abstraction.
pushLog is fine for now, but Milestone 3's radio chatter audio needs a separate event bus.
- 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
- Drag-from-call-to-op affordance (alternative to two-tap, behind a setting).
- Hover callout on map markers — styles exist as
.callout, not wired.
- WASD camera nudge on desktop.
- Sounds — rain bed, radio squelch, jazz needle drop at start/mid/end. No audio bundled.
- Visual — water reflection on streets (PBR-light, optional pass).
- Accessibility — full keyboard nav, reduced-motion mode, color-blind-safe urgent indicator beyond red.
Known visual quirks Acceptable for prototype
- Operatives walk through buildings briefly during the L-shape route — routing is naive. Replace with A* on the street grid in M2.
- Babel inline transform takes 2–3s on first load. Switch to Vite before shipping.
- Mobile scene re-layout on orientation change works but doesn't reposition map labels until the next frame (~16ms blip).
07 Conventions worth preserving
Naming
- Operative
ids are kebab-case lowercase: wraith, hannigan.
- Call
ids 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 — React batches them.
Time
All game time is in seconds. No mixing of ms / sec.
Coordinates
- Grid cell:
{x, z} integers in [-GRID_RADIUS, GRID_RADIUS].
- World:
{x, y, z} three.js units (1 unit ≈ 1 m).
- 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.
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:
- Refactor
Extract
useGameLoop() into game-loop.js. Pass scene + log callbacks in. Both UI files should drop to ~250 lines.
- Implement localStorage save/load
Schema from design-doc §10. Save on call resolution + every 15s autosave. Intro screen gets "continue shift" vs. "new shift."
- 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.
- Resting / down
On PRIORITY failure, the assigned op enters
resting for 30s on return. Two priority failures → down until end-of-shift.
- 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
- Monetization model? Nothing in the build assumes free / paid / ad-supported. The Chapter 7 leaderboard implies social.
- Audio licensing. Design doc calls for sparse jazz needle drops — commission vs. royalty-free?
- Accessibility floor. What WCAG level are we targeting?
- Localization. Call codes and noir flavor text would need a translator with genre sense.
- Platform. Web-only forever, or wrap-for-Steam / mobile-app later?