MatchMaster/tools/test-rainbow-old-visual.cjs
WIN-GKKD951VVMJ\Administrator 74cf4413b5 增加一版彩虹机制
2026-09-20 17:14:40 +08:00

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);
});