fix(scripts): final review Critical+Important fixes (Widget alignment, full readLayerXml, Label _string, atlas selection, hard-coded paths, exit code, dead code removal)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1
-36
@@ -4,42 +4,7 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "YouleNexus",
|
"name": "YouleNexus"
|
||||||
"dependencies": {
|
|
||||||
"@xmldom/xmldom": "^0.9.12",
|
|
||||||
"iconv-lite": "^0.7.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@xmldom/xmldom": {
|
|
||||||
"version": "0.9.12",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.12.tgz",
|
|
||||||
"integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/iconv-lite": {
|
|
||||||
"version": "0.7.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
|
||||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,5 @@
|
|||||||
"uuid": "15252ac4-d24e-4b2c-86b0-51caaf24f779",
|
"uuid": "15252ac4-d24e-4b2c-86b0-51caaf24f779",
|
||||||
"creator": {
|
"creator": {
|
||||||
"version": "3.8.8"
|
"version": "3.8.8"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@xmldom/xmldom": "^0.9.12",
|
|
||||||
"iconv-lite": "^0.7.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import iconv from 'iconv-lite';
|
|
||||||
|
|
||||||
const buf = readFileSync('framework-tests/legacy-layer-migration/fixtures/Layer00002.xml');
|
|
||||||
// NOTE: 实际 Layer00002.xml 是 UTF-8 编码 (XML 头声明 + 字节验证),
|
|
||||||
// spec/brief 要求的 GB18030 在该文件上会乱码 (微信登录背景 不可读).
|
|
||||||
// 转换工具应支持两种编码, 优先按 XML 声明, 否则按 GB18030 fallback.
|
|
||||||
const text = iconv.decode(buf, 'utf8');
|
|
||||||
console.log('decoded length:', text.length);
|
|
||||||
console.log('contains 微信登录背景:', text.includes('微信登录背景'));
|
|
||||||
console.log('contains Login_Layer:', text.includes('Login_Layer'));
|
|
||||||
+46
-1
@@ -1,6 +1,6 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { computeLpos, computeContentSize } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
import { computeLpos, computeContentSize, computeWidgetAlignment } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
||||||
|
|
||||||
test('computeLpos 锚点转换(实测验证:渠道logo)', () => {
|
test('computeLpos 锚点转换(实测验证:渠道logo)', () => {
|
||||||
const spirit = { x: '734', y: '143', w: '59', h: '148' };
|
const spirit = { x: '734', y: '143', w: '59', h: '148' };
|
||||||
@@ -48,3 +48,48 @@ test('computeContentSize SizeCalcMode=1 → throw(不静默)', () => {
|
|||||||
const spirit = { w: '100', h: '50', sizeCalcMode: '1' };
|
const spirit = { w: '100', h: '50', sizeCalcMode: '1' };
|
||||||
assert.throws(() => computeContentSize(spirit), /SizeCalcMode=1.*reverse gameabc\.min\.js/);
|
assert.throws(() => computeContentSize(spirit), /SizeCalcMode=1.*reverse gameabc\.min\.js/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('computeContentSize SizeCalcMode=2 uses ImgResID natural size and relative ratios', () => {
|
||||||
|
const spirit = {
|
||||||
|
id: '2', imgResID: '20', w: '1280', h: '720', sizeCalcMode: '2',
|
||||||
|
sizeRelaWidth: '10000', sizeRelaHeight: '10000',
|
||||||
|
};
|
||||||
|
const size = computeContentSize(spirit);
|
||||||
|
assert.equal(size.width, 1280);
|
||||||
|
assert.equal(size.height, 720);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('computeWidgetAlignment derives full-screen, normal, and top-left alignment cases', () => {
|
||||||
|
const fullScreen = computeWidgetAlignment({
|
||||||
|
name: '微信登录背景', x: '0', y: '0', w: '1280', h: '720', lpos: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
assert.equal(fullScreen._alignFlags, 45);
|
||||||
|
assert.equal(fullScreen._left, 0);
|
||||||
|
assert.equal(fullScreen._right, 0);
|
||||||
|
assert.equal(fullScreen._top, 0);
|
||||||
|
assert.equal(fullScreen._bottom, 0);
|
||||||
|
assert.equal(fullScreen._horizontalCenter, undefined);
|
||||||
|
assert.equal(fullScreen._verticalCenter, undefined);
|
||||||
|
assert.equal(fullScreen._originalWidth, 100);
|
||||||
|
assert.equal(fullScreen._originalHeight, 100);
|
||||||
|
|
||||||
|
const normal = computeWidgetAlignment({
|
||||||
|
name: '渠道logo', x: '734', y: '143', w: '59', h: '148', lpos: { x: 123.5, y: 0 },
|
||||||
|
});
|
||||||
|
assert.equal(normal._alignFlags, 16);
|
||||||
|
assert.equal(normal._horizontalCenter, 123.5);
|
||||||
|
assert.equal(normal._verticalCenter, 0);
|
||||||
|
assert.equal(normal._left, undefined);
|
||||||
|
assert.equal(normal._right, undefined);
|
||||||
|
assert.equal(normal._top, undefined);
|
||||||
|
assert.equal(normal._bottom, undefined);
|
||||||
|
|
||||||
|
const topLeft = computeWidgetAlignment({
|
||||||
|
name: '版本显示', x: '16', y: '9', w: '96', h: '31',
|
||||||
|
});
|
||||||
|
assert.equal(topLeft._alignFlags, 9);
|
||||||
|
assert.equal(topLeft._left, -576);
|
||||||
|
assert.equal(topLeft._top, 335.5);
|
||||||
|
assert.equal(topLeft._horizontalCenter, undefined);
|
||||||
|
assert.equal(topLeft._verticalCenter, undefined);
|
||||||
|
});
|
||||||
+1
-1
@@ -34,7 +34,7 @@
|
|||||||
"rationale": "直接读精灵自身 Width/Height 字段作为 _contentSize; gameabc.min.js 中 SizeCalcMode=0 是最简路径 (背景图 Layer00001/00002 全屏 1280×720 即此路径)"
|
"rationale": "直接读精灵自身 Width/Height 字段作为 _contentSize; gameabc.min.js 中 SizeCalcMode=0 是最简路径 (背景图 Layer00001/00002 全屏 1280×720 即此路径)"
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"formula": "parent_size = lookup_Spirit(Spirit.SizeParentID)._contentSize; _contentSize = { width: parent_size.width * Spirit.SizeRelaWidth / 10000, height: parent_size.height * Spirit.SizeRelaHeight / 10000 }; 当 WidthCalcMode=0 时回退到 Spirit.Width (parseFloat), 当 WidthCalcMode=1 时用 SizeRelaWidth 公式; HeightCalcMode 同理",
|
"formula": "image_natural_size = lookup gameabc_Image.json ImageFileList[imgResID === Spirit.ImgResID].w_all/h_all; parent_size = { width: image_natural_size.width, height: image_natural_size.height }; _contentSize = { width: parent_size.width * Spirit.SizeRelaWidth / 10000, height: parent_size.height * Spirit.SizeRelaHeight / 10000 }; 当 WidthCalcMode=0 时回退到 Spirit.Width (parseFloat), 当 WidthCalcMode=1 时用 SizeRelaWidth 公式; HeightCalcMode 同理",
|
||||||
"rationale": "Layer00001 '友乐背景' (SizeCalcMode=2, SizeParentID=21, SizeRelaWidth=10000) / Layer00002 '微信登录背景' (SizeCalcMode=2, SizeParentID=20, SizeRelaWidth=10000) 全量采样: 全部 WidthCalcMode=1, HeightCalcMode=1, SizeRelaWidth=10000, SizeRelaHeight=10000; 即按 100% 比例继承父 spirit 尺寸; 这种 pattern 用于 '充满父容器' 的背景 (1280×720 全屏背景), SizeParentID 指向另一个 1280×720 的 spirit"
|
"rationale": "Layer00001 '友乐背景' (SizeCalcMode=2, SizeParentID=21, SizeRelaWidth=10000) / Layer00002 '微信登录背景' (SizeCalcMode=2, SizeParentID=20, SizeRelaWidth=10000) 全量采样: 全部 WidthCalcMode=1, HeightCalcMode=1, SizeRelaWidth=10000, SizeRelaHeight=10000; 即按 100% 比例继承父 spirit 尺寸; 这种 pattern 用于 '充满父容器' 的背景 (1280×720 全屏背景), SizeParentID 指向另一个 1280×720 的 spirit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
||||||
|
import { ROOT, HOST_PROJECT } from '../../scripts/lib/paths.mjs';
|
||||||
|
|
||||||
// PoC 阶段 3 端到端: Layer00002.xml Login_Layer (18 spirits, group 2, 包含 Spirit405 渠道logo +
|
// PoC 阶段 3 端到端: Layer00002.xml Login_Layer (18 spirits, group 2, 包含 Spirit405 渠道logo +
|
||||||
// Spirit177 游客登录 anchor-test cases). 验证生成 prefab 与 fixtures/Login_Layer.prefab 在
|
// Spirit177 游客登录 anchor-test cases). 验证生成 prefab 与 fixtures/Login_Layer.prefab 在
|
||||||
@@ -16,8 +18,8 @@ import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-coco
|
|||||||
// layer.spirits 只保留 id=405 (渠道logo) + id=177 (游客登录) 两个 anchor-test case, 完全避开
|
// layer.spirits 只保留 id=405 (渠道logo) + id=177 (游客登录) 两个 anchor-test case, 完全避开
|
||||||
// ImgResID 427 的依赖. 这是 Task 11 fix round 1 的 spec intent: 验证 anchor 公式精度,
|
// ImgResID 427 的依赖. 这是 Task 11 fix round 1 的 spec intent: 验证 anchor 公式精度,
|
||||||
// 而不是验证 CLI 完整管线 (后者已由 poc-iconbutton.test.mjs / poc-protocol.test.mjs 覆盖).
|
// 而不是验证 CLI 完整管线 (后者已由 poc-iconbutton.test.mjs / poc-protocol.test.mjs 覆盖).
|
||||||
const FIXTURE_XML = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml';
|
const FIXTURE_XML = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Layer00002.xml');
|
||||||
const REFERENCE_PREFAB = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab';
|
const REFERENCE_PREFAB = join(HOST_PROJECT, 'assets', 'framework', 'ui', 'prefabs', 'Login_Layer.prefab');
|
||||||
|
|
||||||
function buildFilteredLayer() {
|
function buildFilteredLayer() {
|
||||||
const layer = readLayerXml(FIXTURE_XML);
|
const layer = readLayerXml(FIXTURE_XML);
|
||||||
@@ -27,6 +29,43 @@ function buildFilteredLayer() {
|
|||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nodeComponent(json, node, componentType) {
|
||||||
|
return json[node._components.map(ref => ref.__id__).find(index => json[index]?.__type__ === componentType)];
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Login_Layer full-screen and normal node geometry matches reference strictly', () => {
|
||||||
|
const layer = readLayerXml(FIXTURE_XML);
|
||||||
|
layer.spirits = layer.spirits.filter(s => s.id === '2' || s.id === '405');
|
||||||
|
const generated = emitPrefabJson(layer);
|
||||||
|
const expected = JSON.parse(readFileSync(REFERENCE_PREFAB, 'utf8'));
|
||||||
|
|
||||||
|
const background = generated.find(o => o.__type__ === 'cc.Node' && o._name === '微信登录背景');
|
||||||
|
const backgroundExpected = expected.find(o => o.__type__ === 'cc.Node' && o._name === '微信登录背景');
|
||||||
|
assert.ok(background);
|
||||||
|
assert.ok(backgroundExpected);
|
||||||
|
assert.equal(background._lpos.x, backgroundExpected._lpos.x);
|
||||||
|
assert.equal(background._lpos.y, backgroundExpected._lpos.y);
|
||||||
|
assert.equal(nodeComponent(generated, background, 'cc.Widget')._alignFlags, 45);
|
||||||
|
assert.equal(nodeComponent(generated, background, 'cc.Widget')._originalWidth, 100);
|
||||||
|
assert.equal(nodeComponent(generated, background, 'cc.Widget')._originalHeight, 100);
|
||||||
|
assert.equal(nodeComponent(generated, background, 'cc.UITransform')._contentSize.width, 1600);
|
||||||
|
assert.equal(nodeComponent(generated, background, 'cc.UITransform')._contentSize.height, 720);
|
||||||
|
|
||||||
|
const normal = generated.find(o => o.__type__ === 'cc.Node' && o._name === 'Spirit405');
|
||||||
|
const normalExpected = expected.find(o => o.__type__ === 'cc.Node' && o._name === '渠道logo');
|
||||||
|
assert.ok(normal);
|
||||||
|
assert.ok(normalExpected);
|
||||||
|
assert.equal(normal._lpos.x, normalExpected._lpos.x);
|
||||||
|
assert.equal(normal._lpos.y, normalExpected._lpos.y);
|
||||||
|
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._alignFlags, 16);
|
||||||
|
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._horizontalCenter, 123.5);
|
||||||
|
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._verticalCenter, 0);
|
||||||
|
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._left, undefined);
|
||||||
|
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._top, undefined);
|
||||||
|
assert.equal(nodeComponent(generated, normal, 'cc.UITransform')._contentSize.width, 59);
|
||||||
|
assert.equal(nodeComponent(generated, normal, 'cc.UITransform')._contentSize.height, 148);
|
||||||
|
});
|
||||||
|
|
||||||
test('PoC 阶段 3 - 渠道logo _lpos 误差 < 0.001 (bypass CLI to avoid sprite atlas dep)', () => {
|
test('PoC 阶段 3 - 渠道logo _lpos 误差 < 0.001 (bypass CLI to avoid sprite atlas dep)', () => {
|
||||||
const layer = buildFilteredLayer();
|
const layer = buildFilteredLayer();
|
||||||
const json = emitPrefabJson(layer);
|
const json = emitPrefabJson(layer);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|||||||
// Layer00010.xml 4 个 spirit 全属 group 37 (无 Layer 根直挂), 生成的 prefab 应有:
|
// Layer00010.xml 4 个 spirit 全属 group 37 (无 Layer 根直挂), 生成的 prefab 应有:
|
||||||
// - 1 root Widget (default _alignFlags=45, 全屏铺满)
|
// - 1 root Widget (default _alignFlags=45, 全屏铺满)
|
||||||
// - 1 group-37 Widget (default _alignFlags=45)
|
// - 1 group-37 Widget (default _alignFlags=45)
|
||||||
// - 4 Spirit Widget (apply computeWidgetAlignment: _alignFlags=24=16|8 水平+垂直居中)
|
// - 4 Spirit Widget (apply computeWidgetAlignment: normal spirits use _alignFlags=16)
|
||||||
const CLI = 'node scripts/legacy-layer-to-cocos-prefab-cli.mjs';
|
const CLI = 'node scripts/legacy-layer-to-cocos-prefab-cli.mjs';
|
||||||
const INPUT = '../projects/Game_Surface_3/save/Layer00010.xml';
|
const INPUT = '../projects/Game_Surface_3/save/Layer00010.xml';
|
||||||
const TARGET = 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab';
|
const TARGET = 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab';
|
||||||
@@ -32,7 +32,7 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
|
|||||||
`应有 ≥6 个 cc.Widget 节点 (1 root + 1 group + 4 spirit), 实得 ${widgetNodes.length}`
|
`应有 ≥6 个 cc.Widget 节点 (1 root + 1 group + 4 spirit), 实得 ${widgetNodes.length}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2) Spirit Widget 应 _alignFlags=24 (16|8 水平+垂直居中) — 由 computeWidgetAlignment 算出.
|
// 2) Spirit Widget 应使用 normal spirit 的 _alignFlags=16 — 由 computeWidgetAlignment 算出.
|
||||||
// 这是 PoC 阶段 2 的关键验证: 证明 computeWidgetAlignment 真实生效 (而非全部用默认 45).
|
// 这是 PoC 阶段 2 的关键验证: 证明 computeWidgetAlignment 真实生效 (而非全部用默认 45).
|
||||||
// Spirit Widget 与 root/group Widget 的区别: Widget.node.__id__ 指向的 cc.Node
|
// Spirit Widget 与 root/group Widget 的区别: Widget.node.__id__ 指向的 cc.Node
|
||||||
// _name 不为 'Protol_Layer' (root) 也不以 'group-' 开头 (group 父节点).
|
// _name 不为 'Protol_Layer' (root) 也不以 'group-' 开头 (group 父节点).
|
||||||
@@ -48,7 +48,7 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
|
|||||||
`应有 4 个 Spirit Widget, 实得 ${spiritWidgetCount}`
|
`应有 4 个 Spirit Widget, 实得 ${spiritWidgetCount}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3) Spirit Widget _alignFlags 必须等于 24 (= 16 | 8, 水平居中 + 垂直居中).
|
// 3) Spirit Widget _alignFlags 必须等于 16 (normal spirit = horizontal center).
|
||||||
// 这条断言专门验证 computeWidgetAlignment 已被实际调用 — 未实现时所有 Widget
|
// 这条断言专门验证 computeWidgetAlignment 已被实际调用 — 未实现时所有 Widget
|
||||||
// 都用默认 _alignFlags=45, 此断言必 FAIL.
|
// 都用默认 _alignFlags=45, 此断言必 FAIL.
|
||||||
const spiritWidgets = widgetNodes.filter(w => {
|
const spiritWidgets = widgetNodes.filter(w => {
|
||||||
@@ -60,9 +60,11 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
|
|||||||
&& !targetNode._name.startsWith('group-');
|
&& !targetNode._name.startsWith('group-');
|
||||||
});
|
});
|
||||||
for (const w of spiritWidgets) {
|
for (const w of spiritWidgets) {
|
||||||
|
const targetNode = json[w.node.__id__];
|
||||||
|
const expectedFlags = targetNode._name === '协议遮罩' ? 45 : targetNode._name === '协议背景' ? 9 : 16;
|
||||||
assert.equal(
|
assert.equal(
|
||||||
w._alignFlags, 24,
|
w._alignFlags, expectedFlags,
|
||||||
`Spirit Widget 应 _alignFlags=24 (16|8), 实得 ${w._alignFlags}`
|
`Spirit Widget ${targetNode._name} 应 _alignFlags=${expectedFlags}, 实得 ${w._alignFlags}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,30 @@ test('emitPrefabJson 含 PrefabInfo 块', () => {
|
|||||||
assert.equal(prefabInfo.asset.__id__, 0);
|
assert.equal(prefabInfo.asset.__id__, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('emitPrefabJson uses XML Text for labels and targetName overrides the output name', () => {
|
||||||
|
const layer = {
|
||||||
|
layerId: '10', layerName: 'Protol_Layer', spirits: [{
|
||||||
|
id: '1', name: 'Spirit1', text: '登录', type: '1', fontSize: '18', fontColor: '16777215',
|
||||||
|
alignHorz: '0', alignVert: '1', fontBold: '0', wordWrap: '0', lineSpace: '0', rowSpace: '0',
|
||||||
|
x: '100', y: '100', w: '100', h: '30', group: '37', indexOfLayer: '1',
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
const json = emitPrefabJson(layer, { targetName: 'Layer10_Protocol' });
|
||||||
|
assert.equal(json[0]._name, 'Layer10_Protocol');
|
||||||
|
const textNode = json.find(o => o.__type__ === 'cc.Node' && o._name === 'Spirit1');
|
||||||
|
assert.ok(textNode);
|
||||||
|
const label = json.filter(o => o.__type__ === 'cc.Label')[0];
|
||||||
|
assert.equal(label._string, '登录');
|
||||||
|
assert.equal(label._fontSize, 18);
|
||||||
|
assert.equal(label._color.r, 255);
|
||||||
|
assert.equal(label._color.g, 255);
|
||||||
|
assert.equal(label._color.b, 255);
|
||||||
|
assert.equal(label._horizontalAlign, 0);
|
||||||
|
assert.equal(label._verticalAlign, 1);
|
||||||
|
assert.equal(label._isBold, false);
|
||||||
|
assert.equal(label._enableWrapText, false);
|
||||||
|
});
|
||||||
|
|
||||||
test('emitPrefabJson 渠道logo _lpos 误差 < 0.001', () => {
|
test('emitPrefabJson 渠道logo _lpos 误差 < 0.001', () => {
|
||||||
// 注: brief 原版用 spirit name='Spirit405' 但断言搜 /渠道/; 实际 fixture 里 id=405 的 spirit
|
// 注: brief 原版用 spirit name='Spirit405' 但断言搜 /渠道/; 实际 fixture 里 id=405 的 spirit
|
||||||
// 坐标 (734, 143, 59, 148) 严格对应 Login_Layer '渠道logo' 节点; 这里 name 用 '渠道logo'
|
// 坐标 (734, 143, 59, 148) 严格对应 Login_Layer '渠道logo' 节点; 这里 name 用 '渠道logo'
|
||||||
|
|||||||
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { mapSpiritTypeToComponent } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
import { mapSpiritTypeToComponent } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
||||||
|
import { ROOT } from '../../scripts/lib/paths.mjs';
|
||||||
|
|
||||||
// 绝对路径 -- tests 从 cocoscreator_projects/ 运行, 相对路径会解析到仓库根,
|
|
||||||
// 漏掉 cocoscreator_projects/ 段导致 ENOENT (Task 2 fix round 1 Finding 3).
|
|
||||||
const SEMANTICS = JSON.parse(readFileSync(
|
const SEMANTICS = JSON.parse(readFileSync(
|
||||||
'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json',
|
join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'spirit-semantics.json'),
|
||||||
'utf8'
|
'utf8'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -1,9 +1,11 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { ROOT } from '../../scripts/lib/paths.mjs';
|
||||||
|
|
||||||
const SEMANTICS = JSON.parse(readFileSync(
|
const SEMANTICS = JSON.parse(readFileSync(
|
||||||
'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json',
|
join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'spirit-semantics.json'),
|
||||||
'utf8'
|
'utf8'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,6 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { resolveSpriteFrameUuid } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
import { resolveSpriteFrameUuid, selectAtlasForLayerName } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
||||||
|
|
||||||
// 数据源权威: gameabc_Image.json id=20 -> bmp="00020.png" 位于 atlas-login/,
|
// 数据源权威: gameabc_Image.json id=20 -> bmp="00020.png" 位于 atlas-login/,
|
||||||
// 该 .meta 的 subMetas.f9941.uuid 为 e36acab8-e623-4bfb-af05-3c6fd2fddfb4@f9941
|
// 该 .meta 的 subMetas.f9941.uuid 为 e36acab8-e623-4bfb-af05-3c6fd2fddfb4@f9941
|
||||||
@@ -12,6 +12,12 @@ test('resolveSpriteFrameUuid("20") 返回 atlas-login/00020.png 的 SpriteFrame
|
|||||||
assert.match(uuid, /@f9941$/, '必须含 SpriteFrame sub-asset 后缀 @f9941');
|
assert.match(uuid, /@f9941$/, '必须含 SpriteFrame sub-asset 后缀 @f9941');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('layer name prefix selects the expected atlas directory', () => {
|
||||||
|
assert.equal(selectAtlasForLayerName('Login_Layer'), 'atlas-login');
|
||||||
|
assert.equal(selectAtlasForLayerName('MainScene_PlayerInfo'), 'atlas-hall');
|
||||||
|
assert.equal(selectAtlasForLayerName('Other_Layer'), 'atlas-login');
|
||||||
|
});
|
||||||
|
|
||||||
test('未知 imgResId → throw(不静默兜底)', () => {
|
test('未知 imgResId → throw(不静默兜底)', () => {
|
||||||
// 数据源权威唯一、下游不兜底: ImgResID 不在 gameabc_Image.json 时必须显式报错.
|
// 数据源权威唯一、下游不兜底: ImgResID 不在 gameabc_Image.json 时必须显式报错.
|
||||||
assert.throws(
|
assert.throws(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { readLayerXml } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
import { readLayerXml } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
|
||||||
|
import { ROOT } from '../../scripts/lib/paths.mjs';
|
||||||
|
|
||||||
// 绝对路径 -- tests 从 cocoscreator_projects/ 运行, 相对路径会解析到仓库根,
|
const FIXTURE = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Layer00002.xml');
|
||||||
// 漏掉 cocoscreator_projects/ 段导致 ENOENT (Task 2 fix round 1 Finding 3).
|
|
||||||
const FIXTURE = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml';
|
|
||||||
|
|
||||||
test('readLayerXml 解码 UTF-8 并提取 Layer00002 字段', () => {
|
test('readLayerXml 解码 UTF-8 并提取 Layer00002 字段', () => {
|
||||||
const layer = readLayerXml(FIXTURE);
|
const layer = readLayerXml(FIXTURE);
|
||||||
@@ -41,3 +41,34 @@ test('BelongGroupID 提取(group=0 节点也存在)', () => {
|
|||||||
const g0 = layer.spirits.filter(s => s.group === '0');
|
const g0 = layer.spirits.filter(s => s.group === '0');
|
||||||
assert.ok(g0.length >= 0); // Login_Layer 没有 group=0,但提取逻辑应支持
|
assert.ok(g0.length >= 0); // Login_Layer 没有 group=0,但提取逻辑应支持
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('readLayerXml 保留 SizeCalcMode、坐标、归属与 type-1 字体契约字段', () => {
|
||||||
|
const layer = readLayerXml(FIXTURE);
|
||||||
|
const background = layer.spirits.find(s => s.id === '2');
|
||||||
|
const text = layer.spirits.find(s => s.id === '285');
|
||||||
|
assert.ok(background);
|
||||||
|
assert.ok(text);
|
||||||
|
|
||||||
|
assert.equal(background.text, undefined);
|
||||||
|
assert.equal(background.sizeCalcMode, '2');
|
||||||
|
assert.equal(background.sizeParentID, '20');
|
||||||
|
assert.equal(background.widthCalcMode, '1');
|
||||||
|
assert.equal(background.heightCalcMode, '1');
|
||||||
|
assert.equal(background.sizeRelaWidth, '10000');
|
||||||
|
assert.equal(background.sizeRelaHeight, '10000');
|
||||||
|
assert.equal(background.coordSelfPos, '0');
|
||||||
|
assert.equal(background.coordRelaX, '0');
|
||||||
|
assert.equal(background.coordRelaY, '0');
|
||||||
|
assert.equal(background.coordParentID, '0');
|
||||||
|
assert.equal(background.belongLayerID, '2');
|
||||||
|
|
||||||
|
assert.equal(text.text, '版本显示');
|
||||||
|
assert.equal(text.fontSize, '18');
|
||||||
|
assert.equal(text.fontColor, '16777215');
|
||||||
|
assert.equal(text.alignHorz, '0');
|
||||||
|
assert.equal(text.alignVert, '0');
|
||||||
|
assert.equal(text.fontBold, '0');
|
||||||
|
assert.equal(text.wordWrap, '0');
|
||||||
|
assert.equal(text.lineSpace, '0');
|
||||||
|
assert.equal(text.rowSpace, undefined);
|
||||||
|
});
|
||||||
|
|||||||
Generated
+1
-24
@@ -8,8 +8,7 @@
|
|||||||
"name": "youle-monorepo-tools",
|
"name": "youle-monorepo-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xmldom/xmldom": "^0.9.12",
|
"@xmldom/xmldom": "^0.9.12"
|
||||||
"iconv-lite": "^0.7.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^26.0.1",
|
"@types/node": "^26.0.1",
|
||||||
@@ -518,22 +517,6 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/iconv-lite": {
|
|
||||||
"version": "0.7.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
|
||||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pngjs": {
|
"node_modules/pngjs": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-7.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-7.0.0.tgz",
|
||||||
@@ -543,12 +526,6 @@
|
|||||||
"node": ">=14.19.0"
|
"node": ">=14.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.22.4",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz",
|
"resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
"ws": "^8.21.0"
|
"ws": "^8.21.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xmldom/xmldom": "^0.9.12",
|
"@xmldom/xmldom": "^0.9.12"
|
||||||
"iconv-lite": "^0.7.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,47 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// CLI 入口: Layer XML → Cocos prefab JSON (+ 对比 diff, 如提供 YouleNexus 现存 prefab).
|
// CLI 入口: Layer XML → Cocos prefab JSON (+ 对比 diff, 如提供 YouleNexus 现存 prefab).
|
||||||
//
|
//
|
||||||
// 用法: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> [<youle-prefab>] <output-dir>
|
// 用法: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> [<youle-prefab>] <output-dir> [--target-name <name>]
|
||||||
//
|
//
|
||||||
// 设计要点 (Task 9 brief):
|
// 设计要点:
|
||||||
// - 只输出到隔离目录 (out/legacy-migration/), 不直接覆盖 YouleNexus 现有 prefab.
|
// - 只输出到隔离目录 (out/legacy-migration/), 不直接覆盖 YouleNexus 原 prefab.
|
||||||
// - 必填参数: inputXml + outputDir; youlePrefab 可选, 提供且文件存在时生成 .diff.
|
// - targetName 只影响输出的 prefab/root 名称与 atlas 选择; 默认使用 XML layerName.
|
||||||
// - diff 命令来自 Git Bash / POSIX 环境 (Windows: Git for Windows 自带 diff).
|
import { readLayerXml, emitPrefabJson, selectAtlasForLayerName } from './legacy-layer-to-cocos-prefab.mjs';
|
||||||
import { readLayerXml, emitPrefabJson } from './legacy-layer-to-cocos-prefab.mjs';
|
|
||||||
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
const [, , inputXml, youlePrefab, outputDir] = process.argv;
|
const args = process.argv.slice(2);
|
||||||
|
let inputXml = null;
|
||||||
|
let youlePrefab = null;
|
||||||
|
let outputDir = null;
|
||||||
|
let targetName = null;
|
||||||
|
for (let i = 0; i < args.length; i += 1) {
|
||||||
|
const arg = args[i];
|
||||||
|
if (arg === '--target-name') {
|
||||||
|
targetName = args[++i];
|
||||||
|
if (!targetName) throw new Error('--target-name requires a value');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inputXml) inputXml = arg;
|
||||||
|
else if (!youlePrefab) youlePrefab = arg;
|
||||||
|
else if (!outputDir) outputDir = arg;
|
||||||
|
else throw new Error(`Unexpected argument: ${arg}`);
|
||||||
|
}
|
||||||
if (!inputXml || !outputDir) {
|
if (!inputXml || !outputDir) {
|
||||||
console.error('Usage: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> <youle-prefab-path?> <output-dir>');
|
console.error('Usage: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> <youle-prefab-path?> <output-dir> [--target-name <name>]');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const layer = readLayerXml(inputXml);
|
const layer = readLayerXml(inputXml);
|
||||||
const prefabJson = emitPrefabJson(layer);
|
const outputName = targetName || layer.layerName;
|
||||||
|
const prefabJson = emitPrefabJson(layer, {
|
||||||
|
targetName: outputName,
|
||||||
|
atlasName: selectAtlasForLayerName(outputName),
|
||||||
|
});
|
||||||
|
|
||||||
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
||||||
const outputPath = join(outputDir, `${layer.layerName}.prefab`);
|
const outputPath = join(outputDir, `${outputName}.prefab`);
|
||||||
writeFileSync(outputPath, JSON.stringify(prefabJson, null, 2));
|
writeFileSync(outputPath, JSON.stringify(prefabJson, null, 2));
|
||||||
console.log(`Generated: ${outputPath}`);
|
console.log(`Generated: ${outputPath}`);
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,26 @@ export function readLayerXml(path) {
|
|||||||
h: getProp(propList, 'Height'),
|
h: getProp(propList, 'Height'),
|
||||||
group: getProp(propList, 'BelongGroupID'),
|
group: getProp(propList, 'BelongGroupID'),
|
||||||
indexOfLayer: getProp(propList, 'IndexOfLayer'),
|
indexOfLayer: getProp(propList, 'IndexOfLayer'),
|
||||||
|
text: getProp(propList, 'Text'),
|
||||||
|
sizeCalcMode: getProp(propList, 'SizeCalcMode'),
|
||||||
|
sizeParentID: getProp(propList, 'SizeParentID'),
|
||||||
|
widthCalcMode: getProp(propList, 'WidthCalcMode'),
|
||||||
|
heightCalcMode: getProp(propList, 'HeightCalcMode'),
|
||||||
|
sizeRelaWidth: getProp(propList, 'SizeRelaWidth'),
|
||||||
|
sizeRelaHeight: getProp(propList, 'SizeRelaHeight'),
|
||||||
|
coordSelfPos: getProp(propList, 'CoordSelfPos'),
|
||||||
|
coordRelaX: getProp(propList, 'CoordRelaX'),
|
||||||
|
coordRelaY: getProp(propList, 'CoordRelaY'),
|
||||||
|
coordParentID: getProp(propList, 'CoordParentID'),
|
||||||
|
belongLayerID: getProp(propList, 'BelongLayerID'),
|
||||||
|
fontSize: getProp(propList, 'FontSize'),
|
||||||
|
fontColor: getProp(propList, 'FontColor'),
|
||||||
|
alignHorz: getProp(propList, 'AlignHorz'),
|
||||||
|
alignVert: getProp(propList, 'AlignVert'),
|
||||||
|
fontBold: getProp(propList, 'FontBold'),
|
||||||
|
wordWrap: getProp(propList, 'WordWrap'),
|
||||||
|
lineSpace: getProp(propList, 'LineSpace'),
|
||||||
|
rowSpace: getProp(propList, 'RowSpace'),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,14 +117,31 @@ export function computeLpos(spirit) {
|
|||||||
/**
|
/**
|
||||||
* 计算 Spirit 的 contentSize (Cocos UI 节点尺寸).
|
* 计算 Spirit 的 contentSize (Cocos UI 节点尺寸).
|
||||||
*
|
*
|
||||||
* 数据源权威: SizeCalcMode 缺失视作 0 (直接读 Width/Height); 非 0 抛错,
|
* 数据源权威: SizeCalcMode=0 直接读 Width/Height; SizeCalcMode=2 按
|
||||||
* 由上游显式决定是否逆向 gameabc.min.js (Task 5 范围, 本任务不支持).
|
* ImgResID 对应的 image natural size × SizeRela/10000; 其他未知模式显式报错.
|
||||||
*
|
*
|
||||||
* @param {{ w?: string, h?: string, sizeCalcMode?: string, id?: string, name?: string }} spirit
|
* @param {{ w?: string, h?: string, sizeCalcMode?: string, id?: string, name?: string }} spirit
|
||||||
* @returns {{ width: number, height: number }}
|
* @returns {{ width: number, height: number }}
|
||||||
*/
|
*/
|
||||||
export function computeContentSize(spirit) {
|
export function computeContentSize(spirit, naturalSize = null) {
|
||||||
const sizeCalcMode = parseInt(spirit.sizeCalcMode || '0', 10);
|
const sizeCalcMode = parseInt(spirit.sizeCalcMode || '0', 10);
|
||||||
|
if (sizeCalcMode === 1) {
|
||||||
|
throw new Error(`SizeCalcMode=${sizeCalcMode} not yet supported for Spirit ${spirit.id} (${spirit.name}) - reverse gameabc.min.js first (Task 5)`);
|
||||||
|
}
|
||||||
|
if (sizeCalcMode === 2) {
|
||||||
|
const imageNaturalSize = naturalSize || lookupImageNaturalSize(spirit.imgResID);
|
||||||
|
const widthMode = parseInt(spirit.widthCalcMode || '0', 10);
|
||||||
|
const heightMode = parseInt(spirit.heightCalcMode || '0', 10);
|
||||||
|
const widthRatio = parseFloat(spirit.sizeRelaWidth);
|
||||||
|
const heightRatio = parseFloat(spirit.sizeRelaHeight);
|
||||||
|
if (!Number.isFinite(widthRatio) || !Number.isFinite(heightRatio)) {
|
||||||
|
throw new Error(`Spirit ${spirit.id} (${spirit.name}) missing SizeRelaWidth/SizeRelaHeight for SizeCalcMode=2`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: widthMode === 1 ? imageNaturalSize.width * widthRatio / 10000 : parseFloat(spirit.w || '0'),
|
||||||
|
height: heightMode === 1 ? imageNaturalSize.height * heightRatio / 10000 : parseFloat(spirit.h || '0'),
|
||||||
|
};
|
||||||
|
}
|
||||||
if (sizeCalcMode !== 0) {
|
if (sizeCalcMode !== 0) {
|
||||||
throw new Error(`SizeCalcMode=${sizeCalcMode} not yet supported for Spirit ${spirit.id} (${spirit.name}) - reverse gameabc.min.js first (Task 5)`);
|
throw new Error(`SizeCalcMode=${sizeCalcMode} not yet supported for Spirit ${spirit.id} (${spirit.name}) - reverse gameabc.min.js first (Task 5)`);
|
||||||
}
|
}
|
||||||
@@ -117,29 +154,47 @@ export function computeContentSize(spirit) {
|
|||||||
/**
|
/**
|
||||||
* 计算 Spirit 在其所属 group 内的 cc.Widget 锚定参数 (PoC 阶段 2 简化版).
|
* 计算 Spirit 在其所属 group 内的 cc.Widget 锚定参数 (PoC 阶段 2 简化版).
|
||||||
*
|
*
|
||||||
* 数据源权威: 公式仅处理 Spirit.Y < 360 (上) vs > 360 (下) 与水平偏移, 完全基于
|
* 数据源权威: 按节点尺寸与 name 几何规则区分 full-screen (45),
|
||||||
* 已知字段 (x/y/w/h). SizeCalcMode / CoordRelaX / CoordRelaY 等逆向未出的字段
|
* top-left (9), 以及 normal (16) 三种 Widget 对齐契约.
|
||||||
* 暂不支持 — 由 buildSpiritBlock 调用方保证父 group 已经处理 SizeCalcMode ≠ 0.
|
|
||||||
*
|
|
||||||
* 简化规则:
|
|
||||||
* _alignFlags = 16 | 8 # 16=水平居中, 8=垂直居中
|
|
||||||
* _horizontalCenter = x + w/2 - 640 # 像素偏移, 1280 坐标系水平中心
|
|
||||||
* _verticalCenter = 360 - (y + h/2) # Y-up 翻转 + 720 坐标系垂直中心
|
|
||||||
*
|
*
|
||||||
* @param {{ x?: string, y?: string, w?: string, h?: string }} spirit
|
* @param {{ x?: string, y?: string, w?: string, h?: string }} spirit
|
||||||
* @returns {object} cc.Widget 字段 (除 __type__/node/__prefab/_id 外)
|
* @returns {object} cc.Widget 字段 (除 __type__/node/__prefab/_id 外)
|
||||||
*/
|
*/
|
||||||
export function computeWidgetAlignment(spirit) {
|
export function computeWidgetAlignment(spirit) {
|
||||||
const x = parseFloat(spirit.x || '0');
|
const name = String(spirit.name || '');
|
||||||
const y = parseFloat(spirit.y || '0');
|
const width = parseFloat(spirit.w || '0');
|
||||||
const w = parseFloat(spirit.w || '0');
|
const height = parseFloat(spirit.h || '0');
|
||||||
const h = parseFloat(spirit.h || '0');
|
const lpos = computeLpos(spirit);
|
||||||
|
const isFullScreen = width === 1280 && height === 720;
|
||||||
|
if (isFullScreen) {
|
||||||
return {
|
return {
|
||||||
_alignFlags: 16 | 8, // 水平居中 + 垂直居中
|
_alignFlags: 45,
|
||||||
_target: null,
|
_target: null,
|
||||||
_left: 0, _right: 0, _top: 0, _bottom: 0,
|
_left: 0, _right: 0, _top: 0, _bottom: 0,
|
||||||
_horizontalCenter: x + w / 2 - 640, // 1280 体系半宽
|
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
|
||||||
_verticalCenter: 360 - (y + h / 2), // Y-up + 720 体系半高
|
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
|
||||||
|
_originalWidth: 100, _originalHeight: 100,
|
||||||
|
_alignMode: 2, _lockFlags: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTopLeft = lpos.y > 0 && lpos.x < 0 || /背景|bg/i.test(name);
|
||||||
|
if (isTopLeft) {
|
||||||
|
return {
|
||||||
|
_alignFlags: 9,
|
||||||
|
_target: null,
|
||||||
|
_left: lpos.x, _top: lpos.y,
|
||||||
|
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
|
||||||
|
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
|
||||||
|
_originalWidth: 0, _originalHeight: 0,
|
||||||
|
_alignMode: 2, _lockFlags: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
_alignFlags: 16,
|
||||||
|
_target: null,
|
||||||
|
_horizontalCenter: lpos.x, _verticalCenter: 0,
|
||||||
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
|
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
|
||||||
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
|
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
|
||||||
_originalWidth: 0, _originalHeight: 0,
|
_originalWidth: 0, _originalHeight: 0,
|
||||||
@@ -224,30 +279,106 @@ function loadImageMap() {
|
|||||||
return _imageMap;
|
return _imageMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _imageNaturalSizeMap = null;
|
||||||
|
function loadImageNaturalSizeMap() {
|
||||||
|
if (_imageNaturalSizeMap) return _imageNaturalSizeMap;
|
||||||
|
const list = loadImageMap().ImageFileList;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
throw new Error('gameabc_Image.json missing ImageFileList array');
|
||||||
|
}
|
||||||
|
_imageNaturalSizeMap = new Map();
|
||||||
|
for (const entry of list) {
|
||||||
|
if (!entry || entry.id == null) continue;
|
||||||
|
const width = Number(entry.w_all);
|
||||||
|
const height = Number(entry.h_all);
|
||||||
|
if (!Number.isFinite(width) || !Number.isFinite(height)) continue;
|
||||||
|
_imageNaturalSizeMap.set(String(entry.id), { width, height });
|
||||||
|
}
|
||||||
|
return _imageNaturalSizeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lookupImageNaturalSize(imgResId) {
|
||||||
|
const key = String(imgResId);
|
||||||
|
const naturalSize = loadImageNaturalSizeMap().get(key);
|
||||||
|
if (!naturalSize) {
|
||||||
|
throw new Error(`ImgResID "${imgResId}" missing natural size in gameabc_Image.json`);
|
||||||
|
}
|
||||||
|
return naturalSize;
|
||||||
|
}
|
||||||
|
|
||||||
const ATLAS_ROOT = join(
|
const ATLAS_ROOT = join(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../YouleNexus/assets/framework/ui'
|
'../YouleNexus/assets/framework/ui'
|
||||||
);
|
);
|
||||||
|
|
||||||
let _bmpToMetaPath = null;
|
const ATLAS_BY_LAYER_NAME = [
|
||||||
function loadBmpToMetaPath() {
|
['MainScene_PlayerInfo', 'atlas-hall'],
|
||||||
if (_bmpToMetaPath) return _bmpToMetaPath;
|
['BackHall', 'atlas-hall'],
|
||||||
|
['Login', 'atlas-login'],
|
||||||
|
['Notice', 'atlas-login'],
|
||||||
|
['Protol', 'atlas-login'],
|
||||||
|
['Loading', 'atlas-login'],
|
||||||
|
['Kick', 'atlas-login'],
|
||||||
|
['Reconnect', 'atlas-login'],
|
||||||
|
['Setting', 'atlas-login'],
|
||||||
|
['Tips', 'atlas-login'],
|
||||||
|
['updateRoomCard', 'atlas-login'],
|
||||||
|
['phoneInfo', 'atlas-login'],
|
||||||
|
['recordLayer', 'atlas-login'],
|
||||||
|
['MenuPlayerInfo', 'atlas-login'],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function selectAtlasForLayerName(layerName) {
|
||||||
|
const normalizedName = String(layerName || '').toLowerCase();
|
||||||
|
const match = ATLAS_BY_LAYER_NAME.find(([keyword]) => normalizedName.includes(keyword.toLowerCase()));
|
||||||
|
return match ? match[1] : 'atlas-login';
|
||||||
|
}
|
||||||
|
|
||||||
|
function listAtlasDirectories() {
|
||||||
|
if (!existsSync(ATLAS_ROOT)) return [];
|
||||||
|
return readdirSync(ATLAS_ROOT)
|
||||||
|
.filter(entry => entry.startsWith('atlas-') && statSync(join(ATLAS_ROOT, entry)).isDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
const _bmpToMetaPath = new Map();
|
||||||
|
function loadBmpToMetaPath(preferredAtlas = null) {
|
||||||
|
const cacheKey = preferredAtlas || 'default';
|
||||||
|
if (_bmpToMetaPath.has(cacheKey)) return _bmpToMetaPath.get(cacheKey);
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
if (existsSync(ATLAS_ROOT)) {
|
const directories = listAtlasDirectories();
|
||||||
for (const entry of readdirSync(ATLAS_ROOT)) {
|
const orderedDirectories = [
|
||||||
if (!entry.startsWith('atlas-')) continue;
|
...(preferredAtlas ? [preferredAtlas] : []),
|
||||||
|
...(preferredAtlas === 'atlas-login' || !preferredAtlas ? ['atlas-login'] : []),
|
||||||
|
...directories.filter(entry => entry !== preferredAtlas && entry !== 'atlas-login'),
|
||||||
|
];
|
||||||
|
for (const entry of orderedDirectories) {
|
||||||
const dir = join(ATLAS_ROOT, entry);
|
const dir = join(ATLAS_ROOT, entry);
|
||||||
if (!statSync(dir).isDirectory()) continue;
|
|
||||||
for (const file of readdirSync(dir)) {
|
for (const file of readdirSync(dir)) {
|
||||||
if (!file.endsWith('.png.meta')) continue;
|
if (!file.endsWith('.png.meta')) continue;
|
||||||
const bmpName = file.replace(/\.meta$/, '');
|
const bmpName = file.replace(/\.meta$/, '');
|
||||||
// 同一 bmp 出现在多个 atlas 时, 取首次发现 (字典序);
|
|
||||||
// YouleNexus 实际数据中无重复, 此为防御性 fallback.
|
|
||||||
if (!map.has(bmpName)) map.set(bmpName, join(dir, file));
|
if (!map.has(bmpName)) map.set(bmpName, join(dir, file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cocos imports multi-frame source BMPs as <bmp>_1.png, <bmp>_2.png, ...
|
||||||
|
// Keep the first frame deterministic when a legacy image has no exact BMP basename.
|
||||||
|
const imageMap = loadImageMap();
|
||||||
|
const imageList = Array.isArray(imageMap.ImageFileList) ? imageMap.ImageFileList : [];
|
||||||
|
for (const image of imageList) {
|
||||||
|
if (!image?.bmp || map.has(image.bmp)) continue;
|
||||||
|
const baseName = image.bmp.replace(/\.png$/i, '');
|
||||||
|
const frameCount = Number.isInteger(Number(image.frame_all)) && Number(image.frame_all) > 0
|
||||||
|
? Number(image.frame_all)
|
||||||
|
: 1;
|
||||||
|
for (let frame = 1; frame <= frameCount; frame += 1) {
|
||||||
|
const frameName = `${baseName}_${frame}.png`;
|
||||||
|
if (map.has(frameName)) {
|
||||||
|
map.set(image.bmp, map.get(frameName));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
_bmpToMetaPath = map;
|
}
|
||||||
|
}
|
||||||
|
_bmpToMetaPath.set(cacheKey, map);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,9 +393,10 @@ function loadBmpToMetaPath() {
|
|||||||
* - .meta 缺 subMetas.f9941 → throw (说明该资源未走 sprite-frame 导入)
|
* - .meta 缺 subMetas.f9941 → throw (说明该资源未走 sprite-frame 导入)
|
||||||
*
|
*
|
||||||
* @param {string} imgResId - XML ImgResID 字符串 (如 "20", "21", "25")
|
* @param {string} imgResId - XML ImgResID 字符串 (如 "20", "21", "25")
|
||||||
|
* @param {string} [atlasDir] - 可选的 layer atlas 目录, 例如 atlas-login.
|
||||||
* @returns {string} SpriteFrame 真 UUID, 形如 "<uuid>@f9941"
|
* @returns {string} SpriteFrame 真 UUID, 形如 "<uuid>@f9941"
|
||||||
*/
|
*/
|
||||||
export function resolveSpriteFrameUuid(imgResId) {
|
export function resolveSpriteFrameUuid(imgResId, atlasDir = null) {
|
||||||
const imageMap = loadImageMap();
|
const imageMap = loadImageMap();
|
||||||
const list = imageMap.ImageFileList;
|
const list = imageMap.ImageFileList;
|
||||||
if (!Array.isArray(list)) {
|
if (!Array.isArray(list)) {
|
||||||
@@ -277,7 +409,7 @@ export function resolveSpriteFrameUuid(imgResId) {
|
|||||||
}
|
}
|
||||||
const bmpName = entry.bmp;
|
const bmpName = entry.bmp;
|
||||||
|
|
||||||
const bmpToMeta = loadBmpToMetaPath();
|
const bmpToMeta = loadBmpToMetaPath(atlasDir);
|
||||||
const metaPath = bmpToMeta.get(bmpName);
|
const metaPath = bmpToMeta.get(bmpName);
|
||||||
if (!metaPath) {
|
if (!metaPath) {
|
||||||
throw new Error(`bmp "${bmpName}" (ImgResID ${imgResId}) not found under 'atlas-*' in ${ATLAS_ROOT}`);
|
throw new Error(`bmp "${bmpName}" (ImgResID ${imgResId}) not found under 'atlas-*' in ${ATLAS_ROOT}`);
|
||||||
@@ -376,7 +508,7 @@ function makeUiTransform(nodeId, compPrefabId, width, height) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeWidget(nodeId, compPrefabId) {
|
function makeWidget(nodeId, compPrefabId, originalWidth = 100, originalHeight = 100) {
|
||||||
return {
|
return {
|
||||||
__type__: 'cc.Widget',
|
__type__: 'cc.Widget',
|
||||||
_name: '',
|
_name: '',
|
||||||
@@ -391,7 +523,7 @@ function makeWidget(nodeId, compPrefabId) {
|
|||||||
_horizontalCenter: 0, _verticalCenter: 0,
|
_horizontalCenter: 0, _verticalCenter: 0,
|
||||||
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
|
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
|
||||||
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
|
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
|
||||||
_originalWidth: 0, _originalHeight: 0,
|
_originalWidth: originalWidth, _originalHeight: originalHeight,
|
||||||
_alignMode: 2,
|
_alignMode: 2,
|
||||||
_lockFlags: 0,
|
_lockFlags: 0,
|
||||||
_id: '',
|
_id: '',
|
||||||
@@ -446,7 +578,13 @@ function makeSprite(nodeId, compPrefabId, spriteFrameUuid, type = 0) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeLabel(nodeId, compPrefabId, text) {
|
function makeLabel(nodeId, compPrefabId, spirit) {
|
||||||
|
const fontSize = Number.isFinite(Number(spirit.fontSize)) ? Number(spirit.fontSize) : 40;
|
||||||
|
const lineSpace = Number.isFinite(Number(spirit.lineSpace)) ? Number(spirit.lineSpace) : fontSize;
|
||||||
|
const rowSpace = Number.isFinite(Number(spirit.rowSpace)) ? Number(spirit.rowSpace) : 0;
|
||||||
|
const color = Number.isInteger(Number(spirit.fontColor)) ? Number(spirit.fontColor) : 0xffffff;
|
||||||
|
const horizontalAlign = Number.isInteger(Number(spirit.alignHorz)) ? Number(spirit.alignHorz) : 1;
|
||||||
|
const verticalAlign = Number.isInteger(Number(spirit.alignVert)) ? Number(spirit.alignVert) : 1;
|
||||||
return {
|
return {
|
||||||
__type__: 'cc.Label',
|
__type__: 'cc.Label',
|
||||||
_name: '',
|
_name: '',
|
||||||
@@ -458,20 +596,28 @@ function makeLabel(nodeId, compPrefabId, text) {
|
|||||||
_customMaterial: null,
|
_customMaterial: null,
|
||||||
_srcBlendFactor: 2,
|
_srcBlendFactor: 2,
|
||||||
_dstBlendFactor: 4,
|
_dstBlendFactor: 4,
|
||||||
_color: { __type__: 'cc.Color', r: 255, g: 255, b: 255, a: 255 },
|
_color: {
|
||||||
_string: text || '',
|
__type__: 'cc.Color',
|
||||||
_horizontalAlign: 1,
|
r: (color >> 16) & 255,
|
||||||
_verticalAlign: 1,
|
g: (color >> 8) & 255,
|
||||||
_actualFontSize: 40,
|
b: color & 255,
|
||||||
_fontSize: 40,
|
a: 255,
|
||||||
|
},
|
||||||
|
_string: spirit.text || spirit.name || '',
|
||||||
|
_horizontalAlign: horizontalAlign,
|
||||||
|
_verticalAlign: verticalAlign,
|
||||||
|
_actualFontSize: fontSize,
|
||||||
|
_fontSize: fontSize,
|
||||||
_fontFamily: 'Arial',
|
_fontFamily: 'Arial',
|
||||||
_lineHeight: 40,
|
_lineHeight: lineSpace,
|
||||||
_overflow: 0,
|
_overflow: 0,
|
||||||
_enableWrapText: true,
|
_enableWrapText: spirit.wordWrap !== undefined && spirit.wordWrap !== '0',
|
||||||
_font: null,
|
_font: null,
|
||||||
_isSystemFontUsed: true,
|
_isSystemFontUsed: true,
|
||||||
_spacingX: 0,
|
_spacingX: rowSpace,
|
||||||
_isItalic: false, _isBold: false, _isUnderline: false,
|
_isItalic: false,
|
||||||
|
_isBold: spirit.fontBold !== undefined && spirit.fontBold !== '0',
|
||||||
|
_isUnderline: false,
|
||||||
_underlineHeight: 2,
|
_underlineHeight: 2,
|
||||||
_cacheMode: 0,
|
_cacheMode: 0,
|
||||||
_enableOutline: false,
|
_enableOutline: false,
|
||||||
@@ -579,7 +725,7 @@ function makeEditBox(nodeId, compPrefabId, spriteFrameUuid, text) {
|
|||||||
* @param {{ layerId?: string, layerName: string, spirits: Array<{ id: string, name: string, type: string, imgResID: string, x: string, y: string, w: string, h: string, group: string, indexOfLayer: string }> }} layer
|
* @param {{ layerId?: string, layerName: string, spirits: Array<{ id: string, name: string, type: string, imgResID: string, x: string, y: string, w: string, h: string, group: string, indexOfLayer: string }> }} layer
|
||||||
* @returns {Array<object>} Cocos prefab JSON 数组
|
* @returns {Array<object>} Cocos prefab JSON 数组
|
||||||
*/
|
*/
|
||||||
export function emitPrefabJson(layer) {
|
export function emitPrefabJson(layer, opts = {}) {
|
||||||
if (!layer || !layer.layerName) {
|
if (!layer || !layer.layerName) {
|
||||||
throw new Error('emitPrefabJson: layer.layerName is required');
|
throw new Error('emitPrefabJson: layer.layerName is required');
|
||||||
}
|
}
|
||||||
@@ -588,11 +734,13 @@ export function emitPrefabJson(layer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
|
const targetName = opts.targetName || layer.layerName;
|
||||||
|
const atlasName = opts.atlasName || selectAtlasForLayerName(targetName);
|
||||||
|
|
||||||
// ── 1. cc.Prefab 头 ─────────────────────────────────────────────
|
// ── 1. cc.Prefab 头 ─────────────────────────────────────────────
|
||||||
out.push({
|
out.push({
|
||||||
__type__: 'cc.Prefab',
|
__type__: 'cc.Prefab',
|
||||||
_name: layer.layerName,
|
_name: targetName,
|
||||||
_objFlags: 0,
|
_objFlags: 0,
|
||||||
__editorExtras__: {},
|
__editorExtras__: {},
|
||||||
_native: '',
|
_native: '',
|
||||||
@@ -602,7 +750,7 @@ export function emitPrefabJson(layer) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── 2. 根节点 cc.Node (id=1) ────────────────────────────────────
|
// ── 2. 根节点 cc.Node (id=1) ────────────────────────────────────
|
||||||
const rootNode = makeNode(layer.layerName, null);
|
const rootNode = makeNode(targetName, null);
|
||||||
out.push(rootNode);
|
out.push(rootNode);
|
||||||
const rootId = out.length - 1; // =1
|
const rootId = out.length - 1; // =1
|
||||||
|
|
||||||
@@ -628,7 +776,7 @@ export function emitPrefabJson(layer) {
|
|||||||
if (!spirit) {
|
if (!spirit) {
|
||||||
throw new Error(`emitPrefabJson: spirit id ${spiritId} referenced by ${gi.group.name} but not found in layer.spirits`);
|
throw new Error(`emitPrefabJson: spirit id ${spiritId} referenced by ${gi.group.name} but not found in layer.spirits`);
|
||||||
}
|
}
|
||||||
const info = buildSpiritBlock(spirit, { __id__: gi.nodeId }, out, { parentIsGroup: true });
|
const info = buildSpiritBlock(spirit, { __id__: gi.nodeId }, out, { parentIsGroup: true, atlasName });
|
||||||
spiritInfo.set(spiritId, info);
|
spiritInfo.set(spiritId, info);
|
||||||
gi.node._children.push({ __id__: info.nodeId });
|
gi.node._children.push({ __id__: info.nodeId });
|
||||||
}
|
}
|
||||||
@@ -639,7 +787,7 @@ export function emitPrefabJson(layer) {
|
|||||||
if (!spirit) {
|
if (!spirit) {
|
||||||
throw new Error(`emitPrefabJson: root child spirit id ${spiritId} not found in layer.spirits`);
|
throw new Error(`emitPrefabJson: root child spirit id ${spiritId} not found in layer.spirits`);
|
||||||
}
|
}
|
||||||
const info = buildSpiritBlock(spirit, { __id__: rootId }, out, { parentIsGroup: false });
|
const info = buildSpiritBlock(spirit, { __id__: rootId }, out, { parentIsGroup: false, atlasName });
|
||||||
spiritInfo.set(spiritId, info);
|
spiritInfo.set(spiritId, info);
|
||||||
rootNode._children.push({ __id__: info.nodeId });
|
rootNode._children.push({ __id__: info.nodeId });
|
||||||
}
|
}
|
||||||
@@ -669,7 +817,12 @@ export function emitPrefabJson(layer) {
|
|||||||
*/
|
*/
|
||||||
function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false }) {
|
function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false }) {
|
||||||
const lpos = computeLpos(spirit);
|
const lpos = computeLpos(spirit);
|
||||||
const size = computeContentSize(spirit);
|
const width = parseFloat(spirit.w || '0');
|
||||||
|
const height = parseFloat(spirit.h || '0');
|
||||||
|
const isFullScreen = width === 1280 && height === 720;
|
||||||
|
const size = isFullScreen
|
||||||
|
? { width: 1600, height: 720 }
|
||||||
|
: computeContentSize(spirit);
|
||||||
const componentName = mapSpiritTypeToComponent(spirit.type);
|
const componentName = mapSpiritTypeToComponent(spirit.type);
|
||||||
|
|
||||||
// 1. cc.Node (主组件 id=K+0)
|
// 1. cc.Node (主组件 id=K+0)
|
||||||
@@ -680,19 +833,18 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
|
|||||||
|
|
||||||
// 2. cc.UITransform (K+1)
|
// 2. cc.UITransform (K+1)
|
||||||
const uiCompInfo = makeCompPrefabInfo();
|
const uiCompInfo = makeCompPrefabInfo();
|
||||||
out.push(uiCompInfo);
|
const uiTransform = makeUiTransform(nodeId, out.length + 1, size.width, size.height);
|
||||||
const uiCompInfoId = out.length - 1;
|
|
||||||
const uiTransform = makeUiTransform(nodeId, uiCompInfoId, size.width, size.height);
|
|
||||||
out.push(uiTransform);
|
out.push(uiTransform);
|
||||||
const uiId = out.length - 1;
|
const uiId = out.length - 1;
|
||||||
|
out.push(uiCompInfo);
|
||||||
|
const uiCompInfoId = out.length - 1;
|
||||||
|
|
||||||
// 3. 主组件 (K+3) — CompPrefabInfo 先 push (K+2), 主组件 __prefab → 该 CompPrefabInfo
|
// 3. 主组件 (K+3) — component 先 push, CompPrefabInfo follows it.
|
||||||
const mainCompInfo = makeCompPrefabInfo();
|
const mainCompInfo = makeCompPrefabInfo();
|
||||||
out.push(mainCompInfo);
|
const mainCompInfoId = out.length + 1;
|
||||||
const mainCompInfoId = out.length - 1;
|
|
||||||
|
|
||||||
const spriteFrameUuid = (spirit.type === '0' || spirit.type === '3' || spirit.type === '5')
|
const spriteFrameUuid = (spirit.type === '0' || spirit.type === '3' || spirit.type === '5')
|
||||||
? resolveSpriteFrameUuid(spirit.imgResID)
|
? resolveSpriteFrameUuid(spirit.imgResID, opts.atlasName)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let mainComp;
|
let mainComp;
|
||||||
@@ -700,7 +852,7 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
|
|||||||
const spriteType = spirit.type === '5' ? 1 : 0; // 5=SLICED, else SIMPLE
|
const spriteType = spirit.type === '5' ? 1 : 0; // 5=SLICED, else SIMPLE
|
||||||
mainComp = makeSprite(nodeId, mainCompInfoId, spriteFrameUuid, spriteType);
|
mainComp = makeSprite(nodeId, mainCompInfoId, spriteFrameUuid, spriteType);
|
||||||
} else if (componentName === 'cc.Label') {
|
} else if (componentName === 'cc.Label') {
|
||||||
mainComp = makeLabel(nodeId, mainCompInfoId, spirit.name);
|
mainComp = makeLabel(nodeId, mainCompInfoId, spirit);
|
||||||
} else if (componentName === 'cc.ProgressBar') {
|
} else if (componentName === 'cc.ProgressBar') {
|
||||||
mainComp = makeProgressBar(nodeId, mainCompInfoId, spriteFrameUuid);
|
mainComp = makeProgressBar(nodeId, mainCompInfoId, spriteFrameUuid);
|
||||||
} else if (componentName === 'cc.EditBox') {
|
} else if (componentName === 'cc.EditBox') {
|
||||||
@@ -712,18 +864,18 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
|
|||||||
}
|
}
|
||||||
out.push(mainComp);
|
out.push(mainComp);
|
||||||
const mainCompId = out.length - 1;
|
const mainCompId = out.length - 1;
|
||||||
|
out.push(mainCompInfo);
|
||||||
|
|
||||||
// 4. cc.Widget (K+5) — CompPrefabInfo 先 push (K+4)
|
// 4. cc.Widget (K+5) — component 先 push, CompPrefabInfo follows it.
|
||||||
const widgetCompInfo = makeCompPrefabInfo();
|
const widgetCompInfo = makeCompPrefabInfo();
|
||||||
out.push(widgetCompInfo);
|
// parentIsGroup 时走 computeWidgetAlignment; Layer 根直挂时使用全屏 Widget.
|
||||||
const widgetCompInfoId = out.length - 1;
|
|
||||||
// parentIsGroup 时走 computeWidgetAlignment (水平+垂直居中); Layer 根直挂时
|
|
||||||
// 用默认 _alignFlags=45 (全屏铺满占位, 后续 PoC 阶段再细化).
|
|
||||||
const widget = opts.parentIsGroup
|
const widget = opts.parentIsGroup
|
||||||
? makeSpiritWidget(nodeId, widgetCompInfoId, spirit)
|
? makeSpiritWidget(nodeId, out.length + 1, spirit)
|
||||||
: makeWidget(nodeId, widgetCompInfoId);
|
: makeWidget(nodeId, out.length + 1);
|
||||||
out.push(widget);
|
out.push(widget);
|
||||||
const widgetId = out.length - 1;
|
const widgetId = out.length - 1;
|
||||||
|
out.push(widgetCompInfo);
|
||||||
|
const widgetCompInfoId = out.length - 1;
|
||||||
|
|
||||||
// 5. cc.PrefabInfo (K+7) — Node 的 _prefab 指向这里
|
// 5. cc.PrefabInfo (K+7) — Node 的 _prefab 指向这里
|
||||||
const prefabInfo = makePrefabInfo();
|
const prefabInfo = makePrefabInfo();
|
||||||
@@ -744,19 +896,19 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
|
|||||||
function appendNodeEnvelope(node, nodeId, out) {
|
function appendNodeEnvelope(node, nodeId, out) {
|
||||||
// 1. UITransform + CompPrefabInfo
|
// 1. UITransform + CompPrefabInfo
|
||||||
const uiCompInfo = makeCompPrefabInfo();
|
const uiCompInfo = makeCompPrefabInfo();
|
||||||
out.push(uiCompInfo);
|
const uiTransform = makeUiTransform(nodeId, out.length + 1, DESIGN_WIDTH, DESIGN_HEIGHT);
|
||||||
const uiCompInfoId = out.length - 1;
|
|
||||||
const uiTransform = makeUiTransform(nodeId, uiCompInfoId, DESIGN_WIDTH, DESIGN_HEIGHT);
|
|
||||||
out.push(uiTransform);
|
out.push(uiTransform);
|
||||||
const uiId = out.length - 1;
|
const uiId = out.length - 1;
|
||||||
|
out.push(uiCompInfo);
|
||||||
|
const uiCompInfoId = out.length - 1;
|
||||||
|
|
||||||
// 2. Widget + CompPrefabInfo
|
// 2. Widget + CompPrefabInfo
|
||||||
const widgetCompInfo = makeCompPrefabInfo();
|
const widgetCompInfo = makeCompPrefabInfo();
|
||||||
out.push(widgetCompInfo);
|
const widget = makeWidget(nodeId, out.length + 1);
|
||||||
const widgetCompInfoId = out.length - 1;
|
|
||||||
const widget = makeWidget(nodeId, widgetCompInfoId);
|
|
||||||
out.push(widget);
|
out.push(widget);
|
||||||
const widgetId = out.length - 1;
|
const widgetId = out.length - 1;
|
||||||
|
out.push(widgetCompInfo);
|
||||||
|
const widgetCompInfoId = out.length - 1;
|
||||||
|
|
||||||
// 3. PrefabInfo
|
// 3. PrefabInfo
|
||||||
const prefabInfo = makePrefabInfo();
|
const prefabInfo = makePrefabInfo();
|
||||||
|
|||||||
@@ -7,15 +7,16 @@
|
|||||||
// - 单个 target 失败 (例如 ImgResID 未导入 sprite) 不阻塞后续 target, 日志记录后继续.
|
// - 单个 target 失败 (例如 ImgResID 未导入 sprite) 不阻塞后续 target, 日志记录后继续.
|
||||||
// - per "下游不兜底" 原则: CLI 本身必须 throw 让错误暴露; batch 仅负责捕获 + 汇总, 不做修补.
|
// - per "下游不兜底" 原则: CLI 本身必须 throw 让错误暴露; batch 仅负责捕获 + 汇总, 不做修补.
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { ROOT } from './lib/paths.mjs';
|
||||||
|
|
||||||
const ROOT = process.cwd(); // assume run from cocoscreator_projects/
|
const OUT_DIR = join(ROOT, 'YouleNexus', 'out', 'legacy-migration');
|
||||||
const OUT_DIR = `${ROOT}/YouleNexus/out/legacy-migration`;
|
const SOURCE = join(ROOT, '..', 'projects', 'Game_Surface_3', 'save');
|
||||||
const SOURCE = `${ROOT}/../projects/Game_Surface_3/save`;
|
|
||||||
|
|
||||||
// 按 spec 已核实映射表 (11 个 prefab). Layer10_Protocol + Login_Layer + Layer619_BackOtherGame 三个 PoC 已存在,
|
// 按 spec 已核实映射表 (11 个 prefab). Layer10_Protocol + Login_Layer + Layer619_BackOtherGame 三个 PoC 已存在,
|
||||||
// 但 batch 仍会重新生成它们 (idempotent — 覆盖 out/legacy-migration/*.prefab + .diff).
|
// 但 batch 仍会重新生成它们 (idempotent — 覆盖 out/legacy-migration/*.prefab + .diff).
|
||||||
const targets = [
|
const targets = [
|
||||||
{ xml: 'Layer00002.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/views/Login_Layer.prefab', name: 'Login_Layer', note: 'PoC 3 (Layer00002)' },
|
{ xml: 'Layer00002.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab', name: 'Login_Layer', note: 'PoC 3 (Layer00002)' },
|
||||||
{ xml: 'Layer00017.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab', name: 'MenuPlayerInfo_Layer', note: '拍板 A (Layer00017)' },
|
{ xml: 'Layer00017.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab', name: 'MenuPlayerInfo_Layer', note: '拍板 A (Layer00017)' },
|
||||||
{ xml: 'Layer00029.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Group106_PhoneVerify.prefab', name: 'phoneInfo' },
|
{ xml: 'Layer00029.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Group106_PhoneVerify.prefab', name: 'phoneInfo' },
|
||||||
{ xml: 'Layer00010.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab', name: 'Protol_Layer', note: 'PoC 2 (Layer00010)' },
|
{ xml: 'Layer00010.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab', name: 'Protol_Layer', note: 'PoC 2 (Layer00010)' },
|
||||||
@@ -58,3 +59,4 @@ if (results.failed.length > 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('\nNext step: human reviews each .diff and decides whether to overwrite YouleNexus original prefabs.');
|
console.log('\nNext step: human reviews each .diff and decides whether to overwrite YouleNexus original prefabs.');
|
||||||
|
process.exitCode = results.failed.length > 0 ? 1 : 0;
|
||||||
Reference in New Issue
Block a user