/** * @fortress/format — TypeScript type definitions for the * Fortress Adventure Format (schemaVersion 1.1). * * Two layers are exposed: * * 1. WIRE TYPES (top-level exports) — the JSON shapes inside the .fortress.zip: * ManifestJson, ModuleJson, SceneNode, Edge, Condition, Mutation, ArtifactDecl. * * 2. AUTHORING TYPES (Authoring namespace) — the simpler model an author or AI * agent emits. A small compiler turns this into the WIRE types and dual-emits * the compatibility names (`requires`+`artifact`, `target_id`+`artifact`). * * Reference: fortress-adventure-authoring-guide.md and fortress-adventure-zip-format-spec.md. */ // ─── Shared enums ───────────────────────────────────────────────────────── export type SchemaVersion = "1.1"; export type MatchMode = "all" | "any"; export type LockedDisplay = "hidden" | "greyed"; export type ArtifactVisibility = "visible" | "hidden"; export type ConditionType = | "has" // quantity >= 1 (binary "has it") | "lacks" // quantity === 0 (binary "doesn't have") | "gte" // quantity >= value | "lte" // quantity <= value | "eq"; // quantity === value export type MutationAction = | "add" // set quantity to 1 | "remove" // set quantity to 0 | "toggle" // flip 0 <-> 1 | "add_value" // quantity += value | "subtract_value" // quantity -= value | "set_value"; // quantity = value // ─── Wire types (the JSON inside the ZIP) ───────────────────────────────── /** * A gate clause used inside arrivalConditions and edge.conditions. * Emit BOTH `requires` and `artifact` with the same value (compatibility handshake). */ export interface Condition { requires: string; artifact: string; condition_type: ConditionType; /** Required for `gte` | `lte` | `eq`. Omit for `has` | `lacks`. */ value?: number; } /** * An effect applied on arrival at a scene. * Emit BOTH `target_id` and `artifact` with the same value. */ export interface Mutation { target_id: string; artifact: string; action: MutationAction; /** Required for `add_value` | `subtract_value` | `set_value`. */ value?: number; /** Apply at most once per run (use on item grants inside revisitable loops). */ is_unique?: boolean; visibility?: ArtifactVisibility; } /** An outgoing choice. Embedded in `SceneNode.edges`. */ export interface Edge { target_node_id: string; label: string; conditions: Condition[]; conditionMatch: MatchMode; lockedDisplay: LockedDisplay; lockedMessage?: string; } export interface NodeContent { /** Clean HTML body of the scene. */ prose: string; } export interface SceneNode { id: string; title: string; content: NodeContent; /** Cosmetic/legacy. Runtime behavior is derived from gates and effects. */ type?: string; isEnding: boolean; endingTitle?: string; arrivalConditions: Condition[]; arrivalMatch: MatchMode; state_mutations: Mutation[]; edges: Edge[]; } export interface ArtifactDecl { id: string; label: string; visibility: ArtifactVisibility; } /** Series-chaining entry point. */ export interface StartSocket { /** Module id of the predecessor whose run unlocks this socket. */ predecessorId: string; /** Node in THIS module the reader should start at when the socket matches. */ startNodeId: string; } /** The playable graph — written to `module.json` inside the ZIP. */ export interface ModuleJson { schemaVersion: SchemaVersion; moduleId: string; rootNodeId: string; defaultCanonNodeId: string; /** Keyed by artifact id. */ artifacts: Record; /** Keyed by node id. */ nodes: Record; startSockets: StartSocket[]; } export interface Author { id: string; displayName: string; } export interface SeriesCollection { collectionId: string; title: string; } /** The library card + integrity wrapper — written to `manifest.json` inside the ZIP. */ export interface ManifestJson { schemaVersion: SchemaVersion; /** Same value as `moduleId`. */ id: string; moduleId: string; title: string; author: Author; intro: string; description: string; keywords: string[]; /** Filename of the cover image entry, conventionally `"cover.png"`. */ cover: string; /** Numeric color string (decimal RGB packed). */ coverColor?: string; /** Optional embedded preview cover as a data URL. */ coverDataUrl?: string; version: number; /** Raw lowercase 64-char SHA-256 hex over the EXACT `module.json` bytes. No prefix. */ contentHash: string; publishedAt: string; defaultCanonNodeId: string; seriesId?: string; /** Series display title (string for current live shelf facet compatibility). */ series?: string; /** Richer series metadata. Emit alongside `series`. */ seriesCollection?: SeriesCollection; predecessors: string[]; startSockets: StartSocket[]; engine?: { quantitiesSupported?: boolean; [key: string]: unknown; }; } // ─── Authoring types (the simpler model AI agents and authors emit) ─────── export namespace Authoring { export type Op = "has" | "lacks" | "atLeast" | "atMost" | "exactly"; export interface Rule { artifact: string; op: Op; /** Required for `atLeast` | `atMost` | `exactly`. */ value?: number; } export interface ConditionsBlock { match: MatchMode; rules: Rule[]; } /** One of three effect shapes. Exactly one of gain/lose/set is present. */ export type Effect = | { gain: string; amount?: number; onceOnly?: boolean; visibility?: ArtifactVisibility } | { lose: string; amount?: number; onceOnly?: boolean } | { set: string; amount: number; onceOnly?: boolean }; export interface Choice { label: string; /** Destination scene id. */ to: string; when?: ConditionsBlock | null; lockedDisplay?: LockedDisplay; lockedMessage?: string; } export interface Scene { id: string; /** Exactly one scene per spec should be marked `root: true`. */ root?: boolean; title: string; /** Clean HTML body. */ prose: string; enter?: ConditionsBlock | null; onArrive?: Effect[]; choices?: Choice[]; isEnding?: boolean; endingTitle?: string; } export interface ArtifactDef { id: string; label: string; visibility?: ArtifactVisibility; } export interface Series { id: string; title: string; } /** * The whole adventure in the simple authoring model. * Pass this through your compiler to produce a `ModuleJson` + `ManifestJson` + ZIP. */ export interface AuthoringSpec { moduleId: string; title: string; author: Author; intro?: string; description?: string; keywords?: string[]; coverColor?: string; series?: Series; artifacts: ArtifactDef[]; scenes: Scene[]; version?: number; predecessors?: string[]; startSockets?: StartSocket[]; } } // ─── Narrow type guards (handy at boundaries) ───────────────────────────── export const isNumericCondition = (c: Condition): c is Condition & { value: number } => c.condition_type === "gte" || c.condition_type === "lte" || c.condition_type === "eq"; export const isNumericMutation = (m: Mutation): m is Mutation & { value: number } => m.action === "add_value" || m.action === "subtract_value" || m.action === "set_value";