// 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 = '