hoel/js/helpers.js
xbl 8d5b673044 modified: admin.html
modified:   js/config.js
	modified:   js/entities.js
	modified:   js/game.js
	modified:   js/helpers.js
	modified:   js/ui.js
2026-02-17 19:05:50 +01:00

279 lines
8.7 KiB
JavaScript

// 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'
};
showNotification({
message: messages[type] || 'New content unlocked!',
type: 'unlock',
duration: 4000
});
}
function showOfflineNotification(timeAway, massGained) {
var message =
'<div>Inactive for: <strong>' + timeAway + '</strong></div>' +
'<div>Mass gained: <strong>' + massGained + '</strong></div>';
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';
}