/** * 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: [], // 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 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() { // 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) { this.loadFromData(data); UI.hideImportModal(); UI.showNotification('Save imported!', 'success'); } }); document.getElementById('cancel-import').addEventListener('click', () => { UI.hideImportModal(); }); document.getElementById('reset-btn').addEventListener('click', () => { if (confirm('Are you sure you want to reset all progress? This cannot be undone!')) { this.reset(); } }); // 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'); }); }); }, /** * 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 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; }, /** * 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(() => { this.save(); document.getElementById('last-saved').textContent = `Last saved: ${new Date().toLocaleTimeString()}`; }, this.config.autoSaveInterval); }, /** * Save game state */ save() { this.state.lastSaveTime = Date.now(); localStorage.setItem('cosmicGardenSave', JSON.stringify(this.state)); }, /** * Load game state */ loadState() { const saved = localStorage.getItem('cosmicGardenSave'); if (saved) { try { const parsed = JSON.parse(saved); this.state = { ...this.state, ...parsed }; // 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'); } }, /** * 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: [] }; 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;