import { listSideStoryStarts } from '../graph/sceneGraphLineage'; import type { GraphNodeId, NpcBinding, NpcStorylineRef, Project, ProjectNpc, SceneId } from '../types'; export function noneBinding(): NpcBinding { return { kind: 'none' }; } export function isNpcBindingNone(b: NpcBinding | undefined | null): boolean { return !b || b.kind === 'none'; } export function storylineRefKey(ref: NpcStorylineRef): string { return ref.kind === 'main' ? 'main' : `side:${ref.startGraphNodeId}`; } export function normalizeNpcBinding( raw: unknown, ctx: { sceneIds: Set; sideStartIds: Set; hasMainStart: boolean }, ): NpcBinding { if (!raw || typeof raw !== 'object') return noneBinding(); const obj = raw as { kind?: string; sceneId?: string; storyline?: { kind?: string; startGraphNodeId?: string }; }; if (obj.kind === 'scene' && typeof obj.sceneId === 'string') { const sceneId = obj.sceneId as SceneId; if (ctx.sceneIds.has(sceneId)) return { kind: 'scene', sceneId }; return noneBinding(); } if (obj.kind === 'storyline' && obj.storyline && typeof obj.storyline === 'object') { if (obj.storyline.kind === 'main') { return ctx.hasMainStart ? { kind: 'storyline', storyline: { kind: 'main' } } : noneBinding(); } if (obj.storyline.kind === 'side' && typeof obj.storyline.startGraphNodeId === 'string') { const id = obj.storyline.startGraphNodeId as GraphNodeId; if (ctx.sideStartIds.has(id)) { return { kind: 'storyline', storyline: { kind: 'side', startGraphNodeId: id } }; } } } return noneBinding(); } export function clearNpcBindingsForDeletedScene(npcs: ProjectNpc[], sceneId: SceneId): ProjectNpc[] { return npcs.map((n) => { if (n.binding?.kind === 'scene' && n.binding.sceneId === sceneId) { return { ...n, binding: noneBinding() }; } return n; }); } export function clearNpcBindingsForRemovedStoryline( npcs: ProjectNpc[], removed: NpcStorylineRef, ): ProjectNpc[] { return npcs.map((n) => { if (n.binding?.kind !== 'storyline') return n; const ref = n.binding.storyline; if (removed.kind === 'main' && ref.kind === 'main') return { ...n, binding: noneBinding() }; if (removed.kind === 'side' && ref.kind === 'side' && ref.startGraphNodeId === removed.startGraphNodeId) { return { ...n, binding: noneBinding() }; } return n; }); } export function listStorylineOptionsForBinding(project: Project): { main: boolean; sides: { startGraphNodeId: GraphNodeId; label: string }[]; } { const hasMain = project.sceneGraphNodes.some((n) => n.isStartScene); const sides = listSideStoryStarts(project.sceneGraphNodes).map((n) => { const scene = project.scenes[n.sceneId]; const label = n.sideStoryLineTitle.trim() || scene?.title.trim() || String(n.id); return { startGraphNodeId: n.id, label }; }); return { main: hasMain, sides }; }