Splash case picker + playable-mystery list
GET /api/mysteries returns only mysteries with an entrypoint (empties are filtered out), and the splash now lists them as case files with a per-case BEGIN / RESUME action instead of a single hardcoded New Game. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+32
-10
@@ -1,5 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { CutsceneHost, DialoguePlayer, SplashScreen, type PlaythroughState } from './narrative'
|
||||
import { CutsceneHost, DialoguePlayer, type PlaythroughState } from './narrative'
|
||||
|
||||
type MysterySummary = { slug: string; title: string }
|
||||
|
||||
// The game's front door and campaign runtime. Shows the splash when there's no
|
||||
// active playthrough; otherwise walks the story graph — cutscene and dialogue
|
||||
@@ -8,6 +10,7 @@ import { CutsceneHost, DialoguePlayer, SplashScreen, type PlaythroughState } fro
|
||||
export function Play() {
|
||||
const [state, setState] = useState<PlaythroughState | null>(null)
|
||||
const [resumable, setResumable] = useState<PlaythroughState | null>(null)
|
||||
const [mysteries, setMysteries] = useState<MysterySummary[]>([])
|
||||
const [splash, setSplash] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [status, setStatus] = useState('')
|
||||
@@ -38,23 +41,24 @@ export function Play() {
|
||||
if (!cancelled) res.ok ? apply(await res.json()) : setStatus('NODE NOT FOUND')
|
||||
return
|
||||
}
|
||||
// The bare root is always the front door: show the splash, and offer Resume
|
||||
// when a playthrough is already in progress rather than auto-resuming into it.
|
||||
const res = await fetch('/api/playthroughs/current')
|
||||
// The bare root is always the front door: list the playable cases, and offer
|
||||
// Resume on the one already in progress rather than auto-resuming into it.
|
||||
const [cases, current] = await Promise.all([fetch('/api/mysteries'), fetch('/api/playthroughs/current')])
|
||||
if (cancelled) return
|
||||
if (res.ok && res.status !== 204) setResumable(await res.json())
|
||||
if (cases.ok) setMysteries(await cases.json())
|
||||
if (current.ok && current.status !== 204) setResumable(await current.json())
|
||||
setSplash(true)
|
||||
setStatus('AWAITING PRINCIPAL INVESTIGATOR')
|
||||
setStatus('SELECT A CASE FILE')
|
||||
})()
|
||||
return () => { cancelled = true }
|
||||
}, [apply])
|
||||
|
||||
const newGame = async () => {
|
||||
const newGame = async (slug: string) => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const res = await fetch('/api/playthroughs', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ mystery: 'glass-harbor' }) })
|
||||
const res = await fetch('/api/playthroughs', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ mystery: slug }) })
|
||||
if (res.ok) apply(await res.json())
|
||||
else setStatus('NO MYSTERY AVAILABLE')
|
||||
else setStatus('COULD NOT OPEN CASE')
|
||||
} finally { setBusy(false) }
|
||||
}
|
||||
|
||||
@@ -66,7 +70,25 @@ export function Play() {
|
||||
if (res.ok) apply(await res.json())
|
||||
}
|
||||
|
||||
if (splash) return <SplashScreen hasResume={!!resumable} busy={busy} status={status} onNewGame={newGame} onResume={() => { if (resumable) apply(resumable) }} />
|
||||
if (splash) return <div className="splash">
|
||||
<div className="splash-plate">
|
||||
<div className="seal">GU</div>
|
||||
<h1 className="splash-title">PRINCIPAL INVESTIGATOR</h1>
|
||||
<p className="splash-sub">Glitch University · Case Files</p>
|
||||
<div className="splash-cases">
|
||||
{mysteries.map(mystery => {
|
||||
const canResume = resumable?.playthrough.mysterySlug === mystery.slug
|
||||
return <button key={mystery.slug} className="splash-case" disabled={busy}
|
||||
onClick={() => (canResume && resumable) ? apply(resumable) : newGame(mystery.slug)}>
|
||||
<span className="splash-case-title">{mystery.title}</span>
|
||||
<span className="splash-case-action">{canResume ? 'RESUME ▸' : 'BEGIN ▸'}</span>
|
||||
</button>
|
||||
})}
|
||||
{!mysteries.length && <p className="splash-status">NO CASE FILES AVAILABLE</p>}
|
||||
</div>
|
||||
<small className="splash-status">{busy ? 'OPENING CASE FILE…' : status}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const node = state?.node
|
||||
if (!node) return <div className="boot"><div className="seal">GU</div><p>GLITCH UNIVERSITY NETWORK TERMINAL</p><small>{status || 'OPENING CASE FILE…'}</small></div>
|
||||
|
||||
Reference in New Issue
Block a user