Add case adjudication cutscene
This commit is contained in:
+23
-2
@@ -5,7 +5,7 @@ import { AdminPanel } from './admin'
|
||||
import { audio } from './audio'
|
||||
import { clampBoardZoom, containedIds, dateValue, discardExhibit, folderIsOpen, moveBoardPoint, nextOpenBoardPosition, nextVisibleBoardPosition, normalizeCase, panViewport, projectThreadTag, threadCurve, threadTagLeadIn, threadTagPlacement, timelinePositionPercent, timelineRange, viewportCenteredOnExhibit, zoomFromPinch, zoomFromWheel, zoomViewportAt } from './boardDomain'
|
||||
import { defaultConnectionLabel, documentCapture, documentCaptureRegistry, documentExhibits, documentWidget, evidenceExhibits, exhibitWidget, mugshotIdentification, type ExhibitWidgetContext, type WidgetCommand } from './exhibitRegistry'
|
||||
import type { PlaythroughState } from './narrative'
|
||||
import { CutsceneHost, type PlaythroughState, type RuntimeNode } from './narrative'
|
||||
|
||||
// three.js stays out of the board bundle until the player opens the inventory.
|
||||
const Inventory = lazy(() => import('./inventory').then(m => ({ default: m.Inventory })))
|
||||
@@ -89,6 +89,7 @@ export function App() {
|
||||
const [arrivingExhibitIds, setArrivingExhibitIds] = useState<string[]>([])
|
||||
const [activePlaythroughId, setActivePlaythroughId] = useState<string | null>(null)
|
||||
const [completedGoal, setCompletedGoal] = useState<LevelGoal | null>(null)
|
||||
const [narrativeOverlay, setNarrativeOverlay] = useState<RuntimeNode | null>(null)
|
||||
const [advancing, setAdvancing] = useState(false)
|
||||
const saveTimer = useRef<number | undefined>(undefined)
|
||||
const boardRef = useRef<HTMLDivElement>(null)
|
||||
@@ -527,7 +528,25 @@ export function App() {
|
||||
if (!response.ok) { setStatus('OBJECTIVE COMPLETE · STORY ADVANCE UNAVAILABLE'); return }
|
||||
const next = await response.json() as PlaythroughState
|
||||
if (next.node?.kind === 'level' && next.node.levelSlug) window.location.assign(`/level/${encodeURIComponent(next.node.levelSlug)}`)
|
||||
else window.location.assign('/?resume=1') // resume straight into the next node, not the splash
|
||||
else if (next.node?.kind === 'cutscene') {
|
||||
setReportOpen(false)
|
||||
setCompletedGoal(null)
|
||||
setNarrativeOverlay(next.node)
|
||||
} else window.location.assign('/?resume=1') // resume straight into the next node, not the splash
|
||||
} finally { setAdvancing(false) }
|
||||
}
|
||||
|
||||
const continueAfterNarrativeOverlay = async () => {
|
||||
if (!activePlaythroughId) return
|
||||
setAdvancing(true)
|
||||
try {
|
||||
const response = await fetch(`/api/playthroughs/${encodeURIComponent(activePlaythroughId)}/advance`, {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
|
||||
})
|
||||
if (!response.ok) { setStatus('STORY ADVANCE UNAVAILABLE'); return }
|
||||
const next = await response.json() as PlaythroughState
|
||||
if (next.node?.kind === 'level' && next.node.levelSlug) window.location.assign(`/level/${encodeURIComponent(next.node.levelSlug)}`)
|
||||
else window.location.assign('/?resume=1')
|
||||
} finally { setAdvancing(false) }
|
||||
}
|
||||
|
||||
@@ -811,6 +830,8 @@ export function App() {
|
||||
{flagsOpen && <LevelFlagsEditor levelId={caseState.id} onClose={() => setFlagsOpen(false)} onChanged={async () => { await loadLevelBySlug(caseState.id) }} />}
|
||||
{matchRulesOpen && <EvidenceMatchRulesEditor levelId={caseState.id} onClose={() => setMatchRulesOpen(false)}/>}
|
||||
{completedGoal && <GoalComplete goal={completedGoal} hasNext={Boolean(activePlaythroughId)} busy={advancing} onContinue={() => void continueAfterGoal()}/>}
|
||||
{narrativeOverlay?.kind === 'cutscene' && <CutsceneHost componentKey={narrativeOverlay.componentKey} label={narrativeOverlay.label}
|
||||
presentation={narrativeOverlay.presentation} onComplete={() => { if (!advancing) void continueAfterNarrativeOverlay() }}/>}
|
||||
</main>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user