/** * 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;