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.
23 KiB
通用英雄/技能配置编辑器(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 描述:
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)。
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 区间替换:
- 解析文件 → 定位目标
exportName的ObjectLiteralExpression。 - 在其中按 key 定位单个
PropertyAssignment的完整文本区间(含其尾随逗号)。 - 对该条目调用
serialize()生成新文本。确定性格式(固定,无歧义):受控多行对象字面量,每字段一行、4 空格缩进、字段顺序按 schemafields顺序、尾随逗号;cd用符号形式AtkSpeedSet[AtkSpeedLv.X].cd;info非空时在条目上方输出一行// {info}注释。示例:// 每受击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层护盾"}, - 用新文本替换该区间;其余条目、分节注释、其他 const 原样不动 → diff 最小。
- 新增:在
}前插入;删除:移除区间。 - 落盘前:
createProgram对改动文件做语法检查;失败则回滚.bak并报错。 - 落盘后:
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 字段目标在引用表中存在(英雄触发槽/技能图引用 → SkillSet;field 列表 → FieldSkillSet;call_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(表切换 + 搜索 + 列表),右 detail(schema 驱动表单 + 嵌套编辑器 + 预览),底部校验条。
- 控件映射: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 为常量文件副本,不内联魔法数。
- UI(ADVISORY):
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 进 main(node),面板不包含;可接受 |
| esbuild/Vue 在扩展环境运行 | P0 先打通最小面板(Vue mount 成功)再扩展 |
| 多面板状态不一致 | 主进程单例 + 广播 record-changed |
HeroList 失同步 |
英雄表写入后由 store 强制同步并校验 |
14. 验收标准
- 扩展可从"面板"菜单打开,三张表可切换浏览、搜索。
- 对三张表任一记录:可视化查看/编辑所有字段(含触发槽、技能图、覆盖、驻场、复活),保存后磁盘
.ts被更新且为合法 TS,asset-db 已刷新。 - 保号往返:仅查看后保存,文件零字节变化(测试断言)。
- 实时校验:违反任一 error 规则时"保存"被阻断并列出问题;行/字段级可见。
- 描述预览与游戏内
buildSkillDesc输出一致。 - 新建/复制/删除可用;
HeroList与HeroInfo始终一致。 - 逻辑层单元测试全部通过(BLOCKING)。
- 未改动
assets/script/game/**任何游戏运行时代码(git diff 可证)。