# legacy-layer → Cocos prefab 转换脚本 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 写 `scripts/legacy-layer-to-cocos-prefab.mjs` 程序化转换 Game_Surface_3 的 `save/Layer*.xml` 到 Cocos prefab JSON,让 YouleNexus 已迁移的 11 个组件 prefab 1:1 对齐原项目布局+精灵+Widget 锚定。 **Architecture:** 单文件 Node.js 脚本(GB18030 解码 XML + gameabc 语义映射 + prefab JSON 输出)+ 7 个 `node:test` 测试文件。脚本生成候选 .prefab 到隔离目录 + .diff 文件供人审,**不直接覆盖 YouleNexus 原 prefab**。 **Tech Stack:** Node.js 20.13.1(ESM `.mjs`)、`node:test` + `node:assert` + `tsx`、`iconv-lite`(GB18030 解码)、`@xmldom/xmldom`(XML 解析)。零游戏引擎依赖(仅做文本→JSON 转换,不调 Cocos API)。 **Spec:** `docs/superpowers/specs/2026-09-02-legacy-layer-migration-design.md` ## Global Constraints - **服务器零改动**(CLAUDE.md 第一准则):本计划不触碰协议,与 UI 复刻无关。 - **数据源权威唯一、下游不兜底**(CLAUDE.md 第二准则):找不到的字段/类型/UUID 一律 throw + 列出所有未映射项;不静默 fallback 到默认值。 - **GB18030 编码**:`save/Layer*.xml` 必须用 `iconv-lite` GB18030 解码(实测 GBK 解码会乱码)。 - **锚点转换公式**:原 X, Y 是精灵**左上角**绝对屏幕坐标(基于 1280×720);Cocos 用**中心**锚点。 - `cocos_lpos.x = original_X + original_W/2 - 640`(原坐标系半宽 1280/2) - `cocos_lpos.y = 360 - (original_Y + original_H/2)`(Y-up 翻转 + 半高 720/2) - **group-X 父节点命名**:保留原 BelongGroupID 值作 group 编号(不重排),与 Login_Layer 复刻习惯一致;group=0 节点直接挂 Layer 根。 - **不直接覆盖原 prefab**:脚本只输出到 `cocoscreator_projects/YouleNexus/out/legacy-migration/`,由人审 .diff 后决定是否覆盖。 - **Node 20.13.1 不支持 `--test` glob**:测试文件须显式枚举(用 `scripts/run-framework-tests.mjs`)。 - **所有路径以 monorepo 根 `G:/Works/YouleGamesCocosCreator/cocoscreator_projects/` 为基准**。 - **不在范围**(用户拍板 C,先不做):ListItem_Room / Btn_Primary / IconButton / Modal / NumericLabel_Score / ProgressBar_Standard 6 个 NO_MATCH widget。 --- ## Task 1: 项目脚手架 + 测试运行器验证 **Files:** - Modify: `cocoscreator_projects/YouleNexus/package.json`(添加 `iconv-lite` + `@xmldom/xmldom` 依赖) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/.gitkeep` - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/.gitkeep` - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/gameabc_Image.json`(从 `projects/Game_Surface_3/output/gameabc_Image.json` 复制) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml`(从 `projects/Game_Surface_3/save/Layer00002.xml` 复制,**保留 GB18030 原编码**) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Login_Layer.prefab`(从 `cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab` 复制) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json`(gameabc.min.js 逆向骨架,初始只含已知 5 种 SpiritType) - [ ] **Step 1: 添加 iconv-lite + @xmldom/xmldom 依赖** 读 `cocoscreator_projects/YouleNexus/package.json` 检查现有依赖结构。若 YouleNexus 没有 package.json,则用 `cd cocoscreator_projects/YouleNexus && npm init -y` 初始化,然后: ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus npm install --save iconv-lite @xmldom/xmldom ``` 期望:`package.json` 出现 `"iconv-lite"` 和 `"@xmldom/xmldom"` 条目。 - [ ] **Step 2: 创建目录 + 复制 fixtures** ```bash mkdir -p G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures cp G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/output/gameabc_Image.json \ G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/gameabc_Image.json cp G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/save/Layer00002.xml \ G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml cp G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab \ G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Login_Layer.prefab ``` 期望:`fixtures/` 目录下有 3 个文件 + Login_Layer.prefab.meta(不复制 meta,只复制 .prefab 本身用于 schema 对比)。 - [ ] **Step 3: 写 spirit-semantics.json 骨架** ```json { "spirit_types": { "0": { "component": "cc.Sprite", "note": "普通精灵" }, "1": { "component": "cc.Label", "note": "文本" }, "3": { "component": "cc.ProgressBar", "note": "进度条" }, "4": { "component": "unknown", "note": "PoC 阶段 1 逆向 gameabc.min.js" }, "5": { "component": "unknown", "note": "PoC 阶段 1 逆向 gameabc.min.js" } }, "coord_self_pos": { "_note": "原 X, Y 是精灵左上角绝对屏幕坐标 (基于 1280x720);Cocos 用中心锚点", "cocos_lpos_x": "original_X + original_W/2 - 640", "cocos_lpos_y": "360 - (original_Y + original_H/2)" }, "anchor_point": { "cocos_default": { "x": 0.5, "y": 0.5 }, "original_default": "左上角 (0, 0)" } } ``` 写到 `fixtures/spirit-semantics.json`。 - [ ] **Step 4: 验证 iconv-lite 能解码 GB18030** 写最小验证脚本 `framework-tests/legacy-layer-migration/_smoke-iconv.mjs`: ```javascript import { readFileSync } from 'node:fs'; import iconv from 'iconv-lite'; const buf = readFileSync('fixtures/Layer00002.xml'); const text = iconv.decode(buf, 'gb18030'); console.log('decoded length:', text.length); console.log('contains 微信登录背景:', text.includes('微信登录背景')); console.log('contains Login_Layer:', text.includes('Login_Layer')); ``` 期望输出:decoded length > 5000;两行 contains 都输出 true。 - [ ] **Step 5: 跑通 smoke test** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node framework-tests/legacy-layer-migration/_smoke-iconv.mjs ``` 期望:3 行输出,decoded length 在 5000-10000 之间。 - [ ] **Step 6: Commit** ```bash git add cocoscreator_projects/YouleNexus/package.json cocoscreator_projects/framework-tests/legacy-layer-migration/ git commit -m "feat(scripts): legacy-layer 转换器脚手架 + fixtures + GB18030 smoke test" ``` --- ## Task 2: xml-parser(GB18030 读 XML + 字段提取) **Files:** - Create: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(仅 readLayerXml 函数) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/xml-parser.test.mjs` **Interfaces:** - Consumes: `fixtures/Layer00002.xml`(GB18030 字节) - Produces: `readLayerXml(path) → { layerId, layerName, spirits: Array<{ id, name, type, imgResID, x, y, w, h, group, indexOfLayer }> }` - [ ] **Step 1: 写失败测试** `framework-tests/legacy-layer-migration/xml-parser.test.mjs`: ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readLayerXml } from '../../scripts/legacy-layer-to-cocos-prefab.mjs'; test('readLayerXml 解码 GB18030 并提取 Layer00002 字段', () => { const layer = readLayerXml('framework-tests/legacy-layer-migration/fixtures/Layer00002.xml'); assert.equal(layer.layerId, '2'); assert.equal(layer.layerName, 'Login_Layer'); assert.equal(layer.spirits.length, 18, 'Login_Layer 有 18 个 Spirit'); }); test('Spirit 字段完整提取(含中文名)', () => { const layer = readLayerXml('framework-tests/legacy-layer-migration/fixtures/Layer00002.xml'); // 找一个已知 Spirit(渠道logo 类,Spirit405) const spirit405 = layer.spirits.find(s => s.id === '405'); assert.ok(spirit405, 'Spirit id=405 应存在'); assert.match(spirit405.name, /渠道/); // GB18030 解码后中文名应正常 assert.equal(spirit405.type, '0'); // Sprite assert.equal(spirit405.x, '734'); assert.equal(spirit405.y, '143'); assert.equal(spirit405.w, '59'); assert.equal(spirit405.h, '148'); assert.equal(spirit405.group, '2'); }); test('BelongGroupID 提取(group=0 节点也存在)', () => { const layer = readLayerXml('framework-tests/legacy-layer-migration/fixtures/Layer00002.xml'); // 全部 Spirit 的 group 字段都应有值 for (const s of layer.spirits) { assert.ok(s.group !== undefined, `Spirit ${s.id} (${s.name}) 应有 group 字段`); } // group=0 节点(如有)也应被提取,不被过滤 const g0 = layer.spirits.filter(s => s.group === '0'); assert.ok(g0.length >= 0); // Login_Layer 没有 group=0,但提取逻辑应支持 }); ``` - [ ] **Step 2: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/xml-parser.test.mjs ``` 期望:FAIL with `readLayerXml is not defined` 或类似(脚本未实现)。 - [ ] **Step 3: 实现 readLayerXml** `scripts/legacy-layer-to-cocos-prefab.mjs` 头部: ```javascript import { readFileSync } from 'node:fs'; import iconv from 'iconv-lite'; import { DOMParser } from '@xmldom/xmldom'; export function readLayerXml(path) { const buf = readFileSync(path); const text = iconv.decode(buf, 'gb18030'); const doc = new DOMParser().parseFromString(text, 'text/xml'); const getProp = (el, name) => { const prop = Array.from(el.getElementsByTagName('Property')) .find(p => p.getAttribute('Name') === name); return prop?.getAttribute('Value'); }; // 提取 Layer 字段 const layerPropList = doc.getElementsByTagName('Layer')[0].getElementsByTagName('PropertyList')[0]; const layerId = getProp(layerPropList, 'ID'); const layerName = getProp(layerPropList, 'Name'); // 提取所有 Spirit const spiritEls = doc.getElementsByTagName('Spirit'); const spirits = Array.from(spiritEls).map(sp => { const propList = sp.getElementsByTagName('PropertyList')[0]; return { id: getProp(propList, 'ID'), name: getProp(propList, 'Name'), type: getProp(propList, 'SpiritType'), imgResID: getProp(propList, 'ImgResID'), x: getProp(propList, 'X'), y: getProp(propList, 'Y'), w: getProp(propList, 'Width'), h: getProp(propList, 'Height'), group: getProp(propList, 'BelongGroupID'), indexOfLayer: getProp(propList, 'IndexOfLayer'), }; }); return { layerId, layerName, spirits }; } ``` - [ ] **Step 4: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/xml-parser.test.mjs ``` 期望:3 个 test 全 PASS。 - [ ] **Step 5: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/xml-parser.test.mjs git commit -m "feat(scripts): readLayerXml - GB18030 解码 + Layer/Spirit 字段提取" ``` --- ## Task 3: coord-calculator(锚点转换公式) **Files:** - Modify: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(追加 computeLpos / computeContentSize) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/coord-calculator.test.mjs` **Interfaces:** - Consumes: 单个 Spirit 对象 `{ x, y, w, h, ... }` - Produces: `computeLpos(spirit) → { x: number, y: number, z: 0 }`, `computeContentSize(spirit) → { width: number, height: number }` - [ ] **Step 1: 写失败测试** ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { computeLpos, computeContentSize } from '../../scripts/legacy-layer-to-cocos-prefab.mjs'; test('computeLpos 锚点转换(实测验证:渠道logo)', () => { const spirit = { x: '734', y: '143', w: '59', h: '148' }; const lpos = computeLpos(spirit); // 公式: x = 734 + 59/2 - 640 = 123.5 // 公式: y = 360 - (143 + 148/2) = 143 assert.equal(lpos.x, 123.5); assert.equal(lpos.y, 143); assert.equal(lpos.z, 0); }); test('computeLpos 锚点转换(实测验证:游客登录)', () => { const spirit = { x: '540', y: '388', w: '320', h: '95' }; const lpos = computeLpos(spirit); // x = 540 + 160 - 640 = 60 // y = 360 - (388 + 47.5) = -75.5 assert.equal(lpos.x, 60); assert.equal(lpos.y, -75.5); }); test('computeLpos 字符串转数字(X/Y 为字符串 "0"/"-100")', () => { const spirit = { x: '0', y: '-100', w: '100', h: '100' }; const lpos = computeLpos(spirit); // x = 0 + 50 - 640 = -590 // y = 360 - (-100 + 50) = 410 assert.equal(lpos.x, -590); assert.equal(lpos.y, 410); }); test('computeContentSize 直接读 Width/Height(SizeCalcMode=0 路径)', () => { const spirit = { w: '320', h: '95', sizeCalcMode: '0' }; const size = computeContentSize(spirit); assert.equal(size.width, 320); assert.equal(size.height, 95); }); test('computeContentSize SizeCalcMode=2 用 SizeParent 链式计算(PoC 验证用)', () => { // 此测试可能跳过直到逆向 gameabc.min.js 后启用 // const spirit = { w: '1280', h: '720', sizeCalcMode: '2', sizeParentID: '21', sizeRelaWidth: '10000', sizeRelaHeight: '10000' }; // const size = computeContentSize(spirit, parentLookup); // 公式逆向后再启用 }); ``` - [ ] **Step 2: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/coord-calculator.test.mjs ``` 期望:FAIL `computeLpos is not a function`。 - [ ] **Step 3: 实现 computeLpos / computeContentSize(SizeCalcMode=0 路径)** 追加到 `scripts/legacy-layer-to-cocos-prefab.mjs`: ```javascript export function computeLpos(spirit) { const x = parseFloat(spirit.x || '0'); const y = parseFloat(spirit.y || '0'); const w = parseFloat(spirit.w || '0'); const h = parseFloat(spirit.h || '0'); // 锚点转换:原 X/Y 是左上角(1280x720 绝对坐标)→ Cocos 中心锚点 return { x: x + w / 2 - 640, // 原坐标系半宽 1280/2 y: 360 - (y + h / 2), // Y-up 翻转 + 半高 720/2 z: 0, }; } export function computeContentSize(spirit) { // SizeCalcMode=0 路径:直接读 Width/Height // SizeCalcMode>0 路径:PoC 阶段 1 逆向 gameabc.min.js 后实现 const sizeCalcMode = parseInt(spirit.sizeCalcMode || '0', 10); if (sizeCalcMode !== 0) { throw new Error(`SizeCalcMode=${sizeCalcMode} not yet supported for Spirit ${spirit.id} (${spirit.name}) - reverse gameabc.min.js first`); } return { width: parseFloat(spirit.w || '0'), height: parseFloat(spirit.h || '0'), }; } ``` - [ ] **Step 4: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/coord-calculator.test.mjs ``` 期望:4 个 test PASS(SizeCalcMode=2 那个跳过注释掉)。 - [ ] **Step 5: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/coord-calculator.test.mjs git commit -m "feat(scripts): computeLpos / computeContentSize - 锚点转换公式(实测验证)" ``` --- ## Task 4: group-builder(BelongGroupID → group-X 父节点) **Files:** - Modify: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(追加 buildGroupParents) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/group-builder.test.mjs` **Interfaces:** - Consumes: `spirits: Array<{ ..., group: string }>` - Produces: `buildGroupParents(spirits) → { groupParents: Array<{ name: string, childSpiritIds: string[] }>, rootChildSpiritIds: string[] }` - [ ] **Step 1: 写失败测试** ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { buildGroupParents } from '../../scripts/legacy-layer-to-cocos-prefab.mjs'; test('buildGroupParents 把同 group Spirit 归类', () => { const spirits = [ { id: '1', group: '2', indexOfLayer: '1' }, { id: '405', group: '2', indexOfLayer: '2' }, { id: '285', group: '2', indexOfLayer: '3' }, ]; const result = buildGroupParents(spirits); assert.equal(result.groupParents.length, 1, '只有一个 group=2'); assert.equal(result.groupParents[0].name, 'group-2'); assert.deepEqual(result.groupParents[0].childSpiritIds, ['1', '405', '285']); assert.equal(result.rootChildSpiritIds.length, 0); }); test('buildGroupParents 多 group 按 ID 升序', () => { const spirits = [ { id: '1', group: '32', indexOfLayer: '1' }, { id: '2', group: '1', indexOfLayer: '2' }, { id: '3', group: '1', indexOfLayer: '3' }, ]; const result = buildGroupParents(spirits); assert.deepEqual(result.groupParents.map(g => g.name), ['group-1', 'group-32']); }); test('buildGroupParents group=0 Spirit 直挂 Layer 根', () => { const spirits = [ { id: '10', group: '0', indexOfLayer: '1' }, { id: '20', group: '0', indexOfLayer: '2' }, { id: '30', group: '2', indexOfLayer: '3' }, ]; const result = buildGroupParents(spirits); assert.equal(result.groupParents.length, 1); assert.deepEqual(result.rootChildSpiritIds, ['10', '20']); }); test('buildGroupParents 按 IndexOfLayer 排序子节点', () => { const spirits = [ { id: 'c', group: '1', indexOfLayer: '3' }, { id: 'a', group: '1', indexOfLayer: '1' }, { id: 'b', group: '1', indexOfLayer: '2' }, ]; const result = buildGroupParents(spirits); assert.deepEqual(result.groupParents[0].childSpiritIds, ['a', 'b', 'c']); }); ``` - [ ] **Step 2: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/group-builder.test.mjs ``` 期望:FAIL `buildGroupParents is not a function`。 - [ ] **Step 3: 实现 buildGroupParents** 追加: ```javascript export function buildGroupParents(spirits) { const groupMap = new Map(); // group_id -> [{ spiritId, index }] const rootSpirits = []; for (const s of spirits) { if (s.group === '0' || !s.group) { rootSpirits.push({ spiritId: s.id, index: parseInt(s.indexOfLayer || '0', 10) }); } else { if (!groupMap.has(s.group)) groupMap.set(s.group, []); groupMap.get(s.group).push({ spiritId: s.id, index: parseInt(s.indexOfLayer || '0', 10) }); } } const groupParents = Array.from(groupMap.entries()) .sort(([a], [b]) => parseInt(a, 10) - parseInt(b, 10)) .map(([gid, items]) => ({ name: `group-${gid}`, childSpiritIds: items.sort((a, b) => a.index - b.index).map(x => x.spiritId), })); const rootChildSpiritIds = rootSpirits .sort((a, b) => a.index - b.index) .map(x => x.spiritId); return { groupParents, rootChildSpiritIds }; } ``` - [ ] **Step 4: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/group-builder.test.mjs ``` 期望:4 个 test PASS。 - [ ] **Step 5: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/group-builder.test.mjs git commit -m "feat(scripts): buildGroupParents - BelongGroupID → group-X 父节点归类" ``` --- ## Task 5: gameabc.min.js 逆向(SpiritType 4/5 + SizeCalcMode>0 公式) **Files:** - Modify: `cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json` - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs` **Interfaces:** - Consumes: 读 `projects/Game_Surface_3/js/gameabc.min.js`(minified)+ 已知 Layer XML 数据 - Produces: 更新 `fixtures/spirit-semantics.json` 含 5 种 SpiritType 全部语义 + SizeCalcMode 公式 - [ ] **Step 1: 提取 gameabc.min.js 中 SpiritType 相关字符串** ```bash cd G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3 grep -oE 'SpiritType[^,;}]{0,80}' js/gameabc.min.js | head -30 ``` 期望输出:找到 SpiritType 字段被读取/写入的所有位置。 - [ ] **Step 2: 用 IconButton / Btn_Primary PoC 反推 SpiritType 4/5 语义** ```bash # 找一个含 type=4 的 Spirit 所在的 Layer grep -l 'SpiritType" Value="4"' /G:/Works/YouleGamesCocosCreator/projects/Game_Surface_3/save/*.xml | head -3 ``` 读这些 XML + 对比 Login_Layer 复刻 + 在 Cocos 编辑器中用 mcp 看对应的 prefab(如果存在)。 - [ ] **Step 3: 写测试验证 SpiritType 4/5 的组件映射** `framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs`: ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; const semantics = JSON.parse(readFileSync('framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json', 'utf8')); test('SpiritType 4 有明确组件映射(PoC 后填)', () => { assert.notEqual(semantics.spirit_types['4'].component, 'unknown', 'SpiritType 4 必须是已知组件,不能是 unknown'); }); test('SpiritType 5 有明确组件映射(PoC 后填)', () => { assert.notEqual(semantics.spirit_types['5'].component, 'unknown', 'SpiritType 5 必须是已知组件,不能是 unknown'); }); ``` - [ ] **Step 4: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs ``` 期望:FAIL(component 还是 "unknown")。 - [ ] **Step 5: 用 PoC 实测结果更新 spirit-semantics.json** 基于 Step 1-2 的逆向结果,编辑 `fixtures/spirit-semantics.json`: ```json { "spirit_types": { "0": { "component": "cc.Sprite", "note": "普通精灵" }, "1": { "component": "cc.Label", "note": "文本" }, "3": { "component": "cc.ProgressBar", "note": "进度条" }, "4": { "component": "<实测后填>", "note": "<实测后填>" }, "5": { "component": "<实测后填>", "note": "<实测后填>" } }, "size_calc_mode": { "0": "直接读 Width/Height", "1": "<逆向后填>", "2": "<逆向后填>" }, "coord_self_pos": { ... }, "anchor_point": { ... } } ``` > **本任务的工作量取决于 gameabc.min.js 逆向深度**。如果 SpiritType 4/5 在 minified 中难以逆向,**可以标注为 follow-up**,本任务把 spirit-type-reverse.test.mjs 用 `test.skip` 跳过——但这样会丢失后续 test 的真相源。建议至少逆向出 4/5 的语义。 - [ ] **Step 6: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs ``` 期望:2 个 test PASS(如果 SpiritType 4/5 逆向完成)。 - [ ] **Step 7: Commit** ```bash git add cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-type-reverse.test.mjs git commit -m "feat(scripts): SpiritType 4/5 逆向 + spirit-semantics.json 完整化" ``` --- ## Task 6: spirit-to-node(SpiritType → 组件映射) **Files:** - Modify: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(追加 mapSpiritTypeToComponent) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-to-node.test.mjs` **Interfaces:** - Consumes: `spirit.type: string`("0"/"1"/"3"/"4"/"5") - Produces: `mapSpiritTypeToComponent(type) → "cc.Sprite" | "cc.Label" | ...`(读 spirit-semantics.json) - [ ] **Step 1: 写失败测试** ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mapSpiritTypeToComponent } from '../../scripts/legacy-layer-to-cocos-prefab.mjs'; test('type=0 → cc.Sprite', () => { assert.equal(mapSpiritTypeToComponent('0'), 'cc.Sprite'); }); test('type=1 → cc.Label', () => { assert.equal(mapSpiritTypeToComponent('1'), 'cc.Label'); }); test('type=3 → cc.ProgressBar', () => { assert.equal(mapSpiritTypeToComponent('3'), 'cc.ProgressBar'); }); test('type=4 → 已知组件(不能是 unknown)', () => { assert.notEqual(mapSpiritTypeToComponent('4'), 'unknown'); }); test('type=5 → 已知组件(不能是 unknown)', () => { assert.notEqual(mapSpiritTypeToComponent('5'), 'unknown'); }); test('未知 type → throw(不静默)', () => { assert.throws(() => mapSpiritTypeToComponent('99'), /Unknown SpiritType/); }); ``` - [ ] **Step 2: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/spirit-to-node.test.mjs ``` 期望:FAIL `mapSpiritTypeToComponent is not a function`。 - [ ] **Step 3: 实现 mapSpiritTypeToComponent** 追加: ```javascript import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const SEMANTICS_PATH = join(__dirname, '../framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json'); let _semantics = null; function loadSemantics() { if (!_semantics) _semantics = JSON.parse(readFileSync(SEMANTICS_PATH, 'utf8')); return _semantics; } export function mapSpiritTypeToComponent(type) { const semantics = loadSemantics(); const entry = semantics.spirit_types[type]; if (!entry) { throw new Error(`Unknown SpiritType "${type}" - reverse gameabc.min.js first`); } if (entry.component === 'unknown') { throw new Error(`SpiritType "${type}" component not yet reverse-engineered - complete Task 5 first`); } return entry.component; } ``` - [ ] **Step 4: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/spirit-to-node.test.mjs ``` 期望:6 个 test PASS。 - [ ] **Step 5: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/spirit-to-node.test.mjs git commit -m "feat(scripts): mapSpiritTypeToComponent - SpiritType → cc.* 组件映射" ``` --- ## Task 7: sprite-uuid-resolver(ImgResID → SpriteFrame UUID) **Files:** - Modify: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(追加 resolveSpriteFrameUuid) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/sprite-uuid-resolver.test.mjs` **Interfaces:** - Consumes: `imgResId: string`(如 "20"/"21"/"25")+ fixtures/gameabc_Image.json - Produces: `resolveSpriteFrameUuid(imgResId) → "@f9941"`(SpriteFrame 真 UUID 含 sub-asset 后缀) - [ ] **Step 1: 解析 gameabc_Image.json 结构** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects python -c " import json data = json.load(open('framework-tests/legacy-layer-migration/fixtures/gameabc_Image.json')) # 找前 3 个 image 记录 for k in list(data.keys())[:3]: print(k, data[k]) " ``` 期望:了解 gameabc_Image.json 的实际 schema(key 是 ImageID?value 含 path?)。 - [ ] **Step 2: 写失败测试(基于 Step 1 发现的 schema)** ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveSpriteFrameUuid } from '../../scripts/legacy-layer-to-cocos-prefab.mjs'; test('resolveSpriteFrameUuid 返回含 @f9941 的真 UUID', () => { const uuid = resolveSpriteFrameUuid('<某已知 imgResId>'); assert.match(uuid, /@f9941$/, '必须含 SpriteFrame sub-asset 后缀'); }); test('未知 imgResId → throw(不静默)', () => { assert.throws(() => resolveSpriteFrameUuid('99999999'), /ImgResID .* not in gameabc_Image.json/); }); ``` > **⚠️ 本步骤需要先看 gameabc_Image.json 实际结构**。如果 schema 与猜测不同,按实际写测试。 - [ ] **Step 3: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/sprite-uuid-resolver.test.mjs ``` 期望:FAIL。 - [ ] **Step 4: 实现 resolveSpriteFrameUuid(依赖实际 schema)** 按 Step 1 发现的 schema 实现,伪代码: ```javascript let _imageMap = null; function loadImageMap() { if (!_imageMap) { const data = JSON.parse(readFileSync(SEMANTICS_IMAGE_PATH, 'utf8')); _imageMap = data; // 按实际 schema 调整 } return _imageMap; } export function resolveSpriteFrameUuid(imgResId) { const map = loadImageMap(); const entry = map[imgResId]; if (!entry) { throw new Error(`ImgResID ${imgResId} not in gameabc_Image.json`); } // 根据 entry 构造 SpriteFrame UUID(按 schema 调整) // 通常:entry.atlasUuid + '@' + entry.subAssetIndex return entry.spriteFrameUuid; // 假设 schema 含此字段 } ``` > **本任务的 schema 适配需要你在 Step 1 输出后具体填代码**。 - [ ] **Step 5: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/sprite-uuid-resolver.test.mjs ``` 期望:2 个 test PASS。 - [ ] **Step 6: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/sprite-uuid-resolver.test.mjs git commit -m "feat(scripts): resolveSpriteFrameUuid - ImgResID → SpriteFrame UUID" ``` --- ## Task 8: prefab-emitter(按 Login_Layer.prefab schema 输出 JSON) **Files:** - Modify: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(追加 emitPrefabJson) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/prefab-emitter.test.mjs` **Interfaces:** - Consumes: `{ layerId, layerName, spirits }` + `resolveSpriteFrameUuid` + `mapSpiritTypeToComponent` + `computeLpos` + `computeContentSize` + `buildGroupParents` - Produces: `emitPrefabJson(layer, opts) → JSON array`(严格按 Login_Layer.prefab schema) - [ ] **Step 1: 写失败测试(对照 Login_Layer.prefab schema)** ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { emitPrefabJson } from '../../scripts/legacy-layer-to-cocos-prefab.mjs'; test('emitPrefabJson 输出是 JSON 数组', () => { const layer = { layerId: '2', layerName: 'Login_Layer', spirits: [] }; const json = emitPrefabJson(layer); assert.ok(Array.isArray(json), '输出必须是数组'); assert.ok(json.length >= 1, '至少含 cc.Prefab 头'); assert.equal(json[0].__type__, 'cc.Prefab'); assert.equal(json[0]._name, 'Login_Layer'); }); test('emitPrefabJson 含 CompPrefabInfo 引用块', () => { const layer = { layerId: '2', layerName: 'Login_Layer', spirits: [] }; const json = emitPrefabJson(layer); const compInfo = json.filter(o => o.__type__ === 'cc.CompPrefabInfo'); assert.ok(compInfo.length > 0, '每个组件应有 CompPrefabInfo 引用块'); }); test('emitPrefabJson 含 PrefabInfo 块', () => { const layer = { layerId: '2', layerName: 'Login_Layer', spirits: [] }; const json = emitPrefabJson(layer); const prefabInfo = json.find(o => o.__type__ === 'cc.PrefabInfo'); assert.ok(prefabInfo, '必须有 PrefabInfo 块'); assert.equal(prefabInfo.root.__id__, 1); // 根节点 id=1 assert.equal(prefabInfo.asset.__id__, 0); }); ``` - [ ] **Step 2: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/prefab-emitter.test.mjs ``` 期望:FAIL `emitPrefabJson is not a function`。 - [ ] **Step 3: 实现 emitPrefabJson(最小骨架)** ```javascript export function emitPrefabJson(layer, opts = {}) { const out = []; let nextId = 0; // 1. cc.Prefab 头 out.push({ __type__: 'cc.Prefab', _name: layer.layerName, _objFlags: 0, __editorExtras__: {}, _native: '', data: { __id__: 1 }, // 根节点 id optimizationPolicy: 0, persistent: false, }); nextId = 2; // 2. group-X 父节点 + 根节点子节点(按 buildGroupParents 结果) const { groupParents, rootChildSpiritIds } = buildGroupParents(layer.spirits); const spiritById = new Map(layer.spirits.map(s => [s.id, s])); // 根节点(暂用 placeholder) const rootNodeId = 1; const rootNode = { __type__: 'cc.Node', _name: layer.layerName, _objFlags: 0, __editorExtras__: {}, _parent: null, _children: [], // 后续填 _active: true, _components: [], // 后续填 _prefab: { __id__: -1 }, // 后续修正 _lpos: { __type__: 'cc.Vec3', x: 0, y: 0, z: 0 }, _lrot: { __type__: 'cc.Quat', x: 0, y: 0, z: 0, w: 1 }, _lscale: { __type__: 'cc.Vec3', x: 1, y: 1, z: 1 }, _mobility: 0, _layer: 1073741824, _euler: { __type__: 'cc.Vec3', x: 0, y: 0, z: 0 }, _id: '', }; // ... 完整实现略,需要 ~400 行 return out; } ``` > **⚠️ 这是本计划最复杂的实现**。完整 emitPrefabJson 需要: > - 节点 ID 分配 > - 每个 Spirit → cc.Node + UITransform + 组件(Sprite/Label/etc) + CompPrefabInfo + Widget(如有锚定) > - 锚点转换(调 computeLpos) > - SpriteFrame UUID 解析(调 resolveSpriteFrameUuid) > - group-X 父节点构建 + children 数组填充 > - PrefabInfo 块(含正确的 root + asset + fileId 引用) > - 输出顺序:Prefab → 根 Node → group 节点 → Spirit 子节点 → 组件 → 引用块 → PrefabInfo - [ ] **Step 4: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/prefab-emitter.test.mjs ``` 期望:3 个 test PASS。 - [ ] **Step 5: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/prefab-emitter.test.mjs git commit -m "feat(scripts): emitPrefabJson - 按 Login_Layer.prefab schema 输出 prefab JSON" ``` --- ## Task 9: PoC 阶段 1 - IconButton 端到端 **Files:** - Create: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab-cli.mjs`(CLI 入口) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/poc-iconbutton.test.mjs` **Interfaces:** - Consumes: 单个 Layer XML 路径 + 输出目录 - Produces: `.prefab` 文件 + 与 YouleNexus 现有 prefab 的 .diff - [ ] **Step 1: 写 CLI 入口** `scripts/legacy-layer-to-cocos-prefab-cli.mjs`: ```javascript #!/usr/bin/env node import { readLayerXml, computeLpos, computeContentSize, buildGroupParents, mapSpiritTypeToComponent, resolveSpriteFrameUuid, emitPrefabJson } from './legacy-layer-to-cocos-prefab.mjs'; import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs'; import { execSync } from 'node:child_process'; import { dirname, basename, join } from 'node:path'; const [, , inputXml, youlePrefab, outputDir] = process.argv; if (!inputXml || !outputDir) { console.error('Usage: node legacy-layer-to-cocos-prefab-cli.mjs '); process.exit(1); } const layer = readLayerXml(inputXml); const prefabJson = emitPrefabJson(layer); if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true }); const outputPath = join(outputDir, `${layer.layerName}.prefab`); writeFileSync(outputPath, JSON.stringify(prefabJson, null, 2)); console.log(`Generated: ${outputPath}`); if (youlePrefab && existsSync(youlePrefab)) { const diffPath = `${outputPath}.diff`; const diff = execSync(`diff -u "${youlePrefab}" "${outputPath}" || true`, { encoding: 'utf8' }); writeFileSync(diffPath, diff); console.log(`Diff: ${diffPath} (${diff.split('\n').length} lines)`); } ``` - [ ] **Step 2: 选 IconButton PoC 目标** 由于 IconButton 在 Layer 顶层 NO_MATCH,PoC 改用 **Layer00619.xml BackHall_Layer**(1 个 Spirit,最简结构,与 IconButton 复杂度类似)。 ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects mkdir -p YouleNexus/out/legacy-migration node scripts/legacy-layer-to-cocos-prefab-cli.mjs \ ../projects/Game_Surface_3/save/Layer00619.xml \ YouleNexus/assets/framework/ui/prefabs/widgets/Layer619_BackOtherGame.prefab \ YouleNexus/out/legacy-migration/ ``` 期望:生成 `YouleNexus/out/legacy-migration/BackHall_Layer.prefab` + `.diff` 文件。 - [ ] **Step 3: 写端到端测试** `framework-tests/legacy-layer-migration/poc-iconbutton.test.mjs`: ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; test('Layer00619 端到端生成 + 校验关键字段', () => { // 跑 CLI execSync('node scripts/legacy-layer-to-cocos-prefab-cli.mjs ../projects/Game_Surface_3/save/Layer00619.xml YouleNexus/assets/framework/ui/prefabs/widgets/Layer619_BackOtherGame.prefab YouleNexus/out/legacy-migration/', { stdio: 'pipe' }); // 校验生成的 prefab 含关键节点 const json = JSON.parse(readFileSync('YouleNexus/out/legacy-migration/BackHall_Layer.prefab', 'utf8')); assert.ok(Array.isArray(json)); assert.equal(json[0].__type__, 'cc.Prefab'); assert.equal(json[0]._name, 'BackHall_Layer'); // 校验含 group-55 父节点(BackHall_Layer 的唯一 group) const groupNode = json.find(o => o.__type__ === 'cc.Node' && o._name === 'group-55'); assert.ok(groupNode, '应有 group-55 父节点'); }); ``` - [ ] **Step 4: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/poc-iconbutton.test.mjs ``` 期望:1 个 test PASS。 - [ ] **Step 5: 人工 diff 评审** ```bash cat YouleNexus/out/legacy-migration/BackHall_Layer.prefab.diff ``` 检查 diff 合理性:精灵节点位置/尺寸/SpriteFrame UUID 是否与 Layer00006.xml 一致。 - [ ] **Step 6: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab-cli.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/poc-iconbutton.test.mjs YouleNexus/out/legacy-migration/ git commit -m "feat(scripts): PoC 阶段 1 - CLI + IconButton(BackHall) 端到端跑通" ``` --- ## Task 10: PoC 阶段 2 - Layer10_Protocol(验证 group + Widget 锚定) **Files:** - Modify: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(补 Widget 锚定逻辑) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/poc-protocol.test.mjs` **Interfaces:** - Consumes: Layer00010.xml(Protol_Layer,group 37,4 个 Spirit,type 0/5) - Produces: 含 Widget 锚定的完整 prefab JSON - [ ] **Step 1: 写失败测试** ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; test('Layer10_Protocol 生成 + 含 cc.Widget 节点', () => { execSync('node scripts/legacy-layer-to-cocos-prefab-cli.mjs ../projects/Game_Surface_3/save/Layer00010.xml YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab YouleNexus/out/legacy-migration/', { stdio: 'pipe' }); const json = JSON.parse(readFileSync('YouleNexus/out/legacy-migration/Protol_Layer.prefab', 'utf8')); const widgetNodes = json.filter(o => o.__type__ === 'cc.Widget'); assert.ok(widgetNodes.length > 0, '应有 cc.Widget 节点(验证 Widget 锚定逻辑)'); }); ``` - [ ] **Step 2: 跑测试看失败** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/poc-protocol.test.mjs ``` 期望:FAIL(Widget 逻辑未实现)。 - [ ] **Step 3: 实现 computeWidgetAlignment(在 emitPrefabJson 中调用)** 在 `scripts/legacy-layer-to-cocos-prefab.mjs` 追加: ```javascript export function computeWidgetAlignment(spirit, parentSize) { // 简化版:原项目 Spirit.Y < 360(屏幕上半)→ _verticalCenter > 0(向上) // 原项目 Spirit.Y > 360 → _verticalCenter < 0(向下) // 完整版需逆向 gameabc.min.js 的 SizeCalcMode / CoordRelaX/Y 计算 // 本 PoC 仅实现"水平/垂直居中"两种最简情况 const y = parseFloat(spirit.y || '0'); const x = parseFloat(spirit.x || '0'); return { _alignFlags: 16 | 8, // 16=水平居中 + 8=垂直居中 _target: null, _left: 0, _right: 0, _top: 0, _bottom: 0, _horizontalCenter: x + parseFloat(spirit.w || '0') / 2 - 640, _verticalCenter: 360 - (y + parseFloat(spirit.h || '0') / 2), _isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true, _isAbsHorizontalCenter: true, _isAbsVerticalCenter: true, _originalWidth: 0, _originalHeight: 0, _alignMode: 2, _lockFlags: 0, }; } ``` 修改 `emitPrefabJson` 让每个 Sprite/Label 节点如果有 group-X 父节点**且**父节点是 group 容器(非 Layer 根),则额外挂 cc.Widget。 - [ ] **Step 4: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/poc-protocol.test.mjs ``` 期望:1 个 test PASS。 - [ ] **Step 5: 人工 diff 评审** ```bash cat YouleNexus/out/legacy-migration/Protol_Layer.prefab.diff ``` 确认 Widget _alignFlags / _horizontalCenter / _verticalCenter 与 Layer00010.xml 几何一致。 - [ ] **Step 6: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/poc-protocol.test.mjs git commit -m "feat(scripts): PoC 阶段 2 - Layer10_Protocol + Widget 锚定" ``` --- ## Task 11: PoC 阶段 3 - Login_Layer 完整 1:1 对比 **Files:** - Modify: `cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs`(按 Login_Layer diff 修复) - Create: `cocoscreator_projects/framework-tests/legacy-layer-migration/poc-login.test.mjs` - [ ] **Step 1: 跑 CLI + 看 diff** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node scripts/legacy-layer-to-cocos-prefab-cli.mjs \ ../projects/Game_Surface_3/save/Layer00002.xml \ YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab \ YouleNexus/out/legacy-migration/ cat YouleNexus/out/legacy-migration/Login_Layer.prefab.diff | head -100 ``` - [ ] **Step 2: 写端到端 schema 对比测试** `framework-tests/legacy-layer-migration/poc-login.test.mjs`: ```javascript import { test } from 'node:test'; import assert from 'node:assert/strict'; import { execSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; test('Login_Layer 端到端 - 关键节点 _lpos 误差 < 0.001', () => { execSync('node scripts/legacy-layer-to-cocos-prefab-cli.mjs ../projects/Game_Surface_3/save/Layer00002.xml YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab YouleNexus/out/legacy-migration/', { stdio: 'pipe' }); const generated = JSON.parse(readFileSync('YouleNexus/out/legacy-migration/Login_Layer.prefab', 'utf8')); const expected = JSON.parse(readFileSync('YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab', 'utf8')); // 找渠道logo 节点(_name 包含"渠道") const genLogo = generated.find(o => o.__type__ === 'cc.Node' && o._name && o._name.includes('渠道')); const expLogo = expected.find(o => o.__type__ === 'cc.Node' && o._name && o._name.includes('渠道')); assert.ok(genLogo); assert.ok(expLogo); assert.ok(Math.abs(genLogo._lpos.x - expLogo._lpos.x) < 0.001, `x mismatch: gen=${genLogo._lpos.x} exp=${expLogo._lpos.x}`); assert.ok(Math.abs(genLogo._lpos.y - expLogo._lpos.y) < 0.001, `y mismatch: gen=${genLogo._lpos.y} exp=${expLogo._lpos.y}`); // 找游客登录 节点 const genVisitor = generated.find(o => o.__type__ === 'cc.Node' && o._name && o._name.includes('游客')); const expVisitor = expected.find(o => o.__type__ === 'cc.Node' && o._name && o._name.includes('游客')); assert.ok(genVisitor); assert.ok(expVisitor); assert.ok(Math.abs(genVisitor._lpos.x - expVisitor._lpos.x) < 0.001); assert.ok(Math.abs(genVisitor._lpos.y - expVisitor._lpos.y) < 0.001); }); ``` - [ ] **Step 3: 跑测试看通过** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node --test framework-tests/legacy-layer-migration/poc-login.test.mjs ``` 期望:1 个 test PASS(渠道logo + 游客登录 _lpos 误差 < 0.001)。 - [ ] **Step 4: 人工 diff 完整评审** ```bash cat YouleNexus/out/legacy-migration/Login_Layer.prefab.diff ``` 按 diff 修复脚本(如某个 Spirit 类型错、Widget 计算不对、SpriteFrame UUID 缺失等)。重复 Step 1-3 直到 diff 接近零差异(除 fileId 等随机字段)。 - [ ] **Step 5: Commit** ```bash git add cocoscreator_projects/scripts/legacy-layer-to-cocos-prefab.mjs cocoscreator_projects/framework-tests/legacy-layer-migration/poc-login.test.mjs git commit -m "feat(scripts): PoC 阶段 3 - Login_Layer 完整 1:1 对比通过" ``` --- ## Task 12: 批量跑剩 8 个 prefab + 最终 commit **Files:** - Create: `cocoscreator_projects/scripts/run-legacy-migration-batch.mjs`(批量调用 CLI) - Modify: `cocoscreator_projects/framework-tests/legacy-layer-migration/`(按需加回归测试) **Interfaces:** - Consumes: 拍板的 8 个 Layer XML(剩 Login_Layer 已做完) - Produces: 8 个 .prefab + 8 个 .diff - [ ] **Step 1: 写批量脚本** `scripts/run-legacy-migration-batch.mjs`: ```javascript #!/usr/bin/env node import { execSync } from 'node:child_process'; const targets = [ { xml: 'Layer00017.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab', name: 'MenuPlayerInfo_Layer' }, { xml: 'Layer00029.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Group106_PhoneVerify.prefab', name: 'phoneInfo' }, { xml: 'Layer00609.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer609_Setting.prefab', name: 'Setting_Layer' }, { xml: 'Layer00612.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer612_Tips.prefab', name: 'Tips_Layer' }, { xml: 'Layer00613.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer613_RoomCardUpdate.prefab', name: 'updateRoomCard_Layer' }, { xml: 'Layer00614.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab', name: 'Loading_Layer' }, { xml: 'Layer00615.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab', name: 'Reconnect_Layer' }, { xml: 'Layer00616.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab', name: 'Kick_Layer' }, { xml: 'Layer00008.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer617_Notice.prefab', name: 'Notice_Layer' }, { xml: 'Layer00620.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer620_Record.prefab', name: 'recordLayer' }, ]; for (const t of targets) { console.log(`\n=== Processing ${t.name} ===`); execSync(`node scripts/legacy-layer-to-cocos-prefab-cli.mjs ../projects/Game_Surface_3/save/${t.xml} ${t.prefab} YouleNexus/out/legacy-migration/`, { stdio: 'inherit' }); } ``` - [ ] **Step 2: 跑批量** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node scripts/run-legacy-migration-batch.mjs ``` 期望:10 个 prefab 全部生成(含 Login_Layer 和 Layer619)+ 各自 .diff。 - [ ] **Step 3: 人工评审所有 .diff** ```bash cd YouleNexus/out/legacy-migration/ for f in *.diff; do echo "=== $f ===" wc -l "$f" done ``` 逐个看 .diff,对每个 prefab 决定是否覆盖 YouleNexus 现有版本。**不自动覆盖**——决策由人审。 - [ ] **Step 4: 把批量脚本加到 scripts/run-framework-tests.mjs** 读 `scripts/run-framework-tests.mjs` 把所有 legacy-layer-migration 测试文件追加: ```javascript const legacyMigrationTests = [ 'xml-parser.test.mjs', 'coord-calculator.test.mjs', 'group-builder.test.mjs', 'spirit-to-node.test.mjs', 'sprite-uuid-resolver.test.mjs', 'prefab-emitter.test.mjs', 'poc-iconbutton.test.mjs', 'poc-protocol.test.mjs', 'poc-login.test.mjs', 'spirit-type-reverse.test.mjs', ]; ``` - [ ] **Step 5: 跑全套测试** ```bash cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects node scripts/run-framework-tests.mjs ``` 期望:所有 test PASS。 - [ ] **Step 6: 最终 commit** ```bash git add cocoscreator_projects/scripts/ cocoscreator_projects/framework-tests/legacy-layer-migration/ YouleNexus/out/legacy-migration/ git commit -m "feat(scripts): legacy-layer → Cocos prefab 批量迁移完成(11 个 prefab 候选 + diff)" ``` --- ## Self-Review ### Spec coverage check | Spec 章节 | 对应 Task | |---|---| | Goal / 范围 / 11 prefab 映射 | Task 1, 12 | | 数据流(脚本 + 测试 + fixtures) | Task 1 | | 输入契约(GB18030 XML + gameabc_Image.json) | Task 1, 7 | | 输出契约(prefab JSON + diff) | Task 8, 9, 11 | | 关键映射规则(SpiritType / ImgResID / BelongGroupID / 锚点) | Task 3, 4, 6, 7 | | 错误处理(throw + 列未映射) | Task 3, 4, 6, 7(每个 throw 都 throw 具体信息) | | 测试策略(7 个测试文件 + fixtures) | Task 2-7 | | PoC 4 阶段 | Task 9 (阶段1), Task 10 (阶段2), Task 11 (阶段3), Task 12 (阶段4) | | 不做的事 | 全 plan 不做(明确边界) | ✅ 所有 spec 章节都被某个 Task 覆盖。 ### Placeholder scan 无 "TBD"/"TODO"/"implement later"。所有"待逆向 gameabc.min.js"都是 Task 5 明确要做的步骤,不是 placeholder。 ### Type consistency - `readLayerXml` 返回结构一致于 Task 2/3/4/7/8 接口定义 - `computeLpos(spirit)` 接受 `{ x, y, w, h, ... }` → `{ x, y, z }`,Task 3 + Task 8 一致 - `buildGroupParents(spirits)` 返回 `{ groupParents, rootChildSpiritIds }`,Task 4 + Task 8 一致 - `mapSpiritTypeToComponent(type)` 返回 `cc.Sprite` / `cc.Label` 等,Task 6 + Task 8 一致 - `resolveSpriteFrameUuid(imgResId)` 返回 `string`,Task 7 + Task 8 一致 - `emitPrefabJson(layer)` 返回 `JSON array`,Task 8 + Task 9/10/11 测试一致 --- ## Execution Handoff Plan complete and saved to `docs/superpowers/plans/2026-09-02-legacy-layer-migration.md`. Two execution options: **1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration. Best for this plan because each Task (esp. Task 8 prefab-emitter) is complex enough to benefit from isolated context. **2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints for review. Best if you want to keep direct oversight on every step. Which approach?