Files
youle_cocos/cocoscreator_projects/framework-tests/legacy-layer-migration/layer418-prefab.test.mjs
T
2026-09-05 00:12:11 +08:00

234 lines
9.1 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const testDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(testDir, '..', '..', '..');
const sourcePath = path.join(repoRoot, 'projects', 'Game_Surface_3', 'save', 'Layer00418.xml');
const objectDataPath = path.join(
repoRoot,
'projects',
'Game_Surface_3',
'output',
'gameabc_Object.json',
);
const imageDataPath = path.join(
repoRoot,
'projects',
'Game_Surface_3',
'output',
'gameabc_Image.json',
);
const prefabPath = path.join(
repoRoot,
'cocoscreator_projects',
'YouleNexus',
'assets',
'framework',
'ui',
'prefabs',
'widgets',
'Layer418_ChatPannel.prefab',
);
const atlasRoomPath = path.join(
repoRoot,
'cocoscreator_projects',
'YouleNexus',
'assets',
'framework',
'ui',
'atlas-room',
);
const atlasPaths = [
atlasRoomPath,
path.join(repoRoot, 'cocoscreator_projects', 'YouleNexus', 'assets', 'framework', 'ui', 'atlas-common'),
path.join(repoRoot, 'cocoscreator_projects', 'YouleNexus', 'assets', 'framework', 'ui', 'atlas-hall'),
path.join(repoRoot, 'cocoscreator_projects', 'YouleNexus', 'assets', 'framework', 'ui', 'atlas-login'),
path.join(repoRoot, 'cocoscreator_projects', 'YouleNexus', 'assets', 'framework', 'ui', 'atlas-unassigned'),
];
function property(block, name) {
const match = block.match(new RegExp(`<Property Name="${name}" Value="([^"]*)"\\s*/>`));
assert.ok(match, `source property ${name} should exist`);
return match[1];
}
function optionalProperty(block, name) {
return block.match(new RegExp(`<Property Name="${name}" Value="([^"]*)"\\s*/>`))?.[1];
}
function parseSourceSpirits(xml) {
return [...xml.matchAll(/<Spirit>([\s\S]*?)<\/Spirit>/g)].map((match) => {
const block = match[1];
return {
id: Number(property(block, 'ID')),
name: property(block, 'Name'),
x: Number(property(block, 'X')),
y: Number(property(block, 'Y')),
width: Number(property(block, 'Width')),
height: Number(property(block, 'Height')),
index: Number(property(block, 'IndexOfLayer')),
visible: property(block, 'IsVisible') === '1',
text: optionalProperty(block, 'Text'),
fontSize: Number(optionalProperty(block, 'FontSize')),
fontColorAlpha: Number(optionalProperty(block, 'FontColorA')),
autoSize: Number(optionalProperty(block, 'AutoSize')),
wordWrap: Number(optionalProperty(block, 'WordWrap')),
};
});
}
function nodeComponent(prefab, node, type) {
return node._components.map((ref) => prefab[ref.__id__]).find((component) => component.__type__ === type);
}
function expectedSpriteFrameUuid(imageFileId, frameIndex, imageFiles) {
const image = imageFiles[imageFileId];
assert.equal(image.id, imageFileId, `ImageFileList[${imageFileId}] must be direct indexed`);
const frameAll = Number(image.frame_all);
const frameNumber = frameAll === 1 ? 1 : ((Number(frameIndex) - 1) % frameAll) + 1;
const baseName = String(imageFileId).padStart(5, '0');
const frameSuffix = frameAll > 1 ? `_${String(frameNumber).padStart(String(frameAll).length, '0')}` : '';
const fileName = `${baseName}${frameSuffix}.png.meta`;
const metaPath = atlasPaths.map((atlasPath) => path.join(atlasPath, fileName)).find(fs.existsSync);
assert.ok(metaPath, `${fileName} should exist in the Layer418 atlas search order`);
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
const spriteFrame = Object.values(meta.subMetas).find((entry) => entry.importer === 'sprite-frame');
assert.ok(spriteFrame, `${fileName} should expose a SpriteFrame subasset`);
return spriteFrame.uuid;
}
function round(value) {
return Math.round(value * 1000) / 1000;
}
function sourceSignature(spirit) {
const isFullScreen = spirit.width === 1280 && spirit.height === 720;
return JSON.stringify([
spirit.name,
round(spirit.x + spirit.width / 2 - 640),
round(360 - (spirit.y + spirit.height / 2)),
isFullScreen ? 1600 : spirit.width,
isFullScreen ? 720 : spirit.height,
spirit.visible,
]);
}
function prefabSignature(prefab, node) {
const uiTransformRef = node._components
.map((ref) => prefab[ref.__id__])
.find((component) => component.__type__ === 'cc.UITransform');
assert.ok(uiTransformRef, `${node._name} should have UITransform`);
return JSON.stringify([
node._name,
round(node._lpos.x),
round(node._lpos.y),
uiTransformRef._contentSize.width,
uiTransformRef._contentSize.height,
node._active,
]);
}
test('Layer418 prefab preserves every source spirit and its layout contract', () => {
const sourceSpirits = parseSourceSpirits(fs.readFileSync(sourcePath, 'utf8'));
const objectList = JSON.parse(fs.readFileSync(objectDataPath, 'utf8')).ObjectList;
const imageFileList = JSON.parse(fs.readFileSync(imageDataPath, 'utf8')).ImageFileList;
const prefab = JSON.parse(fs.readFileSync(prefabPath, 'utf8'));
const nodes = prefab.filter((entry) => entry.__type__ === 'cc.Node');
const root = nodes.find((node) => node._name === 'Layer418_ChatPannel');
const group = nodes.find((node) => node._name === 'group-30');
const spiritNodes = nodes.filter(
(node) => node._name !== 'Layer418_ChatPannel' && node._name !== 'group-30',
);
assert.equal(sourceSpirits.length, 54);
assert.equal(nodes.length, 56);
assert.equal(spiritNodes.length, 54);
assert.deepEqual(root._children.map((ref) => prefab[ref.__id__]._name), ['group-30']);
assert.deepEqual(
group._children.map((ref) => prefab[ref.__id__]._name),
[...sourceSpirits].sort((a, b) => a.index - b.index).map((spirit) => spirit.name),
);
assert.deepEqual(
spiritNodes.map((node) => prefabSignature(prefab, node)).sort(),
sourceSpirits.map(sourceSignature).sort(),
);
const types = prefab.reduce((counts, entry) => {
counts[entry.__type__] = (counts[entry.__type__] ?? 0) + 1;
return counts;
}, {});
assert.equal(types['cc.Sprite'], 48);
assert.equal(types['cc.Label'], 6);
for (const spirit of sourceSpirits) {
const sourceObject = objectList[spirit.id]?.Property;
assert.equal(sourceObject?.ObjectID, spirit.id, `ObjectList[${spirit.id}] must be direct indexed`);
assert.equal(sourceObject.ObjectName, spirit.name);
const node = spiritNodes.find((candidate) => candidate._name === spirit.name);
assert.ok(node, `${spirit.name} should exist`);
if (sourceObject.ObjectType === 2) {
const sprite = nodeComponent(prefab, node, 'cc.Sprite');
assert.ok(sprite, `${spirit.name} should be a Sprite`);
assert.equal(
sprite._spriteFrame?.__uuid__,
expectedSpriteFrameUuid(sourceObject.ImageFileID, sourceObject.FrameIndex, imageFileList),
`${spirit.name} SpriteFrame`,
);
const sliced = [sourceObject.L9, sourceObject.T9, sourceObject.R9, sourceObject.B9].some(Number);
assert.equal(sprite._type, sliced ? 1 : 0, `${spirit.name} Sprite.Type`);
assert.equal(sprite._sizeMode, 0, `${spirit.name} Sprite.SizeMode`);
} else if (sourceObject.ObjectType === 4) {
assert.ok(nodeComponent(prefab, node, 'cc.Label'), `${spirit.name} should be a Label`);
} else {
assert.fail(`${spirit.name} has unsupported ObjectType ${sourceObject.ObjectType}`);
}
}
});
test('Layer418 labels preserve text, color, wrapping, and AutoSize semantics', () => {
const sourceSpirits = parseSourceSpirits(fs.readFileSync(sourcePath, 'utf8'));
const objectList = JSON.parse(fs.readFileSync(objectDataPath, 'utf8')).ObjectList;
const prefab = JSON.parse(fs.readFileSync(prefabPath, 'utf8'));
const nodes = prefab.filter((entry) => entry.__type__ === 'cc.Node');
const labels = sourceSpirits.filter((spirit) => objectList[spirit.id]?.Property.ObjectType === 4);
assert.equal(labels.length, 6);
for (const spirit of labels) {
const sourceObject = objectList[spirit.id].Property;
const node = nodes.find((candidate) => candidate._name === spirit.name);
const label = nodeComponent(prefab, node, 'cc.Label');
assert.equal(label._string, spirit.text, `${spirit.name} text`);
assert.equal(label._fontSize, spirit.fontSize, `${spirit.name} font size`);
assert.deepEqual(
[label._color.r, label._color.g, label._color.b, label._color.a],
[sourceObject.FontColorR, sourceObject.FontColorG, sourceObject.FontColorB, spirit.fontColorAlpha],
`${spirit.name} font color`,
);
assert.equal(label._enableWrapText, spirit.wordWrap === 1, `${spirit.name} word wrap`);
assert.equal(label._overflow, spirit.autoSize === 1 ? 0 : 1, `${spirit.name} AutoSize`);
}
});
test('Layer418 sliced sprite frames preserve their source border contract', () => {
const expectedBorders = new Map([
['00084.png.meta', [20, 20, 20, 20]],
['00342.png.meta', [13, 13, 13, 13]],
]);
for (const [fileName, expected] of expectedBorders) {
const meta = JSON.parse(fs.readFileSync(path.join(atlasRoomPath, fileName), 'utf8'));
const userData = meta.subMetas.f9941.userData;
assert.deepEqual(
[userData.borderLeft, userData.borderTop, userData.borderRight, userData.borderBottom],
expected,
fileName,
);
}
});