MatchMaster/tools/test-seven-day-prefab.cjs

157 lines
8.4 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const ts = require('typescript');
const prefab = JSON.parse(fs.readFileSync('assets/seven_day_gift/prefab/sevenDayGift.prefab'));
const source = fs.readFileSync('assets/seven_day_gift/SevenDayGift.ts', 'utf8');
const frameData = new Map();
function scan(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const path = dir + '/' + entry.name;
if (entry.isDirectory()) scan(path);
else if (path.endsWith('.png.meta')) {
const meta = JSON.parse(fs.readFileSync(path));
for (const frame of Object.values(meta.subMetas || {})) frameData.set(frame.uuid, frame);
}
}
}
scan('assets/seven_day_gift/texture');
const frame = uuid => ({ uuid, getRect() { return { height: frameData.get(uuid).height }; }, getOriginalSize() {
const data = frameData.get(uuid);
assert.ok(data, 'SpriteFrame resolves: ' + uuid);
return { width: data.rawWidth, height: data.rawHeight };
} });
const rewards = [
[{ type: 'infinite_health', count: 900 }], [{ type: 'hammer', count: 1 }],
[{ type: 'infinite_health', count: 1800 }], [{ type: 'freeze', count: 1 }, { type: 'magic_wand', count: 1 }],
[{ type: 'coin', count: 600 }], [{ type: 'hammer', count: 1 }, { type: 'freeze', count: 1 }],
[{ type: 'infinite_health', count: 3600 }, { type: 'cat_skin', count: 1 }],
];
function fixture() {
const cc = { Component: class {}, Animation: 'cc.Animation', Sprite: 'cc.Sprite', Label: 'cc.Label', LabelOutline: 'cc.LabelOutline',
SpriteFrame: class {}, Enum: x => x, Color: { WHITE: {} }, color: (...x) => x,
_decorator: { ccclass: x => x, property: () => () => {} },
Node: class { constructor() { throw Error('Runtime node creation is forbidden'); } },
warn() {}, tween() { return { to() { return this; }, repeatForever() { return this; }, start() {} }; },
};
const context = { cc, exports: {}, require: () => ({ default: class {}, ActivityPopupAnimationType: { 'Q弹弹出': 1 } }) };
vm.runInNewContext(ts.transpileModule(source, { compilerOptions: {
experimentalDecorators: true, target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS,
} }).outputText, context);
const nodes = new Map();
prefab.forEach((data, id) => {
if (data.__type__ !== 'cc.Node') return;
const node = { name: data._name, active: data._active, x: data._trs.array[0], y: data._trs.array[1],
width: data._contentSize.width, height: data._contentSize.height, children: [], components: {},
get childrenCount() { return this.children.length; },
getChildByName(name) { return this.children.find(child => child.name === name); },
getComponent(type) { return this.components[type]; },
setContentSize(w, h) { this.width = w; this.height = h; },
setPosition(x, y) { this.x = x; this.y = y; }, stopAllActions() {},
};
for (const ref of data._components) {
const c = prefab[ref.__id__];
node.components[c.__type__] = { node, string: c._string, fontSize: c._fontSize,
spriteFrame: c._spriteFrame && frame(c._spriteFrame.__uuid__) };
if (c.__type__ === 'cc.Animation') {
const state = { isPlaying: false };
node.components[c.__type__] = { state, starts: 0, getAnimationState: () => state,
play() { this.starts++; state.isPlaying = true; }, stop() { state.isPlaying = false; } };
}
}
nodes.set(id, node);
});
for (const [id, node] of nodes) node.children = prefab[id]._children.map(ref => nodes.get(ref.__id__));
const view = new context.exports.default();
const serialized = prefab.find(x => x.rewardNumberFrames);
for (const [key, value] of Object.entries(serialized)) {
if (value && value.__uuid__) view[key] = frame(value.__uuid__);
}
view.rewardNumberFrames = serialized.rewardNumberFrames.map(value => frame(value.__uuid__));
view.node = nodes.get(1);
assert.equal(view.cacheView(), true);
const info = { canClaim: true, nextRewardDay: 1, rewards: rewards.map((items, i) => ({ day: i + 1, items, claimed: false })) };
return { view, info, nodes };
}
test('prefab retains only fixed reward slots and required claim-state nodes', () => {
function visit(value) {
if (!value || typeof value !== 'object') return;
if ('__id__' in value) assert.ok(prefab[value.__id__], 'reference ' + value.__id__);
Object.values(value).forEach(visit);
}
prefab.forEach(visit);
const { view } = fixture();
for (const [day, card] of view.cards.entries()) {
assert.ok(card.getChildByName('claimedOverlay'));
assert.ok(card.getChildByName('claimedBadge'));
const container = card.getChildByName('rewardItems');
assert.equal(container.children.length, rewards[day].length);
for (const icon of container.children) assert.ok(icon.getChildByName('amount').children.every(n => n.active && /^digit\d+$/.test(n.name)));
}
assert.ok(prefab.filter(n => n.__type__ === 'cc.Node' && !n._active).every(n => ['claimedOverlay', 'claimedBadge'].includes(n._name)));
assert.ok(!prefab.some(n => n.__type__ === 'cc.Node' && /^(layout\d+|amountLabel|close|message|title|subtitle)$/.test(n._name)));
const glow = view.cards[6].getChildByName('catGlow');
assert.ok(glow && glow.active);
assert.equal(glow.width, 480);
const clip = JSON.parse(fs.readFileSync('assets/seven_day_gift/CatRewardGlow.anim'));
assert.equal(clip.wrapMode, 2);
assert.equal(clip._duration, 6);
assert.equal(clip.curveData.props.angle[1].value, 360);
assert.deepEqual(fs.readFileSync('assets/seven_day_gift/texture/catAnimation3.png'), fs.readFileSync('assets/gacha_bundle/img/catAnimation3.png'));
});
test('default data matches baked icon and amount geometry without creating nodes', () => {
const { view, info, nodes } = fixture();
const before = [...nodes.values()].map(n => [n.x, n.y, n.width, n.height, n.active]);
view.setData(info, () => {});
const after = [...nodes.values()].map(n => [n.x, n.y, n.width, n.height, n.active]);
assert.deepEqual(after, before);
});
test('cat glow starts explicitly, survives refresh and stops after claiming', () => {
const { view, info } = fixture();
const glow = view.cards[6].getChildByName('catGlow');
const animation = glow.getComponent('cc.Animation');
view.setData(info, () => {});
assert.equal(animation.state.isPlaying, true);
view.setData(info, () => {});
assert.equal(animation.starts, 1, 'refresh must not restart the rotation');
info.rewards[6].claimed = true;
view.setData(info, () => {});
assert.equal(glow.active, false);
assert.equal(animation.state.isPlaying, false);
info.rewards[6].claimed = false;
view.setData(info, () => {});
assert.equal(animation.state.isPlaying, true);
});
test('claim states and quantity updates use existing nodes without text fallback', () => {
const { view, info } = fixture();
for (let day = 1; day <= 7; day++) {
info.nextRewardDay = day;
info.rewards.forEach(reward => reward.claimed = reward.day < day);
view.setData(info, () => {});
view.cards.forEach((card, i) => assert.equal(card.getChildByName('claimedBadge').active, i + 1 < day));
}
const container = view.cards[0].getChildByName('rewardItems');
const amount = container.children[0].getChildByName('amount');
for (const value of [1.5, 123456789]) {
info.rewards[0].items = [{ type: 'coin', amount: value }];
view.setData(info, () => {});
assert.ok(amount.children.every(n => !n.active), 'unsupported quantities do not display a truncated number');
}
info.rewards[0].items = [{ type: 'infinite_health', count: 600 }];
view.setData(info, () => {});
assert.ok(amount.children.every(n => n.active), 'existing image digits are restored after invalid data');
info.rewards[0].items = [];
view.setData(info, () => {});
assert.equal(view.cards[0].getChildByName('rewardItems').children.some(x => x.active), false);
info.rewards[6].claimed = true;
view.setData(info, () => {});
assert.equal(view.cards[6].getChildByName('catGlow').active, false);
assert.doesNotMatch(source, /new cc\.Node|addComponent\(|createAmount\(/);
});