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); });