From 9c496bfe199e08d864c2eead068d1d2242694250 Mon Sep 17 00:00:00 2001 From: jenstandstad Date: Sat, 22 Aug 2026 17:49:09 +0200 Subject: [PATCH] Seed phone + merit nodes (and NPC contacts) via the graph seeder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit authorGraph now seeds merit nodes' awards_flag and phone-node terminals bound to an NPC (npc key -> npc_id, the callee dialed); authorMystery seeds cast phone_number/email. Importer types updated so mystery.json can carry these, matching how Glass Harbour is seeded — the Barricelli graph can now be authored as data. Co-Authored-By: Claude Opus 4.8 --- scripts/importMysteryTemplate.ts | 8 ++++---- server/narrativeRepository.ts | 6 +++--- server/storyGraphRepository.ts | 18 ++++++++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/scripts/importMysteryTemplate.ts b/scripts/importMysteryTemplate.ts index a52031b..41edf19 100644 --- a/scripts/importMysteryTemplate.ts +++ b/scripts/importMysteryTemplate.ts @@ -16,13 +16,13 @@ type MysteryDocument = { } type MysteryGraph = { entry: string - nodes: { key: string; type: 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate'; label?: string; x: number; y: number - componentKey?: string; templateSlug?: string; version?: number - terminals?: { key: string; label?: string; to?: string | null }[] + nodes: { key: string; type: 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate' | 'merit' | 'phone'; label?: string; x: number; y: number + componentKey?: string; templateSlug?: string; version?: number; awardsFlag?: string + terminals?: { key: string; label?: string; to?: string | null; npc?: string }[] utterances?: { npc?: string; pose?: string; text: string; utterer?: 'npc' | 'player' }[] }[] } type MysteryNarrative = { - cast: { key: string; name: string; role?: string; defaultPose?: string; poses?: { poseKey: string; assetId: string }[] }[] + cast: { key: string; name: string; role?: string; defaultPose?: string; phoneNumber?: string; email?: string; poses?: { poseKey: string; assetId: string }[] }[] graph?: MysteryGraph } type MysteryGoal = { diff --git a/server/narrativeRepository.ts b/server/narrativeRepository.ts index 0a1aff1..c901d3c 100644 --- a/server/narrativeRepository.ts +++ b/server/narrativeRepository.ts @@ -32,7 +32,7 @@ export type PlaythroughAdvanceResult = { export type MysteryAuthoring = { slug: string title: string - cast: { key: string; name: string; role?: string; defaultPose?: string; poses?: { poseKey: string; assetId: string }[] }[] + cast: { key: string; name: string; role?: string; defaultPose?: string; phoneNumber?: string; email?: string; poses?: { poseKey: string; assetId: string }[] }[] } /** @@ -212,8 +212,8 @@ export function createNarrativeRepository(pool: Pool, objectStorage: ObjectStora const existing = await client.query('SELECT 1 FROM osint.npcs WHERE npc_key=$1 AND mystery_id IS NULL', [npc.key]) if (existing.rows[0]) continue const npcId = randomUUID() - await client.query('INSERT INTO osint.npcs (id,mystery_id,npc_key,name,role,default_pose_key) VALUES ($1,NULL,$2,$3,$4,$5)', - [npcId, npc.key, npc.name, npc.role || '', npc.defaultPose || null]) + await client.query('INSERT INTO osint.npcs (id,mystery_id,npc_key,name,role,default_pose_key,phone_number,email) VALUES ($1,NULL,$2,$3,$4,$5,$6,$7)', + [npcId, npc.key, npc.name, npc.role || '', npc.defaultPose || null, npc.phoneNumber || null, npc.email || null]) for (const pose of npc.poses || []) await client.query( 'INSERT INTO osint.npc_poses (id,npc_id,pose_key,asset_id) VALUES ($1,$2,$3,$4)', [randomUUID(), npcId, pose.poseKey, pose.assetId]) } diff --git a/server/storyGraphRepository.ts b/server/storyGraphRepository.ts index 809885f..c032fb2 100644 --- a/server/storyGraphRepository.ts +++ b/server/storyGraphRepository.ts @@ -3,8 +3,8 @@ import type { Pool, PoolClient } from 'pg' export type GraphSpecNode = { key: string; type: StoryNodeType; label?: string; x: number; y: number - componentKey?: string; templateSlug?: string; version?: number - terminals?: { key: string; label?: string; to?: string | null }[] + componentKey?: string; templateSlug?: string; version?: number; awardsFlag?: string + terminals?: { key: string; label?: string; to?: string | null; npc?: string }[] utterances?: { npc?: string; pose?: string; text: string; utterer?: Utterer }[] } export type GraphSpec = { entry: string; nodes: GraphSpecNode[] } @@ -261,12 +261,18 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository { versionId = version.rows[0]?.id ?? null if (!versionId) throw new Error(`Graph node ${node.key}: unknown level template ${node.templateSlug}`) } - await client.query('INSERT INTO osint.story_nodes (id,mystery_id,node_type,label,xpos,ypos,has_utterances,level_template_version_id,component_key) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)', - [id, mysteryId, node.type, node.label || node.type, node.x, node.y, Boolean(node.utterances?.length), versionId, node.componentKey || null]) + await client.query('INSERT INTO osint.story_nodes (id,mystery_id,node_type,label,xpos,ypos,has_utterances,level_template_version_id,component_key,awards_flag) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)', + [id, mysteryId, node.type, node.label || node.type, node.x, node.y, Boolean(node.utterances?.length), versionId, node.componentKey || null, node.awardsFlag || null]) for (const [index, terminal] of (node.terminals || []).entries()) { const terminalId = randomUUID(); terminalIds.set(`${node.key}:${terminal.key}`, terminalId) - await client.query('INSERT INTO osint.story_node_terminals (id,parent_node_id,terminal_key,label,sort_order) VALUES ($1,$2,$3,$4,$5)', - [terminalId, id, terminal.key, terminal.label || terminal.key, index]) + let npcId: string | null = null + if (terminal.npc) { // phone-node terminal bound to an NPC (the callee) + const npc = await client.query<{ id: string }>('SELECT id FROM osint.npcs WHERE npc_key=$1 AND mystery_id IS NULL', [terminal.npc]) + npcId = npc.rows[0]?.id ?? null + if (!npcId) throw new Error(`Graph node ${node.key}: terminal ${terminal.key} references unknown NPC ${terminal.npc}`) + } + await client.query('INSERT INTO osint.story_node_terminals (id,parent_node_id,terminal_key,label,sort_order,npc_id) VALUES ($1,$2,$3,$4,$5,$6)', + [terminalId, id, terminal.key, terminal.label || terminal.key, index, npcId]) } } // Wire terminals now that all nodes exist.