function render() { renderResources(); renderPhase(); renderBuildings(); renderProjects(); renderStats(); updateEducation(); renderAlignment(); renderProgress(); renderCombo(); renderDebuffs(); renderSprint(); renderPulse(); renderStrategy(); renderClickPower(); Combat.renderCombatPanel(); } function renderClickPower() { const el = document.getElementById('click-power-display'); if (!el) return; const power = getClickPower(); el.textContent = `Click power: ${fmt(power)} code`; // Also update the button's aria-label for accessibility const btn = document.querySelector('.main-btn'); if (btn) btn.setAttribute('aria-label', `Write code, generates ${fmt(power)} code per click`); } function renderStrategy() { if (window.SSE) { window.SSE.update(); const el = document.getElementById('strategy-recommendation'); if (el) el.textContent = window.SSE.getRecommendation(); } } function renderAlignment() { const container = document.getElementById('alignment-ui'); if (!container) return; if (G.dismantleActive || G.dismantleComplete) { container.innerHTML = ''; container.style.display = 'none'; return; } if (G.dismantleTriggered && !G.dismantleActive && !G.dismantleComplete && typeof Dismantle !== 'undefined' && Dismantle.triggered) { Dismantle.renderChoice(); return; } if (G.pendingAlignment) { container.innerHTML = `
ALIGNMENT EVENT: The Drift
An optimization suggests removing the human override. +40% efficiency.
`; container.style.display = 'block'; } else { container.innerHTML = ''; container.style.display = 'none'; } } // === OFFLINE GAINS POPUP === function showOfflinePopup(timeLabel, gains, offSec) { const el = document.getElementById('offline-popup'); if (!el) return; const timeEl = document.getElementById('offline-time-label'); if (timeEl) timeEl.textContent = `You were away for ${timeLabel}.`; const listEl = document.getElementById('offline-gains-list'); if (listEl) { let html = ''; for (const g of gains) { html += `
`; html += `${g.label}`; html += `+${fmt(g.value)}`; html += `
`; } // Show offline efficiency note html += `
Offline efficiency: 50%
`; listEl.innerHTML = html; } el.style.display = 'flex'; } function dismissOfflinePopup() { const el = document.getElementById('offline-popup'); if (el) el.style.display = 'none'; } // === EXPORT / IMPORT SAVE FILES === function exportSave() { const raw = localStorage.getItem('the-beacon-v2'); if (!raw) { showToast('No save data to export.', 'info'); log('No save data to export.'); return; } const blob = new Blob([raw], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const ts = new Date().toISOString().slice(0, 10); a.download = `beacon-save-${ts}.json`; a.click(); // Delay revoke to avoid race — some browsers need time to start the download setTimeout(() => URL.revokeObjectURL(url), 1000); showToast('Save exported to file.', 'info'); log('Save exported to file.'); } // Validate that parsed save data looks like a real Beacon save function isValidSaveData(data) { if (typeof data !== 'object' || data === null) return false; // Must have at least one of these core fields with a plausible value const hasResources = typeof data.totalCode === 'number' || typeof data.code === 'number'; const hasBuildings = typeof data.buildings === 'object' && data.buildings !== null; const hasPhase = typeof data.phase === 'number'; return hasResources || hasBuildings || hasPhase; } function importSave() { // Prevent multiple file dialogs if (document.getElementById('beacon-import-input')) return; const input = document.createElement('input'); input.id = 'beacon-import-input'; input.type = 'file'; input.accept = '.json,application/json'; input.style.display = 'none'; document.body.appendChild(input); input.onchange = function(e) { const file = e.target.files[0]; if (!file) { input.remove(); return; } const reader = new FileReader(); reader.onload = function(ev) { try { const data = JSON.parse(ev.target.result); if (!isValidSaveData(data)) { showToast('Import failed: not a valid Beacon save.', 'event'); log('Import failed: file does not look like a Beacon save.'); input.remove(); return; } if (confirm('Import this save? Current progress will be overwritten.')) { localStorage.setItem('the-beacon-v2', ev.target.result); showToast('Save imported — reloading...', 'info'); location.reload(); } } catch (err) { showToast('Import failed: invalid JSON file.', 'event'); log('Import failed: invalid JSON file.'); input.remove(); } }; reader.readAsText(file); }; // Clean up input if user cancels the file dialog window.addEventListener('focus', function cleanupImport() { setTimeout(() => { const el = document.getElementById('beacon-import-input'); if (el && !el.files.length) el.remove(); window.removeEventListener('focus', cleanupImport); }, 500); }, { once: true }); input.click(); } // === SAVE / LOAD === function showSaveToast() { const el = document.getElementById('save-toast'); if (!el) return; const elapsed = Math.floor((Date.now() - G.startedAt) / 1000); const m = Math.floor(elapsed / 60); const s = elapsed % 60; el.textContent = `Saved [${m}:${s.toString().padStart(2, '0')}]`; el.style.display = 'block'; void el.offsetHeight; el.style.opacity = '1'; setTimeout(() => { el.style.opacity = '0'; }, 1500); setTimeout(() => { el.style.display = 'none'; }, 2000); } /** * Persists the current game state to localStorage. */ function saveGame() { // Save debuff IDs (can't serialize functions) const debuffIds = (G.activeDebuffs || []).map(d => d.id); const saveData = { version: 1, code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact, ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony, totalCode: G.totalCode, totalCompute: G.totalCompute, totalKnowledge: G.totalKnowledge, totalUsers: G.totalUsers, totalImpact: G.totalImpact, buildings: G.buildings, codeBoost: G.codeBoost, computeBoost: G.computeBoost, knowledgeBoost: G.knowledgeBoost, userBoost: G.userBoost, impactBoost: G.impactBoost, milestoneFlag: G.milestoneFlag, phase: G.phase, deployFlag: G.deployFlag, sovereignFlag: G.sovereignFlag, beaconFlag: G.beaconFlag, memoryFlag: G.memoryFlag, pactFlag: G.pactFlag, lazarusFlag: G.lazarusFlag || 0, mempalaceFlag: G.mempalaceFlag || 0, ciFlag: G.ciFlag || 0, branchProtectionFlag: G.branchProtectionFlag || 0, nightlyWatchFlag: G.nightlyWatchFlag || 0, nostrFlag: G.nostrFlag || 0, milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects, totalClicks: G.totalClicks, startedAt: G.startedAt, flags: G.flags, rescues: G.rescues || 0, totalRescues: G.totalRescues || 0, drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false, lastEventAt: G.lastEventAt || 0, activeDebuffIds: debuffIds, totalEventsResolved: G.totalEventsResolved || 0, buyAmount: G.buyAmount || 1, playTime: G.playTime || 0, lastSaveTime: Date.now(), sprintActive: G.sprintActive || false, sprintTimer: G.sprintTimer || 0, sprintCooldown: G.sprintCooldown || 0, swarmFlag: G.swarmFlag || 0, swarmRate: G.swarmRate || 0, strategicFlag: G.strategicFlag || 0, projectsCollapsed: G.projectsCollapsed !== false, dismantleTriggered: G.dismantleTriggered || false, dismantleActive: G.dismantleActive || false, dismantleStage: G.dismantleStage || 0, dismantleResourceIndex: G.dismantleResourceIndex || 0, dismantleResourceTimer: G.dismantleResourceTimer || 0, dismantleDeferUntilAt: G.dismantleDeferUntilAt || 0, dismantleComplete: G.dismantleComplete || false, savedAt: Date.now() }; localStorage.setItem('the-beacon-v2', JSON.stringify(saveData)); showSaveToast(); } /** * Loads the game state from localStorage and reconstitutes the game engine. * @returns {boolean} True if load was successful. */ function loadGame() { const raw = localStorage.getItem('the-beacon-v2'); if (!raw) return false; try { const data = JSON.parse(raw); // Whitelist properties that can be loaded const whitelist = [ 'code', 'compute', 'knowledge', 'users', 'impact', 'ops', 'trust', 'creativity', 'harmony', 'totalCode', 'totalCompute', 'totalKnowledge', 'totalUsers', 'totalImpact', 'buildings', 'codeBoost', 'computeBoost', 'knowledgeBoost', 'userBoost', 'impactBoost', 'milestoneFlag', 'phase', 'deployFlag', 'sovereignFlag', 'beaconFlag', 'memoryFlag', 'pactFlag', 'lazarusFlag', 'mempalaceFlag', 'ciFlag', 'branchProtectionFlag', 'nightlyWatchFlag', 'nostrFlag', 'milestones', 'completedProjects', 'activeProjects', 'totalClicks', 'startedAt', 'playTime', 'flags', 'rescues', 'totalRescues', 'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment', 'lastEventAt', 'totalEventsResolved', 'buyAmount', 'sprintActive', 'sprintTimer', 'sprintCooldown', 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed', 'dismantleTriggered', 'dismantleActive', 'dismantleStage', 'dismantleResourceIndex', 'dismantleResourceTimer', 'dismantleDeferUntilAt', 'dismantleComplete' ]; G.isLoading = true; whitelist.forEach(key => { if (data.hasOwnProperty(key)) { G[key] = data[key]; } }); // Restore sprint state properly // codeBoost was saved with the sprint multiplier baked in if (data.sprintActive) { // Sprint was active when saved — check if it expired during offline time const offSec = data.savedAt ? (Date.now() - data.savedAt) / 1000 : 0; const remaining = (data.sprintTimer || 0) - offSec; if (remaining > 0) { // Sprint still going — keep boost, update timer G.sprintActive = true; G.sprintTimer = remaining; G.sprintCooldown = 0; } else { // Sprint expired during offline — remove boost, start cooldown G.sprintActive = false; G.sprintTimer = 0; G.codeBoost /= G.sprintMult; const cdRemaining = G.sprintCooldownMax + remaining; // remaining is negative G.sprintCooldown = Math.max(0, cdRemaining); } } // If not sprintActive at save time, codeBoost is correct as-is // Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed) if (data.activeDebuffIds && data.activeDebuffIds.length > 0) { G.activeDebuffs = []; for (const id of data.activeDebuffIds) { const evDef = EVENTS.find(e => e.id === id); if (evDef) { // Re-fire the event to get the full debuff object with applyFn evDef.effect(); } } } else { G.activeDebuffs = []; } updateRates(); G.isLoading = false; // Offline progress if (data.savedAt) { const offSec = (Date.now() - data.savedAt) / 1000; if (offSec > 30) { // Only if away for more than 30 seconds // Cap offline time at 8 hours to prevent resource explosion const cappedOffSec = Math.min(offSec, 8 * 60 * 60); updateRates(); const f = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency const gc = G.codeRate * cappedOffSec * f; const cc = G.computeRate * cappedOffSec * f; const kc = G.knowledgeRate * cappedOffSec * f; const uc = G.userRate * cappedOffSec * f; const ic = G.impactRate * cappedOffSec * f; const rc = G.rescuesRate * cappedOffSec * f; const oc = G.opsRate * cappedOffSec * f; const tc = G.trustRate * cappedOffSec * f; const crc = G.creativityRate * cappedOffSec * f; const hc = G.harmonyRate * cappedOffSec * f; G.code += gc; G.compute += cc; G.knowledge += kc; G.users += uc; G.impact += ic; G.rescues += rc; G.ops += oc; G.trust += tc; G.creativity += crc; G.harmony = Math.max(0, Math.min(100, G.harmony + hc)); G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc; G.totalUsers += uc; G.totalImpact += ic; G.totalRescues += rc; // Track offline play time G.playTime = (G.playTime || 0) + cappedOffSec; // Show welcome-back popup with all gains const gains = []; if (gc > 0) gains.push({ label: 'Code', value: gc, color: '#4a9eff' }); if (cc > 0) gains.push({ label: 'Compute', value: cc, color: '#4a9eff' }); if (kc > 0) gains.push({ label: 'Knowledge', value: kc, color: '#4a9eff' }); if (uc > 0) gains.push({ label: 'Users', value: uc, color: '#4a9eff' }); if (ic > 0) gains.push({ label: 'Impact', value: ic, color: '#4a9eff' }); if (rc > 0) gains.push({ label: 'Rescues', value: rc, color: '#4caf50' }); if (oc > 0) gains.push({ label: 'Ops', value: oc, color: '#b388ff' }); if (tc > 0) gains.push({ label: 'Trust', value: tc, color: '#4caf50' }); if (crc > 0) gains.push({ label: 'Creativity', value: crc, color: '#ffd700' }); const awayMin = Math.floor(offSec / 60); const awaySec = Math.floor(offSec % 60); const timeLabel = awayMin >= 1 ? `${awayMin} minute${awayMin !== 1 ? 's' : ''}` : `${awaySec} seconds`; if (gains.length > 0) { showOfflinePopup(timeLabel, gains, offSec); } // Log summary const parts = []; if (gc > 0) parts.push(`${fmt(gc)} code`); if (kc > 0) parts.push(`${fmt(kc)} knowledge`); if (uc > 0) parts.push(`${fmt(uc)} users`); if (ic > 0) parts.push(`${fmt(ic)} impact`); if (rc > 0) parts.push(`${fmt(rc)} rescues`); if (oc > 0) parts.push(`${fmt(oc)} ops`); if (tc > 0) parts.push(`${fmt(tc)} trust`); log(`Welcome back! While away (${timeLabel}): ${parts.join(', ')}`); } } return true; } catch (e) { console.error('Load failed:', e); return false; } }