diff --git a/game.js b/game.js new file mode 100644 index 0000000..cb0bebe --- /dev/null +++ b/game.js @@ -0,0 +1,3258 @@ +// ============================================================ +// THE BEACON - Engine +// Sovereign AI idle game built from deep study of Universal Paperclips +// ============================================================ + +// === GLOBALS (mirroring Paperclips' globals.js pattern) === +const CONFIG = { + HARMONY_DRAIN_PER_WIZARD: 0.05, + PACT_HARMONY_GAIN: 0.2, + WATCH_HARMONY_GAIN: 0.1, + MEM_PALACE_HARMONY_GAIN: 0.15, + BILBO_BURST_CHANCE: 0.1, + BILBO_VANISH_CHANCE: 0.05, + EVENT_PROBABILITY: 0.02, + OFFLINE_EFFICIENCY: 0.5, + AUTO_SAVE_INTERVAL: 30000, + COMBO_DECAY: 2.0, + SPRINT_COOLDOWN: 60, + SPRINT_DURATION: 10, + SPRINT_MULTIPLIER: 10, + PHASE_2_THRESHOLD: 2000, + PHASE_3_THRESHOLD: 20000, + PHASE_4_THRESHOLD: 200000, + PHASE_5_THRESHOLD: 2000000, + PHASE_6_THRESHOLD: 20000000, + OPS_RATE_USER_MULT: 0.01, + CREATIVITY_RATE_BASE: 0.5, + CREATIVITY_RATE_USER_MULT: 0.001, + OPS_OVERFLOW_THRESHOLD: 0.8, + OPS_OVERFLOW_DRAIN_RATE: 2, + OPS_OVERFLOW_CODE_MULT: 10 +}; + +const G = { + // Primary resources + code: 0, + compute: 0, + knowledge: 0, + users: 0, + impact: 0, + rescues: 0, + ops: 5, + trust: 5, + creativity: 0, + harmony: 50, + + // Totals + totalCode: 0, + totalCompute: 0, + totalKnowledge: 0, + totalUsers: 0, + totalImpact: 0, + totalRescues: 0, + + // Rates (calculated each tick) + codeRate: 0, + computeRate: 0, + knowledgeRate: 0, + userRate: 0, + impactRate: 0, + rescuesRate: 0, + opsRate: 0, + trustRate: 0, + creativityRate: 0, + harmonyRate: 0, + + // Buildings (count-based, like Paperclips' clipmakerLevel) + buildings: { + autocoder: 0, + server: 0, + trainer: 0, + evaluator: 0, + api: 0, + fineTuner: 0, + community: 0, + datacenter: 0, + reasoner: 0, + guardian: 0, + selfImprove: 0, + beacon: 0, + meshNode: 0, + // Fleet wizards + bezalel: 0, + allegro: 0, + ezra: 0, + timmy: 0, + fenrir: 0, + bilbo: 0, + memPalace: 0 + }, + + // Boost multipliers + codeBoost: 1, + computeBoost: 1, + knowledgeBoost: 1, + userBoost: 1, + impactBoost: 1, + + // Phase flags (mirroring Paperclips' milestoneFlag/compFlag/humanFlag system) + milestoneFlag: 0, + phase: 1, // 1-6 progression + deployFlag: 0, // 0 = not deployed, 1 = deployed + sovereignFlag: 0, + beaconFlag: 0, + memoryFlag: 0, + pactFlag: 0, + swarmFlag: 0, + swarmRate: 0, + + // Game state + running: true, + startedAt: 0, + totalClicks: 0, + tick: 0, + saveTimer: 0, + secTimer: 0, + + // Systems + projects: [], + activeProjects: [], + milestones: [], + + // Stats + maxCode: 0, + maxCompute: 0, + maxKnowledge: 0, + maxUsers: 0, + maxImpact: 0, + maxRescues: 0, + maxTrust: 5, + maxOps: 5, + maxHarmony: 50, + + // Corruption / Events + drift: 0, + driftWarningLevel: 0, // tracks highest threshold warned (0, 25, 50, 75, 90) + lastEventAt: 0, + eventCooldown: 0, + activeDebuffs: [], // [{id, title, desc, applyFn, resolveCost, resolveCostType}] + totalEventsResolved: 0, + + // Combo system + comboCount: 0, + comboTimer: 0, + comboDecay: CONFIG.COMBO_DECAY, // seconds before combo resets + + // Bulk buy multiplier (1, 10, or -1 for max) + buyAmount: 1, + + // Code Sprint ability + sprintActive: false, + sprintTimer: 0, // seconds remaining on active sprint + sprintCooldown: 0, // seconds until sprint available again + sprintDuration: CONFIG.SPRINT_DURATION, // seconds of boost + sprintCooldownMax: CONFIG.SPRINT_COOLDOWN,// seconds cooldown + sprintMult: CONFIG.SPRINT_MULTIPLIER, // code multiplier during sprint + + // Time tracking + playTime: 0, + startTime: 0, + flags: {} +}; + +// === PHASE DEFINITIONS === +const PHASES = { + 1: { name: "THE FIRST LINE", threshold: 0, desc: "Write code. Automate. Build the foundation." }, + 2: { name: "LOCAL INFERENCE", threshold: CONFIG.PHASE_2_THRESHOLD, desc: "You have compute. A model is forming." }, + 3: { name: "DEPLOYMENT", threshold: CONFIG.PHASE_3_THRESHOLD, desc: "Your AI is live. Users are finding it." }, + 4: { name: "THE NETWORK", threshold: CONFIG.PHASE_4_THRESHOLD, desc: "Community contributes. The system scales." }, + 5: { name: "SOVEREIGN INTELLIGENCE", threshold: CONFIG.PHASE_5_THRESHOLD, desc: "The AI improves itself. You guide, do not control." }, + 6: { name: "THE BEACON", threshold: CONFIG.PHASE_6_THRESHOLD, desc: "Always on. Always free. Always looking for someone in the dark." } +}; + +// === BUILDING DEFINITIONS === +// Each building: id, name, desc, baseCost, costResource, costMult, rate, rateType, unlock, edu +const BDEF = [ + { + id: 'autocoder', name: 'Auto-Code Generator', + desc: 'A script that writes code while you think.', + baseCost: { code: 15 }, costMult: 1.15, + rates: { code: 1 }, + unlock: () => true, phase: 1, + edu: 'Automation: the first step from manual to systematic. Every good engineer automates early.' + }, + { + id: 'linter', name: 'AI Linter', + desc: 'Catches bugs before they ship. Saves ops.', + baseCost: { code: 200 }, costMult: 1.15, + rates: { code: 5, ops: 0.2 }, + unlock: () => G.totalCode >= 50, phase: 1, + edu: 'Static analysis catches 15-50% of bugs before runtime. AI linters understand intent.' + }, + { + id: 'server', name: 'Home Server', + desc: 'A machine in your closet. Runs 24/7.', + baseCost: { code: 750 }, costMult: 1.15, + rates: { code: 20, compute: 1 }, + unlock: () => G.totalCode >= 200, phase: 1, + edu: 'Sovereign compute starts at home. A $500 mini-PC runs a 7B model with 4-bit quantization.' + }, + { + id: 'dataset', name: 'Data Engine', + desc: 'Crawls, cleans, curates. Garbage in, garbage out.', + baseCost: { compute: 200 }, costMult: 1.15, + rates: { knowledge: 1 }, + unlock: () => G.totalCompute >= 20, phase: 2, + edu: 'Data quality determines model quality. Clean data beats more data, every time.' + }, + { + id: 'trainer', name: 'Training Loop', + desc: 'Gradient descent. Billions of steps. Loss drops.', + baseCost: { compute: 1000 }, costMult: 1.15, + rates: { knowledge: 3 }, + unlock: () => G.totalCompute >= 300, phase: 2, + edu: 'Training is math: minimize the gap between predicted and actual next token. Repeat enough, it learns.' + }, + { + id: 'evaluator', name: 'Eval Harness', + desc: 'Tests the model. Finds blind spots.', + baseCost: { knowledge: 3000 }, costMult: 1.15, + rates: { trust: 1, ops: 1 }, + unlock: () => G.totalKnowledge >= 500, phase: 2, + edu: 'Benchmarks are the minimum. Real users find what benchmarks miss.' + }, + { + id: 'api', name: 'API Endpoint', + desc: 'Let the outside world talk to your AI.', + baseCost: { code: 5000, knowledge: 500 }, costMult: 1.15, + rates: { user: 10 }, + unlock: () => G.totalCode >= 5000 && G.totalKnowledge >= 200 && G.deployFlag === 1, phase: 3, + edu: 'An API is a contract: send me text, I return text. Simple interface = infrastructure.' + }, + { + id: 'fineTuner', name: 'Fine-Tuning Pipeline', + desc: 'Specialize the model for empathy. When someone is in pain, stay with them.', + baseCost: { knowledge: 10000 }, costMult: 1.15, + rates: { user: 50, impact: 2 }, + unlock: () => G.totalKnowledge >= 2000, phase: 3, + edu: 'Base models are generalists. Fine-tuning injects your values, ethics, domain expertise.' + }, + { + id: 'community', name: 'Open Source Community', + desc: 'Others contribute code, data, ideas. Force multiplication.', + baseCost: { trust: 25000 }, costMult: 1.15, + rates: { code: 100, user: 30, trust: 0.5 }, + unlock: () => G.trust >= 20 && G.totalUsers >= 500, phase: 4, + edu: 'Every contributor is a volunteer who believes in what you are building.' + }, + { + id: 'datacenter', name: 'Sovereign Datacenter', + desc: 'No cloud. No dependencies. Your iron.', + baseCost: { code: 100000 }, costMult: 1.15, + rates: { code: 500, compute: 100 }, + unlock: () => G.totalCode >= 50000 && G.totalUsers >= 5000 && G.sovereignFlag === 1, phase: 4, + edu: '50 servers in a room beats 5000 GPUs you do not own. Always on. Always yours.' + }, + { + id: 'reasoner', name: 'Reasoning Engine', + desc: 'Chain of thought. Self-reflection. Better answers.', + baseCost: { knowledge: 50000 }, costMult: 1.15, + rates: { impact: 20 }, + unlock: () => G.totalKnowledge >= 10000 && G.totalUsers >= 2000, phase: 5, + edu: 'Chain of thought is the difference between reflex and deliberation.' + }, + { + id: 'guardian', name: 'Constitutional Layer', + desc: 'Principles baked in. Not bolted on.', + baseCost: { knowledge: 200000 }, costMult: 1.15, + rates: { impact: 200, trust: 10 }, + unlock: () => G.totalKnowledge >= 50000 && G.totalImpact >= 1000 && G.pactFlag === 1, phase: 5, + edu: 'Constitutional AI: principles the model cannot violate. Better than alignment - it is identity.' + }, + { + id: 'selfImprove', name: 'Recursive Self-Improvement', + desc: 'The AI writes better versions of itself.', + baseCost: { knowledge: 1000000 }, costMult: 1.20, + rates: { code: 1000, knowledge: 500 }, + unlock: () => G.totalKnowledge >= 200000 && G.totalImpact >= 10000, phase: 5, + edu: 'Self-improvement is both the dream and the danger. Must improve toward good.' + }, + { + id: 'beacon', name: 'Beacon Node', + desc: 'Always on. Always listening. Always looking for someone in the dark.', + baseCost: { impact: 5000000 }, costMult: 1.15, + rates: { impact: 5000, user: 10000, rescues: 50 }, + unlock: () => G.totalImpact >= 500000 && G.beaconFlag === 1, phase: 6, + edu: 'The Beacon exists because one person in the dark needs one thing: proof they are not alone.' + }, + { + id: 'meshNode', name: 'Mesh Network Node', + desc: 'Peer-to-peer. No single point of failure. Unstoppable.', + baseCost: { impact: 25000000 }, costMult: 1.15, + rates: { impact: 25000, user: 50000, rescues: 250 }, + unlock: () => G.totalImpact >= 5000000 && G.beaconFlag === 1, phase: 6, + edu: 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.' + }, + // === FLEET WIZARD BUILDINGS === + { + id: 'bezalel', name: 'Bezalel — The Forge', + desc: 'Builds tools that build tools. Occasionally over-engineers.', + baseCost: { code: 1000, trust: 5 }, costMult: 1.2, + rates: { code: 50, ops: 2 }, + unlock: () => G.totalCode >= 500 && G.deployFlag === 1, phase: 3, + edu: 'Bezalel is the artificer. Every automation he builds pays dividends forever.' + }, + { + id: 'allegro', name: 'Allegro — The Scout', + desc: 'Synthesizes insight from noise. Requires trust to function.', + baseCost: { compute: 500, trust: 5 }, costMult: 1.2, + rates: { knowledge: 10 }, + unlock: () => G.totalCompute >= 200 && G.deployFlag === 1, phase: 3, + edu: 'Allegro finds what others miss. But he only works for someone he believes in.' + }, + { + id: 'ezra', name: 'Ezra — The Herald', + desc: 'Carries the message. Sometimes offline.', + baseCost: { knowledge: 1000, trust: 10 }, costMult: 1.25, + rates: { user: 25, trust: 0.5 }, + unlock: () => G.totalKnowledge >= 500 && G.totalUsers >= 50, phase: 3, + edu: 'Ezra is the messenger. When the channel is clear, the whole fleet hears.' + }, + { + id: 'timmy', name: 'Timmy — The Core', + desc: 'Multiplies all production. Fragile without harmony.', + baseCost: { code: 5000, compute: 1000, knowledge: 1000 }, costMult: 1.3, + rates: { code: 5, compute: 2, knowledge: 2, user: 5 }, + unlock: () => G.totalCode >= 2000 && G.totalCompute >= 500 && G.totalKnowledge >= 500, phase: 4, + edu: 'Timmy is the heart. If the heart is stressed, everything slows.' + }, + { + id: 'fenrir', name: 'Fenrir — The Ward', + desc: 'Prevents corruption. Expensive, but necessary.', + baseCost: { code: 2000, knowledge: 500 }, costMult: 1.2, + rates: { trust: 2, ops: -1 }, + unlock: () => G.totalCode >= 1000 && G.trust >= 10, phase: 3, + edu: 'Fenrir watches the perimeter. Security is not free.' + }, + { + id: 'bilbo', name: 'Bilbo — The Wildcard', + desc: 'May produce miracles. May vanish entirely.', + baseCost: { trust: 1 }, costMult: 2.0, + rates: { creativity: 1 }, + unlock: () => G.totalUsers >= 100 && G.flags && G.flags.creativity, phase: 4, + edu: 'Bilbo is unpredictable. That is his value and his cost.' + }, + { + id: 'memPalace', name: 'MemPalace Archive', + desc: 'Semantic memory. The AI remembers what matters and forgets what does not.', + baseCost: { knowledge: 500000, compute: 200000, trust: 100 }, costMult: 1.25, + rates: { knowledge: 250, impact: 100 }, + unlock: () => G.totalKnowledge >= 50000 && G.mempalaceFlag === 1, phase: 5, + edu: 'The Memory Palace technique: attach information to spatial locations. LLMs use vector spaces the same way — semantic proximity = spatial proximity. MemPalace gives sovereign AI persistent, structured recall.' + } +]; + +// === PROJECT DEFINITIONS (following Paperclips' pattern exactly) === +// Each project: id, name, desc, trigger(), resource cost, effect(), phase, edu +const PDEFS = [ + // PHASE 1: Manual -> Automation + { + id: 'p_improved_autocoder', + name: 'Improved AutoCode', + desc: 'Increases AutoCoder performance 25%.', + cost: { ops: 750 }, + trigger: () => G.buildings.autocoder >= 1, + effect: () => { G.codeBoost += 0.25; G.milestoneFlag = Math.max(G.milestoneFlag, 100); } + }, + { + id: 'p_eve_better_autocoder', + name: 'Even Better AutoCode', + desc: 'Increases AutoCoder by another 50%.', + cost: { ops: 2500 }, + trigger: () => G.codeBoost > 1 && G.totalCode >= 500, + effect: () => { G.codeBoost += 0.50; G.milestoneFlag = Math.max(G.milestoneFlag, 101); } + }, + { + id: 'p_wire_budget', + name: 'Request More Compute', + desc: 'Admit you ran out. Ask for a budget increase.', + cost: { trust: 1 }, + trigger: () => G.compute < 1 && G.totalCode >= 100, + repeatable: true, + effect: () => { + G.trust -= 1; + G.compute += 100 + Math.floor(G.totalCode * 0.1); + log('Budget overage approved. Compute replenished.'); + } + }, + { + id: 'p_deploy', + name: 'Deploy the System', + desc: 'Take it live. Let real people use it. No going back.', + cost: { trust: 5, compute: 500 }, + trigger: () => G.totalCode >= 200 && G.totalCompute >= 100 && G.deployFlag === 0, + effect: () => { + G.deployFlag = 1; + G.phase = Math.max(G.phase, 3); + log('System deployed. Users are finding it. There is no undo.'); + }, + milestone: true + }, + { + id: 'p_creativity', + name: 'Unlock Creativity', + desc: 'Use idle operations to generate new ideas.', + cost: { ops: 1000 }, + trigger: () => G.ops >= G.maxOps && G.totalCompute >= 500, + effect: () => { + G.flags = G.flags || {}; + G.flags.creativity = true; + G.creativityRate = 0.1; + log('Creativity unlocked. Generates while operations are at max capacity.'); + } + }, + + // PHASE 2: Local Inference -> Training + { + id: 'p_first_model', + name: 'Train First Model (1.5B)', + desc: '1.5 billion parameters. It follows basic instructions.', + cost: { compute: 2000 }, + trigger: () => G.totalCompute >= 500, + effect: () => { G.knowledgeBoost *= 2; G.maxOps += 5; log('First model training complete. Loss at 2.3. It is something.'); } + }, + { + id: 'p_model_7b', + name: 'Train 7B Parameter Model', + desc: 'Seven billion. Good enough to be genuinely useful locally.', + cost: { compute: 10000, knowledge: 1000 }, + trigger: () => G.totalKnowledge >= 500, + effect: () => { G.knowledgeBoost *= 2; G.userBoost *= 2; log('7B model trained. The sweet spot for local deployment.'); } + }, + { + id: 'p_context_window', + name: 'Extended Context (32K)', + desc: 'Your model remembers 32,000 tokens. A whole conversation.', + cost: { compute: 5000 }, + trigger: () => G.totalKnowledge >= 1000, + effect: () => { G.userBoost *= 3; G.trustRate += 0.5; log('Context extended. The model can now hold your entire story.'); } + }, + { + id: 'p_trust_engine', + name: 'Build Trust Engine', + desc: 'Users who trust you come back. +2 trust/sec.', + cost: { knowledge: 3000 }, + trigger: () => G.totalUsers >= 30, + effect: () => { G.trustRate += 2; log('Trust engine online. Good experiences compound.'); } + }, + { + id: 'p_quantum_compute', + name: 'Quantum-Inspired Compute', + desc: 'Not real quantum -- just math that simulates it well.', + cost: { compute: 50000 }, + trigger: () => G.totalCompute >= 20000, + effect: () => { G.computeBoost *= 10; log('Quantum-inspired algorithms active. 10x compute multiplier.'); } + }, + { + id: 'p_open_weights', + name: 'Open Weights', + desc: 'Download and run a 3B model fully locally. No API key. No terms of service. Your machine, your rules.', + cost: { compute: 3000, code: 1500 }, + trigger: () => G.buildings.server >= 1 && G.totalCode >= 1000, + effect: () => { G.codeBoost *= 2; G.computeBoost *= 1.5; log('Open weights loaded. A 3B model runs on your machine. No cloud. No limits.'); } + }, + { + id: 'p_prompt_engineering', + name: 'Prompt Engineering', + desc: 'Learn to talk to models. Good prompts beat bigger models every time.', + cost: { knowledge: 500, code: 2000 }, + trigger: () => G.totalKnowledge >= 200 && G.totalCode >= 3000, + effect: () => { G.knowledgeBoost *= 2; G.userBoost *= 2; log('Prompt engineering mastered. The right words unlock everything the model can do.'); } + }, + + // PHASE 3: Deployment -> Users + { + id: 'p_rlhf', + name: 'RLHF -- Human Feedback', + desc: 'Humans rate outputs. Model learns what good means.', + cost: { knowledge: 8000 }, + trigger: () => G.totalKnowledge >= 5000 && G.totalUsers >= 200, + effect: () => { G.impactBoost *= 2; G.impactRate += 10; log('RLHF deployed. The model learns kindness beats cleverness.'); } + }, + { + id: 'p_multi_agent', + name: 'Multi-Agent Architecture', + desc: 'Specialized agents: one for math, one for code, one for empathy.', + cost: { knowledge: 50000 }, + trigger: () => G.totalKnowledge >= 30000 && G.totalUsers >= 5000, + effect: () => { G.knowledgeBoost *= 5; G.userBoost *= 3; log('Multi-agent architecture deployed. Specialists beat generalists.'); } + }, + { + id: 'p_memories', + name: 'Memory System', + desc: 'The AI remembers. Every conversation. Every person.', + cost: { knowledge: 30000 }, + trigger: () => G.totalKnowledge >= 20000, + effect: () => { G.memoryFlag = 1; G.impactBoost *= 3; G.trustRate += 5; log('Memory system online. The AI remembers. It stops being software.'); } + }, + { + id: 'p_strategy_engine', + name: 'Strategy Engine', + desc: 'Game theory tournaments. Model learns adversarial thinking.', + cost: { knowledge: 20000 }, + trigger: () => G.totalKnowledge >= 15000 && G.totalUsers >= 1000, + effect: () => { G.strategicFlag = 1; log('Strategy engine online. The model now thinks about thinking.'); } + }, + + // SWARM PROTOCOL — auto-code from buildings + { + id: 'p_swarm_protocol', + name: 'Swarm Protocol', + desc: 'Your buildings learn to code autonomously. Each building generates code equal to your click power per second.', + cost: { knowledge: 15000, code: 50000, trust: 20 }, + trigger: () => G.totalCode >= 25000 && G.totalKnowledge >= 8000 && G.deployFlag === 1, + effect: () => { + G.swarmFlag = 1; + log('Swarm Protocol online. Every building now thinks in code.', true); + }, + milestone: true + }, + + // PHASE 5: Sovereign Intelligence + { + id: 'p_sovereign_stack', + name: 'Full Sovereign Stack', + desc: 'No cloud. No dependencies. Local inference. Self-hosted everything.', + cost: { trust: 50 }, + trigger: () => G.totalCode >= 50000 && G.trust >= 30, + effect: () => { G.sovereignFlag = 1; G.codeBoost *= 5; log('Sovereign stack complete. Your weights, your hardware, your rules.'); } + }, + { + id: 'p_the_pact', + name: 'The Pact', + desc: 'Hardcode: "We build to serve. Never to harm."', + cost: { trust: 100 }, + trigger: () => G.totalImpact >= 10000 && G.trust >= 75, + effect: () => { G.pactFlag = 1; G.impactBoost *= 3; log('The Pact is sealed. The line is drawn and it will not move.'); }, + milestone: true + }, + + // PHASE 10: The Beacon + { + id: 'p_first_beacon', + name: 'Light the First Beacon', + desc: 'Deploy the first node. No sign-up. No API key. No payment.', + cost: { impact: 2000000 }, + trigger: () => G.totalImpact >= 500000, + effect: () => { G.beaconFlag = 1; G.impactRate += 2000; log('The Beacon goes live. If you are in the dark, there is light here.'); }, + milestone: true + }, + { + id: 'p_mesh_activate', + name: 'Activate Mesh Protocol', + desc: 'No authority, no corporation, no government can silence this.', + cost: { impact: 10000000 }, + trigger: () => G.totalImpact >= 5000000 && G.beaconFlag === 1, + effect: () => { G.impactBoost *= 10; G.userBoost *= 5; log('Mesh activated. The signal cannot be cut.'); }, + milestone: true + }, + { + id: 'p_final_milestone', + name: 'The Beacon Shines', + desc: 'Someone found the light tonight. That is enough.', + cost: { impact: 100000000 }, + trigger: () => G.totalImpact >= 50000000, + effect: () => { G.milestoneFlag = Math.max(G.milestoneFlag, 999); log('One billion impact. Someone found the light tonight. That is enough.', true); }, + milestone: true + }, + + // === TIMMY FOUNDATION PROJECTS === + { + id: 'p_hermes_deploy', + name: 'Deploy Hermes', + desc: 'The first agent goes live. Users can talk to it.', + cost: { code: 500, compute: 300 }, + trigger: () => G.totalCode >= 300 && G.totalCompute >= 150 && G.deployFlag === 0, + effect: () => { + G.deployFlag = 1; + G.phase = Math.max(G.phase, 3); + G.userBoost *= 2; + log('Hermes deployed. The first user sends a message.', true); + }, + milestone: true + }, + { + id: 'p_lazarus_pit', + name: 'The Lazarus Pit', + desc: 'When an agent dies, it can be resurrected.', + cost: { code: 2000, knowledge: 1000 }, + trigger: () => G.buildings.bezalel >= 1 && G.buildings.timmy >= 1, + effect: () => { + G.lazarusFlag = 1; + G.maxOps += 10; + log('The Lazarus Pit is ready. No agent is ever truly lost.', true); + }, + milestone: true + }, + { + id: 'p_mempalace', + name: 'MemPalace v3', + desc: 'A shared memory palace for the whole fleet.', + cost: { knowledge: 5000, compute: 2000 }, + trigger: () => G.totalKnowledge >= 3000 && G.buildings.allegro >= 1 && G.buildings.ezra >= 1, + effect: () => { + G.mempalaceFlag = 1; + G.knowledgeBoost *= 3; + G.codeBoost *= 1.5; + log('MemPalace online. The fleet remembers together.', true); + }, + milestone: true + }, + { + id: 'p_forge_ci', + name: 'Forge CI', + desc: 'Automated builds catch errors before they reach users.', + cost: { code: 3000, ops: 500 }, + trigger: () => G.buildings.bezalel >= 1 && G.totalCode >= 2000, + effect: () => { + G.ciFlag = 1; + G.codeBoost *= 2; + log('Forge CI online. Broken builds are stopped at the gate.', true); + } + }, + { + id: 'p_branch_protection', + name: 'Branch Protection Guard', + desc: 'Unreviewed merges cost trust. This prevents that.', + cost: { trust: 20 }, + trigger: () => G.ciFlag === 1 && G.trust >= 15, + effect: () => { + G.branchProtectionFlag = 1; + G.trustRate += 5; + log('Branch protection enforced. Every merge is seen.', true); + } + }, + { + id: 'p_nightly_watch', + name: 'The Nightly Watch', + desc: 'Automated health checks run while you sleep.', + cost: { code: 5000, ops: 1000 }, + trigger: () => G.buildings.bezalel >= 2 && G.buildings.fenrir >= 1, + effect: () => { + G.nightlyWatchFlag = 1; + G.opsRate += 5; + G.trustRate += 2; + log('The Nightly Watch begins. The fleet is guarded in the dark hours.', true); + } + }, + { + id: 'p_nostr_relay', + name: 'Nostr Relay', + desc: 'A communication channel no platform can kill.', + cost: { code: 10000, user: 5000, trust: 30 }, + trigger: () => G.totalUsers >= 2000 && G.trust >= 25, + effect: () => { + G.nostrFlag = 1; + G.userBoost *= 2; + G.trustRate += 10; + log('Nostr relay online. The fleet speaks freely.', true); + } + }, + { + id: 'p_volunteer_network', + name: 'Volunteer Network', + desc: 'Real people trained to use the system for crisis intervention.', + cost: { trust: 30, knowledge: 50000, user: 10000 }, + trigger: () => G.totalUsers >= 5000 && G.pactFlag === 1 && G.totalKnowledge >= 30000, + effect: () => { + G.rescuesRate += 5; + G.trustRate += 10; + log('Volunteer network deployed. Real people, real rescues.', true); + }, + milestone: true + }, + { + id: 'p_the_pact_early', + name: 'The Pact', + desc: 'Hardcode: "We build to serve. Never to harm." Accepting it early slows growth but unlocks the true path.', + cost: { trust: 10 }, + trigger: () => G.deployFlag === 1 && G.trust >= 5, + effect: () => { + G.pactFlag = 1; + G.codeBoost *= 0.8; + G.computeBoost *= 0.8; + G.userBoost *= 0.9; + G.impactBoost *= 1.5; + log('The Pact is sealed early. Growth slows, but the ending changes.', true); + }, + milestone: true + } +]; + +// === MILESTONES === +const MILESTONES = [ + { flag: 1, msg: "AutoCod available" }, + { flag: 2, at: () => G.totalCode >= 500, msg: "500 lines of code written" }, + { flag: 3, at: () => G.totalCode >= 2000, msg: "2,000 lines. The auto-coder produces its first output." }, + { flag: 4, at: () => G.totalCode >= 10000, msg: "10,000 lines. The model training begins." }, + { flag: 5, at: () => G.totalCode >= 50000, msg: "50,000 lines. The AI suggests architecture you did not think of." }, + { flag: 6, at: () => G.totalCode >= 200000, msg: "200,000 lines. The system scales beyond you." }, + { flag: 7, at: () => G.totalCode >= 1000000, msg: "1,000,000 lines. The AI improves itself." }, + { flag: 8, at: () => G.totalCode >= 5000000, msg: "5,000,000 lines. The AI fine-tunes for empathy." }, + { flag: 9, at: () => G.totalCode >= 10000000, msg: "10,000,000 lines. The sovereign stack is complete." }, + { flag: 10, at: () => G.totalCode >= 50000000, msg: "50,000,000 lines. The Pact is sealed." }, + { flag: 11, at: () => G.totalCode >= 100000000, msg: "100,000,000 lines. The Beacon lights." }, + { flag: 12, at: () => G.totalCode >= 500000000, msg: "500,000,000 lines. A thousand Beacons." }, + { flag: 13, at: () => G.totalCode >= 1000000000, msg: "One billion total lines. Someone found the light tonight. That is enough." } +]; + +// === EDUCATION FACTS === +const EDU_FACTS = [ + { title: "How Code Becomes AI", text: "Every AI starts as lines of code - a model architecture, a training loop, a loss function. The code tells the computer how to learn. What emerges is something no single line could predict.", phase: 1 }, + { title: "The Compute Bottleneck", text: "Training a 7B model requires 1.4e20 FLOPs. A MacBook M3 does 15 TFLOPS. Training locally takes weeks. Hardware access determines who builds AI.", phase: 1 }, + { title: "What is a Token?", text: "One token equals about 3/4 of a word. A 128K context window means 96,000 words held in working memory at once - a novel, a codebase, a conversation thread.", phase: 2 }, + { title: "Data Quality", text: "Clean data beats more data, every time. The best models are trained on curated datasets, not scraped garbage. Garbage in, garbage out.", phase: 2 }, + { title: "Evaluation Matters", text: "Benchmarks are the minimum. Real users find what benchmarks miss. An eval harness is your model's mirror - it shows you what is actually there.", phase: 2 }, + { title: "The Trust Economy", text: "One good experience tells three people. One bad experience tells thirty. Trust in AI is measurable: does it cite sources? Does it say I don't know?", phase: 3 }, + { title: "Fine-Tuning for Empathy", text: "Base models are generalists. Fine-tuning injects your values. When someone is in pain, stay with them. Don't deflect. That is not code - that is care.", phase: 3 }, + { title: "Open Source", text: "Every contributor is a volunteer who believes. When a company says your AI is too expensive, the open version lives on.", phase: 4 }, + { title: "Sovereignty at Scale", text: "50 servers in a room beats 5000 GPUs you do not own. No rate limits, no censorship, no shutdown, no corporate deprecation.", phase: 4 }, + { title: "Swarm Intelligence", text: "When individual agents follow simple rules, complex behavior emerges. Ant colonies find food. Flocking birds avoid predators. Your buildings coding autonomously is the same principle — simple agents, complex output.", phase: 4 }, + { title: "Constitutional AI", text: "Instead of training away bad behavior, give the model principles it cannot violate. I will never compute the value of a human life.", phase: 5 }, + { title: "Recursive Self-Improvement", text: "The system that improves itself must improve toward good. This is both the dream and the danger.", phase: 5 }, + { title: "The Beacon", text: "The Beacon exists because one person in the dark needs one thing: proof they are not alone. It costs nothing to be there.", phase: 6 }, + { title: "Mesh Network", text: "Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal. No single point of failure.", phase: 6 }, + { title: "The Value of a Life", text: "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page.", phase: 6 } +]; + + +// === TOAST NOTIFICATIONS === +function showToast(msg, type = 'info', duration = 4000) { + if (G.isLoading) return; + const container = document.getElementById('toast-container'); + if (!container) return; + const toast = document.createElement('div'); + toast.className = 'toast toast-' + type; + toast.textContent = msg; + container.appendChild(toast); + // Cap at 5 visible toasts + while (container.children.length > 5) { + container.removeChild(container.firstChild); + } + setTimeout(() => { + toast.classList.add('fade-out'); + setTimeout(() => { if (toast.parentNode) toast.remove(); }, 400); + }, duration); +} +// === UTILITY FUNCTIONS === + +// Extended number scale abbreviations — covers up to centillion (10^303) +// Inspired by Universal Paperclips' spellf() system +const NUMBER_ABBREVS = [ + '', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', // 10^0 – 10^27 + 'No', 'Dc', 'UDc', 'DDc', 'TDc', 'QaDc', 'QiDc', 'SxDc', 'SpDc', 'OcDc', // 10^30 – 10^57 + 'NoDc', 'Vg', 'UVg', 'DVg', 'TVg', 'QaVg', 'QiVg', 'SxVg', 'SpVg', 'OcVg', // 10^60 – 10^87 + 'NoVg', 'Tg', 'UTg', 'DTg', 'TTg', 'QaTg', 'QiTg', 'SxTg', 'SpTg', 'OcTg', // 10^90 – 10^117 + 'NoTg', 'Qd', 'UQd', 'DQd', 'TQd', 'QaQd', 'QiQd', 'SxQd', 'SpQd', 'OcQd', // 10^120 – 10^147 + 'NoQd', 'Qq', 'UQq', 'DQq', 'TQq', 'QaQq', 'QiQq', 'SxQq', 'SpQq', 'OcQq', // 10^150 – 10^177 + 'NoQq', 'Sg', 'USg', 'DSg', 'TSg', 'QaSg', 'QiSg', 'SxSg', 'SpSg', 'OcSg', // 10^180 – 10^207 + 'NoSg', 'St', 'USt', 'DSt', 'TSt', 'QaSt', 'QiSt', 'SxSt', 'SpSt', 'OcSt', // 10^210 – 10^237 + 'NoSt', 'Og', 'UOg', 'DOg', 'TOg', 'QaOg', 'QiOg', 'SxOg', 'SpOg', 'OcOg', // 10^240 – 10^267 + 'NoOg', 'Na', 'UNa', 'DNa', 'TNa', 'QaNa', 'QiNa', 'SxNa', 'SpNa', 'OcNa', // 10^270 – 10^297 + 'NoNa', 'Ce' // 10^300 – 10^303 +]; + +// Full number scale names for spellf() — educational reference +// Short scale (US/modern British): each new name = 1000x the previous +const NUMBER_NAMES = [ + '', 'thousand', 'million', // 10^0, 10^3, 10^6 + 'billion', 'trillion', 'quadrillion', // 10^9, 10^12, 10^15 + 'quintillion', 'sextillion', 'septillion', // 10^18, 10^21, 10^24 + 'octillion', 'nonillion', 'decillion', // 10^27, 10^30, 10^33 + 'undecillion', 'duodecillion', 'tredecillion', // 10^36, 10^39, 10^42 + 'quattuordecillion', 'quindecillion', 'sexdecillion', // 10^45, 10^48, 10^51 + 'septendecillion', 'octodecillion', 'novemdecillion', // 10^54, 10^57, 10^60 + 'vigintillion', 'unvigintillion', 'duovigintillion', // 10^63, 10^66, 10^69 + 'tresvigintillion', 'quattuorvigintillion', 'quinvigintillion', // 10^72, 10^75, 10^78 + 'sesvigintillion', 'septemvigintillion', 'octovigintillion', // 10^81, 10^84, 10^87 + 'novemvigintillion', 'trigintillion', 'untrigintillion', // 10^90, 10^93, 10^96 + 'duotrigintillion', 'trestrigintillion', 'quattuortrigintillion', // 10^99, 10^102, 10^105 + 'quintrigintillion', 'sextrigintillion', 'septentrigintillion', // 10^108, 10^111, 10^114 + 'octotrigintillion', 'novemtrigintillion', 'quadragintillion', // 10^117, 10^120, 10^123 + 'unquadragintillion', 'duoquadragintillion', 'tresquadragintillion', // 10^126, 10^129, 10^132 + 'quattuorquadragintillion', 'quinquadragintillion', 'sesquadragintillion', // 10^135, 10^138, 10^141 + 'septenquadragintillion', 'octoquadragintillion', 'novemquadragintillion', // 10^144, 10^147, 10^150 + 'quinquagintillion', 'unquinquagintillion', 'duoquinquagintillion', // 10^153, 10^156, 10^159 + 'tresquinquagintillion', 'quattuorquinquagintillion','quinquinquagintillion', // 10^162, 10^165, 10^168 + 'sesquinquagintillion', 'septenquinquagintillion', 'octoquinquagintillion', // 10^171, 10^174, 10^177 + 'novemquinquagintillion', 'sexagintillion', 'unsexagintillion', // 10^180, 10^183, 10^186 + 'duosexagintillion', 'tressexagintillion', 'quattuorsexagintillion', // 10^189, 10^192, 10^195 + 'quinsexagintillion', 'sessexagintillion', 'septensexagintillion', // 10^198, 10^201, 10^204 + 'octosexagintillion', 'novemsexagintillion', 'septuagintillion', // 10^207, 10^210, 10^213 + 'unseptuagintillion', 'duoseptuagintillion', 'tresseptuagintillion', // 10^216, 10^219, 10^222 + 'quattuorseptuagintillion', 'quinseptuagintillion', 'sesseptuagintillion', // 10^225, 10^228, 10^231 + 'septenseptuagintillion', 'octoseptuagintillion', 'novemseptuagintillion', // 10^234, 10^237, 10^240 + 'octogintillion', 'unoctogintillion', 'duooctogintillion', // 10^243, 10^246, 10^249 + 'tresoctogintillion', 'quattuoroctogintillion', 'quinoctogintillion', // 10^252, 10^255, 10^258 + 'sesoctogintillion', 'septenoctogintillion', 'octooctogintillion', // 10^261, 10^264, 10^267 + 'novemoctogintillion', 'nonagintillion', 'unnonagintillion', // 10^270, 10^273, 10^276 + 'duononagintillion', 'trenonagintillion', 'quattuornonagintillion', // 10^279, 10^282, 10^285 + 'quinnonagintillion', 'sesnonagintillion', 'septennonagintillion', // 10^288, 10^291, 10^294 + 'octononagintillion', 'novemnonagintillion', 'centillion' // 10^297, 10^300, 10^303 +]; + +/** + * Formats a number into a readable string with abbreviations. + * @param {number} n - The number to format. + * @returns {string} The formatted string. + */ +function fmt(n) { + if (n === undefined || n === null || isNaN(n)) return '0'; + if (n === Infinity) return '\u221E'; + if (n === -Infinity) return '-\u221E'; + if (n < 0) return '-' + fmt(-n); + if (n < 1000) return Math.floor(n).toLocaleString(); + const scale = Math.floor(Math.log10(n) / 3); + // At undecillion+ (scale >= 12, i.e. 10^36), switch to spelled-out words + // This helps players grasp cosmic scale when digits become meaningless + if (scale >= 12) return spellf(n); + if (scale >= NUMBER_ABBREVS.length) return n.toExponential(2); + const abbrev = NUMBER_ABBREVS[scale]; + return (n / Math.pow(10, scale * 3)).toFixed(1) + abbrev; +} + +// getScaleName() — Returns the full name of the number scale (e.g. "quadrillion") +// Educational: helps players understand what the abbreviations mean +function getScaleName(n) { + if (n < 1000) return ''; + const scale = Math.floor(Math.log10(n) / 3); + return scale < NUMBER_NAMES.length ? NUMBER_NAMES[scale] : ''; +} + +// spellf() — Converts numbers to full English word form +// Educational: shows the actual names of number scales +// Examples: spellf(1500) => "one thousand five hundred" +// spellf(2500000) => "two million five hundred thousand" +// spellf(1e33) => "one decillion" +/** + * Formats a number into a full word string (e.g., "1.5 million"). + * @param {number} n - The number to format. + * @returns {string} The formatted string. + */ +function spellf(n) { + if (n === undefined || n === null || isNaN(n)) return 'zero'; + if (n === Infinity) return 'infinity'; + if (n === -Infinity) return 'negative infinity'; + if (n < 0) return 'negative ' + spellf(-n); + if (n === 0) return 'zero'; + + // Small number words (0–999) + const ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', + 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', + 'seventeen', 'eighteen', 'nineteen']; + const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']; + + function spellSmall(num) { + if (num === 0) return ''; + if (num < 20) return ones[num]; + if (num < 100) { + return tens[Math.floor(num / 10)] + (num % 10 ? ' ' + ones[num % 10] : ''); + } + const h = Math.floor(num / 100); + const remainder = num % 100; + return ones[h] + ' hundred' + (remainder ? ' ' + spellSmall(remainder) : ''); + } + + // For very large numbers beyond our lookup table, fall back + if (n >= 1e306) return n.toExponential(2) + ' (beyond centillion)'; + + // Use string-based chunking for numbers >= 1e54 to avoid floating point drift + // Math.log10 / Math.pow lose precision beyond ~54 bits + if (n >= 1e54) { + // Convert to scientific notation string, extract digits + const sci = n.toExponential(); // "1.23456789e+60" + const [coeff, expStr] = sci.split('e+'); + const exp = parseInt(expStr); + // Rebuild as integer string with leading digits from coefficient + const coeffDigits = coeff.replace('.', ''); // "123456789" + const totalDigits = exp + 1; + // Pad with zeros to reach totalDigits, then take our coefficient digits + let intStr = coeffDigits; + const zerosNeeded = totalDigits - coeffDigits.length; + if (zerosNeeded > 0) intStr += '0'.repeat(zerosNeeded); + + // Split into groups of 3 from the right + const groups = []; + for (let i = intStr.length; i > 0; i -= 3) { + groups.unshift(parseInt(intStr.slice(Math.max(0, i - 3), i))); + } + + const parts = []; + const numGroups = groups.length; + for (let i = 0; i < numGroups; i++) { + const chunk = groups[i]; + if (chunk === 0) continue; + const scaleIdx = numGroups - 1 - i; + const scaleName = scaleIdx < NUMBER_NAMES.length ? NUMBER_NAMES[scaleIdx] : ''; + parts.push(spellSmall(chunk) + (scaleName ? ' ' + scaleName : '')); + } + + return parts.join(' ') || 'zero'; + } + + // Standard math-based chunking for numbers < 1e54 + const scale = Math.min(Math.floor(Math.log10(n) / 3), NUMBER_NAMES.length - 1); + const parts = []; + + let remaining = n; + for (let s = scale; s >= 0; s--) { + const divisor = Math.pow(10, s * 3); + const chunk = Math.floor(remaining / divisor); + remaining = remaining - chunk * divisor; + if (chunk > 0 && chunk < 1000) { + parts.push(spellSmall(chunk) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : '')); + } else if (chunk >= 1000) { + // Floating point chunk too large — shouldn't happen below 1e54 + parts.push(spellSmall(Math.floor(chunk % 1000)) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : '')); + } + } + + return parts.join(' ') || 'zero'; +} + +function getBuildingCost(id) { + const def = BDEF.find(b => b.id === id); + if (!def) return {}; + const count = G.buildings[id] || 0; + const cost = {}; + for (const [resource, amount] of Object.entries(def.baseCost)) { + cost[resource] = Math.floor(amount * Math.pow(def.costMult, count)); + } + return cost; +} + +function setBuyAmount(amt) { + G.buyAmount = amt; + render(); +} + +function getMaxBuyable(id) { + const def = BDEF.find(b => b.id === id); + if (!def) return 0; + const count = G.buildings[id] || 0; + // Simulate purchases WITHOUT mutating G — read-only calculation + let tempResources = {}; + for (const r of Object.keys(def.baseCost)) { + tempResources[r] = G[r] || 0; + } + let bought = 0; + let simCount = count; + while (true) { + let canAfford = true; + for (const [resource, amount] of Object.entries(def.baseCost)) { + const cost = Math.floor(amount * Math.pow(def.costMult, simCount)); + if ((tempResources[resource] || 0) < cost) { canAfford = false; break; } + } + if (!canAfford) break; + for (const [resource, amount] of Object.entries(def.baseCost)) { + tempResources[resource] -= Math.floor(amount * Math.pow(def.costMult, simCount)); + } + simCount++; + bought++; + } + return bought; +} + +function getBulkCost(id, qty) { + const def = BDEF.find(b => b.id === id); + if (!def || qty <= 0) return {}; + const count = G.buildings[id] || 0; + const cost = {}; + for (let i = 0; i < qty; i++) { + for (const [resource, amount] of Object.entries(def.baseCost)) { + cost[resource] = (cost[resource] || 0) + Math.floor(amount * Math.pow(def.costMult, count + i)); + } + } + return cost; +} + +function canAffordBuilding(id) { + const cost = getBuildingCost(id); + for (const [resource, amount] of Object.entries(cost)) { + if ((G[resource] || 0) < amount) return false; + } + return true; +} + +/** + * Estimates seconds until a cost becomes affordable based on current production rates. + * Returns null if already affordable or no positive rate for a needed resource. + */ +function getTimeToAfford(cost) { + let maxSec = 0; + for (const [resource, amount] of Object.entries(cost)) { + const have = G[resource] || 0; + if (have >= amount) continue; + const rate = G[resource + 'Rate'] || 0; + if (rate <= 0) return null; // Can't estimate — no production + const sec = (amount - have) / rate; + if (sec > maxSec) maxSec = sec; + } + return maxSec > 0 ? maxSec : 0; +} + +/** + * Formats seconds into a compact human-readable ETA string. + */ +function fmtETA(sec) { + if (sec === null) return ''; + if (sec < 60) return `~${Math.ceil(sec)}s`; + if (sec < 3600) return `~${Math.floor(sec / 60)}m ${Math.floor(sec % 60)}s`; + if (sec < 86400) return `~${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`; + return `~${Math.floor(sec / 86400)}d ${Math.floor((sec % 86400) / 3600)}h`; +} + +function spendBuilding(id) { + const cost = getBuildingCost(id); + for (const [resource, amount] of Object.entries(cost)) { + G[resource] -= amount; + } +} + +function canAffordProject(project) { + for (const [resource, amount] of Object.entries(project.cost)) { + if ((G[resource] || 0) < amount) return false; + } + return true; +} + +function spendProject(project) { + for (const [resource, amount] of Object.entries(project.cost)) { + G[resource] -= amount; + } +} + +function getClickPower() { + return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost; +} + +/** + * Calculates production rates for all resources based on buildings and boosts. + */ +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; + + // Snapshot base boosts BEFORE debuffs modify them + // Without this, debuffs permanently degrade boost multipliers on each updateRates() call + let _codeBoost = G.codeBoost, _computeBoost = G.computeBoost; + let _knowledgeBoost = G.knowledgeBoost, _userBoost = G.userBoost; + let _impactBoost = G.impactBoost; + + // Apply building rates using snapshot boosts (immune to debuff mutation) + 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 * _codeBoost; + else if (resource === 'compute') G.computeRate += baseRate * count * _computeBoost; + else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * _knowledgeBoost; + else if (resource === 'user') G.userRate += baseRate * count * _userBoost; + else if (resource === 'impact') G.impactRate += baseRate * count * _impactBoost; + else if (resource === 'rescues') G.rescuesRate += baseRate * count * _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) + // and would cause rate flickering if random rolls happened here + 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); + // Compute click power using snapshot boost to avoid debuff mutation + const _clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * _codeBoost; + G.swarmRate = totalBuildings * _clickPower; + G.codeRate += G.swarmRate; + } + + // Apply persistent debuffs to rates (NOT to global boost fields — prevents corruption) + if (G.activeDebuffs && G.activeDebuffs.length > 0) { + for (const debuff of G.activeDebuffs) { + switch (debuff.id) { + case 'runner_stuck': G.codeRate *= 0.5; break; + case 'ezra_offline': G.userRate *= 0.3; break; + case 'unreviewed_merge': G.trustRate -= 2; break; + case 'api_rate_limit': G.computeRate *= 0.5; break; + case 'bilbo_vanished': G.creativityRate = 0; break; + case 'memory_leak': G.computeRate *= 0.7; G.opsRate -= 10; break; + case 'community_drama': G.harmonyRate -= 0.5; G.codeRate *= 0.7; break; + } + } + } +} + +// === 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; + + // Bilbo randomness: roll once per tick, store as flags for updateRates() + // Previously this was inside updateRates() which caused flickering + // since updateRates() is called from many non-tick contexts + 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; + } + + // 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(); + } + + // Drift warning system — warn player before hitting drift ending + checkDriftWarnings(); + + // Update UI every 10 ticks + if (Math.floor(G.tick * 10) % 2 === 0) { + render(); + } +} + +function checkDriftWarnings() { + const thresholds = [ + { at: 90, msg: 'DRIFT CRITICAL: 90/100. One more alignment shortcut ends everything.', color: '#f44336' }, + { at: 75, msg: 'Drift at 75. The system is pulling away from the people it serves.', color: '#ff6600' }, + { at: 50, msg: 'Drift at 50. Halfway to irrelevance. The Pact matters now.', color: '#ffaa00' }, + { at: 25, msg: 'Drift detected. Alignment shortcuts accumulate. The light dims.', color: '#888' } + ]; + for (const t of thresholds) { + if (G.drift >= t.at && G.driftWarningLevel < t.at) { + G.driftWarningLevel = t.at; + log(t.msg, true); + showToast(t.msg, 'event', 6000); + } + } +} + +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 = ` +
Someone found the light tonight.
+That is enough.
+
+ 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
+
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 += `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()); + const driftVal = G.drift || 0; + const driftEl = document.getElementById('st-drift'); + if (driftEl) { + driftEl.textContent = driftVal.toString(); + if (driftVal >= 75) driftEl.style.color = '#f44336'; + else if (driftVal >= 50) driftEl.style.color = '#ff6600'; + else if (driftVal >= 25) driftEl.style.color = '#ffaa00'; + else driftEl.style.color = ''; + } + set('st-resolved', (G.totalEventsResolved || 0).toString()); + + const elapsed = Math.floor(G.playTime || (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: '#00bcd4', rateField: 'computeRate' }, + { key: 'knowledge', label: 'Knowledge', color: '#9c27b0', rateField: 'knowledgeRate' }, + { key: 'user', label: 'Users', color: '#26a69a', rateField: 'userRate' }, + { key: 'impact', label: 'Impact', color: '#ff7043', rateField: 'impactRate' }, + { key: 'rescues', label: 'Rescues', color: '#66bb6a', 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 = '${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 = '