"use strict"; const fs = require("fs"); const vm = require("vm"); const path = require("path"); const root = path.join(__dirname, ".."); const achievementSource = fs.readFileSync(path.join(root, "js", "achievements.js"), "utf8"); class ClassList { add() {} remove() {} contains() { return false; } toggle() { return false; } } class Element { constructor() { this.classList = new ClassList(); this.children = []; this.dataset = {}; this.style = { setProperty() {}, removeProperty() {} }; this.hidden = true; } append(...children) { this.children.push(...children); } replaceChildren(...children) { this.children = children; } addEventListener() {} focus() {} setAttribute() {} getBoundingClientRect() { return { left: 0, top: 0, width: 100, height: 30 }; } get offsetWidth() { return 100; } } function harness() { const storage = new Map(); const ids = ["achievementsBtn","achievementButtonCount","achievementsDialog","achievementDialogCount","achievementsCloseBtn","achievementsCloseIconBtn","achievementRefreshBtn","achievementResetBtn","achievementUnlockAllBtn","achievementList","achievementSharedStatus","achievementToast","achievementToastTitle","pauseBtn"]; const elements = new Map(ids.map(id => [id, new Element()])); let timer = 0; const c = { console, Date, Intl, Math, JSON, Object, Array, Map, Set, WeakMap, Promise, Number, String, Boolean, Error, AbortController, encodeURIComponent, URLSearchParams, location: { protocol: "file:", search: "" }, crypto: { randomUUID: () => "11111111-1111-4111-8111-111111111111" }, localStorage: { getItem: k => storage.get(k) ?? null, setItem: (k, v) => storage.set(k, String(v)), removeItem: k => storage.delete(k) }, document: { hidden: false, getElementById: id => elements.get(id) || null, createElement: () => new Element(), addEventListener() {} }, TarinaiEvents: { emit() {} }, TarinaiGameDialogs: { confirm: async () => true }, showToast() {}, audio: { uiClick() {} }, setTimeout() { return ++timer; }, clearTimeout() {}, setInterval() { return ++timer; }, clearInterval() {}, fetch: async () => { throw new Error("offline"); }, TARINAI_VERSION: "39.16.77", }; c.window = c; c.globalThis = c; vm.createContext(c); vm.runInContext(achievementSource, c, { filename: "achievements.js" }); return { c, api: c.TarinaiAchievements }; } function assert(value, message) { if (!value) throw new Error(message); } (async function run() { // Sauna timing must start on entering the hot state, not refresh every hot frame. let h = harness(); let temperature = 40; const t = { dead: false, x: 0, y: 0 }; const world = { time: 0, tarinai: [t], ants: [], items: [], feltTemperatureFor() { return temperature; }, temperatureStatusFor(v) { return { direction: v > 25 ? "hot" : (v < 10 ? "cold" : "comfort"), comfortable: v >= 10 && v <= 25 }; }, }; h.api.evaluateWorld(world, {}); world.time = 20; h.api.evaluateWorld(world, {}); // Still hot: must not restart the timer. world.time = 20.1; temperature = 0; h.api.evaluateWorld(world, {}); assert(!h.api.isUnlocked("sauna_cold_plunge"), "sauna timer refreshed while continuously hot"); world.time = 30; temperature = 40; h.api.evaluateWorld(world, {}); // New hot entry. world.time = 40; temperature = 0; h.api.evaluateWorld(world, {}); assert(h.api.isUnlocked("sauna_cold_plunge"), "sauna timer did not restart on a genuine new hot entry"); // Achievement reset must clear per-world/per-object hidden progress. h = harness(); const tarinai = { dead: false, _achievementDirectFeedCount: 9, _achievementDirectTreatmentCount: 3, _achievementSaunaHotAt: 12, _achievementSaunaWasHot: true, _achievementHeldStartedAt: 1234, }; const bomb = { type: "sticky_bomb", stickyBombPassCount: 14, _achievementPlayerPlaced: true, _achievementPlacedAt: 9 }; const resetWorld = { time: 100, tarinai: [tarinai], items: [bomb] }; h.c.world = resetWorld; await h.api.reset(); assert(tarinai._achievementDirectFeedCount === 0 && tarinai._achievementDirectTreatmentCount === 0, "care progress survived achievement reset"); assert(tarinai._achievementSaunaHotAt == null && tarinai._achievementHeldStartedAt == null, "timed per-tarinai progress survived achievement reset"); assert(bomb.stickyBombPassCount === 0, "sticky-bomb relay progress survived achievement reset"); assert(bomb._achievementPlayerPlaced === true && bomb._achievementPlacedAt == null, "reset should preserve placement identity but clear quick-delete timing"); // World-changing history/ground operations are interventions, so passive observation must restart. h = harness(); const observerWorld = { time: 0, tarinai: [], ants: [], items: [], fieldType: "garden", groundType: "soil" }; h.api.evaluateWorld(observerWorld, {}); observerWorld.time = 359.9; h.api.evaluateWorld(observerWorld, {}); assert(!h.api.isUnlocked("idle_observer_5_minutes"), "observer unlocked before three game days"); h.api.recordHistoryAction("undo", { world: observerWorld, deadRestored: 0 }); observerWorld.time = 360; h.api.evaluateWorld(observerWorld, {}); assert(!h.api.isUnlocked("idle_observer_5_minutes"), "undo did not reset passive-observation progress"); observerWorld.time = 500; h.api.recordGroundChange({ world: observerWorld, previous: "soil", next: "ice", now: 1000 }); observerWorld.time = 859.9; h.api.evaluateWorld(observerWorld, {}); assert(!h.api.isUnlocked("idle_observer_5_minutes"), "ground change did not reset passive-observation progress"); observerWorld.time = 860; h.api.evaluateWorld(observerWorld, {}); assert(h.api.isUnlocked("idle_observer_5_minutes"), "observer did not unlock after three uninterrupted game days"); // Manually adding a Tarinai is also a gameplay intervention. This must reset // the surveillance-camera window even if the separate lively-making achievement // has already been completed. h = harness(); const addWorld = { time: 0, tarinai: [], ants: [], items: [], fieldType: "garden", groundType: "soil" }; h.api.evaluateWorld(addWorld, {}); addWorld.time = 359.9; h.api.recordManualTarinaiAdded({ world: addWorld, tarinai: { id: "manual-1" }, tool: "new" }); addWorld.time = 719.8; h.api.evaluateWorld(addWorld, {}); assert(!h.api.isUnlocked("idle_observer_5_minutes"), "manual Tarinai addition did not reset passive-observation progress"); addWorld.time = 719.9; h.api.evaluateWorld(addWorld, {}); assert(h.api.isUnlocked("idle_observer_5_minutes"), "observer did not unlock after three game days following manual Tarinai addition"); // Minimalist needs player-placement identity even after Unplanned City is already unlocked. const snapshotSource = fs.readFileSync(path.join(root, "js", "snapshot_system.js"), "utf8"); assert(snapshotSource.includes('const needPlayerPlacedMetadata = needQuickDelete || needMinimalist;'), "minimalist placement metadata is not retained independently of quick-delete progress"); assert(snapshotSource.includes('needPlayerPlacedMetadata && rec?.item?._achievementPlayerPlaced ? 1 : 0'), "snapshot does not serialize minimalist placement identity"); // Lethal shocks must count before damage can mark the target dead. const signalSource = fs.readFileSync(path.join(root, "js", "signal_system.js"), "utf8"); const shockStart = signalSource.indexOf("function shockTarget"); const shockEnd = signalSource.indexOf("function damageFromWires", shockStart); const shockBody = signalSource.slice(shockStart, shockEnd); const recordAt = shockBody.indexOf("recordWireShock"); const damageAt = shockBody.indexOf("target.damage?."); assert(recordAt >= 0 && damageAt >= 0 && recordAt < damageAt, "wire-shock achievement is still recorded after potentially lethal damage"); console.log("[OK] secondary achievement regressions: sauna onset, reset cleanup, observer interventions including manual Tarinai addition, minimalist save metadata, and lethal wire shocks passed"); })();