hoel/js/ui.js
2026-01-22 19:21:59 +01:00

339 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
};