339 lines
14 KiB
JavaScript
339 lines
14 KiB
JavaScript
// 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 = `
|
||
<span class="${'rate-' + (state.prevRateShortTrend || 'same')}">${rateShortText}</span>,
|
||
<span class="${'rate-' + (state.prevRateMediumTrend || 'same')}">${rateMediumText}</span>,
|
||
<span class="${'rate-' + (state.prevRateLongTrend || 'same')}">${rateLongText}</span>
|
||
`;
|
||
|
||
const bhMassText = this.formatMass(state.blackHoleTotalMass, { forceSolarMass: true });
|
||
const consumedText = this.formatMass(state.totalMassConsumedEver);
|
||
|
||
this.elements.totalMass.innerHTML = `
|
||
<span class="tooltip-trigger">
|
||
Black Hole Mass: ${bhMassText}
|
||
<span class="tooltip">
|
||
The mass of your black hole in solar masses.<br>
|
||
1 Solar Mass (M☉) ≈ 1.989 × 10³⁰ kg.<br><br>
|
||
Total mass absorbed:<br>
|
||
${consumedText}
|
||
</span>
|
||
</span><br>
|
||
<span style="font-size:10px; opacity:0.7;">Rate: ${rateText}</span>
|
||
`;
|
||
},
|
||
|
||
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 =
|
||
'<span class="tooltip-trigger">' +
|
||
'Total Level: ' + totalLevel +
|
||
'<span class="tooltip">' +
|
||
'Sum of all upgrades.<br>'
|
||
'</span>' +
|
||
'</span>';
|
||
},
|
||
|
||
|
||
updateAsteroidUpgrade: function(gameState) {
|
||
var rate = (1 / gameState.currentAsteroidSpawnInterval * 1000).toFixed(2);
|
||
var bonusPercent = (gameState.asteroidUpgradeLevel * 10);
|
||
var tooltipText = 'Rate: ' + rate + '/sec <br>Bonus: ' + bonusPercent + '%';
|
||
|
||
this.elements.asteroidLevel.innerHTML = '<span class="tooltip-trigger">Asteroids: Level ' +
|
||
gameState.asteroidUpgradeLevel +
|
||
'<span class="tooltip">' + tooltipText + '</span></span>';
|
||
|
||
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 <br>Bonus: ' + bonusPercent + '%';
|
||
|
||
this.elements.cometLevel.innerHTML = '<span class="tooltip-trigger">Comets: Level ' +
|
||
gameState.cometUpgradeLevel +
|
||
'<span class="tooltip">' + tooltipText + '</span></span>';
|
||
|
||
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 <br>Bonus: ' + bonusPercent + '%';
|
||
|
||
this.elements.planetLevel.innerHTML = '<span class="tooltip-trigger">Planets: Level ' +
|
||
gameState.planetUpgradeLevel +
|
||
'<span class="tooltip">' + tooltipText + '</span></span>';
|
||
|
||
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 <br>Bonus: ' + bonusPercent + '%';
|
||
|
||
this.elements.giantLevel.innerHTML = '<span class="tooltip-trigger">Giants: Level ' +
|
||
gameState.giantUpgradeLevel +
|
||
'<span class="tooltip">' + tooltipText + '</span></span>';
|
||
|
||
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 = '<div style="text-align:center; padding:20px;">Loading...</div>';
|
||
|
||
// Fetch leaderboard
|
||
let data = await Server.getLeaderboard(sortBy);
|
||
if (!data.length) {
|
||
container.innerHTML = '<div style="text-align:center; padding:20px;">No entries yet</div>';
|
||
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 ? '<span class="online-indicator"></span>' : '';
|
||
const nameSuffix = isLocal ? '💗' : '';
|
||
|
||
// Use last 5 chars of playerId if available, otherwise fallback
|
||
const displayName = entry.id
|
||
? entry.id.slice(-5)
|
||
: 'Anon';
|
||
|
||
html += `<div class="leaderboard-entry${isLocal ? ' local-player' : ''}">
|
||
<span class="rank">${medal}</span>
|
||
<span class="name">${this.escapeHtml(displayName)}${onlineIndicator}${nameSuffix}</span>
|
||
<span class="value mass${sortBy==='mass'?' sorted':''}">${this.formatMass(entry.mass,{forceSolarMass:true})}</span>
|
||
<span class="value age${sortBy==='age'?' sorted':''}">${this.formatAge(entry.holeAge)}</span>
|
||
</div>`;
|
||
});
|
||
container.innerHTML = html;
|
||
},
|
||
|
||
escapeHtml: function(text) {
|
||
if (!text) return '';
|
||
return text
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
};
|