modified: admin.html

modified:   js/config.js
	modified:   js/entities.js
	modified:   js/game.js
	modified:   js/helpers.js
	modified:   js/ui.js
This commit is contained in:
xbl
2026-02-17 19:05:50 +01:00
parent a1fd271811
commit 8d5b673044
6 changed files with 297 additions and 87 deletions

View File

@ -29,6 +29,7 @@
<button onclick="adminSpawn('giant')">Giant</button>
<button onclick="adminSpawn('mtype')">M-Type</button>
<button onclick="adminSpawn('ktype')">K-Type</button>
<button onclick="adminSpawn('gtype')">G-Type</button>
</div>
</div>

View File

@ -22,6 +22,7 @@ var CONFIG = {
BASE_GIANT_SPAWN_INTERVAL: 21600000, // 6 hours
BASE_MTYPE_SPAWN_INTERVAL: 86400000, // 1 day
BASE_KTYPE_SPAWN_INTERVAL: 172800000, // 2 days
BASE_GTYPE_SPAWN_INTERVAL: 259200000, // 3 days
// Rate tracking windows (in milliseconds)
RATE_WINDOWS: {
@ -36,7 +37,8 @@ var CONFIG = {
PLANET_BASE_COST: 1e24,
GIANT_BASE_COST: 1e27,
MTYPE_BASE_COST: 1e29,
KTYPE_BASE_COST: 1e30,
KTYPE_BASE_COST: 1e31,
GTYPE_BASE_COST: 1e33,
// Upgrade scaling
UPGRADE_COST_MULTIPLIER_ASTER: 1.25,
@ -45,6 +47,7 @@ var CONFIG = {
UPGRADE_COST_MULTIPLIER_GIANT: 2,
UPGRADE_COST_MULTIPLIER_MTYPE: 2.1,
UPGRADE_COST_MULTIPLIER_KTYPE: 2.2,
UPGRADE_COST_MULTIPLIER_GTYPE: 2.3,
UPGRADE_BONUS_PER_LEVEL_ASTER: 0.1,
UPGRADE_BONUS_PER_LEVEL_COMET: 0.1,
@ -52,6 +55,7 @@ var CONFIG = {
UPGRADE_BONUS_PER_LEVEL_GIANT: 0.03,
UPGRADE_BONUS_PER_LEVEL_MTYPE: 0.03,
UPGRADE_BONUS_PER_LEVEL_KTYPE: 0.03,
UPGRADE_BONUS_PER_LEVEL_GTYPE: 0.03,
// Asteroid spawn patterns
ASTEROID_SPAWN_PATTERNS: {
@ -67,7 +71,8 @@ var CONFIG = {
planet: [1e22, 1e25], // Moons & Planets
giant: [1e26, 1e28], // Gas & Ice Giants
mtype: [1.589e29, 8.95e29],// M-Type: 0.08-0.45 M☉
ktype: [8.95e29, 1.59e30] // K-Type: 0.45-0.8 M☉
ktype: [8.95e29, 1.59e30], // K-Type: 0.45-0.8 M☉
gtype: [1.59e30, 2.07e30] // K-Type: 0.8-1.04 M☉
},
// Planet color schemes
@ -122,6 +127,15 @@ var CONFIG = {
{ light: '#ffe8b0', mid: '#ffc055', dark: '#e08820' },
{ light: '#ffd070', mid: '#e8952a', dark: '#c87018' }
],
// G-Type star color schemes (yellow to yellowish-white)
GTYPE_COLORS: [
{ light: '#fff9e6', mid: '#ffe066', dark: '#ffcc33' },
{ light: '#fffbe8', mid: '#ffeb99', dark: '#ffd24d' },
{ light: '#fffde0', mid: '#fff176', dark: '#ffd54f' },
{ light: '#fff8cc', mid: '#ffe57f', dark: '#ffc107' },
{ light: '#fffff0', mid: '#fff59d', dark: '#ffca28' }
],
// Star color distribution
STAR_COLORS: [

View File

@ -164,12 +164,12 @@ Asteroid.prototype.initializeTypeSpecificProperties = function() {
this.planetColors = CONFIG.GIANT_COLORS[Math.floor(Math.random() * CONFIG.GIANT_COLORS.length)];
// Initialize rings
this.rings = [];
var ringCount = Math.floor(Math.random() * 4) + 1; // 14 rings
var ringCount = Math.floor(Math.random() * 3) + 1; // 13 rings
for (var i = 0; i < ringCount; i++) {
this.rings.push({
color: CONFIG.GIANT_RING_COLORS[Math.floor(Math.random() * CONFIG.GIANT_RING_COLORS.length)],
thickness: 1 + Math.random() * 9, // random thickness
radiusOffset: 10 + i * 5 + Math.random() * 5, // spread rings outward
radiusOffset: 5 + i * 5 + Math.random() * 5, // spread rings outward
alpha: 0.3 + Math.random() * 0.4 // stored permanently
});
}
@ -614,6 +614,181 @@ Asteroid.prototype.drawGiant = function(ctx) {
ctx.restore();
};
Asteroid.prototype.drawGType = function(ctx, time) {
var massRange = CONFIG.ASTEROID_MASS_RANGES.gtype;
var massNormalized = (this.massKg - massRange[0]) / (massRange[1] - massRange[0]);
var pulse = 0.06 * Math.sin(time * 1.4 + this.massKg * 0.001) + 1;
var slowPulse = 0.04 * Math.sin(time * 0.4) + 1;
ctx.save();
ctx.translate(this.x, this.y);
// ── Layer 1: tight ambient halo ──────────────────────────────────────
var haloSize = this.size * (2.2 + massNormalized * 0.5) * slowPulse;
var halo = ctx.createRadialGradient(0, 0, this.size * 0.9, 0, 0, haloSize);
halo.addColorStop(0, 'rgba(255, 160, 40, 0.10)');
halo.addColorStop(0.5, 'rgba(255, 130, 20, 0.04)');
halo.addColorStop(1, 'rgba(240, 100, 10, 0)');
ctx.fillStyle = halo;
ctx.beginPath();
ctx.arc(0, 0, haloSize, 0, Math.PI * 2);
ctx.fill();
// ── Layer 2: spikes ──────────────────────────────────────────────────
for (var i = 0; i < this.spikes.length; i++) {
var sp = this.spikes[i];
var spikeAngle = sp.baseAngle + time * sp.rotSpeed;
var spikeLen = this.size * (sp.lenBase + sp.lenAmp * Math.sin(time * sp.lenSpeed + sp.lenPhase));
var alpha = sp.alphaBase + sp.alphaAmp * Math.sin(time * sp.alphaSpeed + sp.alphaPhase);
var tipWidth = this.size * sp.widthBase;
ctx.save();
ctx.rotate(spikeAngle);
var spikeGrad = ctx.createLinearGradient(
this.size, 0,
this.size + spikeLen, 0
);
spikeGrad.addColorStop(0, 'rgba(255, 210, 110, ' + alpha + ')');
spikeGrad.addColorStop(0.4, 'rgba(255, 185, 70, ' + (alpha * 0.6) + ')');
spikeGrad.addColorStop(1, 'rgba(240, 150, 30, 0)');
ctx.fillStyle = spikeGrad;
ctx.beginPath();
ctx.moveTo(this.size * 0.95, -tipWidth);
ctx.quadraticCurveTo(
this.size + spikeLen * 0.5, -tipWidth * 0.3,
this.size + spikeLen, 0
);
ctx.quadraticCurveTo(
this.size + spikeLen * 0.5, tipWidth * 0.3,
this.size * 0.95, tipWidth
);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// ── Layer 3: inner corona ring ───────────────────────────────────────
var coronaGrad = ctx.createRadialGradient(0, 0, this.size * 0.9, 0, 0, this.size * 1.3 * pulse);
coronaGrad.addColorStop(0, 'rgba(255, 200, 100, 0.18)');
coronaGrad.addColorStop(0.5, 'rgba(255, 170, 60, 0.07)');
coronaGrad.addColorStop(1, 'rgba(255, 140, 30, 0)');
ctx.fillStyle = coronaGrad;
ctx.beginPath();
ctx.arc(0, 0, this.size * 1.3 * pulse, 0, Math.PI * 2);
ctx.fill();
// ── Layer 4: main body ───────────────────────────────────────────────
var bodyGrad = ctx.createRadialGradient(
-this.size * 0.15, -this.size * 0.15, 0,
0, 0, this.size
);
bodyGrad.addColorStop(0, this.planetColors.light);
bodyGrad.addColorStop(0.45, this.planetColors.mid);
bodyGrad.addColorStop(1, this.planetColors.dark);
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.fill();
// ── Layer 5: surface granulation ────────────────────────────────────
for (var g = 0; g < this.granules.length; g++) {
var gran = this.granules[g];
var gAlpha = 0.05 + 0.03 * Math.sin(time * 0.35 + gran.phase);
var granGrad = ctx.createRadialGradient(
gran.x, gran.y, 0,
gran.x, gran.y, gran.radius
);
granGrad.addColorStop(0, 'rgba(255, 248, 200, ' + gAlpha + ')');
granGrad.addColorStop(1, 'rgba(255, 200, 100, 0)');
ctx.fillStyle = granGrad;
ctx.beginPath();
ctx.arc(gran.x, gran.y, gran.radius, 0, Math.PI * 2);
ctx.fill();
}
// ── Layer 6a: limb darkening ──────────────────────────────────────────
var limbGrad = ctx.createRadialGradient(0, 0, this.size * 0.45, 0, 0, this.size * 1.04);
limbGrad.addColorStop(0, 'rgba(0, 0, 0, 0)');
limbGrad.addColorStop(0.6, 'rgba(0, 0, 0, 0)');
limbGrad.addColorStop(0.82, 'rgba(0, 0, 0, 0.12)');
limbGrad.addColorStop(0.93, 'rgba(0, 0, 0, 0.20)');
limbGrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
// ── Layer 6b: sparkles (viewer-facing spikes) ────────────────────────
for (var k = 0; k < this.sparkles.length; k++) {
var sp = this.sparkles[k];
// Drift angle slowly over time
sp.angle += sp.wanderSpeed;
sp.x = Math.cos(sp.angle) * sp.dist;
sp.y = Math.sin(sp.angle) * sp.dist;
var bright = 0.5 + 0.5 * Math.sin(time * sp.speed + sp.phase);
var sAlpha = bright * bright; // squaring gives snappier flash
if (sAlpha < 0.05) continue; // skip when nearly invisible
ctx.save();
ctx.beginPath();
ctx.arc(0, 0, this.size, 0, Math.PI * 2);
ctx.clip(); // keep sparkles inside the disc
ctx.translate(sp.x, sp.y);
// Four-pointed cross — two perpendicular arms
for (var arm = 0; arm < 2; arm++) {
ctx.save();
ctx.rotate(arm * Math.PI / 2);
var armGrad = ctx.createLinearGradient(-sp.armLen, 0, sp.armLen, 0);
armGrad.addColorStop(0, 'rgba(255, 255, 240, 0)');
armGrad.addColorStop(0.35, 'rgba(255, 250, 210, ' + (sAlpha * 0.5) + ')');
armGrad.addColorStop(0.5, 'rgba(255, 255, 255, ' + sAlpha + ')');
armGrad.addColorStop(0.65, 'rgba(255, 250, 210, ' + (sAlpha * 0.5) + ')');
armGrad.addColorStop(1, 'rgba(255, 255, 240, 0)');
ctx.fillStyle = armGrad;
ctx.beginPath();
ctx.moveTo(-sp.armLen, 0);
ctx.lineTo(0, -sp.armWidth);
ctx.lineTo( sp.armLen, 0);
ctx.lineTo(0, sp.armWidth);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Bright centre dot
var dotGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, sp.size);
dotGrad.addColorStop(0, 'rgba(255, 255, 255, ' + sAlpha + ')');
dotGrad.addColorStop(0.5, 'rgba(255, 245, 200, ' + (sAlpha * 0.5) + ')');
dotGrad.addColorStop(1, 'rgba(255, 230, 150, 0)');
ctx.fillStyle = dotGrad;
ctx.beginPath();
ctx.arc(0, 0, sp.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// ── Layer 7: bright core ─────────────────────────────────────────────
var coreGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size * 0.45 * pulse);
coreGrad.addColorStop(0, 'rgba(255, 255, 230, 0.55)');
coreGrad.addColorStop(0.35,'rgba(255, 235, 160, 0.25)');
coreGrad.addColorStop(0.7, 'rgba(255, 200, 100, 0.08)');
coreGrad.addColorStop(1, 'rgba(255, 170, 50, 0)');
ctx.fillStyle = coreGrad;
ctx.beginPath();
ctx.arc(0, 0, this.size * 0.45 * pulse, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
};
Asteroid.prototype.drawPlanet = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);

View File

@ -207,31 +207,26 @@ var Game = (function() {
offlineTime = now - savedState.tabHiddenAt;
}
if (offlineTime > 1000) {
var rateToUse = 0;
if (state.rateLong > 0 && (now - state.lT) > 3600000) {
rateToUse = state.rateLong;
} else if (state.rateMedium > 0 && (now - state.mT) > 300000) {
rateToUse = state.rateMedium;
} else {
rateToUse = state.rateShort || 0;
}
if (rateToUse > 0) {
var offlineMass = rateToUse * (offlineTime / 1000);
state.blackHoleTotalMass += offlineMass;
state.totalMassConsumedEver += offlineMass;
state.totalMassConsumed += offlineMass;
state.sM += offlineMass;
state.mM += offlineMass;
state.lM += offlineMass;
showOfflineNotification(
formatOfflineTime(offlineTime),
UI.formatMass(offlineMass),
'offline'
);
}
}
if (offlineTime > 1000) {
var rateToUse = calculateTheoreticalRate(state, CONFIG);
if (rateToUse > 0) {
var offlineMass = rateToUse * (offlineTime / 1000);
state.blackHoleTotalMass += offlineMass;
state.totalMassConsumedEver += offlineMass;
state.totalMassConsumed += offlineMass;
state.sM = 0; state.sT = now;
state.mM = 0; state.mT = now;
state.lM = 0; state.lT = now;
showOfflineNotification(
formatOfflineTime(offlineTime),
UI.formatMass(offlineMass),
'offline'
);
}
}
state.tabHiddenAt = null;

View File

@ -46,65 +46,105 @@ function saturateColor(r, g, b, saturationBoost) {
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();
// Initialize tracking windows if needed
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;
// Add mass to all windows
// 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;
// Update per-second rate (with trend coloring)
state.cM += consumedMassKg;
// Per-second rate (display only — no longer drives trend)
const elapsedShort = now - state.sT;
if (elapsedShort >= 1000) {
const newRate = state.sM / (elapsedShort / 1000);
// Compare to previous rate for trend
if (state.rateShort === undefined) {
state.rateShort = newRate;
state.rateShortTrend = 'same';
} else {
// Determine trend with 5% threshold to avoid jitter
if (newRate > state.rateShort * 1.05) {
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 (newRate < state.rateShort * 0.95) {
} else if (ratio < 0.95) {
state.rateShortTrend = 'down';
} else {
state.rateShortTrend = 'same';
}
state.rateShort = newRate;
}
// Reset short window
state.sM = 0;
state.sT = now;
state.cM = 0;
state.cT = now;
}
// Calculate hourly average (rolling 1-hour window)
// Hourly average
const elapsedMedium = now - state.mT;
if (elapsedMedium > 0) {
state.rateMedium = state.mM / (elapsedMedium / 1000);
}
// Reset hourly window after 1 hour
if (elapsedMedium >= CONFIG.RATE_WINDOWS.MEDIUM) {
state.mM = 0;
state.mT = now;
}
// Calculate daily average (rolling 24-hour window)
// Daily average
const elapsedLong = now - state.lT;
if (elapsedLong > 0) {
state.rateLong = state.lM / (elapsedLong / 1000);
}
// Reset daily window after 24 hours
if (elapsedLong >= CONFIG.RATE_WINDOWS.LONG) {
state.lM = 0;
state.lT = now;

View File

@ -237,38 +237,23 @@ var UI = {
},
updateRateDisplay: function(state, config) {
const now = Date.now();
const theoretical = calculateTheoreticalRate(state, config);
const windowedTheoretical = calculateTheoreticalRate(state, config, 1000);
const trend = state.rateShortTrend || 'same';
// Format per-second rate (always real)
const rateShortText = this.formatMass(state.rateShort || 0) + '/s';
const rateShortText = this.formatMass(theoretical) + '/s';
const rateMediumText = this.formatMass(theoretical * 3600) + '/h';
const rateLongText = this.formatMass(theoretical * 86400) + '/d';
// 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';
}
const delta = (state.rateShort || 0) - windowedTheoretical;
const sign = delta >= 0 ? '+' : '-';
const deltaText = sign + this.formatMass(Math.abs(delta)) + '/s';
const deltaClass = trend === 'same' ? 'rate-same' : 'rate-' + trend;
// 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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${rateMediumText}</span>
<span class="rate-same">${rateShortText}</span>
<span class="${deltaClass}">&nbsp;${deltaText}</span>
</br><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${rateMediumText}</span>
</br><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;${rateLongText}</span>
`;
@ -427,7 +412,7 @@ var UI = {
updatePlanetUpgrade: function(gameState) {
var rate = (CONFIG.BASE_PLANET_SPAWN_INTERVAL / gameState.currentPlanetSpawnInterval).toFixed(2);
var bonusPercent = (gameState.planetUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_PLANT * 100).toFixed(0);
var tooltipText = 'Planets are roughly spherical accumulations of rock and metal, with solid surfaces and relatively thin atmospheres. Local examples include Mercury, Venus, Earth, and Mars. They may vary greatly in size.<br><br>Rate: ' + rate + '/hour<br>Bonus: ' + bonusPercent + '%';
var tooltipText = 'Planets are roughly spherical accumulations of rock and metal, with solid surfaces and relatively thin atmospheres. Local examples include Mercury, Earth, and Pluto. They may vary greatly in size.<br><br>Rate: ' + rate + '/hour<br>Bonus: ' + bonusPercent + '%';
if (!gameState.giantUnlocked) tooltipText += '<br>Unlocks Giants at level 10';
this.elements.planetLevel.innerHTML = '<span class="tooltip-trigger">Planets: Level ' +
@ -444,7 +429,7 @@ var UI = {
updateGiantUpgrade: function(gameState) {
var rate = (CONFIG.BASE_GIANT_SPAWN_INTERVAL / gameState.currentGiantSpawnInterval).toFixed(2);
var bonusPercent = (gameState.giantUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_GIANT * 100).toFixed(0);
var tooltipText = 'Gas giants are large planets made mostly of hydrogen and helium, with thick atmospheres and no solid surface; Jupiter and Saturn are examples.<br>Ice giants are similar but contain higher amounts of frozen materials like water, ammonia, and methane beneath their atmospheres; Uranus and Neptune fall into this category.<br><br>Spawn Rate: ' + rate + '/6hours<br>Bonus: ' + bonusPercent + '%';
var tooltipText = 'Gas giants are large planets made mostly of hydrogen and helium, with thick atmospheres and no solid surface; Jupiter and Saturn are examples.<br>Ice giants are similar but contain higher amounts of frozen materials like water, ammonia, and methane beneath their atmospheres; Uranus and Neptune fall into this category.<br><br>Rate: ' + rate + '/6hours<br>Bonus: ' + bonusPercent + '%';
if (!gameState.mtypeUnlocked) tooltipText += '<br>Unlocks M-Type at level 5';
this.elements.giantLevel.innerHTML = '<span class="tooltip-trigger">Giants: Level ' +
@ -461,7 +446,7 @@ var UI = {
updateMtypeUpgrade: function(gameState) {
var rate = (CONFIG.BASE_MTYPE_SPAWN_INTERVAL / gameState.currentMtypeSpawnInterval).toFixed(2);
var bonusPercent = (gameState.mtypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_MTYPE * 100).toFixed(0);
var tooltipText = 'M-types, also known as red dwarfs, are the smallest and coolest stars with masses ranging from about 0.08 to 0.45 M☉.<br>They are the most common star in the universe, making up roughly three quarters of all main-sequence stars, but are not easily visible due to their low luminosity.<br><br>Spawn Rate: ' + rate + '/day<br>Bonus: ' + bonusPercent + '%';
var tooltipText = 'M-types, also known as red dwarfs, are the smallest and coolest stars with masses ranging from about 0.08 to 0.45 M☉.<br>They are the most common star in the universe, making up roughly three quarters of all main-sequence stars, but are not easily visible due to their low luminosity.<br><br>Rate: ' + rate + '/day<br>Bonus: ' + bonusPercent + '%';
if (!gameState.ktypeUnlocked) tooltipText += '<br>Unlocks K-Type at level 5';
this.elements.mtypeLevel.innerHTML = '<span class="tooltip-trigger">M-Type: Level ' +
@ -476,7 +461,7 @@ var UI = {
updateKtypeUpgrade: function(gameState) {
var rate = (CONFIG.BASE_KTYPE_SPAWN_INTERVAL / gameState.currentKtypeSpawnInterval).toFixed(2);
var bonusPercent = (gameState.ktypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_KTYPE * 100).toFixed(0);
var tooltipText = 'K-types, also known as orange dwarfs, are medium-sized stars that are cooler than the Sun, with masses ranging from about 0.45 to 0.8 M☉. They are known for their stability and long lifespans (20 to 70 billion years) making them potential candidates for supporting inhabited planets.<br><br>Spawn Rate: ' + rate + '/2days<br>Bonus: ' + bonusPercent + '%';
var tooltipText = 'K-types, also known as orange dwarfs, are medium-sized stars that are cooler than the Sun, with masses ranging from about 0.45 to 0.8 M☉. They are known for their stability and long lifespans (20 to 70 billion years), making them potential candidates for supporting inhabited planets.<br><br>Rate: ' + rate + '/2days<br>Bonus: ' + bonusPercent + '%';
this.elements.ktypeLevel.innerHTML = '<span class="tooltip-trigger">K-Type: Level ' +
gameState.ktypeUpgradeLevel +