Utterance award + requirement, and branching dialogue seeding
A dialogue line can award an achievement when reached (granted server-side, validated against the player's current node so it can't be forged) and an option can require an achievement to be offered (filtered out of the resolved tree otherwise). authorGraph gains a branching form (utterance key/parent/ terminal) so option trees — not just linear lines — can be seeded. This is gaps 1 & 2 for the Barricelli Dobby/Glitch-Hunter dialogues; phone dialing stays stubbed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,10 @@ export type GraphSpecNode = {
|
||||
key: string; type: StoryNodeType; 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?: Utterer }[]
|
||||
// Linear form: an ordered list (chained automatically). Branching form: give each
|
||||
// utterance a `key` and set `parent` (its predecessor) + `terminal` (its exit);
|
||||
// multiple children of one parent become player options.
|
||||
utterances?: { key?: string; parent?: string; terminal?: string; npc?: string; pose?: string; text: string; utterer?: Utterer; awardsFlag?: string; requiresFlag?: string }[]
|
||||
}
|
||||
export type GraphSpec = { entry: string; nodes: GraphSpecNode[] }
|
||||
|
||||
@@ -285,24 +288,38 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository {
|
||||
// Utterances (linear seed): create, then chain them and exit the last one via
|
||||
// the node's first terminal, so the crafter shows a connected flow.
|
||||
for (const node of spec.nodes) {
|
||||
const spec2 = node.utterances || []
|
||||
const created: string[] = []
|
||||
for (const [index, utterance] of (node.utterances || []).entries()) {
|
||||
const uttKeyToId = new Map<string, string>()
|
||||
for (const [index, utterance] of spec2.entries()) {
|
||||
let npcId: string | null = null
|
||||
if (utterance.npc) {
|
||||
const npc = await client.query<{ id: string }>('SELECT id FROM osint.npcs WHERE npc_key=$1 AND mystery_id IS NULL', [utterance.npc])
|
||||
npcId = npc.rows[0]?.id ?? null
|
||||
}
|
||||
const utteranceId = randomUUID(); created.push(utteranceId)
|
||||
await client.query('INSERT INTO osint.utterances (id,node_id,utterer,npc_id,pose_key,text,xpos,ypos,sort_order) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)',
|
||||
[utteranceId, nodeIds.get(node.key), utterance.utterer || 'npc', npcId, utterance.pose || null, utterance.text, 60, 60 + index * 120, index])
|
||||
if (utterance.key) uttKeyToId.set(utterance.key, utteranceId)
|
||||
await client.query('INSERT INTO osint.utterances (id,node_id,utterer,npc_id,pose_key,text,awards_flag,requires_flag,xpos,ypos,sort_order) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)',
|
||||
[utteranceId, nodeIds.get(node.key), utterance.utterer || 'npc', npcId, utterance.pose || null, utterance.text, utterance.awardsFlag || null, utterance.requiresFlag || null, 60, 60 + index * 120, index])
|
||||
}
|
||||
const branching = spec2.some(utterance => utterance.key)
|
||||
if (branching) {
|
||||
// Explicit tree: wire each utterance's parent + exit terminal by key.
|
||||
for (const utterance of spec2) {
|
||||
const id = utterance.key ? uttKeyToId.get(utterance.key) : undefined
|
||||
if (!id) continue
|
||||
if (utterance.parent) await client.query('UPDATE osint.utterances SET parent_utterance_id=$2 WHERE id=$1', [id, uttKeyToId.get(utterance.parent) ?? null])
|
||||
if (utterance.terminal) await client.query('UPDATE osint.utterances SET terminal_id=$2 WHERE id=$1', [id, terminalIds.get(`${node.key}:${utterance.terminal}`) ?? null])
|
||||
}
|
||||
} else {
|
||||
// Linear: each line follows the previous; the last exits via the first terminal.
|
||||
for (let i = 1; i < created.length; i++)
|
||||
await client.query('UPDATE osint.utterances SET parent_utterance_id=$2 WHERE id=$1', [created[i], created[i - 1]])
|
||||
const firstTerminalKey = node.terminals?.[0]?.key
|
||||
const exitTerminalId = firstTerminalKey ? terminalIds.get(`${node.key}:${firstTerminalKey}`) : undefined
|
||||
if (created.length && exitTerminalId)
|
||||
await client.query('UPDATE osint.utterances SET terminal_id=$2 WHERE id=$1', [created[created.length - 1], exitTerminalId])
|
||||
}
|
||||
// Chain via parent: each line follows the previous one (one child = linear).
|
||||
for (let i = 1; i < created.length; i++)
|
||||
await client.query('UPDATE osint.utterances SET parent_utterance_id=$2 WHERE id=$1', [created[i], created[i - 1]])
|
||||
const firstTerminalKey = node.terminals?.[0]?.key
|
||||
const exitTerminalId = firstTerminalKey ? terminalIds.get(`${node.key}:${firstTerminalKey}`) : undefined
|
||||
if (created.length && exitTerminalId)
|
||||
await client.query('UPDATE osint.utterances SET terminal_id=$2 WHERE id=$1', [created[created.length - 1], exitTerminalId])
|
||||
}
|
||||
const entryId = nodeIds.get(spec.entry)
|
||||
if (!entryId) throw new Error(`Graph entry node ${spec.entry} not found`)
|
||||
|
||||
Reference in New Issue
Block a user