Adding migrations and lot of work. Starting work on the demo scope
This commit is contained in:
+51
-11
@@ -228,16 +228,16 @@ function PhoneDevice({ open, onKey }: { open: boolean; onKey: (k: string) => voi
|
||||
return <div ref={stageRef} className="phone-canvas" />
|
||||
}
|
||||
|
||||
// ---- directory (spike stub) ---------------------------------------------------
|
||||
// Real version: numbers are the phone node's terminals -> dialogue nodes; `requires`
|
||||
// are the target node's flag requirements, checked against the playthrough flags.
|
||||
// ---- directory ----------------------------------------------------------------
|
||||
// The number->node directory stays authored config for now; `requires` are the
|
||||
// target node's flag requirements, checked live against the player's ACHIEVEMENTS
|
||||
// (the real playthrough case-state). Wiring numbers to actual dialogue nodes and
|
||||
// moving the directory server-side is the next slice.
|
||||
type Contact = { name: string; requires: string[] }
|
||||
const DIRECTORY: Record<string, Contact> = {
|
||||
'55501': { name: 'Elias Board', requires: [] }, // always enabled -> connects
|
||||
'55502': { name: 'Voss Antiquities', requires: ['found_voss_number'] }, // enabled below -> connects
|
||||
'55503': { name: 'Preservation Soc.', requires: ['society_clearance'] }, // not enabled -> voicemail
|
||||
'55501': { name: 'Elias Board', requires: ['elias_number_callable'] }, // dev-grant unlocks -> connects
|
||||
'55502': { name: 'Voss Antiquities', requires: ['voss_number_known'] }, // not earned -> voicemail
|
||||
}
|
||||
const FLAGS = new Set<string>(['found_voss_number']) // toggle to demo connect vs. voicemail
|
||||
|
||||
type Mode = 'home' | 'dial' | 'calling' | 'unknown' | 'voicemail' | 'connected'
|
||||
|
||||
@@ -270,6 +270,9 @@ export function PhonePreview() {
|
||||
const [mode, setMode] = useState<Mode>('home')
|
||||
const [dialed, setDialed] = useState('')
|
||||
const [callee, setCallee] = useState('')
|
||||
const [playthroughId, setPlaythroughId] = useState<string | null>(null)
|
||||
const [achieved, setAchieved] = useState<Set<string>>(new Set())
|
||||
const [called, setCalled] = useState<Set<string>>(new Set())
|
||||
const callTimer = useRef<number | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -277,6 +280,38 @@ export function PhonePreview() {
|
||||
setScreenOn(false); setMode('home'); setDialed('')
|
||||
}, [open])
|
||||
|
||||
const refreshAchievements = async (id: string) => {
|
||||
const res = await fetch(`/api/playthroughs/${id}/achievements`)
|
||||
if (res.ok) setAchieved(new Set(await res.json() as string[]))
|
||||
}
|
||||
// Attach to the player's live playthrough (or create one) and load its case-state.
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
let id: string | null = null
|
||||
const cur = await fetch('/api/playthroughs/current')
|
||||
if (cur.ok && cur.status !== 204) id = (await cur.json())?.playthrough?.id ?? null
|
||||
if (!id) {
|
||||
const made = await fetch('/api/playthroughs', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ mystery: 'glass-harbor' }) })
|
||||
if (made.ok) id = (await made.json())?.playthrough?.id ?? null
|
||||
}
|
||||
if (cancelled || !id) return
|
||||
setPlaythroughId(id)
|
||||
await refreshAchievements(id)
|
||||
})()
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
|
||||
const connectable = (num: string) => { const c = DIRECTORY[num]; return c ? c.requires.every(f => achieved.has(f)) : false }
|
||||
// Glow the handset when an enabled, not-yet-called number is waiting.
|
||||
const glow = Object.keys(DIRECTORY).some(num => connectable(num) && !called.has(num))
|
||||
|
||||
const grantElias = async () => {
|
||||
if (!playthroughId) return
|
||||
await fetch(`/api/playthroughs/${playthroughId}/achievements`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ flagKey: 'elias_number_callable' }) })
|
||||
await refreshAchievements(playthroughId)
|
||||
}
|
||||
|
||||
const resolveCall = (num: string) => {
|
||||
sfx.ring()
|
||||
setMode('calling')
|
||||
@@ -285,8 +320,8 @@ export function PhonePreview() {
|
||||
callTimer.current = window.setTimeout(() => {
|
||||
if (!contact) { sfx.unobtainable(); setMode('unknown'); return }
|
||||
setCallee(contact.name)
|
||||
const enabled = contact.requires.every(f => FLAGS.has(f))
|
||||
if (enabled) setMode('connected')
|
||||
setCalled(prev => new Set(prev).add(num))
|
||||
if (connectable(num)) setMode('connected')
|
||||
else { sfx.voicemail(); setMode('voicemail') }
|
||||
}, 950)
|
||||
}
|
||||
@@ -319,7 +354,12 @@ export function PhonePreview() {
|
||||
<PhoneDevice open={open} onKey={press} />
|
||||
<PhoneScreen visible={screenOn} mode={mode} dialed={dialed} callee={callee} />
|
||||
</div>
|
||||
<button className="phone-open-btn" onClick={() => setOpen(o => !o)}>{open ? 'CLOSE' : 'OPEN'}</button>
|
||||
<p className="phone-hint">spike — dial <code>55501</code> connects · <code>55503</code> voicemail · anything else unobtainable</p>
|
||||
<button className={`phone-open-btn${glow && !open ? ' glow' : ''}`} onClick={() => setOpen(o => !o)}>{open ? 'CLOSE' : 'OPEN'}</button>
|
||||
<div className="phone-dev">
|
||||
<button className="phone-dev-btn" disabled={!playthroughId || achieved.has('elias_number_callable')} onClick={grantElias}>
|
||||
{achieved.has('elias_number_callable') ? '✓ elias_number_callable' : '▸ grant elias_number_callable'}
|
||||
</button>
|
||||
<p className="phone-hint">dial <code>55501</code> Elias (voicemail → connect once granted) · <code>55502</code> Voss (voicemail) · else unobtainable</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user