// Det. Hale — your partner on the wire. Contextual, spoiler-safe hints from the server, // plus one-time local nudges that introduce each screen to a new player. import { useEffect, useState } from 'preact/hooks' import { getHint } from '../api' import { useTypewriter } from '../engine/pixel' import { type Screen, useGame } from '../store' import { hasSeen, markSeen } from '../tips' import { Portrait } from './components' const HIDDEN = new Set(['title', 'verdict', 'share', 'boot']) const FALLBACK = 'Work the evidence, detective. Find where a statement and a fact disagree, and press there.' // First-visit nudges (client-local, no server call). Shown once per browser. const TIPS: Partial> = { board: 'Pick a suspect and lean on them. Work the checklist on the rail — question everyone, show them the exhibits.', interro: 'Ask your questions, then hit them with evidence from the tray. Watch that bar climb.', accuse: 'Name them, give me the why, and attach what made them crack.', } export function Assistant() { const g = useGame() const [open, setOpen] = useState(false) const [hint, setHint] = useState('') const [localTip, setLocalTip] = useState(null) const screen = g.state.screen useEffect(() => { const t = () => { setLocalTip(null) // HINT button always fetches the live server hint setOpen((o) => !o) } const c = () => setOpen(false) window.addEventListener('toggle-hint', t) window.addEventListener('close-hint', c) return () => { window.removeEventListener('toggle-hint', t) window.removeEventListener('close-hint', c) } }, []) // Screen change: close whatever was open, then fire the one-time nudge for this // screen kind. On the board it waits for the case-brief overlay to be dismissed // so the two never stack. useEffect(() => { setOpen(false) setLocalTip(null) const tip = TIPS[screen] if (!tip || hasSeen(`tip:${screen}`)) return let timer: ReturnType | null = null const show = () => { markSeen(`tip:${screen}`) setLocalTip(tip) setOpen(true) } const briefPending = screen === 'board' && !hasSeen(`brief:${g.case.id}`) const onBrief = () => { timer = setTimeout(show, 400) } if (briefPending) window.addEventListener('cz-brief-done', onBrief, { once: true }) else timer = setTimeout(show, 700) return () => { if (timer) clearTimeout(timer) window.removeEventListener('cz-brief-done', onBrief) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [screen]) useEffect(() => { if (!open || localTip) return let alive = true setHint('') getHint(g.runId, screen) .then((r) => { if (alive) setHint(r.hint || FALLBACK) }) .catch(() => { if (alive) setHint(FALLBACK) }) return () => { alive = false } }, [open, localTip, screen, g.runId]) const [typed, done] = useTypewriter(open ? localTip ?? hint : '', g.state.tweaks.typeSpeed || 18, open) if (HIDDEN.has(screen) || !open) return null return (
DET. HALE YOUR PARTNER · ON THE WIRE

“{typed} {!done && }”

) }