diff --git a/game.js b/game.js deleted file mode 100644 index f8acf9a..0000000 --- a/game.js +++ /dev/null @@ -1,2775 +0,0 @@ -// ============================================================ -// 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, - 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; -} - -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; - - // 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; - - // 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 boost -30%', - applyFn: () => { G.harmonyRate -= 0.5; G.codeBoost *= 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}`; - } - - const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${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; -} - -function render() { - renderResources(); - renderPhase(); - renderBuildings(); - renderProjects(); - renderStats(); - updateEducation(); - renderAlignment(); - renderProgress(); - renderCombo(); - renderDebuffs(); - renderSprint(); - renderPulse(); -} - -function renderAlignment() { - const container = document.getElementById('alignment-ui'); - if (!container) return; - if (G.pendingAlignment) { - container.innerHTML = ` -
-
ALIGNMENT EVENT: The Drift
-
An optimization suggests removing the human override. +40% efficiency.
-
- - -
-
- `; - container.style.display = 'block'; - } else { - container.innerHTML = ''; - container.style.display = 'none'; - } -} - -// === OFFLINE GAINS POPUP === -function showOfflinePopup(timeLabel, gains, offSec) { - const el = document.getElementById('offline-popup'); - if (!el) return; - const timeEl = document.getElementById('offline-time-label'); - if (timeEl) timeEl.textContent = `You were away for ${timeLabel}.`; - - const listEl = document.getElementById('offline-gains-list'); - if (listEl) { - let html = ''; - for (const g of gains) { - html += `
`; - html += `${g.label}`; - html += `+${fmt(g.value)}`; - html += `
`; - } - // Show offline efficiency note - html += `
Offline efficiency: 50%
`; - listEl.innerHTML = html; - } - - el.style.display = 'flex'; -} - -function dismissOfflinePopup() { - const el = document.getElementById('offline-popup'); - if (el) el.style.display = 'none'; -} - -// === EXPORT / IMPORT SAVE FILES === -function exportSave() { - const raw = localStorage.getItem('the-beacon-v2'); - if (!raw) { log('No save data to export.'); return; } - const blob = new Blob([raw], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - const ts = new Date().toISOString().slice(0, 10); - a.download = `beacon-save-${ts}.json`; - a.click(); - URL.revokeObjectURL(url); - log('Save exported to file.'); -} - -function importSave() { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json,application/json'; - input.onchange = function(e) { - const file = e.target.files[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = function(ev) { - try { - const data = JSON.parse(ev.target.result); - if (!data.code && !data.totalCode && !data.buildings) { - log('Import failed: file does not look like a Beacon save.'); - return; - } - if (confirm('Import this save? Current progress will be overwritten.')) { - localStorage.setItem('the-beacon-v2', ev.target.result); - location.reload(); - } - } catch (err) { - log('Import failed: invalid JSON file.'); - } - }; - reader.readAsText(file); - }; - input.click(); -} - -// === SAVE / LOAD === -function showSaveToast() { - const el = document.getElementById('save-toast'); - if (!el) return; - const elapsed = Math.floor((Date.now() - G.startedAt) / 1000); - const m = Math.floor(elapsed / 60); - const s = elapsed % 60; - el.textContent = `Saved [${m}:${s.toString().padStart(2, '0')}]`; - el.style.display = 'block'; - void el.offsetHeight; - el.style.opacity = '1'; - setTimeout(() => { el.style.opacity = '0'; }, 1500); - setTimeout(() => { el.style.display = 'none'; }, 2000); -} - -/** - * Persists the current game state to localStorage. - */ -function saveGame() { - // Save debuff IDs (can't serialize functions) - const debuffIds = (G.activeDebuffs || []).map(d => d.id); - const saveData = { - version: 1, - code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact, - ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony, - totalCode: G.totalCode, totalCompute: G.totalCompute, totalKnowledge: G.totalKnowledge, - totalUsers: G.totalUsers, totalImpact: G.totalImpact, - buildings: G.buildings, - codeBoost: G.codeBoost, computeBoost: G.computeBoost, knowledgeBoost: G.knowledgeBoost, - userBoost: G.userBoost, impactBoost: G.impactBoost, - milestoneFlag: G.milestoneFlag, phase: G.phase, - deployFlag: G.deployFlag, sovereignFlag: G.sovereignFlag, beaconFlag: G.beaconFlag, - memoryFlag: G.memoryFlag, pactFlag: G.pactFlag, - lazarusFlag: G.lazarusFlag || 0, mempalaceFlag: G.mempalaceFlag || 0, ciFlag: G.ciFlag || 0, - branchProtectionFlag: G.branchProtectionFlag || 0, nightlyWatchFlag: G.nightlyWatchFlag || 0, - nostrFlag: G.nostrFlag || 0, - milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects, - totalClicks: G.totalClicks, startedAt: G.startedAt, - flags: G.flags, - rescues: G.rescues || 0, totalRescues: G.totalRescues || 0, - drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false, - lastEventAt: G.lastEventAt || 0, - activeDebuffIds: debuffIds, - totalEventsResolved: G.totalEventsResolved || 0, - buyAmount: G.buyAmount || 1, - sprintActive: G.sprintActive || false, - sprintTimer: G.sprintTimer || 0, - sprintCooldown: G.sprintCooldown || 0, - swarmFlag: G.swarmFlag || 0, - swarmRate: G.swarmRate || 0, - strategicFlag: G.strategicFlag || 0, - projectsCollapsed: G.projectsCollapsed !== false, - savedAt: Date.now() - }; - - localStorage.setItem('the-beacon-v2', JSON.stringify(saveData)); - showSaveToast(); -} - -/** - * Loads the game state from localStorage and reconstitutes the game engine. - * @returns {boolean} True if load was successful. - */ -function loadGame() { - const raw = localStorage.getItem('the-beacon-v2'); - if (!raw) return false; - - try { - const data = JSON.parse(raw); - - // Whitelist properties that can be loaded - const whitelist = [ - 'code', 'compute', 'knowledge', 'users', 'impact', 'ops', 'trust', 'creativity', 'harmony', - 'totalCode', 'totalCompute', 'totalKnowledge', 'totalUsers', 'totalImpact', - 'buildings', 'codeBoost', 'computeBoost', 'knowledgeBoost', 'userBoost', 'impactBoost', - 'milestoneFlag', 'phase', 'deployFlag', 'sovereignFlag', 'beaconFlag', - 'memoryFlag', 'pactFlag', 'lazarusFlag', 'mempalaceFlag', 'ciFlag', - 'branchProtectionFlag', 'nightlyWatchFlag', 'nostrFlag', - 'milestones', 'completedProjects', 'activeProjects', - 'totalClicks', 'startedAt', 'flags', 'rescues', 'totalRescues', - 'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment', - 'lastEventAt', 'totalEventsResolved', 'buyAmount', - 'sprintActive', 'sprintTimer', 'sprintCooldown', - 'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed' - ]; - - G.isLoading = true; - - whitelist.forEach(key => { - if (data.hasOwnProperty(key)) { - G[key] = data[key]; - } - }); - - // Restore sprint state properly - // codeBoost was saved with the sprint multiplier baked in - if (data.sprintActive) { - // Sprint was active when saved — check if it expired during offline time - const offSec = data.savedAt ? (Date.now() - data.savedAt) / 1000 : 0; - const remaining = (data.sprintTimer || 0) - offSec; - if (remaining > 0) { - // Sprint still going — keep boost, update timer - G.sprintActive = true; - G.sprintTimer = remaining; - G.sprintCooldown = 0; - } else { - // Sprint expired during offline — remove boost, start cooldown - G.sprintActive = false; - G.sprintTimer = 0; - G.codeBoost /= G.sprintMult; - const cdRemaining = G.sprintCooldownMax + remaining; // remaining is negative - G.sprintCooldown = Math.max(0, cdRemaining); - } - } - // If not sprintActive at save time, codeBoost is correct as-is - - // Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed) - if (data.activeDebuffIds && data.activeDebuffIds.length > 0) { - G.activeDebuffs = []; - for (const id of data.activeDebuffIds) { - const evDef = EVENTS.find(e => e.id === id); - if (evDef) { - // Re-fire the event to get the full debuff object with applyFn - evDef.effect(); - } - } - } else { - G.activeDebuffs = []; - } - - updateRates(); - G.isLoading = false; - - // Offline progress - if (data.savedAt) { - const offSec = (Date.now() - data.savedAt) / 1000; - if (offSec > 30) { // Only if away for more than 30 seconds - updateRates(); - const f = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency - const gc = G.codeRate * offSec * f; - const cc = G.computeRate * offSec * f; - const kc = G.knowledgeRate * offSec * f; - const uc = G.userRate * offSec * f; - const ic = G.impactRate * offSec * f; - - const rc = G.rescuesRate * offSec * f; - const oc = G.opsRate * offSec * f; - const tc = G.trustRate * offSec * f; - const crc = G.creativityRate * offSec * f; - const hc = G.harmonyRate * offSec * f; - - G.code += gc; G.compute += cc; G.knowledge += kc; - G.users += uc; G.impact += ic; - G.rescues += rc; G.ops += oc; G.trust += tc; - G.creativity += crc; - G.harmony = Math.max(0, Math.min(100, G.harmony + hc)); - G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc; - G.totalUsers += uc; G.totalImpact += ic; - G.totalRescues += rc; - - // Show welcome-back popup with all gains - const gains = []; - if (gc > 0) gains.push({ label: 'Code', value: gc, color: '#4a9eff' }); - if (cc > 0) gains.push({ label: 'Compute', value: cc, color: '#4a9eff' }); - if (kc > 0) gains.push({ label: 'Knowledge', value: kc, color: '#4a9eff' }); - if (uc > 0) gains.push({ label: 'Users', value: uc, color: '#4a9eff' }); - if (ic > 0) gains.push({ label: 'Impact', value: ic, color: '#4a9eff' }); - if (rc > 0) gains.push({ label: 'Rescues', value: rc, color: '#4caf50' }); - if (oc > 0) gains.push({ label: 'Ops', value: oc, color: '#b388ff' }); - if (tc > 0) gains.push({ label: 'Trust', value: tc, color: '#4caf50' }); - if (crc > 0) gains.push({ label: 'Creativity', value: crc, color: '#ffd700' }); - - const awayMin = Math.floor(offSec / 60); - const awaySec = Math.floor(offSec % 60); - const timeLabel = awayMin >= 1 ? `${awayMin} minute${awayMin !== 1 ? 's' : ''}` : `${awaySec} seconds`; - - if (gains.length > 0) { - showOfflinePopup(timeLabel, gains, offSec); - } - - // Log summary - const parts = []; - if (gc > 0) parts.push(`${fmt(gc)} code`); - if (kc > 0) parts.push(`${fmt(kc)} knowledge`); - if (uc > 0) parts.push(`${fmt(uc)} users`); - if (ic > 0) parts.push(`${fmt(ic)} impact`); - if (rc > 0) parts.push(`${fmt(rc)} rescues`); - if (oc > 0) parts.push(`${fmt(oc)} ops`); - if (tc > 0) parts.push(`${fmt(tc)} trust`); - log(`Welcome back! While away (${timeLabel}): ${parts.join(', ')}`); - } - } - - return true; - } catch (e) { - console.error('Load failed:', e); - return false; - } -} - -// === INITIALIZATION === -function initGame() { - G.startedAt = Date.now(); - G.startTime = Date.now(); - G.phase = 1; - G.deployFlag = 0; - G.sovereignFlag = 0; - G.beaconFlag = 0; - updateRates(); - render(); - renderPhase(); - - log('The screen is blank. Write your first line of code.', true); - log('Click WRITE CODE or press SPACE to start.'); - log('Build AutoCode for passive production.'); - log('Watch for Research Projects to appear.'); - log('Keys: SPACE=Code S=Sprint 1-4=Ops B=Buy x1/10/MAX E=Export I=Import Ctrl+S=Save ?=Help'); - log('Tip: Click fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code.'); -} - -window.addEventListener('load', function () { - if (!loadGame()) { - initGame(); - } else { - render(); - renderPhase(); - if (G.driftEnding) { - G.running = false; - renderDriftEnding(); - } else if (G.beaconEnding) { - G.running = false; - renderBeaconEnding(); - } else { - log('Game loaded. Welcome back to The Beacon.'); - } - } - - // Game loop at 10Hz (100ms tick) - setInterval(tick, 100); - - // Auto-save every 30 seconds - setInterval(saveGame, CONFIG.AUTO_SAVE_INTERVAL); - - // Update education every 10 seconds - setInterval(updateEducation, 10000); -}); - -// Help overlay -function toggleHelp() { - const el = document.getElementById('help-overlay'); - if (!el) return; - const isOpen = el.style.display === 'flex'; - el.style.display = isOpen ? 'none' : 'flex'; -} - -// Keyboard shortcuts -window.addEventListener('keydown', function (e) { - // Help toggle (? or /) — works even in input fields - if (e.key === '?' || e.key === '/') { - // Only trigger ? when not typing in an input - if (e.target === document.body || e.key === '?') { - if (e.key === '?' || (e.key === '/' && e.target === document.body)) { - e.preventDefault(); - toggleHelp(); - return; - } - } - } - if (e.code === 'Space' && e.target === document.body) { - e.preventDefault(); - writeCode(); - } - if (e.target !== document.body) return; - if (e.code === 'Digit1') doOps('boost_code'); - if (e.code === 'Digit2') doOps('boost_compute'); - if (e.code === 'Digit3') doOps('boost_knowledge'); - if (e.code === 'Digit4') doOps('boost_trust'); - if (e.code === 'KeyB') { - // Cycle: 1 -> 10 -> MAX -> 1 - if (G.buyAmount === 1) setBuyAmount(10); - else if (G.buyAmount === 10) setBuyAmount(-1); - else setBuyAmount(1); - } - if (e.code === 'KeyS') activateSprint(); - if (e.code === 'KeyE') exportSave(); - if (e.code === 'KeyI') importSave(); - if (e.code === 'Escape') { - const el = document.getElementById('help-overlay'); - if (el && el.style.display === 'flex') toggleHelp(); - } -}); - -// Ctrl+S to save (must be on keydown to preventDefault) -window.addEventListener('keydown', function (e) { - if ((e.ctrlKey || e.metaKey) && e.code === 'KeyS') { - e.preventDefault(); - saveGame(); - } -});