262 lines
10 KiB
JavaScript
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);
|
|
});
|