Files
youle_cocos/docs/superpowers/specs/2026-09-02-legacy-layer-migration-design.md
joywayerandClaude Opus 5 02dc5d51ca chore(spec): 综合清理 + legacy-layer 迁移 spec/plan/data
主要改动:
- 切到 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>
2026-09-02 07:36:54 +08:00

19 KiB
Raw Permalink Blame History

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 阶段)

  1. 红:写测试(输入已知 Layer XML,期望已知 prefab JSON 结构)
  2. 绿:写脚本直到测试通过
  3. 重构:优化代码
  4. 人工 diff:diff -u out/legacy-migration/<Name>.prefab prefabs/<path>/<Name>.prefab,看 diff
  5. 修订:根据 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

  1. gameabc.min.js 逆向深度:SpiritType 枚举只有 5 种(0/1/3/4/5,全量解析已确认),但 SpiritType 4 和 5 的语义仍需逆向(猜测 4=Particle / 5=Button,但需 PoC 阶段 1 验证);CoordSelfPos / SizeCalcMode 公式逆向中。
  2. SpriteFrame UUID 跨 atlas 选择规则:atlas 选哪个取决于原 Layer 来源场景 —— Login_Layer 用 atlas-login,MainScene 用 atlas-hall,Room 用 atlas-room。具体规则按 Layer 编号或 Spirit 名称模式决定,PoC 阶段 1 实测后固化到 fixtures/spirit-semantics.json。
  3. Layer00007 ewm_Layer 含 group=0:表示有节点不在 group-X 父节点下(直接挂在 Layer 根)。脚本需支持 BelongGroupID=0 节点(按现有 group-X 父节点方案应直接挂 Layer 根)—— 已在范围表规则中说明,待 PoC 验证。
  4. 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 阶段的事实源