548 lines
22 KiB
JavaScript
548 lines
22 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'),
|
||
mtypeLevel: document.getElementById('mtype-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'),
|
||
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();
|
||
});
|
||
}
|
||
|
||
// ---------- 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);
|
||
},
|
||
|
||
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 = `
|
||
<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>
|
||
`;
|
||
},
|
||
|
||
updateRateDisplay: function(state, config) {
|
||
const now = Date.now();
|
||
|
||
// Format per-second rate (always real)
|
||
const rateShortText = this.formatMass(state.rateShort || 0) + '/s';
|
||
|
||
// Check if we have enough data for hourly average
|
||
const hourlyElapsed = now - (state.mT || now);
|
||
let rateMediumText;
|
||
if (hourlyElapsed < 300000) { // Less than 5 minutes of data
|
||
// Use estimated value from per-second rate
|
||
rateMediumText = '~' + this.formatMass((state.rateShort || 0) * 3600) + '/h';
|
||
} else {
|
||
// Use real average
|
||
rateMediumText = this.formatMass((state.rateMedium || 0) * 3600) + '/h';
|
||
}
|
||
|
||
// Check if we have enough data for daily average
|
||
const dailyElapsed = now - (state.lT || now);
|
||
let rateLongText;
|
||
if (dailyElapsed < 3600000) { // Less than 1 hour of data
|
||
// Use estimated value from best available rate
|
||
const baseRate = (hourlyElapsed >= 300000) ? state.rateMedium : state.rateShort;
|
||
rateLongText = '~' + this.formatMass((baseRate || 0) * 86400) + '/d';
|
||
} else {
|
||
// Use real average
|
||
rateLongText = this.formatMass((state.rateLong || 0) * 86400) + '/d';
|
||
}
|
||
|
||
// Only color the per-second rate based on trend
|
||
const rateText = `
|
||
<span class="${'rate-' + (state.rateShortTrend || 'same')}">${rateShortText}</span>
|
||
</br><span> ${rateMediumText}</span>
|
||
</br><span> ${rateLongText}</span>
|
||
`;
|
||
|
||
this.elements.massRate.innerHTML = `
|
||
<span> Rates: ${rateText}</span>
|
||
`;
|
||
},
|
||
|
||
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';
|
||
}
|
||
|
||
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);
|
||
|
||
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;
|
||
|
||
el.innerHTML =
|
||
'<span class="tooltip-trigger">' +
|
||
'Total Level: ' + totalLevel +
|
||
'<span class="tooltip">' +
|
||
'Sum of all upgrades.<br>'
|
||
'</span>' +
|
||
'</span>';
|
||
},
|
||
|
||
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: <span class="rate-' + this._objectTrend + '">' + count + '</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;
|
||
},
|
||
|
||
updateMtypeUpgrade: function(gameState) {
|
||
var rate = (86400000 / gameState.currentMtypeSpawnInterval).toFixed(2);
|
||
var bonusPercent = (gameState.mtypeUpgradeLevel * 0.5);
|
||
var tooltipText = 'Spawn Rate: ' + rate + '/day <br>Bonus: ' + bonusPercent + '%';
|
||
this.elements.mtypeLevel.innerHTML = '<span class="tooltip-trigger">M-Type: Level ' +
|
||
gameState.mtypeUpgradeLevel +
|
||
'<span class="tooltip">' + tooltipText + '</span></span>';
|
||
this.elements.mtypeUpgradeBtn.textContent =
|
||
'Upgrade (Cost: ' + this.formatMass(gameState.mtypeUpgradeCost) + ')';
|
||
this.elements.mtypeUpgradeBtn.disabled =
|
||
gameState.totalMassConsumed < gameState.mtypeUpgradeCost;
|
||
},
|
||
|
||
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 = '<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;
|
||
}
|
||
|
||
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 ? '<span class="online-indicator"></span>' : '';
|
||
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 += `<div class="leaderboard-entry${isLocal ? ' local-player' : ''}">
|
||
<span class="rank">${medal}</span>
|
||
<span class="name">${this.escapeHtml(displayName)}${onlineIndicator}${nameSuffix}</span>
|
||
<span class="value last-seen">${lastSeenText}</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;
|
||
};
|
||
|
||
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, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
};
|