Files
MatchMaster/tools/test-new-mode-settlement.cjs
2026-10-27 13:28:12 +08:00

158 lines
8.7 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const { loadMembers } = require('./new-player-ab-test-support.cjs');
function setup(newMode) {
const events = [];
let entrance;
const streak = {};
const win = { active: false, getChildByName: () => streak };
const cc = {
error: assert.fail,
fx: { GameConfig: {
GM_INFO: { level: 20, addLevel: 5 },
canSettleCareer: () => true,
getFeatureUnlockLevel: () => 20,
usesRainbowWinStreakUI: () => false,
} },
};
const Map = loadMembers('assets/Script/Map.ts', [
'autoNextAfterNewMode', 'showWinWhenReady', 'playWinSettlement', 'startWinPresentation',
'onNewModeConfirmed', 'onNewModeReturned', 'onNewModeClosed',
'settlementAnimationTime', 'settlementAnimationSpeed', 'hideNewModeSettlementDecorations',
], { cc, PopupType: { Bounce: 1 }, popupContent: root => [root],
PopupAnimator: { open: (node, type, done) => { entrance = done; } } });
const map = Object.assign(new Map(), {
new_mode: newMode ? 3 : 0,
gameOverNode: { active: false },
isMapRuntimeAlive: () => true,
ensureWinLoaded: done => done(null, win),
getWinRoot: () => win,
releaseBlockAssets() {}, releaseLosePanel() {},
prepareWinSettlement: () => events.push('prepare'),
setPassProgress: () => events.push('pass'),
createRank: () => events.push('rank'),
createCareer: () => events.push('career'),
Settlement: () => events.push('animation'),
openWinStreak: () => events.push('streak'),
openNewMode: type => events.push(`intro:${type}`),
releaseNewModePanel: () => events.push('close'),
winLevel: () => assert.fail('must not enter the next level automatically'),
scheduleOnce: () => assert.fail('must not schedule automatic progression'),
});
return { map, events, win, enter: () => entrance() };
}
test('new gameplay introduction accompanies the same full settlement as ordinary wins', () => {
const normal = setup(false), introduced = setup(true);
normal.map.showWinWhenReady();
introduced.map.showWinWhenReady();
assert.deepEqual(normal.events, ['prepare', 'pass']);
assert.deepEqual(introduced.events, ['prepare', 'pass']);
normal.enter(); introduced.enter();
assert.deepEqual(normal.events, ['prepare', 'pass', 'rank', 'career', 'animation', 'streak']);
assert.deepEqual(introduced.events, [...normal.events, 'intro:0']);
assert.equal(introduced.win.active, true);
assert.equal(introduced.map.gameOverNode.active, true);
assert.equal(introduced.map.settlementAnimationTime(2), 2);
assert.equal(introduced.map.settlementAnimationSpeed(), 1);
assert.equal(introduced.map.hideNewModeSettlementDecorations(), false);
});
test('introduction buttons only close the panel without repeating settlement or advancing', () => {
for (const action of ['onNewModeConfirmed', 'onNewModeReturned']) {
const { map, events, enter } = setup(true);
map.showWinWhenReady();
enter();
const before = events.slice();
map[action]();
assert.deepEqual(events, [...before, 'close']);
assert.equal(map.autoNextAfterNewMode, false);
assert.equal(map.gameOverNode.active, true);
}
});
// Use the authored prefab states: nextBtn, homeBtn and the coin group start active.
function presentationFixture(newMode, otherLevel) {
const fs = require('node:fs');
const data = JSON.parse(fs.readFileSync('assets/win/prefab/Win.prefab', 'utf8'));
function inflate(id) {
const raw = data[id], position = raw._trs.array;
const node = { name: raw._name, active: raw._active, opacity: raw._opacity,
position: { x: position[0], y: position[1], z: position[2] }, scaleX: position[7], scaleY: position[8],
children: (raw._children || []).map(ref => inflate(ref.__id__)),
getChildByName(name) { return this.children.find(child => child.name === name); },
};
node.childrenCount = node.children.length;
return node;
}
const root = inflate(data[0].data.__id__), body = root.getChildByName('tween');
const deferred = ['gongxi', 'homeBtn', 'homeBtn2', 'nextBtn', 'nextBtn2', 'tiaodik', 'guang', 'diguan', 'endlessNode', 'Ruzhi'];
const panels = ['Career', 'Rank', 'Ruzhi', 'WinStreak', 'WinStreak2'];
const assertHidden = () => {
for (const name of deferred) assert.equal(body.getChildByName(name).active, false, `${name} must wait for its animation`);
for (const name of panels) assert.equal(root.getChildByName(name).active, false, `${name} must wait for initialized data`);
};
let visible = false, entrance, careerUpdates = 0, intros = 0;
Object.defineProperty(root, 'active', { get: () => visible, set(value) {
if (value) assertHidden(); // Initialization must precede activation itself.
visible = value;
} });
const tweens = [];
const cc = { isValid: n => !!n, error: assert.fail, v3: (x,y,z) => ({x,y,z}), Tween: { stopAllByTarget() {} },
tween(target) {
const steps = [], tween = { target, steps,
to(time, props) { steps.push(() => Object.assign(target, props)); return tween; },
delay() { return tween; }, call(fn) { steps.push(fn); return tween; },
start() { tweens.push(tween); return tween; },
}; return tween;
},
fx: { AudioManager: { _instance: { playEffect() {} } }, GameConfig: {
GM_INFO: { level: 20, addLevel: 8, otherLevel },
canSettleCareer: () => true, getFeatureUnlockLevel: () => 20,
usesRainbowWinStreakUI: () => false, isNewPlayerB: () => false,
} },
};
const Controller = loadMembers('assets/Script/Map.ts', [
'showWinWhenReady', 'prepareWinSettlement', 'playWinSettlement', 'startWinPresentation', 'Settlement',
'playButtonGroup', 'getSettlementNextButton', 'resetRuzhiVariantBButton',
'settlementAnimationTime', 'hideNewModeSettlementDecorations', 'setSettlementNodeVisible',
], { cc, PopupType: { Bounce: 1 }, popupContent: root => [body],
PopupAnimator: { open(content, type, done) { assertHidden(); entrance = done; } } });
const map = Object.assign(new Controller(), {
node: {}, arr: [], new_mode: newMode ? 3 : 0, autoNextAfterNewMode: false,
gameOverNode: body, getWinRoot: () => root, isMapRuntimeAlive: () => true,
ensureWinLoaded: done => done(null, root), releaseBlockAssets() {}, releaseLosePanel() {}, setPassProgress() {},
createRank() { root.getChildByName('Rank').active = true; },
createCareer() { careerUpdates++; }, openWinStreak() {}, openNewMode() { intros++; },
playDecorationGroup() {},
});
return { map, root, body, tweens, assertHidden, enter: () => entrance(),
careerUpdates: () => careerUpdates, intros: () => intros,
finish(target) { const tween = tweens.find(t => t.target === target); assert.ok(tween); tween.steps.forEach(step => step()); } };
}
for (const newMode of [false, true]) for (const reused of [false, true]) {
test(`Win prefab newMode=${newMode} reused=${reused}: no controls appear before their settlement phase`, () => {
const f = presentationFixture(newMode, reused ? 1 : 0);
assert.equal(f.body.getChildByName('nextBtn').active, true, 'prefab reproduces the original flash');
if (reused) f.body.children.forEach(node => { node.active = true; });
f.map.showWinWhenReady();
f.assertHidden(); assert.equal(f.careerUpdates(), 0);
f.enter(); // Generic entrance completes; only now start settlement animations.
assert.equal(f.careerUpdates(), 1); assert.equal(f.intros(), newMode ? 1 : 0);
assert.equal(f.body.getChildByName('nextBtn').active, false);
assert.equal(f.body.getChildByName('nextBtn2').active, false);
assert.equal(f.body.getChildByName('tiaodik').active, false);
f.finish(f.body.getChildByName('gongxi')); // Title finishes and schedules button group.
assert.equal(f.body.getChildByName('nextBtn2').active, false);
f.finish(f.body.getChildByName(reused ? 'homeBtn2' : 'homeBtn'));
f.finish(f.body.getChildByName('nextBtn2'));
assert.equal(f.body.getChildByName('nextBtn2').active, !reused);
assert.equal(f.body.getChildByName(reused ? 'homeBtn2' : 'homeBtn').active, true);
assert.equal(f.body.getChildByName('nextBtn').active, false, 'obsolete button stays hidden');
f.finish(f.body.getChildByName('tiaodik')); assert.equal(f.body.getChildByName('tiaodik').active, true);
assert.equal(f.careerUpdates(), 1, 'preparation does not duplicate settlement side effects');
});
}