MatchMaster/tools/test-win-streak-popup.cjs
2026-09-22 13:41:47 +08:00

144 lines
7.6 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const { loadMembers } = require('./new-player-ab-test-support.cjs');
const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
const popupFile = 'assets/action_bundle/script/WinStreak.ts';
const normalAtlas = json('assets/action_bundle/img/action_hammer.plist.meta');
const rainbowAtlas = json('assets/action_bundle/img/xinWinStreak/winStreak.plist.meta');
const Config = loadMembers('assets/Script/module/Config/GameConfig.ts', [
'isRainbowWinStreak', 'isOldRainbowWinStreak', 'usesRainbowWinStreakUI',
]);
class Sprite {
set spriteFrame(frame) {
this.frame = frame;
if (frame && frame.width) { this.node.width = frame.width; this.node.height = frame.height; }
}
get spriteFrame() { return this.frame; }
}
class Node {
constructor(raw = {}) {
this.name = raw._name || '';
this.active = raw._active !== false;
this.children = [];
this.components = new Map();
this.x = raw._trs ? raw._trs.array[0] : 0;
this.y = raw._trs ? raw._trs.array[1] : 0;
this.scaleX = raw._trs ? raw._trs.array[7] : 1;
this.scaleY = raw._trs ? raw._trs.array[8] : 1;
this.angle = raw._eulerAngles ? raw._eulerAngles.z : 0;
this.width = raw._contentSize ? raw._contentSize.width : 0;
this.height = raw._contentSize ? raw._contentSize.height : 0;
}
set parent(node) { this._parent = node; node.children.push(this); }
get parent() { return this._parent; }
getChildByName(name) { return this.children.find(node => node.name === name); }
getComponent(type) { return this.components.get(type); }
addComponent(type) { const value = new type(); value.node = this; this.components.set(type, value); return value; }
removeAllChildren() { this.children = []; }
convertToWorldSpaceAR(p) {
const r = this.angle * Math.PI / 180, x = p.x * this.scaleX, y = p.y * this.scaleY;
const q = { x: this.x + x * Math.cos(r) - y * Math.sin(r), y: 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 { x: (x * Math.cos(r) - y * Math.sin(r)) / this.scaleX, y: (x * Math.sin(r) + y * Math.cos(r)) / this.scaleY };
}
}
function inflate(data, index = 1) {
const raw = data[index], node = new Node(raw);
for (const ref of raw._components) {
const component = data[ref.__id__];
if (component.__type__ === 'cc.Sprite') node.addComponent(Sprite).fillRange = component._fillRange;
}
for (const ref of raw._children) inflate(data, ref.__id__).parent = node;
return node;
}
function atlas(meta) {
return { _spriteFrames: Object.fromEntries(Object.entries(meta.subMetas)
.map(([name, frame]) => [name.replace(/\.png$/, ''), frame])) };
}
function fixture(group, remote) {
const config = { forceRainbowWinStreakB: false, forceOldRainbowWinStreakC: false,
GM_INFO: { abTests: { layer_two: group }, winStreak: 1,
useravatarIcon: remote ? 'https://example.test/avatar.png' : 'icon_3' },
isRainbowWinStreak: Config.isRainbowWinStreak,
isOldRainbowWinStreak: Config.isOldRainbowWinStreak,
usesRainbowWinStreakUI: Config.usesRainbowWinStreakUI };
const calls = { spine: [], remote: [] };
const cc = { Node, Sprite, v2: (x = 0, y = 0) => ({ x, y }), isValid: () => true,
SpriteFrame: class { constructor(texture) { this.texture = texture; } setTexture(texture) { this.texture = texture; } },
tween: () => ({ delay() { return this; }, to() { return this; }, start() {} }),
fx: { GameConfig: config, GameTool: {
getActionName: selected => selected,
loadSpineSimple: (node, action) => calls.spine.push({ node, action }),
} },
assetManager: { loadRemote: (url, options, callback) => calls.remote.push({ url, options, callback }) },
};
const data = json(`assets/action_bundle/prefab/${group === 1 ? 'winStreak' : 'winStreak2'}.prefab`);
const binding = data.find(record => record.fontUI);
assert.equal(binding.fontUI.__uuid__, normalAtlas.uuid);
if (group !== 1) assert.equal(binding.fontUI2.__uuid__, rainbowAtlas.uuid);
const Popup = loadMembers(popupFile, ['rainbowStars', 'starTime', 'avatarFrame', 'init',
'initRainbowMarkers', 'update', 'numberToImageNodes2', 'numberToImageNodes3'], { cc });
const popup = Object.assign(new Popup(), { node: inflate(data), fontUI: atlas(normalAtlas),
fontUI2: binding.fontUI2 ? atlas(rainbowAtlas) : null });
return { popup, config, calls };
}
function assertDigits(popup, count, meta) {
const actual = popup.node.getChildByName('number').children.map(node => node.getComponent(Sprite).spriteFrame.uuid);
assert.deepEqual(actual, String(count).split('').map(digit => meta.subMetas[`${digit}.png`].uuid));
}
for (const group of [2, 3]) for (const remote of [false, true]) {
test(`${group === 2 ? 'B' : 'C'} popup uses rainbow digit frames, current milestone and ${remote ? 'remote' : 'local'} avatar`, () => {
const { popup, config, calls } = fixture(group, remote);
const root = popup.node, avatar = root.getChildByName('avatar'), reward = root.getChildByName('reward');
const progress = root.getChildByName('progress'), hammer = root.getChildByName('hammer');
const mask = avatar.getChildByName('mask'), icon = mask.getChildByName('icon'), spine = mask.getChildByName('sp');
const originalY = [avatar.y, reward.y];
for (const value of [1, 10, 0, 11, 1]) {
config.GM_INFO.winStreak = value;
popup.init();
const count = Math.min(10, value);
assertDigits(popup, count, rainbowAtlas);
assert.equal(progress.getComponent(Sprite).fillRange, count / 10);
assert.equal(hammer.active, false);
const marker = count ? hammer.getChildByName(String(count)) : progress;
const expectedX = root.convertToNodeSpaceAR(marker.convertToWorldSpaceAR({ x: 0, y: 0 })).x;
assert.equal(avatar.x, expectedX);
assert.equal(reward.x, expectedX);
assert.deepEqual([avatar.y, reward.y], originalY);
assert.equal(icon.active, remote);
assert.equal(spine.active, !remote);
if (remote) {
const request = calls.remote[calls.remote.length - 1];
assert.equal(request.url, config.GM_INFO.useravatarIcon);
assert.equal(request.options.ext, '.png');
const texture = { count };
request.callback(null, texture);
assert.equal(icon.getComponent(Sprite).spriteFrame.texture, texture);
assert.equal(calls.spine.length, 0);
} else {
const request = calls.spine[calls.spine.length - 1];
assert.equal(request.node, spine);
assert.equal(request.action, 'icon_3');
assert.equal(calls.remote.length, 0);
}
}
});
}
test('A popup keeps the original digit atlas and hammer initialization', () => {
const { popup, calls } = fixture(1, false);
popup.init();
assertDigits(popup, 1, normalAtlas);
assert.equal(popup.node.getChildByName('hammer').active, true);
assert.equal(popup.node.getChildByName('hammer').children[0].active, true);
assert.equal(calls.spine.length, 0);
assert.equal(calls.remote.length, 0);
});