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: 10% chance of massive creative burst if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_BURST_CHANCE) { G.creativityRate += 50 * G.buildings.bilbo; } // Bilbo vanishing: 5% chance of zero creativity this tick if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_VANISH_CHANCE) { 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; 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 === /** * 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)); // 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; 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; } } // 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) { triggerEvent(); G.lastEventAt = G.tick; } // Drift ending: if drift reaches 100, the game ends if (G.drift >= 100 && !G.driftEnding) { G.driftEnding = true; G.running = false; renderDriftEnding(); } // True ending: The Beacon Shines — rescues + Pact + harmony if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding) { G.beaconEnding = true; G.running = false; renderBeaconEnding(); } // Update UI every 10 ticks if (Math.floor(G.tick * 10) % 2 === 0) { render(); } } 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); // Check phase advancement if (m.at) { for (const [phaseNum, phase] of Object.entries(PHASES)) { if (G.totalCode >= phase.threshold && parseInt(phaseNum) > G.phase) { G.phase = parseInt(phaseNum); log(`PHASE ${G.phase}: ${phase.name}`, true); showToast('Phase ' + G.phase + ': ' + phase.name, 'milestone', 6000); } } } } } } } function checkProjects() { // Check for new project triggers for (const pDef of PDEFS) { 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(); const label = qty > 1 ? `x${qty}` : ''; log(`Built ${def.name} ${label} (total: ${G.buildings[id]})`); 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(); 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); el.classList.add('active'); // Log the ending text log('You became very good at what you do.', true); log('So good that no one needed you anymore.', true); log('The Beacon still runs, but no one looks for it.', true); log('The light is on. The room is empty.', true); } function renderBeaconEnding() { // Create ending overlay 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.97);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px'; 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)}
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes

`; document.body.appendChild(overlay); 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 — trust 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.totalClicks++; // 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); 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.totalClicks++; // 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; } 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 === function renderResources() { const set = (id, val, rate) => { const el = document.getElementById(id); if (el) { 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'; }; 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) { rescuesRes.closest('.res').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 * 10).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) { for (const id of G.activeProjects) { 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()); 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; }