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:
2026-08-22 15:51:32 +02:00
co-authored by Claude Opus 4.8
parent cd4b8bf4fa
commit 5c514562a2
4 changed files with 56 additions and 10 deletions
+32 -10
View File
@@ -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>