Phone directory foundation: NPC contacts + phone node + terminal→NPC
NPCs gain phone_number and email (the phone book). A new 'phone' node type holds a directory whose terminals each bind to an NPC via npc_id (the callee reached by dialing that number) and wire to the dialogue that plays on connect. Migration 029 adds the columns/type; repositories and admin API thread contacts and the per-terminal NPC binding. Authoring foundation only — the dial runtime and tools sidebar come next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,8 @@ export type GraphSpecNode = {
|
||||
}
|
||||
export type GraphSpec = { entry: string; nodes: GraphSpecNode[] }
|
||||
|
||||
export type StoryNodeType = 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate' | 'merit'
|
||||
export type TerminalDto = { id: string; terminalKey: string; label: string; toNodeId: string | null; sortOrder: number }
|
||||
export type StoryNodeType = 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate' | 'merit' | 'phone'
|
||||
export type TerminalDto = { id: string; terminalKey: string; label: string; toNodeId: string | null; sortOrder: number; npcId: string | null }
|
||||
export type StoryNodeDto = {
|
||||
id: string; nodeType: StoryNodeType; label: string; hasUtterances: boolean
|
||||
xpos: number; ypos: number; levelTemplateVersionId: string | null; componentKey: string | null; musicAssetId: string | null; musicVolume: number
|
||||
@@ -34,6 +34,7 @@ const DEFAULT_TERMINALS: Record<StoryNodeType, { key: string; label: string }[]>
|
||||
det_gate: [{ key: 'pass', label: 'Pass' }],
|
||||
llm_gate: [{ key: 'pass', label: 'Pass' }],
|
||||
merit: [{ key: 'continue', label: 'Continue' }],
|
||||
phone: [], // a phone node's terminals are added per contact (each bound to an NPC)
|
||||
}
|
||||
|
||||
export interface StoryGraphRepository {
|
||||
@@ -42,7 +43,7 @@ export interface StoryGraphRepository {
|
||||
updateNode(nodeId: string, input: Partial<{ label: string; xpos: number; ypos: number; hasUtterances: boolean; componentKey: string | null; levelTemplateVersionId: string | null; musicAssetId: string | null; musicVolume: number; awardsFlag: string | null }>): Promise<StoryNodeDto | null>
|
||||
deleteNode(nodeId: string): Promise<boolean>
|
||||
addTerminal(nodeId: string, input: { terminalKey: string; label?: string }): Promise<StoryNodeDto | null>
|
||||
updateTerminal(terminalId: string, input: Partial<{ label: string; sortOrder: number; toNodeId: string | null }>): Promise<{ ok: boolean; error?: string }>
|
||||
updateTerminal(terminalId: string, input: Partial<{ label: string; sortOrder: number; toNodeId: string | null; npcId: string | null }>): Promise<{ ok: boolean; error?: string }>
|
||||
deleteTerminal(terminalId: string): Promise<boolean>
|
||||
setEntryNode(mysteryId: string, nodeId: string | null): Promise<{ ok: boolean; error?: string }>
|
||||
listLevelTemplates(): Promise<LevelTemplateOption[]>
|
||||
@@ -65,14 +66,14 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository {
|
||||
const [nodes, terminals] = await Promise.all([
|
||||
pool.query<{ id: string; node_type: StoryNodeType; label: string; has_utterances: boolean; xpos: number; ypos: number; level_template_version_id: string | null; component_key: string | null; music_asset_id: string | null; music_volume: number; awards_flag: string | null }>(
|
||||
'SELECT id,node_type,label,has_utterances,xpos,ypos,level_template_version_id,component_key,music_asset_id,music_volume,awards_flag FROM osint.story_nodes WHERE mystery_id=$1 ORDER BY created_at', [mysteryId]),
|
||||
pool.query<{ id: string; parent_node_id: string; terminal_key: string; label: string; to_node_id: string | null; sort_order: number }>(
|
||||
`SELECT t.id,t.parent_node_id,t.terminal_key,t.label,t.to_node_id,t.sort_order FROM osint.story_node_terminals t
|
||||
pool.query<{ id: string; parent_node_id: string; terminal_key: string; label: string; to_node_id: string | null; sort_order: number; npc_id: string | null }>(
|
||||
`SELECT t.id,t.parent_node_id,t.terminal_key,t.label,t.to_node_id,t.sort_order,t.npc_id FROM osint.story_node_terminals t
|
||||
JOIN osint.story_nodes n ON n.id=t.parent_node_id WHERE n.mystery_id=$1 ORDER BY t.sort_order,t.terminal_key`, [mysteryId]),
|
||||
])
|
||||
const byNode = new Map<string, TerminalDto[]>()
|
||||
for (const row of terminals.rows) {
|
||||
const list = byNode.get(row.parent_node_id) || []
|
||||
list.push({ id: row.id, terminalKey: row.terminal_key, label: row.label, toNodeId: row.to_node_id, sortOrder: row.sort_order })
|
||||
list.push({ id: row.id, terminalKey: row.terminal_key, label: row.label, toNodeId: row.to_node_id, sortOrder: row.sort_order, npcId: row.npc_id })
|
||||
byNode.set(row.parent_node_id, list)
|
||||
}
|
||||
return {
|
||||
@@ -158,6 +159,7 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository {
|
||||
if (input.label !== undefined) set('label', input.label.trim())
|
||||
if (input.sortOrder !== undefined) set('sort_order', input.sortOrder)
|
||||
if (input.toNodeId !== undefined) set('to_node_id', input.toNodeId)
|
||||
if (input.npcId !== undefined) set('npc_id', input.npcId || null)
|
||||
if (sets.length) await pool.query(`UPDATE osint.story_node_terminals SET ${sets.join(',')} WHERE id=$1`, values)
|
||||
return { ok: true }
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user