583 lines
21 KiB
JavaScript
583 lines
21 KiB
JavaScript
// Main game logic and animation loop
|
|
|
|
var Game = (function() {
|
|
var canvas = document.getElementById('space');
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
var state = {
|
|
blackHoleTotalMass: CONFIG.INITIAL_BLACK_HOLE_MASS * CONFIG.SOLAR_MASS_KG,
|
|
totalMassConsumedEver: 0,
|
|
totalMassConsumed: 0,
|
|
asteroidUpgradeLevel: 0,
|
|
asteroidUpgradeCost: CONFIG.ASTEROID_BASE_COST,
|
|
cometUpgradeLevel: 0,
|
|
cometUpgradeCost: CONFIG.COMET_BASE_COST,
|
|
planetUpgradeLevel: 0,
|
|
planetUpgradeCost: CONFIG.PLANET_BASE_COST,
|
|
giantUpgradeLevel: 0,
|
|
giantUpgradeCost: CONFIG.GIANT_BASE_COST,
|
|
mtypeUpgradeLevel: 0,
|
|
mtypeUpgradeCost: CONFIG.MTYPE_BASE_COST,
|
|
currentAsteroidSpawnInterval: CONFIG.BASE_ASTEROID_SPAWN_INTERVAL,
|
|
currentCometSpawnInterval: CONFIG.BASE_COMET_SPAWN_INTERVAL,
|
|
currentPlanetSpawnInterval: CONFIG.BASE_PLANET_SPAWN_INTERVAL,
|
|
currentGiantSpawnInterval: CONFIG.BASE_GIANT_SPAWN_INTERVAL,
|
|
currentMtypeSpawnInterval: CONFIG.BASE_MTYPE_SPAWN_INTERVAL,
|
|
asteroidSpawnCount: 0,
|
|
lastAsteroidSpawn: Date.now(),
|
|
lastCometSpawn: Date.now(),
|
|
lastPlanetSpawn: Date.now(),
|
|
lastGiantSpawn: Date.now(),
|
|
lastMtypeSpawn: Date.now(),
|
|
sM: 0, sT: Date.now(),
|
|
mM: 0, mT: Date.now(),
|
|
lM: 0, lT: Date.now(),
|
|
rateShort: 0, // 1 second average
|
|
rateMedium: 0, // 1 hour average
|
|
rateLong: 0, // 1 day average
|
|
isReady: false,
|
|
tabHiddenAt: null,
|
|
cometUnlocked: false,
|
|
planetUnlocked: false,
|
|
giantUnlocked: false,
|
|
mtypeUnlocked: false
|
|
};
|
|
|
|
var blackHole;
|
|
var stars = [];
|
|
var asteroids = [];
|
|
var lastFrameTime = 0;
|
|
var lastStarUpdate = 0;
|
|
var needsRender = true;
|
|
var currentTime = Date.now();
|
|
var lastLogicUpdate = Date.now();
|
|
|
|
// Expose to admin interface
|
|
window.gameBlackHole = null;
|
|
window.gameAsteroids = asteroids;
|
|
|
|
function resizeCanvas() {
|
|
// Set canvas to actual viewport size
|
|
canvas.width = window.innerWidth
|
|
canvas.height = window.innerHeight
|
|
}
|
|
|
|
function init() {
|
|
resizeCanvas();
|
|
|
|
blackHole = new BlackHole(
|
|
canvas.width / 2,
|
|
canvas.height / 2,
|
|
CONFIG.INITIAL_BLACK_HOLE_RADIUS
|
|
);
|
|
|
|
window.gameBlackHole = blackHole;
|
|
|
|
for (var i = 0; i < CONFIG.STAR_COUNT; i++) {
|
|
stars.push(new Star(canvas));
|
|
}
|
|
setInterval(function() {
|
|
UI.updateActiveObjects();
|
|
}, 1000);
|
|
|
|
UI.init();
|
|
|
|
UI.setUpgradeHandlers({
|
|
asteroid: handleAsteroidUpgrade,
|
|
comet: handleCometUpgrade,
|
|
planet: handlePlanetUpgrade,
|
|
giant: handleGiantUpgrade,
|
|
mtype: handleMtypeUpgrade
|
|
});
|
|
|
|
Server.init()
|
|
loadGameState();
|
|
|
|
window.addEventListener('resize', function() {
|
|
resizeCanvas();
|
|
blackHole.x = canvas.width / 2;
|
|
blackHole.y = canvas.height / 2;
|
|
stars.forEach(function(star) {
|
|
star.reset();
|
|
});
|
|
});
|
|
|
|
window.addEventListener('wheel', function(e) {
|
|
if (e.ctrlKey) {
|
|
e.preventDefault();
|
|
}
|
|
}, { passive: false });
|
|
|
|
window.addEventListener('keydown', function(e) {
|
|
if ((e.ctrlKey || e.metaKey) && (e.key === '+' || e.key === '-' || e.key === '=')) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
animate(currentTime);
|
|
UI.update(state, CONFIG);
|
|
|
|
// Save when page becomes hidden (tab switch, minimize, close)
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (document.hidden) {
|
|
state.tabHiddenAt = Date.now(); // NEW: Record when hidden
|
|
Server.checkpoint(state);
|
|
} else if (!document.hidden) {
|
|
loadGameState();
|
|
Server.checkpoint(state);
|
|
}
|
|
});
|
|
|
|
// Save when page is about to unload (close tab, navigate away)
|
|
window.addEventListener('beforeunload', function() {
|
|
state.tabHiddenAt = Date.now();
|
|
Server.checkpoint(state);
|
|
});
|
|
|
|
// Save and update scores when window gains focus
|
|
window.addEventListener('focus', function() {
|
|
if (window.Server && window.Game && Game.getState) {
|
|
Server.checkpoint(Game.getState());
|
|
}
|
|
});
|
|
}
|
|
|
|
function loadGameState() {
|
|
Server.loadGameState().then(function(savedState) {
|
|
if (savedState) {
|
|
state.blackHoleTotalMass = savedState.blackHoleTotalMass;
|
|
state.totalMassConsumedEver = savedState.totalMassConsumedEver;
|
|
state.totalMassConsumed = savedState.totalMassConsumed;
|
|
state.asteroidUpgradeLevel = savedState.asteroidUpgradeLevel;
|
|
state.asteroidUpgradeCost = savedState.asteroidUpgradeCost;
|
|
state.cometUpgradeLevel = savedState.cometUpgradeLevel;
|
|
state.cometUpgradeCost = savedState.cometUpgradeCost;
|
|
state.planetUpgradeLevel = savedState.planetUpgradeLevel;
|
|
state.planetUpgradeCost = savedState.planetUpgradeCost;
|
|
state.giantUpgradeLevel = savedState.giantUpgradeLevel;
|
|
state.giantUpgradeCost = savedState.giantUpgradeCost;
|
|
state.mtypeUpgradeLevel = savedState.mtypeUpgradeLevel || 0;
|
|
state.mtypeUpgradeCost = savedState.mtypeUpgradeCost || CONFIG.MTYPE_BASE_COST;
|
|
state.asteroidSpawnCount = savedState.asteroidSpawnCount;
|
|
|
|
// Load unlock states (with backward compatibility)
|
|
state.cometUnlocked = savedState.cometUnlocked !== undefined ? savedState.cometUnlocked : (savedState.asteroidUpgradeLevel >= 20);
|
|
state.planetUnlocked = savedState.planetUnlocked !== undefined ? savedState.planetUnlocked : (savedState.cometUpgradeLevel >= 15);
|
|
state.giantUnlocked = savedState.giantUnlocked !== undefined ? savedState.giantUnlocked : (savedState.planetUpgradeLevel >= 10);
|
|
state.mtypeUnlocked = savedState.mtypeUnlocked !== undefined ? savedState.mtypeUnlocked : (savedState.giantUpgradeLevel >= 5);
|
|
|
|
state.lastAsteroidSpawn = savedState.lastAsteroidSpawn || Date.now();
|
|
state.lastCometSpawn = savedState.lastCometSpawn || Date.now();
|
|
state.lastPlanetSpawn = savedState.lastPlanetSpawn || Date.now();
|
|
state.lastGiantSpawn = savedState.lastGiantSpawn || Date.now();
|
|
state.lastMtypeSpawn = savedState.lastMtypeSpawn || Date.now();
|
|
|
|
// Load consumption rates
|
|
state.sM = savedState.sM || 0;
|
|
state.mM = savedState.mM || 0;
|
|
state.lM = savedState.lM || 0;
|
|
|
|
var now = Date.now();
|
|
|
|
state.sT = savedState.sT || now;
|
|
state.mT = savedState.mT || now;
|
|
state.lT = savedState.lT || now;
|
|
|
|
state.rateShort = savedState.rateShort || 0;
|
|
state.rateShortTrend = savedState.rateShortTrend || 'same';
|
|
state.rateMedium = savedState.rateMedium || 0;
|
|
state.rateLong = savedState.rateLong || 0;
|
|
|
|
state.tabHiddenAt = savedState.tabHiddenAt || null;
|
|
|
|
// Calculate offline progression ONLY if tab was actually hidden
|
|
var offlineTime = 0;
|
|
|
|
if (savedState.tabHiddenAt) {
|
|
// Tab was hidden - calculate time between hiding and now
|
|
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'
|
|
);
|
|
}
|
|
}
|
|
|
|
// Clear the hidden timestamp after processing
|
|
state.tabHiddenAt = null;
|
|
|
|
updateSpawnIntervals();
|
|
|
|
// Restore black hole size
|
|
blackHole.radius = 0.5 * (state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG)
|
|
|
|
// Send checkpoint after loading
|
|
Server.checkpoint(state).then(function() {
|
|
state.isReady = true; // Mark as ready after checkpoint
|
|
});
|
|
} else {
|
|
// No saved state - send initial checkpoint for new player
|
|
Server.checkpoint(state).then(function() {
|
|
state.isReady = true; // Mark as ready after checkpoint
|
|
});
|
|
}
|
|
|
|
UI.update(state, CONFIG);
|
|
});
|
|
}
|
|
|
|
function updateSpawnIntervals() {
|
|
state.currentAsteroidSpawnInterval = CONFIG.BASE_ASTEROID_SPAWN_INTERVAL /
|
|
(1 + state.asteroidUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_ASTER);
|
|
state.currentCometSpawnInterval = CONFIG.BASE_COMET_SPAWN_INTERVAL /
|
|
(1 + state.cometUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_COMET);
|
|
state.currentPlanetSpawnInterval = CONFIG.BASE_PLANET_SPAWN_INTERVAL /
|
|
(1 + state.planetUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_PLANT);
|
|
state.currentGiantSpawnInterval = CONFIG.BASE_GIANT_SPAWN_INTERVAL /
|
|
(1 + state.giantUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_GIANT);
|
|
state.currentMtypeSpawnInterval = CONFIG.BASE_MTYPE_SPAWN_INTERVAL /
|
|
(1 + state.mtypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_MTYPE);
|
|
}
|
|
|
|
function handleAsteroidUpgrade() {
|
|
if (state.totalMassConsumed >= state.asteroidUpgradeCost) {
|
|
state.totalMassConsumed -= state.asteroidUpgradeCost;
|
|
state.asteroidUpgradeLevel++;
|
|
state.asteroidUpgradeCost = Math.floor(
|
|
CONFIG.ASTEROID_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_ASTER, state.asteroidUpgradeLevel)
|
|
);
|
|
if (state.asteroidUpgradeLevel >= 20 && !state.cometUnlocked) {
|
|
state.cometUnlocked = true;
|
|
showUnlockNotification('comet');
|
|
asteroids.push(new Asteroid('comet', blackHole, canvas));
|
|
state.lastCometSpawn = Date.now();
|
|
}
|
|
|
|
updateSpawnIntervals();
|
|
UI.update(state, CONFIG);
|
|
}
|
|
}
|
|
|
|
function handleCometUpgrade() {
|
|
if (state.totalMassConsumed >= state.cometUpgradeCost) {
|
|
state.totalMassConsumed -= state.cometUpgradeCost;
|
|
state.cometUpgradeLevel++;
|
|
state.cometUpgradeCost = Math.floor(
|
|
CONFIG.COMET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_COMET, state.cometUpgradeLevel)
|
|
);
|
|
if (state.cometUpgradeLevel >= 15 && !state.planetUnlocked) {
|
|
state.planetUnlocked = true;
|
|
showUnlockNotification('planet');
|
|
asteroids.push(new Asteroid('planet', blackHole, canvas));
|
|
state.lastPlanetSpawn = Date.now();
|
|
}
|
|
updateSpawnIntervals();
|
|
UI.update(state, CONFIG);
|
|
}
|
|
}
|
|
|
|
function handlePlanetUpgrade() {
|
|
if (state.totalMassConsumed >= state.planetUpgradeCost) {
|
|
state.totalMassConsumed -= state.planetUpgradeCost;
|
|
state.planetUpgradeLevel++;
|
|
state.planetUpgradeCost = Math.floor(
|
|
CONFIG.PLANET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_PLANT, state.planetUpgradeLevel)
|
|
);
|
|
if (state.planetUpgradeLevel >= 10 && !state.giantUnlocked) {
|
|
state.giantUnlocked = true;
|
|
showUnlockNotification('giant');
|
|
asteroids.push(new Asteroid('giant', blackHole, canvas));
|
|
state.lastGiantSpawn = Date.now();
|
|
}
|
|
updateSpawnIntervals();
|
|
UI.update(state, CONFIG);
|
|
}
|
|
}
|
|
|
|
function handleGiantUpgrade() {
|
|
if (state.totalMassConsumed >= state.giantUpgradeCost) {
|
|
state.totalMassConsumed -= state.giantUpgradeCost;
|
|
state.giantUpgradeLevel++;
|
|
state.giantUpgradeCost = Math.floor(
|
|
CONFIG.GIANT_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_GIANT, state.giantUpgradeLevel)
|
|
);
|
|
|
|
// Unlock M-Type at giant level 5
|
|
if (state.giantUpgradeLevel >= 5 && !state.mtypeUnlocked) {
|
|
state.mtypeUnlocked = true;
|
|
state.lastMtypeSpawn = Date.now();
|
|
showUnlockNotification('mtype');
|
|
asteroids.push(new Asteroid('mtype', blackHole, canvas));
|
|
}
|
|
|
|
updateSpawnIntervals();
|
|
UI.update(state, CONFIG);
|
|
}
|
|
}
|
|
|
|
function handleMtypeUpgrade() {
|
|
if (state.totalMassConsumed >= state.mtypeUpgradeCost) {
|
|
state.totalMassConsumed -= state.mtypeUpgradeCost;
|
|
state.mtypeUpgradeLevel++;
|
|
state.mtypeUpgradeCost = Math.floor(
|
|
CONFIG.MTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_MTYPE, state.mtypeUpgradeLevel)
|
|
);
|
|
updateSpawnIntervals();
|
|
UI.update(state, CONFIG);
|
|
}
|
|
}
|
|
|
|
function spawnAsteroids() {
|
|
var currentTime = Date.now();
|
|
|
|
if (currentTime - state.lastAsteroidSpawn > state.currentAsteroidSpawnInterval) {
|
|
state.asteroidSpawnCount++;
|
|
|
|
var asteroidType;
|
|
if (state.asteroidSpawnCount % CONFIG.ASTEROID_SPAWN_PATTERNS.LARGE_EVERY === 0) {
|
|
asteroidType = 'large';
|
|
} else if (state.asteroidSpawnCount % CONFIG.ASTEROID_SPAWN_PATTERNS.MEDIUM_EVERY === 0) {
|
|
asteroidType = 'medium';
|
|
} else {
|
|
asteroidType = 'small';
|
|
}
|
|
|
|
asteroids.push(new Asteroid(asteroidType, blackHole, canvas));
|
|
state.lastAsteroidSpawn = currentTime;
|
|
}
|
|
|
|
if (state.cometUnlocked && state.cometUpgradeLevel >= 0) {
|
|
if (currentTime - state.lastCometSpawn > state.currentCometSpawnInterval) {
|
|
state.lastCometSpawn = currentTime;
|
|
asteroids.push(new Asteroid('comet', blackHole, canvas));
|
|
}
|
|
}
|
|
|
|
if (state.planetUnlocked && state.planetUpgradeLevel >= 0) {
|
|
if (currentTime - state.lastPlanetSpawn > state.currentPlanetSpawnInterval) {
|
|
state.lastPlanetSpawn = currentTime;
|
|
asteroids.push(new Asteroid('planet', blackHole, canvas));
|
|
}
|
|
}
|
|
|
|
if (state.giantUnlocked && state.giantUpgradeLevel >= 0) {
|
|
if (currentTime - state.lastGiantSpawn > state.currentGiantSpawnInterval) {
|
|
state.lastGiantSpawn = currentTime;
|
|
asteroids.push(new Asteroid('giant', blackHole, canvas));
|
|
}
|
|
}
|
|
if (state.mtypeUnlocked) {
|
|
if (currentTime - state.lastMtypeSpawn > state.currentMtypeSpawnInterval) {
|
|
state.lastMtypeSpawn = currentTime;
|
|
asteroids.push(new Asteroid('mtype', blackHole, canvas));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function updateAsteroids() {
|
|
for (var i = asteroids.length - 1; i >= 0; i--) {
|
|
asteroids[i].update();
|
|
|
|
if (asteroids[i].isDestroyed()) {
|
|
var consumedMassKg = asteroids[i].massKg;
|
|
|
|
state.blackHoleTotalMass += consumedMassKg;
|
|
state.totalMassConsumedEver += consumedMassKg;
|
|
state.totalMassConsumed += consumedMassKg;
|
|
|
|
calculateConsumptionRates(state, CONFIG, consumedMassKg);
|
|
|
|
// Grow black hole radius
|
|
var solarMasses = state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG;
|
|
blackHole.radius = CONFIG.INITIAL_BLACK_HOLE_RADIUS + (solarMasses - CONFIG.INITIAL_BLACK_HOLE_MASS);
|
|
|
|
blackHole.consumeAsteroid(asteroids[i]);
|
|
|
|
asteroids.splice(i, 1);
|
|
|
|
|
|
UI.update(state, CONFIG);
|
|
}
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
ctx.fillStyle = '#000';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Draw stars (don't update them here - that happens in animate())
|
|
for (var i = 0; i < stars.length; i++) {
|
|
stars[i].draw(ctx);
|
|
}
|
|
|
|
// Draw asteroids
|
|
for (var i = 0; i < asteroids.length; i++) {
|
|
asteroids[i].draw(ctx);
|
|
}
|
|
|
|
blackHole.draw(ctx, state.rateShort || 0);
|
|
}
|
|
|
|
function animate(currentTime) {
|
|
requestAnimationFrame(animate);
|
|
|
|
// Logic Update (Low Frequency)
|
|
if (!lastLogicUpdate) lastLogicUpdate = currentTime;
|
|
|
|
var elapsedTime = Date.now() - lastLogicUpdate
|
|
|
|
if (elapsedTime >= CONFIG.LOGIC_INTERVAL) {
|
|
spawnAsteroids();
|
|
updateAsteroids();
|
|
lastLogicUpdate = Date.now();
|
|
needsRender = true;
|
|
}
|
|
|
|
// Update stars less frequently (they twinkle slowly)
|
|
if (currentTime - lastStarUpdate > CONFIG.STAR_UPDATE_INTERVAL) {
|
|
for (var i = 0; i < stars.length; i++) {
|
|
stars[i].update();
|
|
}
|
|
lastStarUpdate = currentTime;
|
|
needsRender = true;
|
|
}
|
|
|
|
if (needsRender) {
|
|
render();
|
|
needsRender = false;
|
|
}
|
|
}
|
|
|
|
return {
|
|
init: init,
|
|
getState: function() {
|
|
return state;
|
|
}
|
|
}
|
|
}());
|
|
|
|
var Server = {
|
|
playerId: null,
|
|
|
|
init: function() {
|
|
// Get or create player ID
|
|
this.playerId = Storage.getCookie('playerId');
|
|
if (!this.playerId) {
|
|
this.playerId = 'p_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
Storage.setCookie('playerId', this.playerId, 365);
|
|
}
|
|
|
|
// Start checkpoints
|
|
this.startCheckpoints();
|
|
},
|
|
|
|
checkpoint: async function(gameState) {
|
|
try {
|
|
// Add savedAt timestamp
|
|
gameState.savedAt = Date.now();
|
|
|
|
const response = await fetch('/api/checkpoint', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
playerId: this.playerId,
|
|
gameState: gameState
|
|
})
|
|
});
|
|
|
|
return await response.json();
|
|
} catch (err) {
|
|
console.error('Checkpoint failed', err);
|
|
return { success: false };
|
|
}
|
|
},
|
|
|
|
loadGameState: async function() {
|
|
try {
|
|
const resp = await fetch(`/api/player/${encodeURIComponent(this.playerId)}/state`);
|
|
if (!resp.ok) return null;
|
|
return await resp.json();
|
|
} catch (err) {
|
|
console.error('Failed to load game state', err);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
getLeaderboard: async function(sortBy) {
|
|
try {
|
|
const resp = await fetch(`/api/leaderboard?sort=${encodeURIComponent(sortBy)}&playerId=${encodeURIComponent(this.playerId)}`);
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
const data = await resp.json();
|
|
return data;
|
|
} catch (err) {
|
|
console.error('Failed to fetch leaderboard', err);
|
|
return [];
|
|
}
|
|
},
|
|
|
|
deletePlayer: async function(playerId) {
|
|
try {
|
|
const resp = await fetch(`/api/player/${encodeURIComponent(playerId)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
return await resp.json();
|
|
} catch (err) {
|
|
console.error('Failed to delete player', err);
|
|
return { success: false };
|
|
}
|
|
},
|
|
|
|
startCheckpoints: function() {
|
|
let lastCheckpointData = null;
|
|
|
|
setInterval(() => {
|
|
if (window.Game && Game.getState) {
|
|
const state = Game.getState();
|
|
|
|
// Only checkpoint if significant changes
|
|
const currentData = JSON.stringify({
|
|
mass: Math.floor(state.blackHoleTotalMass / 1e27), // Round to reduce noise
|
|
levels: state.asteroidUpgradeLevel + state.cometUpgradeLevel +
|
|
state.planetUpgradeLevel + state.giantUpgradeLevel
|
|
});
|
|
|
|
if (currentData !== lastCheckpointData) {
|
|
this.checkpoint(state);
|
|
lastCheckpointData = currentData;
|
|
}
|
|
}
|
|
}, 15000); // Check every 15s, but only send if changed
|
|
},
|
|
};
|
|
|
|
// Start the game when the page loads
|
|
window.addEventListener('load', function() {
|
|
Game.init();
|
|
});
|