主要改动: - 切到 funplay-cocos-mcp v0.5.1 (用户级配置, 项目级 .mcp.json 删除) - 仓库文档/CLAUDE.md/.gitignore 等清理过时 cocos-mcp-server 引用 - memory 文件同步: cocos-mcp-setup/path/blocker/spriteframe-uuid/prefab-persist 等加 funplay 实测警告 - memory 新建 funplay-cocos-mcp-pending-verification.md (后已被实测覆盖) - spec/plan/data: - docs/superpowers/specs/2026-09-02-legacy-layer-migration-design.md - docs/superpowers/plans/2026-09-02-legacy-layer-migration.md - docs/superpowers/data/layer-spirit-summary.json - YouleNexus: profiles.ts / defaults.ts / PlayerInfoView.prefab / scene 改动 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
19 KiB
legacy-layer → Cocos prefab 转换脚本设计
Status: Draft v1 (brainstorming approved, awaiting user spec review) Date: 2026-09-02 Spec for:
scripts/legacy-layer-to-cocos-prefab.mjs+ 测试套件
Goal
把 projects/Game_Surface_3/save/Layer*.xml(GB18030 编码 XML)+ output/gameabc_*.json 的结构化 UI 数据,程序化转换为 Cocos prefab JSON,让 YouleNexus 已迁移的 ~17 个组件 prefab(widgets/ + views/ + Login_Layer)能 1:1 对齐原项目 Game_Surface_3 的布局+精灵+Widget 锚定。
成功标准:脚本生成的 prefab 加载到 Cocos 编辑器 0 报错,节点树/SpriteFrame/Widget 锚定与原 Layer XML 字段语义一致。
范围(本设计覆盖)
已核实映射表(2026-09-02 全量 XML 解析 + 用户拍板)
| 目标 prefab(YouleNexus) | 对应 Layer XML | Layer 名称 | Group | Spirit 数 | 备注 |
|---|---|---|---|---|---|
prefabs/Login_Layer.prefab |
Layer00002.xml | Login_Layer | 2 | 18 | 已有完整复刻,端到端 PoC 验证 |
prefabs/views/PlayerInfoView.prefab |
Layer00017.xml | MenuPlayerInfo_Layer | 18 | 15 | 用户拍板 A |
prefabs/widgets/Group106_PhoneVerify.prefab |
Layer00029.xml | phoneInfo | 106, 108 | 38 | |
prefabs/widgets/Layer10_Protocol.prefab |
Layer00010.xml | Protol_Layer | 37 | 4 | PoC 阶段 2(含 Widget 锚定) |
prefabs/widgets/Layer609_Setting.prefab |
Layer00609.xml | Setting_Layer | 6 | 15 | |
prefabs/widgets/Layer612_Tips.prefab |
Layer00612.xml | Tips_Layer | 32, 89 | 5 | |
prefabs/widgets/Layer613_RoomCardUpdate.prefab |
Layer00613.xml | updateRoomCard_Layer | 34 | 3 | |
prefabs/widgets/Layer614_Loading.prefab |
Layer00614.xml | Loading_Layer | 40 | 2 | |
prefabs/widgets/Layer615_Reconnect.prefab |
Layer00615.xml | Reconnect_Layer | 35 | 4 | |
prefabs/widgets/Layer616_Kick.prefab |
Layer00616.xml | Kick_Layer | 33 | 3 | |
prefabs/widgets/Layer617_Notice.prefab |
Layer00008.xml | Notice_Layer | 3, 95 | 9 | 用户拍板 B |
prefabs/widgets/Layer619_BackOtherGame.prefab |
Layer00619.xml | BackHall_Layer | 55 | 1 | Layer 名 BackHall ≠ BackOtherGame,但 group 唯一匹配 |
prefabs/widgets/Layer620_Record.prefab |
Layer00620.xml | recordLayer | 101 | 3 |
总计:11 个 prefab 全部映射到明确 Layer XML(不含 6 个 NO_MATCH widget)
范围外(用户拍板 C:先不做)
| 目标 prefab | 原因 |
|---|---|
prefabs/templates/ListItem_Room.prefab |
跨 Layer 复用的 widget 子模板 / YouleNexus 新加,未在 save/Layer*.xml 中找到 |
prefabs/widgets/Btn_Primary.prefab |
通用按钮 widget,YouleNexus 新加 |
prefabs/widgets/IconButton.prefab |
通用图标按钮 widget,YouleNexus 新加 |
prefabs/widgets/Modal.prefab |
通用弹窗 widget,YouleNexus 新加 |
prefabs/widgets/NumericLabel_Score.prefab |
数字滚动 Label widget,YouleNexus 新加 |
prefabs/widgets/ProgressBar_Standard.prefab |
通用进度条 widget,YouleNexus 新加 |
事实源
完整 Layer 解析数据见 docs/superpowers/data/layer-spirit-summary.json(68 个 Layer 全量、81 个 group、5 种 SpiritType 分布)。
未来扩展(如需)
如果要补做 6 个 NO_MATCH widget,需先在 scripts/legacy-layer-to-cocos-prefab.mjs 加"跨 Layer Spirit 提取"能力(按 Sprite name / Layer 内 widget 模式匹配),不在本设计范围。
不在范围:
- 未迁移的 68 个 Layer 中的其余 ~50 个
- 动画(Spirit 的
ani_*属性 / Spine / DragonBones) - 多语言文本
- 协议层字段
Architecture
数据流
projects/Game_Surface_3/save/Layer*.xml ─┐
projects/Game_Surface_3/output/ ─┼→ [legacy-layer-to-cocos-prefab.mjs] → out/legacy-migration/
gameabc_Image.json (sprite 映射) │ ├─ <Name>.prefab (UTF-8 JSON 数组)
gameabc_Project.json (适配元数据) │ └─ <Name>.diff (vs YouleNexus 现有)
projects/Game_Surface_3/js/ ─┘
gameabc.min.js (语义来源,需逆向)
模块划分(单文件脚本 + 7 个测试)
| 模块 | 职责 |
|---|---|
readLayerXml(path) |
GB18030 编码读 XML → JS 对象 |
resolveSpriteFrameUuid(imgResId) |
查 gameabc_Image.json → atlas SpriteFrame 真 UUID(含 @f9941) |
buildGroupParents(spirits) |
按 BelongGroupID 分组 → group-X 父节点骨架 |
mapSpiritTypeToComponent(type) |
SpiritType → 组件类型枚举(逆向 gameabc.min.js) |
computeLpos(spirit, parentGroup) |
CoordSelfPos + CoordRelaX/Y → _lpos {x,y,z} |
computeContentSize(spirit) |
SizeCalcMode + SizeRelaWidth/Height + Width/HeightCalcMode → _contentSize {width,height} |
computeWidgetAlignment(spirit) |
SizeCalcMode + 父 group 关系 → cc.Widget._alignFlags + _horizontalCenter 等 |
emitPrefabJson(name, group, root) |
按 Login_Layer.prefab schema 输出 JSON 数组(含 CompPrefabInfo / PrefabInfo) |
diffAgainstExisting(generated, existing) |
输出 .diff 文件 |
输入契约
save/Layer<id>.xml
GB18030 编码(不是 UTF-8 也不是 GBK!实测 Python xml.etree 用 GBK 解码会乱码,GB18030 才能完整解析中文 Spirit 名)。结构:
<?xml version="1.0" encoding="UTF-8"?>
<Layer Version="2.6" SaveTime="...">
<PropertyList>
<Property Name="ID" Value="2"/>
<Property Name="Name" Value="Login_Layer"/>
<Property Name="Description" Value=""/>
</PropertyList>
<OptionList/> <EventList/>
<SpiritList>
<Spirit>
<PropertyList>
<Property Name="SpiritType" Value="0"/>
<Property Name="ID" Value="<spirit_id>"/>
<Property Name="Name" Value="<spirit_name_zh>"/> <!-- GB18030 -->
<Property Name="ImgResID" Value="<sprite_id>"/>
<Property Name="X" Value="0"/>
<Property Name="Y" Value="0"/>
<Property Name="Width" Value="1280"/>
<Property Name="Height" Value="720"/>
<Property Name="BelongLayerID" Value="2"/>
<Property Name="IndexOfLayer" Value="1"/>
<Property Name="BelongGroupID" Value="2"/> <!-- 0=根, >0=group-N -->
<Property Name="CoordParentID" Value="0"/>
<Property Name="CoordParentPos" Value="0"/>
<Property Name="CoordSelfPos" Value="0"/>
<Property Name="CoordRelaX" Value="0"/>
<Property Name="CoordRelaY" Value="0"/>
<Property Name="SizeCalcMode" Value="2"/>
<Property Name="SizeParentID" Value="21"/>
<Property Name="WidthCalcMode" Value="1"/>
<Property Name="SizeRelaWidth" Value="10000"/>
<Property Name="HeightCalcMode" Value="1"/>
<Property Name="SizeRelaHeight" Value="10000"/>
</PropertyList>
<OptionList/> <EventList/>
</Spirit>
...
</SpiritList>
</Layer>
GB18030 编码陷阱:脚本必须用 GB18030 读(GBK 是 GB18030 子集,但部分生僻字 / 私有区字符需要 GB18030 才能解码)。UTF-8 会乱码。Spirit 名是中文,输出 .prefab JSON 时按 UTF-8 写。Node.js 用
iconv-lite包支持 GB18030 解码。
output/gameabc_Image.json
ImgResID → atlas spriteFrame path 映射(具体 schema 在 PoC 阶段 1 解析)。
output/gameabc_Project.json
{
"Property": {
"ProjectName": "Game_Surface_3",
"ScreenWidth": 1280,
"ScreenHeight": 720,
"GameSceneWidth": 1699,
"GameSceneHeight": 1800,
"ScreenFitMode": 1
}
}
适配策略:原项目 ScreenWidth=1280, ScreenHeight=720,但 YouleNexus 当前 Login_Layer.prefab 根 _contentSize = 1600×720。脚本默认生成 1600×720 的根 UITransform(与 Login_Layer.prefab 一致),但 Spirit 内部坐标按原 XML 不变——Widget 锚定负责多分辨率适配。
js/gameabc.min.js
逆向目标:
SpiritType枚举(0=Sprite?1=Text?2=Button?3=ProgressBar?...)CoordSelfPos计算公式(X/Y 相对父节点的偏移量)SizeCalcMode公式(SizeParentID / SizeRelaWidth / SizeRelaHeight 怎么合成 _contentSize)WidthCalcMode/HeightCalcMode公式- BelongGroupID 的语义
逆向策略:在 PoC 阶段 1 之前完成。逆向结果固化到
framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json作为已知事实源。
输出契约
out/legacy-migration/<Name>.prefab
UTF-8 JSON 数组(严格按 Login_Layer.prefab schema),字段含义:
type PrefabJson = Array<PrefabObject>
type PrefabObject =
| { __type__: "cc.Prefab", _name: string, ... } // 0
| { __type__: "cc.Node", _name: string, _children: [{__id__: number}], _components: [{__id__: number}], _lpos: Vec3, _lrot: Quat, _lscale: Vec3, ... }
| { __type__: "cc.UITransform", _contentSize: Size, _anchorPoint: Vec2, ... }
| { __type__: "cc.Sprite", _spriteFrame: { __uuid__: string, __expectedType__: "cc.SpriteFrame" }, _type: 0|1, _sizeMode: 0|1|2, ... }
| { __type__: "cc.Label", _string: string, _fontSize: number, _horizontalAlign: 0|1|2, _verticalAlign: 0|1|2, _color: Color, _font: null, _isSystemFontUsed: true, ... }
| { __type__: "cc.Widget", _alignFlags: number, _left?: number, _right?: number, _top?: number, _bottom?: number, _horizontalCenter?: number, _verticalCenter?: number, _isAbs*: true, _alignMode: 2, ... }
| { __type__: "cc.CompPrefabInfo", fileId: string }
| { __type__: "cc.PrefabInfo", root: {__id__: number}, asset: {__id__: number}, fileId: string, ... }
| { __type__: string /* custom class UUID */, _name: "", _enabled: true, ... }
out/legacy-migration/<Name>.diff
diff -u 格式,对比脚本输出 vs YouleNexus prefabs/<path>/<Name>.prefab 现有内容。仅供人审,不自动覆盖。
输出根节点 _contentSize
默认 1600×720(与 Login_Layer.prefab 一致),不按原 Project.GameSceneWidth/Height——理由:YouleNexus Canvas 设计尺寸固定 1600×720(CLAUDE.md 第一准则精神:UI 层自己处理适配,原项目 ScreenWidth 是另一回事)。
关键映射规则
Spirit → cc.Node
| 原字段 | Cocos 字段 |
|---|---|
Spirit.Name |
cc.Node._name |
Spirit.ID |
用于生成 _id(cocos 内部 ID,与 fileId 不同) |
Spirit.BelongGroupID = "0" |
节点挂在 Layer 根节点下 |
Spirit.BelongGroupID = "N" (N>0) |
节点挂在 group-N 父节点下 |
Spirit.IndexOfLayer |
同 group 内的兄弟顺序(生成 _children 数组顺序) |
Spirit.CoordParentID |
父节点引用(=同 group 内其他 Spirit 的 ID) |
Group 父节点自动生成
- 自动创建
cc.Node _name = "group-<N>",N = 原 BelongGroupID 值 - 保留全局编号(不重新编号为 group-1/2/3),便于 cross-layer 调试(与 Login_Layer 复刻一致)
- 父节点本身无 sprite/label,只挂 UITransform + Widget(与 Login_Layer.prefab 的 group-2 一致)
- 多个 group 时按 group ID 升序排(group-1, group-2, ...)
SpiritType → 组件
逆向目标(在 PoC 阶段 1 之前完成)。当前猜测:
| SpiritType | 组件类型 | 说明 |
|---|---|---|
| 0 | cc.Sprite |
普通精灵 |
| 1 | cc.Label |
文本 |
| 2 | cc.Button |
按钮(待确认) |
| 3 | cc.ProgressBar |
进度条(Login_Layer "进度条底" 是 type=3) |
| ... | ... | 全部逆向 gameabc.min.js 后填表 |
未识别 type → throw,不静默。
ImgResID → SpriteFrame 真 UUID
查 output/gameabc_Image.json 拿到 atlas 图片路径 → 查 atlas-hall/atlas-login/atlas-room atlas 的 SpriteFrame UUID(含 @f9941 sub-asset 后缀)。
找不到 → throw + 列出所有未映射 ID(CLAUDE.md 第二准则)。
坐标/尺寸计算
锚点转换公式(实测验证 2026-09-02,Login_Layer 渠道logo + 游客登录双节点匹配):
原项目精灵锚点 = 左上角(X/Y 是精灵左上角的绝对屏幕坐标,基于 1280×720) Cocos 精灵锚点 = 中心(_lpos 是精灵中心在父节点坐标系的位置,Y-up)
# Spirit → cc.Node._lpos(假设父节点 _contentSize 1600×720、anchorPoint (0.5, 0.5))
cocos_lpos.x = original_X + original_W / 2 - 640 # 原坐标系半宽 1280/2
cocos_lpos.y = 360 - (original_Y + original_H / 2) # Y-up 翻转 + 原坐标系半高 720/2
cocos_lpos.z = 0
验证证据:
- 渠道logo:原 (734, 143, 59, 148) → Cocos (763.5 - 640, 360 - 217) = (123.5, 143) ✓
- 游客登录:原 (540, 388, 320, 95) → Cocos (700 - 640, 360 - 435.5) = (60, -75.5) ✓
SizeCalcMode 公式(待 PoC 阶段 1 逆向 gameabc.min.js):
_contentSize.width = Spirit.Width || 0
_contentSize.height = Spirit.Height || 0
# 如果 SizeCalcMode != 0,用 SizeParentID + SizeRelaWidth/Height 计算(公式逆向中)
if Spirit.SizeCalcMode != 0:
parent_size = lookup(Spirit.SizeParentID)
_contentSize.width = parent_size.width * Spirit.SizeRelaWidth / 10000
_contentSize.height = parent_size.height * Spirit.SizeRelaHeight / 10000
Widget 锚定:
| 情况 | _alignFlags |
其他字段 |
|---|---|---|
| 节点相对 Canvas 全屏铺满 | 45 (= 左+右+上+下) | _left/_right/_top/_bottom = 0 |
| 节点水平居中 | 16 (= 水平居中) | _horizontalCenter = Spirit.X |
| 节点垂直居中 | 8 | _verticalCenter = Spirit.Y |
| 节点左上对齐 | 9 (= 左+上) | _left = Spirit.X, _top = Spirit.Y |
| ... | ... | 全部逆向后填表 |
逆向不出公式 → throw,不兜底默认值。
错误处理(CLAUDE.md 第二准则:不静默兜底)
| 场景 | 处理 |
|---|---|
gameabc_Image.json 找不到 ImgResID |
throw new Error(\ImgResID ${id} (Spirit ${sid}) not in gameabc_Image.json`)` |
| SpiritType 不在已知枚举 | throw new Error(\Unknown SpiritType ${type} (Spirit ${sid}) — reverse gameabc.min.js first`)` |
| CoordSelfPos/SizeCalcMode 公式逆向不出 | throw new Error(\Cannot compute coord/size for Spirit ${sid}: formula unknown`)` |
| XML 字段缺失或编码错 | throw new Error(\Spirit ${sid} missing field ${fieldName}`)` |
| 输出目录有同名 .prefab | 覆盖(隔离目录是 transient)+ stdout 列 diff 路径 |
gameabc_Image.json schema 变更 |
启动时 schema 校验,失败直接 throw |
| Layer 在 PoC 阶段 4 找不到目标 YouleNexus prefab | throw + 列出所有"无目标"Layer,让用户补映射 |
测试策略(TDD)
按 ui-asset-and-skin plan 风格:node:test + node:assert + tsx,零第三方依赖。
测试套件:framework-tests/legacy-layer-migration/
| 文件 | 覆盖 |
|---|---|
xml-parser.test.mjs |
GB18030 编码读、字段提取、BelongGroupID 分组 |
spirit-to-node.test.mjs |
SpiritType=0/1/3 → Sprite/Text/ProgressBar 组件映射 |
group-builder.test.mjs |
BelongGroupID → group-X 父节点合并 + 同 group 内 IndexOfLayer 排序 |
coord-calculator.test.mjs |
CoordSelfPos/SizeCalcMode/WidthCalcMode/HeightCalcMode 公式正确性 |
sprite-uuid-resolver.test.mjs |
ImgResID → 现有 atlas SpriteFrame UUID(基于真实 gameabc_Image.json fixture) |
prefab-emitter.test.mjs |
输出 JSON 格式严格按 Login_Layer.prefab schema(用 Login_Layer.prefab 实际内容作 fixture) |
poc-iconbutton.test.mjs |
端到端:输入 save/Layer*.xml → 输出 .prefab → 与 YouleNexus 原 prefab 做 schema 对比(不对比内容,只对比结构) |
TDD 红绿循环(每个 PoC 阶段)
- 红:写测试(输入已知 Layer XML,期望已知 prefab JSON 结构)
- 绿:写脚本直到测试通过
- 重构:优化代码
- 人工 diff:
diff -u out/legacy-migration/<Name>.prefab prefabs/<path>/<Name>.prefab,看 diff - 修订:根据 diff 调整映射规则,回到 1
测试 fixtures
framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json— gameabc.min.js 逆向结果固化framework-tests/legacy-layer-migration/fixtures/gameabc_Image.json— 真实 gameabc_Image.json 副本framework-tests/legacy-layer-migration/fixtures/Layer00002.xml— Login_Layer XML 副本(GB18030)framework-tests/legacy-layer-migration/fixtures/Login_Layer.prefab— 已复刻的 Login_Layer prefab 副本(用于端到端对比)
PoC 路径(4 阶段)
| 阶段 | 目标 | 验证点 |
|---|---|---|
| 阶段 1 | IconButton(单 Sprite、无 Widget、无 group) | Spirit→Node + SpriteFrame 解析 + JSON 输出 + schema 对比 |
| 阶段 2 | Layer10_Protol(Layer00010,group 37,含 Widget 锚定) | group-X 父节点合并 + Widget 锚定逻辑 |
| 阶段 3 | Login_Layer(已有完整 1:1 复刻可对比) | 端到端验证 + 人工 diff 接近零差异 |
| 阶段 4 | 批量跑剩 14 个 | 每个输出 .diff 供人审 |
阶段 1 必须先逆向 gameabc.min.js 中 IconButton 用到的 SpiritType/Coord 公式。
不做的事(明确边界)
- ❌ 不覆盖 YouleNexus 原 prefab(只生成候选 + diff)
- ❌ 不处理动画(Spirit 的
ani_*属性 → throw + 标出该 Spirit) - ❌ 不处理 Spine / DragonBones(unknown SpiritType → throw)
- ❌ 不处理多语言(只读
_string字面值,不查国际化表) - ❌ 不迁移协议字段(CLAUDE.md 第一准则:协议不动)
- ❌ 不重写为 click handler / 业务逻辑(只做布局+精灵迁移,业务挂脚本留 follow-up)
- ❌ 不处理 OptionList.tag/tag1-3 / EventList(除非 layer-to-prefab 有明确映射,待逆向)
Risks & Open Questions
- gameabc.min.js 逆向深度:SpiritType 枚举只有 5 种(0/1/3/4/5,全量解析已确认),但 SpiritType 4 和 5 的语义仍需逆向(猜测 4=Particle / 5=Button,但需 PoC 阶段 1 验证);CoordSelfPos / SizeCalcMode 公式逆向中。
- SpriteFrame UUID 跨 atlas 选择规则:atlas 选哪个取决于原 Layer 来源场景 —— Login_Layer 用
atlas-login,MainScene 用atlas-hall,Room 用atlas-room。具体规则按 Layer 编号或 Spirit 名称模式决定,PoC 阶段 1 实测后固化到 fixtures/spirit-semantics.json。 - Layer00007 ewm_Layer 含 group=0:表示有节点不在 group-X 父节点下(直接挂在 Layer 根)。脚本需支持 BelongGroupID=0 节点(按现有 group-X 父节点方案应直接挂 Layer 根)—— 已在范围表规则中说明,待 PoC 验证。
- YouleNexus 原 prefab 的偏差:某些 prefab 可能已经人工调整过布局,与原 Layer 不一致 —— diff 工具帮人审,但最终覆盖决策在你
Out of Scope(follow-up)
- 把所有 68 个 Layer 全迁移
- 自动生成 sprite atlas(走 ui-asset-and-skin plan Task 3 独立推进)
- 迁移 OptionList.tag/tag1-3 业务字段
- 迁移 EventList 事件处理
Success Metrics
- 17 个目标 prefab 全部生成;与 YouleNexus 现有版本 diff 仅显示手工已修字段的差异(坐标/尺寸/spriteFrame UUID/Widget 锚定全部精确匹配)
- 锚点转换公式精度:至少 3 个已知 Spirit 的 _lpos 与人工复刻版 prefab 误差 < 0.001
- 7 个测试文件全部通过(node:test)
framework-tests/legacy-layer-migration/覆盖率 ≥ 80%- 脚本可在 CI 跑(无 GUI 依赖)
- gameabc.min.js 逆向结果固化到 fixtures/spirit-semantics.json,作为后续 PoC 阶段的事实源