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

23 KiB
Raw Blame History

通用英雄/技能配置编辑器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/61xxSkillSet(技能 60xx/63xx/64xx/65xxFieldSkillSet(驻场 70xx/72xx/74xx三张表。
  • 原地回写对应 .ts 文件,保留符号表达式(如 AtkSpeedSet[AtkSpeedLv.Slow3].cd)与手工注释/分节标题。
  • 异构触发槽、技能引用覆盖(SkillOverrides)、进化配置的可视化编辑。
  • 实时校验 + 实时描述预览(与游戏内 buildSkillDesc 一致)。
  • 新建/复制/删除/保存/还原,并保持 HeroListHeroInfo 一致。
  • 纯逻辑层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 (SkillSetSkillConfig)
    • 基础: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 (FieldSkillSetFieldSkillConfig)
    • 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

基于 typescriptnpm在扩展主进程 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].cdAST 形态:PropertyAccessExpression(ElementAccessExpression(Ident,ElementAccessExpression(Ident,Ident)),Ident),固定匹配该模式;不匹配则降级为 {kind:'num',value:<节点文本>} 并标记需人工确认)
  • 对象:{kind:'obj', props: {key: RecordValue}}
  • 数组:{kind:'arr', items: RecordValue[]}

回写策略 = 条目级 AST 区间替换

  1. 解析文件 → 定位目标 exportNameObjectLiteralExpression
  2. 在其中按 key 定位单个 PropertyAssignment 的完整文本区间(含其尾随逗号)。
  3. 对该条目调用 serialize() 生成新文本。确定性格式(固定,无歧义)受控多行对象字面量每字段一行、4 空格缩进、字段顺序按 schema fields 顺序、尾随逗号;cd 用符号形式 AtkSpeedSet[AtkSpeedLv.X].cdinfo 非空时在条目上方输出一行 // {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层护盾"},
    
  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
英雄表:HeroListHeroInfo 一致(每 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

  • 打包esbuildsrc/panels/default/index.ts(含 Vue 3 runtime+compiler打成单文件 dist/panels/default.js。Vue 用字符串模板compiler 在线编译),避免 SFC 工具链。
  • 面板入口Editor.Panel.definetemplate=<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=普攻)。
    • FieldListEditorfield:number[] 多选驻场技能。
    • ReviveEditorrevive 单对象 {s_uuid,r_num,upr} 或空。
    • OverrideEditor:依据所选基础技能 kind,仅渲染相关覆盖键(如 Damage→ap/hit_count/crt/stun…Support→TGroup/ap/buff_type…
  • 实时预览PreviewPane 调主进程 query-preview-desc(移植 buildSkillDesc),随编辑即时刷新。
  • 图标/特效预览iconsp_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 buildesbuild 同时打包 mainplatform=node, format=cjspanels/defaultplatform=browser, format=iife, bundle vue输出到 dist/
  • 开发:改完 build → 在 Cocos"扩展管理器"重载扩展。
  • 依赖:typescriptIO 用)、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:给定结构化值 → 序列化文本 → 再解析 → 等价。
    • speedExprAtkSpeedSet[AtkSpeedLv.Slow3].cd{kind:'speed',level:'Slow3'} 双向。
    • validation:构造各类非法数据 → 断言 Issue 正确。
    • schema:所有表 schema 字段 key 与对应 interface 一致;枚举镜像与游戏 .ts 枚举一致(assertEnumsMatchGame)。
    • 命名/隔离/无外部依赖遵循测试规范fixture 为常量文件副本,不内联魔法数。
  • UIADVISORYproduction/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. 新建/复制/删除可用;HeroListHeroInfo 始终一致。
  7. 逻辑层单元测试全部通过BLOCKING
  8. 未改动 assets/script/game/** 任何游戏运行时代码git diff 可证)。