hoel/js/game.js
xbl b37a8192e4 modified: js/config.js
modified:   js/game.js
2026-03-04 23:23:17 +01:00

712 lines
26 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,
ktypeUpgradeLevel: 0,
ktypeUpgradeCost: CONFIG.KTYPE_BASE_COST,
gtypeUpgradeLevel: 0,
gtypeUpgradeCost: CONFIG.GTYPE_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,
currentKtypeSpawnInterval: CONFIG.BASE_KTYPE_SPAWN_INTERVAL,
currentGtypeSpawnInterval: CONFIG.BASE_GTYPE_SPAWN_INTERVAL,
asteroidSpawnCount: 0,
lastAsteroidSpawn: Date.now(),
lastCometSpawn: Date.now(),
lastPlanetSpawn: Date.now(),
lastGiantSpawn: Date.now(),
lastMtypeSpawn: Date.now(),
lastKtypeSpawn: Date.now(),
lastGtypeSpawn: 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,
ktypeUnlocked: false,
gtypeUnlocked: 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,
ktype: handleKtypeUpgrade,
gtype: handleGtypeUpgrade
});
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) {
// Restore only what cannot be derived
state.blackHoleTotalMass = savedState.blackHoleTotalMass;
state.totalMassConsumedEver = savedState.totalMassConsumedEver;
state.totalMassConsumed = savedState.totalMassConsumed;
state.asteroidSpawnCount = savedState.asteroidSpawnCount;
// Upgrade levels — the only upgrade data we store
state.asteroidUpgradeLevel = savedState.asteroidUpgradeLevel || 0;
state.cometUpgradeLevel = savedState.cometUpgradeLevel || 0;
state.planetUpgradeLevel = savedState.planetUpgradeLevel || 0;
state.giantUpgradeLevel = savedState.giantUpgradeLevel || 0;
state.mtypeUpgradeLevel = savedState.mtypeUpgradeLevel || 0;
state.ktypeUpgradeLevel = savedState.ktypeUpgradeLevel || 0;
state.gtypeUpgradeLevel = savedState.gtypeUpgradeLevel || 0;
// Derive unlock states from levels
state.cometUnlocked = state.asteroidUpgradeLevel >= 20;
state.planetUnlocked = state.cometUpgradeLevel >= 15;
state.giantUnlocked = state.planetUpgradeLevel >= 10;
state.mtypeUnlocked = state.giantUpgradeLevel >= 5;
state.ktypeUnlocked = state.mtypeUpgradeLevel >= 5;
state.gtypeUnlocked = state.ktypeUpgradeLevel >= 5;
// Derive costs and intervals from levels + CONFIG
recalculateUpgradeCosts();
updateSpawnIntervals();
// Spawn timestamps
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();
state.lastKtypeSpawn = savedState.lastKtypeSpawn || Date.now();
state.lastGtypeSpawn = savedState.lastGtypeSpawn || Date.now();
// 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;
// Offline progression
var offlineTime = 0;
if (savedState.tabHiddenAt) {
offlineTime = now - savedState.tabHiddenAt;
}
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;
// Restore black hole size
blackHole.radius = 0.5 * (state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG);
Server.checkpoint(state).then(function() {
state.isReady = true;
});
} 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() {
// Calculate star-tier spawn boost (multiplicative)
var starBonus = 1 +
(state.mtypeUpgradeLevel * CONFIG.MTYPE_SPAWN_BOOST_PER_LEVEL) +
(state.ktypeUpgradeLevel * CONFIG.KTYPE_SPAWN_BOOST_PER_LEVEL) +
(state.gtypeUpgradeLevel * CONFIG.GTYPE_SPAWN_BOOST_PER_LEVEL);
// Asteroids boosted by all stars
state.currentAsteroidSpawnInterval = CONFIG.BASE_ASTEROID_SPAWN_INTERVAL /
((1 + state.asteroidUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_ASTER) * starBonus);
// Comets boosted by all stars
state.currentCometSpawnInterval = CONFIG.BASE_COMET_SPAWN_INTERVAL /
((1 + state.cometUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_COMET) * starBonus);
// Planets boosted by all stars
state.currentPlanetSpawnInterval = CONFIG.BASE_PLANET_SPAWN_INTERVAL /
((1 + state.planetUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_PLANT) * starBonus);
// Giants boosted by all stars
state.currentGiantSpawnInterval = CONFIG.BASE_GIANT_SPAWN_INTERVAL /
((1 + state.giantUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_GIANT) * starBonus);
// M-type boosted by all stars
state.currentMtypeSpawnInterval = CONFIG.BASE_MTYPE_SPAWN_INTERVAL /
((1 + state.mtypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_MTYPE) * starBonus);
// K-type boosted by all stars
state.currentKtypeSpawnInterval = CONFIG.BASE_KTYPE_SPAWN_INTERVAL /
((1 + state.ktypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_KTYPE) * starBonus);
// G-type boosted by all stars
state.currentGtypeSpawnInterval = CONFIG.BASE_GTYPE_SPAWN_INTERVAL /
((1 + state.gtypeUpgradeLevel * CONFIG.UPGRADE_BONUS_PER_LEVEL_GTYPE) * starBonus);
}
function recalculateUpgradeCosts() {
state.asteroidUpgradeCost = Math.floor(CONFIG.ASTEROID_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_ASTER, state.asteroidUpgradeLevel));
state.cometUpgradeCost = Math.floor(CONFIG.COMET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_COMET, state.cometUpgradeLevel));
state.planetUpgradeCost = Math.floor(CONFIG.PLANET_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_PLANT, state.planetUpgradeLevel));
state.giantUpgradeCost = Math.floor(CONFIG.GIANT_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_GIANT, state.giantUpgradeLevel));
state.mtypeUpgradeCost = Math.floor(CONFIG.MTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_MTYPE, state.mtypeUpgradeLevel));
state.ktypeUpgradeCost = Math.floor(CONFIG.KTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_KTYPE, state.ktypeUpgradeLevel));
state.gtypeUpgradeCost = Math.floor(CONFIG.GTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_GTYPE, state.gtypeUpgradeLevel));
}
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)
);
// Unlock K-Type at m-type level 5
if (state.mtypeUpgradeLevel >= 5 && !state.ktypeUnlocked) {
state.ktypeUnlocked = true;
state.lastKtypeSpawn = Date.now();
showUnlockNotification('ktype');
asteroids.push(new Asteroid('ktype', blackHole, canvas));
}
updateSpawnIntervals();
UI.update(state, CONFIG);
}
}
function handleKtypeUpgrade() {
if (state.totalMassConsumed >= state.ktypeUpgradeCost) {
state.totalMassConsumed -= state.ktypeUpgradeCost;
state.ktypeUpgradeLevel++;
state.ktypeUpgradeCost = Math.floor(
CONFIG.KTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_KTYPE, state.ktypeUpgradeLevel)
);
// Unlock K-Type at m-type level 5
if (state.ktypeUpgradeLevel >= 5 && !state.gtypeUnlocked) {
state.gtypeUnlocked = true;
state.lastGtypeSpawn = Date.now();
showUnlockNotification('gtype');
asteroids.push(new Asteroid('gtype', blackHole, canvas));
}
updateSpawnIntervals();
UI.update(state, CONFIG);
}
}
function handleGtypeUpgrade() {
if (state.totalMassConsumed >= state.gtypeUpgradeCost) {
state.totalMassConsumed -= state.gtypeUpgradeCost;
state.gtypeUpgradeLevel++;
state.gtypeUpgradeCost = Math.floor(
CONFIG.GTYPE_BASE_COST * Math.pow(CONFIG.UPGRADE_COST_MULTIPLIER_GTYPE, state.gtypeUpgradeLevel)
);
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));
}
}
if (state.ktypeUnlocked) {
if (currentTime - state.lastKtypeSpawn > state.currentKtypeSpawnInterval) {
state.lastKtypeSpawn = currentTime;
asteroids.push(new Asteroid('ktype', blackHole, canvas));
}
}
if (state.gtypeUnlocked) {
if (currentTime - state.lastGtypeSpawn > state.currentGtypeSpawnInterval) {
state.lastGtypeSpawn = currentTime;
asteroids.push(new Asteroid('gtype', 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
blackHole.radius = (state.blackHoleTotalMass / CONFIG.SOLAR_MASS_KG) * 0.333;
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);
}
// Get or create session token
this.sessionToken = Storage.getOrCreateToken();
// 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,
secretToken: this.sessionToken,
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
},
createTransferCode: async function() {
try {
const resp = await fetch('/api/transfer/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId: this.playerId,
secretToken: this.sessionToken
})
});
const data = await resp.json();
if (!resp.ok) return { error: data.error };
return data;
}catch (err) {
console.error('Failed to create transfer code', err);
return null;
}
},
claimTransferCode: async function(code) {
try {
const resp = await fetch('/api/transfer/claim', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
if (!resp.ok) throw new Error('HTTP ' + resp.status);
return await resp.json();
} catch (err) {
console.error('Failed to claim transfer code', err);
return null;
}
}
};
// Start the game when the page loads
window.addEventListener('load', function() {
Game.init();
});