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