Spaces:
Running
Running
Per-case visuals, incident vignettes, guided play, no-repeat dealing, four new crime kinds
aabd7c7 verified | // Detective's checklist: the "what do I do next" rail. Four objectives derived | |
| // entirely from game state; the ACCUSE step pulses when the player is ready. | |
| import { useState } from 'preact/hooks' | |
| import { HOT_SUSPICION, useGame } from '../store' | |
| import { Btn } from './components' | |
| function useObjectives() { | |
| const g = useGame() | |
| const c = g.case | |
| const questioned = c.suspects.filter((s) => (g.state.interrogations[s.id] || []).length > 1).length | |
| const presented = Object.values(g.state.usedEv).some((l) => l.length > 0) | |
| const crackedAny = Object.values(g.state.cracked).some(Boolean) | |
| const hot = Object.values(g.state.suspicion).some((v) => v >= HOT_SUSPICION) | |
| const ready = (questioned >= c.suspects.length && presented && crackedAny) || hot | |
| return { g, total: c.suspects.length, questioned, presented, crackedAny, ready } | |
| } | |
| function Step({ done, label }: { done: boolean; label: string }) { | |
| return ( | |
| <div class={'checklist__step' + (done ? ' checklist__step--done' : '')}> | |
| <span class="checklist__box">{done ? '■' : '□'}</span> | |
| <span>{label}</span> | |
| </div> | |
| ) | |
| } | |
| export function Checklist({ slim = false }: { slim?: boolean }) { | |
| const { g, total, questioned, presented, crackedAny, ready } = useObjectives() | |
| const doneCount = (questioned >= total ? 1 : 0) + (presented ? 1 : 0) + (crackedAny ? 1 : 0) | |
| const [open, setOpen] = useState(questioned === 0) // collapse once the player gets going | |
| const steps = ( | |
| <> | |
| <Step done={questioned >= total} label={`QUESTION EVERY SUSPECT ${questioned}/${total}`} /> | |
| <Step done={presented} label="PRESENT EVIDENCE" /> | |
| <Step done={crackedAny} label="CRACK A LIE" /> | |
| <Btn | |
| sm | |
| variant={ready ? 'ox' : 'ghost'} | |
| className={ready ? 'accuse-cta--pulse' : undefined} | |
| style={{ marginTop: 6 }} | |
| onClick={() => g.nav('accuse')} | |
| > | |
| {ready ? '▸ MAKE THE ACCUSATION' : 'Accuse ▸'} | |
| </Btn> | |
| </> | |
| ) | |
| if (!slim) return <div class="checklist">{steps}</div> | |
| return ( | |
| <div class="checklist checklist--slim"> | |
| <button class="checklist__toggle" onClick={() => setOpen(!open)}> | |
| <span class="t-label" style={{ color: 'var(--amber-2)' }}>OBJECTIVES {doneCount}/3</span> | |
| <span class="t-mono dim">{open ? '▾' : '▸'}</span> | |
| </button> | |
| {open && <div class="col" style={{ gap: 4, paddingTop: 6 }}>{steps}</div>} | |
| {!open && ready && ( | |
| <Btn sm variant="ox" className="accuse-cta--pulse" onClick={() => g.nav('accuse')}>▸ ACCUSE</Btn> | |
| )} | |
| </div> | |
| ) | |
| } | |