diff -ruN base/src/app.html mod/src/app.html --- base/src/app.html 2026-11-02 20:48:05.666206528 -0300 +++ mod/src/app.html 2026-11-02 20:48:06.072534140 -0300 @@ -2,10 +2,17 @@ + + + + Atom Clicker %sveltekit.head% - +
%sveltekit.body%
diff -ruN base/src/env.ts mod/src/env.ts --- base/src/env.ts 2026-11-02 20:48:05.666889453 -0300 +++ mod/src/env.ts 2026-11-02 20:48:06.072432597 -0300 @@ -1,13 +1,4 @@ import { defineEnvVars } from '@sveltejs/kit/env'; -export const variables = defineEnvVars({ - PUBLIC_GOOGLE_CLIENT_ID: { - description: 'Web client ID of the Supabase Google provider, the One Tap prompt only shows when it is set.', - public: true, - schema: (value) => value || undefined, - static: true, - }, - PUBLIC_SUPABASE_PUBLISHABLE_KEY: { public: true, static: true }, - PUBLIC_SUPABASE_URL: { public: true, static: true }, - SUPABASE_SECRET_KEY: { static: true }, -}); +/** O build do FredyGames nao tem servidor, contas nem chaves: nenhuma variavel de ambiente. */ +export const variables = defineEnvVars({}); diff -ruN base/src/hooks.client.ts mod/src/hooks.client.ts --- base/src/hooks.client.ts 2026-11-02 20:48:05.482994962 -0300 +++ mod/src/hooks.client.ts 2026-11-02 20:48:06.072628141 -0300 @@ -1,47 +1,10 @@ -import { dev } from '$app/env'; import type { HandleClientError } from '@sveltejs/kit/hooks'; -import { initGlobalErrorHandlers, reportError } from '#lib/helpers/errorReporting.js'; -import { getItem, setItem } from '#lib/utils/safeLocalStorage.js'; - -initGlobalErrorHandlers(); - -/** A first visit loads its chunks before the service worker takes over, so their URLs are handed to it to make the game playable offline. */ -if ('serviceWorker' in navigator && !navigator.serviceWorker.controller) { - navigator.serviceWorker.ready.then(({ active }) => active?.postMessage(performance.getEntriesByType('resource').map(({ name }) => name))); -} - -/** Cloudflare Workers serves only the current build, so a tab left open across a deploy asks for a chunk that is gone. */ -const STALE_CHUNK_PATTERN = /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module/i; -const RELOAD_GUARD_KEY = 'stale-chunk-reload'; -const RELOAD_GUARD_WINDOW = 60_000; - -let reloadAttempted = false; - -/** Reloads once to pick up the new build, guarded on both sides so a genuinely broken chunk cannot loop. */ -function recoverFromStaleChunk(): boolean { - if (reloadAttempted) return false; - reloadAttempted = true; - - const lastReload = Number(getItem(RELOAD_GUARD_KEY) ?? 0); - if (Number.isFinite(lastReload) && Date.now() - lastReload < RELOAD_GUARD_WINDOW) return false; - - setItem(RELOAD_GUARD_KEY, String(Date.now())); - location.reload(); - return true; -} export const handleError: HandleClientError = async ({ error, kind }) => { // Errors thrown with `error(...)` are expected and keep their own status and message if (kind === 'app') return; if (kind === 'framework' && error.status === 404) return { message: 'Page not found' }; - const reported = error instanceof Error ? error : new Error(kind === 'framework' ? error.message : 'Unknown client error'); - if (STALE_CHUNK_PATTERN.test(reported.message) && recoverFromStaleChunk()) { - return { message: 'A new version was deployed, reloading...' }; - } - console.error('[Client Error]', error); - await reportError(reported); - - return dev ? { message: reported.message, stack: reported.stack } : { message: 'An unexpected error occurred. The error has been reported.' }; + return { message: 'An unexpected error occurred.' }; }; diff -ruN base/src/lib/components/game/Achievements.svelte mod/src/lib/components/game/Achievements.svelte --- base/src/lib/components/game/Achievements.svelte 2026-11-02 20:48:05.581640516 -0300 +++ mod/src/lib/components/game/Achievements.svelte 2026-11-02 20:48:06.079925812 -0300 @@ -103,8 +103,7 @@ {#snippet content()}

- Each achievement awards 1 the first time your account unlocks it, unlocking it again after a reset pays nothing. Hidden - achievements stay secret until you find them. + Hidden achievements stay secret until you find them.

{/snippet}
diff -ruN base/src/lib/components/layout/NavBar.svelte mod/src/lib/components/layout/NavBar.svelte --- base/src/lib/components/layout/NavBar.svelte 2026-11-02 20:48:05.567628682 -0300 +++ mod/src/lib/components/layout/NavBar.svelte 2026-11-02 20:48:06.075907139 -0300 @@ -1,24 +1,19 @@ - - diff -ruN base/src/lib/components/modals/SaveRecovery.svelte mod/src/lib/components/modals/SaveRecovery.svelte --- base/src/lib/components/modals/SaveRecovery.svelte 2026-11-02 20:48:05.573162040 -0300 +++ mod/src/lib/components/modals/SaveRecovery.svelte 2026-11-02 20:48:06.079635308 -0300 @@ -1,5 +1,4 @@ @@ -100,7 +89,7 @@
-
+
{#if current} {:else} diff -ruN base/src/lib/components/settings/Changelog.svelte mod/src/lib/components/settings/Changelog.svelte --- base/src/lib/components/settings/Changelog.svelte 2026-11-02 20:48:05.547200063 -0300 +++ mod/src/lib/components/settings/Changelog.svelte 2026-11-02 20:48:06.078392585 -0300 @@ -62,7 +62,7 @@ gameManager.unlockAchievement('changelog_modal_opener'); changelog.markSeen(); try { - releases = parse(await (await fetch('/Changelog.md')).text()); + releases = parse(await (await fetch('Changelog.md')).text()); } catch (error) { console.error('Failed to load changelog:', error); } diff -ruN base/src/lib/components/settings/Credits.svelte mod/src/lib/components/settings/Credits.svelte --- base/src/lib/components/settings/Credits.svelte 2026-11-02 20:48:05.553236968 -0300 +++ mod/src/lib/components/settings/Credits.svelte 2026-11-02 20:48:06.077652368 -0300 @@ -1,27 +1,12 @@ + +
+
+
+ + This device +
+
+
Play time
+
{formatDuration(gameManager.inGameTime)}
+
Level
+
{gameManager.playerLevel}
+
Saved
+
{gameManager.lastSave ? dateFormat.format(gameManager.lastSave) : 'Never'}
+
+
+ {#each Object.values(CurrenciesTypes) as type (type)} + {const amount = $derived(currenciesManager.currencies[type]?.amount ?? 0)} + {#if amount > 0 || type === CurrenciesTypes.ATOMS} + + {/if} + {/each} +
+

Your progress is saved automatically in this browser, nowhere else. Clearing the site data removes it.

+
+ +
+
+

Hard reset

+

Wipes all progress on this device and starts over from scratch.

+
+ +
+
+ +{#if showHardReset} + (showHardReset = false)} /> +{/if} diff -ruN base/src/lib/helpers/errorReporting.ts mod/src/lib/helpers/errorReporting.ts --- base/src/lib/helpers/errorReporting.ts 2026-11-02 20:48:05.651315725 -0300 +++ mod/src/lib/helpers/errorReporting.ts 2026-11-02 20:48:06.074072575 -0300 @@ -1,148 +1,6 @@ -import { browser, dev } from '$app/env'; -import { CurrenciesTypes, type CurrencyName } from '#data/currencies.js'; -import { getBrowserInfo, isCrawler, type BrowserInfo } from '#helpers/clientInfo.js'; -import { gameManager } from '#helpers/GameManager.svelte.js'; -import { isNoiseError } from '#lib/utils/errorNoise.js'; -import { supabaseAuth } from '#stores/supabaseAuth.svelte.js'; - -interface ErrorReport { - browserInfo: BrowserInfo | null; - errorMessage: string; - gameState: Record | null; - stackTrace: string | null; - url: string | null; -} - -const recentErrorHashes = new Set(); -const DEDUP_WINDOW = 5 * 60 * 1000; - -/** Zaraz frames carry a `?z=` payload that encodes the player's referrer and inflates the field. */ -function sanitizeStackTrace(stackTrace: string | null): string | null { - if (!stackTrace) return null; - return stackTrace.replace(/\?z=[^\s):]*/g, ''); -} - -function isDuplicateError(errorMessage: string, stackTrace: string | null): boolean { - const hash = `${errorMessage}::${stackTrace?.substring(0, 500) || ''}`; - if (recentErrorHashes.has(hash)) return true; - - recentErrorHashes.add(hash); - setTimeout(() => recentErrorHashes.delete(hash), DEDUP_WINDOW); - return false; -} - -/** An allow-list rather than the whole state, so the payload stays small and a newly saved field never leaks in by accident. */ -function captureGameState(): Record | null { - if (!browser) return null; - - try { - const state = gameManager.getCurrentState(); - const amount = (currency: CurrencyName) => state.currencies?.[currency]?.amount ?? 0; - - return { - achievements: state.achievements?.length ?? 0, - activePowerUps: state.activePowerUps?.length ?? 0, - atoms: amount(CurrenciesTypes.ATOMS), - generators: Object.entries(state.generators || {}).reduce( - (acc, [key, generator]) => { - if (generator) { - acc[key] = { count: generator.count, level: generator.level }; - } - return acc; - }, - {} as Record - ), - electronizes: state.totalElectronizesAllTime ?? 0, - electrons: amount(CurrenciesTypes.ELECTRONS), - excitedPhotons: amount(CurrenciesTypes.EXCITED_PHOTONS), - higgsBoson: amount(CurrenciesTypes.HIGGS_BOSON), - highestAPS: state.highestAPS ?? 0, - inGameTime: state.inGameTime ?? 0, - photons: amount(CurrenciesTypes.PHOTONS), - protonises: state.totalProtonisesAllTime ?? 0, - protons: amount(CurrenciesTypes.PROTONS), - radiationMass: state.radiation?.mass ?? 0, - radiationUnlocked: state.radiation?.unlocked ?? false, - // Every realm stays mounted, so this is the visible one only - realm: state.selectedRealmId ?? null, - saveTampered: gameManager.integrityFlagged, - saveWarnings: gameManager.saveIntegrityWarnings, - skillUpgrades: state.skillUpgrades?.length ?? 0, - totalClicks: state.totalClicksAllTime ?? 0, - totalXP: state.totalXP ?? 0, - upgrades: state.upgrades?.length ?? 0, - version: state.version - }; - } catch { - return null; - } -} - -function createErrorReport(error: Error | string): ErrorReport { - const errorMessage = error instanceof Error ? error.message : String(error); - const stackTrace = error instanceof Error ? error.stack ?? null : null; - - return { - browserInfo: getBrowserInfo(), - errorMessage, - gameState: captureGameState(), - stackTrace: sanitizeStackTrace(stackTrace), - url: browser ? window.location.href : null - }; -} - +/** Versao do FredyGames: erros ficam no console, nada e enviado a servidor. */ export async function reportError(error: Error | string): Promise { - if (!browser) return; - - if (dev) { - console.log('[ErrorReporting] Skipping error report in dev mode:', error); - return; - } - - if (isCrawler()) return; - - try { - const report = createErrorReport(error); - if (isNoiseError(report.errorMessage, report.stackTrace)) return; - if (isDuplicateError(report.errorMessage, report.stackTrace)) return; - - // The server attributes the report to the session behind this token, so an unauthenticated report stays anonymous. - const authHeaders = await supabaseAuth.authHeaders().catch(() => ({})); - - await fetch('/api/errors', { - body: JSON.stringify(report), - headers: { ...authHeaders, 'Content-Type': 'application/json' }, - method: 'POST' - }); - } catch { - console.error('[ErrorReporting] Failed to report error'); - } -} - -/** - * Errors thrown by extension content scripts fail `instanceof Error` (other realm), and `new Error(String(reason))` - * would stamp our own frame on them, so the original message and stack are carried over as-is. - */ -function toReportable(reason: unknown): Error | string { - if (reason instanceof Error) return reason; - if (reason && typeof reason === 'object' && 'message' in reason && typeof reason.message === 'string') { - const error = new Error(reason.message); - error.stack = 'stack' in reason && typeof reason.stack === 'string' ? reason.stack : undefined; - return error; - } - return String(reason); + console.error('[Error]', error); } -export function initGlobalErrorHandlers(): void { - if (!browser) return; - - window.addEventListener('unhandledrejection', (event) => { - reportError(toReportable(event.reason)); - }); - - window.addEventListener('error', (event) => { - // Script loading errors carry no error object and nothing actionable - if (!event.error) return; - reportError(event.error); - }); -} +export function initGlobalErrorHandlers(): void {} diff -ruN base/src/lib/helpers/particles.ts mod/src/lib/helpers/particles.ts --- base/src/lib/helpers/particles.ts 2026-11-02 20:48:05.646606085 -0300 +++ mod/src/lib/helpers/particles.ts 2026-11-02 20:48:06.079139052 -0300 @@ -104,7 +104,7 @@ for (const { id } of Object.values(CURRENCIES)) { const image = new Image(); image.onload = () => this.iconSprites.set(id, this.rasterizeIcon(image)); - image.src = `/currencies/${id}.svg`; + image.src = `currencies/${id}.svg`; } } diff -ruN base/src/lib/helpers/PerfMonitor.ts mod/src/lib/helpers/PerfMonitor.ts --- base/src/lib/helpers/PerfMonitor.ts 2026-11-02 20:48:05.651721813 -0300 +++ mod/src/lib/helpers/PerfMonitor.ts 2026-11-02 20:48:06.074221493 -0300 @@ -1,352 +1,8 @@ -import { FeatureTypes } from '#data/features.js'; -import { getDeviceInfo, isCrawler, isUuid, SESSION_ID } from '#helpers/clientInfo.js'; -import { gameManager } from '#helpers/GameManager.svelte.js'; -import { INCIDENT_MIN_MS, INCIDENTS_SENT, stripUrlDetails } from '#helpers/perfIncidents.js'; -import { EVENT_DURATION_THRESHOLD, isDegraded, rankSources, RESUME_WARMUP_MS, SLOW_INPUT_MS, WARMUP_MS } from '#helpers/perfStats.js'; -import { perfTracker } from '#helpers/perfTracker.js'; -import { realmManager } from '#helpers/RealmManager.svelte.js'; -import type { LoafScript, PerfWindow, SessionGame, SessionPerf, SessionStartPayload, SessionStartResponse, SessionUpdatePayload } from '#lib/types/session.js'; -import { getJSON, setItem } from '#lib/utils/safeLocalStorage.js'; -import { privacy } from '#stores/privacy.svelte.js'; -import { supabaseAuth } from '#stores/supabaseAuth.svelte.js'; -import { ui } from '#stores/ui.svelte.js'; -import { prefersReducedMotion } from 'svelte/motion'; - -/** `durationThreshold` is missing from the DOM lib. */ -interface EventObserverInit extends PerformanceObserverInit { - durationThreshold: number; -} - -/** Long Animation Frames aren't in the DOM lib yet, and `sourceURL` is missing in some Chromium versions. */ -interface LoafScriptTiming { - duration: number; - invoker: string; - invokerType: string; - sourceFunctionName: string; - sourceURL?: string; -} - -interface LoafTiming extends PerformanceEntry { - scripts: readonly LoafScriptTiming[]; - styleAndLayoutStart: number; -} - -const DAY_MS = 86_400_000; -const FINAL_BEACON_GAP_MS = 30_000; -const INTERACTION_EVENTS = new Set(['click', 'keydown', 'pointerdown', 'pointerup']); -const MAX_FILE_LENGTH = 60; -const MAX_INVOKER_LENGTH = 80; -const MAX_SCRIPTS = 3; -const MAX_TARGET_LENGTH = 60; -const MAX_FN_LENGTH = 60; -const MAX_TEXT_LENGTH = 24; -/** A large container's text is cut before it is scanned. */ -const MAX_TEXT_SOURCE = 200; -/** Ten windows of 10 s a minute, so a periodic update goes out every five minutes of visible play. */ -const PERIODIC_WINDOWS = 30; -const RING_SIZE = 12; -const SOURCES_SENT = 12; -const STORAGE_KEY = 'atomic-clicker-perf'; -const TARGET_SELECTOR = '[aria-label],[data-hint],[title],[role="button"],a,button,canvas'; - -/** The nearest labelled element's name, digits masked so a counter that ticks does not make every label unique: `Atom`, `Buy # atoms`. */ -function describeTarget(target: Element): string { - const element = target.closest(TARGET_SELECTOR) ?? target; - const label = element.getAttribute('aria-label') ?? element.getAttribute('data-hint') ?? element.getAttribute('title'); - if (label) return label.slice(0, MAX_TARGET_LENGTH); - - const text = element.textContent?.slice(0, MAX_TEXT_SOURCE).replace(/[\d.,]+/g, '#').replace(/\s+/g, ' ').trim().slice(0, MAX_TEXT_LENGTH); - return text || element.tagName.toLowerCase(); -} - -const toScript = ({ duration, invoker, invokerType, sourceFunctionName, sourceURL }: LoafScriptTiming): LoafScript => ({ - durationMs: Math.round(duration), - file: sourceURL?.split(/[?#]/)[0].split('/').pop()?.slice(0, MAX_FILE_LENGTH) || null, - fn: sourceFunctionName.slice(0, MAX_FN_LENGTH) || null, - invoker: stripUrlDetails(`${invokerType}:${invoker}`).slice(0, MAX_INVOKER_LENGTH), -}); - -/** - * Watches the session for main thread blocks through the perf tracker and the browser's own entries (Event Timing, Long Animation - * Frames), tags each with what the player did just before, and reports to `/api/sessions`. Everything respects the usage analytics switch. - */ +/** Versao do FredyGames: sem telemetria de desempenho enviada ao servidor. */ export class PerfMonitor { - private awaitingKey = false; - /** Set once a degraded report is due, the 24 h cooldown starts when the server accepts an update that carries it. */ - private cooldownPending = false; - private dirty = false; - private evidence: PerfWindow[] | null = null; - private eventTiming = false; - private lastBeaconAt = 0; - private loafSupported = false; - private observers: PerformanceObserver[] = []; - private readonly onAction = (event: Event) => { - if (event.target instanceof Element) perfTracker.action(event.type === 'keydown' ? 'key:' : 'tap:', event.target, event.timeStamp); - }; - private readonly onPageShow = (event: PageTransitionEvent) => { - if (event.persisted) this.resume(RESUME_WARMUP_MS); - }; - private readonly onVisibility = () => { - if (document.hidden) this.hide(); - else this.resume(RESUME_WARMUP_MS); - }; - private readonly onHide = () => this.hide(); - private readonly ring: PerfWindow[] = []; - /** The server refused the session (unknown, capped or too old), nothing more is sent for this page load. */ - private rejected = false; - private reportedThisLoad = false; - private started = false; - private warmup: ReturnType | undefined; - private windowsSinceUpdate = 0; - private writeKey: string | null = null; - - get degraded() { - return this.evidence !== null; - } - - /** Resolves the player once through `fetch` with the bearer token, `sendBeacon` cannot carry one, and keeps the write key. */ - async open() { - if (!this.started || this.writeKey || !privacy.analytics) return; - - try { - const device = getDeviceInfo(); - if (!device) return; - - const authHeaders = await supabaseAuth.authHeaders().catch(() => ({})); - const headers = { ...authHeaders, 'Content-Type': 'application/json' }; - const payload: SessionStartPayload = { device, game: this.gameContext() }; - - const response = await fetch('/api/sessions', { body: JSON.stringify(payload), headers, method: 'POST' }); - if (!response.ok) return; - this.writeKey = ((await response.json()) as SessionStartResponse).writeKey; - } catch { - return; - } - - if (this.awaitingKey) this.send(false); - } - - /** Idempotent, and a no-op for crawlers, non-UUID sessions (which is every server-side run), missing `performance` or analytics turned off. */ - start() { - if (this.started || typeof performance === 'undefined' || isCrawler() || !isUuid(SESSION_ID) || !privacy.analytics) return; - - this.started = true; - try { - perfTracker.onWindow = closed => this.onWindow(closed); - perfTracker.incidents.environment = { describe: describeTarget, modal: () => ui.activeModalId, realm: () => realmManager.selectedRealmId }; - document.addEventListener('visibilitychange', this.onVisibility); - document.addEventListener('pointerdown', this.onAction, { capture: true, passive: true }); - document.addEventListener('keydown', this.onAction, { capture: true, passive: true }); - window.addEventListener('pagehide', this.onHide); - window.addEventListener('pageshow', this.onPageShow); - this.observe(); - if (!document.hidden) this.resume(WARMUP_MS); - } catch { - this.stop(); - } - } - - stop() { - if (!this.started) return; - - this.started = false; - clearTimeout(this.warmup); - perfTracker.pause(); - perfTracker.onWindow = null; - perfTracker.incidents.environment = null; - document.removeEventListener('visibilitychange', this.onVisibility); - document.removeEventListener('pointerdown', this.onAction, { capture: true }); - document.removeEventListener('keydown', this.onAction, { capture: true }); - window.removeEventListener('pagehide', this.onHide); - window.removeEventListener('pageshow', this.onPageShow); - for (const observer of this.observers) observer.disconnect(); - this.observers = []; - } - - summary(): SessionPerf { - return { - evidence: this.evidence ?? [...this.ring], - incidents: perfTracker.incidents.worst(INCIDENTS_SENT), - sources: rankSources(perfTracker.sessionSources(), SOURCES_SENT), - totals: { ...perfTracker.totals(), eventTiming: this.eventTiming, loaf: this.loafSupported }, - }; - } - - /** Game context is read at report time only, nothing here runs on a hot path. */ - private gameContext(): SessionGame { - let canvasBytes = 0; - const canvases = document.querySelectorAll('canvas'); - for (const canvas of canvases) canvasBytes += canvas.width * canvas.height * 4; - - return { - animations: document.getAnimations?.().length ?? null, - autoClicksPerSecond: gameManager.autoClicksPerSecond, - backdrops: document.querySelectorAll('[class*="backdrop-blur"]').length, - canvasBytes, - canvases: canvases.length, - covered: ui.covered, - electronizes: gameManager.totalElectronizesAllTime, - glows: document.querySelectorAll('[data-perf-layer="glow"]').length, - ionizes: gameManager.totalIonizesAllTime, - level: gameManager.features[FeatureTypes.LEVELS] ? gameManager.playerLevel : null, - modal: ui.activeModalId, - protonises: gameManager.totalProtonisesAllTime, - realm: realmManager.selectedRealmId, - realms: realmManager.availableRealms.map(({ id }) => id), - reducedMotion: prefersReducedMotion.current, - }; - } - - /** Pauses, drops the partial window and sends what changed since the last report, a closing tab gets one beacon per 30 s at most. */ - private hide() { - clearTimeout(this.warmup); - perfTracker.pause(); - if (this.dirty && Date.now() - this.lastBeaconAt >= FINAL_BEACON_GAP_MS) this.send(true); - } - - private observe() { - if (typeof PerformanceObserver === 'undefined') return; - - const supported = PerformanceObserver.supportedEntryTypes ?? []; - this.eventTiming = supported.includes('event'); - this.loafSupported = supported.includes('long-animation-frame'); - - if (this.eventTiming) { - const init: EventObserverInit = { durationThreshold: EVENT_DURATION_THRESHOLD, type: 'event' }; - this.watch(init, entries => this.onEvents(entries as PerformanceEventTiming[])); - } - if (this.loafSupported) this.watch({ type: 'long-animation-frame' }, entries => this.onLoaf(entries as LoafTiming[])); - } - - /** Every interaction feeds the latency p95, the slow ones become incidents with their three phases. */ - private onEvents(entries: PerformanceEventTiming[]) { - if (perfTracker.paused) return; - - for (const entry of entries) { - const interaction = entry.interactionId > 0 || (entry.interactionId === undefined && INTERACTION_EVENTS.has(entry.name)); - if (!interaction || entry.startTime < perfTracker.resumedAt) continue; - - perfTracker.input(entry.duration); - if (entry.duration < SLOW_INPUT_MS) continue; - - const end = entry.startTime + entry.duration; - perfTracker.incidents.observe({ - end, - input: { - delayMs: Math.max(0, Math.round(entry.processingStart - entry.startTime)), - presentationMs: Math.max(0, Math.round(end - entry.processingEnd)), - processingMs: Math.max(0, Math.round(entry.processingEnd - entry.processingStart)), - target: entry.target instanceof Element ? describeTarget(entry.target) : null, - }, - start: entry.startTime, - }); - } - } - - private onLoaf(entries: LoafTiming[]) { - if (perfTracker.paused) return; - - for (const entry of entries) { - if (entry.duration < INCIDENT_MIN_MS || entry.startTime < perfTracker.resumedAt) continue; - - const end = entry.startTime + entry.duration; - const scripts = entry.scripts.map(toScript).sort((a, b) => b.durationMs - a.durationMs); - const scriptMs = entry.scripts.reduce((sum, script) => sum + script.duration, 0); - const styleLayoutMs = entry.styleAndLayoutStart > 0 ? Math.max(0, end - entry.styleAndLayoutStart) : 0; - perfTracker.incidents.observe({ - end, - loaf: { - otherMs: Math.max(0, Math.round(entry.duration - scriptMs - styleLayoutMs)), - scriptMs: Math.round(scriptMs), - scripts: scripts.slice(0, MAX_SCRIPTS), - styleLayoutMs: Math.round(styleLayoutMs), - }, - start: entry.startTime, - }); - } - } - - private onWindow(closed: PerfWindow) { - if (!privacy.analytics) { - this.stop(); - return; - } - - this.dirty = true; - this.windowsSinceUpdate++; - if (!this.evidence) { - this.ring.push(closed); - if (this.ring.length > RING_SIZE) this.ring.shift(); - if (isDegraded(this.ring)) { - this.evidence = [...this.ring]; - this.reportDegraded(); - return; - } - } - if (this.windowsSinceUpdate >= PERIODIC_WINDOWS) this.send(false); - } - - /** - * One degraded report per page load, and one per 24 h per device: inside that day the periodic and final updates still carry the - * evidence, so the server still flags the session. The day only starts once the server took the report. - */ - private reportDegraded() { - const { degradedAt = 0 } = getJSON<{ degradedAt?: number }>(STORAGE_KEY, {}); - if (this.reportedThisLoad || Date.now() - degradedAt < DAY_MS) return; - - this.reportedThisLoad = true; - this.cooldownPending = true; - this.send(false); - } - - private resume(delay: number) { - clearTimeout(this.warmup); - this.warmup = setTimeout(() => { - if (!document.hidden) perfTracker.resume(); - }, delay); - } - - /** `final` sends through `sendBeacon` for a closing page, anything else through a keepalive `fetch`. */ - private send(final: boolean) { - if (!privacy.analytics || this.rejected) return; - if (!this.writeKey) { - this.awaitingKey = true; - return; - } - - this.awaitingKey = false; - this.dirty = false; - this.windowsSinceUpdate = 0; - const payload: SessionUpdatePayload = { game: this.gameContext(), perf: this.summary(), sessionId: SESSION_ID, writeKey: this.writeKey }; - const body = JSON.stringify(payload); - - if (final) { - this.lastBeaconAt = Date.now(); - navigator.sendBeacon('/api/sessions/update', new Blob([body], { type: 'application/json' })); - return; - } - - const carriesDegraded = this.degraded; - fetch('/api/sessions/update', { body, headers: { 'Content-Type': 'application/json' }, keepalive: true, method: 'POST' }) - .then(response => { - if (response.status === 404) this.rejected = true; - if (response.ok && carriesDegraded && this.cooldownPending) { - this.cooldownPending = false; - setItem(STORAGE_KEY, JSON.stringify({ degradedAt: Date.now() })); - } - }) - .catch(() => {}); - } - - private watch(init: PerformanceObserverInit, handle: (entries: PerformanceEntryList) => void) { - try { - const observer = new PerformanceObserver(list => handle(list.getEntries())); - observer.observe(init); - this.observers.push(observer); - } catch { - // An entry type the browser lists but refuses to observe only costs its own numbers. - } - } + async open() {} + start() {} + stop() {} } export const perfMonitor = new PerfMonitor(); diff -ruN base/src/lib/stores/remoteMessage.svelte.ts mod/src/lib/stores/remoteMessage.svelte.ts --- base/src/lib/stores/remoteMessage.svelte.ts 2026-11-02 20:48:05.500195543 -0300 +++ mod/src/lib/stores/remoteMessage.svelte.ts 2026-11-02 20:48:06.073940698 -0300 @@ -1,97 +1,11 @@ -import { browser } from '$app/env'; -import { getJSON, setItem } from '#lib/utils/safeLocalStorage.js'; -import { supabaseAuth } from '#stores/supabaseAuth.svelte.js'; -import type { GameMessage } from '#lib/types/supabase.js'; - +/** Versao do FredyGames: sem mensagens remotas do autor (a original busca um aviso no servidor). */ class RemoteMessageStore { - message = $state(null); - dismissedIds = $state([]); - isVisible = $state(true); - - /** Banner height in rem, one 1.5rem row per 100 visible characters, so a long message does not overflow the bar. Zero when nothing is shown. */ - bannerHeightRem = $derived.by(() => { - if (!this.message || !this.isVisible) return 0; - const visibleLength = this.message.message_html.replace(/<[^>]*>/g, '').length; - return 1.5 * Math.max(1, Math.ceil(visibleLength / 100)); - }); - - private interval: ReturnType | null = null; - - constructor() { - if (browser) { - this.dismissedIds = getJSON('dismissed_messages', []); - - $effect.root(() => { - $effect(() => { - if (supabaseAuth.supabase) { - this.fetchMessage(); - } - }); - }); - } - } - - async fetchMessage() { - if (!browser || !supabaseAuth.supabase) return; - - try { - const { data, error } = await supabaseAuth.supabase - .from('game_messages') - .select('*') - .eq('is_active', true) - .order('created_at', { ascending: false }) - .limit(1) - .maybeSingle(); - - if (error) { - if (error.code !== '42P01') { - console.error('Error fetching remote message:', error); - } - return; - } - - if (data) { - const isNewMessage = data.id !== this.message?.id; - this.message = data; - if (isNewMessage) { - this.isVisible = !this.dismissedIds.includes(data.id); - } - } else { - this.message = null; - } - } catch (err) { - console.error('Failed to fetch remote message:', err); - } - } - - dismiss() { - if (!this.message) return; - const newDismissedIds = [...new Set([...this.dismissedIds, this.message.id])]; - this.dismissedIds = newDismissedIds; - if (browser) { - setItem('dismissed_messages', JSON.stringify(newDismissedIds)); - } - this.isVisible = false; - } - - show() { - this.isVisible = true; - } - - startPolling() { - if (!browser) return; - if (this.interval) return; - - this.fetchMessage(); - this.interval = setInterval(() => this.fetchMessage(), 30000); // 30 seconds - } + bannerHeightRem = 0; + isVisible = false; + message = null as null; - stopPolling() { - if (this.interval) { - clearInterval(this.interval); - this.interval = null; - } - } + startPolling() {} + stopPolling() {} } export const remoteMessage = new RemoteMessageStore(); diff -ruN base/src/lib/stores/supabaseAuth.svelte.ts mod/src/lib/stores/supabaseAuth.svelte.ts --- base/src/lib/stores/supabaseAuth.svelte.ts 2026-11-02 20:48:05.504213633 -0300 +++ mod/src/lib/stores/supabaseAuth.svelte.ts 2026-11-02 20:48:06.073664862 -0300 @@ -1,461 +1,47 @@ -import type { SupabaseClient, User, Session, Provider } from '@supabase/supabase-js'; -import { browser } from '$app/env'; -import { PUBLIC_GOOGLE_CLIENT_ID, PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY } from '$app/env/public'; import type { GameState } from '#lib/types.js'; -import type { Database, Profile } from '#lib/types/supabase.js'; -import { getItem, isLocalStorageAvailable, removeItem, setItem } from '#lib/utils/safeLocalStorage.js'; -import { multiTabDetector } from '#stores/multiTab.svelte.js'; -import { SAVE_VERSION, migrateSavedState, validateAndRepairGameState } from '#helpers/saves.js'; +import type { Profile } from '#lib/types/auth.js'; -/** postMessage type the /callback page sends to the window that opened it as a login popup. */ +/** + * Versao do FredyGames: o jogo nao tem contas, placar nem nuvem. Esta classe mantem a forma da original (todo o resto do + * jogo a consulta) mas fica sempre "sem login" e nunca faz uma requisicao. + */ export const AUTH_CALLBACK_MESSAGE = 'atom-clicker:auth-callback'; -/** The profile columns the client reads, the save blob stays out of it and is only fetched by the cloud save calls. */ -type AccountProfile = Pick; -const PROFILE_COLUMNS = 'id, picture, username'; - export type CloudSaveInfo = GameState & { lastSaveDate: number | null }; class SupabaseAuth { - isAuthenticated = $state(false); - user = $state(null); - profile = $state.raw(null); - /** The profile row wins over the OAuth provider metadata, which only seeds it. */ - avatarUrl = $derived(this.profile?.picture || this.user?.user_metadata?.avatar_url || this.user?.user_metadata?.picture || null); - displayName = $derived( - this.profile?.username || this.user?.user_metadata?.username || this.user?.user_metadata?.full_name || this.user?.email?.split('@')[0] || null, - ); - loading = $state(true); - supabase = $state | null>(null); - error = $state(null); - /** Another device wrote the cloud save since this one last synced, auto-save holds off until an upload or a load picks a side. */ - cloudConflict = $state(false); - - private currentSession: Session | null = null; - private heartbeatInterval: ReturnType | null = null; - private initialized = false; - - /** - * Initializes the Supabase client and sets up auth listeners. - * Must be called in a browser environment. - */ - async init() { - if (!browser || this.initialized) { - this.loading = false; - return; - } - - // Not available in Web Workers, and blocked outright when site data is disabled or storage is partitioned - if (!isLocalStorageAvailable()) { - console.warn('SupabaseAuth: localStorage not available, skipping init'); - this.loading = false; - return; - } - - try { - // Kept out of the initial bundle: the client weighs ~60 kB gzipped and nothing renders through it. - const { createClient } = await import('@supabase/supabase-js'); - this.supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLISHABLE_KEY, { - auth: { - autoRefreshToken: true, - persistSession: true, - detectSessionInUrl: true, - }, - }); - - multiTabDetector.init(); - - this.initialized = true; - - const { - data: { session }, - } = await this.supabase.auth.getSession(); - this.currentSession = session; - await this.handleAuthStateChange(session?.user || null); - - this.supabase.auth.onAuthStateChange(async (event, session) => { - console.log('Auth state changed:', event); - this.currentSession = session; - await this.handleAuthStateChange(session?.user || null); - }); - - window.addEventListener('beforeunload', () => { - if (this.currentSession?.access_token) { - fetch('/api/auth/status', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.currentSession.access_token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ is_online: false }), - keepalive: true, - }); - } - }); - } catch (err) { - console.error('Supabase auth initialization error:', err); - this.error = err as Error; - this.loading = false; - } - } - - private async handleAuthStateChange(user: User | null) { - try { - if (user) { - this.isAuthenticated = true; - this.user = user; - if (this.profile?.id === user.id) { - this.loading = false; - this.error = null; - return; - } - this.loading = true; - - console.log('Auth state change for user:', user.id); - - let { data: profile, error } = await (this.supabase!).from('profiles').select(PROFILE_COLUMNS).eq('id', user.id).single(); - - // If profile doesn't exist yet (might be due to trigger lag), wait a bit and retry - if (error && error.code === 'PGRST116') { - console.log('Profile not found yet, retrying in 1s...'); - await new Promise((resolve) => setTimeout(resolve, 1000)); - const retry = await (this.supabase!).from('profiles').select(PROFILE_COLUMNS).eq('id', user.id).single(); - profile = retry.data; - error = retry.error; - } - - if (profile) { - this.profile = profile; - console.log('Found profile:', profile.username); - - // Fire and forget: nothing downstream reads it, and awaiting it would stall the boot sequence. - void (this.supabase!).from('profiles').update({ is_online: true, updated_at: new Date().toISOString() }).eq('id', user.id); - - this.startHeartbeat(); - } else { - console.error('Failed to find or create profile:', error?.message); - } + avatarUrl = null as string | null; + cloudConflict = false; + displayName = 'Player'; + error = null as string | null; + isAuthenticated = false; + profile = null as Profile | null; + supabase = null as null; + user = null as { id: string } | null; - this.loading = false; - this.error = null; - } else { - console.log('User signed out'); - this.stopHeartbeat(); - this.isAuthenticated = false; - this.user = null; - this.profile = null; - this.loading = false; - this.error = null; - } - } catch (err) { - console.error('Error handling auth state change:', err); - this.error = err as Error; - this.loading = false; - } - } - - private startHeartbeat() { - this.stopHeartbeat(); - this.heartbeatInterval = setInterval(async () => { - /** supabase-js pauses its token refresh in hidden tabs, so the cached session can be expired here, getSession() refreshes it. */ - const headers = await this.authHeaders(); - if (!('Authorization' in headers)) return; - try { - await fetch('/api/auth/status', { - body: JSON.stringify({ is_online: true }), - headers: { ...headers, 'Content-Type': 'application/json' }, - method: 'POST', - }); - } catch (err) { - console.error('Heartbeat failed:', err); - } - }, 45_000); - } - - private stopHeartbeat() { - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } - } - - /** - * Returns a fresh access token for authenticating requests to our own API routes. - * Reads from Supabase (which auto-refreshes) rather than the cached session, so it stays valid. - */ - private async getAccessToken(): Promise { - if (!this.supabase) return null; - const { data: { session } } = await this.supabase.auth.getSession(); - this.currentSession = session; - return session?.access_token ?? null; - } - - /** The bearer header for our own API routes, empty when signed out. */ + async init() {} async authHeaders(): Promise> { - const accessToken = await this.getAccessToken(); - return accessToken ? { Authorization: `Bearer ${accessToken}` } : {}; - } - - async signInWithProvider(provider: Provider) { - if (!browser || !this.supabase) { - await this.init(); - if (!this.supabase) return; - } - - // Google refuses to render inside a frame, so embeds (itch.io, galaxy.click) log in through a popup that posts the callback URL back. - const embedded = window.self !== window.top; - // Opened synchronously in the click handler, popup blockers reject a window.open that comes after an await. - const popup = embedded ? window.open('', 'atom-clicker-login', 'popup,width=520,height=700') : null; - - try { - const { data, error } = await this.supabase.auth.signInWithOAuth({ - provider, - options: { - redirectTo: `${window.location.origin}/callback`, - queryParams: { - access_type: 'offline', - prompt: 'consent', - }, - skipBrowserRedirect: embedded, - }, - }); - - if (error) { - popup?.close(); - this.error = error; - throw error; - } - if (!embedded || !data.url) return; - if (!popup) throw new Error('Your browser blocked the login window, allow popups for this page and try again.'); - popup.location.href = data.url; - this.listenForPopupCallback(popup); - } catch (err) { - console.error('Sign in error:', err); - throw err; - } - } - - private oneTapPrompted = false; - - /** Signs in from Google's One Tap prompt without leaving the page, Google itself backs off for days once a player dismisses it. */ - async promptGoogleOneTap() { - // FedCM only runs in a frame whose host grants `identity-credentials-get`, which itch.io and galaxy.click don't. - if (!PUBLIC_GOOGLE_CLIENT_ID || !this.supabase || this.isAuthenticated || this.oneTapPrompted || window.self !== window.top) return; - this.oneTapPrompted = true; - - try { - await new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = 'https://accounts.google.com/gsi/client'; - script.onload = resolve; - script.onerror = reject; - document.head.append(script); - }); - - /** Google signs the SHA-256 of the nonce into the ID token, Supabase hashes the raw one to compare them. */ - const nonce = crypto.randomUUID(); - const hashedNonce = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(nonce))).toHex(); - - window.google!.accounts.id.initialize({ - callback: async ({ credential }) => { - const { error } = await this.supabase!.auth.signInWithIdToken({ nonce, provider: 'google', token: credential }); - if (error) { - console.error('Google One Tap sign in error:', error); - this.error = error; - } - }, - client_id: PUBLIC_GOOGLE_CLIENT_ID, - context: 'signin', - itp_support: true, - nonce: hashedNonce, - use_fedcm_for_prompt: true, - }); - window.google!.accounts.id.prompt(); - } catch (err) { - // Content blockers commonly block the Google script, the regular sign in dialog still works without it. - console.warn('Google One Tap unavailable:', err); - } - } - - private popupListener: ((event: MessageEvent) => void) | null = null; - - private listenForPopupCallback(popup: Window) { - if (this.popupListener) window.removeEventListener('message', this.popupListener); - this.popupListener = async (event: MessageEvent) => { - if (event.origin !== window.location.origin || event.data?.type !== AUTH_CALLBACK_MESSAGE) return; - window.removeEventListener('message', this.popupListener!); - this.popupListener = null; - popup.close(); - const url = new URL(String(event.data.url)); - const code = url.searchParams.get('code'); - const hash = new URLSearchParams(url.hash.slice(1)); - const accessToken = hash.get('access_token'); - const refreshToken = hash.get('refresh_token'); - try { - if (code) await (this.supabase!).auth.exchangeCodeForSession(code); else if (accessToken && refreshToken) await (this.supabase!).auth.setSession({ access_token: accessToken, refresh_token: refreshToken }); - } catch(err) { - console.error('Popup sign in error:', err); - this.error = err as Error; - } - }; - window.addEventListener('message', this.popupListener); - } - - async signOut() { - if (!browser || !this.supabase) return; - - try { - if (this.user) { - await this.supabase - .from('profiles') - .update({ - is_online: false, - updated_at: new Date().toISOString(), - }) - .eq('id', this.user.id); - } - - const { error } = await this.supabase.auth.signOut(); - if (error) { - this.error = error; - throw error; - } - } catch (err) { - console.error('Sign out error:', err); - throw err; - } + return {}; } - - async updateProfile(updates: Partial>) { - if (!browser || !this.supabase) return; - - try { - if (!this.user) throw new Error('No authenticated user'); - - const { error } = await this.supabase - .from('profiles') - .update({ - ...updates, - updated_at: new Date().toISOString(), - }) - .eq('id', this.user.id); - - if (error) throw error; - - if (this.profile) { - this.profile = { ...this.profile, ...updates }; - } - } catch (err) { - console.error('Error updating profile:', err); - throw err; - } + async signInWithProvider(_provider: string) {} + async promptGoogleOneTap() {} + async signOut() {} + async updateProfile(_updates: Record) {} + adoptCloudSave(_lastSaveDate: number | null, _cloudAhead: boolean) {} + async saveGameToCloud(_state: GameState, _onlyIfSynced = false): Promise { + return null; } - - /** `lastSaveDate` of the cloud copy this device last uploaded or loaded, per account. */ - private get syncedSaveDate(): number | null { - const stored = Number(getItem(`cloudSaveSyncedAt:${this.user?.id}`)); - return stored > 0 ? stored : null; - } - - private markSynced(lastSaveDate: number | null) { - setItem(`cloudSaveSyncedAt:${this.user?.id}`, String(lastSaveDate ?? 0)); - removeItem(`cloudSavePendingAt:${this.user?.id}`); - this.cloudConflict = false; + flushSaveToCloud(_state: GameState): Promise { + return Promise.resolve(null); } - - /** - * A device that never synced adopts the cloud copy as its own unless the cloud has more play time, so the first auto-save goes through. - * A cloud copy matching the last upload sent also counts as synced, a tab closing mid-upload never gets the answer that confirms it. - */ - adoptCloudSave(lastSaveDate: number | null, cloudAhead: boolean) { - const pending = Number(getItem(`cloudSavePendingAt:${this.user?.id}`)); - if ((this.syncedSaveDate === null && !cloudAhead) || (pending > 0 && lastSaveDate === pending)) this.markSynced(lastSaveDate); - } - - /** - * Returns what now sits in the cloud, a snapshot so it stops following the live game state. With `onlyIfSynced` the write only lands - * while the cloud still holds the copy this device last synced, otherwise it flags `cloudConflict` and returns null. - */ - async saveGameToCloud(currentState: GameState, onlyIfSynced = false): Promise { - if (!browser || !this.supabase) return null; - return this.uploadSave(currentState, onlyIfSynced, await this.getAccessToken()); - } - - /** `saveGameToCloud` sent synchronously with the cached token, for the tab hiding or closing, where an await may never resume. */ - flushSaveToCloud(currentState: GameState): Promise { - return this.uploadSave(currentState, true, this.currentSession?.access_token ?? null); - } - - /** A raw PostgREST call, supabase-js can't set `keepalive` (which lets the request outlive the tab) and awaits a session lock first. */ - private async uploadSave(currentState: GameState, onlyIfSynced: boolean, accessToken: string | null): Promise { - const userId = this.user?.id; - if (!userId || !accessToken) throw new Error('No authenticated user'); - - const saveData: CloudSaveInfo = $state.snapshot({ ...currentState, lastSaveDate: Date.now(), version: SAVE_VERSION }); - const query = new URLSearchParams({ id: `eq.${userId}`, select: 'id' }); - if (onlyIfSynced) { - const synced = this.syncedSaveDate; - query.set('save->>lastSaveDate', synced === null ? 'is.null' : `eq.${synced}`); - } - setItem(`cloudSavePendingAt:${userId}`, String(saveData.lastSaveDate)); - - const response = await fetch(`${PUBLIC_SUPABASE_URL}/rest/v1/profiles?${query}`, { - body: JSON.stringify({ save: saveData, updated_at: new Date().toISOString() }), - headers: { - 'apikey': PUBLIC_SUPABASE_PUBLISHABLE_KEY, - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'Prefer': 'return=representation', - }, - keepalive: true, - method: 'PATCH', - }); - if (!response.ok) throw new Error(`Cloud save failed: ${response.status} ${await response.text()}`); - if ((await response.json() as unknown[]).length === 0) { - this.cloudConflict = true; - return null; - } - this.markSynced(saveData.lastSaveDate); - return saveData; - } - - /** The repaired state, since `loadSaveData` skips missing keys and would keep this device's values for them. */ - private async fetchCloudSave(): Promise { - if (!browser || !this.supabase || !this.user) return null; - - const { data: profile, error } = await this.supabase.from('profiles').select('save').eq('id', this.user.id).single(); - if (error) throw error; - if (!profile?.save) return null; - - const state = validateAndRepairGameState(migrateSavedState(profile.save)).state; - return state && { ...state, lastSaveDate: (profile.save as { lastSaveDate?: number }).lastSaveDate ?? null }; - } - async loadGameFromCloud(): Promise { - const save = await this.fetchCloudSave(); - if (save) this.markSynced(save.lastSaveDate); - return save; + return null; } - getCloudSaveInfo(): Promise { - return this.fetchCloudSave().catch(err => { - console.error('Error getting cloud save info:', err); - return null; - }); + return Promise.resolve(null); } - - /** Only the play time and date of the cloud save, so the "cloud save available" check never downloads the whole blob. */ async getCloudSaveStamp(): Promise<{ inGameTime: number | null; lastSaveDate: number | null } | null> { - if (!browser || !this.supabase || !this.user) return null; - - const { data, error } = await this.supabase - .from('profiles') - .select('inGameTime:save->inGameTime, lastSaveDate:save->lastSaveDate') - .eq('id', this.user.id) - .single() - .overrideTypes<{ inGameTime: number | null; lastSaveDate: number | null }, { merge: false }>(); - if (error) throw error; - return data; + return null; } } diff -ruN base/src/lib/stores/ui.svelte.ts mod/src/lib/stores/ui.svelte.ts --- base/src/lib/stores/ui.svelte.ts 2026-11-02 20:48:05.496604792 -0300 +++ mod/src/lib/stores/ui.svelte.ts 2026-11-02 20:48:06.076659564 -0300 @@ -83,7 +83,7 @@ this.#settingsLoader = loader; } - openSettings(tab: string = 'profile') { + openSettings(tab: string = 'gameplay') { if (this.#settingsLoader) this.openModalLazy('settings', this.#settingsLoader, tab); } diff -ruN base/src/routes/+layout.svelte mod/src/routes/+layout.svelte --- base/src/routes/+layout.svelte 2026-11-02 20:48:05.690895240 -0300 +++ mod/src/routes/+layout.svelte 2026-11-02 20:48:06.075281673 -0300 @@ -4,9 +4,6 @@ import { beforeNavigate } from '$app/navigation'; import { page, updated } from '$app/state'; import PrestigeAnimation from '#components/prestige/PrestigeAnimation.svelte'; - import Analytics from '#components/system/Analytics.svelte'; - import DevTools from '#components/system/devtools/DevTools.svelte'; - import SEO from '#components/system/SEO.svelte'; import Hints from '#components/tutorial/Hints.svelte'; import TooltipPortal from '#components/ui/TooltipPortal.svelte'; import { prestigeStore } from '#stores/prestige.svelte.js'; @@ -21,8 +18,8 @@ let { children }: Props = $props(); - // The error page and the benchmark analysis tool stand alone, none of the in-game overlays belong there. - const isStandalone = $derived(!!page.error || page.url.pathname.startsWith('/benchmark')); + // The error page stands alone, none of the in-game overlays belong there. + const isStandalone = $derived(!!page.error); let mounted = $state(false); let updatePromptShown = false; @@ -52,16 +49,13 @@ }); - - {#if !mounted}

Atom Clicker

- A free incremental game. Click atoms, buy generators and upgrades, prestige through protons, electrons and photons, and climb the - leaderboard. + A free incremental game. Click atoms, buy generators and upgrades, prestige through protons, electrons and photons.

{@render children?.()} {#if !isStandalone} - {/if} diff -ruN base/src/routes/+page.svelte mod/src/routes/+page.svelte --- base/src/routes/+page.svelte 2026-11-02 20:48:05.668822268 -0300 +++ mod/src/routes/+page.svelte 2026-11-02 20:48:06.074996919 -0300 @@ -1,11 +1,8 @@