import { z } from 'zod'; import type { ActionDef } from './schema'; export const storyOutcomeSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('setFlag'), flag: z.string().min(1) }), z.object({ type: z.literal('clearFlag'), flag: z.string().min(1) }), z.object({ type: z.literal('grantResource'), resourceId: z.string().min(1), amount: z.number().positive(), }), z.object({ type: z.literal('consumeResource'), resourceId: z.string().min(1), amount: z.number().positive(), }), z.object({ type: z.literal('log'), text: z.string().min(1) }), ]); export const choiceRequirementsSchema = z.object({ minResources: z.record(z.string(), z.number().nonnegative()).optional(), requireStoryFlags: z.array(z.string().min(1)).optional(), excludeStoryFlags: z.array(z.string().min(1)).optional(), }); export const storyChoiceSchema = z.object({ id: z.string().min(1), label: z.string().min(1), requirements: choiceRequirementsSchema.optional(), outcomes: z.array(storyOutcomeSchema).default([]), targetNodeId: z.string().min(1), }); export const storyTriggerSchema = z.object({ type: z.enum(['boot', 'actionComplete', 'minResources']), actionId: z.string().min(1).optional(), minResources: z.record(z.string(), z.number().nonnegative()).optional(), targetNodeId: z.string().min(1), once: z.boolean().default(true), }); export const storyNodeSchema = z.object({ id: z.string().min(1), prose: z.string().min(1), choices: z.array(storyChoiceSchema).optional(), triggers: z.array(storyTriggerSchema).optional(), enterOutcomes: z.array(storyOutcomeSchema).optional(), }); export type StoryOutcome = z.infer; export type StoryChoice = z.infer; export type StoryTrigger = z.infer; export type StoryNode = z.infer; export interface StoryContent { storyNodes: StoryNode[]; storyNodesById: Record; bootTargetNodeId: string; } function indexStoryNodes(nodes: StoryNode[]): Record { const byId: Record = {}; for (const node of nodes) { if (byId[node.id]) throw new Error(`Duplicate story node id "${node.id}"`); byId[node.id] = node; } return byId; } export function buildStoryContent( rawNodes: unknown[], actionsById: Record, resourcesById: Record, ): StoryContent { const storyNodes = rawNodes.map((n) => storyNodeSchema.parse(n)); const storyNodesById = indexStoryNodes(storyNodes); let bootTargetNodeId: string | null = null; for (const node of storyNodes) { for (const trigger of node.triggers ?? []) { if (trigger.type === 'boot') { if (bootTargetNodeId !== null) { throw new Error('Story graph must have exactly one boot trigger'); } bootTargetNodeId = trigger.targetNodeId; } if (trigger.type === 'actionComplete' && !actionsById[trigger.actionId ?? '']) { throw new Error(`Story trigger references unknown action "${trigger.actionId}"`); } if (trigger.minResources) { for (const resourceId of Object.keys(trigger.minResources)) { if (!resourcesById[resourceId]) { throw new Error(`Story trigger references unknown resource "${resourceId}"`); } } } if (!storyNodesById[trigger.targetNodeId]) { throw new Error(`Story trigger references unknown story node "${trigger.targetNodeId}"`); } } for (const choice of node.choices ?? []) { if (!storyNodesById[choice.targetNodeId]) { throw new Error(`Story choice references unknown story node "${choice.targetNodeId}"`); } for (const outcome of choice.outcomes) { if (outcome.type === 'grantResource' || outcome.type === 'consumeResource') { if (!resourcesById[outcome.resourceId]) { throw new Error(`Story outcome references unknown resource "${outcome.resourceId}"`); } } } } for (const outcome of node.enterOutcomes ?? []) { if (outcome.type === 'grantResource' || outcome.type === 'consumeResource') { if (!resourcesById[outcome.resourceId]) { throw new Error(`Story enterOutcome references unknown resource "${outcome.resourceId}"`); } } } } if (bootTargetNodeId === null) { throw new Error('Story graph must have exactly one boot trigger'); } return { storyNodes, storyNodesById, bootTargetNodeId }; }