// UI management and event handlers var UI = { elements: {}, init: function() { // Cache DOM elements this.elements = { totalMass: document.getElementById('total-mass'), spendableMass: document.getElementById('spendable-mass'), totalLevel: document.getElementById('total-level'), asteroidLevel: document.getElementById('asteroid-level'), cometLevel: document.getElementById('comet-level'), planetLevel: document.getElementById('planet-level'), giantLevel: document.getElementById('giant-level'), mtypeLevel: document.getElementById('mtype-level'), ktypeLevel: document.getElementById('ktype-level'), gtypeLevel: document.getElementById('gtype-level'), asteroidUpgradeBtn: document.getElementById('asteroid-upgrade-btn'), cometUpgradeBtn: document.getElementById('comet-upgrade-btn'), planetUpgradeBtn: document.getElementById('planet-upgrade-btn'), giantUpgradeBtn: document.getElementById('giant-upgrade-btn'), mtypeUpgradeBtn: document.getElementById('mtype-upgrade-btn'), ktypeUpgradeBtn: document.getElementById('ktype-upgrade-btn'), gtypeUpgradeBtn: document.getElementById('gtype-upgrade-btn'), gearIcon: document.getElementById('gear-icon'), settingsMenu: document.getElementById('settings-menu'), resetBtn: document.getElementById('reset-btn'), leaderboardModal: document.getElementById('leaderboardModal'), leaderboardToggle: document.getElementById('leaderboardToggle'), leaderboardPanel: document.getElementById('leaderboardPanel'), leaderboardList: document.getElementById('leaderboardList'), sortMass: document.getElementById('sortMass'), sortAge: document.getElementById('sortAge'), massRate: document.getElementById('massRate') }; // Sorting buttons this.elements.sortMass.addEventListener('click', () => this.openLeaderboard('mass')); this.elements.sortAge.addEventListener('click', () => this.openLeaderboard('age')); this.setupEventListeners(); }, setupEventListeners: function() { const self = this; const gear = self.elements.gearIcon; const settingsMenu = self.elements.settingsMenu; const leaderboardToggle = self.elements.leaderboardToggle; const leaderboardPanel = self.elements.leaderboardPanel; const resetBtn = self.elements.resetBtn; const holeIcon = document.getElementById('hole-icon'); const upgradePanel = document.getElementById('upgrade-panel'); // ---------- Upgrade Panel ---------- if (holeIcon && upgradePanel) { // Toggle upgrade panel holeIcon.addEventListener('click', (e) => { e.stopPropagation(); if (settingsMenu) settingsMenu.classList.remove('open'); if (leaderboardPanel) leaderboardPanel.classList.remove('show'); upgradePanel.classList.toggle('open'); }); // Prevent clicks inside panel from closing it upgradePanel.addEventListener('click', (e) => { e.stopPropagation(); }); } // ---------- Leaderboard ---------- if (leaderboardToggle && leaderboardPanel) { // Toggle leaderboard panel leaderboardToggle.addEventListener('click', (e) => { e.stopPropagation(); // prevent document click if (settingsMenu) settingsMenu.classList.remove('open'); if (upgradePanel) upgradePanel.classList.remove('open'); leaderboardPanel.classList.toggle('show'); if (leaderboardPanel.classList.contains('show')) { self.openLeaderboard('mass'); // load leaderboard by mass immediately } }); // Prevent clicks inside the panel from closing it leaderboardPanel.addEventListener('click', (e) => { e.stopPropagation(); }); } if (resetBtn) { resetBtn.addEventListener('click', function() { Storage.resetGame(); }); } // ---------- Settings Menu ---------- if (gear && settingsMenu) { // Toggle settings menu gear.addEventListener('click', (e) => { e.stopPropagation(); // prevent document click from immediately closing if (upgradePanel) upgradePanel.classList.remove('open'); if (leaderboardPanel) leaderboardPanel.classList.remove('show'); settingsMenu.classList.toggle('open'); }); // Prevent clicks inside menu from closing it settingsMenu.addEventListener('click', (e) => { e.stopPropagation(); }); } // ---------- Session Transfer ---------- var exportBtn = document.getElementById('export-session-btn'); var claimBtn = document.getElementById('transfer-claim-btn'); if (exportBtn) { exportBtn.addEventListener('click', function(e) { e.stopPropagation(); self.exportSession(); }); } if (claimBtn) { claimBtn.addEventListener('click', async function(e) { e.stopPropagation(); var input = document.getElementById('transfer-code-input'); var isVisible = input.style.display !== 'none'; if (!isVisible) { input.style.display = 'block'; claimBtn.classList.add('active'); input.focus(); return; } var code = input.value.trim(); if (!code) { input.style.display = 'none'; claimBtn.classList.remove('active'); return; } if (!/^[0-9a-fA-F]{5}$/.test(code)) { showErrorNotification('Invalid or expired code'); return; } try { var result = await Server.claimTransferCode(code.toLowerCase()); if (!result || !result.playerId) throw new Error(); Storage.setCookie('playerId', result.playerId, 365); Storage.setCookie('sessionToken', result.sessionToken, 365); showSuccessNotification('Session imported — reloading...'); setTimeout(function() { location.reload(); }, 1500); } catch (err) { showErrorNotification('Invalid or expired code'); } }); } var transferInput = document.getElementById('transfer-code-input'); if (transferInput) { transferInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.stopPropagation(); claimBtn.click(); } }); } // ---------- Click outside to close ---------- document.addEventListener('click', () => { // Close settings menu if (settingsMenu) { settingsMenu.classList.remove('open'); } // Close leaderboard panel if (leaderboardPanel) { leaderboardPanel.classList.remove('show'); // Clear refresh intervals when closing if (this._leaderboardRefreshInterval) { clearInterval(this._leaderboardRefreshInterval); this._leaderboardRefreshInterval = null; } if (this._leaderboardIndicatorInterval) { clearInterval(this._leaderboardIndicatorInterval); this._leaderboardIndicatorInterval = null; } } if (upgradePanel) { upgradePanel.classList.remove('open'); } }); }, setUpgradeHandlers: function(handlers) { this.elements.asteroidUpgradeBtn.addEventListener('click', handlers.asteroid); this.elements.cometUpgradeBtn.addEventListener('click', handlers.comet); this.elements.planetUpgradeBtn.addEventListener('click', handlers.planet); this.elements.giantUpgradeBtn.addEventListener('click', handlers.giant); this.elements.mtypeUpgradeBtn.addEventListener('click', handlers.mtype); this.elements.ktypeUpgradeBtn.addEventListener('click', handlers.ktype); this.elements.gtypeUpgradeBtn.addEventListener('click', handlers.gtype); }, update: function(gameState, config) { if (this.elements.spendableMass) { this.elements.spendableMass.textContent = 'Available to Spend: ' + this.formatMass(gameState.totalMassConsumed); } this.updateMassDisplay(gameState, config); this.updateRateDisplay(gameState, config); this.updateUpgradeDisplay(gameState); this.updateTotalLevel(gameState); this.updateActiveObjects(); }, updateMassDisplay: function(state, config) { const bhMassText = this.formatMass(state.blackHoleTotalMass, { forceSolarMass: true }); const consumedText = this.formatMass(state.totalMassConsumedEver); this.elements.totalMass.innerHTML = ` Black Hole Mass: ${bhMassText} The mass of your black hole in solar masses.
1 Solar Mass (M☉) ≈ 1.989 × 10³⁰ kg.

Total mass absorbed:
${consumedText}
`; }, updateRateDisplay: function(state, config) { const theoretical = calculateTheoreticalRate(state, config); const windowedTheoretical = calculateTheoreticalRate(state, config, 1000); const trend = state.rateShortTrend || 'same'; const rateShortText = this.formatMass(theoretical) + '/s'; const rateMediumText = this.formatMass(theoretical * 3600) + '/h'; const rateLongText = this.formatMass(theoretical * 86400) + '/d'; const delta = (state.rateShort || 0) - windowedTheoretical; const sign = delta >= 0 ? '+' : '-'; const deltaText = sign + this.formatMass(Math.abs(delta)) + '/s'; const deltaClass = trend === 'same' ? 'rate-same' : 'rate-' + trend; const rateText = ` ${rateShortText}  ${deltaText}
         ${rateMediumText}
         ${rateLongText} `; this.elements.massRate.innerHTML = `   Rates: ${rateText} `; }, updateUpgradeDisplay: function(gameState) { this.updateAsteroidUpgrade(gameState); // Only show comet upgrade if unlocked if (gameState.cometUnlocked) { this.updateCometUpgrade(gameState); this.elements.cometLevel.parentElement.style.display = ''; } else { this.elements.cometLevel.parentElement.style.display = 'none'; } // Only show planet upgrade if unlocked if (gameState.planetUnlocked) { this.updatePlanetUpgrade(gameState); this.elements.planetLevel.parentElement.style.display = ''; } else { this.elements.planetLevel.parentElement.style.display = 'none'; } // Only show giant upgrade if unlocked if (gameState.giantUnlocked) { this.updateGiantUpgrade(gameState); this.elements.giantLevel.parentElement.style.display = ''; } else { this.elements.giantLevel.parentElement.style.display = 'none'; } // Only show mtype upgrade if unlocked if (gameState.mtypeUnlocked) { this.updateMtypeUpgrade(gameState); this.elements.mtypeLevel.parentElement.style.display = ''; } else { this.elements.mtypeLevel.parentElement.style.display = 'none'; } // Only show ktype upgrade if unlocked if (gameState.ktypeUnlocked) { this.updateKtypeUpgrade(gameState); this.elements.ktypeLevel.parentElement.style.display = ''; } else { this.elements.ktypeLevel.parentElement.style.display = 'none'; } // Only show gtype upgrade if unlocked if (gameState.gtypeUnlocked) { this.updateGtypeUpgrade(gameState); this.elements.gtypeLevel.parentElement.style.display = ''; } else { this.elements.gtypeLevel.parentElement.style.display = 'none'; } var canAffordAny = gameState.totalMassConsumed >= gameState.asteroidUpgradeCost || (gameState.cometUnlocked && gameState.totalMassConsumed >= gameState.cometUpgradeCost) || (gameState.planetUnlocked && gameState.totalMassConsumed >= gameState.planetUpgradeCost) || (gameState.giantUnlocked && gameState.totalMassConsumed >= gameState.giantUpgradeCost) || (gameState.mtypeUnlocked && gameState.totalMassConsumed >= gameState.mtypeUpgradeCost) || (gameState.ktypeUnlocked && gameState.totalMassConsumed >= gameState.ktypeUpgradeCost) || (gameState.gtypeUnlocked && gameState.totalMassConsumed >= gameState.gtypeUpgradeCost); var holeIcon = document.getElementById('hole-icon'); if (holeIcon) { if (canAffordAny) { holeIcon.classList.add('pulse'); } else { holeIcon.classList.remove('pulse'); } } }, updateTotalLevel: function(gameState) { var el = this.elements.totalLevel; if (!el) return; var totalLevel = gameState.asteroidUpgradeLevel + gameState.cometUpgradeLevel + gameState.planetUpgradeLevel + gameState.giantUpgradeLevel + gameState.mtypeUpgradeLevel + gameState.ktypeUpgradeLevel + gameState.gtypeUpgradeLevel; el.innerHTML = '' + 'Total Level: ' + totalLevel + '' + 'Sum of all upgrades.
' '
' + '
'; }, updateActiveObjects: function() { var el = document.getElementById('active-objects'); if (!el) return; var count = window.gameAsteroids ? window.gameAsteroids.length : 0; var now = Date.now(); // Initialize tracking if (this._lastObjectCount === undefined) { this._lastObjectCount = count; this._objectTrend = 'same'; this._lastObjectCheck = now; } // Only update trend once per second to avoid flicker if (now - this._lastObjectCheck >= 1000) { if (count > this._lastObjectCount) { this._objectTrend = 'up'; } else if (count < this._lastObjectCount) { this._objectTrend = 'down'; } else { this._objectTrend = 'same'; } this._lastObjectCount = count; this._lastObjectCheck = now; } // Apply trend color el.innerHTML = 'Objects: ' + count + ''; }, updateAsteroidUpgrade: function(gameState) { var rate = (1000 / gameState.currentAsteroidSpawnInterval).toFixed(2); var bonusPercent = (gameState.asteroidUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_ASTER * 100).toFixed(0); var tooltipText = 'Asteroids are rocky, airless bodies that range in size from tiny pebbles to hundreds of kilometers across. Some asteroids have moons or even binary companions.

Rate: ' + rate + '/sec
Bonus: ' + bonusPercent + '%'; if (!gameState.cometUnlocked) tooltipText += '
Unlocks Comets at level 20'; this.elements.asteroidLevel.innerHTML = 'Asteroids: Level ' + gameState.asteroidUpgradeLevel + '' + tooltipText + ''; this.elements.asteroidUpgradeBtn.textContent = 'Upgrade (Cost: ' + this.formatMass(gameState.asteroidUpgradeCost) + ')'; this.elements.asteroidUpgradeBtn.disabled = gameState.totalMassConsumed < gameState.asteroidUpgradeCost; }, updateCometUpgrade: function(gameState) { var rate = (60000 / gameState.currentCometSpawnInterval).toFixed(2); var bonusPercent = (gameState.cometUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_COMET * 100).toFixed(0); var tooltipText = 'Comets are small icy bodies that typically release gas and dust, forming a glowing coma and often a tail. They are composed mainly of ice, rock, and organic compounds.

Rate: ' + rate + '/min
Bonus: ' + bonusPercent + '%'; if (!gameState.planetUnlocked) tooltipText += '
Unlocks Planets at level 15'; this.elements.cometLevel.innerHTML = 'Comets: Level ' + gameState.cometUpgradeLevel + '' + tooltipText + ''; this.elements.cometUpgradeBtn.textContent = 'Upgrade (Cost: ' + this.formatMass(gameState.cometUpgradeCost) + ')'; this.elements.cometUpgradeBtn.disabled = gameState.totalMassConsumed < gameState.cometUpgradeCost; }, updatePlanetUpgrade: function(gameState) { var rate = (CONFIG.BASE_PLANET_SPAWN_INTERVAL / gameState.currentPlanetSpawnInterval).toFixed(2); var bonusPercent = (gameState.planetUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_PLANT * 100).toFixed(0); var tooltipText = 'Planets are roughly spherical accumulations of rock and metal, with solid surfaces and relatively thin atmospheres. Local examples include Mercury, Earth, and Pluto. They may vary greatly in size.

Rate: ' + rate + '/hour
Bonus: ' + bonusPercent + '%'; if (!gameState.giantUnlocked) tooltipText += '
Unlocks Giants at level 10'; this.elements.planetLevel.innerHTML = 'Planets: Level ' + gameState.planetUpgradeLevel + '' + tooltipText + ''; this.elements.planetUpgradeBtn.textContent = 'Upgrade (Cost: ' + this.formatMass(gameState.planetUpgradeCost) + ')'; this.elements.planetUpgradeBtn.disabled = gameState.totalMassConsumed < gameState.planetUpgradeCost; }, updateGiantUpgrade: function(gameState) { var rate = (CONFIG.BASE_GIANT_SPAWN_INTERVAL / gameState.currentGiantSpawnInterval).toFixed(2); var bonusPercent = (gameState.giantUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_GIANT * 100).toFixed(0); var tooltipText = 'Gas giants are large planets made mostly of hydrogen and helium, with thick atmospheres and no solid surface; Jupiter and Saturn are examples.
Ice giants are similar but contain higher amounts of frozen materials like water, ammonia, and methane beneath their atmospheres; Uranus and Neptune fall into this category.

Rate: ' + rate + '/6hours
Bonus: ' + bonusPercent + '%'; if (!gameState.mtypeUnlocked) tooltipText += '
Unlocks M-Type at level 5'; this.elements.giantLevel.innerHTML = 'Giants: Level ' + gameState.giantUpgradeLevel + '' + tooltipText + ''; this.elements.giantUpgradeBtn.textContent = 'Upgrade (Cost: ' + this.formatMass(gameState.giantUpgradeCost) + ')'; this.elements.giantUpgradeBtn.disabled = gameState.totalMassConsumed < gameState.giantUpgradeCost; }, updateMtypeUpgrade: function(gameState) { var rate = (CONFIG.BASE_MTYPE_SPAWN_INTERVAL / gameState.currentMtypeSpawnInterval).toFixed(2); var bonusPercent = (gameState.mtypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_MTYPE * 100).toFixed(0); var tooltipText = 'M-types, also known as red dwarfs, are the smallest and coolest stars with masses ranging from about 0.08 to 0.45 M☉.
They are the most common star in the universe, making up roughly three quarters of all main-sequence stars, but are not easily visible due to their low luminosity.

Rate: ' + rate + '/day
Bonus: ' + bonusPercent + '%'; if (!gameState.ktypeUnlocked) tooltipText += '
Unlocks K-Type at level 5'; this.elements.mtypeLevel.innerHTML = 'M-Type: Level ' + gameState.mtypeUpgradeLevel + '' + tooltipText + ''; this.elements.mtypeUpgradeBtn.textContent = 'Upgrade (Cost: ' + this.formatMass(gameState.mtypeUpgradeCost) + ')'; this.elements.mtypeUpgradeBtn.disabled = gameState.totalMassConsumed < gameState.mtypeUpgradeCost; }, updateKtypeUpgrade: function(gameState) { var rate = (CONFIG.BASE_KTYPE_SPAWN_INTERVAL / gameState.currentKtypeSpawnInterval).toFixed(2); var bonusPercent = (gameState.ktypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_KTYPE * 100).toFixed(0); var tooltipText = 'K-types, also known as orange dwarfs, are medium-sized stars that are cooler than the Sun, with masses ranging from about 0.45 to 0.8 M☉. They are known for their stability and long lifespans (20 to 70 billion years), making them potential candidates for supporting inhabited planets.

Rate: ' + rate + '/2days
Bonus: ' + bonusPercent + '%'; if (!gameState.gtypeUnlocked) tooltipText += '
Unlocks G-Type at level 5'; this.elements.ktypeLevel.innerHTML = 'K-Type: Level ' + gameState.ktypeUpgradeLevel + '' + tooltipText + ''; this.elements.ktypeUpgradeBtn.textContent = 'Upgrade (Cost: ' + this.formatMass(gameState.ktypeUpgradeCost) + ')'; this.elements.ktypeUpgradeBtn.disabled = gameState.totalMassConsumed < gameState.ktypeUpgradeCost; }, updateGtypeUpgrade: function(gameState) { var rate = (CONFIG.BASE_GTYPE_SPAWN_INTERVAL / gameState.currentGtypeSpawnInterval).toFixed(2); var bonusPercent = (gameState.gtypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_GTYPE * 100).toFixed(0); var tooltipText = 'G-Types, also known as yellow dwarfs, are medium-sized stars that are about the size of our Sun (~1 M☉). The term yellow dwarf is a misnomer, as most G-Types (including the Sun) are in fact white. These stars are stable and long-lived, making them strong candidates for hosting habitable planets.

Rate: ' + rate + '/3days
Bonus: ' + bonusPercent + '%'; this.elements.gtypeLevel.innerHTML = 'G-Type: Level ' + gameState.gtypeUpgradeLevel + '' + tooltipText + ''; this.elements.gtypeUpgradeBtn.textContent = 'Upgrade (Cost: ' + this.formatMass(gameState.gtypeUpgradeCost) + ')'; this.elements.gtypeUpgradeBtn.disabled = gameState.totalMassConsumed < gameState.gtypeUpgradeCost; }, formatMass: function(massKg, options = {}) { if (massKg == null) return '0'; // fallback const SOLAR_SWITCH_KG = CONFIG.SOLAR_MASS_KG * 0.01; // ~1% solar mass // Forced solar mass display (e.g. black holes) if (options.forceSolarMass || massKg >= SOLAR_SWITCH_KG) { var solarMasses = massKg / CONFIG.SOLAR_MASS_KG; // Precision scales nicely with magnitude if (solarMasses >= 1000) return solarMasses.toFixed(0) + ' M☉'; if (solarMasses >= 1) return solarMasses.toFixed(3) + ' M☉'; return solarMasses.toFixed(5) + ' M☉'; } // SI mass units (tonnes-based) if (massKg >= 1e30) return (massKg / 1e30).toFixed(2) + ' Qt'; // Quettatonnes if (massKg >= 1e27) return (massKg / 1e27).toFixed(2) + ' Rt'; // Ronnatonnes if (massKg >= 1e24) return (massKg / 1e24).toFixed(2) + ' Yt'; // Yottatonnes if (massKg >= 1e21) return (massKg / 1e21).toFixed(2) + ' Zt'; // Zettatonnes if (massKg >= 1e18) return (massKg / 1e18).toFixed(2) + ' Et'; // Exatonnes if (massKg >= 1e15) return (massKg / 1e15).toFixed(2) + ' Pt'; // Petatonnes if (massKg >= 1e12) return (massKg / 1e12).toFixed(2) + ' Tt'; // Teratonnes if (massKg >= 1e9) return (massKg / 1e9).toFixed(2) + ' Gt'; // Gigatonnes if (massKg >= 1e6) return (massKg / 1e6).toFixed(2) + ' Mt'; // Megatonnes if (massKg >= 1e3) return (massKg / 1e3).toFixed(2) + ' tonnes'; // Tonnes return massKg.toFixed(0) + ' kg'; }, formatTime: function(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); var hours = Math.floor(minutes / 60); var days = Math.floor(hours / 24); if (days > 0) { return days + 'd ' + (hours % 24) + 'h'; } else if (hours > 0) { return hours + 'h ' + (minutes % 60) + 'm'; } else if (minutes > 0) { return minutes + 'm ' + (seconds % 60) + 's'; } else { return seconds + 's'; } }, formatAge: function(ms) { var days = Math.floor(ms / (24 * 60 * 60 * 1000)); var hours = Math.floor((ms % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); if (days > 0) { return days + 'd ' + hours + 'h'; } else if (hours > 0) { return hours + 'h'; } else { return 'New'; } }, openLeaderboard: async function(sortBy = 'mass') { const container = this.elements.leaderboardList; const localPlayerId = Server.playerId; // Show loading only on first load if (!this._cachedLeaderboardData) { container.innerHTML = '
Loading...
'; } // Fetch leaderboard let data = await Server.getLeaderboard(sortBy); if (!data.length) { container.innerHTML = '
No entries yet
'; return; } this._cachedLeaderboardData = data; this._currentSort = sortBy; // Render function (can be called independently) const render = () => { const now = Date.now(); const fifteenMinutes = 15 * 60 * 1000; let html = ''; data.forEach((entry, i) => { const rank = i + 1; const medal = rank===1?'🥇':rank===2?'🥈':rank===3?'🥉':rank+'.'; const isLocal = entry.id === localPlayerId; // Calculate online status in real-time const isOnline = (now - entry.lastSeen) < fifteenMinutes; const onlineIndicator = isOnline ? '' : ''; const nameSuffix = isLocal ? '💗' : ''; const displayName = entry.id ? entry.id.slice(-5) : 'Anon'; // Calculate time since last seen const timeSinceLastSeen = now - entry.lastSeen; const lastSeenText = formatTimeSince(timeSinceLastSeen); html += `
${medal} ${this.escapeHtml(displayName)}${onlineIndicator}${nameSuffix} ${lastSeenText} ${this.formatMass(entry.mass,{forceSolarMass:true})} ${this.formatAge(entry.holeAge)}
`; }); container.innerHTML = html; }; render(); // Auto-refresh data every 30 seconds if (this._leaderboardRefreshInterval) { clearInterval(this._leaderboardRefreshInterval); } this._leaderboardRefreshInterval = setInterval(async () => { if (this.elements.leaderboardPanel.classList.contains('show')) { // Re-fetch data data = await Server.getLeaderboard(sortBy); this._cachedLeaderboardData = data; } else { clearInterval(this._leaderboardRefreshInterval); this._leaderboardRefreshInterval = null; } }, 30000); // Re-render indicators every 5 seconds (lightweight, no API call) if (this._leaderboardIndicatorInterval) { clearInterval(this._leaderboardIndicatorInterval); } this._leaderboardIndicatorInterval = setInterval(() => { if (this.elements.leaderboardPanel.classList.contains('show')) { render(); // Just re-render with updated time calculations } else { clearInterval(this._leaderboardIndicatorInterval); this._leaderboardIndicatorInterval = null; } }, 5000); }, escapeHtml: function(text) { if (!text) return ''; return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, exportSession: async function() { var btn = document.getElementById('export-session-btn'); var display = document.getElementById('export-code-display'); var codeEl = document.getElementById('transfer-code-value'); var expiryEl = document.getElementById('transfer-code-expiry'); if (!btn || !display || !codeEl) return; btn.disabled = true; btn.textContent = 'Export'; display.style.display = 'none'; try { var result = await Server.createTransferCode(); if (!result || !result.code) { showErrorNotification(result && result.error ? result.error : 'Failed to generate transfer code'); return; } codeEl.textContent = result.code.toUpperCase(); display.style.display = 'block'; btn.classList.add('menu-btn-export-active'); var expiresAt = result.expiresAt; if (this._transferTimer) clearInterval(this._transferTimer); var self = this; var tick = function() { var remaining = Math.max(0, expiresAt - Date.now()); var mins = Math.floor(remaining / 60000); var secs = Math.floor((remaining % 60000) / 1000); if (expiryEl) expiryEl.textContent = 'Expires in ' + mins + ':' + (secs < 10 ? '0' : '') + secs; if (remaining === 0) { clearInterval(self._transferTimer); if (codeEl) codeEl.textContent = 'EXPIRED'; if (expiryEl) expiryEl.textContent = ''; btn.classList.remove('menu-btn-export-active'); } }; tick(); // run immediately this._transferTimer = setInterval(tick, 1000); } catch (e) { showErrorNotification('Failed to generate transfer code'); } btn.disabled = false; btn.textContent = 'Export'; } };