MatchMaster/tools/test-lose-confirmation.cjs
2026-09-21 15:30:27 +08:00

144 lines
8.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const { loadMembers, withNewPlayerConfig, loadModule } = require('./new-player-ab-test-support.cjs');
const prefab = JSON.parse(fs.readFileSync('assets/lose/prefab/Lose.prefab', 'utf8'));
const frameData = Array.from({ length: 6 }, (_, i) => {
const meta = JSON.parse(fs.readFileSync(`assets/lose/texture/giveup${i + 1}.png.meta`, 'utf8'));
return Object.values(meta.subMetas)[0];
});
function fixture({ infinite = false, level = 50, group = 'B', streak = 10, help = 0, rainbow = false } = {}) {
const config = withNewPlayerConfig({ GM_INFO: {
level, otherLevel: help, winStreak: streak, revive: 0,
abTestAssignments: { layer_1: { name: 'new1_50', group, enabled: true } },
}, isRainbowWinStreak: () => rainbow });
let resets = 0, stoppedRevive = 0;
const cc = {
v2: (x, y) => ({ x, y }), Sprite: { SizeMode: { RAW: 2 } },
tween: () => ({ to() { return this; }, start() {} }),
fx: { GameConfig: config, AudioManager: { _instance: { playEffect() {} } },
GameTool: { getUserPowerTime: () => infinite, shushu_Track() {}, setWinStreak: () => resets++ } },
};
const nodes = prefab.map(data => data.__type__ === 'cc.Node' ? {
name: data._name, active: data._active, width: data._contentSize.width, height: data._contentSize.height,
x: data._trs.array[0], y: data._trs.array[1], scaleX: data._trs.array[7], scaleY: data._trs.array[8],
children: [], sprite: {},
getChildByName(name) { return this.children.find(n => n.name === name); },
getComponent() { return this.sprite; },
setPosition(x, y) { this.x = typeof x === 'object' ? x.x : x; this.y = typeof x === 'object' ? x.y : y; },
setScale(value) { this.scaleX = this.scaleY = value; },
setContentSize({ width, height }) { this.width = width; this.height = height; },
} : null);
nodes.forEach((node, i) => { if (node) node.children = prefab[i]._children.map(ref => nodes[ref.__id__]); });
const root = nodes[1], lose = root.getChildByName('lose');
const confirmation = loadModule('assets/Script/GiveUpConfirmation.ts', {}, { cc });
const Panel = loadMembers('assets/lose/script/LosePanel.ts', ['showGiveUpConfirmation'], { cc, ...confirmation });
const panel = Object.assign(new Panel(), { node: root, giveUpFrames: frameData.map(data => ({
uuid: data.uuid, getOriginalSize: () => ({ width: data.rawWidth, height: data.rawHeight }),
})) });
root.getComponent = () => panel;
const MapController = loadMembers('assets/Script/Map.ts',
['homeBtn', 'openHealth', 'closeWinStreak', 'hideLoseReasonPanels'], { cc, ...confirmation });
const map = Object.assign(new MapController(), {
node: { active: true }, homeCanTouch: true, reviewState: false, isMapRuntimeAlive: () => true,
getLoseRoot: () => root, trackFinishi() {},
revive: { getComponent: () => ({ offShow: () => stoppedRevive++ }) },
});
map.hideLoseReasonPanels(lose);
lose.getChildByName('Time').active = true;
return { panel, map, lose, config, setInfinite: value => infinite = value,
resets: () => resets, stoppedRevive: () => stoppedRevive };
}
test('all six title frames are serialized in giveup1–6 order', () => {
const component = prefab.find(data => data.__type__ === '0fbfcSygcRKl6AshXSeUHTO');
assert.deepEqual(component.giveUpFrames.map(ref => ref.__uuid__), frameData.map(data => data.uuid));
});
for (const rainbow of [false, true]) {
test(`all eight loss combinations reuse the ${rainbow ? 'rainbow' : 'normal'} panel without stale icons`, () => {
const { panel, lose } = fixture();
const cases = [
[true, true, true, 3], [true, true, false, 2], [true, false, false, 1],
[false, true, false, 4], [false, true, true, 5], [false, false, true, 6],
[true, false, true, 1], [false, false, false, null],
];
for (const [hp, day, streak, titleNumber] of cases) {
assert.equal(panel.showGiveUpConfirmation(hp, day, streak, rainbow), titleNumber !== null);
const current = lose.getChildByName(rainbow ? 'WinStreak2' : 'WinStreak');
const other = lose.getChildByName(rainbow ? 'WinStreak' : 'WinStreak2');
assert.equal(other.active, false);
assert.equal(current.active, titleNumber !== null);
if (!titleNumber) continue;
const icons = ['hp', 'day', 'winStreak'].map(name => current.getChildByName(name));
assert.deepEqual(icons.map(icon => icon.active), [hp, day, streak]);
const title = current.getChildByName('result_title');
assert.equal(title.sprite.spriteFrame.uuid, frameData[titleNumber - 1].uuid);
assert.equal(title.sprite.trim, false);
assert.equal(title.width / title.height, frameData[titleNumber - 1].rawWidth / frameData[titleNumber - 1].rawHeight);
const visible = icons.filter(icon => icon.active);
for (const icon of visible) {
assert.equal(icon.scaleX, icon.scaleY, 'icons retain their aspect ratio');
assert.ok(icon.y - icon.height * icon.scaleY / 2 > -263, 'icons stay above the think-again button');
}
if (visible.length === 1) assert.equal(visible[0].x, 0);
if (visible.length >= 2) {
assert.ok(visible[0].x < 0 && visible[1].x > 0);
assert.ok(visible[0].y >= visible[1].y);
}
if (visible.length === 3) assert.ok(visible[2].x === 0 && visible[2].y < visible[0].y);
}
});
}
test('mainline unlock boundaries and help levels select the correct losses', () => {
const cases = [
[{ level: 34 }, [true, false, false]], [{ level: 35 }, [true, true, false]],
[{ level: 49 }, [true, true, false]], [{ level: 50 }, [true, true, true]],
[{ level: 50, streak: 9 }, [true, true, false]],
[{ group: 'A', level: 0, streak: 0 }, [true, true, false]],
[{ infinite: true }, [false, true, true]],
[{ help: 1 }, [true, false, false]],
[{ help: 1, infinite: true }, null], [{ level: 34, infinite: true }, null],
];
for (const rainbow of [false, true]) for (const [options, expected] of cases) {
const f = fixture({ ...options, rainbow });
const before = JSON.stringify(f.config.GM_INFO);
f.map.homeBtn(null, 'time');
const current = f.lose.getChildByName(rainbow ? 'WinStreak2' : 'WinStreak');
assert.equal(current.active, !!expected, JSON.stringify(options));
assert.equal(f.lose.getChildByName('Health').active, !expected);
assert.equal(JSON.stringify(f.config.GM_INFO), before, 'display does not mutate reward or task state');
if (expected) {
assert.deepEqual(['hp', 'day', 'winStreak'].map(name => current.getChildByName(name).active), expected);
assert.equal(f.resets(), 0, 'opening confirmation must not clear a streak');
assert.equal(f.stoppedRevive(), 0, 'revive remains available until exit is confirmed');
} else {
assert.equal(f.lose.getChildByName('Health').getChildByName('node2').active, true);
}
}
});
test('cancel keeps the revive screen available; confirm opens Health with the current stamina state', () => {
for (const help of [0, 1]) for (const infinite of [false, true]) {
const f = fixture({ help });
f.map.homeBtn(null, 'time');
f.map.closeWinStreak();
assert.equal(f.map.homeCanTouch, true);
assert.equal(f.lose.getChildByName('Time').active, true);
assert.equal(f.resets(), 0);
f.map.homeBtn(null, 'time');
f.setInfinite(infinite);
f.map.openHealth();
const health = f.lose.getChildByName('Health');
assert.equal(health.active, true);
assert.equal(health.getChildByName('node1').active, !infinite);
assert.equal(health.getChildByName('node2').active, infinite);
assert.equal(f.lose.getChildByName('WinStreak').active, false);
assert.equal(f.lose.getChildByName('Time').active, false);
assert.equal(f.resets(), help ? 0 : 1, 'assist exits must not clear the player streak');
}
});