import type { TreeTech } from "./types"; /** * Pure helpers for editing a technology's prerequisites from the tree canvas * and the side panel. The tree flattens the document's prerequisite rows into * two unions (AND / OR), and edits are written back as a single row, which * preserves the meaning of the rows the tree renders. */ export type PrereqLists = { and: number[]; or: number[] }; export type PrereqEditResult = | { ok: true; lists: PrereqLists } | { ok: false; error: string }; /** Does `fromId` (transitively) depend on `toId`? Cycle probe over prereq edges. */ export function dependsOn(techs: TreeTech[], fromId: number, toId: number): boolean { const byId = new Map(techs.map((t) => [t.id, t])); const seen = new Set(); const stack = [fromId]; while (stack.length > 0) { const current = stack.pop()!; if (current === toId) return true; if (seen.has(current)) continue; seen.add(current); const tech = byId.get(current); if (!tech) continue; stack.push(...tech.prereqAnd, ...tech.prereqOr); } return false; } /** The document-shaped prerequisite rows for a pair of id lists. */ export function buildPrereqRows(lists: PrereqLists) { return [ { requiresAnd: { technology: lists.and }, requiresOr: { technology: lists.or }, }, ]; } /** * Add `sourceId` as a prerequisite of `targetId`. Rejects self-references, * duplicates, and connections that would create a circular dependency. */ export function addPrerequisite( techs: TreeTech[], targetId: number, sourceId: number, kind: "and" | "or", ): PrereqEditResult { const target = techs.find((t) => t.id === targetId); if (!target) return { ok: false, error: "Technology not found." }; if (targetId === sourceId) { return { ok: false, error: "A technology cannot be its own prerequisite." }; } if (target.prereqAnd.includes(sourceId) || target.prereqOr.includes(sourceId)) { return { ok: false, error: "That technology is already a prerequisite." }; } // Adding source -> target means target depends on source. If source already // depends on target, this closes a loop. if (dependsOn(techs, sourceId, targetId)) { return { ok: false, error: "That would create a circular dependency." }; } const lists: PrereqLists = { and: kind === "and" ? [...target.prereqAnd, sourceId] : [...target.prereqAnd], or: kind === "or" ? [...target.prereqOr, sourceId] : [...target.prereqOr], }; return { ok: true, lists }; } /** Remove `sourceId` from both prerequisite lists of `targetId`. */ export function removePrerequisite( techs: TreeTech[], targetId: number, sourceId: number, ): PrereqEditResult { const target = techs.find((t) => t.id === targetId); if (!target) return { ok: false, error: "Technology not found." }; const lists: PrereqLists = { and: target.prereqAnd.filter((id) => id !== sourceId), or: target.prereqOr.filter((id) => id !== sourceId), }; return { ok: true, lists }; } /** Move an existing prerequisite between the AND and OR groups. */ export function setPrerequisiteKind( techs: TreeTech[], targetId: number, sourceId: number, kind: "and" | "or", ): PrereqEditResult { const target = techs.find((t) => t.id === targetId); if (!target) return { ok: false, error: "Technology not found." }; if (!target.prereqAnd.includes(sourceId) && !target.prereqOr.includes(sourceId)) { return { ok: false, error: "That technology is not a prerequisite yet." }; } const lists: PrereqLists = { and: kind === "and" ? [...new Set([...target.prereqAnd, sourceId])] : target.prereqAnd.filter((id) => id !== sourceId), or: kind === "or" ? [...new Set([...target.prereqOr, sourceId])] : target.prereqOr.filter((id) => id !== sourceId), }; return { ok: true, lists }; } /** Apply a set of prerequisite lists to a TreeTech-shaped object. */ export function withPrereqs(tech: TreeTech, lists: PrereqLists): TreeTech { return { ...tech, prereqAnd: lists.and, prereqOr: lists.or }; }