// Helper functions for the game // Helper function to increase saturation of RGB color function saturateColor(r, g, b, saturationBoost) { // Normalize RGB to 0-1 r /= 255; g /= 255; b /= 255; // Convert to HSL var max = Math.max(r, g, b); var min = Math.min(r, g, b); var h, s, l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { var d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; case g: h = ((b - r) / d + 2) / 6; break; case b: h = ((r - g) / d + 4) / 6; break; } } // Boost saturation s = Math.min(1, s * saturationBoost); // Convert back to RGB function hue2rgb(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = Math.round(hue2rgb(p, q, h + 1/3) * 255); g = Math.round(hue2rgb(p, q, h) * 255); b = Math.round(hue2rgb(p, q, h - 1/3) * 255); return { r: r, g: g, b: b }; } function calculateTheoreticalRate(state, config, windowMs) { var rate = 0; var ranges = config.ASTEROID_MASS_RANGES; var patterns = config.ASTEROID_SPAWN_PATTERNS; var avgLarge = (ranges.large[0] + ranges.large[1]) / 2; var avgMedium = (ranges.medium[0] + ranges.medium[1]) / 2; var avgSmall = (ranges.small[0] + ranges.small[1]) / 2; var mediumCount = (patterns.LARGE_EVERY / patterns.MEDIUM_EVERY) - 1; var smallCount = patterns.LARGE_EVERY - mediumCount - 1; var avgMassPerAsteroid = (avgLarge + mediumCount * avgMedium + smallCount * avgSmall) / patterns.LARGE_EVERY; rate += avgMassPerAsteroid / (state.currentAsteroidSpawnInterval / 1000); // Only include object types whose spawn interval fits within the measurement window. // When windowMs is not provided (display/offline use), limit is Infinity so all types included. var limit = windowMs || Infinity; if (state.cometUnlocked && state.currentCometSpawnInterval && state.currentCometSpawnInterval <= limit) { rate += ((ranges.comet[0] + ranges.comet[1]) / 2) / (state.currentCometSpawnInterval / 1000); } if (state.planetUnlocked && state.currentPlanetSpawnInterval && state.currentPlanetSpawnInterval <= limit) { rate += ((ranges.planet[0] + ranges.planet[1]) / 2) / (state.currentPlanetSpawnInterval / 1000); } if (state.giantUnlocked && state.currentGiantSpawnInterval && state.currentGiantSpawnInterval <= limit) { rate += ((ranges.giant[0] + ranges.giant[1]) / 2) / (state.currentGiantSpawnInterval / 1000); } if (state.mtypeUnlocked && state.currentMtypeSpawnInterval && state.currentMtypeSpawnInterval <= limit) { rate += ((ranges.mtype[0] + ranges.mtype[1]) / 2) / (state.currentMtypeSpawnInterval / 1000); } return rate; } function calculateConsumptionRates(state, CONFIG, consumedMassKg) { const now = Date.now(); if (!state.sM) state.sM = 0; if (!state.sT) state.sT = now; if (!state.mM) state.mM = 0; if (!state.mT) state.mT = now; if (!state.lM) state.lM = 0; if (!state.lT) state.lT = now; // Comparison window for trend coloring if (!state.cM) state.cM = 0; if (!state.cT) state.cT = now; state.sM += consumedMassKg; state.mM += consumedMassKg; state.lM += consumedMassKg; state.cM += consumedMassKg; // Per-second rate (display only — no longer drives trend) const elapsedShort = now - state.sT; if (elapsedShort >= 1000) { state.rateShort = state.sM / (elapsedShort / 1000); state.sM = 0; state.sT = now; } // Trend: actual vs theoretical over 60-second window const elapsedComparison = now - state.cT; if (elapsedComparison >= 1000) { const actualRate = state.cM / (elapsedComparison / 1000); const theoreticalRate = calculateTheoreticalRate(state, CONFIG, elapsedComparison); if (theoreticalRate > 0) { const ratio = actualRate / theoreticalRate; if (ratio > 1.05) { state.rateShortTrend = 'up'; } else if (ratio < 0.95) { state.rateShortTrend = 'down'; } else { state.rateShortTrend = 'same'; } } state.cM = 0; state.cT = now; } // Hourly average const elapsedMedium = now - state.mT; if (elapsedMedium > 0) { state.rateMedium = state.mM / (elapsedMedium / 1000); } if (elapsedMedium >= CONFIG.RATE_WINDOWS.MEDIUM) { state.mM = 0; state.mT = now; } // Daily average const elapsedLong = now - state.lT; if (elapsedLong > 0) { state.rateLong = state.lM / (elapsedLong / 1000); } if (elapsedLong >= CONFIG.RATE_WINDOWS.LONG) { state.lM = 0; state.lT = now; } } function formatOfflineTime(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 + ' day' + (days > 1 ? 's' : '') + ', ' + (hours % 24) + ' hour' + ((hours % 24) !== 1 ? 's' : ''); } else if (hours > 0) { return hours + ' hour' + (hours > 1 ? 's' : '') + ', ' + (minutes % 60) + ' min'; } else if (minutes > 0) { return minutes + ' minute' + (minutes > 1 ? 's' : ''); } else { return seconds + ' second' + (seconds > 1 ? 's' : ''); } } // Unified notification system function showNotification(options) { // Default options var defaults = { message: '', type: 'info', // 'unlock', 'offline', 'error', 'success', 'info' html: false // if true, message is HTML; if false, it's text }; // Merge options with defaults var config = {}; for (var key in defaults) { config[key] = options[key] !== undefined ? options[key] : defaults[key]; } // Create notification element var notification = document.createElement('div'); notification.className = 'notification ' + config.type; // Set content if (config.html) { notification.innerHTML = config.message; } else { notification.textContent = config.message; } // Add to DOM document.body.appendChild(notification); // Dismiss on canvas click var canvas = document.getElementById('space'); var dismissHandler = function(e) { // Only dismiss if clicking canvas, not the notification if (e.target === canvas) { notification.remove(); canvas.removeEventListener('click', dismissHandler); } }; // Small delay to prevent immediate dismissal if notification was triggered by a click setTimeout(function() { canvas.addEventListener('click', dismissHandler); }, 100); return notification; } // Helper functions for specific notification types function showUnlockNotification(type) { var messages = { comet: 'Comets Unlocked', planet: 'Planets Unlocked', giant: 'Gas & Ice Giants Unlocked', mtype: 'M-Type Stars Unlocked', ktype: 'K-Type Stars Unlocked', gtype: 'G-Type Stars Unlocked' }; showNotification({ message: messages[type] || 'New content unlocked!', type: 'unlock', duration: 4000 }); } function showOfflineNotification(timeAway, massGained) { var message = '
Inactive for: ' + timeAway + '
' + '
Mass gained: ' + massGained + '
'; showNotification({ message: message, type: 'offline', duration: 5000, dismissable: true, html: true }); } // Additional helper for errors (useful for debugging/user feedback) function showErrorNotification(message) { showNotification({ message: message, type: 'error', duration: 5000, dismissable: true }); } // Additional helper for success messages function showSuccessNotification(message) { showNotification({ message: message, type: 'success', duration: 3000 }); } function formatTimeSince(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 (seconds < 60) return 'Just now'; if (minutes < 60) return minutes + 'm ago'; if (hours < 24) return hours + 'h ago'; return days + 'd ago'; }