From 91012674ac7ed4f74375c04bbee422c2358e4afe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:48:07 +0000 Subject: [PATCH 1/2] feat: persist stats to localStorage (first arrival, theme, accelerator game state) Agent-Logs-Url: https://github.com/nitrocode/token-deathclock/sessions/dbf157f1-17d0-4091-8b39-12198e88a121 Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- script.js | 71 ++++++++++++++++++++++++++++++++++++++++++-- tests/script.test.js | 1 + 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/script.js b/script.js index 8b669b8..be7f596 100644 --- a/script.js +++ b/script.js @@ -49,6 +49,22 @@ let currentTheme = 'dark'; let chartInstance = null; + // ---- Persistence keys ----------------------------------- + const LS_FIRST_ARRIVAL_KEY = 'tokenDeathclockFirstArrival'; + const LS_THEME_KEY = 'tokenDeathclockTheme'; + + // Cumulative first-arrival timestamp — loaded from localStorage so the + // "Tokens Since You Arrived" counter continues across return visits. + let firstArrivalTime = pageLoadTime; + try { + const stored = parseInt(localStorage.getItem(LS_FIRST_ARRIVAL_KEY) || '0', 10); + if (stored > 0 && stored <= pageLoadTime) { + firstArrivalTime = stored; + } else { + localStorage.setItem(LS_FIRST_ARRIVAL_KEY, String(pageLoadTime)); + } + } catch (_) { /* ignore quota / access errors */ } + // ---- Helpers --------------------------------------------- function getCurrentTokens() { const elapsed = (Date.now() - BASE_DATE_MS) / 1000; @@ -74,6 +90,7 @@ function toggleTheme() { const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; applyTheme(newTheme); + try { localStorage.setItem(LS_THEME_KEY, newTheme); } catch (_) { /* ignore */ } if (newTheme === 'light') awardBadge('optimist'); } @@ -82,8 +99,9 @@ const now = Date.now(); const tokens = getCurrentTokens(); const currentRate = getRateAtDate(new Date(now)); - const sessionTokens = Math.round((now - pageLoadTime) / 1000 * currentRate); - const elapsed = Math.floor((now - pageLoadTime) / 1000); + // Use firstArrivalTime so the counter accumulates across return visits + const sessionTokens = Math.round((now - firstArrivalTime) / 1000 * currentRate); + const elapsed = Math.floor((now - firstArrivalTime) / 1000); const totalEl = document.getElementById('totalCounter'); const sessionEl = document.getElementById('sessionCounter'); @@ -96,7 +114,8 @@ if (sessionTimeEl) { const m = Math.floor(elapsed / 60); const s = elapsed % 60; - sessionTimeEl.textContent = m > 0 ? `${m}m ${s}s on page` : `${s}s on page`; + const suffix = firstArrivalTime < pageLoadTime ? 'since first visit' : 'on page'; + sessionTimeEl.textContent = m > 0 ? `${m}m ${s}s ${suffix}` : `${s}s ${suffix}`; } if (rateEl) rateEl.textContent = formatTokenCount(currentRate); if (rateEventEl) { @@ -1925,6 +1944,7 @@ const LS_UPGRADES_KEY = 'tokenDeathclockUpgrades'; const LS_BESTSCORE_KEY = 'tokenDeathclockBestScore'; const LS_COMPANY_KEY = 'tokenDeathclockCompany'; + const LS_GAME_STATE_KEY = 'tokenDeathclockGameState'; // ---- State ----------------------------------------------- @@ -1981,6 +2001,31 @@ } } } catch (_) { /* ignore */ } + try { + const raw = localStorage.getItem(LS_GAME_STATE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + if (typeof parsed.personalTokens === 'number' && isFinite(parsed.personalTokens) && parsed.personalTokens >= 0) { + acc.personalTokens = parsed.personalTokens; + } + if (typeof parsed.doomPoints === 'number' && isFinite(parsed.doomPoints) && parsed.doomPoints >= 0) { + acc.doomPoints = parsed.doomPoints; + } + if (typeof parsed.totalTaps === 'number' && isFinite(parsed.totalTaps) && parsed.totalTaps >= 0) { + acc.totalTaps = Math.floor(parsed.totalTaps); + } + if (typeof parsed.milestonesTriggered === 'number' && isFinite(parsed.milestonesTriggered) && parsed.milestonesTriggered >= 0) { + acc.milestonesTriggered = Math.floor(parsed.milestonesTriggered); + } + if (Array.isArray(parsed.personalMilestoneSet)) { + acc.personalMilestoneSet = new Set( + parsed.personalMilestoneSet.filter((id) => typeof id === 'string') + ); + } + } + } + } catch (_) { /* ignore */ } // Recompute tap multiplier and passive rate from persisted state acc.tapMultiplier = currentTapMultiplier(); acc.passiveRate = computePassiveRate(acc.ownedAgents, acc.replacedWorkers); @@ -1995,6 +2040,15 @@ ownedAgents: acc.ownedAgents, })); } catch (_) { /* ignore */ } + try { + localStorage.setItem(LS_GAME_STATE_KEY, JSON.stringify({ + personalTokens: acc.personalTokens, + doomPoints: acc.doomPoints, + totalTaps: acc.totalTaps, + milestonesTriggered: acc.milestonesTriggered, + personalMilestoneSet: [...acc.personalMilestoneSet], + })); + } catch (_) { /* ignore */ } } function currentTapMultiplier() { @@ -2589,6 +2643,11 @@ startComboResetLoop(); // Passive token generation loop startPassiveLoop(); + // Persist game state every 30 seconds and immediately when the page is hidden + setInterval(saveAcceleratorState, 30000); + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') saveAcceleratorState(); + }); } // ============================================================ @@ -2848,6 +2907,12 @@ // ---- Bootstrap ------------------------------------------ function init() { + // Restore persisted theme preference before rendering anything + try { + const savedTheme = localStorage.getItem(LS_THEME_KEY); + if (savedTheme === 'dark' || savedTheme === 'light') applyTheme(savedTheme); + } catch (_) { /* ignore */ } + // Theme toggle const toggleBtn = document.getElementById('themeToggle'); if (toggleBtn) toggleBtn.addEventListener('click', toggleTheme); diff --git a/tests/script.test.js b/tests/script.test.js index 98a1704..1fd1455 100644 --- a/tests/script.test.js +++ b/tests/script.test.js @@ -87,6 +87,7 @@ function runUpdateCounters() { beforeEach(() => { updateCountersFn = null; + localStorage.clear(); document.body.innerHTML = MIN_HTML; jest.clearAllMocks(); loadScript(); From a70ab33a42c35f2213b9d1dc4ebe7b1464376058 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:50:28 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20code=20review=20=E2=80=94?= =?UTF-8?q?=20use=20!=3D=3D=20for=20return-visit=20detection,=20move=20sav?= =?UTF-8?q?e=20listener=20to=20init()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/nitrocode/token-deathclock/sessions/dbf157f1-17d0-4091-8b39-12198e88a121 Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- script.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/script.js b/script.js index be7f596..f2f5939 100644 --- a/script.js +++ b/script.js @@ -114,7 +114,7 @@ if (sessionTimeEl) { const m = Math.floor(elapsed / 60); const s = elapsed % 60; - const suffix = firstArrivalTime < pageLoadTime ? 'since first visit' : 'on page'; + const suffix = firstArrivalTime !== pageLoadTime ? 'since first visit' : 'on page'; sessionTimeEl.textContent = m > 0 ? `${m}m ${s}s ${suffix}` : `${s}s ${suffix}`; } if (rateEl) rateEl.textContent = formatTokenCount(currentRate); @@ -2643,11 +2643,6 @@ startComboResetLoop(); // Passive token generation loop startPassiveLoop(); - // Persist game state every 30 seconds and immediately when the page is hidden - setInterval(saveAcceleratorState, 30000); - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') saveAcceleratorState(); - }); } // ============================================================ @@ -2956,6 +2951,12 @@ initPresenceStrip(); initEventLog(); + // Persist accelerator game state every 30 seconds and on page hide + setInterval(saveAcceleratorState, 30000); + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') saveAcceleratorState(); + }); + // Kick off the live counter RAF loop requestAnimationFrame(updateCounters);