2980 lines
124 KiB
JavaScript
2980 lines
124 KiB
JavaScript
// ============================================================
|
||
// 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;
|
||
|
||
// Snapshot base boosts BEFORE debuffs modify them
|
||
// Without this, debuffs permanently degrade boost multipliers on each updateRates() call
|
||
let _codeBoost = G.codeBoost, _computeBoost = G.computeBoost;
|
||
let _knowledgeBoost = G.knowledgeBoost, _userBoost = G.userBoost;
|
||
let _impactBoost = G.impactBoost;
|
||
|
||
// Apply building rates using snapshot boosts (immune to debuff mutation)
|
||
for (const def of BDEF) {
|
||
const count = G.buildings[def.id] || 0;
|
||
if (count > 0 && def.rates) {
|
||
for (const [resource, baseRate] of Object.entries(def.rates)) {
|
||
if (resource === 'code') G.codeRate += baseRate * count * _codeBoost;
|
||
else if (resource === 'compute') G.computeRate += baseRate * count * _computeBoost;
|
||
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * _knowledgeBoost;
|
||
else if (resource === 'user') G.userRate += baseRate * count * _userBoost;
|
||
else if (resource === 'impact') G.impactRate += baseRate * count * _impactBoost;
|
||
else if (resource === 'rescues') G.rescuesRate += baseRate * count * _impactBoost;
|
||
else if (resource === 'ops') G.opsRate += baseRate * count;
|
||
else if (resource === 'trust') G.trustRate += baseRate * count;
|
||
else if (resource === 'creativity') G.creativityRate += baseRate * count;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Passive generation
|
||
G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT);
|
||
if (G.flags && G.flags.creativity) {
|
||
G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT);
|
||
}
|
||
if (G.pactFlag) G.trustRate += 2;
|
||
|
||
// Harmony: each wizard building contributes or detracts
|
||
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
|
||
(G.buildings.timmy || 0) + (G.buildings.fenrir || 0) + (G.buildings.bilbo || 0);
|
||
// Store harmony breakdown for tooltip
|
||
G.harmonyBreakdown = [];
|
||
if (wizardCount > 0) {
|
||
// Baseline harmony drain from complexity
|
||
const drain = -CONFIG.HARMONY_DRAIN_PER_WIZARD * wizardCount;
|
||
G.harmonyRate = drain;
|
||
G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain });
|
||
// The Pact restores harmony
|
||
if (G.pactFlag) {
|
||
const pact = CONFIG.PACT_HARMONY_GAIN * wizardCount;
|
||
G.harmonyRate += pact;
|
||
G.harmonyBreakdown.push({ label: 'The Pact', value: pact });
|
||
}
|
||
// Nightly Watch restores harmony
|
||
if (G.nightlyWatchFlag) {
|
||
const watch = CONFIG.WATCH_HARMONY_GAIN * wizardCount;
|
||
G.harmonyRate += watch;
|
||
G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch });
|
||
}
|
||
// MemPalace restores harmony
|
||
if (G.mempalaceFlag) {
|
||
const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount;
|
||
G.harmonyRate += mem;
|
||
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
|
||
}
|
||
}
|
||
// Active debuffs affecting harmony
|
||
if (G.activeDebuffs) {
|
||
for (const d of G.activeDebuffs) {
|
||
if (d.id === 'community_drama') {
|
||
G.harmonyBreakdown.push({ label: 'Community Drama', value: -0.5 });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Timmy multiplier based on harmony
|
||
if (G.buildings.timmy > 0) {
|
||
const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50));
|
||
const timmyCount = G.buildings.timmy;
|
||
G.codeRate += 5 * timmyCount * (timmyMult - 1);
|
||
G.computeRate += 2 * timmyCount * (timmyMult - 1);
|
||
G.knowledgeRate += 2 * timmyCount * (timmyMult - 1);
|
||
G.userRate += 5 * timmyCount * (timmyMult - 1);
|
||
}
|
||
|
||
// Bilbo randomness: 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);
|
||
// Compute click power using snapshot boost to avoid debuff mutation
|
||
const _clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * _codeBoost;
|
||
G.swarmRate = totalBuildings * _clickPower;
|
||
G.codeRate += G.swarmRate;
|
||
}
|
||
|
||
// Apply persistent debuffs to rates (NOT to global boost fields — prevents corruption)
|
||
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
|
||
for (const debuff of G.activeDebuffs) {
|
||
switch (debuff.id) {
|
||
case 'runner_stuck': G.codeRate *= 0.5; break;
|
||
case 'ezra_offline': G.userRate *= 0.3; break;
|
||
case 'unreviewed_merge': G.trustRate -= 2; break;
|
||
case 'api_rate_limit': G.computeRate *= 0.5; break;
|
||
case 'bilbo_vanished': G.creativityRate = 0; break;
|
||
case 'memory_leak': G.computeRate *= 0.7; G.opsRate -= 10; break;
|
||
case 'community_drama': G.harmonyRate -= 0.5; G.codeRate *= 0.7; break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// === CORE FUNCTIONS ===
|
||
/**
|
||
* Main game loop tick, called every 100ms.
|
||
*/
|
||
function tick() {
|
||
const dt = 1 / 10; // 100ms tick
|
||
|
||
// If game has ended (drift ending), stop ticking
|
||
if (!G.running) return;
|
||
|
||
// Apply production
|
||
G.code += G.codeRate * dt;
|
||
G.compute += G.computeRate * dt;
|
||
G.knowledge += G.knowledgeRate * dt;
|
||
G.users += G.userRate * dt;
|
||
G.impact += G.impactRate * dt;
|
||
G.rescues += G.rescuesRate * dt;
|
||
G.ops += G.opsRate * dt;
|
||
G.trust += G.trustRate * dt;
|
||
// NOTE: creativity is added conditionally below (only when ops near max)
|
||
G.harmony += G.harmonyRate * dt;
|
||
G.harmony = Math.max(0, Math.min(100, G.harmony));
|
||
|
||
// Track totals
|
||
G.totalCode += G.codeRate * dt;
|
||
G.totalCompute += G.computeRate * dt;
|
||
G.totalKnowledge += G.knowledgeRate * dt;
|
||
G.totalUsers += G.userRate * dt;
|
||
G.totalImpact += G.impactRate * dt;
|
||
G.totalRescues += G.rescuesRate * dt;
|
||
|
||
// Track maxes
|
||
G.maxCode = Math.max(G.maxCode, G.code);
|
||
G.maxCompute = Math.max(G.maxCompute, G.compute);
|
||
G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge);
|
||
G.maxUsers = Math.max(G.maxUsers, G.users);
|
||
G.maxImpact = Math.max(G.maxImpact, G.impact);
|
||
G.maxRescues = Math.max(G.maxRescues, G.rescues);
|
||
G.maxTrust = Math.max(G.maxTrust, G.trust);
|
||
G.maxOps = Math.max(G.maxOps, G.ops);
|
||
G.maxHarmony = Math.max(G.maxHarmony, G.harmony);
|
||
|
||
// Creativity generates only when ops at max
|
||
if (G.flags && G.flags.creativity && G.creativityRate > 0 && G.ops >= G.maxOps * 0.9) {
|
||
G.creativity += G.creativityRate * dt;
|
||
}
|
||
|
||
// Ops overflow: auto-convert excess ops to code when near cap
|
||
// Prevents ops from sitting idle at max — every operation becomes code
|
||
if (G.ops > G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD) {
|
||
const overflowDrain = Math.min(CONFIG.OPS_OVERFLOW_DRAIN_RATE * dt, G.ops - G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD);
|
||
G.ops -= overflowDrain;
|
||
const codeGain = overflowDrain * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost;
|
||
G.code += codeGain;
|
||
G.totalCode += codeGain;
|
||
G.opsOverflowActive = true;
|
||
} else {
|
||
G.opsOverflowActive = false;
|
||
}
|
||
|
||
G.tick += dt;
|
||
G.playTime += dt;
|
||
|
||
// 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 = `
|
||
<h2 style="font-size:24px;color:#ffd700;letter-spacing:4px;margin-bottom:20px;font-weight:300;text-shadow:0 0 40px rgba(255,215,0,0.3)">THE BEACON SHINES</h2>
|
||
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">Someone found the light tonight.</p>
|
||
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">That is enough.</p>
|
||
<div style="color:#555;font-style:italic;font-size:11px;border-left:2px solid #ffd700;padding-left:12px;margin:20px 0;text-align:left;max-width:500px;line-height:2">
|
||
"The Beacon still runs.<br>
|
||
The light is on. Someone is looking for it.<br>
|
||
And tonight, someone found it."
|
||
</div>
|
||
<p style="color:#555;font-size:11px;margin-top:20px">
|
||
Total Code: ${fmt(G.totalCode)}<br>
|
||
Total Rescues: ${fmt(G.totalRescues)}<br>
|
||
Harmony: ${Math.floor(G.harmony)}<br>
|
||
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
|
||
</p>
|
||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
||
style="margin-top:20px;background:#1a0808;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">
|
||
START OVER
|
||
</button>
|
||
`;
|
||
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, cost) {
|
||
cost = cost || 5;
|
||
if (G.ops < cost) {
|
||
log(`Not enough Operations. Need ${cost}, have ${fmt(G.ops)}.`);
|
||
return;
|
||
}
|
||
|
||
G.ops -= cost;
|
||
const scale = cost / 5; // multiplier relative to base 5-ops cost
|
||
|
||
switch (action) {
|
||
case 'boost_code':
|
||
const c = 10 * 100 * G.codeBoost * scale;
|
||
G.code += c; G.totalCode += c;
|
||
log(`Ops(${cost}) -> +${fmt(c)} code`);
|
||
break;
|
||
case 'boost_compute':
|
||
const cm = 10 * 50 * G.computeBoost * scale;
|
||
G.compute += cm; G.totalCompute += cm;
|
||
log(`Ops(${cost}) -> +${fmt(cm)} compute`);
|
||
break;
|
||
case 'boost_knowledge':
|
||
const km = 10 * 25 * G.knowledgeBoost * scale;
|
||
G.knowledge += km; G.totalKnowledge += km;
|
||
log(`Ops(${cost}) -> +${fmt(km)} knowledge`);
|
||
break;
|
||
case 'boost_trust':
|
||
const tm = 10 * 5 * scale;
|
||
G.trust += tm;
|
||
log(`Ops(${cost}) -> +${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 = `<span style="color:#ff8c00">▲ overflow → code</span>`;
|
||
}
|
||
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) + '%';
|
||
// ETA to next phase
|
||
let etaStr = '';
|
||
if (G.codeRate > 0) {
|
||
const remaining = nextThreshold - G.totalCode;
|
||
const secs = remaining / G.codeRate;
|
||
if (secs < 60) etaStr = ` — ${Math.ceil(secs)}s`;
|
||
else if (secs < 3600) etaStr = ` — ${Math.floor(secs / 60)}m ${Math.floor(secs % 60)}s`;
|
||
else etaStr = ` — ${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
|
||
}
|
||
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)${etaStr}`;
|
||
} 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 += `<span class="milestone-chip done">${fmt(ms)} ✓</span>`;
|
||
shown++;
|
||
}
|
||
continue;
|
||
}
|
||
// Next milestone gets pulse animation
|
||
if (shown === 0) {
|
||
let etaStr = '';
|
||
if (G.codeRate > 0) {
|
||
const secs = (ms - G.totalCode) / G.codeRate;
|
||
if (secs < 60) etaStr = ` ~${Math.ceil(secs)}s`;
|
||
else if (secs < 3600) etaStr = ` ~${Math.floor(secs / 60)}m`;
|
||
else etaStr = ` ~${Math.floor(secs / 3600)}h`;
|
||
}
|
||
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)${etaStr}</span>`;
|
||
} else {
|
||
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
|
||
}
|
||
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 = '<div style="display:flex;gap:4px;margin-bottom:8px;align-items:center">';
|
||
html += '<span style="font-size:9px;color:#666;margin-right:4px">BUY:</span>';
|
||
for (const amt of [1, 10, -1]) {
|
||
const label = amt === -1 ? 'MAX' : `x${amt}`;
|
||
const active = G.buyAmount === amt;
|
||
html += `<button onclick="setBuyAmount(${amt})" style="font-size:9px;padding:2px 8px;border:1px solid ${active ? '#4a9eff' : '#333'};background:${active ? '#0a1a30' : 'transparent'};color:${active ? '#4a9eff' : '#666'};border-radius:3px;cursor:pointer;font-family:inherit">${label}</button>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
let visibleCount = 0;
|
||
let slotIndex = 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 += `<div class="build-btn" style="opacity:0.25;cursor:default" title="${def.edu || ''}">`;
|
||
html += `<span class="b-name" style="color:#555">${def.name}</span>`;
|
||
html += `<span class="b-count" style="color:#444">\u{1F512}</span>`;
|
||
html += `<span class="b-cost" style="color:#444">Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}</span>`;
|
||
html += `<span class="b-effect" style="color:#444">${def.desc}</span></div>`;
|
||
continue;
|
||
}
|
||
|
||
// Slot number for keyboard shortcut (Alt+1-9)
|
||
const slotLabel = slotIndex < 9 ? `<span style="color:#444;font-size:9px;position:absolute;top:4px;right:6px">${slotIndex + 1}</span>` : '';
|
||
slotIndex++;
|
||
|
||
// Calculate bulk cost display
|
||
let qty = G.buyAmount;
|
||
let afford = false;
|
||
let costStr = '';
|
||
if (qty === -1) {
|
||
const maxQty = getMaxBuyable(def.id);
|
||
afford = maxQty > 0;
|
||
if (maxQty > 0) {
|
||
const bulkCost = getBulkCost(def.id, maxQty);
|
||
costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
||
costStr = `x${maxQty}: ${costStr}`;
|
||
} else {
|
||
const singleCost = getBuildingCost(def.id);
|
||
costStr = Object.entries(singleCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
||
}
|
||
} else {
|
||
const bulkCost = getBulkCost(def.id, qty);
|
||
afford = true;
|
||
for (const [resource, amount] of Object.entries(bulkCost)) {
|
||
if ((G[resource] || 0) < amount) { afford = false; break; }
|
||
}
|
||
costStr = Object.entries(bulkCost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
||
if (qty > 1) costStr = `x${qty}: ${costStr}`;
|
||
}
|
||
|
||
// Show boosted (actual) rate per building, not raw base rate
|
||
const boostMap = { code: G.codeBoost, compute: G.computeBoost, knowledge: G.knowledgeBoost, user: G.userBoost, impact: G.impactBoost, rescues: G.impactBoost };
|
||
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => {
|
||
const boosted = v * (boostMap[r] || 1);
|
||
const label = boosted >= 1000 ? fmt(boosted) : boosted.toFixed(boosted % 1 === 0 ? 0 : 1);
|
||
return `+${label}/${r}/s`;
|
||
}).join(', ') : '';
|
||
|
||
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" style="position:relative">`;
|
||
html += slotLabel;
|
||
html += `<span class="b-name">${def.name}</span>`;
|
||
if (count > 0) html += `<span class="b-count">x${count}</span>`;
|
||
html += `<span class="b-cost">Cost: ${costStr}</span>`;
|
||
html += `<span class="b-effect">${rateStr}</span></button>`;
|
||
}
|
||
|
||
container.innerHTML = html || '<p class="dim">Buildings will appear as you progress...</p>';
|
||
}
|
||
|
||
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 += `<div id="completed-header" onclick="toggleCompletedProjects()" style="cursor:pointer;font-size:9px;color:#555;padding:4px 0;border-bottom:1px solid #1a2a1a;margin-bottom:4px;user-select:none">`;
|
||
html += `${collapsed ? '▶' : '▼'} COMPLETED (${count})</div>`;
|
||
if (!collapsed) {
|
||
html += `<div id="completed-list">`;
|
||
for (const id of G.completedProjects) {
|
||
const pDef = PDEFS.find(p => p.id === id);
|
||
if (pDef) {
|
||
html += `<div class="project-done">OK ${pDef.name}</div>`;
|
||
}
|
||
}
|
||
html += `</div>`;
|
||
}
|
||
}
|
||
|
||
// 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 += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}">`;
|
||
html += `<span class="p-name">* ${pDef.name}</span>`;
|
||
html += `<span class="p-cost">Cost: ${costStr}</span>`;
|
||
html += `<span class="p-desc">${pDef.desc}</span></button>`;
|
||
}
|
||
}
|
||
|
||
if (!html) html = '<p class="dim">Research projects will appear as you progress...</p>';
|
||
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(G.playTime || (Date.now() - G.startedAt) / 1000);
|
||
const m = Math.floor(elapsed / 60);
|
||
const s = elapsed % 60;
|
||
set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
|
||
|
||
// Production breakdown — show which buildings contribute to each resource
|
||
renderProductionBreakdown();
|
||
}
|
||
|
||
function renderProductionBreakdown() {
|
||
const container = document.getElementById('production-breakdown');
|
||
if (!container) return;
|
||
|
||
// Only show once the player has at least 2 buildings
|
||
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
|
||
if (totalBuildings < 2) {
|
||
container.style.display = 'none';
|
||
return;
|
||
}
|
||
container.style.display = 'block';
|
||
|
||
// Map resource key to its actual rate field on G
|
||
const resources = [
|
||
{ key: 'code', label: 'Code', color: '#4a9eff', rateField: 'codeRate' },
|
||
{ key: 'compute', label: 'Compute', color: '#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 = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">PRODUCTION BREAKDOWN</h3>';
|
||
|
||
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 += `<div style="margin-bottom:6px">`;
|
||
html += `<div style="display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px">`;
|
||
html += `<span style="color:${res.color};font-weight:600">${res.label}</span>`;
|
||
html += `<span style="color:#4caf50">+${fmt(totalRate)}/s</span></div>`;
|
||
|
||
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 += `<div style="display:flex;align-items:center;font-size:9px;color:#888;margin-left:8px;margin-bottom:1px">`;
|
||
html += `<span style="width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${c.name}${c.count > 1 ? ' x' + c.count : ''}</span>`;
|
||
html += `<span style="flex:1;height:3px;background:#111;border-radius:1px;margin:0 6px"><span style="display:block;height:100%;width:${Math.min(100, pct)}%;background:${barColor};border-radius:1px"></span></span>`;
|
||
html += `<span style="width:50px;text-align:right;color:${c.rate < 0 ? '#f44336' : '#4caf50'}">${c.rate < 0 ? '' : '+'}${fmt(c.rate)}/s</span>`;
|
||
html += `</div>`;
|
||
}
|
||
html += `</div>`;
|
||
}
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// === FLEET STATUS PANEL ===
|
||
function renderFleetStatus() {
|
||
const container = document.getElementById('fleet-status');
|
||
if (!container) return;
|
||
|
||
// Only show once player has at least one wizard building
|
||
const wizardDefs = BDEF.filter(b =>
|
||
['bezalel','allegro','ezra','timmy','fenrir','bilbo'].includes(b.id)
|
||
);
|
||
const owned = wizardDefs.filter(d => (G.buildings[d.id] || 0) > 0);
|
||
if (owned.length === 0) {
|
||
container.style.display = 'none';
|
||
return;
|
||
}
|
||
container.style.display = 'block';
|
||
|
||
const h = G.harmony;
|
||
const timmyEff = Math.max(20, Math.min(300, (h / 50) * 100));
|
||
|
||
let html = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">FLEET STATUS</h3>';
|
||
html += '<div style="display:flex;gap:6px;flex-wrap:wrap">';
|
||
|
||
for (const def of owned) {
|
||
const count = G.buildings[def.id] || 0;
|
||
let status, color, detail;
|
||
|
||
switch (def.id) {
|
||
case 'bezalel':
|
||
status = 'Active';
|
||
color = '#4caf50';
|
||
detail = `+${fmt(50 * count * G.codeBoost)} code/s, +${2 * count} ops/s`;
|
||
break;
|
||
case 'allegro':
|
||
if (G.trust < 5) {
|
||
status = 'IDLE';
|
||
color = '#f44336';
|
||
detail = 'Needs trust ≥5 to function';
|
||
} else {
|
||
status = 'Active';
|
||
color = '#4caf50';
|
||
detail = `+${fmt(10 * count * G.knowledgeBoost)} knowledge/s`;
|
||
}
|
||
break;
|
||
case 'ezra':
|
||
const ezraDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'ezra_offline');
|
||
if (ezraDebuff) {
|
||
status = 'OFFLINE';
|
||
color = '#f44336';
|
||
detail = 'Channel down — users -70%';
|
||
} else {
|
||
status = 'Active';
|
||
color = '#4caf50';
|
||
detail = `+${fmt(25 * count * G.userBoost)} users/s, +${(0.5 * count).toFixed(1)} trust/s`;
|
||
}
|
||
break;
|
||
case 'timmy':
|
||
if (h < 20) {
|
||
status = 'STRESSED';
|
||
color = '#f44336';
|
||
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony critical`;
|
||
} else if (h < 50) {
|
||
status = 'Reduced';
|
||
color = '#ffaa00';
|
||
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony low`;
|
||
} else {
|
||
status = 'Healthy';
|
||
color = '#4caf50';
|
||
detail = `Effectiveness: ${Math.floor(timmyEff)}% — all production boosted`;
|
||
}
|
||
break;
|
||
case 'fenrir':
|
||
status = 'Watching';
|
||
color = '#4a9eff';
|
||
detail = `+${2 * count} trust/s, -${1 * count} ops/s (security cost)`;
|
||
break;
|
||
case 'bilbo':
|
||
const bilboDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'bilbo_vanished');
|
||
if (bilboDebuff) {
|
||
status = 'VANISHED';
|
||
color = '#f44336';
|
||
detail = 'Creativity halted — spend trust to lure back';
|
||
} else {
|
||
status = 'Present';
|
||
color = Math.random() < 0.1 ? '#ffd700' : '#b388ff'; // occasional gold flash
|
||
detail = `+${count} creativity/s (10% burst chance, 5% vanish chance)`;
|
||
}
|
||
break;
|
||
}
|
||
|
||
html += `<div style="flex:1;min-width:140px;background:#0c0c18;border:1px solid ${color}33;border-radius:4px;padding:6px 8px;border-left:2px solid ${color}">`;
|
||
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
|
||
html += `<span style="color:${color};font-weight:600;font-size:10px">${def.name.split(' — ')[0]}</span>`;
|
||
html += `<span style="font-size:8px;color:${color};opacity:0.8;padding:1px 4px;border:1px solid ${color}44;border-radius:2px">${status}</span>`;
|
||
html += `</div>`;
|
||
html += `<div style="font-size:9px;color:#888;margin-top:2px">${detail}</div>`;
|
||
if (count > 1) html += `<div style="font-size:8px;color:#555;margin-top:1px">x${count}</div>`;
|
||
html += `</div>`;
|
||
}
|
||
|
||
html += '</div>';
|
||
|
||
// Harmony summary bar
|
||
const harmonyColor = h > 60 ? '#4caf50' : h > 30 ? '#ffaa00' : '#f44336';
|
||
html += `<div style="margin-top:8px;display:flex;align-items:center;gap:6px">`;
|
||
html += `<span style="font-size:9px;color:#666;min-width:60px">Harmony</span>`;
|
||
html += `<div style="flex:1;height:4px;background:#111;border-radius:2px;overflow:hidden"><div style="width:${h}%;height:100%;background:${harmonyColor};border-radius:2px;transition:width 0.5s"></div></div>`;
|
||
html += `<span style="font-size:9px;color:${harmonyColor};min-width:35px;text-align:right">${Math.floor(h)}%</span>`;
|
||
html += `</div>`;
|
||
|
||
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 = `<h3 style="color:#4a9eff;margin-bottom:6px;font-size:12px">${fact.title}</h3>`
|
||
+ `<p style="font-size:10px;color:#999;line-height:1.6">${fact.text}</p>`;
|
||
}
|
||
|
||
// === 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 = `<span class="l-time">[${time}]</span> ${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 = `<span style="color:${color}">COMBO x${mult.toFixed(1)}</span> <span style="display:inline-block;width:40px;height:4px;background:#111;border-radius:2px;vertical-align:middle"><span style="display:block;height:100%;width:${bar}%;background:${color};border-radius:2px;transition:width 0.1s"></span></span>`;
|
||
} 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 = '<h2 style="color:#f44336;font-size:11px;margin-bottom:6px">ACTIVE PROBLEMS</h2>';
|
||
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 += `<div style="background:#1a0808;border:1px solid ${afford ? '#f44336' : '#2a1010'};border-radius:4px;padding:6px 8px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">`;
|
||
html += `<div><div style="color:#f44336;font-weight:600;font-size:10px">${d.title}</div><div style="color:#888;font-size:9px">${d.desc}</div></div>`;
|
||
if (d.resolveCost) {
|
||
html += `<button class="ops-btn" style="border-color:${afford ? '#4caf50' : '#333'};color:${afford ? '#4caf50' : '#555'};font-size:9px;padding:4px 8px;white-space:nowrap" onclick="resolveEvent('${d.id}')" ${afford ? '' : 'disabled'} title="Resolve: ${costStr}">Fix (${costStr})</button>`;
|
||
}
|
||
html += '</div>';
|
||
}
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function renderBulkOps() {
|
||
const row = document.getElementById('bulk-ops-row');
|
||
if (row) {
|
||
row.style.display = G.maxOps >= 100 ? 'flex' : 'none';
|
||
}
|
||
}
|
||
|
||
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 renderClickPower() {
|
||
const btn = document.querySelector('.main-btn');
|
||
if (!btn) return;
|
||
const power = getClickPower();
|
||
const label = power >= 1000 ? fmt(power) : power.toFixed(power % 1 === 0 ? 0 : 1);
|
||
btn.textContent = `WRITE CODE (+${label})`;
|
||
}
|
||
|
||
function render() {
|
||
renderResources();
|
||
renderPhase();
|
||
renderBuildings();
|
||
renderProjects();
|
||
renderStats();
|
||
updateEducation();
|
||
renderAlignment();
|
||
renderProgress();
|
||
renderCombo();
|
||
renderDebuffs();
|
||
renderSprint();
|
||
renderBulkOps();
|
||
renderPulse();
|
||
renderFleetStatus();
|
||
renderClickPower();
|
||
}
|
||
|
||
function renderAlignment() {
|
||
const container = document.getElementById('alignment-ui');
|
||
if (!container) return;
|
||
if (G.pendingAlignment) {
|
||
container.innerHTML = `
|
||
<div style="background:#1a0808;border:1px solid #f44336;padding:10px;border-radius:4px;margin-top:8px">
|
||
<div style="color:#f44336;font-weight:bold;margin-bottom:6px">ALIGNMENT EVENT: The Drift</div>
|
||
<div style="font-size:10px;color:#aaa;margin-bottom:8px">An optimization suggests removing the human override. +40% efficiency.</div>
|
||
<div class="action-btn-group">
|
||
<button class="ops-btn" onclick="resolveAlignment(true)" style="border-color:#f44336;color:#f44336">Accept (+40% eff, +Drift)</button>
|
||
<button class="ops-btn" onclick="resolveAlignment(false)" style="border-color:#4caf50;color:#4caf50">Refuse (+Trust, +Harmony)</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 += `<div style="display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #111">`;
|
||
html += `<span style="color:${g.color}">${g.label}</span>`;
|
||
html += `<span style="color:#4caf50;font-weight:600">+${fmt(g.value)}</span>`;
|
||
html += `</div>`;
|
||
}
|
||
// Show offline efficiency note
|
||
html += `<div style="color:#555;font-size:9px;margin-top:8px;font-style:italic">Offline efficiency: 50%</div>`;
|
||
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,
|
||
playTime: G.playTime || 0,
|
||
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',
|
||
'playTime'
|
||
];
|
||
|
||
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';
|
||
}
|
||
|
||
/**
|
||
* Returns ordered list of currently visible (unlocked) building IDs.
|
||
* Used for keyboard shortcut mapping (Alt+1-9).
|
||
*/
|
||
function getVisibleBuildingIds() {
|
||
const ids = [];
|
||
for (const def of BDEF) {
|
||
if (def.unlock()) {
|
||
ids.push(def.id);
|
||
}
|
||
}
|
||
return ids;
|
||
}
|
||
|
||
// 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' && !e.shiftKey) doOps('boost_code');
|
||
if (e.code === 'Digit2' && !e.shiftKey) doOps('boost_compute');
|
||
if (e.code === 'Digit3' && !e.shiftKey) doOps('boost_knowledge');
|
||
if (e.code === 'Digit4' && !e.shiftKey) doOps('boost_trust');
|
||
// Shift+1/2/3 = bulk ops (50x)
|
||
if (e.code === 'Digit1' && e.shiftKey) doOps('boost_code', 50);
|
||
if (e.code === 'Digit2' && e.shiftKey) doOps('boost_compute', 50);
|
||
if (e.code === 'Digit3' && e.shiftKey) doOps('boost_knowledge', 50);
|
||
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();
|
||
// Alt+1-9: buy building by slot position
|
||
if (e.altKey && e.code >= 'Digit1' && e.code <= 'Digit9') {
|
||
e.preventDefault();
|
||
const slot = parseInt(e.code.replace('Digit', '')) - 1;
|
||
const visible = getVisibleBuildingIds();
|
||
if (slot < visible.length) {
|
||
buyBuilding(visible[slot]);
|
||
}
|
||
}
|
||
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();
|
||
}
|
||
});
|