Phone runtime (2b): inventory phone on the board dials for real
The phone gains a session mode that reads the live directory (/phone) and dials (/dial); the inventory threads it through. On a campaign level the board shows an INVENTORY nav button that opens the tool rack, and connecting a call hands back to the campaign via /?resume=1 (a fast-path that resumes the current node instead of the splash). On Barricelli's note-board: open inventory -> phone -> dial 5550100 -> Glitch Hunter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+37
-16
@@ -264,16 +264,22 @@ function PhoneScreen({ visible, mode, dialed, callee }: { visible: boolean; mode
|
||||
</div>
|
||||
}
|
||||
|
||||
export function PhonePreview() {
|
||||
// In-game the phone runs against a live playthrough: it dials the connected phone
|
||||
// node's directory and, on connect, hands back to the campaign runtime.
|
||||
export type PhoneSession = { playthroughId: string; onConnect: () => void }
|
||||
|
||||
export function PhonePreview({ session }: { session?: PhoneSession } = {}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [screenOn, setScreenOn] = useState(false)
|
||||
const [mode, setMode] = useState<Mode>('home')
|
||||
const [dialed, setDialed] = useState('')
|
||||
const [callee, setCallee] = useState('')
|
||||
const [playthroughId, setPlaythroughId] = useState<string | null>(null)
|
||||
const [playthroughId, setPlaythroughId] = useState<string | null>(session?.playthroughId ?? null)
|
||||
const [achieved, setAchieved] = useState<Set<string>>(new Set())
|
||||
const [directory, setDirectory] = useState<{ number: string; name: string }[]>([])
|
||||
const [called, setCalled] = useState<Set<string>>(new Set())
|
||||
const callTimer = useRef<number | undefined>(undefined)
|
||||
const digits = (value: string) => value.replace(/\D/g, '')
|
||||
|
||||
useEffect(() => {
|
||||
if (open) { const t = setTimeout(() => setScreenOn(true), 560); return () => clearTimeout(t) }
|
||||
@@ -284,10 +290,15 @@ export function PhonePreview() {
|
||||
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.
|
||||
// Game session -> load the connected directory. Spike -> a demo playthrough + case-state.
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
if (session) {
|
||||
const res = await fetch(`/api/playthroughs/${session.playthroughId}/phone`)
|
||||
if (!cancelled && res.ok) setDirectory((await res.json()).numbers || [])
|
||||
return
|
||||
}
|
||||
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
|
||||
@@ -300,11 +311,11 @@ export function PhonePreview() {
|
||||
await refreshAchievements(id)
|
||||
})()
|
||||
return () => { cancelled = true }
|
||||
}, [])
|
||||
}, [session])
|
||||
|
||||
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 connectable = (num: string) => session ? directory.some(d => digits(d.number) === num) : (DIRECTORY[num] ? DIRECTORY[num].requires.every(f => achieved.has(f)) : false)
|
||||
// Glow the handset when a callable, not-yet-called number is waiting.
|
||||
const glow = session ? directory.some(d => !called.has(digits(d.number))) : Object.keys(DIRECTORY).some(num => connectable(num) && !called.has(num))
|
||||
|
||||
const grantElias = async () => {
|
||||
if (!playthroughId) return
|
||||
@@ -315,12 +326,20 @@ export function PhonePreview() {
|
||||
const resolveCall = (num: string) => {
|
||||
sfx.ring()
|
||||
setMode('calling')
|
||||
const contact = DIRECTORY[num]
|
||||
window.clearTimeout(callTimer.current)
|
||||
callTimer.current = window.setTimeout(() => {
|
||||
callTimer.current = window.setTimeout(async () => {
|
||||
setCalled(prev => new Set(prev).add(num))
|
||||
if (session) {
|
||||
const res = await fetch(`/api/playthroughs/${session.playthroughId}/dial`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ number: num }) })
|
||||
const result = res.ok ? await res.json() : { outcome: 'unknown' }
|
||||
if (result.outcome === 'connect') { setCallee(result.name || ''); setMode('connected'); window.setTimeout(() => session.onConnect(), 950) }
|
||||
else if (result.outcome === 'voicemail') { setCallee(result.name || ''); sfx.voicemail(); setMode('voicemail') }
|
||||
else { sfx.unobtainable(); setMode('unknown') }
|
||||
return
|
||||
}
|
||||
const contact = DIRECTORY[num]
|
||||
if (!contact) { sfx.unobtainable(); setMode('unknown'); return }
|
||||
setCallee(contact.name)
|
||||
setCalled(prev => new Set(prev).add(num))
|
||||
if (connectable(num)) setMode('connected')
|
||||
else { sfx.voicemail(); setMode('voicemail') }
|
||||
}, 950)
|
||||
@@ -355,11 +374,13 @@ export function PhonePreview() {
|
||||
<PhoneScreen visible={screenOn} mode={mode} dialed={dialed} callee={callee} />
|
||||
</div>
|
||||
<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>
|
||||
{session
|
||||
? <p className="phone-hint">Key in a number, then press Call.</p>
|
||||
: <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