234 lines
9.1 KiB
JavaScript
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,
|
|
);
|
|
}
|
|
});
|