Files
MatchMaster/tools/test-first-attempt-task-progress.cjs
T

262 lines
10 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const { stripTypeScriptTypes } = require('node:module');
const source = fs.readFileSync(path.join(__dirname, '../assets/Script/module/Task/DailyTaskProgressTracker.ts'), 'utf8')
.replace('export type', 'type').replace('export default class', 'class');
function method(file, name) {
const text = fs.readFileSync(path.join(__dirname, '../assets/Script', file), 'utf8');
const start = text.search(new RegExp(`^ ${name}\\(`, 'm'));
assert.ok(start >= 0);
const body = text.slice(start);
return body.slice(0, body.search(/^ }\r?$/m) + 5);
}
const code = stripTypeScriptTypes(source + `
class MapMethods { ${['failLevel', 'runRewive', 'runRewiveCopy'].map(name => method('Map.ts', name)).join('\n')} }
class ReviveMethods { ${method('Revive.ts', 'revivew')} }
globalThis.MapMethods = MapMethods; globalThis.ReviveMethods = ReviveMethods;
globalThis.Tracker = DailyTaskProgressTracker;`);
function setup(storage = new Map(), uid = 'player-a') {
const task = { taskId: 'daily_3_11', metric: 'first_try_pass', value: 0, target: 2, state: 0 };
const info = { uid, level: 356, otherLevel: 0, GameplayType: 0, endLevelNum: 1,
tasks: { version: 2, dayKey: '2026-10-14', dailyTasks: [task] } };
storage.set('uid', uid);
const api = {
getStorage: key => storage.get(key) ?? null,
setStorage: (key, value) => storage.set(key, structuredClone(value)),
removeStorage: key => storage.delete(key),
};
const context = { console: { log() {} }, setTimeout: () => 1, clearTimeout() {}, cc: { find: () => null, isValid: () => true, fx: {
GameConfig: { GM_INFO: info }, StorageMessage: api, GameTool: {},
} } };
vm.runInNewContext(code, context);
const map = Object.assign(new context.MapMethods(), {
isMapRuntimeAlive: () => true, stopBoom() {}, blocks: [], runReviveAndCheck() {},
iceTrue: () => false, isPauseOpen: () => false, releaseLosePanel() {}, stopTimeCutDown() {},
setPropNum() {}, finishRewiveCopy() {}, add_Time: 0,
});
return { tracker: context.Tracker, map, revive: new context.ReviveMethods(), info, task, storage, api,
key: `dailyTaskAttempt:${uid}:main:357` };
}
test('first operation persists, first win counts once and removes its record', () => {
const h = setup();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
assert.equal(h.storage.get(h.key), true);
h.tracker.recordLevelWon();
assert.equal(h.task.value, 1);
assert.equal(h.storage.has(h.key), false);
h.tracker.recordLevelWon();
assert.equal(h.task.value, 1, 'duplicate settlement cannot count another first win');
});
test('viewing a level then leaving or restarting consumes no attempt', () => {
const h = setup();
h.tracker.beginLevel();
assert.equal(h.storage.has(h.key), false);
h.tracker.invalidateAttempt(); // Return-home/abandon callback without any play.
assert.equal(h.storage.has(h.key), false);
const reopened = setup(h.storage);
reopened.tracker.beginLevel();
assert.equal(h.storage.has(h.key), false);
reopened.tracker.recordLevelOperation();
assert.equal(h.storage.get(h.key), true);
reopened.tracker.recordLevelWon();
assert.equal(reopened.task.value, 1);
assert.equal(h.storage.has(h.key), false);
});
test('repeated operations write once, and actual prop use also starts an attempt', () => {
const h = setup();
let writes = 0;
const save = h.api.setStorage;
h.api.setStorage = (...args) => { writes++; save(...args); };
h.tracker.beginLevel();
assert.equal(writes, 0);
h.tracker.recordPropUsed('freeze');
h.tracker.recordLevelOperation();
h.tracker.recordPropUsed('hammer');
assert.equal(writes, 1);
assert.equal(h.storage.get(h.key), true);
h.tracker.recordLevelWon();
assert.equal(h.task.value, 1);
});
for (const reviveType of ['coin', 'gift']) {
for (const retry of [false, true]) {
test(`${reviveType} revival preserves first-entry eligibility but cannot restore it after retry=${retry}`, () => {
const h = setup();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.map.failLevel('time');
if (retry) {
h.tracker.invalidateAttempt();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.map.gameOver = false;
h.map.failLevel('time');
}
if (reviveType === 'coin') h.map.runRewive({ type: 'time' });
else {
h.map.runRewiveCopy();
h.revive.revivew(h.map);
}
h.tracker.recordLevelWon();
h.tracker.recordLevelWon();
assert.equal(h.task.value, retry ? 0 : 1);
assert.equal(h.storage.has(h.key), false);
});
}
}
test('level 357 failed then retried does not count as a first win and still clears its record', () => {
const h = setup();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.invalidateAttempt();
assert.equal(h.storage.get(h.key), true);
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 0);
assert.equal(h.storage.has(h.key), false);
h.info.level = 357;
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 1, 'next level has its own first attempt');
});
test('leaving without a failure callback still consumes the first attempt', () => {
const h = setup();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 0);
assert.equal(h.storage.has(h.key), false);
});
test('restarting the client preserves the attempted level', () => {
const h = setup();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
const restarted = setup(h.storage);
restarted.tracker.beginLevel();
restarted.tracker.recordLevelOperation();
restarted.tracker.recordLevelWon();
assert.equal(restarted.task.value, 0);
assert.equal(h.storage.has(h.key), false);
});
test('abandoning an active attempt invalidates first win and success clears the record', () => {
const h = setup();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.invalidateAttempt();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 0);
assert.equal(h.storage.has(h.key), false);
});
test('players have independent records and clearing one never clears another', () => {
const a = setup();
a.tracker.beginLevel();
a.tracker.recordLevelOperation();
const b = setup(a.storage, 'player-b');
b.tracker.beginLevel();
b.tracker.recordLevelOperation();
b.tracker.recordLevelWon();
assert.equal(b.task.value, 1);
assert.equal(a.storage.get(a.key), true);
assert.equal(b.storage.has(b.key), false);
const returning = setup(a.storage);
returning.tracker.beginLevel();
returning.tracker.recordLevelOperation();
returning.tracker.recordLevelWon();
assert.equal(returning.task.value, 0);
});
test('an account switch without a new entry cannot use or remove the previous player attempt', () => {
const h = setup();
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.storage.set('uid', 'player-b');
h.tracker.recordLevelWon();
assert.equal(h.task.value, 0);
assert.equal(h.storage.get(h.key), true);
});
test('daily refresh keeps the attempted level and task loading is not required to persist it', () => {
const h = setup();
h.info.tasks = null;
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.changePeriod();
h.tracker.setTaskData({ version: 2, dayKey: '2026-10-15', dailyTasks: [h.task] });
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 0);
assert.equal(h.storage.has(h.key), false);
});
test('success clears the entry key even if level data already changed or no first-win task exists', () => {
const h = setup();
h.info.tasks.dailyTasks = [];
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.info.level = 357;
const nextKey = 'dailyTaskAttempt:player-a:main:358';
h.storage.set(nextKey, true);
h.tracker.recordLevelWon();
assert.equal(h.storage.has(h.key), false);
assert.equal(h.storage.get(nextKey), true);
});
test('special levels do not create mainline attempt records', () => {
const h = setup();
h.info.otherLevel = 20;
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 0);
assert.equal(h.storage.has(h.key), false);
});
test('endless attempts use the sequence number independently of mainline and random map', () => {
const h = setup();
h.info.GameplayType = 1;
h.info.endLevelNum = 357;
h.info.randomLevel = 40;
const endlessKey = 'dailyTaskAttempt:player-a:endless:357';
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
assert.equal(h.storage.get(endlessKey), true);
assert.equal(h.storage.has(h.key), false);
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 0);
assert.equal(h.storage.has(endlessKey), false);
h.info.endLevelNum++;
h.tracker.beginLevel();
h.tracker.recordLevelOperation();
h.tracker.recordLevelWon();
assert.equal(h.task.value, 1);
});
test('unavailable storage cannot grant first-attempt progress or block normal settlement', () => {
const h = setup();
h.api.setStorage = () => { throw Error('storage unavailable'); };
assert.doesNotThrow(() => h.tracker.beginLevel());
assert.doesNotThrow(() => h.tracker.recordLevelOperation());
assert.doesNotThrow(() => h.tracker.recordLevelWon());
assert.equal(h.task.value, 0);
});