Files
idle-game/js/game.js
T
hermes cc1fa6bc4d Session 16: Global 'Buy All Buildings' bulk-buy feature Added
Adds a global Buy All button to the Buildings panel that purchases as many
of every building type the player can afford at once, bringing Plants in line
with the Upgrades (Buy All) and Prestige (Buy Max) panels.

- game.js: Game.buyAllBuildings() loops buyMaxBuilding() per building type
- ui.js: UI.updateBuyAllBuildings() shows/hides + enables/disables live each tick
- index.html: Buy All button in Buildings panel header (reuses .upgrades-header/.buy-all-btn)
- Added headless test buy-all-buildings-test.mjs (31/31 pass); all existing suites green
2026-08-24 18:00:27 +00:00

1029 lines
35 KiB
JavaScript

/**
* Cosmic Garden - Main Game Engine
* Handles game state, tick loop, and core logic
*/
const Game = {
// Core game state
state: {
stardust: 0,
totalStardust: 0,
totalClicks: 0,
clickPower: 1,
startTime: Date.now(),
lastSaveTime: Date.now(),
buildings: {},
upgrades: [],
achievements: [],
stellarEssence: 0,
prestigeCount: 0,
totalPrestigeEssence: 0,
prestigeUpgrades: [],
// Save Slot System
activeSlot: 0, // index of the currently selected save slot (0..SLOTS-1)
autoSave: true, // whether the periodic autosave loop is running
// Golden Starbloom random event state
goldenBlooms: {
active: false, // is a bloom currently visible on screen?
timer: 0, // seconds until the bloom vanishes
lastSpawn: Date.now(), // ms when we last (re)spawned one
totalClaimed: 0 // lifetime count of blooms the player has clicked
}
},
// Configuration
config: {
tickRate: 100, // ms per tick (10 ticks/sec)
autoSaveInterval: 30000, // 30 seconds
costScaling: 1.15, // Cost multiplier per building level
// Save Slot System - number of independent save slots available
SLOTS: 3,
goldenBloomMinMs: 45000, // min ms between bloom spawns (45s)
goldenBloomMaxMs: 90000, // max ms between bloom spawns (90s)
goldenBloomDuration: 12, // seconds a visible bloom stays before vanishing
goldenBloomBonusMult: 15 // production-per-second worth of stardust awarded
},
/**
* Initialize the game
*/
init() {
this.loadState();
this.setupEventListeners();
this.startGameLoop();
this.startAutoSave();
Audio.init();
UI.init();
Stats.init();
PrestigeUI.init();
console.log('🌌 Cosmic Garden initialized!');
},
/**
* Setup event listeners
*/
setupEventListeners() {
// Track which slot the user has selected for importing.
this.importSlot = 0;
// Harvest button - support both click and touch
const harvestBtn = document.getElementById('harvest-btn');
harvestBtn.addEventListener('click', (e) => {
this.harvest(e);
this.createRipple(harvestBtn, e);
this.animateClickButton(harvestBtn);
});
// Touch support for better mobile experience
harvestBtn.addEventListener('touchstart', (e) => {
e.preventDefault(); // Prevent double-fire with click
const touch = e.touches[0];
const clickEvent = new MouseEvent('click', {
clientX: touch.clientX,
clientY: touch.clientY,
bubbles: true
});
// Create ripple effect for touch
this.createRipple(harvestBtn, { clientX: touch.clientX, clientY: touch.clientY });
this.animateClickButton(harvestBtn);
}, { passive: false });
// Save buttons
document.getElementById('save-btn').addEventListener('click', () => {
this.save();
UI.showNotification('Game saved!', 'success');
});
document.getElementById('export-btn').addEventListener('click', () => {
SaveSystem.exportSave();
});
document.getElementById('import-btn').addEventListener('click', () => {
UI.showImportModal();
});
document.getElementById('confirm-import').addEventListener('click', () => {
const data = document.getElementById('import-data').value;
if (data) {
const ok = this.importSlotFromData(data, this.importSlot);
UI.hideImportModal();
if (ok) {
// If the import landed in the active slot, switchSlot already re-rendered.
UI.update();
}
}
});
document.getElementById('cancel-import').addEventListener('click', () => {
UI.hideImportModal();
});
// Buy All upgrades button.
const buyAllBtn = document.getElementById('buy-all-upgrades');
if (buyAllBtn) {
buyAllBtn.addEventListener('click', () => {
Game.buyAllUpgrades();
});
}
// Buy All buildings button - buy as many of every building at once.
const buyAllBuildingsBtn = document.getElementById('buy-all-buildings');
if (buyAllBuildingsBtn) {
buyAllBuildingsBtn.addEventListener('click', () => {
Game.buyAllBuildings();
});
}
document.getElementById('reset-btn').addEventListener('click', () => {
if (confirm('Are you sure you want to reset all progress? This cannot be undone!')) {
this.reset();
}
});
// Save Slot System: slot selection buttons.
document.querySelectorAll('.slot-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (this.state.activeSlot === parseInt(btn.dataset.slot, 10)) {
UI.showNotification(`Already on Slot ${parseInt(btn.dataset.slot, 10) + 1}`, 'info');
return;
}
if (confirm(`Switch to Slot ${parseInt(btn.dataset.slot, 10) + 1}?\nUnsaved changes will be saved first.`)) {
this.save(); // Persist current slot before switching.
this.switchSlot(parseInt(btn.dataset.slot, 10));
}
});
});
// Autosave toggle button.
document.getElementById('autosave-btn').addEventListener('click', () => {
this.state.autoSave = !this.state.autoSave;
const btn = document.getElementById('autosave-btn');
if (this.state.autoSave) {
btn.classList.add('on');
btn.textContent = 'ON';
UI.showNotification('Autosave enabled', 'success');
} else {
btn.classList.remove('on');
btn.textContent = 'OFF';
UI.showNotification('Autosave disabled', 'info');
}
// Persist the toggle preference immediately.
localStorage.setItem('cosmicGardenAutoSave', String(this.state.autoSave));
});
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const tab = e.target.dataset.tab;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
e.target.classList.add('active');
document.getElementById(`${tab}-tab`).classList.add('active');
});
});
// Import slot selector buttons.
document.querySelectorAll('[data-import-slot]').forEach(btn => {
btn.addEventListener('click', () => {
this.importSlot = parseInt(btn.dataset.importSlot, 10);
UI.showNotification(`Importing into Slot ${this.importSlot + 1}`, 'info');
});
});
},
/**
* Harvest stardust by clicking
*/
harvest(event) {
const amount = this.getClickPower();
this.addStardust(amount);
this.state.totalClicks++;
// Audio feedback
Audio.playClick();
// Visual feedback
UI.showFloatingText(event.clientX, event.clientY, `+${this.formatNumber(amount)}`);
Particles.createClickParticles(event.clientX, event.clientY, 8);
UI.update();
},
/**
* Add stardust to the game
*/
addStardust(amount) {
this.state.stardust += amount;
this.state.totalStardust += amount;
},
/**
* Get current click power
*/
getClickPower() {
let power = 1;
// Apply upgrade bonuses
for (const upgradeId of this.state.upgrades) {
const upgrade = UPGRADES[upgradeId];
if (upgrade && upgrade.clickPowerBonus) {
power *= upgrade.clickPowerBonus;
}
}
// Apply prestige multipliers
const prestigeMultiplier = PRESTIGE.getMultiplier();
power *= prestigeMultiplier;
// Apply prestige upgrades
for (const upgradeId of (this.state.prestigeUpgrades || [])) {
const upgrade = PRESTIGE_UPGRADES[upgradeId];
if (upgrade && upgrade.effect === 'clickMultiplier') {
power *= upgrade.value;
}
}
return Math.floor(power);
},
/**
* Calculate production per second
*/
getProductionPerSecond() {
let total = 0;
for (const [buildingId, count] of Object.entries(this.state.buildings)) {
if (count === 0) continue;
const building = BUILDINGS[buildingId];
if (!building) continue;
let production = building.baseProduction * count;
// Apply prestige multiplier
const prestigeMultiplier = PRESTIGE.getMultiplier();
production *= prestigeMultiplier;
// Apply Golden Starbloom temporary production boost (+50%)
if (this.state.productionBoostUntil && this.state.productionBoostUntil > Date.now()) {
production *= 1.5;
}
// Apply prestige upgrades
for (const upgradeId of (this.state.prestigeUpgrades || [])) {
const upgrade = PRESTIGE_UPGRADES[upgradeId];
if (upgrade && upgrade.effect === 'productionMultiplier') {
production *= upgrade.value;
}
}
// Apply upgrade bonuses
for (const upgradeId of this.state.upgrades) {
const upgrade = UPGRADES[upgradeId];
if (upgrade && upgrade.buildingBonus && upgrade.buildingBonus[buildingId]) {
production *= upgrade.buildingBonus[buildingId];
}
if (upgrade && upgrade.globalBonus) {
production *= upgrade.globalBonus;
}
}
total += production;
}
return total;
},
/**
* Get building cost
*/
getBuildingCost(buildingId) {
const building = BUILDINGS[buildingId];
const count = this.state.buildings[buildingId] || 0;
let cost = Math.floor(building.baseCost * Math.pow(this.config.costScaling, count));
// Apply prestige cost reduction
for (const upgradeId of (this.state.prestigeUpgrades || [])) {
const upgrade = PRESTIGE_UPGRADES[upgradeId];
if (upgrade && upgrade.effect === 'costReduction') {
cost *= upgrade.value;
}
}
return Math.floor(cost);
},
/**
* Buy a single building
*/
buyBuilding(buildingId) {
const cost = this.getBuildingCost(buildingId);
if (this.state.stardust >= cost) {
this.state.stardust -= cost;
this.state.buildings[buildingId] = (this.state.buildings[buildingId] || 0) + 1;
UI.showNotification(`Bought ${BUILDINGS[buildingId].name}!`, 'success');
// Audio feedback
Audio.playPurchase();
// Particle effect on building purchase
const card = document.getElementById(`building-${buildingId}`);
if (card) {
const rect = card.getBoundingClientRect();
Particles.createBuildingEffect(rect.left + rect.width / 2, rect.top + rect.height / 2);
}
UI.update();
return true;
}
return false;
},
/**
* Calculate how many of a building the player can currently afford.
* Does not modify game state.
*/
getMaxAffordable(buildingId) {
const building = BUILDINGS[buildingId];
if (!building) return 0;
let stardust = this.state.stardust;
let count = 0;
let owned = this.state.buildings[buildingId] || 0;
while (true) {
const cost = Math.floor(building.baseCost * Math.pow(this.config.costScaling, owned + count));
if (stardust < cost) break;
stardust -= cost;
count++;
}
return count;
},
/**
* Buy as many of a building as the player can afford at once.
* Returns the number purchased.
*/
buyMaxBuilding(buildingId) {
const building = BUILDINGS[buildingId];
if (!building) return 0;
let count = 0;
while (this.state.stardust >= this.getBuildingCost(buildingId)) {
const cost = this.getBuildingCost(buildingId);
this.state.stardust -= cost;
this.state.buildings[buildingId] = (this.state.buildings[buildingId] || 0) + 1;
count++;
}
if (count > 0) {
UI.showNotification(`Bought ${count}x ${building.name}!`, 'success');
// Audio feedback
Audio.playPurchase();
// Particle effect on building purchase
const card = document.getElementById(`building-${buildingId}`);
if (card) {
const rect = card.getBoundingClientRect();
Particles.createBuildingEffect(rect.left + rect.width / 2, rect.top + rect.height / 2);
}
UI.update();
}
return count;
},
/**
* Buy as many of EVERY building the player can currently afford at once,
* across all building types. Returns the total number purchased. Buildings
* cost-scales independently per type, so this loops until a full pass buys
* nothing (kept for symmetry with buyAllUpgrades / buyMaxPrestigeUpgrades
* and to stay safe if future buildings gain requirement gates); in practice
* a single pass suffices. Mirrors buyMaxBuilding per building.
*/
buyAllBuildings() {
let totalBought = 0;
// Loop until a full pass purchases nothing (buying one building type
// never unlocks another, but the loop keeps us safe against future
// requirement gates and matches the bulk-buy convention elsewhere).
let progress = true;
while (progress) {
progress = false;
for (const buildingId of Object.keys(BUILDINGS)) {
const bought = this.buyMaxBuilding(buildingId);
if (bought > 0) {
totalBought += bought;
progress = true;
}
}
}
return totalBought;
},
/**
* Buy an upgrade
*/
buyUpgrade(upgradeId) {
const upgrade = UPGRADES[upgradeId];
if (!upgrade || this.state.upgrades.includes(upgradeId)) {
return false;
}
if (this.state.stardust >= upgrade.cost) {
// Check requirements
if (upgrade.requirements) {
for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) {
const currentCount = this.state.buildings[reqId] || 0;
if (currentCount < reqCount) {
UI.showNotification('Requirements not met!', 'warning');
return false;
}
}
}
this.state.stardust -= upgrade.cost;
this.state.upgrades.push(upgradeId);
UI.showNotification(`Unlocked: ${upgrade.name}!`, 'success');
UI.update();
return true;
}
return false;
},
/**
* Buy every upgrade the player can currently afford at once.
* Returns an object describing what happened: { bought: count, ids: [...] }.
* Upgrades are one-time purchases, so we loop until nothing affordable remains.
*/
buyAllUpgrades() {
const bought = [];
// Loop until a pass buys nothing (each purchase may unlock new affordability).
let progress = true;
while (progress) {
progress = false;
for (const [id, upgrade] of Object.entries(UPGRADES)) {
if (this.state.upgrades.includes(id)) continue;
// Respect requirements before attempting purchase.
if (upgrade.requirements) {
let met = true;
for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) {
if ((this.state.buildings[reqId] || 0) < reqCount) {
met = false;
break;
}
}
if (!met) continue;
}
if (this.state.stardust >= upgrade.cost) {
this.state.stardust -= upgrade.cost;
this.state.upgrades.push(id);
bought.push(id);
progress = true; // another purchase may have unlocked more affordability
}
}
}
if (bought.length > 0) {
UI.showNotification(`Bought ${bought.length} upgrade(s)!`, 'success');
Audio.playPurchase();
UI.update();
}
return { bought: bought.length, ids: bought };
},
/**
* Buy every prestige upgrade the player can currently afford at once,
* paid from Stellar Essence. Returns an object describing what happened:
* { bought: count, ids: [...] }. Prestige upgrades are one-time purchases,
* so we loop until a full pass buys nothing (purchasing one never unlocks
* another, but the loop is kept for symmetry with buyAllUpgrades and to
* stay safe should future prestige upgrades gain requirement gates).
*/
buyMaxPrestigeUpgrades() {
const bought = [];
let progress = true;
while (progress) {
progress = false;
for (const [id, upgrade] of Object.entries(PRESTIGE_UPGRADES)) {
if ((this.state.prestigeUpgrades || []).includes(id)) continue;
// Respect requirements before attempting purchase, mirroring
// PRESTIGE.buyUpgrade semantics.
if (upgrade.requirements) {
let met = true;
for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) {
if ((this.state.buildings[reqId] || 0) < reqCount) {
met = false;
break;
}
}
if (!met) continue;
}
if ((this.state.stellarEssence || 0) >= upgrade.cost) {
this.state.prestigeUpgrades = this.state.prestigeUpgrades || [];
this.state.prestigeUpgrades.push(id);
// Spend Stellar Essence, mirroring PRESTIGE.buyUpgrade.
this.state.stellarEssence -= upgrade.cost;
bought.push(id);
progress = true;
}
}
}
if (bought.length > 0) {
UI.showNotification(`Bought ${bought.length} prestige upgrade(s)!`, 'achievement');
Audio.playPurchase();
UI.update();
}
return { bought: bought.length, ids: bought };
},
/**
* Golden Starbloom - random event system.
* A rare, time-limited bloom occasionally appears on screen. Clicking it
* awards a stardust bonus and grants a temporary production boost.
*/
/**
* Seconds remaining until the current bloom vanishes (0 if none active).
*/
getGoldenBloomRemaining() {
return this.state.goldenBlooms.active ? Math.ceil(this.state.goldenBlooms.timer) : 0;
},
/**
* Award a Golden Starbloom bonus and apply the production multiplier.
* Called when the player clicks a visible bloom. Returns the amount earned.
*/
claimGoldenBloom(x, y) {
const state = this.state.goldenBlooms;
// No active bloom - nothing to claim.
if (!state.active) return 0;
state.active = false;
state.timer = 0;
state.totalClaimed++;
// Bonus: goldenBloomBonusMult x current production per second (min 1).
const bonus = Math.max(1, Math.floor(this.getProductionPerSecond() * this.config.goldenBloomBonusMult));
this.addStardust(bonus);
// Temporary production boost: +50% for 30 seconds.
this.state.productionBoostUntil = Date.now() + 30000;
UI.showNotification('⭐ Golden Starbloom! +' + this.formatNumber(bonus) + ' ✨', 'achievement');
Audio.playGoldenBloom();
Particles.createGoldenEffect(x, y);
UI.update();
return bonus;
},
/**
* Update golden bloom scheduling on each tick.
* Spawns a new bloom if none is active and enough time has elapsed;
* decrements the countdown of an active bloom until it vanishes.
*/
updateGoldenBloom(dtSeconds) {
const state = this.state.goldenBlooms;
// If a bloom is still on screen, count down its lifetime.
if (state.active) {
state.timer -= dtSeconds;
if (state.timer <= 0) {
state.active = false;
state.timer = 0;
}
return;
}
// Otherwise, check whether it is time to spawn the next bloom.
const elapsed = Date.now() - state.lastSpawn;
const minMs = this.config.goldenBloomMinMs;
const maxMs = this.config.goldenBloomMaxMs;
if (elapsed >= minMs) {
// Wait until somewhere in [min, max] before spawning.
if (elapsed >= maxMs || Math.random() < 0.05) {
state.active = true;
state.timer = this.config.goldenBloomDuration;
state.lastSpawn = Date.now();
UI.spawnGoldenBloom();
}
}
},
/**
* Whether the temporary production boost is currently active, and how
* many seconds remain.
*/
getProductionBoostInfo() {
const until = this.state.productionBoostUntil || 0;
if (until <= Date.now()) return { active: false, remaining: 0 };
return { active: true, remaining: Math.ceil((until - Date.now()) / 1000) };
},
/**
* Game tick - called every config.tickRate ms
*/
tick() {
const production = this.getProductionPerSecond();
const tickSeconds = this.config.tickRate / 1000;
if (production > 0) {
this.addStardust(production * tickSeconds);
}
// Advance the Golden Starbloom random event clock.
this.updateGoldenBloom(tickSeconds);
UI.update();
Achievements.check();
},
/**
* Start the game loop
*/
startGameLoop() {
setInterval(() => this.tick(), this.config.tickRate);
},
/**
* Start auto-save
*/
startAutoSave() {
setInterval(() => {
// Only autosave when the player has enabled it (default on).
if (!this.state.autoSave) return;
this.save();
document.getElementById('last-saved').textContent =
`Last saved: ${new Date().toLocaleTimeString()}`;
}, this.config.autoSaveInterval);
},
/**
* localStorage key for a given slot's save data.
*/
getSaveKey(slot) {
return `cosmicGardenSave-${slot}`;
},
/**
* The key holding the selected slot index across page loads.
*/
getActiveSlotKey() {
return 'cosmicGardenActiveSlot';
},
/**
* Save game state to the currently active slot.
*/
save() {
this.state.lastSaveTime = Date.now();
localStorage.setItem(this.getSaveKey(this.state.activeSlot), JSON.stringify(this.state));
},
/**
* Load game state
*/
loadState() {
// Restore the persisted selected-slot index before reading its data.
const persistedSlot = localStorage.getItem(this.getActiveSlotKey());
if (persistedSlot !== null) {
this.state.activeSlot = parseInt(persistedSlot, 10) || 0;
}
const saved = localStorage.getItem(this.getSaveKey(this.state.activeSlot));
if (saved) {
try {
const parsed = JSON.parse(saved);
this.state = { ...this.state, ...parsed };
// The explicitly-persisted slot selection always wins.
if (persistedSlot !== null) {
this.state.activeSlot = parseInt(persistedSlot, 10) || 0;
}
// Calculate offline progress
const offlineTime = (Date.now() - this.state.lastSaveTime) / 1000;
if (offlineTime > 60) {
const production = this.getProductionPerSecond();
let offlineRate = 0.5; // 50% offline efficiency
for (const upgradeId of (this.state.prestigeUpgrades || [])) {
const upgrade = PRESTIGE_UPGRADES[upgradeId];
if (upgrade && upgrade.effect === 'offlineBonus') {
offlineRate = upgrade.value;
}
}
const offlineEarnings = production * offlineTime * offlineRate;
if (offlineEarnings > 0) {
this.addStardust(offlineEarnings);
UI.showNotification(
`Welcome back! Earned ${this.formatNumber(offlineEarnings)} ✨ while away`,
'success'
);
}
}
} catch (e) {
console.error('Failed to load save:', e);
}
}
},
/**
* Load from imported data
*/
loadFromData(data) {
try {
const parsed = JSON.parse(atob(data));
this.state = { ...this.state, ...parsed };
UI.update();
} catch (e) {
UI.showNotification('Invalid save data!', 'warning');
}
},
/**
* Load the data stored in a specific slot index (without changing it).
* Returns the parsed state, or null if that slot is empty.
*/
loadSlot(slot) {
const raw = localStorage.getItem(this.getSaveKey(slot));
if (!raw) return null;
try {
return JSON.parse(raw);
} catch (e) {
console.error(`Failed to read slot ${slot}:`, e);
return null;
}
},
/**
* Build a lightweight preview of a given save slot WITHOUT mutating the
* live game state. Returns an object describing what that slot holds:
* - present: whether the slot has any saved data
* - stardust: current (live) stardust production/sec for the slot
* - totalStardust: lifetime stardust earned in the slot
* - buildings: total number of buildings owned in the slot
* - achievements: count/total of achievements unlocked in the slot
* - prestigeCount: how many times the slot has been prestiged
* - lastSaved: human-readable "time ago" string for the slot's save time
* An empty/unreadable slot returns present:false with zeroed fields.
*/
getSlotPreview(slot) {
const data = this.loadSlot(slot);
if (!data) {
return {
present: false,
stardust: 0,
totalStardust: 0,
buildings: 0,
achievements: [0, 33],
prestigeCount: 0,
lastSaved: 'never'
};
}
const buildingCount = Object.values(data.buildings || {})
.reduce((sum, c) => sum + (Number(c) || 0), 0);
let timeAgo = 'never';
if (typeof data.lastSaveTime === 'number') {
const secs = Math.max(0, Math.floor((Date.now() - data.lastSaveTime) / 1000));
timeAgo = this.formatTimeAgo(secs);
}
return {
present: true,
stardust: Number(data.stardust) || 0,
totalStardust: Number(data.totalStardust) || 0,
buildings: buildingCount,
achievements: [
Array.isArray(data.achievements) ? data.achievements.length : 0,
33
],
prestigeCount: Number(data.prestigeCount) || 0,
lastSaved: timeAgo
};
},
/**
* Format a number of seconds as a compact "time ago" string
* (e.g. "just now", "1m ago", "3h ago", "2d ago"). Used by slot previews.
*/
formatTimeAgo(secs) {
if (secs < 5) return 'just now';
// Largest units first; each entry is [thresholdSeconds, label].
const buckets = [
[365 * 86400, 'y'],
[30 * 86400, 'mo'],
[86400, 'd'],
[3600, 'h'],
[60, 'm']
];
for (const [threshold, label] of buckets) {
if (secs >= threshold) {
const value = Math.floor(secs / threshold);
return `${value}${label} ago`;
}
}
// Below one minute.
return `${Math.max(1, secs)}s ago`;
},
/**
* Switch the active save slot. Persists the selection and reloads that
* slot's data from localStorage into the live game state, preserving
* prestige-related counters that should carry across slots only when
* present in the target slot.
*/
switchSlot(slot) {
slot = ((parseInt(slot, 10) || 0) + this.config.SLOTS) % this.config.SLOTS;
// Persist the selection first so loadState() reads it back.
localStorage.setItem(this.getActiveSlotKey(), String(slot));
this.state.activeSlot = slot;
const data = this.loadSlot(slot);
if (data) {
try {
const parsed = JSON.parse(JSON.stringify(data));
this.state = { ...this.state, ...parsed };
// Ensure the slot index stays consistent.
this.state.activeSlot = slot;
UI.showNotification(
`Saved to Slot ${slot + 1}!`, 'success'
);
} catch (e) {
console.error('Failed to load selected slot:', e);
UI.showNotification('Could not load that slot!', 'warning');
}
} else {
// Target slot is empty: reset to a fresh start but keep essence.
this.state = {
stardust: 0,
totalStardust: 0,
totalClicks: 0,
clickPower: 1,
startTime: Date.now(),
lastSaveTime: Date.now(),
buildings: {},
upgrades: [],
achievements: [],
stellarEssence: this.state.stellarEssence || 0,
prestigeCount: this.state.prestigeCount || 0,
totalPrestigeEssence: this.state.totalPrestigeEssence || 0,
prestigeUpgrades: this.state.prestigeUpgrades || [],
goldenBlooms: { active: false, timer: 0, lastSpawn: Date.now(), totalClaimed: 0 },
// Reset the temporary golden-bloom production boost so a boost
// from a previous slot cannot leak into a fresh slot.
productionBoostUntil: 0,
activeSlot: slot,
autoSave: this.state.autoSave
};
UI.showNotification(
`Empty Slot ${slot + 1} - new game started!`, 'success'
);
}
// Re-render dynamic UI and refresh the golden-bloom badge.
UI.init();
UI.update();
},
/**
* Export a single slot's save as base64 (for backup or transfer).
*/
exportSlotToBase64(slot) {
const data = this.loadSlot(slot);
if (!data) {
UI.showNotification(`Slot ${slot + 1} is empty!`, 'warning');
return null;
}
const encoded = btoa(JSON.stringify(data));
navigator.clipboard.writeText(encoded).then(() => {
UI.showNotification(`Slot ${slot + 1} copied to clipboard!`, 'success');
});
return encoded;
},
/**
* Import base64 save data into a chosen slot.
*/
importSlotFromData(data, slot) {
try {
const parsed = JSON.parse(atob(data));
localStorage.setItem(this.getSaveKey(slot), JSON.stringify(parsed));
UI.showNotification(`Imported into Slot ${slot + 1}!`, 'success');
// If we imported into the active slot, reload it.
if (parseInt(slot, 10) === this.state.activeSlot) {
this.switchSlot(slot);
} else {
UI.update();
}
return true;
} catch (e) {
UI.showNotification('Invalid save data!', 'warning');
return false;
}
},
/**
* Export save as base64
*/
exportSave() {
this.save();
const data = btoa(JSON.stringify(this.state));
navigator.clipboard.writeText(data).then(() => {
UI.showNotification('Save copied to clipboard!', 'success');
});
},
/**
* Reset game
*/
reset() {
localStorage.removeItem('cosmicGardenSave');
this.state = {
stardust: 0,
totalStardust: 0,
totalClicks: 0,
clickPower: 1,
startTime: Date.now(),
lastSaveTime: Date.now(),
buildings: {},
upgrades: [],
achievements: [],
stellarEssence: 0,
prestigeCount: 0,
totalPrestigeEssence: 0,
prestigeUpgrades: [],
goldenBlooms: { active: false, timer: 0, lastSpawn: Date.now(), totalClaimed: 0 },
activeSlot: this.state.activeSlot,
autoSave: this.state.autoSave
};
UI.update();
UI.showNotification('Game reset!', 'warning');
},
/**
* Format numbers with commas
*/
formatNumber(num) {
if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T';
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
return Math.floor(num).toLocaleString();
},
/**
* Create ripple effect on button click
*/
createRipple(button, event) {
const ripple = document.createElement('span');
ripple.className = 'ripple';
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = `${size}px`;
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
button.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
},
/**
* Animate click button with glow effect
*/
animateClickButton(button) {
button.classList.add('clicked');
setTimeout(() => button.classList.remove('clicked'), 200);
}
};
// Make Game globally accessible
window.Game = Game;