// Title · Story · Briefing (dossier) · Boot — the "cold" pre-investigation screens. import { useEffect, useRef, useState } from 'preact/hooks' import type { ComponentChildren } from 'preact' import { useTypewriter } from '../engine/pixel' import { useGame } from '../store' import type { PublicCase } from '../types' import { playSfx } from '../ui/audio' import { BottomNav, Btn, Controls, ExhibitArt, HintButton, Hud, Panel, Portrait, Scene, Stamp } from '../ui/components' export function TitleScreen() { const g = useGame() const c = g.case const [enterId, setEnterId] = useState(false) const [idVal, setIdVal] = useState(c.id) const pull = () => { const id = idVal.trim() if (id) g.loadCase(id) // load that exact case (fresh run) and jump straight into it } return (
A PROCEDURAL DETECTIVE MYSTERY

CASE
ZERO

Every case is generated. The city, the body, the lies. Solve one no one has ever seen.

“{c.tagline}”
{!enterId ? (
g.newCase()}>▸ Begin New Case setEnterId(true)}>Enter Case ID g.nav('board')}>Continue · {c.id}
) : ( ENTER A CASE FILE NUMBER setIdVal((e.target as HTMLInputElement).value)} style={{ textAlign: 'center', fontSize: 'calc(17px*var(--mono-scale))' }} />
setEnterId(false)}>Back Pull File ▸
)}
) } export function StoryScreen() { const g = useGame() const beats = g.case.storyBeats const [i, setI] = useState(0) const beat = beats[i] const last = i === beats.length - 1 const [body, done] = useTypewriter(beat.text, (g.state.tweaks.typeSpeed || 18) + 2, true) return (
{beat.kicker}
g.nav('board')}>Skip ▸

{beat.title}

{body} {!done && }

{beats.map((_, k) => ( setI(k)} style={{ width: 26, height: 6, cursor: 'pointer', background: k === i ? 'var(--amber-2)' : k < i ? 'var(--slate-2)' : 'var(--ink-3)', boxShadow: 'inset 0 0 0 1px var(--ink-0)' }} /> ))}
{i > 0 && setI(i - 1)}>◂ Back} {!last ? ( setI(i + 1)}>Next ▸ ) : ( g.nav('board')}>Start Investigating ▸▸ )}
) } export function BootScreen() { const g = useGame() const boot = g.case.bootLines const [lines, setLines] = useState([]) const [cur, setCur] = useState('') const [stamped, setStamped] = useState(false) const [done, setDone] = useState(false) const idx = useRef(0) const ch = useRef(0) useEffect(() => { const speed = g.state.tweaks.typeSpeed || 16 let timer: ReturnType const tick = () => { const i = idx.current if (i >= boot.length) { setStamped(true) setTimeout(() => setDone(true), 900) return } const line = boot[i] ch.current++ setCur(line.slice(0, ch.current)) if (ch.current >= line.length) { setLines((l) => [...l, line]) setCur('') ch.current = 0 idx.current++ timer = setTimeout(tick, 240) } else { timer = setTimeout(tick, speed) } } timer = setTimeout(tick, 300) return () => clearTimeout(timer) }, []) return (
{g.case.city} · {g.case.district}
{lines.map((l, i) => (
{l}
))} {cur &&
{cur}
} {lines.length === 0 && !cur && }
CASE FILE No. {stamped ? ( {g.case.id} ) : ( — — — — — )}
{done && g.nav('briefing')}>Take the Case ▸}
) } // ---- dossier ---- function PageHead({ tab, idx, c }: { tab: string; idx: string; c: PublicCase }) { return (
{c.city} CO. · {c.division || 'HOMICIDE DIVISION'} {tab}
FILE {c.id} · pg.{idx}
) } function dossierPages(c: PublicCase): { tab: string; render: () => ComponentChildren }[] { return [ { tab: 'COVER', render: () => (
{c.district}
{c.kindLabel || 'HOMICIDE'} — CASE FILE

{c.title}

{c.id}
{c.district}
CONFIDENTIAL
▸ flip the page →
), }, { tab: 'THE VICTIM', render: () => (
{c.victim.name.split(' ').slice(-1)[0]}

{c.victim.name}

{c.victim.role}
{c.victimStatus || 'DECEASED'} {c.todLabel || 'T.O.D.'} {c.tod} · AGE {c.victim.age}

{c.victim.bio}

), }, { tab: 'THE SCENE', render: () => (
EXHIBIT A — {c.scene}

FOUND — {c.found}

CAUSE — {c.cause}

), }, { tab: 'THE EXHIBITS', render: () => (

Photographed and logged at the scene. Examine each on the wall.

{c.evidence.slice(0, 6).map((e, i) => (
{e.name} {e.time}
))}
), }, { tab: 'KEY FACTS', render: () => (
{c.kindLabel || 'HOMICIDE'}
{c.facts.map(([k, v], i) => (
{k} {v}
))}
), }, { tab: 'PERSONS OF INTEREST', render: () => (

Those who stayed when the others fled the sirens. Each had reason to be near.

{c.suspects.map((s) => (
{s.name} {s.tag}
))}

One of them is lying. Find the crack.

), }, ] } export function BriefingScreen() { const g = useGame() const c = g.case const PAGES = dossierPages(c) const [page, setPage] = useState(0) const [flip, setFlip] = useState(null) const go = (dir: number) => { const next = page + dir if (next < 0 || next >= PAGES.length) return playSfx('page') setFlip(dir > 0 ? 'fwd' : 'back') setPage(next) setTimeout(() => setFlip(null), 470) } const P = PAGES[page] return (
{P.render()}
{page > 0 &&
go(-1)} title="Previous page" />} {page < PAGES.length - 1 &&
go(1)} title="Next page" />}
go(-1)}>◂ Prev
{PAGES.map((pg, i) => ( { setFlip(i > page ? 'fwd' : 'back'); setPage(i); setTimeout(() => setFlip(null), 470) }} /> ))}
{page < PAGES.length - 1 ? ( go(1)}>Next page ▸ ) : ( g.nav('board')}>Open the Wall ▸▸ )}
) }