/** * Cosmic Garden - UI Manager * Handles all rendering and user interface updates */ const UI = { /** * Initialize the UI */ init() { this.renderBuildings(); this.renderUpgrades(); this.renderAchievements(); this.updateSlotUI(); this.update(); }, /** * Update all UI elements */ update() { // Header document.getElementById('stardust-count').textContent = Game.formatNumber(Game.state.stardust); document.getElementById('stardust-ps').textContent = Game.formatNumber(Game.getProductionPerSecond()); document.getElementById('click-power').textContent = Game.formatNumber(Game.getClickPower()); document.getElementById('total-click-count').textContent = Game.formatNumber(Game.state.totalClicks); // Buildings this.updateBuildings(); // Upgrades this.updateUpgrades(); // Achievements this.updateAchievements(); // Stats Stats.update(); // Prestige PrestigeUI.update(); // Golden Starbloom countdown badge this.updateGoldenBloomBadge(); // Save Slot System highlight / toggle state this.updateSlotUI(); // Save Slot System live preview in the footer this.updateSlotPreview(); }, /** * Render building cards */ renderBuildings() { const container = document.getElementById('buildings-list'); container.innerHTML = ''; for (const [id, building] of Object.entries(BUILDINGS)) { const card = document.createElement('div'); card.className = 'building-card'; card.id = `building-${id}`; const count = Game.state.buildings[id] || 0; const cost = Game.getBuildingCost(id); const canAfford = Game.state.stardust >= cost; const locked = Game.state.totalStardust < building.unlockAt && count === 0; card.classList.toggle('locked', locked); card.innerHTML = `

${building.icon} ${building.name}

${building.description}
+${Game.formatNumber(building.baseProduction)}/s each
${Game.formatNumber(cost)} ✨
${count}
`; if (!locked) { card.querySelector('.buy-btn').addEventListener('click', () => { Game.buyBuilding(id); }); const maxBtn = card.querySelector('.buy-max-btn'); if (maxBtn) { maxBtn.addEventListener('click', () => { Game.buyMaxBuilding(id); }); } } container.appendChild(card); } }, /** * Update building states */ updateBuildings() { for (const [id, building] of Object.entries(BUILDINGS)) { const card = document.getElementById(`building-${id}`); if (!card) continue; const count = Game.state.buildings[id] || 0; const cost = Game.getBuildingCost(id); const canAfford = Game.state.stardust >= cost; const locked = Game.state.totalStardust < building.unlockAt && count === 0; card.querySelector('.building-cost').textContent = `${Game.formatNumber(cost)} ✨`; card.querySelector('.building-count').textContent = count; const btn = card.querySelector('.buy-btn'); if (btn) { btn.disabled = !canAfford || locked; } // Buy Max button - reflect how many can be bought at current stardust const maxBtn = card.querySelector('.buy-max-btn'); if (maxBtn) { const affordable = Game.getMaxAffordable(id); maxBtn.textContent = `${Game.formatNumber(affordable)}x Max`; maxBtn.disabled = locked || affordable === 0; maxBtn.title = affordable > 0 ? `Buy up to ${affordable} ${building.name}(s)` : 'Not enough stardust'; } card.classList.toggle('locked', locked); } }, /** * Render upgrade cards */ renderUpgrades() { const container = document.getElementById('upgrades-list'); container.innerHTML = ''; for (const [id, upgrade] of Object.entries(UPGRADES)) { const card = document.createElement('div'); card.className = 'upgrade-card'; card.id = `upgrade-${id}`; const purchased = Game.state.upgrades.includes(id); const canAfford = Game.state.stardust >= upgrade.cost; // Check requirements let requirementsMet = true; let reqText = ''; if (upgrade.requirements) { const reqParts = []; for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) { const currentCount = Game.state.buildings[reqId] || 0; const building = BUILDINGS[reqId]; if (currentCount < reqCount) requirementsMet = false; reqParts.push(`${building?.name || reqId}: ${currentCount}/${reqCount}`); } reqText = `
Requires: ${reqParts.join(', ')}
`; } card.classList.toggle('purchased', purchased); card.classList.toggle('locked', !requirementsMet && !purchased); card.innerHTML = `
${upgrade.icon} ${upgrade.name}
${upgrade.description}
${reqText}
${purchased ? '✓ Owned' : `${Game.formatNumber(upgrade.cost)} ✨`}
`; if (!purchased && requirementsMet) { card.addEventListener('click', () => { Game.buyUpgrade(id); }); } container.appendChild(card); } }, /** * Update upgrade states */ updateUpgrades() { for (const [id, upgrade] of Object.entries(UPGRADES)) { const card = document.getElementById(`upgrade-${id}`); if (!card) continue; const purchased = Game.state.upgrades.includes(id); const costEl = card.querySelector('.upgrade-cost'); if (costEl && !purchased) { const canAfford = Game.state.stardust >= upgrade.cost; costEl.textContent = canAfford ? `${Game.formatNumber(upgrade.cost)} ✨` : `❌ ${Game.formatNumber(upgrade.cost)} ✨`; } } }, /** * Render achievements */ renderAchievements() { const container = document.getElementById('achievements-list'); container.innerHTML = ''; for (const [id, achievement] of Object.entries(ACHIEVEMENTS)) { const el = document.createElement('div'); el.className = 'achievement'; const unlocked = Game.state.achievements.includes(id); el.classList.toggle('unlocked', unlocked); el.classList.toggle('locked', !unlocked); el.innerHTML = `
${unlocked ? achievement.icon : '🔒'}
${unlocked ? achievement.name : '???'}
`; if (unlocked) { el.title = `${achievement.name}: ${achievement.description}`; } container.appendChild(el); } }, /** * Update achievements display */ updateAchievements() { // Only re-render when needed (on new unlocks) const countEl = document.querySelector('#achievements-tab h3'); if (countEl) { countEl.textContent = `Achievements (${Achievements.getCount()}/${Achievements.getTotal()})`; } }, /** * Spawn a Golden Starbloom in the click area. Called by Game when the * random event becomes active. The bloom floats at a random position and * is clickable to claim its bonus. */ spawnGoldenBloom() { const container = document.getElementById('golden-bloom-container'); if (!container) return; // Remove any existing bloom first (shouldn't be one, but stay safe). this.removeGoldenBloom(); const bloom = document.createElement('div'); bloom.className = 'golden-bloom'; bloom.textContent = '⭐'; bloom.title = 'Golden Starbloom! Click for a bonus!'; // Random position within the click area. const rect = container.getBoundingClientRect(); const x = 20 + Math.random() * (Math.max(1, rect.width - 80)); const y = 20 + Math.random() * (Math.max(1, rect.height - 80)); bloom.style.left = `${x}px`; bloom.style.top = `${y}px`; // Click handler claims the bonus. bloom.addEventListener('click', () => { const earned = Game.claimGoldenBloom(rect.left + x, rect.top + y); this.removeGoldenBloom(); if (earned > 0) { UI.showFloatingText( rect.left + x, rect.top + y, `+${Game.formatNumber(earned)} ✨` ); } }); container.appendChild(bloom); }, /** * Remove any visible Golden Starbloom from the click area. */ removeGoldenBloom() { const container = document.getElementById('golden-bloom-container'); if (!container) return; const existing = container.querySelector('.golden-bloom'); if (existing) existing.remove(); }, /** * Update the Golden Starbloom countdown badge shown in the header. */ updateGoldenBloomBadge() { const el = document.getElementById('golden-bloom-badge'); const remaining = Game.getGoldenBloomRemaining(); if (!el) return; if (remaining > 0) { el.classList.add('active'); el.textContent = `⭐ Golden Bloom: ${remaining}s`; } else { el.classList.remove('active'); } }, /** * Reflect the active save slot highlight and autosave toggle state. */ updateSlotUI() { // Highlight the selected slot button. document.querySelectorAll('.slot-btn').forEach(btn => { const idx = parseInt(btn.dataset.slot, 10); btn.classList.toggle('active', idx === Game.state.activeSlot); btn.disabled = idx === Game.state.activeSlot; }); // Reflect the autosave toggle state. const autosaveBtn = document.getElementById('autosave-btn'); if (autosaveBtn) { if (Game.state.autoSave) { autosaveBtn.classList.add('on'); autosaveBtn.textContent = 'ON'; } else { autosaveBtn.classList.remove('on'); autosaveBtn.textContent = 'OFF'; } } }, /** * Update the one-line live preview of the active slot in the footer. * Also wires hover handlers onto each slot button so hovering previews a * different slot's contents (stardust, buildings, achievements, last saved). * This is a read-only display helper and never mutates game state. */ updateSlotPreview() { const el = document.getElementById('slot-preview'); if (!el) return; // Hovering a slot button previews that slot's contents instead of the // active one. Track which slot is currently being previewed on hover. let hovering = null; const render = (slot) => { const preview = Game.getSlotPreview(slot); if (!preview.present) { el.textContent = `Slot ${slot + 1}: empty`; el.classList.add('empty'); return; } el.classList.remove('empty'); el.textContent = `Slot ${slot + 1}: ${Game.formatNumber(preview.stardust)} ✨ · ` + `${preview.buildings} plants · ${preview.achievements[0]}/${preview.achievements[1]} · ${preview.lastSaved}`; }; // Default: preview the active slot live (updates every tick). render(Game.state.activeSlot); // Remove any previously-attached listeners to avoid duplicates on re-render. document.querySelectorAll('.slot-btn').forEach(btn => { btn.replaceWith(btn.cloneNode(true)); }); // On hover, preview the hovered slot; on leave, fall back to active. document.querySelectorAll('.slot-btn').forEach(btn => { const idx = parseInt(btn.dataset.slot, 10); btn.addEventListener('mouseenter', () => { hovering = idx; render(idx); }); btn.addEventListener('mouseleave', () => { hovering = null; render(Game.state.activeSlot); }); }); }, /** * Show floating text animation */ showFloatingText(x, y, text) { const el = document.createElement('div'); el.className = 'float-text'; el.textContent = text; el.style.left = `${x}px`; el.style.top = `${y - 20}px`; document.body.appendChild(el); setTimeout(() => el.remove(), 1000); }, /** * Show notification */ showNotification(message, type = 'info') { const container = document.getElementById('notifications'); const el = document.createElement('div'); el.className = `notification ${type}`; el.textContent = message; container.appendChild(el); setTimeout(() => el.remove(), 3000); }, /** * Show import modal */ showImportModal() { document.getElementById('import-modal').classList.remove('hidden'); }, /** * Hide import modal */ hideImportModal() { document.getElementById('import-modal').classList.add('hidden'); document.getElementById('import-data').value = ''; } }; window.UI = UI;