158 lines
8.7 KiB
JavaScript
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');
|
|
});
|
|
}
|