# @forgeax-extension/game-video

玩法优先的视频游戏蓝图编辑器与运行时。它把视频演出、血条、QTE、限时选择和热点交互组合成可序列化的 `GraphLibraryDocument`，由纯 TypeScript 状态机确定性执行。

## 当前契约

- 图文档权威文件是宿主绑定游戏工作区内的逻辑路径 `blueprint.json`；首次保存同时补齐
  `project.json`。物理目录布局由宿主 workspace adapter 决定。
- 未保存草稿留在浏览器 localStorage。未初始化项目由 `GameBootstrap` 引导宿主 `initialize`；
  首次 initialize 默认写入 **empty library**（单 `entry` 节点空壳）。已初始化 package 读取失败会进入可重试错误页，不会自动
  写入空蓝图覆盖原文件。
- 游戏身份只来自宿主：后端读取 `ExtensionContext.gameId`，浏览器等待 nonce-bound
  handshake 后读取 `ExtensionClient.ready()` 返回的 `gameId`。AI 工具都不接受调用者提供的
  `gameSlug`。
- 游戏支柱、总脉络与整装共享 `activityContract.blueprintCapabilities`：上游据此设计可实现的互动，下游使用
  `game-video:configure-blueprint-node` 原子增删改单节点的界面、事件响应、连线与结算；卸载界面、删除结算或
  解除界面绑定时复用节点配置面板相同的级联语义。图拓扑和未覆盖字段使用
  `game-video:patch-graph` 原子增量改图；新增、删除、前后插入、复制、移动节点以及新增、删除、改目标连线
  都落到画布共用的 `authoring` 领域方法。任一步或整本校验失败时都不写盘。
  `game-video:save-graph` 保留给编辑器 UI 覆盖保存整份文档，不向 AI 暴露。
  可玩蓝图交付后，作者仍可通过对话明确要求增删改节点、连线、界面、结算和规则；Host 不重开 workflow，
  但仍执行对象定位后的领域工具写入、revision/幂等保护和当前版本复验。
  节点事务任务、阶段权限、前端配置面板映射、完整原子方法调用链与验收指标见
  [`docs/BLUEPRINT_NODE_ATOMIC_CONFIGURATION.md`](./docs/BLUEPRINT_NODE_ATOMIC_CONFIGURATION.md)。
- 前端和 Agent 的持久化能力由
  [`src/workflow/authoring-capability-registry.ts`](./src/workflow/authoring-capability-registry.ts) 统一登记，并自动派生
  `blueprintCapabilities.operations[]` 给运行时 Agent。`bun run check:authoring-capabilities` 会审计 schema op、
  UI/Agent 入口、共享领域原语、公开工具和 parity 测试，避免产品改版后双端能力静默漂移。
- 支柱通过 `pillar-interaction-contract` 定义互动节拍和战斗回合；总脉络用 beat/action/settlement 三级 ID、
  `edge.data.design.producer/outcomeEvidenceId` 和目标节点 `outcomeEvidence` 编译成节点/边。outline 完成后 Host
  冻结这层设计，整装只落地界面、状态、反馈和结算，无权临场发明玩法。状态生命周期质量门与 GraphSession
  多种子、逐 producer 路径回放共同保证完整因果链。
- 可序列化运行内核位于 [`src/runtime/core`](./src/runtime/core)，React 渲染层位于
  [`src/runtime/react`](./src/runtime/react)；图中只保存组件 id 与可序列化输入。
- 扩展提供 manifest 驱动的 host 工具：增量改图、规则目录、图读取、项目基础控件生成、
  镜头脚本/关键帧/视频生成、素材查询和角色/场景引用导入。完整调用契约见 [`SKILL.md`](./SKILL.md)。

## 本地开发

本仓是独立仓库，使用 Bun：

```bash
git clone https://github.com/ForgeaXGame/forgeax-game-video.git
cd forgeax-game-video
bun install --frozen-lockfile
bun run dev
bun run test
bun run lint
bun run build
```

`bun run build` 依次跑 frontend、backend、`build:standalone` 与 `check:release`。
Player 播放页源码位于 `src/player/`，产物统一写入根目录 `dist/player/`；单独构建或预览：

```bash
bun run build:standalone
bun run start:standalone
```

`bun run dev` 启动 Vite 开发适配器（独立运行时默认 `15185`；Studio launcher 可通过
`VITE_DEV_PORT` 注入端口）和后端 watch。适配器只挂载
`/__extension__/v1` 的标准 Extension HTTP 契约；用宿主 iframe 的 nonce-bound
handshake 注入 game id、runtime id 和端点后再打开编辑器。它不提供旧的兼容业务路由。
本地游戏包保存在被忽略的 `.extension-dev/games/<gameId>/`，首次 `initialize` 时由
  扩展的 empty library seed 创建 `project.json`、`blueprint.json` 与 `assets/manifest.json`。
`bun test` 是无 DOM 的 server/release-contract gate；浏览器、React 与 Vite 覆盖使用
完整的 `bun run test`（Vitest）。

`@forgeax/extension-host@0.2.6` 通过 registry 安装。开发和 CI 使用
`bun install --frozen-lockfile`，以 `bun.lock` 固定已发布的 Host 契约；不需要也不应配置本地
tarball、路径 override 或 vendored provenance。

### Media contract release dependency

浏览器素材库使用 Host 的 `/games/:gameId/media` 可恢复上传和元数据 API；扩展不再提供
`assets/game-video-media.json` 或 `media/resources` 生命周期。该契约由
`@forgeax/extension-host@0.2.6` 发布并以 registry tarball + integrity pin 固定。

## 宿主集成

发布包要求精确 peer：`@forgeax/extension-platform@0.0.3` 与
`@forgeax/extension-host@0.2.6`。包导出：

- `@forgeax-extension/game-video` / `./host` — Extension 扩展 host（seed、工具、router）
- `@forgeax-extension/game-video/standalone` — player 播放页 HTML（`dist/player/index.html`）
- `@forgeax-extension/game-video/styles.css` — 编辑器样式

### 游戏组件模块契约

游戏专属组件由 `gameComponents.moduleUrl('index.js')` 提供的模块注册。模块导出
`register(host)`（也接受默认导出的函数或带 `register` 的对象），其中 `host` 只提供：

- 共享的 `React` 实例；
- `registerComponent(id, manifest)`；
- `registerOverlayRenderer(id, component, manifest?)`。

```ts
export function register(host) {
  host.registerComponent(manifest.id, manifest)
  host.registerOverlayRenderer(manifest.id, Component, manifest)
}
```

可序列化 manifest 位于
[`src/runtime/core/component-catalog`](./src/runtime/core/component-catalog)，React 叶子组件位于
[`src/runtime/react/component-host/components`](./src/runtime/react/component-host/components)。二者使用
[`LocalComponentManifest`](./src/runtime/core/component-catalog/manifest.ts) 描述包内契约；转为平台
`ComponentDef` / `ComponentManifest` 的类型边界只位于
[`component-host/index.ts`](./src/runtime/react/component-host/index.ts)。叶子组件不应直接依赖 editor、
engine/session 状态或平台 schema 来解释作者输入。

PR #141 后不再存在 `registerInteractionSkin`、`registerHpBar`，也不再提供
`interactionSkins()`、`hpBarComponents()`、`skinPositioning()`、`skinDefaultAnchor()` 这类并行元数据
访问器；组件发现以 manifest 与 renderer 注册为唯一入口。`src/runtime/react/index.ts` 导出
`bootComponents`、`createCoreSkinRegistry` 与 `createDefaultComponentRegistry`；
`src/runtime/core/component-catalog/index.ts` 只提供无 React 的 manifest 列表与安装器。不再提供
`registerCoreSkins`、`newComponents` 或 `installNewComponents`。

该组件目录是仓内内建 catalog，不是受支持的 npm 子路径。公开包入口仅以上述根入口、`./host`、
`./styles.css`、`./standalone` 为准。

其中的 `host` 提供游戏包 seed、35 个工具和扩展 HTTP router。生产宿主负责加载它，并为每个已解析的游戏
创建唯一的 `ExtensionContext`：

```ts
import { host as videoGameExtension } from '@forgeax-extension/game-video/host'
import { createExtensionContext } from '@forgeax/extension-host/node'

const response = await workspace.withGameRoot(
  resolvedGame.id,
  { create: false, versioning },
  async (scope) => {
    const context = createExtensionContext({
      gameId: resolvedGame.id,
      gameRoot: scope.gameRoot,
      files: scope.files,
      media: hostMedia,
      models: hostModels,
      videoGeneration: hostVideoGeneration,
      services: hostServices,
    })
    const router = videoGameExtension.createRouter?.(context)
    if (!router) throw new Error('game-video router is unavailable')
    const routed = await router.handle(request)
    return {
      ...routed,
      ...(routed.body ? { body: new Uint8Array(routed.body) } : {}),
    }
  },
)
```

上面的 context 构造必须发生在
`workspace.withGameRoot(resolvedGame.id, { create: false, versioning }, async (scope) => …)`
回调内；router 构造、请求处理与响应字节复制也必须在该回调返回前完成。不得根据
`gameRoot` 路径临时构造 files，也不得在 scope 关闭后保留 context。
`gameId` 与 `scope.gameRoot` 在进入扩展前就由宿主解析完成。扩展后端只使用 context 注入的能力：

- `files` 提供限定在游戏根内的读写、目录枚举和跨进程 `withLocks`；
- `media` 提供素材读写、幂等落盘与回收；
- `models` 提供文本、图片和视频生成；
- `videoGeneration` 是宿主的视频生成 job facade；
- `services` 是宿主限定范围的服务访问 facade；
- `gameId` 是工具调用和 HTTP router 的唯一游戏身份。

扩展不会再适配任何宿主产品专用的请求形状，也不会读取进程环境、全局 active-game 文件或
请求中的 `gameSlug` 来选择游戏。

浏览器端在初始化前必须等待 `createExtensionClient().ready()`。这次 nonce-bound handshake
返回精确的 `gameId`、`runtimeId`、capability 列表和宿主端点；浏览器不得从 URL query、
location 或默认 slug 推导这些值。包读写和扩展请求分别使用 `gamePackage` 与
`extension.fetch()`。版本入口仅在 `versions.supported()` 为 true 时显示，组件模块仅使用
`gameComponents.moduleUrl()` 返回的 handshake 端点；缺少相应 capability 时按“不支持”处理，
不得拼接备用 URL。

### 发布顺序

发布必须按以下顺序：

1. 先发布已经过评审的 `@forgeax/extension-host@0.2.6`；
2. 从 registry 验证其类型与能力契约，并更新 `bun.lock`；
3. 完成 frozen install、测试、构建和 pack 检查后，最后发布当前 manifest 中声明的扩展版本。

## 代码导航

| 关注点 | 位置 |
|---|---|
| 图 schema 与校验 | [`src/runtime/core/schema/graph-schema.ts`](./src/runtime/core/schema/graph-schema.ts) · [`src/runtime/core/validate/validate.ts`](./src/runtime/core/validate/validate.ts) |
| 状态机与 session | [`src/runtime/core/engine`](./src/runtime/core/engine) |
| manifest catalog | [`src/runtime/core/component-catalog`](./src/runtime/core/component-catalog) |
| React 覆盖组件与播放适配 | [`src/runtime/react`](./src/runtime/react) |
| 蓝图、规则与场景纯创作逻辑 | [`src/authoring`](./src/authoring) |
| 蓝图持久化能力注册表 | [`src/workflow/authoring-capability-registry.ts`](./src/workflow/authoring-capability-registry.ts) |
| 编辑器壳、画布与持久化 | [`src/editor`](./src/editor) |
| 顶层独立播放器 | [`src/player`](./src/player) |
| Player 本地开发样例游戏包 | [`game-package`](./game-package) |
| AI 工具后端 | [`src/server/tool-handlers.ts`](./src/server/tool-handlers.ts) |
| 扩展声明 | [`forgeax-extension.json`](./forgeax-extension.json) |

仓库只有一个源码根 `src/` 和一个发布产物根 `dist/`。`src/` 按业务领域分层，editor、backend 与
player 的构建结果分别进入 `dist/`、`dist/server/` 与 `dist/player/`；源码树内不保存构建产物。

历史设计记录位于 [`docs/superpowers`](./docs/superpowers)；代码、manifest、schema 与本页是当前发布契约。

## License

MIT
