Files
pixelheros/docs/superpowers/specs/2026-06-20-hero-skill-config-editor-design.md
panFD d8f02b568b docs(config-editor): add design spec for hero/skill visual config editor extension
Schema-driven Cocos Creator 3.8.6 extension that round-trips the existing
Record<number,X> .ts configs via the TypeScript compiler API (preserves
symbolic AtkSpeedSet expressions and hand-written comments). Non-invasive:
zero changes to game runtime code.
2026-06-20 22:59:20 +08:00

352 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 通用英雄/技能配置编辑器Cocos Creator 扩展)— 设计规格
- 日期2026-06-20
- 作者brainstorm 协作产出
- 目标引擎Cocos Creator **3.8.6**
- 部署位置:`extensions/pixelhero-config-editor/`
- 关联配置源:`assets/script/game/common/config/{heroSet,SkillSet,HeroAttrs,HeroSkillDesc}.ts`
---
## 1. 概述Overview
构建一个 Cocos Creator 编辑器扩展,提供**可视化**的英雄与技能配置编辑能力。扩展以 **schema 驱动 + TypeScript AST 往返**为核心:直接读写现有 `Record<number,X>` 形态的 `.ts` 配置文件,**不改动任何游戏运行时代码**。通过声明式 schema 描述每张表的字段、枚举、引用关系UI 由 schema 自动生成,从而实现"通用"——未来新增驻场技能表、卡牌表等只需追加 schema无需编写新 UI。
## 2. 目标与非目标
### 目标
- 可视化编辑 `HeroInfo`(英雄 50xx + 怪物 60xx/61xx`SkillSet`(技能 60xx/63xx/64xx/65xx`FieldSkillSet`(驻场 70xx/72xx/74xx三张表。
- 原地回写对应 `.ts` 文件,**保留符号表达式**(如 `AtkSpeedSet[AtkSpeedLv.Slow3].cd`)与手工注释/分节标题。
- 异构触发槽、技能引用覆盖(`SkillOverrides`)、进化配置的可视化编辑。
- 实时校验 + 实时描述预览(与游戏内 `buildSkillDesc` 一致)。
- 新建/复制/删除/保存/还原,并保持 `HeroList``HeroInfo` 一致。
- 纯逻辑层schema/IO/校验)有自动化单元测试,作为 BLOCKING 证据。
### 非目标v1
- 不重构游戏代码、不把数据迁移到 JSON。
- 不做运行时热重载游戏逻辑(仅通过 asset-db 刷新让编辑器与编辑器内预览生效)。
- 撤销/重做Undo/Redo列为 v1.1。
- 不编辑 `CardSet / HighlightSet / GameSet / ScoreSet`(架构允许后续以新增 schema 方式扩展,但不在本次范围)。
- 不做多人协作/版本对比。
## 3. 背景事实(已核实)
| 事实 | 影响 |
|---|---|
| 引擎 Cocos Creator 3.8.6 | 使用 `package_version:2` 扩展格式;面板经 `Editor.Panel.define({...})`;消息经 `contributions.messages` |
| 配置运行时只读、仅按 key 访问、无动态加载 | 回写安全;输出只需是合法 TS 且 `HeroList` 一致 |
| `skills[n].cd` 为符号表达式 | IO 必须基于 TS Compiler API识别并保号往返 |
| 现有 `oops-plugin-framework` 仅运行时框架 | 无面板示例可抄;需自建打包 |
| `HeroList` 被运行时迭代(`CardSet.ts` | 写英雄表后必须同步 `HeroList = 排序后的英雄(HERO) uuid` |
| `HeroSkillDesc.buildSkillDesc` 生成游戏内描述 | 移植为 JS 用于面板实时预览 |
## 4. 架构(五层)
```
┌────────────────────────────────────────────────────────────┐
│ UI 层 Vue3 面板 (dist/panels/default.js) │
│ master-detail + 嵌套编辑器 + 校验面板 + 描述预览 │
└───────────────────────────────┬────────────────────────────┘
Editor.Message.request │ broadcast 'record-changed'
┌───────────────────────────────┴────────────────────────────┐
│ 主进程 dist/main.js 内存真理源 + 消息处理 + 广播 │
└──────┬──────────────────────┬──────────────────────┬───────┘
│ │ │
┌──────▼───────┐ ┌───────────▼──────────┐ ┌─────────▼────────┐
│ 校验层 │ │ Schema 注册表 │ │ IO 层 (TS 往返) │
│ validate() │ │ tables/fields/enums │ │ TsConfigFile │
│ → Issue[] │ │ → 驱动 UI 生成 │ │ load/patch/save │
└──────────────┘ └──────────────────────┘ └──────────┬────────┘
│ 写回 .ts
Editor.Message.request('asset-db','refresh-asset')
```
层次依赖单向UI → 主进程 → {校验, schema, IO}。校验与 schema 为纯逻辑可独立测试。IO 依赖 `typescript` 包。
## 5. 组件详设
### 5.1 Schema 注册表(`src/shared/schema/`
"通用"的核心。每个数据表用一份 `TableSchema` 描述:
```ts
interface TableSchema {
id: 'hero' | 'skill' | 'field'; // 表标识
label: string; // "英雄/怪物"
sourceFile: string; // 相对 assets 配置目录,如 'heroSet.ts'
exportName: string; // 'HeroInfo' | 'SkillSet' | 'FieldSkillSet'
keyType: 'number';
idSegments: { label: string; min: number; max: number; note?: string }[];
listExportName?: string; // 仅 hero'HeroList'(需同步)
fields: FieldSchema[]; // 记录字段(顺序即 UI 顺序)
}
interface FieldSchema {
key: string; // 字段名(对应 .ts 对象键)
label: string; // 中文标签
type: FieldType; // 见下
required?: boolean;
default?: unknown;
group?: string; // UI 分组("基础"/"触发技能"/...
help?: string;
// type 相关的可选元数据:
enumRef?: string; // type=enum → enums.ts 中的枚举键
refTable?: TableId; // type=ref → 引用哪张表
overlayKeys?: string[]; // type=overrides → 可覆盖键集合
showIf?: { field: string; in: unknown[] };// 条件显示
}
type FieldType =
| 'number' | 'string' | 'boolean'
| 'enum' // 下拉,选项来自 enumRef
| 'ref' // 引用另一表的 uuid下拉显示 目标.name
| 'speedExpr' // 攻速符号表达式,下拉=AtkSpeedLv 档位
| 'skillMap' // Record<number,HSkillInfo>(英雄专用)
| 'triggerSlots' // 6 种数组触发槽(英雄专用)
| 'fieldList' // number[] 驻场技能列表(英雄专用)
| 'reviveSlot' // 单对象 {s_uuid,r_num,upr}(英雄专用)
| 'overrides'; // SkillOverrides 覆盖层(出现在 triggerSlots 内部)
```
**枚举源 `src/shared/schema/enums.ts`**:镜像游戏枚举为 `{label,value}[]` 字典——`HType, FacSet, TGroup, DTType, SkillKind, DType, IType, RType, EType, FieldSkillType, Attrs(buff_type), AtkSpeedLv`。此文件为编辑器侧单一事实源;并提供 `assertEnumsMatchGame()` 调试期检查(读取游戏 `.ts` 枚举定义比对,不一致则告警),避免漂移。
**三张表的字段清单v1**
- **hero (`HeroInfo`)** — 分组:
- 基础:`uuid`(number,必填), `name`(string,必填), `path`(string,必填), `icon`?(string), `fac`(enum FacSet,必填), `pool_lv`?(number), `lv`(number,必填,默认1), `type`(enum HType,必填), `hp`(number,必填), `ap`(number,必填), `dis`?(number), `speed`?(number), `info`(string,必填)
- 技能:`skills`(skillMap,必填), 触发槽组 `call/dead/fstart/fend/atking/atked`(triggerSlots), `field`(fieldList), `revive`(reviveSlot), `evolve`(evolveMap — v1 只读展示,标注"v1.1 编辑")
- ID 段:英雄 [5000,5999];怪物 [6000,6999]6101-6106 为 Boss
- **skill (`SkillSet``SkillConfig`)**
- 基础:`uuid`(number,必填), `name`(string,必填), `sp_name`(string,必填), `icon`(string,必填), `act`(string,必填), `info`(string,必填)
- 目标/类型:`TGroup`(enum,必填), `DTType`(enum,必填), `IType`(enum,必填), `RType`(enum,必填), `EType`(enum,必填), `kind`?(enum SkillKind), `DType`?(enum,默认 ATK)
- 数值:`ap`(number,必填), `gold`?(number), `hit_count`(number,必填), `hitcd`(number,必填), `speed`(number,必填), `ready`(number,必填), `with`(number,必填,默认0)
- 动画/特效:`readyAnm`,`endAnm`,`DAnm`(string), `EAnm`(number)
- 高级:`crt?`,`stun?`,`frz?`,`bck?`(number), `buff_type`?(enum Attrs), `call_hero`?(ref hero), `time`?, `bezier_start_y?`,`bezier_mid_y?`,`bezier_arc?`(number)
- ID 段6001-6999
- **field (`FieldSkillSet``FieldSkillConfig`)**
- `uuid`(number,必填), `name`(string,必填), `icon`(string,必填), `type`(enum FieldSkillType,必填), `value`(number,必填), `info`(string,必填)
- ID 段7001-7999
> 字段清单来源 = 直接对照 `heroSet.ts`/`SkillSet.ts` 的 interface 定义,已逐字段核对类型与必填性。
### 5.2 IO 层(`src/main/io/TsConfigFile.ts`
基于 `typescript`npm在扩展主进程 Node 上下文中 `require`)。
```ts
class TsConfigFile {
load(file, exportName): void; // 解析并缓存 SourceFile + 目标 VariableDeclaration
getKeys(): number[]; // 该 const 的所有键
read(key): RecordValue; // AST → 结构化值
patch(key, value: RecordValue): void; // AST 区间替换该条目
add(key, value): void; // 在 const 末尾插入新条目
delete(key): void; // 删除条目区间
serialize(value): string; // 单条目确定性序列化
save(): { ok: boolean; error?: string }; // 写 .bak → 校验可解析 → 落盘 → 触发 asset-db refresh
reload(): void;
}
```
**值模型 `RecordValue`**
- 标量:`{kind:'num',value}` / `{kind:'str',value}` / `{kind:'bool',value}`
- 符号表达式:`{kind:'speed', level:'Slow3'}` ⇄ 源 `AtkSpeedSet[AtkSpeedLv.Slow3].cd`AST 形态:`PropertyAccessExpression(ElementAccessExpression(Ident,ElementAccessExpression(Ident,Ident)),Ident)`,固定匹配该模式;不匹配则降级为 `{kind:'num',value:<节点文本>}` 并标记需人工确认)
- 对象:`{kind:'obj', props: {key: RecordValue}}`
- 数组:`{kind:'arr', items: RecordValue[]}`
**回写策略 = 条目级 AST 区间替换**
1. 解析文件 → 定位目标 `exportName``ObjectLiteralExpression`
2. 在其中按 key 定位单个 `PropertyAssignment` 的完整文本区间(含其尾随逗号)。
3. 对该条目调用 `serialize()` 生成新文本。**确定性格式(固定,无歧义)**受控多行对象字面量每字段一行、4 空格缩进、字段顺序按 schema `fields` 顺序、尾随逗号;`cd` 用符号形式 `AtkSpeedSet[AtkSpeedLv.X].cd``info` 非空时在条目上方输出一行 `// {info}` 注释。示例:
```ts
// 每受击3次为自身添加4层护盾
5011:{uuid:5011,name:"小铁卫",path:"hk1",fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:400,ap:20,
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
atked:[{s_uuid:6301,t_num:3,overrides:{TGroup:TGroup.Self,ap:4}}],
info:"每受击3次为自身添加4层护盾"},
```
4. 用新文本替换该区间;其余条目、分节注释、其他 const 原样不动 → **diff 最小**。
5. 新增:在 `}` 前插入;删除:移除区间。
6. 落盘前:`createProgram` 对改动文件做语法检查;失败则回滚 `.bak` 并报错。
7. 落盘后:`await Editor.Message.request('asset-db','refresh-asset', url)`,使编辑器重新导入该脚本。
**降级与安全**
- 任一步骤异常 → 不写文件,返回结构化错误,面板展示。
- 始终先写 `*.bak`;校验通过后再覆盖原文件。
- 若条目内出现未识别的初始化表达式(非上述 RecordValue 形态),该条目标记为"只读(含未支持表达式)",可编辑其他条目但不改动它,避免破坏。
### 5.3 校验层(`src/shared/validation/`
纯函数 `validate(tableId, allRecords): Issue[]`,每次改动后对受影响表运行。规则:
| 规则 | 级别 |
|---|---|
| uuid 在表内唯一 | error |
| uuid 落在该实体声明的 idSegments 内 | error |
| 必填字段非空 | error |
| enum 字段值 ∈ 枚举集合 | error |
| ref 字段目标在引用表中存在(英雄触发槽/技能图引用 → SkillSetfield 列表 → FieldSkillSetcall_hero → HeroInfo | error |
| overrides 仅含 `SkillOverrides` 允许键TGroup,ap,gold,hit_count,hitcd,crt,frz,stun,bck,buff_type,call_hero | error |
| 英雄表:`HeroList` 与 `HeroInfo` 一致(每 HeroList 项存在且 fac=HERO每 fac=HERO 条目都在 HeroList | error |
**`HeroList` 同步策略(最小 diff**:保留现有数组顺序与分节注释,仅在新增英雄时把新 uuid 追加到数组末尾、删除英雄时移除其 uuid。**不重排、不重生成**,以避免破坏手工分节注释。同步后再次运行一致性校验。
| 怪物无 `pool_lv`/`evolve`(语义警告) | warn |
| `info` 文案长度 >0 | warn |
`Issue = {tableId, key, fieldPath, severity:'error'|'warn', code, message}`。面板据此在列表行显红点、字段内联报错;存在 error 时禁用"保存"。
### 5.4 主进程(`src/main/index.ts`
扩展入口。在内存持有三张表的 `TsConfigFile` 实例与 schema 注册表,作为唯一真理源。处理消息(见 §7。任何写入先校验error 则拒绝并返回 Issue 列表;成功后广播 `record-changed {tableId, key}`,所有打开面板据此刷新。
### 5.5 UI 层(`src/panels/default/`Vue 3
- **打包**`esbuild` 将 `src/panels/default/index.ts`(含 Vue 3 runtime+compiler打成单文件 `dist/panels/default.js`。Vue 用**字符串模板**`compiler` 在线编译),避免 SFC 工具链。
- **面板入口**`Editor.Panel.define``template`=`<div id="app"></div>``ready()` 中 `createApp({…}).mount(this.$.app)``close()` 卸载。
- **布局**:左 master表切换 + 搜索 + 列表),右 detailschema 驱动表单 + 嵌套编辑器 + 预览),底部校验条。
- **控件映射**number→`<ui-num-input>`string→`<ui-input>`boolean→`<ui-checkbox>`enum→`<ui-select>`ref→`<ui-select>`(选项=目标表 `{uuid:name}`,空选项=清除speedExpr→`<ui-select>`(选项=AtkSpeedLv 档位,含"自定义数值"兜底)。
- **嵌套编辑器**
- `TriggerSlotsEditor`:对 6 种触发类型,每组可增删行 `{s_uuid(技能 ref 选择器), t_num(number), overrides(可折叠)}`行内显示该技能基础信息name/kind
- `SkillMapEditor`:英雄 `skills`;每项 `uuid + lv + cd(speedExpr)`;至少 1 项index 0=普攻)。
- `FieldListEditor``field:number[]` 多选驻场技能。
- `ReviveEditor``revive` 单对象 `{s_uuid,r_num,upr}` 或空。
- `OverrideEditor`:依据所选基础技能 `kind`,仅渲染相关覆盖键(如 Damage→ap/hit_count/crt/stun…Support→TGroup/ap/buff_type…
- **实时预览**`PreviewPane` 调主进程 `query-preview-desc`(移植 `buildSkillDesc`),随编辑即时刷新。
- **图标/特效预览**`icon`、`sp_name` 变更时 `query-asset` 取贴图,旁置缩略图。
- **操作栏**:新建(自动取 idSegment 内下一个可用 uuid、复制uuid+2 或手动指定、删除、还原reload、保存并刷新触发 §5.2 save。未保存改动用 `*` 标记;切换记录前若有未保存改动,弹确认。
- **原生观感**:尽量用 Cocos `<ui-*>` 元素Vue 仅做状态与组合。
## 6. 内存与持久化
- 真理源 = 磁盘 `.ts` 文件。主进程首次 `query-*` 时懒加载并缓存 `TsConfigFile`。
- 编辑改动先作用于内存 AST"保存"才落盘。还原=丢弃内存改动重载。
- 多面板实例:主进程单例,广播保证一致。
## 7. 消息协议(`contributions.messages`
| 消息 | 方向 | 载荷 | 返回 |
|---|---|---|---|
| `query-schema` | panel→main | `tableId?` | schema全部或单表 |
| `query-enums` | panel→main | — | 枚举字典 |
| `query-keys` | panel→main | `tableId` | `number[]` |
| `query-record` | panel→main | `tableId,key` | `RecordValue` |
| `query-preview-desc` | panel→main | `hero RecordValue` | `string`(描述) |
| `query-asset` | panel→main | `name, type` | 贴图 url 或 null |
| `validate` | panel→main | `tableId` | `Issue[]` |
| `save-record` | panel→main | `tableId,key,RecordValue` | `{ok, issues?}` |
| `add-record` | panel→main | `tableId,key,RecordValue` | `{ok, issues?}` |
| `delete-record` | panel→main | `tableId,key` | `{ok, issues?}` |
| `revert-record` | panel→main | `tableId,key` | `RecordValue`(重载后值) |
| `record-changed` | main→broadcast | `tableId,key` | — |
`save/add/delete` 成功后主进程自动广播 `record-changed`。
## 8. 数据流(保存一条英雄)
```
面板改字段 → save-record(hero,5011,value)
→ 主进程 validate(hero)error? 返回 {ok:false,issues}
→ TsConfigFile.patch(5011, value) // AST 区间替换
→ TsConfigFile.save():写 .bak → createProgram 语法校验 → 覆盖原 .ts → asset-db refresh
→ 广播 record-changed(hero,5011)
→ 面板重查 → UI 刷新(含 HeroList 若变动)
```
## 9. 模块/文件布局
```
extensions/pixelhero-config-editor/
├── package.json # package_version:2, panels, contributions.messages/menu, deps: typescript, vue, esbuild
├── tsconfig.json
├── esbuild.config.mjs # 打包 main + 面板
├── i18n/{en,zh}.js
├── static/
│ ├── template/default/index.html
│ └── style/default/index.css
├── src/
│ ├── main/
│ │ ├── index.ts # 扩展入口 + onMessage 注册
│ │ └── store.ts # 三张表 TsConfigFile 单例 + 广播
│ ├── io/
│ │ ├── TsConfigFile.ts # 解析/序列化/patch/save
│ │ ├── recordValue.ts # RecordValue 类型与归一化(含 speedExpr
│ │ └── serializer.ts # serialize(entry) 确定性文本生成
│ ├── shared/ # 纯逻辑(无 Cocos/Editor 依赖,可独立测试)
│ │ ├── schema/
│ │ │ ├── types.ts # TableSchema/FieldSchema 定义
│ │ │ ├── registry.ts # 三张表 schema 注册
│ │ │ ├── hero.ts
│ │ │ ├── skill.ts
│ │ │ ├── field.ts
│ │ │ └── enums.ts # 枚举镜像 + assertEnumsMatchGame
│ │ ├── validation/
│ │ │ └── index.ts # validate() 规则
│ │ └── desc/
│ │ └── buildSkillDesc.ts # HeroSkillDesc 的 JS 移植(预览用)
│ └── panels/default/
│ ├── index.ts # Editor.Panel.define + Vue mount
│ └── app/ # Vue 组件App, MasterList, DetailForm, TriggerSlots, SkillMap, FieldList, Revive, Override, PreviewPane, ValidationBar
├── dist/ # 打包产物main.js, panels/default.js
└── __tests__/ # node:test 纯逻辑测试
├── tsConfigFile.roundtrip.test.ts
├── serializer.test.ts
├── validation.test.ts
├── speedExpr.test.ts
├── schema.test.ts
└── fixtures/ # 真实配置副本
```
## 10. 构建与开发
- `npm run build``esbuild` 同时打包 `main`platform=node, format=cjs与 `panels/default`platform=browser, format=iife, bundle vue输出到 `dist/`。
- 开发:改完 `build` → 在 Cocos"扩展管理器"重载扩展。
- 依赖:`typescript`IO 用)、`vue`(面板用)、`esbuild`(打包)、`fs-extra`(可选,读模板)。`typescript` 体积大但必要;打 main 时 bundle 进 dist。
## 11. 测试策略
遵循项目 `coding-standards.md`
- **逻辑层BLOCKING自动化`node:test`**
- `tsConfigFile.roundtrip`:用真实 `heroSet.ts`/`SkillSet.ts` 副本作 fixture → load → 读全部 → 不改动 → save → 与原文件逐字节相等(验证保号保注释)。
- 改动往返patch 一条英雄 → save → 重新 load → 读回值 == 改入值;且产物经 `createProgram` 语法合法。
- `serializer`:给定结构化值 → 序列化文本 → 再解析 → 等价。
- `speedExpr``AtkSpeedSet[AtkSpeedLv.Slow3].cd` ⇄ `{kind:'speed',level:'Slow3'}` 双向。
- `validation`:构造各类非法数据 → 断言 Issue 正确。
- `schema`:所有表 schema 字段 key 与对应 interface 一致;枚举镜像与游戏 `.ts` 枚举一致(`assertEnumsMatchGame`)。
- 命名/隔离/无外部依赖遵循测试规范fixture 为常量文件副本,不内联魔法数。
- **UIADVISORY**`production/qa/evidence/` 下手动走查文档(覆盖三表增删改查、校验阻断、预览一致性)+ 编辑器内截图。
## 12. 分阶段实施(供 writing-plans 细化)
| 阶段 | 产出 | 验证 |
|---|---|---|
| P0 脚手架 | package.json/panels/menu/build面板可从菜单打开显示 "hello" | 截图 |
| P1 IO 层 | TsConfigFile + RecordValue + serializer + roundtrip 测试 | 测试通过 |
| P2 schema+枚举+校验 | 三表 schema、enums、validate + 测试 | 测试通过 |
| P3 主进程 | store + 全部消息处理(先只读类) | 手动 query 验证 |
| P4 面板基础 | master 列表 + 标量/枚举/ref 字段编辑 + 保存往返 | 端到端改一个英雄保存 |
| P5 嵌套与预览 | 触发槽/技能图/驻场/复活/覆盖 + 描述预览 + 缩略图 | 编辑复杂英雄保存往返一致 |
| P6 收尾 | 新建/复制/删除/HeroList 同步/还原/打磨/QA 文档 | QA 走查通过 |
## 13. 风险与缓解
| 风险 | 缓解 |
|---|---|
| TS AST 往返破坏文件 | 条目级区间替换 + `.bak` + `createProgram` 校验 + 未识别表达式标记只读 |
| 符号表达式形态多样 | 固定匹配 `AtkSpeedSet[AtkSpeedLv.X].cd` 模式;其余降级为只读数值并提示 |
| 枚举镜像与游戏漂移 | `assertEnumsMatchGame` 调试期比对告警 |
| `typescript` 打包体积 | 仅 bundle 进 mainnode面板不包含可接受 |
| esbuild/Vue 在扩展环境运行 | P0 先打通最小面板Vue mount 成功)再扩展 |
| 多面板状态不一致 | 主进程单例 + 广播 record-changed |
| `HeroList` 失同步 | 英雄表写入后由 store 强制同步并校验 |
## 14. 验收标准
1. 扩展可从"面板"菜单打开,三张表可切换浏览、搜索。
2. 对三张表任一记录:可视化查看/编辑所有字段(含触发槽、技能图、覆盖、驻场、复活),保存后磁盘 `.ts` 被更新且为合法 TSasset-db 已刷新。
3. **保号往返**:仅查看后保存,文件零字节变化(测试断言)。
4. 实时校验:违反任一 error 规则时"保存"被阻断并列出问题;行/字段级可见。
5. 描述预览与游戏内 `buildSkillDesc` 输出一致。
6. 新建/复制/删除可用;`HeroList` 与 `HeroInfo` 始终一致。
7. 逻辑层单元测试全部通过BLOCKING
8. 未改动 `assets/script/game/**` 任何游戏运行时代码git diff 可证)。