Add story-graph narrative system (campaigns, editors, runtime)
Introduce the narrative layer as a directed story-flow graph: an authored campaign a player walks node by node, replacing the interim slot/chapter model. Schema (migrations 015-021): - mysteries, global NPC templates + named poses, per-user playthroughs - story_nodes, terminals, utterances (the flow graph and dialogue trees) - clean cutover: retire slot cutscenes/chapters/seen_dialogue Runtime: - New Game creates a playthrough bound to the JWT identity (dev test-user fallback) - advance() walks the graph cutscene -> dialogue -> level -> ..., auto-skipping gates - branching dialogue: player choices route out through node terminals Admin authoring: - NPC editor: upload named poses to the gupi MinIO bucket - mystery graph editor: vertical node canvas, wiring, entrypoint, delete-by-click - dialogue crafter: utterance tree, Tab to add child, 1/2 speaker, undo Content authored via the manifest importer / admin panel and seeded for Glass Harbour. MinIO added to the dev stack; dev container runs in development mode. Also includes a folder-widget simplification (removes open/close) and a resolveUserId auth helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { BookOpen, Building2, CalendarClock, FileText, Folder, FolderOpen, Image as ImageIcon, Pencil, UserRound } from 'lucide-react'
|
||||
import { BookOpen, Building2, CalendarClock, FileText, Image as ImageIcon, Pencil, UserRound } from 'lucide-react'
|
||||
import type { CaseDocument, DocumentExhibit, Evidence, Exhibit, ExhibitRelation, ExhibitType, SourceFileType, TemporalFact } from './types'
|
||||
|
||||
export type WidgetCommand =
|
||||
| { type: 'open-document'; documentId: string }
|
||||
| { type: 'toggle-folder'; folderId: string }
|
||||
| { type: 'edit-folder'; folderId: string }
|
||||
| { type: 'edit-event'; eventId: string }
|
||||
| { type: 'edit-party'; partyId: string }
|
||||
@@ -40,13 +39,8 @@ const relationsFrom = (context: ExhibitWidgetContext, exhibitId: string, type: E
|
||||
|
||||
function FolderWidget({ exhibit, context }: ExhibitWidgetProps) {
|
||||
if (exhibit.type !== 'folder') return null
|
||||
const documents = relationsFrom(context, exhibit.id, 'contains').flatMap(relation => {
|
||||
const document = context.exhibits.find(candidate => candidate.id === relation.toExhibitId)
|
||||
return document?.type === 'document' ? [document] : []
|
||||
})
|
||||
return <div className="card-content"><h3>{exhibit.title}</h3><p>{exhibit.content}</p>
|
||||
{!exhibit.isOpen && <div className="folder-documents">{documents.slice(0,3).map(document => <button key={document.id} onClick={() => context.dispatch({ type:'open-document',documentId:document.id })} title={document.title}><FileText size={12}/><span>{document.title}</span>{document.publishedAt && <time>{document.publishedAt.slice(0,10)}</time>}</button>)}{documents.length > 3 && <small>+ {documents.length - 3} MORE FILES</small>}</div>}
|
||||
<div className="folder-actions"><button onClick={() => context.dispatch({ type:'toggle-folder',folderId:exhibit.id })}>{exhibit.isOpen ? <Folder size={12}/> : <FolderOpen size={12}/>} {exhibit.isOpen ? 'CLOSE' : 'OPEN'}</button><button onClick={() => context.dispatch({ type:'edit-folder',folderId:exhibit.id })}><Pencil size={12}/> EDIT</button></div>
|
||||
<div className="folder-actions"><button onClick={() => context.dispatch({ type:'edit-folder',folderId:exhibit.id })}><Pencil size={15}/> EDIT</button></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user