// 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'), asteroidUpgradeBtn: document.getElementById('asteroid-upgrade-btn'), cometUpgradeBtn: document.getElementById('comet-upgrade-btn'), planetUpgradeBtn: document.getElementById('planet-upgrade-btn'), giantUpgradeBtn: document.getElementById('giant-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') }; // 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; // ---------- Settings Menu ---------- if (gear && settingsMenu) { // Toggle settings menu gear.addEventListener('click', (e) => { e.stopPropagation(); // prevent document click from immediately closing settingsMenu.classList.toggle('open'); }); // Prevent clicks inside menu from closing it settingsMenu.addEventListener('click', (e) => { e.stopPropagation(); }); } // ---------- Leaderboard ---------- if (leaderboardToggle && leaderboardPanel) { // Toggle leaderboard panel leaderboardToggle.addEventListener('click', (e) => { e.stopPropagation(); // prevent document click 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(); }); } // ---------- 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'); } }); }, 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); }, update: function(gameState, config) { if (this.elements.spendableMass) { this.elements.spendableMass.textContent = 'Available to Spend: ' + this.formatMass(gameState.totalMassConsumed); } this.updateMassDisplay(gameState, config); this.updateUpgradeDisplay(gameState); this.updateTotalLevel(gameState); }, updateMassDisplay: function(state, config) { // Format rates const rateShortText = this.formatMass(state.rateShort) + '/s'; const rateMediumText = this.formatMass(state.rateMedium * 3600) + '/h'; const rateLongText = this.formatMass(state.rateLong * 86400) + '/d'; // Use span with class based on trend const rateText = ` ${rateShortText}, ${rateMediumText}, ${rateLongText} `; 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}

Rate: ${rateText} `; }, updateUpgradeDisplay: function(gameState) { this.updateAsteroidUpgrade(gameState); this.updateCometUpgrade(gameState); this.updatePlanetUpgrade(gameState); this.updateGiantUpgrade(gameState); }, updateTotalLevel: function(gameState) { var el = this.elements.totalLevel; if (!el) return; var totalLevel = gameState.asteroidUpgradeLevel + gameState.cometUpgradeLevel + gameState.planetUpgradeLevel + gameState.giantUpgradeLevel; el.innerHTML = '' + 'Total Level: ' + totalLevel + '' + 'Sum of all upgrades.
' '
' + '
'; }, updateAsteroidUpgrade: function(gameState) { var rate = (1 / gameState.currentAsteroidSpawnInterval * 1000).toFixed(2); var bonusPercent = (gameState.asteroidUpgradeLevel * 10); var tooltipText = 'Rate: ' + rate + '/sec
Bonus: ' + bonusPercent + '%'; 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 = (1 / gameState.currentCometSpawnInterval * 60000).toFixed(2); var bonusPercent = (gameState.cometUpgradeLevel * 10); var tooltipText = 'Rate: ' + rate + '/min
Bonus: ' + bonusPercent + '%'; 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 = (3600000 / gameState.currentPlanetSpawnInterval).toFixed(2); var bonusPercent = (gameState.planetUpgradeLevel * 10); var tooltipText = 'Rate: ' + rate + '/hour
Bonus: ' + bonusPercent + '%'; 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 = (21600000 / gameState.currentGiantSpawnInterval).toFixed(2); var bonusPercent = (gameState.giantUpgradeLevel * 10); var tooltipText = 'Spawn Rate: ' + rate + '/6hours
Bonus: ' + bonusPercent + '%'; 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; }, formatMass: function(massKg, options = {}) { // If forcing solar mass display (for black hole) if (options.forceSolarMass) { var solarMasses = massKg / CONFIG.SOLAR_MASS_KG; return solarMasses.toFixed(3) + ' M☉'; } // Traditional engineering/astronomical units 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'; // Kilograms }, 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 old'; } else if (hours > 0) { return hours + 'h old'; } else { return 'New'; } }, openLeaderboard: async function(sortBy = 'mass') { const container = this.elements.leaderboardList; container.innerHTML = '
Loading...
'; // Fetch leaderboard let data = await Server.getLeaderboard(sortBy); if (!data.length) { container.innerHTML = '
No entries yet
'; return; } // Sort data server-side should already be sorted; else fallback: data.sort((a,b) => sortBy==='mass'?b.mass-a.mass:b.holeAge-a.holeAge); const localPlayerId = Server.playerId; // your local player ID // Build entries let html = ''; data.forEach((entry, i) => { const rank = i + 1; const medal = rank===1?'🥇':rank===2?'🥈':rank===3?'🥉':rank+'.'; const isLocal = entry.id === localPlayerId; // Online indicator (green dot if active in last 15 min) const onlineIndicator = entry.isOnline ? '' : ''; const nameSuffix = isLocal ? '💗' : ''; // Use last 5 chars of playerId if available, otherwise fallback const displayName = entry.id ? entry.id.slice(-5) : 'Anon'; html += `
${medal} ${this.escapeHtml(displayName)}${onlineIndicator}${nameSuffix} ${this.formatMass(entry.mass,{forceSolarMass:true})} ${this.formatAge(entry.holeAge)}
`; }); container.innerHTML = html; }, escapeHtml: function(text) { if (!text) return ''; return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } };