# 游戏接入平台

适用于 HTML、CSS、JavaScript 构成的静态 HTML5 游戏。平台分发已构建的文件，通过 iframe 外壳提供身份展示、侧栏和防 Boss。游戏自己的运行、音频和进度保存仍由游戏实现。

游戏构建环境由游戏项目自行决定，包括 Node.js 26.5.0 与 pnpm 11.17.0。平台只托管构建后的静态网页文件；需要常驻 Node 服务、服务端渲染或私有后端的项目，应先导出为静态资源或另行部署后端。上传可使用[独立 MCP 客户端](/docs/mcp)，无需平台源码。

## 文件与来源

将[游戏适配器](/youxi-game-v1.js)下载后复制到游戏发布目录，使用相对路径加载。平台不会替游戏运行构建命令；入口 HTML 及其引用的资源都需要包含在提交目录或 ZIP 中。

正式平台来源固定为 `https://scalia.ltd`。游戏文件来自 `https://game-游戏UUID.scalia.ltd`，候选预览来自 `https://preview-候选UUID.scalia.ltd`。同款游戏使用稳定独立来源，版本入口位于 `/v/版本UUID/入口路径`，以便更新后继续访问同源浏览器存档。各游戏和平台的存储彼此隔离。

详情链接为 `/games/slug`，游玩外壳为 `/play/slug`。分享详情或游玩外壳链接，不分享私有候选、凭据或带个人数据的链接。首页和详情不提前下载游戏包。

## 最小接入示例

下面的回调需要连接到游戏现有的身份、场景和音频系统。顶层单独打开游戏或私有文件预览时不建立父窗口连接，保留游戏原本的启动逻辑。

```html
<script src="./youxi-game-v1.js"></script>
<script>
const platform = window.parent !== window ? YouxiGame.connect({
  platformOrigin: 'https://scalia.ltd',
  onIdentity(identity) {
    updatePlayerDisplay(identity);
  },
  onState({ sidebar, boss }) {
    setPlatformScene({ sidebar, boss });
  },
  setTemporaryMuted(muted) {
    setPlatformAudioMute(muted);
    return true; // 仅在实际同步覆盖全部音源后返回 true
  },
  isRunning() { return gameIsRunning(); }
}) : null;

// 在资源、场景与玩法均已准备好后调用，而不是刚加载脚本时。
// 放入你自己的游戏加载完成回调：
function onGameReady() {
  platform?.ready();
}

// 游戏中的平台入口按需调用：
// platform?.open('sidebar');
// platform?.open('login');
// platform?.open('instructions');
// platform?.open('settings');
// 游戏彻底结束时可 platform?.destroy()。
</script>
```

`updatePlayerDisplay`、`setPlatformScene`、`setPlatformAudioMute` 和 `gameIsRunning` 是示意函数，需要由游戏实现。不要从收到的消息、查询参数或不可信输入自动取 `platformOrigin`。本地开发时才改成对应本地平台来源，例如 `http://127.0.0.1:53200`。

## 玩家身份与存档

`onIdentity` 收到游客或成员身份。成员的 `user` 只含 `id`、`nickname`、`avatarId`、`revision`。登录、退出或资料变化可在游戏运行中更新，无需重新加载。

这些资料仅用于展示，不能作为游戏后端登录凭证。适配器不提供邮箱、管理员角色、网页 Cookie、CSRF 或 MCP 凭据；当前没有统一云存档和游戏后端身份令牌。各游戏自行保存进度，平台不调用游戏存档接口或回放旧快照。

真实可玩后调用 `ready()`，平台才会确认最近玩过。游客记录保存在该浏览器，登录后不会自动合并到账号记录。游戏加载失败或未完成 ready 不应产生一次成功游玩记录。

## 运行与音频

为手动暂停、游戏菜单、平台侧栏和防 Boss 分别维护暂停原因。恢复平台遮盖时只移除对应原因，保留玩家原有的暂停状态。增量游戏可以按自身玩法继续结算经过时间；浏览器可能限制后台计时，时间修正由游戏自行处理。

音频需要独立维护“用户音量/静音”和“平台临时静音”。Web Audio 应统一接入输出 GainNode，HTMLAudio、HTMLVideo 及第三方播放器也要接入中央音频控制。之后新创建的音源在播放前同样应用临时静音。

`setTemporaryMuted` 必须同步完成静音后返回 `true`。Promise 不算成功回执。平台能立即遮盖 iframe 画面，但实际静音依赖游戏适配，必须逐款验收。

## 消息协议 v1

适配器发送 `game.hello`，平台按来源、父子窗口和当前实例完成握手。iframe 每次加载产生新实例；旧实例消息不生效。常规业务消息信封如下：

```json
{
  "protocol_version": 1,
  "instance_id": "11111111-1111-4111-8111-111111111111",
  "request_id": "22222222-2222-4222-8222-222222222222",
  "type": "game.ready",
  "payload": {}
}
```

| 方向 | 消息 | 用途 |
| --- | --- | --- |
| 游戏 → 平台 | `game.ready` | 资源和玩法已可用，确认当前启动 |
| 游戏 → 平台 | `platform.open` | 打开 sidebar、login、instructions 或 settings |
| 游戏 → 平台 | `shortcut.triggered` | 转发 sidebar、enter 或 exit |
| 游戏 → 平台 | `boss.ack` | 报告实际 muted 和 running/paused 状态 |
| 平台 → 游戏 | `platform.init` | 初始身份、侧栏/防 Boss 状态和偏好 |
| 平台 → 游戏 | `identity.changed` | 更新游客或有限成员资料 |
| 平台 → 游戏 | `sidebar.changed` | 更新侧栏和防 Boss 状态 |
| 平台 → 游戏 | `boss.enter` / `boss.exit` | 进入或退出遮盖 |
| 平台 → 游戏 | `preferences.changed` | 更新本机侧栏位置和快捷键 |

游戏侧业务消息上限 4 KiB，请求去重保留最近 1024 个 UUID。使用适配器即可完成来源、窗口、实例和结构校验；不要改成通配 `postMessage('*')`。游戏消息不能直接触发平台管理、上传或发布。

## 快捷键与预览

默认 Alt+M 展开或收起侧栏，Alt+Q 进入防 Boss，Alt+Shift+Q 恢复；macOS 对应 Option。玩家可在设置中修改，适配器会接收新偏好。进入是幂等操作，不通过同一组合键切回。输入框内不触发侧栏快捷键，但进入和恢复防 Boss 仍有效。

侧栏覆盖游戏，iframe 尺寸不变。全屏由平台外壳统一控制，游戏不能单独全屏、顶层跳转或弹窗。私有候选预览在独立顶层页面打开；它用于检查文件，不等同于完整外壳联调。正式发布后仍需验证游玩外壳里的身份、侧栏、快捷键和音频。

## 上架前检查

- 确认入口和相对资源完整；资源准备成功后才 ready，失败不记成功游玩。
- 登录、退出、更新身份、展开侧栏和防 Boss 均不重载游戏。
- 遮盖期间所有已有和新音源都静音，恢复后保留用户原音量与静音设置。
- 原有手动暂停不被恢复操作清空；增量游戏进度不回滚。
- 游戏有输入焦点、鼠标锁定和外壳全屏时，快捷键与恢复均正常。
- 在目标电脑浏览器和真实手机检查触控、系统快捷键及浏览器存档。

下一步见[上传与发布](/docs/publishing)。
