主要改动: - 切到 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>
50 KiB
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-liteGB18030 解码(实测 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 不支持
--testglob:测试文件须显式枚举(用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 初始化,然后:
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus
npm install --save iconv-lite @xmldom/xmldom
期望:package.json 出现 "iconv-lite" 和 "@xmldom/xmldom" 条目。
- Step 2: 创建目录 + 复制 fixtures
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 骨架
{
"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:
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
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node framework-tests/legacy-layer-migration/_smoke-iconv.mjs
期望:3 行输出,decoded length 在 5000-10000 之间。
- Step 6: Commit
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:
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: 跑测试看失败
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 头部:
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: 跑测试看通过
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node --test framework-tests/legacy-layer-migration/xml-parser.test.mjs
期望:3 个 test 全 PASS。
- Step 5: Commit
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: 写失败测试
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: 跑测试看失败
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:
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: 跑测试看通过
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
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: 写失败测试
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: 跑测试看失败
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
追加:
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: 跑测试看通过
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node --test framework-tests/legacy-layer-migration/group-builder.test.mjs
期望:4 个 test PASS。
- Step 5: Commit
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 相关字符串
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 语义
# 找一个含 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:
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: 跑测试看失败
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:
{
"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: 跑测试看通过
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
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: 写失败测试
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: 跑测试看失败
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
追加:
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: 跑测试看通过
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node --test framework-tests/legacy-layer-migration/spirit-to-node.test.mjs
期望:6 个 test PASS。
- Step 5: Commit
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) → "<uuid>@f9941"(SpriteFrame 真 UUID 含 sub-asset 后缀) -
Step 1: 解析 gameabc_Image.json 结构
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)
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: 跑测试看失败
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 实现,伪代码:
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: 跑测试看通过
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node --test framework-tests/legacy-layer-migration/sprite-uuid-resolver.test.mjs
期望:2 个 test PASS。
- Step 6: Commit
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)
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: 跑测试看失败
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(最小骨架)
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: 跑测试看通过
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node --test framework-tests/legacy-layer-migration/prefab-emitter.test.mjs
期望:3 个 test PASS。
- Step 5: Commit
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:
<Name>.prefab文件 + 与 YouleNexus 现有 prefab 的 .diff -
Step 1: 写 CLI 入口
scripts/legacy-layer-to-cocos-prefab-cli.mjs:
#!/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 <input.xml> <youle-prefab-path?> <output-dir>');
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 复杂度类似)。
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:
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: 跑测试看通过
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node --test framework-tests/legacy-layer-migration/poc-iconbutton.test.mjs
期望:1 个 test PASS。
- Step 5: 人工 diff 评审
cat YouleNexus/out/legacy-migration/BackHall_Layer.prefab.diff
检查 diff 合理性:精灵节点位置/尺寸/SpriteFrame UUID 是否与 Layer00006.xml 一致。
- Step 6: Commit
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: 写失败测试
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: 跑测试看失败
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 追加:
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: 跑测试看通过
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node --test framework-tests/legacy-layer-migration/poc-protocol.test.mjs
期望:1 个 test PASS。
- Step 5: 人工 diff 评审
cat YouleNexus/out/legacy-migration/Protol_Layer.prefab.diff
确认 Widget _alignFlags / _horizontalCenter / _verticalCenter 与 Layer00010.xml 几何一致。
- Step 6: Commit
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
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:
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: 跑测试看通过
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 完整评审
cat YouleNexus/out/legacy-migration/Login_Layer.prefab.diff
按 diff 修复脚本(如某个 Spirit 类型错、Widget 计算不对、SpriteFrame UUID 缺失等)。重复 Step 1-3 直到 diff 接近零差异(除 fileId 等随机字段)。
- Step 5: Commit
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:
#!/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: 跑批量
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node scripts/run-legacy-migration-batch.mjs
期望:10 个 prefab 全部生成(含 Login_Layer 和 Layer619)+ 各自 .diff。
- Step 3: 人工评审所有 .diff
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 测试文件追加:
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: 跑全套测试
cd G:/Works/YouleGamesCocosCreator/cocoscreator_projects
node scripts/run-framework-tests.mjs
期望:所有 test PASS。
- Step 6: 最终 commit
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?