import * as _forgeax_workbench_host_node from '@forgeax/workbench-host/node'; import { WorkbenchToolHandler } from '@forgeax/workbench-host/node'; interface Position { x: number; y: number; } type HandleType = 'source' | 'target'; type HandlePosition = 'left' | 'right' | 'top' | 'bottom'; interface Handle { id: string; type: HandleType; position: HandlePosition; label?: string; data?: TData; } interface Node { id: string; type: TType; position: Position; inputs: Handle[]; outputs: Handle[]; data: TData; } interface Edge { id: string; source: string; target: string; sourceHandle: string; targetHandle: string; label?: string; data?: TData; } interface Graph { nodes: TNode[]; edges: TEdge[]; } /** * node-config-schema — 视频覆盖物类型 SSOT。 * * 统一命名(HTML 心智): * - **Overlay**:界面编辑器配好的可复用覆盖物(`children` = 包内组件) * - **OverlayNode**:演出节点上的 overlay 配置(含 `reactions` 作者 SSOT) * - **OverlayInstance**:运行态展开结果(不落盘) * * 视频上只能挂 overlay(经 OverlayNode),不能把裸组件直接贴视频。 * 作者面:组件导出事件 → Overlay 聚合 → `reactions`(when/do);走向经 do 内 advance + 边。 * * 接入:`GameScenario.ui.overlays` / `NodeData.overlayNodes`。 */ /** * 闭合动作原语(改状态 / 沿边推进 / 刷出瞬态组件)——同级并列,一个 do 可含多件事。 * - effect:施加副作用(改 attr/var/flag/item) * - advance:沿指定出边 `edgeId` 推进到其 `target`(唯一「换节点」通道;目标只在边上)。 * 交互/生命周期事件里可省略——省略时若存在匹配出边则默认推进;state 打断必须显式。 * - spawn:由反应**主动实例化**一个 overlay 组件模板。`from` = `overlayId/childId` 引用目录模板, * `inputs` 可含 `{expr}` 读 watch 局部量(prev/next/delta);省略 `ttlMs` 时常驻到节点退出。 * - hideOverlay:隐藏当前节点中 `mountId` 对应的整组已显示界面;重复隐藏或尚未显示时无操作。 */ type NodeAction = { kind: 'effect'; effects: GraphEffect[]; } | { kind: 'advance'; edgeId: string; } | { kind: 'spawn'; from: string; inputs?: Record; layout?: Layout; ttlMs?: number; } | { kind: 'hideOverlay'; mountId: string; }; /** Overlay 目录事件动作:目录是可复用表现/副作用模板,不得携带节点专属走向。 */ type OverlayReactionAction = Exclude; /** * Overlay 目录专用 reaction。稳定 key 恒为 `${childId}:${eventId}`; * 只允许组件 event → effect/spawn,节点挂载可再按顺序追加通用 NodeAction(含 advance)。 */ interface OverlayReaction { when: { type: 'event'; id: string; }; do: OverlayReactionAction[]; } /** * 触发面(闭合)——节点生命周期 + 事件 + 数据/状态 + 组件生命周期。effect 一律挂 reactions。 * - enter:进入节点(演出开始) * - at(ms):演出播到第 ms 毫秒 * - exit:离开节点前 * - complete:节点收尾自动推进(`if` 缺省 = 无条件) * - event:组件事件(挂 mount.reactions;do = effect/spawn/advance) * - state:GraphCondition 从不成立变为成立时触发;编辑器与出边复用同一套条件配置。 * - watch:观察某表达式(`of`)的值变化(`on` change/inc/dec)→ do(effect/spawn/advance); * 在每个写屏障处重采样比对(pull-diff)。局部量 prev/next/delta 供 do 内 `{expr}` 使用。 * - shown / hidden:某 overlay 组件实例**出现 / 消失**时触发(`of` = childId / mountId/childId / overlayId/childId)。 */ type ReactionTrigger = { type: 'enter'; } | { type: 'at'; ms: number; } | { type: 'exit'; } | { type: 'complete'; if?: GraphCondition; } | { type: 'event'; id: string; } | { type: 'state'; condition: GraphCondition; } | { type: 'watch'; of: string; on?: 'change' | 'inc' | 'dec'; } | { type: 'shown'; of: string; } | { type: 'hidden'; of: string; }; /** * 瘦 Reaction:when + do。作用域由挂载位置决定。 * do 同级承载副作用(effect/spawn)+ 走向(advance edgeId);**换节点只经边**。 */ interface Reaction { when: ReactionTrigger; do: NodeAction[]; } /** * 绝对定位排版(CSS inset 心智)—— overlay 内组件、挂载相对视频等共用。 * * `number` = 相对父框的 0~1 比例(含负值,如 translateY: -0.5); * 也可用 `'50%'` / `'12px'` 字符串。 */ type LayoutValue = number | `${number}%` | `${number}px`; interface Layout { top?: LayoutValue; right?: LayoutValue; bottom?: LayoutValue; left?: LayoutValue; width?: LayoutValue; height?: LayoutValue; /** 自身偏移,对齐 CSS `transform: translate(...)` */ translateX?: LayoutValue; translateY?: LayoutValue; /** 叠层顺序,对齐 CSS `z-index` */ zIndex?: number; } /** * Overlay 内一个 **组件实例**。 * - `component`:唯一类型键(行为 + 皮均由此查注册表) * - layout:overlay 内组件相对**挂载盒**的排版(含 zIndex);挂载有显式尺寸时缺省 = 左上角 * - trigger / window:出现时机 * - inputs:玩法 / 表现入参(不含摆放) */ interface OverlayChild { id: string; /** 组件 id(注册表键)。 */ component: string; layout?: Layout; trigger?: Trigger; window?: { startMs?: number; endMs?: number; }; /** * 组件参数袋(In)。 * 禁止:pos / layout / component(摆放用 layout 字段;component 用顶栏字段) */ inputs?: Record; note?: string; } /** * 一张可复用 **Overlay**。 * 键 = `scenario.ui.overlays[id]`。`children.length === 1` 也合法。 */ interface Overlay { id: string; title?: string; children: OverlayChild[]; /** 目录继承动作;严格使用 `childId:eventId`,运行时仅在组件 emit 路径消费。 */ reactions?: OverlayReaction[]; /** * 面向 AI 的整块模板摆放提示:描述这张拼好的模板**作为一个整体**一般放在视频 * 画布的什么位置(例如「我方状态 HUD 一般放在右下角」「字幕一般放在底部居中」)。 * 模板内部各组件的相对布局已由 children[].layout 固化,此字段不再重复描述内部 * 排布。作者在模板编辑页手动填写,也可用「AI 润色」按钮生成/优化。 */ prompt?: string; } /** scenario.ui:overlay 目录。 */ interface GameScenarioUi { overlays: Record; } /** * **OverlayNode** — 演出节点上的一份 overlay **挂载**(`NodeData.overlayNodes[]` 之一)。 * - `id`:挂载键(多挂载时事件命名空间用);缺省 = `overlay` * - `overlay`:引用哪张可复用 Overlay(原型;本挂载始终跟随其后续编辑,除被 override 的字段外) * - `layout`:整块相对视频画面;**无显式宽高时自适应内容**(单组件 overlay = 组件大小)。 * 舞台坐标类(floatText / dialogue / transition)挂载时应配 `{ left:0, top:0, width:1, height:1 }` * - `reactions`:本挂载 when→do(多为 event;走向经 do 内 advance + 边) * - `overrides` / `added` / `removed`:本挂载对 `overlay` 的**稀疏差量**(prototype + override,对齐 * Figma 实例覆盖 / Unity Prefab modifications 心智)——未出现在这三者里的组件永远跟随原型; * 只有显式改过的字段才脱钩。合并规则见 `expand-overlay.ts#resolveMountChildren`。 */ interface OverlayNode { /** * 挂载键。缺省 = `overlay`。 * 同一节点多次挂同一张 overlay 时必须显式且唯一。 */ id?: string; /** `scenario.ui.overlays` 中的 overlay id(原型;持续可跟随,见上)。 */ overlay: string; /** * 旧版节点副本的完整 child 列表。只读兼容;新写入必须使用 overrides/added/removed。 * 运行时展开时,存在该字段就以它作为本挂载的有效基底。 */ children?: OverlayChild[]; /** 整块相对本节点视频;无显式尺寸 → 自适应子组件内容。 */ layout?: Layout; reactions?: Reaction[]; /** * 逐组件差量:childId → 对原型该组件的字段级覆盖(仅存被改字段,未改字段仍读原型)。 * 键在原型里已不存在(方案改动导致孤儿)时,解析器忽略该条目。 */ overrides?: Record>; /** 本挂载本地新增的组件(不写回共享方案,只属于这个节点)。 */ added?: OverlayChild[]; /** 屏蔽原型里的这些 childId(tombstone;不物理删除共享方案)。 */ removed?: string[]; } /** * Kino 视频生成参数契约。 * * 这里只描述会影响生成结果的参数;`gameId`、任务 id、展示 label 等宿主路由或 * 编辑器状态不属于生成参数。参考图使用 Kino resource id,不接受宿主 asset id 或 URL。 */ declare const KINO_VIDEO_SIZES: readonly ["2560x1440", "1440x2560", "2496x1664", "1664x2496"]; type KinoVideoSize = (typeof KINO_VIDEO_SIZES)[number]; declare const KINO_VIDEO_RESOLUTIONS: readonly ["720p", "1080p"]; type KinoVideoResolution = (typeof KINO_VIDEO_RESOLUTIONS)[number]; declare const KINO_VIDEO_GENERATION_MODES: readonly ["strict", "firstref", "ref", "t2v"]; type KinoVideoGenerationMode = (typeof KINO_VIDEO_GENERATION_MODES)[number]; /** * 域 graph schema —— 新引擎/蓝图的**持久化 SSOT 形态**(存 blueprint.json)。 * * 在 `./react-flow-schema.ts` 的泛型骨架(Node/Handle/Edge/Graph)之上落定本引擎的数据契约。 * 视频覆盖物见 `./node-config-schema.ts`: * - **Overlay** → `GameScenario.ui.overlays`(含 `children`) * - **OverlayNode** → `NodeData.overlayNodes[]` * * **图原生类型**(GraphEffect / GraphCondition / Entity…)保持通用、无品类假设: * - 实体 = 一袋开放数值 `attrs` + 可选约束 `attrMeta`(min/max/initial);**没有 hp 特权字段**—— * hp 只是"名为 hp、attrMeta 带 max/initial 的一个 attr"的**约定**。 * - 条件/副作用全走 attr/var,`attrRatio` = `attrs[attr] / attrMeta[attr].max`。 * - 一切逻辑声明式、可序列化、无函数入库。 */ /** * 数值 = 常量或表达式字符串。求值语法只此一套,见 expr.ts —— `pick` 不是第二套表达式。 * `pick` 是编辑器专属 sidecar:记录「用下拉选取式拼出该 expr」时的选择结构,供重开时复原 * 下拉;由编辑器编译进 `expr`。引擎只读 `expr`,从不读 `pick`。 */ type NumOrExpr = number | { expr: string; pick?: ValuePick; }; /** `pick` 的选取式结构(运行时忽略):常量,或一条左结合的 ±×÷ 条款链。 */ type ValuePick = { mode: 'const'; const: number; } | { mode: 'pick'; terms: ValueTerm[]; }; type ValueTermOp = '+' | '-' | '*' | '/'; /** 一项:与前项做 op(首项仅 ±);取值按 source —— entity..attr. / var. / const。 */ type ValueTerm = { op?: ValueTermOp; source: 'entity' | 'var' | 'const'; refId: string; attr?: string; constValue?: number; }; type CmpOp = 'gte' | 'lte' | 'gt' | 'lt' | 'eq' | 'neq'; type TextCmpOp = 'eq' | 'neq'; /** 文本值可为字面量,或对变量/实体文案的引用。 */ type TextValue = string | { ref: string; }; /** 数值类 effect 的运算:加 / 乘 / 设为(减 = 增加负数)。 */ type NumericEffectOp = 'add' | 'mul' | 'set'; type GraphEffect = { kind: 'attr'; entityId: string; attr: string; op: NumericEffectOp; value: NumOrExpr; once?: boolean; id?: string; } /** 缺少 valueType 的旧数据按数值变量解释。 */ | { kind: 'var'; varId: string; valueType?: 'number'; op: NumericEffectOp; value: NumOrExpr; once?: boolean; id?: string; } | { kind: 'var'; varId: string; valueType: 'text'; op: 'set'; value: TextValue; once?: boolean; id?: string; } | { kind: 'flag'; varId: string; value: boolean; id?: string; } | { kind: 'item'; itemId: string; op: 'give' | 'take'; count: number; id?: string; }; type GraphClause = /** 缺少 valueType 的旧数据按数值变量解释。 */ { type: 'var'; varId: string; valueType?: 'number'; op: CmpOp; value: number; } | { type: 'var'; varId: string; valueType: 'text'; op: TextCmpOp; value: TextValue; } | { type: 'flag'; varId: string; equals: boolean; } | { type: 'visited'; nodeId: string; } /** 直接比某实体某 attr 的值。 */ | { type: 'attr'; entityId: string; attr: string; op: CmpOp; value: number; } /** 比某 attr 的"比例"= attrs[attr] / attrMeta[attr].max(如 hp 血量比例;死亡 = attrRatio hp lte 0)。 */ | { type: 'attrRatio'; entityId: string; attr: string; op: CmpOp; value: number; } /** 两实体同名 attr 运行时比较(如出手判断比 speed)。 */ | { type: 'attrCompare'; left: string; right: string; attr: string; op: CmpOp; } | { type: 'score'; op: CmpOp; value: number; } | { type: 'hasItem'; itemId: string; count?: number; }; interface GraphCondition { all: GraphClause[]; } /** 规则作者可持久化的值。字符串不参与数值公式或实体数值属性。 */ type ScalarValue = string | number; /** attr 的静态约束/元信息(值本身在运行态的 attrs 里)。 */ interface AttrMeta { min?: number; max?: number; /** 复位/复活的初值(restart/resetGlobals 用)。 */ initial?: number; /** HUD/编辑器显示名。 */ label?: string; } interface Entity { /** 与 `GameScenario.entities` 的 Record key 对齐;编辑器添加时自动生成,不可手填。 */ id: string; name?: string; kind?: string; /** 开放标量袋:数值可被运行时公式引用;字符串仅供作者编辑与存储。 */ attrs?: Record; /** 每个 attr 的约束(clamp 上下界 / 初值 / 显示名);可选。 */ attrMeta?: Record; } /** 节点使用哪些场景,以及是否作为该节点视频生成的参考。 */ interface NodeSceneBinding { sceneId: string; role?: 'primary' | 'secondary' | 'background'; useAsVideoReference?: boolean; } interface Variable { /** 与 `GameScenario.variables` 的 Record key 对齐;编辑器添加时自动生成,不可手填。 */ id: string; name?: string; initial?: ScalarValue; min?: number; max?: number; } /** * 文字「长什么样」。位置/尺寸见 `Layout`。 * 字段名与 CSS / React.CSSProperties 一一对应;呈现层只做单位换算与缺省兜底。 * · `fontSize`:数值 = 画面高度百分比,渲染为 `${n}cqh` * · `WebkitTextStroke*`:对应 `-webkit-text-stroke-color/width` */ interface GraphTextStyle { fontFamily?: string; fontWeight?: number; color?: string; fontSize?: number; textAlign?: 'left' | 'center' | 'right'; textDecoration?: string; backgroundColor?: string; opacity?: number; textShadow?: string; WebkitTextStrokeColor?: string; WebkitTextStrokeWidth?: number; } /** 文字预设样式(预设网格一格);内置在 text-style.ts,用户自定义存 GameScenario.textStylePresets。 */ interface GraphTextStylePreset { id: string; name: string; style: GraphTextStyle; /** 仅字幕预设:勾选后展示说话人前缀(映射到 dialogue.speaker)。 */ speakerPrefix?: boolean; /** 内置只读;用户自定义可编辑/删除。 */ builtin?: boolean; } /** * 触发时机(相对**本演出节点**的生命周期 / 时间线)。 * 引擎按 when 在对应时刻跑该元素(`fired` 去重)。 * 收尾 / 离场副作用走 `Reaction`(`complete` / `exit`),不在 Trigger 上表达。 */ type Trigger = /** 进入本节点、演出开始时立刻触发(开场 HUD / 进门对话等)。 */ { when: 'enter'; } /** * 相对本节点演出起点经过 `ms` 毫秒后触发(卡点 QTE、中段飘字等)。 * `ms` 与视频时间轴对齐;同一元素只触发一次。 */ | { when: 'at'; ms: number; }; interface NodeMedia { kind: string; ref?: string; /** 写给视频生成模型的镜头/动作/氛围提示词(图编辑器「重新生成」面板)。 */ prompt?: string; meta?: Record; /** 视频节点的生成预设;见 `NodeVideoGenerationPreset`。旧节点没有。 */ generation?: NodeVideoGenerationPreset; } /** * 节点视频的生成预设,**provider-resource-neutral**。 * * 只保存作者选项和平台 asset id:Kino resource id 属于 provider、生成前可能还不存在, * 写进蓝图会让文档不可移植。完整 `KinoVideoGenerationParams` 只在用户点击生成的提交 * 边界由 Host 解析。任务状态与成片 URL 住 `assets/manifest.json`,不在这里。 * * `prompt` 不在本结构内 —— `NodeMedia.prompt` 保持基础 prompt 的唯一真相源,避免双写。 */ interface NodeVideoGenerationPreset { schemaVersion: 1; durationSeconds: number; generateAudio: boolean; mode: KinoVideoGenerationMode; size?: KinoVideoSize; resolution?: KinoVideoResolution; model?: string; visualStyleKey?: string; /** 逻辑参考:平台 asset id,**不是** Kino resource id。 */ references?: { sceneAssetIds?: string[]; firstFrameAssetId?: string; lastFrameAssetId?: string; extraImageAssetIds?: string[]; }; } /** 节点出场角色绑定;只引用 `characterId`,不复制角色名或外观正文。 */ interface NodeCastBinding { characterId: string; role?: 'primary' | 'supporting' | 'background'; /** 缺省 true。false = 仅叙事提及,不参与画面参考推导。 */ onScreen?: boolean; } /** 音频资产引用:只挂 `assets/manifest` 里的 id,永不落 URL(壳层 resolve,引擎只传 id)。 */ type AudioRef = string; /** * 文档默认床轨(挂 `GameScenario` 根,与 `variables` / `entities` 同级)。 * 会话 `start` 时以 owner `'__doc__'` 压入 BGM 栈,通常不在中途 pop。 * 缺省 = 静音起局,直到走进首个带 `bgm` 的作用域。 * * 不要拿入口节点的 `data.bgm` 充当「整局默认」:它确实会一直播(D5),但它是**作用域层**—— * `jump` 会把它退掉、清局会按 `scenario.bgm` 重 derive,之后整局就再没有床轨了。整局默认只有 * 写在这里才是地板(`stop` 也弹不掉,D13)。 */ interface DocumentBgm { ref: AudioRef; /** 0..1,默认 1。 */ volume?: number; fadeInMs?: number; /** * 文档床**离场**时的淡出时长(ms,默认 0 = 硬切)。 * * 淡出恒取自离场那一帧(`BgmStack.resume`),所以「叙事床 → 战斗床」这条最常听到的转场 * 只有写在这里才淡得出去;缺了它,进战斗时正响的叙事床会 0ms 掉到静音,再由战斗床按自己的 * `fadeInMs` 淡入 —— 听感是一次明显的空档。 */ fadeOutMs?: number; /** 默认 true。 */ loop?: boolean; } /** * 节点作用域 BGM(owner 视角,不是「触发类型」)。 * * **配了就一直播**:进入该节点(容器则 `descend` 时)起播,跨多少节点都不停; * **走边离开该节点不结束**。结束只有两个来源,别无其他: * - 后面某个节点配 `mode: 'stop'` → 结束当前这层,回到上一层还没结束的那首; * - `jump` / 清局 → 引擎整体退栈(`unwindBgmToDocBed`)。 * * 容器(`subProcess` / `subFlowPack`)**不是**作用域:容器上的 `bgm` 与普通节点同一规则,弹回外层 * 不结束它。想要「出了这个子流程就结束」只能在包的每个出口终端上写 `mode: 'stop'`;被硬打断 * 弹出容器(没走终端)时这首会漏到调用方继续播。反过来也成立——**配 BGM 不得要求作者改蓝图 * 结构**(D11):一段平铺节点共用一首曲子只需在头一个节点上配一次,不必包进容器。 * * 内层节点不配 `bgm` 即继承当前栈顶(一律不动栈)。只配 `volume` 时不换曲、不新建作用域层, * 仅调整当前栈顶的音量;当前没有 BGM 时无操作。 */ interface NodeBgm { /** `mode: 'stop'` 或仅调整当前 BGM 音量时可省;其余情况必填。 */ ref?: AudioRef; /** * - `push`(默认):起播并**记住**当前正响的那首,一直播到有人结束它。 * - `replace`:换曲但**不**记住上一首(栈深不变)——「这首之后不需要回去」。 * - `stop`:结束当前这层,回到上一层还没结束的那首。文档床是地板,弹不掉(D13)。 */ mode?: 'push' | 'replace' | 'stop'; /** 0..1;未配置时沿用当前正在播放的 BGM 音量,起局无 BGM 时默认 1。 */ volume?: number; fadeInMs?: number; fadeOutMs?: number; /** * 同 ref 再次成为栈顶时是否从头播。 * 默认 false = 续播(回合循环友好)。 */ restart?: boolean; /** 默认 true = 循环;false = 单次播放。仅在配置了 ref 时生效。 */ loop?: boolean; } /** * 图节点 `data` **基类**(普通演出节点)。 * * 子流程 / 子蓝图容器用特化类型: * - `SubProcessNodeData` — 节点私有的内嵌子图 * - `SubFlowPackNodeData` — 跨图 pack 引用 * `GameNode.data` = `GameNodeData` 联合;读写嵌套字段用 `getSubProcess` / `getSubFlowPack`。 * * 覆盖物一律经 `overlayNodes` 引用并展开;视频上只能挂 Overlay,不能直挂裸组件。 */ /** * 节点玩法契约 —— 三条线之间唯一的共享设计。 * * 真跑里三条线各自猜对方的意图:总脉络编出没有元件提供的出口,数值线写了 43 个公式 * 只有 3 个被用上,整装拿到一堆零件却不知道哪个配哪个。根因不是「看不见零件」, * 而是**没人把玩法本身写下来**——玩法是「这个节拍里观众做什么、什么变了、走向哪里」, * 它跨了三条线的写域,所以必须是显式契约而不是各自的默契。 * * 由总脉络声明(它定节拍与分支,且能看元件清单),数值线**只读**它来决定要造哪些公式, * 整装照它挂载与接线。三段都有对应的完成门校验,见 `outline.interaction-plan` / * `rules.plan-formulas` / `finalization.plan-wired`。 */ interface NodeInteractionPlan { /** * 节拍类型。`narrative` 是**有意的纯叙事段**(观众只看),其余四类必须有 actions; * 这条区分让「漏配」和「刻意不配」在机器上可分辨。 */ beat: 'narrative' | 'choice' | 'combat' | 'check' | 'timed'; /** 观众在这个节拍能做的事;每条对应一个界面元件事件。 */ actions?: NodeInteractionAction[]; /** 回合节拍未分胜负时回到的节点;由 Host 校验真实回边。 */ loop?: { backTo: string; note?: string; }; /** 终局/转折条件,例如 `entity.tiger.attr.hp <= 0`。整装把它落成出边条件或 state reaction。 */ terminals?: Array<{ when: string; note?: string; }>; } /** 一次玩家动作:点哪个元件的哪个事件、改什么、走向哪里。 */ interface NodeInteractionAction { /** 界面元件 id,必须在元件清单里(`list_ui_components`)。 */ component: string; /** 元件事件 id;它同时是本节点的出口 handle(见 `ComponentRegistry.deriveOutputs`)。 */ event: string; /** 一句话说明观众为什么会点它(作者语言),供整装与后续人工审读。 */ intent: string; /** * 这个动作改什么。`target` 用 `entity..attr.` 或 `var.`; * `formulaId` 是**期望数值线提供的公式名**——数值线读这里决定要造哪些公式, * 整装用它写 effect。纯分支动作(只跳转不改数值)可以不填。 */ effect?: { target: string; op: 'add' | 'sub' | 'set'; formulaId?: string; value?: number; }; /** * 这个动作要走的出边 `sourceHandle`。缺省即与 `event` 同名; * 填 `none` 表示只结算不离开节点;一次性组件不会因此获得第二次 emit。 */ exit?: string; } interface NodeData { name: string; /** 章节/节拍概览,供节点卡与节点生产上下文使用。 */ chapterSummary?: string; /** 本节拍的玩法契约;见 `NodeInteractionPlan`。纯叙事节点也要显式声明 beat。 */ interaction?: NodeInteractionPlan; /** * 作者可读的演出/叙事正文(演出描述 + 台词 + 内心独白 + 选项)。 * 视频是它的下游产物:正文 → 分镜 → 关键帧 → 视频。字段名是 storyText,不是 scriptText。 */ storyText?: string; /** 本节点出场角色;引用 `assets/manifest.json.assetCatalog.entities.character` 的 key。 */ cast?: NodeCastBinding[]; /** 本节点使用的场景;引用 manifest 场景实体的 key。与 cast 对称。 */ scenes?: NodeSceneBinding[]; media?: NodeMedia; mediaPlayMode?: 'once' | 'loop'; /** * 延迟事件边的统一结算点。缺省在演出结束时结算;`at` 到点时提前收尾并离开节点。 */ routingSettlement?: RoutingSettlement; /** * 可选播放时长上限(ms)。Inspector 不再暴露编辑;字段仍由 bindVideo、既有图数据、 * 以及程序化写入保留,runtime 继续消费。 * - 无视频节点:作停留节拍 / 时间轴标尺。 * - 有视频节点:`>0` 且 `≤ 视频本身长度` 时,到点提前收演出;未填 / `≤0` / 超过视频长度 * → 视为无效、丢弃,以视频本身长度为准(不截断,交给 onEnded)。 */ durationMs?: number; /** 本节点上的 overlay 挂载列表;纯过场可省略。 */ overlayNodes?: OverlayNode[]; /** * 本节点作为 owner 的作用域 BGM;缺省 = 不动 BGM 栈(继承上层)。 * `SubFlow*NodeData` 由基类自动继承本字段,容器与普通节点同一套寿命规则,见 `NodeBgm`。 */ bgm?: NodeBgm; reactions?: Reaction[]; } /** * 跨图子蓝图指针(图上只存指针,不存子图节点)。 * 本体在 `manifest.packs[id]`;engine 开跑用根 `graph`,执行中遇依赖再查表。 */ interface SubFlowPack { /** 蓝图 id(与 BlueprintDoc.id 对齐)。 */ id: string; /** 可选版本钉死;解析时优先 `id@version`,否则回退 `id`。 */ version?: string; /** 覆盖包内默认入口;缺省用蓝图文档 entry。 */ entry?: string; } /** * 内嵌子流程本体。`entry` 只允许指向直属 `graph.nodes`;父子图之间不得直接连边。 */ interface SubProcess { entry: string; graph: GameGraph; } /** * 节点私有的内嵌子流程容器:首次进入压栈并切到 `subProcess.graph`; * 子图叶子无自动出边时弹回,容器不重播、沿父图出边续走。 */ interface SubProcessNodeData extends NodeData { subProcess: SubProcess; } /** * 跨图子蓝图容器:进入后加载 pack,从 entry 跑;包内叶子无出边时弹回主图。 */ interface SubFlowPackNodeData extends NodeData { subFlowPack: SubFlowPack; } /** 图上节点 data 联合(基类 ∪ 子流程特化)。 */ type GameNodeData = NodeData | SubProcessNodeData | SubFlowPackNodeData; /** 延迟事件边共用节点的统一结算点。 */ type RoutingSettlement = { type: 'complete'; } | { type: 'at'; ms: number; }; /** 事件边的跳转方式;缺省 `immediate` 保持旧图行为。 */ type EdgeTransition = 'immediate' | 'onSettlement'; /** 边路由数据(edge.data)——条件 / 权重 / 跳转方式;副作用走 reactions / option.effects。 */ interface EdgeRouting { condition?: GraphCondition; weight?: number; transition?: EdgeTransition; } /** * 图节点 type 字面量。合法集合的 **SSOT 是 `NodeKindRegistry`**(`runtime/nodes`), * 故用开放联合:保留 `'perf'` 的补全,同时允许任意已注册类型——加节点 = 注册一个 NodeKind, * 不必回改本行。浅守卫(isGameGraph)只认「非空字符串」,「是否已注册」的深校验走 validate.ts。 */ type GameNodeType = 'perf' | (string & {}); type GameNode = Node; type GameEdge = Edge; type GameGraph = Graph; /** 顶层容器(blueprint.json 的文档内容形态)。 */ interface GameScenario { version: string; /** Record key === Variable.id(添加时自动生成)。 */ variables?: Record; /** Record key === Entity.id(添加时自动生成)。 */ entities?: Record; /** * Runtime-safe formula catalog. Authoring keeps the same AST plus optional `pick` * sidecars; runtime expressions may reference a formula with `formula.`. */ formulas?: Record; /** overlay 目录(`ui.overlays`)+ 可选主题色。 */ ui?: GameScenarioUi; /** 用户自定义文字预设(内置在 text-style.ts;这里只存用户新建的,按 subtitle/overlay 分组)。 */ textStylePresets?: { subtitle?: GraphTextStylePreset[]; overlay?: GraphTextStylePreset[]; }; /** 文档默认床轨;缺省 = 静音起局。与 `NodeData.bgm` 一起构成 BGM 配置的唯一两处 SSOT。 */ bgm?: DocumentBgm; graph: GameGraph; } /** 编辑器界面目录 sidecar;scheme 只引用 `ui.overlays`,不复制标题或 Overlay 内容。 */ type UiTreeNode = UiTreeFolderNode | UiTreeSchemeNode; interface UiTreeFolderNode { kind: 'folder'; id: string; name: string; children: UiTreeNode[]; } interface UiTreeSchemeNode { kind: 'scheme'; id: string; overlayId: string; } interface UiTree { root: UiTreeNode[]; } /** 蓝图文档:一张独立可存取的图(主蓝图或子蓝图),与 `SubFlowPackDef` 同级但语义面向新蓝图库。 */ interface BlueprintDoc { id: string; title: string; /** 子蓝图内容版本(对齐 `SubFlowPackDef.version` / `subFlowPack` 钉死);主蓝图可缺省。 */ version?: string; entry: string; graph: GameGraph; requires?: { vars?: string[]; entities?: string[]; }; } /** * 嵌在 scenario 根上的蓝图库(单文件 SSOT 的增量字段)。 * `packs` 含主蓝图 + 全部子蓝图完整文档(与 `subFlowPack` 用语对齐); * 根上的 `graph` 与 `packs[mainPackId].graph` 同步(双源,便于编辑库与运行入口共用)。 */ interface BlueprintManifest { version: 'wb-game-video.blueprint-manifest.v1'; mainPackId: string; packs: Record; } /** * 完整落盘/编辑文档 = 原 `GameScenario`(graph/variables/entities/…)+ `manifest`。 * 不再使用独立的 sharedMeta / blueprints/ 文件夹。 */ type GraphLibraryDocument = GameScenario & { /** * 服务端权威文档修订号,由 Host 在写盘边界戳入(见 `src/server/host/document-revision.ts`)。 * 供 `expectedRevision` 乐观锁使用;不是内容字段,编辑器纯函数不产生也不消费它。 * `normalizeDocument` 会重建文档并丢弃本字段——这是有意的,只有持久化边界关心它。 */ revision?: number; formulas?: Record; uiTree?: UiTree; manifest: BlueprintManifest; }; interface EmptyProject { id: string; title: string; platform: 'wb-game-video'; platformVersion: '1'; entry: { blueprint: 'blueprint.json'; components: 'dist/components'; }; } interface EmptyLibrarySeed extends Record { project: EmptyProject; blueprint: GraphLibraryDocument; assetsManifest: { version: 2; assets: []; }; } declare function createEmptyLibrarySeed(context: { gameId: string; }): Promise; declare function validateEmptyLibrarySeed(seed: unknown): asserts seed is EmptyLibrarySeed; interface NodiaBundledVideoAsset { id: string; kind: 'video'; productionType: 'bundled_video'; status: 'ready'; file: { provider: 'extension'; key: string; mime: 'video/mp4'; }; } interface NodiaAssetsManifest { version: 2; assets: NodiaBundledVideoAsset[]; } interface NodiaProject { id: 'nodia'; title: 'Nodia'; platform: 'wb-game-video'; platformVersion: '1'; entry: { blueprint: 'blueprint.json'; components: 'dist/components'; }; } interface NodiaSeed extends Record { project: NodiaProject; blueprint: GraphLibraryDocument; assetsManifest: NodiaAssetsManifest; } /** Returns a fully independent, extension-owned seed; video bytes remain in the extension bundle. */ declare function createNodiaSeed(): Promise; /** Fail loudly before a package is materialized; it never manufactures a fallback manifest. */ declare function validateNodiaSeed(seed: unknown): asserts seed is NodiaSeed; /** * Manifest-facing tool adapters. * * Each handler receives a capability-bounded host context from the Workbench * runtime and forwards its published schema input to the shared service. */ declare const tools: Record; declare const host: _forgeax_workbench_host_node.WorkbenchExtensionModule; export { createEmptyLibrarySeed, createNodiaSeed, host as default, host, tools, validateEmptyLibrarySeed, validateNodiaSeed };