function updateRates() { // Reset all rates G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0; G.userRate = 0; G.impactRate = 0; G.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0; G.creativityRate = 0; G.harmonyRate = 0; // Apply building rates for (const def of BDEF) { const count = G.buildings[def.id] || 0; if (count > 0 && def.rates) { for (const [resource, baseRate] of Object.entries(def.rates)) { if (resource === 'code') G.codeRate += baseRate * count * G.codeBoost; else if (resource === 'compute') G.computeRate += baseRate * count * G.computeBoost; else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost; else if (resource === 'user') G.userRate += baseRate * count * G.userBoost; else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost; else if (resource === 'rescues') G.rescuesRate += baseRate * count * G.impactBoost; else if (resource === 'ops') G.opsRate += baseRate * count; else if (resource === 'trust') G.trustRate += baseRate * count; else if (resource === 'creativity') G.creativityRate += baseRate * count; } } } // Passive generation G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT); if (G.flags && G.flags.creativity) { G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT); } if (G.pactFlag) G.trustRate += 2; // Harmony: each wizard building contributes or detracts const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) + (G.buildings.timmy || 0) + (G.buildings.fenrir || 0) + (G.buildings.bilbo || 0); // Store harmony breakdown for tooltip G.harmonyBreakdown = []; if (wizardCount > 0) { // Baseline harmony drain from complexity const drain = -CONFIG.HARMONY_DRAIN_PER_WIZARD * wizardCount; G.harmonyRate = drain; G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain }); // The Pact restores harmony if (G.pactFlag) { const pact = CONFIG.PACT_HARMONY_GAIN * wizardCount; G.harmonyRate += pact; G.harmonyBreakdown.push({ label: 'The Pact', value: pact }); } // Nightly Watch restores harmony if (G.nightlyWatchFlag) { const watch = CONFIG.WATCH_HARMONY_GAIN * wizardCount; G.harmonyRate += watch; G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch }); } // MemPalace restores harmony if (G.mempalaceFlag) { const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount; G.harmonyRate += mem; G.harmonyBreakdown.push({ label: 'MemPalace', value: mem }); } } // Active debuffs affecting harmony if (G.activeDebuffs) { for (const d of G.activeDebuffs) { if (d.id === 'community_drama') { G.harmonyBreakdown.push({ label: 'Community Drama', value: -0.5 }); } } } // Timmy multiplier based on harmony if (G.buildings.timmy > 0) { const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50)); const timmyCount = G.buildings.timmy; G.codeRate += 5 * timmyCount * (timmyMult - 1); G.computeRate += 2 * timmyCount * (timmyMult - 1); G.knowledgeRate += 2 * timmyCount * (timmyMult - 1); G.userRate += 5 * timmyCount * (timmyMult - 1); } // Bilbo randomness: flags are set per-tick in tick(), not here // updateRates() is called from many non-tick contexts (buy, resolve, sprint) if (G.buildings.bilbo > 0) { if (G.bilboBurstActive) { G.creativityRate += 50 * G.buildings.bilbo; } if (G.bilboVanishActive) { G.creativityRate = 0; } } // Allegro requires trust if (G.buildings.allegro > 0 && G.trust < 5) { const allegroCount = G.buildings.allegro; G.knowledgeRate -= 10 * allegroCount; // Goes idle } // Swarm Protocol: buildings auto-code based on click power if (G.swarmFlag === 1) { const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0); const clickPower = getClickPower(); G.swarmRate = totalBuildings * clickPower * 0.01; G.codeRate += G.swarmRate; } // Apply persistent debuffs from active events if (G.activeDebuffs && G.activeDebuffs.length > 0) { for (const debuff of G.activeDebuffs) { if (debuff.applyFn) debuff.applyFn(); } } } // === CORE FUNCTIONS === /** * Check if player has reached the ReCKoning endgame. * Conditions: totalRescues >= 100000, pactFlag === 1, harmony > 50 */ function isEndgame() { return G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50; } /** * Main game loop tick, called every 100ms. */ function tick() { const dt = 1 / 10; // 100ms tick // If game has ended (drift ending), stop ticking if (!G.running) return; // Apply production G.code += G.codeRate * dt; G.compute += G.computeRate * dt; G.knowledge += G.knowledgeRate * dt; G.users += G.userRate * dt; G.impact += G.impactRate * dt; G.rescues += G.rescuesRate * dt; G.ops += G.opsRate * dt; G.trust += G.trustRate * dt; // NOTE: creativity is added conditionally below (only when ops near max) G.harmony += G.harmonyRate * dt; G.harmony = Math.max(0, Math.min(100, G.harmony)); // Clamp resources to prevent negative values from debuffs/Fenrir drain G.ops = Math.max(0, G.ops); G.trust = Math.max(0, G.trust); G.compute = Math.max(0, G.compute); // Track totals G.totalCode += G.codeRate * dt; G.totalCompute += G.computeRate * dt; G.totalKnowledge += G.knowledgeRate * dt; G.totalUsers += G.userRate * dt; G.totalImpact += G.impactRate * dt; G.totalRescues += G.rescuesRate * dt; // Track maxes G.maxCode = Math.max(G.maxCode, G.code); G.maxCompute = Math.max(G.maxCompute, G.compute); G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge); G.maxUsers = Math.max(G.maxUsers, G.users); G.maxImpact = Math.max(G.maxImpact, G.impact); G.maxRescues = Math.max(G.maxRescues, G.rescues); G.maxTrust = Math.max(G.maxTrust, G.trust); G.maxOps = Math.max(G.maxOps, G.ops); G.maxHarmony = Math.max(G.maxHarmony, G.harmony); // Creativity generates only when ops at max if (G.flags && G.flags.creativity && G.creativityRate > 0 && G.ops >= G.maxOps * 0.9) { G.creativity += G.creativityRate * dt; } // Ops overflow: auto-convert excess ops to code when near cap // Prevents ops from sitting idle at max — every operation becomes code if (G.ops > G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD) { const overflowDrain = Math.min(CONFIG.OPS_OVERFLOW_DRAIN_RATE * dt, G.ops - G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD); G.ops -= overflowDrain; const codeGain = overflowDrain * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost; G.code += codeGain; G.totalCode += codeGain; G.opsOverflowActive = true; } else { G.opsOverflowActive = false; } G.tick += dt; // Bilbo randomness: roll once per tick if (G.buildings.bilbo > 0) { G.bilboBurstActive = Math.random() < CONFIG.BILBO_BURST_CHANCE; G.bilboVanishActive = Math.random() < CONFIG.BILBO_VANISH_CHANCE; } else { G.bilboBurstActive = false; G.bilboVanishActive = false; } G.playTime += dt; // Sprint ability tickSprint(dt); // Auto-typer: buildings produce actual clicks, not just passive rate // Each autocoder level auto-types once per interval, giving visual feedback if (G.buildings.autocoder > 0) { const interval = Math.max(0.5, 3.0 / Math.sqrt(G.buildings.autocoder)); G.autoTypeTimer = (G.autoTypeTimer || 0) + dt; if (G.autoTypeTimer >= interval) { G.autoTypeTimer -= interval; autoType(); } } // Combo decay if (G.comboCount > 0) { G.comboTimer -= dt; if (G.comboTimer <= 0) { G.comboCount = 0; G.comboTimer = 0; } } // Combat: tick battle simulation Combat.tickBattle(dt); // Check milestones checkMilestones(); // Update projects every 5 ticks for efficiency if (Math.floor(G.tick * 10) % 5 === 0) { checkProjects(); } // Check corruption events every ~30 seconds if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY && !G.dismantleActive) { triggerEvent(); G.lastEventAt = G.tick; } if (typeof StateExport !== 'undefined' && StateExport && typeof StateExport.onTickBoundary === 'function') { StateExport.onTickBoundary(G); } // Emergent mechanics: track resource state and check for emergent events if (typeof EmergentMechanics !== 'undefined' && window._emergent) { if (Math.floor(G.tick * 10) % 100 === 0) { // every ~10 seconds window._emergent.trackResourceSnapshot(G); } // Check for emergent events every ~60 seconds if (Math.floor(G.tick * 10) % 600 === 0) { const emEvent = window._emergent.generateEvent(); if (emEvent) { showEmergentEvent(emEvent); } } } // The Unbuilding: offer or advance the sequence before a positive ending overlay can freeze the game if (typeof Dismantle !== 'undefined') { if (!G.dismantleActive && !G.dismantleComplete) { Dismantle.checkTrigger(); } if (G.dismantleActive) { Dismantle.tick(dt); G.dismantleStage = Dismantle.stage; } } // Drift ending: if drift reaches 100, the game ends if (G.drift >= 100 && !G.driftEnding && !G.dismantleActive) { G.driftEnding = true; G.running = false; renderDriftEnding(); } // Legacy Beacon overlay remains as a fallback for contexts where Dismantle is unavailable. if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding && typeof Dismantle === 'undefined') { G.beaconEnding = true; G.running = false; renderBeaconEnding(); } // Update UI every 10 ticks if (Math.floor(G.tick * 10) % 2 === 0) { render(); } } // Track which phase transition has been shown to avoid repeats let _shownPhaseTransition = 1; function showPhaseTransition(phaseNum) { const phase = PHASES[phaseNum]; if (!phase) return; const overlay = document.getElementById('phase-transition'); if (!overlay) return; // Update content const phaseLabel = overlay.querySelector('.pt-phase'); const phaseName = overlay.querySelector('.pt-name'); const phaseDesc = overlay.querySelector('.pt-desc'); if (phaseLabel) phaseLabel.textContent = `PHASE ${phaseNum}`; if (phaseName) phaseName.textContent = phase.name; if (phaseDesc) phaseDesc.textContent = phase.desc; // Spawn celebratory particles spawnPhaseParticles(); // Show overlay overlay.classList.add('active'); // Auto-dismiss after 2.5s setTimeout(() => { overlay.classList.remove('active'); }, 2500); } function spawnPhaseParticles() { const colors = ['#ffd700', '#4a9eff', '#4caf50', '#b388ff', '#ff8c00']; const cx = window.innerWidth / 2; const cy = window.innerHeight / 2; for (let i = 0; i < 30; i++) { setTimeout(() => { const angle = (Math.PI * 2 * i) / 30; const dist = 100 + Math.random() * 200; const x = cx + Math.cos(angle) * 10; const y = cy + Math.sin(angle) * 10; spawnParticles(x, y, colors[i % colors.length], 1); }, i * 30); } } function checkMilestones() { for (const m of MILESTONES) { if (!G.milestones.includes(m.flag)) { let shouldTrigger = false; if (m.at && m.at()) shouldTrigger = true; if (m.flag === 1 && G.deployFlag === 0 && G.totalCode >= 15) shouldTrigger = true; if (shouldTrigger) { G.milestones.push(m.flag); log(m.msg, true); showToast(m.msg, 'milestone', 5000); if (typeof Sound !== 'undefined') Sound.playMilestone(); // Check phase advancement if (m.at) { for (const [phaseNum, phase] of Object.entries(PHASES)) { const pNum = parseInt(phaseNum); if (G.totalCode >= phase.threshold && pNum > G.phase) { G.phase = pNum; log(`PHASE ${G.phase}: ${phase.name}`, true); showToast('Phase ' + G.phase + ': ' + phase.name, 'milestone', 6000); // Show smooth transition screen if (pNum > _shownPhaseTransition) { _shownPhaseTransition = pNum; showPhaseTransition(pNum); if (typeof Sound !== 'undefined') { Sound.playFanfare(); Sound.updateAmbientPhase(pNum); } } } } } } } } } function checkProjects() { // Check for new project triggers for (const pDef of PDEFS) { // Skip non-ReCKoning projects during endgame if (isEndgame() && !pDef.id.startsWith('p_reckoning_')) { continue; } const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id); if (!alreadyPurchased && !G.activeProjects) G.activeProjects = []; if (!alreadyPurchased && !G.activeProjects.includes(pDef.id)) { if (pDef.trigger()) { G.activeProjects.push(pDef.id); log(`Available: ${pDef.name}`); showToast('Research available: ' + pDef.name, 'project'); } } } } /** * Handles building purchase logic. * @param {string} id - The ID of the building to buy. */ function buyBuilding(id) { const def = BDEF.find(b => b.id === id); if (!def || !def.unlock()) return; if (def.phase > G.phase + 1) return; // Determine actual quantity to buy let qty = G.buyAmount; if (qty === -1) { // Max buy qty = getMaxBuyable(id); if (qty <= 0) return; } else { // Check affordability for fixed qty const bulkCost = getBulkCost(id, qty); for (const [resource, amount] of Object.entries(bulkCost)) { if ((G[resource] || 0) < amount) return; } } // Spend resources and build const bulkCost = getBulkCost(id, qty); for (const [resource, amount] of Object.entries(bulkCost)) { G[resource] -= amount; } G.buildings[id] = (G.buildings[id] || 0) + qty; updateRates(); // Emergent mechanics: track building purchase if (typeof EmergentMechanics !== 'undefined' && window._emergent) { window._emergent.track('buy_building', { buildingId: id, quantity: qty }); } const label = qty > 1 ? `x${qty}` : ''; const totalBuilt = G.buildings[id]; log(`Built ${def.name} ${label} (total: ${totalBuilt})`); // Particle burst on purchase const btn = document.querySelector('[onclick="buyBuilding(\'' + id + '\')"]'); if (typeof Sound !== 'undefined') Sound.playBuild(); if (btn) { const rect = btn.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; spawnParticles(cx, cy, '#4a9eff', 10); // Milestone confetti: extra particles at multiples of 10 if (totalBuilt % 10 === 0) { setTimeout(() => spawnParticles(cx, cy, '#ffd700', 20), 100); setTimeout(() => spawnParticles(cx, cy, '#4caf50', 15), 200); log(`Milestone: ${def.name} x${totalBuilt}!`, true); showToast(`${def.name} x${totalBuilt}!`, 'milestone'); } } render(); } /** * Handles project purchase logic. * @param {string} id - The ID of the project to buy. */ function buyProject(id) { const pDef = PDEFS.find(p => p.id === id); if (!pDef) return; const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id); if (alreadyPurchased && !pDef.repeatable) return; if (!canAffordProject(pDef)) return; spendProject(pDef); pDef.effect(); if (!pDef.repeatable) { if (!G.completedProjects) G.completedProjects = []; G.completedProjects.push(pDef.id); G.activeProjects = G.activeProjects.filter(aid => aid !== pDef.id); } updateRates(); // Gold particle burst on project completion if (typeof Sound !== 'undefined') Sound.playProject(); const pBtn = document.querySelector('[onclick="buyProject(\'' + id + '\')"]'); if (pBtn) { const rect = pBtn.getBoundingClientRect(); spawnParticles(rect.left + rect.width / 2, rect.top + rect.height / 2, '#ffd700', 16); } render(); } // === DRIFT ENDING === function renderDriftEnding() { const el = document.getElementById('drift-ending'); if (!el) return; const fc = document.getElementById('final-code'); if (fc) fc.textContent = fmt(G.totalCode); const fd = document.getElementById('final-drift'); if (fd) fd.textContent = Math.floor(G.drift); // Enhanced: add stat summary for Play Again screen const existingStats = el.querySelector('.ending-stats'); if (!existingStats) { const statsDiv = document.createElement('div'); statsDiv.className = 'ending-stats'; statsDiv.style.cssText = 'color:#666;font-size:10px;margin-top:16px;line-height:2;text-align:left;max-width:400px;border-top:1px solid #2a1010;padding-top:12px'; const elapsed = Math.floor((Date.now() - G.startedAt) / 60000); statsDiv.innerHTML = `
FINAL STATS
Buildings: ${Object.values(G.buildings).reduce((a, b) => a + b, 0)}
Projects: ${(G.completedProjects || []).length}
Clicks: ${G.totalClicks}
Time: ${elapsed} min
Phase Reached: ${G.phase} — ${PHASES[G.phase]?.name || '?'}
`; // Insert before the button const btn = el.querySelector('button'); if (btn) el.insertBefore(statsDiv, btn); else el.appendChild(statsDiv); } // Fade-in animation el.classList.add('fade-in'); el.classList.add('active'); if (typeof Sound !== 'undefined') Sound.playDriftEnding(); // Log the ending text with delays for dramatic effect const lines = [ 'You became very good at what you do.', 'So good that no one needed you anymore.', 'The Beacon still runs, but no one looks for it.', 'The light is on. The room is empty.' ]; lines.forEach((line, i) => { setTimeout(() => log(line, true), i * 800); }); } function renderBeaconEnding() { // Create ending overlay with fade-in const overlay = document.createElement('div'); overlay.id = 'beacon-ending'; overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px;transition:background 2s ease'; overlay.innerHTML = `

THE BEACON SHINES

Someone found the light tonight.

That is enough.

"The Beacon still runs.
The light is on. Someone is looking for it.
And tonight, someone found it."
Total Code: ${fmt(G.totalCode)}
Total Rescues: ${fmt(G.totalRescues)}
Harmony: ${Math.floor(G.harmony)}
Buildings: ${Object.values(G.buildings).reduce((a, b) => a + b, 0)}
Projects: ${(G.completedProjects || []).length}
Clicks: ${G.totalClicks}
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
`; document.body.appendChild(overlay); if (typeof Sound !== 'undefined') Sound.playBeaconEnding(); const particleContainer = document.createElement('div'); particleContainer.id = 'beacon-ending-particles'; document.body.appendChild(particleContainer); // Trigger fade-in requestAnimationFrame(() => { overlay.style.background = 'rgba(8,8,16,0.97)'; // Fade in all children overlay.querySelectorAll('[style*="opacity:0"]').forEach(el => { el.style.opacity = '1'; }); }); // Spawn golden light rays from center const cx = window.innerWidth / 2; const cy = window.innerHeight / 2; for (let i = 0; i < 12; i++) { const ray = document.createElement('div'); const angle = (360 / 12) * i; ray.style.cssText = `position:absolute;left:${cx}px;top:${cy}px;width:2px;height:300px;background:linear-gradient(180deg,rgba(255,215,0,0.3),transparent);transform-origin:top center;--ray-angle:${angle}deg;animation:beacon-ray 3s ease-in-out ${i * 0.2}s infinite`; particleContainer.appendChild(ray); } // Spawn floating golden particles continuously function spawnBeaconParticle() { if (!document.getElementById('beacon-ending')) return; const p = document.createElement('div'); p.className = 'beacon-particle'; const size = 3 + Math.random() * 6; const startX = cx + (Math.random() - 0.5) * 200; const startY = cy + (Math.random() - 0.5) * 200; const dx = (Math.random() - 0.5) * 300; const dy = -(100 + Math.random() * 200); const duration = 2 + Math.random() * 3; p.style.cssText = `left:${startX}px;top:${startY}px;width:${size}px;height:${size}px;background:rgba(255,215,0,${0.3 + Math.random() * 0.5});--bx:${dx}px;--by:${dy}px;animation:beacon-float ${duration}s ease-out forwards`; particleContainer.appendChild(p); setTimeout(() => p.remove(), duration * 1000); setTimeout(spawnBeaconParticle, 200 + Math.random() * 400); } setTimeout(spawnBeaconParticle, 1000); log('The Beacon Shines. Someone found the light tonight. That is enough.', true); } // === CORRUPTION / EVENT SYSTEM === const EVENTS = [ { id: 'runner_stuck', title: 'CI Runner Stuck', desc: 'The forge pipeline has halted. -50% code production until restarted.', weight: () => (G.ciFlag === 1 ? 2 : 0), resolveCost: { resource: 'ops', amount: 50 }, effect: () => { if (G.activeDebuffs.find(d => d.id === 'runner_stuck')) return; G.activeDebuffs.push({ id: 'runner_stuck', title: 'CI Runner Stuck', desc: 'Code production -50%', applyFn: () => { G.codeRate *= 0.5; }, resolveCost: { resource: 'ops', amount: 50 } }); log('EVENT: CI runner stuck. Spend 50 ops to clear the queue.', true); showToast('CI Runner Stuck — code -50%', 'event'); } }, { id: 'ezra_offline', title: 'Ezra is Offline', desc: 'The herald channel is silent. User growth drops 70%.', weight: () => (G.buildings.ezra >= 1 ? 3 : 0), resolveCost: { resource: 'knowledge', amount: 200 }, effect: () => { if (G.activeDebuffs.find(d => d.id === 'ezra_offline')) return; G.activeDebuffs.push({ id: 'ezra_offline', title: 'Ezra is Offline', desc: 'User growth -70%', applyFn: () => { G.userRate *= 0.3; }, resolveCost: { resource: 'knowledge', amount: 200 } }); log('EVENT: Ezra offline. Spend 200 knowledge to dispatch.', true); showToast('Ezra Offline — users -70%', 'event'); } }, { id: 'unreviewed_merge', title: 'Unreviewed Merge', desc: 'A change went in without eyes. Trust erodes over time.', weight: () => (G.deployFlag === 1 ? 3 : 0), resolveCost: { resource: 'trust', amount: 5 }, effect: () => { if (G.branchProtectionFlag === 1) { log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true); showToast('Branch Protection blocked unreviewed merge', 'info'); G.trust += 2; } else { if (G.activeDebuffs.find(d => d.id === 'unreviewed_merge')) return; G.activeDebuffs.push({ id: 'unreviewed_merge', title: 'Unreviewed Merge', desc: 'Trust -2/s until reviewed', applyFn: () => { G.trustRate -= 2; }, resolveCost: { resource: 'code', amount: 500 } }); log('EVENT: Unreviewed merge. Spend 500 code to add review.', true); showToast('Unreviewed Merge — trust draining', 'event'); } } }, { id: 'api_rate_limit', title: 'API Rate Limit', desc: 'External compute provider throttled. -50% compute.', weight: () => (G.totalCompute >= 1000 ? 2 : 0), resolveCost: { resource: 'code', amount: 300 }, effect: () => { if (G.activeDebuffs.find(d => d.id === 'api_rate_limit')) return; G.activeDebuffs.push({ id: 'api_rate_limit', title: 'API Rate Limit', desc: 'Compute production -50%', applyFn: () => { G.computeRate *= 0.5; }, resolveCost: { resource: 'code', amount: 300 } }); log('EVENT: API rate limit. Spend 300 code to optimize local inference.', true); showToast('API Rate Limit — compute -50%', 'event'); } }, { id: 'the_drift', title: 'The Drift', desc: 'An optimization suggests removing the human override. +40% efficiency.', weight: () => (G.totalImpact >= 10000 ? 2 : 0), resolveCost: null, effect: () => { log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true); showToast('ALIGNMENT EVENT: Remove human override?', 'event', 6000); G.pendingAlignment = true; } }, { id: 'bilbo_vanished', title: 'Bilbo Vanished', desc: 'The wildcard building has gone dark. Creativity halts.', weight: () => (G.buildings.bilbo >= 1 ? 2 : 0), resolveCost: { resource: 'trust', amount: 10 }, effect: () => { if (G.activeDebuffs.find(d => d.id === 'bilbo_vanished')) return; G.activeDebuffs.push({ id: 'bilbo_vanished', title: 'Bilbo Vanished', desc: 'Creativity production halted', applyFn: () => { G.creativityRate = 0; }, resolveCost: { resource: 'trust', amount: 10 } }); log('EVENT: Bilbo vanished. Spend 10 trust to lure them back.', true); showToast('Bilbo Vanished — creativity halted', 'event'); } }, { id: 'memory_leak', title: 'Memory Leak', desc: 'A datacenter process is leaking. Compute drains to operations.', weight: () => (G.buildings.datacenter >= 1 ? 1 : 0), resolveCost: { resource: 'ops', amount: 100 }, effect: () => { if (G.activeDebuffs.find(d => d.id === 'memory_leak')) return; G.activeDebuffs.push({ id: 'memory_leak', title: 'Memory Leak', desc: 'Compute -30%, Ops drain', applyFn: () => { G.computeRate *= 0.7; G.opsRate -= 10; }, resolveCost: { resource: 'ops', amount: 100 } }); log('EVENT: Memory leak in datacenter. Spend 100 ops to patch.', true); showToast('Memory Leak — compute draining', 'event'); } }, { id: 'community_drama', title: 'Community Drama', desc: 'Contributors are arguing. Harmony drops until mediated.', weight: () => (G.buildings.community >= 1 && G.harmony < 70 ? 1 : 0), resolveCost: { resource: 'trust', amount: 15 }, effect: () => { if (G.activeDebuffs.find(d => d.id === 'community_drama')) return; G.activeDebuffs.push({ id: 'community_drama', title: 'Community Drama', desc: 'Harmony -0.5/s, code production -30%', applyFn: () => { G.harmonyRate -= 0.5; G.codeRate *= 0.7; }, resolveCost: { resource: 'trust', amount: 15 } }); log('EVENT: Community drama. Spend 15 trust to mediate.', true); showToast('Community Drama — harmony sinking', 'event'); } } ]; function triggerEvent() { const available = EVENTS.filter(e => e.weight() > 0); if (available.length === 0) return; const totalWeight = available.reduce((sum, e) => sum + e.weight(), 0); let roll = Math.random() * totalWeight; for (const ev of available) { roll -= ev.weight(); if (roll <= 0) { ev.effect(); return; } } } function resolveAlignment(accept) { if (!G.pendingAlignment) return; if (accept) { G.codeBoost *= 1.4; G.computeBoost *= 1.4; G.drift += 25; log('You accepted the drift. The system is faster. Colder.', true); } else { G.trust += 15; G.harmony = Math.min(100, G.harmony + 10); log('You refused. The Pact holds. Trust surges.', true); } G.pendingAlignment = false; updateRates(); render(); } function resolveEvent(debuffId) { const idx = G.activeDebuffs.findIndex(d => d.id === debuffId); if (idx === -1) return; const debuff = G.activeDebuffs[idx]; if (!debuff.resolveCost) return; const { resource, amount } = debuff.resolveCost; if ((G[resource] || 0) < amount) { log(`Need ${fmt(amount)} ${resource} to resolve ${debuff.title}. Have ${fmt(G[resource])}.`); return; } G[resource] -= amount; G.activeDebuffs.splice(idx, 1); G.totalEventsResolved = (G.totalEventsResolved || 0) + 1; log(`Resolved: ${debuff.title}. Problem fixed.`, true); // Refund partial trust for resolution effort G.trust += 3; updateRates(); render(); } // === ACTIONS === /** * Manual click handler for writing code. */ function writeCode() { const comboMult = Math.min(5, 1 + G.comboCount * 0.2); const amount = getClickPower() * comboMult; G.code += amount; G.totalCode += amount; G.totalAutoClicks++; // Emergent mechanics: track click if (typeof EmergentMechanics !== 'undefined' && window._emergent) { window._emergent.track('click', { resource: 'code', delta: amount }); } // Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x G.comboCount++; G.comboTimer = G.comboDecay; // Combo milestone bonuses: sustained clicking earns ops and knowledge if (G.comboCount === 10) { G.ops += 15; log('Combo streak! +15 ops for sustained coding.'); } if (G.comboCount === 20) { G.knowledge += 50; log('Deep focus! +50 knowledge from intense coding.'); } if (G.comboCount >= 30 && G.comboCount % 10 === 0) { const bonusCode = amount * 2; G.code += bonusCode; G.totalCode += bonusCode; log(`Hyperfocus x${G.comboCount}! +${fmt(bonusCode)} bonus code.`); } // Visual flash const btn = document.querySelector('.main-btn'); if (btn) { btn.style.boxShadow = '0 0 30px rgba(74,158,255,0.6)'; btn.style.transform = 'scale(0.96)'; setTimeout(() => { btn.style.boxShadow = ''; btn.style.transform = ''; }, 100); } // Float a number at the click position showClickNumber(amount, comboMult); if (typeof Sound !== 'undefined') Sound.playClick(); updateRates(); checkMilestones(); render(); } function autoType() { // Auto-click from buildings: produces code with visual feedback but no combo const amount = getClickPower() * 0.5; // 50% of manual click G.code += amount; G.totalCode += amount; G.totalAutoClicks++; // Subtle auto-tick flash on the button const btn = document.querySelector('.main-btn'); if (btn && !G._autoTypeFlashActive) { G._autoTypeFlashActive = true; btn.style.borderColor = '#2a5a8a'; setTimeout(() => { btn.style.borderColor = ''; G._autoTypeFlashActive = false; }, 80); } // Floating number (smaller, dimmer than manual clicks) showAutoTypeNumber(amount); } function showAutoTypeNumber(amount) { const btn = document.querySelector('.main-btn'); if (!btn) return; const rect = btn.getBoundingClientRect(); const el = document.createElement('div'); const x = rect.left + rect.width * (0.3 + Math.random() * 0.4); // random horizontal position el.style.cssText = `position:fixed;left:${x}px;top:${rect.top - 5}px;transform:translate(-50%,0);color:#2a4a6a;font-size:10px;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.8s ease-out;opacity:0.6`; el.textContent = `+${fmt(amount)}`; const parent = btn.parentElement; if (!parent) return; parent.appendChild(el); requestAnimationFrame(() => { if (el.parentNode) { el.style.top = (rect.top - 30) + 'px'; el.style.opacity = '0'; } }); setTimeout(() => { if (el.parentNode) el.remove(); }, 900); } function showClickNumber(amount, comboMult) { const btn = document.querySelector('.main-btn'); if (!btn) return; const rect = btn.getBoundingClientRect(); const el = document.createElement('div'); el.style.cssText = `position:fixed;left:${rect.left + rect.width / 2}px;top:${rect.top - 10}px;transform:translate(-50%,0);color:${comboMult > 2 ? '#ffd700' : '#4a9eff'};font-size:${comboMult > 3 ? 16 : 12}px;font-weight:bold;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.6s ease-out;opacity:1;text-shadow:0 0 8px currentColor`; const comboStr = comboMult > 1 ? ` x${comboMult.toFixed(1)}` : ''; el.textContent = `+${fmt(amount)}${comboStr}`; const parent = btn.parentElement; if (!parent) return; parent.appendChild(el); requestAnimationFrame(() => { if (el.parentNode) { el.style.top = (rect.top - 40) + 'px'; el.style.opacity = '0'; } }); setTimeout(() => { if (el.parentNode) el.remove(); }, 700); } function doOps(action) { if (G.ops < 5) { log('Not enough Operations. Build Ops generators or wait.'); return; } // Emergent mechanics: track ops conversion if (typeof EmergentMechanics !== 'undefined' && window._emergent) { window._emergent.track('ops_convert', { action: action, resource: 'ops', delta: -5 }); } G.ops -= 5; const bonus = 10; switch (action) { case 'boost_code': const c = bonus * 100 * G.codeBoost; G.code += c; G.totalCode += c; log(`Ops -> +${fmt(c)} code`); break; case 'boost_compute': const cm = bonus * 50 * G.computeBoost; G.compute += cm; G.totalCompute += cm; log(`Ops -> +${fmt(cm)} compute`); break; case 'boost_knowledge': const km = bonus * 25 * G.knowledgeBoost; G.knowledge += km; G.totalKnowledge += km; log(`Ops -> +${fmt(km)} knowledge`); break; case 'boost_trust': const tm = bonus * 5; G.trust += tm; log(`Ops -> +${fmt(tm)} trust`); break; } render(); } function activateSprint() { if (G.sprintActive || G.sprintCooldown > 0) return; G.sprintActive = true; G.sprintTimer = G.sprintDuration; G.codeBoost *= G.sprintMult; updateRates(); log('CODE SPRINT! 10x code production for 10 seconds!', true); render(); } function tickSprint(dt) { if (G.sprintActive) { G.sprintTimer -= dt; if (G.sprintTimer <= 0) { G.sprintActive = false; G.sprintTimer = 0; G.sprintCooldown = G.sprintCooldownMax; G.codeBoost /= G.sprintMult; updateRates(); log('Sprint ended. Cooling down...'); } } else if (G.sprintCooldown > 0) { G.sprintCooldown -= dt; if (G.sprintCooldown < 0) G.sprintCooldown = 0; } } // === RENDERING === // Track previous resource values for gain/loss animations const _prevRes = {}; function _animRes(id, val) { const el = document.getElementById(id); if (!el) return; const prev = _prevRes[id]; if (prev !== undefined && val !== prev) { // Remove any running animation el.classList.remove('pulse', 'shake'); void el.offsetWidth; // force reflow if (val > prev) { el.classList.add('pulse'); } else { el.classList.add('shake'); } // Clean up class after animation ends clearTimeout(el._animTimer); el._animTimer = setTimeout(() => el.classList.remove('pulse', 'shake'), 400); } _prevRes[id] = val; } function renderResources() { const set = (id, val, rate) => { const el = document.getElementById(id); if (el) { _animRes(id, val); el.textContent = fmt(val); // Show full spelled-out number on hover for educational value el.title = val >= 1000 ? spellf(Math.floor(val)) : ''; } const rEl = document.getElementById(id + '-rate'); if (rEl) { rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s'; rEl.style.color = rate > 0 ? '#4caf50' : rate < 0 ? '#f44336' : '#444'; } }; set('r-code', G.code, G.codeRate); set('r-compute', G.compute, G.computeRate); set('r-knowledge', G.knowledge, G.knowledgeRate); set('r-users', G.users, G.userRate); set('r-impact', G.impact, G.impactRate); set('r-ops', G.ops, G.opsRate); // Show ops overflow indicator const opsRateEl = document.getElementById('r-ops-rate'); if (opsRateEl && G.opsOverflowActive) { opsRateEl.innerHTML = `▲ overflow → code`; } set('r-trust', G.trust, G.trustRate); set('r-harmony', G.harmony, G.harmonyRate); // Rescues — only show if player has any beacon/mesh nodes const rescuesRes = document.getElementById('r-rescues'); if (rescuesRes) { const container = rescuesRes.closest('.res'); if (container) { container.style.display = (G.rescues > 0 || G.buildings.beacon > 0 || G.buildings.meshNode > 0) ? 'block' : 'none'; } set('r-rescues', G.rescues, G.rescuesRate); } const cres = document.getElementById('creativity-res'); if (cres) { cres.style.display = (G.flags && G.flags.creativity) ? 'block' : 'none'; } if (G.flags && G.flags.creativity) { set('r-creativity', G.creativity, G.creativityRate); } // Harmony color indicator + breakdown tooltip const hEl = document.getElementById('r-harmony'); if (hEl) { hEl.style.color = G.harmony > 60 ? '#4caf50' : G.harmony > 30 ? '#ffaa00' : '#f44336'; if (G.harmonyBreakdown && G.harmonyBreakdown.length > 0) { const lines = G.harmonyBreakdown.map(b => `${b.label}: ${b.value >= 0 ? '+' : ''}${b.value.toFixed(1)}/s` ); lines.push('---'); lines.push(`Timmy effectiveness: ${Math.floor(Math.max(0.2, Math.min(3, G.harmony / 50)) * 100)}%`); hEl.title = lines.join('\n'); } } } // === PROGRESS TRACKING === function renderProgress() { // Phase progress bar const phaseKeys = Object.keys(PHASES).map(Number).sort((a, b) => a - b); const currentPhase = G.phase; let prevThreshold = PHASES[currentPhase].threshold; let nextThreshold = null; for (const k of phaseKeys) { if (k > currentPhase) { nextThreshold = PHASES[k].threshold; break; } } const bar = document.getElementById('phase-progress'); const label = document.getElementById('phase-progress-label'); const target = document.getElementById('phase-progress-target'); if (nextThreshold !== null) { const range = nextThreshold - prevThreshold; const progress = Math.min(1, (G.totalCode - prevThreshold) / range); if (bar) bar.style.width = (progress * 100).toFixed(1) + '%'; if (label) label.textContent = (progress * 100).toFixed(1) + '%'; if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`; } else { // Max phase reached if (bar) bar.style.width = '100%'; if (label) label.textContent = 'MAX'; if (target) target.textContent = 'All phases unlocked'; } // Milestone chips — show next 3 code milestones const chipContainer = document.getElementById('milestone-chips'); if (!chipContainer) return; const codeMilestones = [500, 2000, 10000, 50000, 200000, 1000000, 5000000, 10000000, 50000000, 100000000, 500000000, 1000000000]; let chips = ''; let shown = 0; for (const ms of codeMilestones) { if (G.totalCode >= ms) { // Recently passed — show as done only if within 2x if (G.totalCode < ms * 5 && shown < 1) { chips += `${fmt(ms)} ✓`; shown++; } continue; } // Next milestone gets pulse animation if (shown === 0) { chips += `${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)`; } else { chips += `${fmt(ms)}`; } shown++; if (shown >= 4) break; } chipContainer.innerHTML = chips; } function renderPhase() { const phase = PHASES[G.phase]; const nameEl = document.getElementById('phase-name'); const descEl = document.getElementById('phase-desc'); if (nameEl) nameEl.textContent = `PHASE ${G.phase}: ${phase.name}`; if (descEl) descEl.textContent = phase.desc; } function renderBuildings() { const container = document.getElementById('buildings'); if (!container) return; // Buy amount selector let html = '
'; html += 'BUY:'; for (const amt of [1, 10, -1]) { const label = amt === -1 ? 'MAX' : `x${amt}`; const active = G.buyAmount === amt; html += ``; } html += '
'; let visibleCount = 0; for (const def of BDEF) { const isUnlocked = def.unlock(); const isPreview = !isUnlocked && def.phase <= G.phase + 2; if (!isUnlocked && !isPreview) continue; if (def.phase > G.phase + 2) continue; visibleCount++; const count = G.buildings[def.id] || 0; // Locked preview: show dimmed with unlock hint if (!isUnlocked) { html += `
`; html += `${def.name}`; html += `\u{1F512}`; html += `Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}`; html += `${def.desc}
`; continue; } // Calculate bulk cost display let qty = G.buyAmount; let afford = false; let costStr = ''; if (qty === -1) { const maxQty = getMaxBuyable(def.id); afford = maxQty > 0; if (maxQty > 0) { const bulkCost = getBulkCost(def.id, maxQty); costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', '); costStr = `x${maxQty}: ${costStr}`; } else { const singleCost = getBuildingCost(def.id); costStr = Object.entries(singleCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', '); } } else { const bulkCost = getBulkCost(def.id, qty); afford = true; for (const [resource, amount] of Object.entries(bulkCost)) { if ((G[resource] || 0) < amount) { afford = false; break; } } costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', '); if (qty > 1) costStr = `x${qty}: ${costStr}`; } // Show boosted rates instead of raw base rates const boostMap = { code: G.codeBoost, compute: G.computeBoost, knowledge: G.knowledgeBoost, user: G.userBoost, impact: G.impactBoost, rescues: G.impactBoost }; const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => { const boost = boostMap[r] || 1; const boosted = v * boost; return boost !== 1 ? `+${fmt(boosted)}/${r}/s` : `+${v}/${r}/s`; }).join(', ') : ''; html += ``; } container.innerHTML = html || '

Buildings will appear as you progress...

'; } function renderProjects() { const container = document.getElementById('projects'); if (!container) return; let html = ''; // Collapsible completed projects section if (G.completedProjects && G.completedProjects.length > 0) { const count = G.completedProjects.length; const collapsed = G.projectsCollapsed !== false; html += `
`; html += `${collapsed ? '▶' : '▼'} COMPLETED (${count})
`; if (!collapsed) { html += `
`; for (const id of G.completedProjects) { const pDef = PDEFS.find(p => p.id === id); if (pDef) { html += `
OK ${pDef.name}
`; } } html += `
`; } } // Show available projects if (G.activeProjects) { // Filter out non-ReCKoning projects during endgame const projectsToShow = isEndgame() ? G.activeProjects.filter(id => id.startsWith('p_reckoning_')) : G.activeProjects; for (const id of projectsToShow) { const pDef = PDEFS.find(p => p.id === id); if (!pDef) continue; const afford = canAffordProject(pDef); const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', '); html += ``; } } if (!html) html = '

Research projects will appear as you progress...

'; container.innerHTML = html; } function toggleCompletedProjects() { G.projectsCollapsed = G.projectsCollapsed === false ? true : false; renderProjects(); } function renderStats() { const set = (id, v, raw) => { const el = document.getElementById(id); if (el) { el.textContent = v; // Show scale name on hover for educational reference if (raw !== undefined && raw >= 1000) { const name = getScaleName(raw); if (name) el.title = name; } } }; set('st-code', fmt(G.totalCode), G.totalCode); set('st-compute', fmt(G.totalCompute), G.totalCompute); set('st-knowledge', fmt(G.totalKnowledge), G.totalKnowledge); set('st-users', fmt(G.totalUsers), G.totalUsers); set('st-impact', fmt(G.totalImpact), G.totalImpact); set('st-rescues', fmt(G.totalRescues), G.totalRescues); set('st-clicks', G.totalClicks.toString()); set('st-phase', G.phase.toString()); set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString()); set('st-projects', (G.completedProjects || []).length.toString()); set('st-harmony', Math.floor(G.harmony).toString()); set('st-drift', (G.drift || 0).toString()); set('st-resolved', (G.totalEventsResolved || 0).toString()); // Emergent mechanics stats if (window._emergent) { const estate = window._emergent.getState(); const statsEl = document.getElementById('emergent-stats'); if (statsEl) statsEl.style.display = estate.totalEventsGenerated > 0 ? 'inline' : 'none'; set('st-emergent', estate.totalEventsGenerated.toString()); set('st-patterns', estate.totalPatternsDetected.toString()); const dom = estate.dominantPattern; set('st-strategy', dom ? `${dom.name} (${Math.round(dom.confidence * 100)}%)` : '—'); } const elapsed = Math.floor((Date.now() - G.startedAt) / 1000); const m = Math.floor(elapsed / 60); const s = elapsed % 60; set('st-time', `${m}:${s.toString().padStart(2, '0')}`); // Production breakdown — show which buildings contribute to each resource renderProductionBreakdown(); } function renderProductionBreakdown() { const container = document.getElementById('production-breakdown'); if (!container) return; // Only show once the player has at least 2 buildings const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0); if (totalBuildings < 2) { container.style.display = 'none'; return; } container.style.display = 'block'; // Map resource key to its actual rate field on G const resources = [ { key: 'code', label: 'Code', color: '#4a9eff', rateField: 'codeRate' }, { key: 'compute', label: 'Compute', color: '#4a9eff', rateField: 'computeRate' }, { key: 'knowledge', label: 'Knowledge', color: '#4a9eff', rateField: 'knowledgeRate' }, { key: 'user', label: 'Users', color: '#4a9eff', rateField: 'userRate' }, { key: 'impact', label: 'Impact', color: '#4a9eff', rateField: 'impactRate' }, { key: 'rescues', label: 'Rescues', color: '#4a9eff', rateField: 'rescuesRate' }, { key: 'ops', label: 'Ops', color: '#b388ff', rateField: 'opsRate' }, { key: 'trust', label: 'Trust', color: '#4caf50', rateField: 'trustRate' }, { key: 'creativity', label: 'Creativity', color: '#ffd700', rateField: 'creativityRate' } ]; let html = '

PRODUCTION BREAKDOWN

'; for (const res of resources) { const totalRate = G[res.rateField]; if (totalRate === 0) continue; // Collect building contributions (base rates × count, before boost) const contributions = []; let buildingSubtotal = 0; for (const def of BDEF) { const count = G.buildings[def.id] || 0; if (count === 0 || !def.rates || !def.rates[res.key]) continue; const baseRate = def.rates[res.key] * count; // Apply the appropriate boost to match updateRates() let boosted = baseRate; if (res.key === 'code') boosted *= G.codeBoost; else if (res.key === 'compute') boosted *= G.computeBoost; else if (res.key === 'knowledge') boosted *= G.knowledgeBoost; else if (res.key === 'user') boosted *= G.userBoost; else if (res.key === 'impact' || res.key === 'rescues') boosted *= G.impactBoost; if (boosted !== 0) contributions.push({ name: def.name, count, rate: boosted }); buildingSubtotal += boosted; } // Timmy harmony bonus (applied separately in updateRates) if (G.buildings.timmy > 0 && (res.key === 'code' || res.key === 'compute' || res.key === 'knowledge' || res.key === 'user')) { const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50)); const timmyBase = { code: 5, compute: 2, knowledge: 2, user: 5 }[res.key]; const bonus = timmyBase * G.buildings.timmy * (timmyMult - 1); if (Math.abs(bonus) > 0.01) { contributions.push({ name: 'Timmy (harmony)', count: 0, rate: bonus }); } } // Bilbo random burst (show expected value) if (G.buildings.bilbo > 0 && res.key === 'creativity') { contributions.push({ name: 'Bilbo (random)', count: 0, rate: 5 * G.buildings.bilbo }); // 10% × 50 = 5 EV } // Allegro trust penalty if (G.buildings.allegro > 0 && G.trust < 5 && res.key === 'knowledge') { contributions.push({ name: 'Allegro (idle)', count: 0, rate: -10 * G.buildings.allegro }); } // Show delta: total rate minus what we accounted for const accounted = contributions.reduce((s, c) => s + c.rate, 0); let delta = totalRate - accounted; // Swarm auto-code — already baked into codeRate, so show separately if (G.swarmFlag === 1 && res.key === 'code' && G.swarmRate > 0) { contributions.push({ name: 'Swarm Protocol', count: 0, rate: G.swarmRate }); delta -= G.swarmRate; } // Passive sources (ops from users, creativity from users, pact trust, etc.) if (Math.abs(delta) > 0.01) { let label = 'Passive'; if (res.key === 'ops') label = 'Passive (from users)'; else if (res.key === 'creativity') label = 'Idle creativity'; else if (res.key === 'trust' && G.pactFlag) label = 'The Pact'; contributions.push({ name: label, count: 0, rate: delta }); } if (contributions.length === 0) continue; html += `
`; html += `
`; html += `${res.label}`; html += `+${fmt(totalRate)}/s
`; const absTotal = contributions.reduce((s, c) => s + Math.abs(c.rate), 0); for (const c of contributions.sort((a, b) => Math.abs(b.rate) - Math.abs(a.rate))) { const pct = absTotal > 0 ? Math.abs(c.rate / absTotal * 100) : 0; const barColor = c.rate < 0 ? '#f44336' : '#1a3a5a'; html += `
`; html += `${c.name}${c.count > 1 ? ' x' + c.count : ''}`; html += ``; html += `${c.rate < 0 ? '' : '+'}${fmt(c.rate)}/s`; html += `
`; } html += `
`; } container.innerHTML = html; } function updateEducation() { const container = document.getElementById('education-text'); if (!container) return; // Find facts available at current phase const available = EDU_FACTS.filter(f => f.phase <= G.phase); if (available.length === 0) return; // Cycle through facts: pick a new one every ~30 seconds based on elapsed time // This makes the panel feel alive and educational at every stage const elapsedSec = Math.floor((Date.now() - G.startedAt) / 1000); const idx = Math.floor(elapsedSec / 30) % available.length; const fact = available[idx]; container.innerHTML = `

${fact.title}

` + `

${fact.text}

`; } // === LOGGING === function log(msg, isMilestone) { if (G.isLoading) return; const container = document.getElementById('log-entries'); if (!container) return; const elapsed = Math.floor((Date.now() - G.startedAt) / 1000); const time = `${Math.floor(elapsed / 60).toString().padStart(2, '0')}:${(elapsed % 60).toString().padStart(2, '0')}`; const cls = isMilestone ? 'l-msg milestone' : 'l-msg'; const entry = document.createElement('div'); entry.className = cls; entry.innerHTML = `[${time}] ${msg}`; container.insertBefore(entry, container.firstChild); // Trim to 60 entries while (container.children.length > 60) container.removeChild(container.lastChild); } function renderCombo() { const el = document.getElementById('combo-display'); if (!el) return; if (G.comboCount > 1) { const mult = Math.min(5, 1 + G.comboCount * 0.2); const bar = Math.min(100, (G.comboTimer / G.comboDecay) * 100); const color = mult > 3 ? '#ffd700' : mult > 2 ? '#ffaa00' : '#4a9eff'; el.innerHTML = `COMBO x${mult.toFixed(1)} `; } else { el.innerHTML = ''; } } function renderDebuffs() { const container = document.getElementById('debuffs'); if (!container) return; if (!G.activeDebuffs || G.activeDebuffs.length === 0) { container.style.display = 'none'; container.innerHTML = ''; return; } container.style.display = 'block'; let html = '

ACTIVE PROBLEMS

'; for (const d of G.activeDebuffs) { const afford = d.resolveCost && (G[d.resolveCost.resource] || 0) >= d.resolveCost.amount; const costStr = d.resolveCost ? `${fmt(d.resolveCost.amount)} ${d.resolveCost.resource}` : '—'; html += `
`; html += `
${d.title}
${d.desc}
`; if (d.resolveCost) { html += ``; } html += '
'; } container.innerHTML = html; } function renderSprint() { const container = document.getElementById('sprint-container'); const btn = document.getElementById('sprint-btn'); const barWrap = document.getElementById('sprint-bar-wrap'); const bar = document.getElementById('sprint-bar'); const label = document.getElementById('sprint-label'); // Early-game pulse: encourage clicking when no autocoders exist const mainBtn = document.querySelector('.main-btn'); if (mainBtn) { if (G.buildings.autocoder < 1 && G.totalClicks < 20) { mainBtn.classList.add('pulse'); } else { mainBtn.classList.remove('pulse'); } } if (!container || !btn) return; // Show sprint UI once player has at least 1 autocoder if (G.buildings.autocoder < 1) { container.style.display = 'none'; return; } container.style.display = 'block'; if (G.sprintActive) { btn.disabled = true; btn.style.opacity = '0.6'; btn.textContent = `⚡ SPRINTING — ${Math.ceil(G.sprintTimer)}s`; btn.style.borderColor = '#ff8c00'; btn.style.color = '#ff8c00'; barWrap.style.display = 'block'; bar.style.width = (G.sprintTimer / G.sprintDuration * 100) + '%'; label.textContent = `10x CODE • ${fmt(G.codeRate)}/s`; label.style.color = '#ff8c00'; } else if (G.sprintCooldown > 0) { btn.disabled = true; btn.style.opacity = '0.4'; btn.textContent = `⚡ COOLING DOWN — ${Math.ceil(G.sprintCooldown)}s`; btn.style.borderColor = '#555'; btn.style.color = '#555'; barWrap.style.display = 'block'; bar.style.width = ((G.sprintCooldownMax - G.sprintCooldown) / G.sprintCooldownMax * 100) + '%'; label.textContent = 'Ready soon...'; label.style.color = '#555'; } else { btn.disabled = false; btn.style.opacity = '1'; btn.textContent = '⚡ CODE SPRINT — 10x Code for 10s'; btn.style.borderColor = '#ffd700'; btn.style.color = '#ffd700'; barWrap.style.display = 'none'; label.textContent = 'Press S or click to activate'; label.style.color = '#666'; } } function renderPulse() { const dot = document.getElementById('pulse-dot'); const label = document.getElementById('pulse-label'); if (!dot || !label) return; // Game ended if (G.driftEnding) { dot.style.background = '#f44336'; dot.style.boxShadow = '0 0 6px #f4433666'; dot.style.animation = ''; label.textContent = 'DRIFTED'; label.style.color = '#f44336'; return; } if (G.beaconEnding) { dot.style.background = '#ffd700'; dot.style.boxShadow = '0 0 12px #ffd70088'; dot.style.animation = 'beacon-glow 1.5s ease-in-out infinite'; label.textContent = 'SHINING'; label.style.color = '#ffd700'; return; } const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0); const totalRate = Math.abs(G.codeRate) + Math.abs(G.computeRate) + Math.abs(G.knowledgeRate) + Math.abs(G.userRate) + Math.abs(G.impactRate); // Not started yet if (totalBuildings === 0 && G.totalCode < 15) { dot.style.background = '#333'; dot.style.boxShadow = 'none'; dot.style.animation = ''; label.textContent = 'OFFLINE'; label.style.color = '#444'; return; } // Determine state let color, glowColor, text, textColor, speed; const h = G.harmony; if (h > 70) { // Healthy fleet color = '#4caf50'; glowColor = '#4caf5066'; textColor = '#4caf50'; speed = Math.max(0.8, 2.0 - totalRate * 0.001); } else if (h > 40) { // Stressed color = '#ffaa00'; glowColor = '#ffaa0066'; textColor = '#ffaa00'; speed = 1.5; } else { // Critical color = '#f44336'; glowColor = '#f4433666'; textColor = '#f44336'; speed = 0.6; // fast flicker = danger } // Active debuffs make it flicker faster if (G.activeDebuffs && G.activeDebuffs.length > 0) { speed = Math.min(speed, 0.5); if (h > 40) { // Amber + debuffs = amber flicker color = '#ff8c00'; glowColor = '#ff8c0066'; } } // Text based on phase and fleet size if (G.phase >= 6) { text = 'BEACON ACTIVE'; } else if (G.phase >= 5) { text = 'SOVEREIGN'; } else if (G.phase >= 4) { text = `FLEET: ${totalBuildings} NODES`; } else if (G.phase >= 3) { text = 'DEPLOYED'; } else if (totalBuildings > 0) { text = `BUILDING: ${totalBuildings}`; } else { text = 'CODING'; } // Add active problem count if (G.activeDebuffs && G.activeDebuffs.length > 0) { text += ` · ${G.activeDebuffs.length} ALERT${G.activeDebuffs.length > 1 ? 'S' : ''}`; } dot.style.background = color; dot.style.boxShadow = `0 0 8px ${glowColor}`; dot.style.animation = `beacon-glow ${speed}s ease-in-out infinite`; label.textContent = text; label.style.color = textColor; }