168 lines
10 KiB
JavaScript
168 lines
10 KiB
JavaScript
// TYPESCRIPT_PATH may point to the TypeScript bundled with the local editor.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const ts = require(process.env.TYPESCRIPT_PATH || 'typescript');
|
|
const root = path.resolve(__dirname, '..');
|
|
const json = file => JSON.parse(fs.readFileSync(path.join(root, file), 'utf8'));
|
|
const vec = (x = 0, y = 0) => ({ x, y });
|
|
class Node {
|
|
constructor(data) {
|
|
this.name = data._name;
|
|
this.width = data._contentSize.width; this.height = data._contentSize.height;
|
|
this.anchorX = data._anchorPoint.x; this.anchorY = data._anchorPoint.y;
|
|
this.x = data._trs.array[0]; this.y = data._trs.array[1];
|
|
this.scaleX = data._trs.array[7]; this.scaleY = data._trs.array[8];
|
|
this.angle = data._eulerAngles.z; this.active = data._active;
|
|
this.children = []; this.components = new Map();
|
|
}
|
|
get position() { return vec(this.x, this.y); }
|
|
addChild(node) { node.parent = this; this.children.push(node); }
|
|
getChildByName(name) { return this.children.find(n => n.name === name); }
|
|
getComponent(type) { return this.components.get(type); }
|
|
getAnchorPoint() { return vec(this.anchorX, this.anchorY); }
|
|
getContentSize() { return { width: this.width, height: this.height }; }
|
|
getSiblingIndex() { return this.parent.children.indexOf(this); }
|
|
setSiblingIndex(index) { const siblings = this.parent.children; siblings.splice(this.getSiblingIndex(), 1); siblings.splice(index, 0, this); }
|
|
setPosition(x, y) { this.x = typeof x === 'object' ? x.x : x; this.y = typeof x === 'object' ? x.y : y; }
|
|
setAnchorPoint(x, y) { this.anchorX = typeof x === 'object' ? x.x : x; this.anchorY = typeof x === 'object' ? x.y : y; }
|
|
setContentSize(x, y) { this.width = typeof x === 'object' ? x.width : x; this.height = typeof x === 'object' ? x.height : y; }
|
|
setScale(x, y) { this.scaleX = x; this.scaleY = y; }
|
|
convertToWorldSpaceAR(p) {
|
|
const r = this.angle * Math.PI / 180, x = p.x * this.scaleX, y = p.y * this.scaleY;
|
|
const q = vec(this.x + x * Math.cos(r) - y * Math.sin(r), this.y + x * Math.sin(r) + y * Math.cos(r));
|
|
return this.parent ? this.parent.convertToWorldSpaceAR(q) : q;
|
|
}
|
|
convertToNodeSpaceAR(p) {
|
|
const q = this.parent ? this.parent.convertToNodeSpaceAR(p) : p;
|
|
const r = -this.angle * Math.PI / 180, x = q.x - this.x, y = q.y - this.y;
|
|
return vec((x * Math.cos(r) - y * Math.sin(r)) / this.scaleX, (x * Math.sin(r) + y * Math.cos(r)) / this.scaleY);
|
|
}
|
|
}
|
|
class Sprite {}
|
|
Sprite.SizeMode = { CUSTOM: 0 };
|
|
class Mask {}
|
|
const cc = { Component: class {}, Node, Sprite, Mask, SpriteFrame: class {}, v2: vec,
|
|
_decorator: { ccclass: type => type, property: () => () => {} } };
|
|
function load(file, deps = {}) {
|
|
const mod = { exports: {} };
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync(path.join(root, file), 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017, experimentalDecorators: true }
|
|
}).outputText, { cc, module: mod, exports: mod.exports, require: name => { assert.ok(deps[name], name); return deps[name]; } });
|
|
return mod.exports;
|
|
}
|
|
const Scroll = load('assets/Script/prop/RainbowScroll.ts').default;
|
|
const Visual = load('assets/Script/RainbowOldVisual.ts', { './prop/RainbowScroll': { default: Scroll } }).default;
|
|
const { BLOCK_TEXTURE_TRIMS } = load('assets/Script/module/Tool/BlockTexturePaths.ts', {
|
|
'./GameplayAtlasFrames': { GAMEPLAY_ATLAS_PATHS: [] }
|
|
});
|
|
const oldPrefab = json('assets/prefab/prop/rainbow_old.prefab');
|
|
const shapeFrames = Array.from({ length: 23 }, (_, shape) => {
|
|
const meta = json(`assets/UI/prop/rainbow/100color${shape}.png.meta`).subMetas[`100color${shape}`];
|
|
return { uuid: meta.uuid, getRect: () => ({ width: meta.width, height: meta.height }),
|
|
getOriginalSize: () => ({ width: meta.rawWidth, height: meta.rawHeight }), getOffset: () => vec(meta.offsetX, meta.offsetY) };
|
|
});
|
|
function instantiate(data, index = 1) {
|
|
const record = data[index], n = new Node(record);
|
|
record._children.forEach(child => n.addChild(instantiate(data, child.__id__)));
|
|
for (const ref of record._components) {
|
|
const comp = data[ref.__id__], type = { 'cc.Sprite': Sprite, 'cc.Mask': Mask }[comp.__type__];
|
|
if (type) n.components.set(type, { node: n });
|
|
}
|
|
return n;
|
|
}
|
|
function setup(shape, color = 5) {
|
|
const node = instantiate(json(`assets/resources/prefab/block/block${shape}.prefab`));
|
|
const attachment = new Node(oldPrefab[11]); attachment.name = 'key'; node.addChild(attachment);
|
|
const icon = node.getChildByName('icon');
|
|
// Match BlockAssetManager's runtime trim table (which can differ from editor .meta).
|
|
const texture = fs.readFileSync(path.join(root, `assets/Block/${color}color${shape}.png`));
|
|
const width = texture.readUInt32BE(16), height = texture.readUInt32BE(20);
|
|
const trim = BLOCK_TEXTURE_TRIMS[`${color}color${shape}`];
|
|
const sourceFrame = {
|
|
getRect: () => ({ width: trim ? trim[2] : width, height: trim ? trim[3] : height }),
|
|
getOriginalSize: () => ({ width, height }), getOffset: () => trim ? vec(trim[4], trim[5]) : vec()
|
|
};
|
|
icon.getComponent(Sprite).spriteFrame = sourceFrame;
|
|
icon.setContentSize(sourceFrame.getRect());
|
|
const block = { node, block_Info: { block: shape, key: true }, color, type: 2 };
|
|
const overlay = instantiate(oldPrefab); node.addChild(overlay);
|
|
const scroll = new Scroll(); scroll.node = overlay; overlay.components.set(Scroll, scroll);
|
|
const visual = new Visual(); visual.node = overlay; visual.shapeFrames = shapeFrames;
|
|
return { node, attachment, icon, block, overlay, scroll, visual };
|
|
}
|
|
function assertNear(a, b, label) { assert.ok(Math.abs(a - b) < 1e-7, `${label}: ${a} vs ${b}`); }
|
|
function frameBounds(node, frame) {
|
|
const raw = frame.getOriginalSize(), rect = frame.getRect(), off = frame.getOffset();
|
|
return { x: node.x - node.width * node.anchorX + (off.x + (raw.width - rect.width) / 2) * node.width / raw.width,
|
|
y: node.y - node.height * node.anchorY + (off.y + (raw.height - rect.height) / 2) * node.height / raw.height,
|
|
width: rect.width * node.width / raw.width, height: rect.height * node.height / raw.height };
|
|
}
|
|
|
|
test('prefab retains both supplied large textures and serializes all 23 shape dependencies', () => {
|
|
for (const name of ['color1', 'color2']) {
|
|
const node = oldPrefab.find(o => o._name === name);
|
|
const sprite = oldPrefab[node._components[0].__id__];
|
|
assert.equal(node._contentSize.width, 764); assert.equal(node._contentSize.height, 542);
|
|
assert.equal(sprite._spriteFrame.__uuid__, '852480cd-725a-4d3f-af5e-b8dca166dbe2');
|
|
}
|
|
const component = oldPrefab.find(o => o.shapeFrames);
|
|
assert.equal(component.shapeFrames.length, 23);
|
|
component.shapeFrames.forEach((ref, shape) => assert.equal(ref.__uuid__, shapeFrames[shape].uuid));
|
|
assert.ok(oldPrefab[1]._components.some(ref => oldPrefab[ref.__id__] === component));
|
|
});
|
|
|
|
test('all 23 shapes and 10 colors align solid masks and outlines, preserving block data and attachments', () => {
|
|
for (let shape = 0; shape < 23; shape++) for (let color = 1; color <= 10; color++) {
|
|
const f = setup(shape, color), info = f.block.block_Info, iconPosition = f.icon.position;
|
|
f.visual.apply(f.block);
|
|
assert.equal(f.block.color, color); assert.equal(f.block.type, 2); assert.equal(f.block.block_Info, info);
|
|
assert.equal(f.icon.active, false); assert.equal(f.attachment.active, true);
|
|
assert.ok(f.overlay.getSiblingIndex() < f.attachment.getSiblingIndex());
|
|
assert.deepEqual(f.overlay.position, iconPosition);
|
|
assert.equal(f.overlay.anchorX, f.icon.anchorX); assert.equal(f.overlay.anchorY, f.icon.anchorY);
|
|
for (const name of ['bg', 'mask']) {
|
|
const node = f.overlay.getChildByName(name);
|
|
const frame = name === 'bg' ? shapeFrames[shape] : f.icon.getComponent(Sprite).spriteFrame;
|
|
assert.equal(node.getComponent(name === 'bg' ? Sprite : Mask).spriteFrame, frame);
|
|
const bounds = frameBounds(node, frame);
|
|
assertNear(bounds.x, -f.icon.width * f.icon.anchorX, `${shape}/${name} left`);
|
|
assertNear(bounds.y, -f.icon.height * f.icon.anchorY, `${shape}/${name} bottom`);
|
|
assertNear(bounds.width, f.icon.width, `${shape}/${name} width`);
|
|
assertNear(bounds.height, f.icon.height, `${shape}/${name} height`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('large mirrored textures cover every mask continuously through repeated wraps and long frame gaps', () => {
|
|
for (let shape = 0; shape < 23; shape++) {
|
|
const f = setup(shape); f.visual.apply(f.block); f.scroll.onLoad();
|
|
const mask = f.overlay.getChildByName('mask');
|
|
const colors = ['color1', 'color2'].map(name => mask.getChildByName(name));
|
|
const points = [];
|
|
for (let x = 0; x <= 4; x++) for (let y = 0; y <= 4; y++) points.push(vec((x / 4 - 0.5) * mask.width, (y / 4 - 0.5) * mask.height));
|
|
for (let tick = 0; tick < 2400; tick++) {
|
|
for (const point of points) {
|
|
const world = mask.convertToWorldSpaceAR(point);
|
|
assert.ok(colors.some(color => {
|
|
const local = color.convertToNodeSpaceAR(world);
|
|
return local.x >= -color.anchorX * color.width - 1e-6 && local.x <= (1 - color.anchorX) * color.width + 1e-6 &&
|
|
local.y >= -color.anchorY * color.height - 1e-6 && local.y <= (1 - color.anchorY) * color.height + 1e-6;
|
|
}), `shape ${shape}, tick ${tick}, point ${JSON.stringify(point)} uncovered`);
|
|
}
|
|
f.scroll.update(tick === 200 ? 35 : tick === 800 ? 1000 : 1 / 30);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('existing B scroll initializes the same geometry through the new explicit refresh method', () => {
|
|
const node = instantiate(json('assets/prefab/prop/rainbow.prefab'));
|
|
const scroll = new Scroll(); scroll.node = node; scroll.onLoad();
|
|
const before = JSON.stringify({ direction: scroll.direction, exits: scroll.exits, length: scroll.loopLength });
|
|
scroll.refreshGeometry();
|
|
assert.equal(JSON.stringify({ direction: scroll.direction, exits: scroll.exits, length: scroll.loopLength }), before);
|
|
assert.ok(scroll.loopLength > 0);
|
|
});
|