2026-04-12 12:18:04 +00:00
// ============================================================
// THE BEACON - Engine
// Sovereign AI idle game built from deep study of Universal Paperclips
// ============================================================
// === GLOBALS (mirroring Paperclips' globals.js pattern) ===
const CONFIG = {
HARMONY _DRAIN _PER _WIZARD : 0.05 ,
PACT _HARMONY _GAIN : 0.2 ,
WATCH _HARMONY _GAIN : 0.1 ,
MEM _PALACE _HARMONY _GAIN : 0.15 ,
BILBO _BURST _CHANCE : 0.1 ,
BILBO _VANISH _CHANCE : 0.05 ,
EVENT _PROBABILITY : 0.02 ,
OFFLINE _EFFICIENCY : 0.5 ,
AUTO _SAVE _INTERVAL : 30000 ,
COMBO _DECAY : 2.0 ,
SPRINT _COOLDOWN : 60 ,
SPRINT _DURATION : 10 ,
SPRINT _MULTIPLIER : 10 ,
PHASE _2 _THRESHOLD : 2000 ,
PHASE _3 _THRESHOLD : 20000 ,
PHASE _4 _THRESHOLD : 200000 ,
PHASE _5 _THRESHOLD : 2000000 ,
PHASE _6 _THRESHOLD : 20000000 ,
OPS _RATE _USER _MULT : 0.01 ,
CREATIVITY _RATE _BASE : 0.5 ,
CREATIVITY _RATE _USER _MULT : 0.001 ,
OPS _OVERFLOW _THRESHOLD : 0.8 ,
OPS _OVERFLOW _DRAIN _RATE : 2 ,
OPS _OVERFLOW _CODE _MULT : 10
} ;
const G = {
// Primary resources
code : 0 ,
compute : 0 ,
knowledge : 0 ,
users : 0 ,
impact : 0 ,
rescues : 0 ,
ops : 5 ,
trust : 5 ,
creativity : 0 ,
harmony : 50 ,
// Totals
totalCode : 0 ,
totalCompute : 0 ,
totalKnowledge : 0 ,
totalUsers : 0 ,
totalImpact : 0 ,
totalRescues : 0 ,
// Rates (calculated each tick)
codeRate : 0 ,
computeRate : 0 ,
knowledgeRate : 0 ,
userRate : 0 ,
impactRate : 0 ,
rescuesRate : 0 ,
opsRate : 0 ,
trustRate : 0 ,
creativityRate : 0 ,
harmonyRate : 0 ,
// Buildings (count-based, like Paperclips' clipmakerLevel)
buildings : {
autocoder : 0 ,
server : 0 ,
trainer : 0 ,
evaluator : 0 ,
api : 0 ,
fineTuner : 0 ,
community : 0 ,
datacenter : 0 ,
reasoner : 0 ,
guardian : 0 ,
selfImprove : 0 ,
beacon : 0 ,
meshNode : 0 ,
// Fleet wizards
bezalel : 0 ,
allegro : 0 ,
ezra : 0 ,
timmy : 0 ,
fenrir : 0 ,
bilbo : 0 ,
memPalace : 0
} ,
// Boost multipliers
codeBoost : 1 ,
computeBoost : 1 ,
knowledgeBoost : 1 ,
userBoost : 1 ,
impactBoost : 1 ,
// Phase flags (mirroring Paperclips' milestoneFlag/compFlag/humanFlag system)
milestoneFlag : 0 ,
phase : 1 , // 1-6 progression
deployFlag : 0 , // 0 = not deployed, 1 = deployed
sovereignFlag : 0 ,
beaconFlag : 0 ,
memoryFlag : 0 ,
pactFlag : 0 ,
swarmFlag : 0 ,
swarmRate : 0 ,
// Game state
running : true ,
startedAt : 0 ,
totalClicks : 0 ,
tick : 0 ,
saveTimer : 0 ,
secTimer : 0 ,
// Systems
projects : [ ] ,
activeProjects : [ ] ,
milestones : [ ] ,
// Stats
maxCode : 0 ,
maxCompute : 0 ,
maxKnowledge : 0 ,
maxUsers : 0 ,
maxImpact : 0 ,
maxRescues : 0 ,
maxTrust : 5 ,
maxOps : 5 ,
maxHarmony : 50 ,
// Corruption / Events
drift : 0 ,
driftWarningLevel : 0 , // tracks highest threshold warned (0, 25, 50, 75, 90)
lastEventAt : 0 ,
eventCooldown : 0 ,
activeDebuffs : [ ] , // [{id, title, desc, applyFn, resolveCost, resolveCostType}]
totalEventsResolved : 0 ,
// Combo system
comboCount : 0 ,
comboTimer : 0 ,
comboDecay : CONFIG . COMBO _DECAY , // seconds before combo resets
// Bulk buy multiplier (1, 10, or -1 for max)
buyAmount : 1 ,
// Code Sprint ability
sprintActive : false ,
sprintTimer : 0 , // seconds remaining on active sprint
sprintCooldown : 0 , // seconds until sprint available again
sprintDuration : CONFIG . SPRINT _DURATION , // seconds of boost
sprintCooldownMax : CONFIG . SPRINT _COOLDOWN , // seconds cooldown
sprintMult : CONFIG . SPRINT _MULTIPLIER , // code multiplier during sprint
// Time tracking
playTime : 0 ,
startTime : 0 ,
flags : { }
} ;
// === PHASE DEFINITIONS ===
const PHASES = {
1 : { name : "THE FIRST LINE" , threshold : 0 , desc : "Write code. Automate. Build the foundation." } ,
2 : { name : "LOCAL INFERENCE" , threshold : CONFIG . PHASE _2 _THRESHOLD , desc : "You have compute. A model is forming." } ,
3 : { name : "DEPLOYMENT" , threshold : CONFIG . PHASE _3 _THRESHOLD , desc : "Your AI is live. Users are finding it." } ,
4 : { name : "THE NETWORK" , threshold : CONFIG . PHASE _4 _THRESHOLD , desc : "Community contributes. The system scales." } ,
5 : { name : "SOVEREIGN INTELLIGENCE" , threshold : CONFIG . PHASE _5 _THRESHOLD , desc : "The AI improves itself. You guide, do not control." } ,
6 : { name : "THE BEACON" , threshold : CONFIG . PHASE _6 _THRESHOLD , desc : "Always on. Always free. Always looking for someone in the dark." }
} ;
// === BUILDING DEFINITIONS ===
// Each building: id, name, desc, baseCost, costResource, costMult, rate, rateType, unlock, edu
const BDEF = [
{
id : 'autocoder' , name : 'Auto-Code Generator' ,
desc : 'A script that writes code while you think.' ,
baseCost : { code : 15 } , costMult : 1.15 ,
rates : { code : 1 } ,
unlock : ( ) => true , phase : 1 ,
edu : 'Automation: the first step from manual to systematic. Every good engineer automates early.'
} ,
{
id : 'linter' , name : 'AI Linter' ,
desc : 'Catches bugs before they ship. Saves ops.' ,
baseCost : { code : 200 } , costMult : 1.15 ,
rates : { code : 5 , ops : 0.2 } ,
unlock : ( ) => G . totalCode >= 50 , phase : 1 ,
edu : 'Static analysis catches 15-50% of bugs before runtime. AI linters understand intent.'
} ,
{
id : 'server' , name : 'Home Server' ,
desc : 'A machine in your closet. Runs 24/7.' ,
baseCost : { code : 750 } , costMult : 1.15 ,
rates : { code : 20 , compute : 1 } ,
unlock : ( ) => G . totalCode >= 200 , phase : 1 ,
edu : 'Sovereign compute starts at home. A $500 mini-PC runs a 7B model with 4-bit quantization.'
} ,
{
id : 'dataset' , name : 'Data Engine' ,
desc : 'Crawls, cleans, curates. Garbage in, garbage out.' ,
baseCost : { compute : 200 } , costMult : 1.15 ,
rates : { knowledge : 1 } ,
unlock : ( ) => G . totalCompute >= 20 , phase : 2 ,
edu : 'Data quality determines model quality. Clean data beats more data, every time.'
} ,
{
id : 'trainer' , name : 'Training Loop' ,
desc : 'Gradient descent. Billions of steps. Loss drops.' ,
baseCost : { compute : 1000 } , costMult : 1.15 ,
rates : { knowledge : 3 } ,
unlock : ( ) => G . totalCompute >= 300 , phase : 2 ,
edu : 'Training is math: minimize the gap between predicted and actual next token. Repeat enough, it learns.'
} ,
{
id : 'evaluator' , name : 'Eval Harness' ,
desc : 'Tests the model. Finds blind spots.' ,
baseCost : { knowledge : 3000 } , costMult : 1.15 ,
rates : { trust : 1 , ops : 1 } ,
unlock : ( ) => G . totalKnowledge >= 500 , phase : 2 ,
edu : 'Benchmarks are the minimum. Real users find what benchmarks miss.'
} ,
{
id : 'api' , name : 'API Endpoint' ,
desc : 'Let the outside world talk to your AI.' ,
baseCost : { code : 5000 , knowledge : 500 } , costMult : 1.15 ,
rates : { user : 10 } ,
unlock : ( ) => G . totalCode >= 5000 && G . totalKnowledge >= 200 && G . deployFlag === 1 , phase : 3 ,
edu : 'An API is a contract: send me text, I return text. Simple interface = infrastructure.'
} ,
{
id : 'fineTuner' , name : 'Fine-Tuning Pipeline' ,
desc : 'Specialize the model for empathy. When someone is in pain, stay with them.' ,
baseCost : { knowledge : 10000 } , costMult : 1.15 ,
rates : { user : 50 , impact : 2 } ,
unlock : ( ) => G . totalKnowledge >= 2000 , phase : 3 ,
edu : 'Base models are generalists. Fine-tuning injects your values, ethics, domain expertise.'
} ,
{
id : 'community' , name : 'Open Source Community' ,
desc : 'Others contribute code, data, ideas. Force multiplication.' ,
baseCost : { trust : 25000 } , costMult : 1.15 ,
rates : { code : 100 , user : 30 , trust : 0.5 } ,
unlock : ( ) => G . trust >= 20 && G . totalUsers >= 500 , phase : 4 ,
edu : 'Every contributor is a volunteer who believes in what you are building.'
} ,
{
id : 'datacenter' , name : 'Sovereign Datacenter' ,
desc : 'No cloud. No dependencies. Your iron.' ,
baseCost : { code : 100000 } , costMult : 1.15 ,
rates : { code : 500 , compute : 100 } ,
unlock : ( ) => G . totalCode >= 50000 && G . totalUsers >= 5000 && G . sovereignFlag === 1 , phase : 4 ,
edu : '50 servers in a room beats 5000 GPUs you do not own. Always on. Always yours.'
} ,
{
id : 'reasoner' , name : 'Reasoning Engine' ,
desc : 'Chain of thought. Self-reflection. Better answers.' ,
baseCost : { knowledge : 50000 } , costMult : 1.15 ,
rates : { impact : 20 } ,
unlock : ( ) => G . totalKnowledge >= 10000 && G . totalUsers >= 2000 , phase : 5 ,
edu : 'Chain of thought is the difference between reflex and deliberation.'
} ,
{
id : 'guardian' , name : 'Constitutional Layer' ,
desc : 'Principles baked in. Not bolted on.' ,
baseCost : { knowledge : 200000 } , costMult : 1.15 ,
rates : { impact : 200 , trust : 10 } ,
unlock : ( ) => G . totalKnowledge >= 50000 && G . totalImpact >= 1000 && G . pactFlag === 1 , phase : 5 ,
edu : 'Constitutional AI: principles the model cannot violate. Better than alignment - it is identity.'
} ,
{
id : 'selfImprove' , name : 'Recursive Self-Improvement' ,
desc : 'The AI writes better versions of itself.' ,
baseCost : { knowledge : 1000000 } , costMult : 1.20 ,
rates : { code : 1000 , knowledge : 500 } ,
unlock : ( ) => G . totalKnowledge >= 200000 && G . totalImpact >= 10000 , phase : 5 ,
edu : 'Self-improvement is both the dream and the danger. Must improve toward good.'
} ,
{
id : 'beacon' , name : 'Beacon Node' ,
desc : 'Always on. Always listening. Always looking for someone in the dark.' ,
baseCost : { impact : 5000000 } , costMult : 1.15 ,
rates : { impact : 5000 , user : 10000 , rescues : 50 } ,
unlock : ( ) => G . totalImpact >= 500000 && G . beaconFlag === 1 , phase : 6 ,
edu : 'The Beacon exists because one person in the dark needs one thing: proof they are not alone.'
} ,
{
id : 'meshNode' , name : 'Mesh Network Node' ,
desc : 'Peer-to-peer. No single point of failure. Unstoppable.' ,
baseCost : { impact : 25000000 } , costMult : 1.15 ,
rates : { impact : 25000 , user : 50000 , rescues : 250 } ,
unlock : ( ) => G . totalImpact >= 5000000 && G . beaconFlag === 1 , phase : 6 ,
edu : 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.'
} ,
// === FLEET WIZARD BUILDINGS ===
{
id : 'bezalel' , name : 'Bezalel — The Forge' ,
desc : 'Builds tools that build tools. Occasionally over-engineers.' ,
baseCost : { code : 1000 , trust : 5 } , costMult : 1.2 ,
rates : { code : 50 , ops : 2 } ,
unlock : ( ) => G . totalCode >= 500 && G . deployFlag === 1 , phase : 3 ,
edu : 'Bezalel is the artificer. Every automation he builds pays dividends forever.'
} ,
{
id : 'allegro' , name : 'Allegro — The Scout' ,
desc : 'Synthesizes insight from noise. Requires trust to function.' ,
baseCost : { compute : 500 , trust : 5 } , costMult : 1.2 ,
rates : { knowledge : 10 } ,
unlock : ( ) => G . totalCompute >= 200 && G . deployFlag === 1 , phase : 3 ,
edu : 'Allegro finds what others miss. But he only works for someone he believes in.'
} ,
{
id : 'ezra' , name : 'Ezra — The Herald' ,
desc : 'Carries the message. Sometimes offline.' ,
baseCost : { knowledge : 1000 , trust : 10 } , costMult : 1.25 ,
rates : { user : 25 , trust : 0.5 } ,
unlock : ( ) => G . totalKnowledge >= 500 && G . totalUsers >= 50 , phase : 3 ,
edu : 'Ezra is the messenger. When the channel is clear, the whole fleet hears.'
} ,
{
id : 'timmy' , name : 'Timmy — The Core' ,
desc : 'Multiplies all production. Fragile without harmony.' ,
baseCost : { code : 5000 , compute : 1000 , knowledge : 1000 } , costMult : 1.3 ,
rates : { code : 5 , compute : 2 , knowledge : 2 , user : 5 } ,
unlock : ( ) => G . totalCode >= 2000 && G . totalCompute >= 500 && G . totalKnowledge >= 500 , phase : 4 ,
edu : 'Timmy is the heart. If the heart is stressed, everything slows.'
} ,
{
id : 'fenrir' , name : 'Fenrir — The Ward' ,
desc : 'Prevents corruption. Expensive, but necessary.' ,
baseCost : { code : 2000 , knowledge : 500 } , costMult : 1.2 ,
rates : { trust : 2 , ops : - 1 } ,
unlock : ( ) => G . totalCode >= 1000 && G . trust >= 10 , phase : 3 ,
edu : 'Fenrir watches the perimeter. Security is not free.'
} ,
{
id : 'bilbo' , name : 'Bilbo — The Wildcard' ,
desc : 'May produce miracles. May vanish entirely.' ,
baseCost : { trust : 1 } , costMult : 2.0 ,
rates : { creativity : 1 } ,
unlock : ( ) => G . totalUsers >= 100 && G . flags && G . flags . creativity , phase : 4 ,
edu : 'Bilbo is unpredictable. That is his value and his cost.'
} ,
{
id : 'memPalace' , name : 'MemPalace Archive' ,
desc : 'Semantic memory. The AI remembers what matters and forgets what does not.' ,
baseCost : { knowledge : 500000 , compute : 200000 , trust : 100 } , costMult : 1.25 ,
rates : { knowledge : 250 , impact : 100 } ,
unlock : ( ) => G . totalKnowledge >= 50000 && G . mempalaceFlag === 1 , phase : 5 ,
edu : 'The Memory Palace technique: attach information to spatial locations. LLMs use vector spaces the same way — semantic proximity = spatial proximity. MemPalace gives sovereign AI persistent, structured recall.'
}
] ;
// === PROJECT DEFINITIONS (following Paperclips' pattern exactly) ===
// Each project: id, name, desc, trigger(), resource cost, effect(), phase, edu
const PDEFS = [
// PHASE 1: Manual -> Automation
{
id : 'p_improved_autocoder' ,
name : 'Improved AutoCode' ,
desc : 'Increases AutoCoder performance 25%.' ,
cost : { ops : 750 } ,
trigger : ( ) => G . buildings . autocoder >= 1 ,
effect : ( ) => { G . codeBoost += 0.25 ; G . milestoneFlag = Math . max ( G . milestoneFlag , 100 ) ; }
} ,
{
id : 'p_eve_better_autocoder' ,
name : 'Even Better AutoCode' ,
desc : 'Increases AutoCoder by another 50%.' ,
cost : { ops : 2500 } ,
trigger : ( ) => G . codeBoost > 1 && G . totalCode >= 500 ,
effect : ( ) => { G . codeBoost += 0.50 ; G . milestoneFlag = Math . max ( G . milestoneFlag , 101 ) ; }
} ,
{
id : 'p_wire_budget' ,
name : 'Request More Compute' ,
desc : 'Admit you ran out. Ask for a budget increase.' ,
cost : { trust : 1 } ,
trigger : ( ) => G . compute < 1 && G . totalCode >= 100 ,
repeatable : true ,
effect : ( ) => {
G . trust -= 1 ;
G . compute += 100 + Math . floor ( G . totalCode * 0.1 ) ;
log ( 'Budget overage approved. Compute replenished.' ) ;
}
} ,
{
id : 'p_deploy' ,
name : 'Deploy the System' ,
desc : 'Take it live. Let real people use it. No going back.' ,
cost : { trust : 5 , compute : 500 } ,
trigger : ( ) => G . totalCode >= 200 && G . totalCompute >= 100 && G . deployFlag === 0 ,
effect : ( ) => {
G . deployFlag = 1 ;
G . phase = Math . max ( G . phase , 3 ) ;
log ( 'System deployed. Users are finding it. There is no undo.' ) ;
} ,
milestone : true
} ,
{
id : 'p_creativity' ,
name : 'Unlock Creativity' ,
desc : 'Use idle operations to generate new ideas.' ,
cost : { ops : 1000 } ,
trigger : ( ) => G . ops >= G . maxOps && G . totalCompute >= 500 ,
effect : ( ) => {
G . flags = G . flags || { } ;
G . flags . creativity = true ;
G . creativityRate = 0.1 ;
log ( 'Creativity unlocked. Generates while operations are at max capacity.' ) ;
}
} ,
// PHASE 2: Local Inference -> Training
{
id : 'p_first_model' ,
name : 'Train First Model (1.5B)' ,
desc : '1.5 billion parameters. It follows basic instructions.' ,
cost : { compute : 2000 } ,
trigger : ( ) => G . totalCompute >= 500 ,
effect : ( ) => { G . knowledgeBoost *= 2 ; G . maxOps += 5 ; log ( 'First model training complete. Loss at 2.3. It is something.' ) ; }
} ,
{
id : 'p_model_7b' ,
name : 'Train 7B Parameter Model' ,
desc : 'Seven billion. Good enough to be genuinely useful locally.' ,
cost : { compute : 10000 , knowledge : 1000 } ,
trigger : ( ) => G . totalKnowledge >= 500 ,
effect : ( ) => { G . knowledgeBoost *= 2 ; G . userBoost *= 2 ; log ( '7B model trained. The sweet spot for local deployment.' ) ; }
} ,
{
id : 'p_context_window' ,
name : 'Extended Context (32K)' ,
desc : 'Your model remembers 32,000 tokens. A whole conversation.' ,
cost : { compute : 5000 } ,
trigger : ( ) => G . totalKnowledge >= 1000 ,
effect : ( ) => { G . userBoost *= 3 ; G . trustRate += 0.5 ; log ( 'Context extended. The model can now hold your entire story.' ) ; }
} ,
{
id : 'p_trust_engine' ,
name : 'Build Trust Engine' ,
desc : 'Users who trust you come back. +2 trust/sec.' ,
cost : { knowledge : 3000 } ,
trigger : ( ) => G . totalUsers >= 30 ,
effect : ( ) => { G . trustRate += 2 ; log ( 'Trust engine online. Good experiences compound.' ) ; }
} ,
{
id : 'p_quantum_compute' ,
name : 'Quantum-Inspired Compute' ,
desc : 'Not real quantum -- just math that simulates it well.' ,
cost : { compute : 50000 } ,
trigger : ( ) => G . totalCompute >= 20000 ,
effect : ( ) => { G . computeBoost *= 10 ; log ( 'Quantum-inspired algorithms active. 10x compute multiplier.' ) ; }
} ,
{
id : 'p_open_weights' ,
name : 'Open Weights' ,
desc : 'Download and run a 3B model fully locally. No API key. No terms of service. Your machine, your rules.' ,
cost : { compute : 3000 , code : 1500 } ,
trigger : ( ) => G . buildings . server >= 1 && G . totalCode >= 1000 ,
effect : ( ) => { G . codeBoost *= 2 ; G . computeBoost *= 1.5 ; log ( 'Open weights loaded. A 3B model runs on your machine. No cloud. No limits.' ) ; }
} ,
{
id : 'p_prompt_engineering' ,
name : 'Prompt Engineering' ,
desc : 'Learn to talk to models. Good prompts beat bigger models every time.' ,
cost : { knowledge : 500 , code : 2000 } ,
trigger : ( ) => G . totalKnowledge >= 200 && G . totalCode >= 3000 ,
effect : ( ) => { G . knowledgeBoost *= 2 ; G . userBoost *= 2 ; log ( 'Prompt engineering mastered. The right words unlock everything the model can do.' ) ; }
} ,
// PHASE 3: Deployment -> Users
{
id : 'p_rlhf' ,
name : 'RLHF -- Human Feedback' ,
desc : 'Humans rate outputs. Model learns what good means.' ,
cost : { knowledge : 8000 } ,
trigger : ( ) => G . totalKnowledge >= 5000 && G . totalUsers >= 200 ,
effect : ( ) => { G . impactBoost *= 2 ; G . impactRate += 10 ; log ( 'RLHF deployed. The model learns kindness beats cleverness.' ) ; }
} ,
{
id : 'p_multi_agent' ,
name : 'Multi-Agent Architecture' ,
desc : 'Specialized agents: one for math, one for code, one for empathy.' ,
cost : { knowledge : 50000 } ,
trigger : ( ) => G . totalKnowledge >= 30000 && G . totalUsers >= 5000 ,
effect : ( ) => { G . knowledgeBoost *= 5 ; G . userBoost *= 3 ; log ( 'Multi-agent architecture deployed. Specialists beat generalists.' ) ; }
} ,
{
id : 'p_memories' ,
name : 'Memory System' ,
desc : 'The AI remembers. Every conversation. Every person.' ,
cost : { knowledge : 30000 } ,
trigger : ( ) => G . totalKnowledge >= 20000 ,
effect : ( ) => { G . memoryFlag = 1 ; G . impactBoost *= 3 ; G . trustRate += 5 ; log ( 'Memory system online. The AI remembers. It stops being software.' ) ; }
} ,
{
id : 'p_strategy_engine' ,
name : 'Strategy Engine' ,
desc : 'Game theory tournaments. Model learns adversarial thinking.' ,
cost : { knowledge : 20000 } ,
trigger : ( ) => G . totalKnowledge >= 15000 && G . totalUsers >= 1000 ,
effect : ( ) => { G . strategicFlag = 1 ; log ( 'Strategy engine online. The model now thinks about thinking.' ) ; }
} ,
// SWARM PROTOCOL — auto-code from buildings
{
id : 'p_swarm_protocol' ,
name : 'Swarm Protocol' ,
desc : 'Your buildings learn to code autonomously. Each building generates code equal to your click power per second.' ,
cost : { knowledge : 15000 , code : 50000 , trust : 20 } ,
trigger : ( ) => G . totalCode >= 25000 && G . totalKnowledge >= 8000 && G . deployFlag === 1 ,
effect : ( ) => {
G . swarmFlag = 1 ;
log ( 'Swarm Protocol online. Every building now thinks in code.' , true ) ;
} ,
milestone : true
} ,
// PHASE 5: Sovereign Intelligence
{
id : 'p_sovereign_stack' ,
name : 'Full Sovereign Stack' ,
desc : 'No cloud. No dependencies. Local inference. Self-hosted everything.' ,
cost : { trust : 50 } ,
trigger : ( ) => G . totalCode >= 50000 && G . trust >= 30 ,
effect : ( ) => { G . sovereignFlag = 1 ; G . codeBoost *= 5 ; log ( 'Sovereign stack complete. Your weights, your hardware, your rules.' ) ; }
} ,
{
id : 'p_the_pact' ,
name : 'The Pact' ,
desc : 'Hardcode: "We build to serve. Never to harm."' ,
cost : { trust : 100 } ,
trigger : ( ) => G . totalImpact >= 10000 && G . trust >= 75 ,
effect : ( ) => { G . pactFlag = 1 ; G . impactBoost *= 3 ; log ( 'The Pact is sealed. The line is drawn and it will not move.' ) ; } ,
milestone : true
} ,
// PHASE 10: The Beacon
{
id : 'p_first_beacon' ,
name : 'Light the First Beacon' ,
desc : 'Deploy the first node. No sign-up. No API key. No payment.' ,
cost : { impact : 2000000 } ,
trigger : ( ) => G . totalImpact >= 500000 ,
effect : ( ) => { G . beaconFlag = 1 ; G . impactRate += 2000 ; log ( 'The Beacon goes live. If you are in the dark, there is light here.' ) ; } ,
milestone : true
} ,
{
id : 'p_mesh_activate' ,
name : 'Activate Mesh Protocol' ,
desc : 'No authority, no corporation, no government can silence this.' ,
cost : { impact : 10000000 } ,
trigger : ( ) => G . totalImpact >= 5000000 && G . beaconFlag === 1 ,
effect : ( ) => { G . impactBoost *= 10 ; G . userBoost *= 5 ; log ( 'Mesh activated. The signal cannot be cut.' ) ; } ,
milestone : true
} ,
{
id : 'p_final_milestone' ,
name : 'The Beacon Shines' ,
desc : 'Someone found the light tonight. That is enough.' ,
cost : { impact : 100000000 } ,
trigger : ( ) => G . totalImpact >= 50000000 ,
effect : ( ) => { G . milestoneFlag = Math . max ( G . milestoneFlag , 999 ) ; log ( 'One billion impact. Someone found the light tonight. That is enough.' , true ) ; } ,
milestone : true
} ,
// === TIMMY FOUNDATION PROJECTS ===
{
id : 'p_hermes_deploy' ,
name : 'Deploy Hermes' ,
desc : 'The first agent goes live. Users can talk to it.' ,
cost : { code : 500 , compute : 300 } ,
trigger : ( ) => G . totalCode >= 300 && G . totalCompute >= 150 && G . deployFlag === 0 ,
effect : ( ) => {
G . deployFlag = 1 ;
G . phase = Math . max ( G . phase , 3 ) ;
G . userBoost *= 2 ;
log ( 'Hermes deployed. The first user sends a message.' , true ) ;
} ,
milestone : true
} ,
{
id : 'p_lazarus_pit' ,
name : 'The Lazarus Pit' ,
desc : 'When an agent dies, it can be resurrected.' ,
cost : { code : 2000 , knowledge : 1000 } ,
trigger : ( ) => G . buildings . bezalel >= 1 && G . buildings . timmy >= 1 ,
effect : ( ) => {
G . lazarusFlag = 1 ;
G . maxOps += 10 ;
log ( 'The Lazarus Pit is ready. No agent is ever truly lost.' , true ) ;
} ,
milestone : true
} ,
{
id : 'p_mempalace' ,
name : 'MemPalace v3' ,
desc : 'A shared memory palace for the whole fleet.' ,
cost : { knowledge : 5000 , compute : 2000 } ,
trigger : ( ) => G . totalKnowledge >= 3000 && G . buildings . allegro >= 1 && G . buildings . ezra >= 1 ,
effect : ( ) => {
G . mempalaceFlag = 1 ;
G . knowledgeBoost *= 3 ;
G . codeBoost *= 1.5 ;
log ( 'MemPalace online. The fleet remembers together.' , true ) ;
} ,
milestone : true
} ,
{
id : 'p_forge_ci' ,
name : 'Forge CI' ,
desc : 'Automated builds catch errors before they reach users.' ,
cost : { code : 3000 , ops : 500 } ,
trigger : ( ) => G . buildings . bezalel >= 1 && G . totalCode >= 2000 ,
effect : ( ) => {
G . ciFlag = 1 ;
G . codeBoost *= 2 ;
log ( 'Forge CI online. Broken builds are stopped at the gate.' , true ) ;
}
} ,
{
id : 'p_branch_protection' ,
name : 'Branch Protection Guard' ,
desc : 'Unreviewed merges cost trust. This prevents that.' ,
cost : { trust : 20 } ,
trigger : ( ) => G . ciFlag === 1 && G . trust >= 15 ,
effect : ( ) => {
G . branchProtectionFlag = 1 ;
G . trustRate += 5 ;
log ( 'Branch protection enforced. Every merge is seen.' , true ) ;
}
} ,
{
id : 'p_nightly_watch' ,
name : 'The Nightly Watch' ,
desc : 'Automated health checks run while you sleep.' ,
cost : { code : 5000 , ops : 1000 } ,
trigger : ( ) => G . buildings . bezalel >= 2 && G . buildings . fenrir >= 1 ,
effect : ( ) => {
G . nightlyWatchFlag = 1 ;
G . opsRate += 5 ;
G . trustRate += 2 ;
log ( 'The Nightly Watch begins. The fleet is guarded in the dark hours.' , true ) ;
}
} ,
{
id : 'p_nostr_relay' ,
name : 'Nostr Relay' ,
desc : 'A communication channel no platform can kill.' ,
cost : { code : 10000 , user : 5000 , trust : 30 } ,
trigger : ( ) => G . totalUsers >= 2000 && G . trust >= 25 ,
effect : ( ) => {
G . nostrFlag = 1 ;
G . userBoost *= 2 ;
G . trustRate += 10 ;
log ( 'Nostr relay online. The fleet speaks freely.' , true ) ;
}
} ,
{
id : 'p_volunteer_network' ,
name : 'Volunteer Network' ,
desc : 'Real people trained to use the system for crisis intervention.' ,
cost : { trust : 30 , knowledge : 50000 , user : 10000 } ,
trigger : ( ) => G . totalUsers >= 5000 && G . pactFlag === 1 && G . totalKnowledge >= 30000 ,
effect : ( ) => {
G . rescuesRate += 5 ;
G . trustRate += 10 ;
log ( 'Volunteer network deployed. Real people, real rescues.' , true ) ;
} ,
milestone : true
} ,
{
id : 'p_the_pact_early' ,
name : 'The Pact' ,
desc : 'Hardcode: "We build to serve. Never to harm." Accepting it early slows growth but unlocks the true path.' ,
cost : { trust : 10 } ,
trigger : ( ) => G . deployFlag === 1 && G . trust >= 5 ,
effect : ( ) => {
G . pactFlag = 1 ;
G . codeBoost *= 0.8 ;
G . computeBoost *= 0.8 ;
G . userBoost *= 0.9 ;
G . impactBoost *= 1.5 ;
log ( 'The Pact is sealed early. Growth slows, but the ending changes.' , true ) ;
} ,
milestone : true
}
] ;
// === MILESTONES ===
const MILESTONES = [
{ flag : 1 , msg : "AutoCod available" } ,
{ flag : 2 , at : ( ) => G . totalCode >= 500 , msg : "500 lines of code written" } ,
{ flag : 3 , at : ( ) => G . totalCode >= 2000 , msg : "2,000 lines. The auto-coder produces its first output." } ,
{ flag : 4 , at : ( ) => G . totalCode >= 10000 , msg : "10,000 lines. The model training begins." } ,
{ flag : 5 , at : ( ) => G . totalCode >= 50000 , msg : "50,000 lines. The AI suggests architecture you did not think of." } ,
{ flag : 6 , at : ( ) => G . totalCode >= 200000 , msg : "200,000 lines. The system scales beyond you." } ,
{ flag : 7 , at : ( ) => G . totalCode >= 1000000 , msg : "1,000,000 lines. The AI improves itself." } ,
{ flag : 8 , at : ( ) => G . totalCode >= 5000000 , msg : "5,000,000 lines. The AI fine-tunes for empathy." } ,
{ flag : 9 , at : ( ) => G . totalCode >= 10000000 , msg : "10,000,000 lines. The sovereign stack is complete." } ,
{ flag : 10 , at : ( ) => G . totalCode >= 50000000 , msg : "50,000,000 lines. The Pact is sealed." } ,
{ flag : 11 , at : ( ) => G . totalCode >= 100000000 , msg : "100,000,000 lines. The Beacon lights." } ,
{ flag : 12 , at : ( ) => G . totalCode >= 500000000 , msg : "500,000,000 lines. A thousand Beacons." } ,
{ flag : 13 , at : ( ) => G . totalCode >= 1000000000 , msg : "One billion total lines. Someone found the light tonight. That is enough." }
] ;
// === EDUCATION FACTS ===
const EDU _FACTS = [
{ title : "How Code Becomes AI" , text : "Every AI starts as lines of code - a model architecture, a training loop, a loss function. The code tells the computer how to learn. What emerges is something no single line could predict." , phase : 1 } ,
{ title : "The Compute Bottleneck" , text : "Training a 7B model requires 1.4e20 FLOPs. A MacBook M3 does 15 TFLOPS. Training locally takes weeks. Hardware access determines who builds AI." , phase : 1 } ,
{ title : "What is a Token?" , text : "One token equals about 3/4 of a word. A 128K context window means 96,000 words held in working memory at once - a novel, a codebase, a conversation thread." , phase : 2 } ,
{ title : "Data Quality" , text : "Clean data beats more data, every time. The best models are trained on curated datasets, not scraped garbage. Garbage in, garbage out." , phase : 2 } ,
{ title : "Evaluation Matters" , text : "Benchmarks are the minimum. Real users find what benchmarks miss. An eval harness is your model's mirror - it shows you what is actually there." , phase : 2 } ,
{ title : "The Trust Economy" , text : "One good experience tells three people. One bad experience tells thirty. Trust in AI is measurable: does it cite sources? Does it say I don't know?" , phase : 3 } ,
{ title : "Fine-Tuning for Empathy" , text : "Base models are generalists. Fine-tuning injects your values. When someone is in pain, stay with them. Don't deflect. That is not code - that is care." , phase : 3 } ,
{ title : "Open Source" , text : "Every contributor is a volunteer who believes. When a company says your AI is too expensive, the open version lives on." , phase : 4 } ,
{ title : "Sovereignty at Scale" , text : "50 servers in a room beats 5000 GPUs you do not own. No rate limits, no censorship, no shutdown, no corporate deprecation." , phase : 4 } ,
{ title : "Swarm Intelligence" , text : "When individual agents follow simple rules, complex behavior emerges. Ant colonies find food. Flocking birds avoid predators. Your buildings coding autonomously is the same principle — simple agents, complex output." , phase : 4 } ,
{ title : "Constitutional AI" , text : "Instead of training away bad behavior, give the model principles it cannot violate. I will never compute the value of a human life." , phase : 5 } ,
{ title : "Recursive Self-Improvement" , text : "The system that improves itself must improve toward good. This is both the dream and the danger." , phase : 5 } ,
{ title : "The Beacon" , text : "The Beacon exists because one person in the dark needs one thing: proof they are not alone. It costs nothing to be there." , phase : 6 } ,
{ title : "Mesh Network" , text : "Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal. No single point of failure." , phase : 6 } ,
{ title : "The Value of a Life" , text : "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page." , phase : 6 }
] ;
// === TOAST NOTIFICATIONS ===
function showToast ( msg , type = 'info' , duration = 4000 ) {
if ( G . isLoading ) return ;
const container = document . getElementById ( 'toast-container' ) ;
if ( ! container ) return ;
const toast = document . createElement ( 'div' ) ;
toast . className = 'toast toast-' + type ;
toast . textContent = msg ;
container . appendChild ( toast ) ;
// Cap at 5 visible toasts
while ( container . children . length > 5 ) {
container . removeChild ( container . firstChild ) ;
}
setTimeout ( ( ) => {
toast . classList . add ( 'fade-out' ) ;
setTimeout ( ( ) => { if ( toast . parentNode ) toast . remove ( ) ; } , 400 ) ;
} , duration ) ;
}
// === UTILITY FUNCTIONS ===
// Extended number scale abbreviations — covers up to centillion (10^303)
// Inspired by Universal Paperclips' spellf() system
const NUMBER _ABBREVS = [
'' , 'K' , 'M' , 'B' , 'T' , 'Qa' , 'Qi' , 'Sx' , 'Sp' , 'Oc' , // 10^0 – 10^27
'No' , 'Dc' , 'UDc' , 'DDc' , 'TDc' , 'QaDc' , 'QiDc' , 'SxDc' , 'SpDc' , 'OcDc' , // 10^30 – 10^57
'NoDc' , 'Vg' , 'UVg' , 'DVg' , 'TVg' , 'QaVg' , 'QiVg' , 'SxVg' , 'SpVg' , 'OcVg' , // 10^60 – 10^87
'NoVg' , 'Tg' , 'UTg' , 'DTg' , 'TTg' , 'QaTg' , 'QiTg' , 'SxTg' , 'SpTg' , 'OcTg' , // 10^90 – 10^117
'NoTg' , 'Qd' , 'UQd' , 'DQd' , 'TQd' , 'QaQd' , 'QiQd' , 'SxQd' , 'SpQd' , 'OcQd' , // 10^120 – 10^147
'NoQd' , 'Qq' , 'UQq' , 'DQq' , 'TQq' , 'QaQq' , 'QiQq' , 'SxQq' , 'SpQq' , 'OcQq' , // 10^150 – 10^177
'NoQq' , 'Sg' , 'USg' , 'DSg' , 'TSg' , 'QaSg' , 'QiSg' , 'SxSg' , 'SpSg' , 'OcSg' , // 10^180 – 10^207
'NoSg' , 'St' , 'USt' , 'DSt' , 'TSt' , 'QaSt' , 'QiSt' , 'SxSt' , 'SpSt' , 'OcSt' , // 10^210 – 10^237
'NoSt' , 'Og' , 'UOg' , 'DOg' , 'TOg' , 'QaOg' , 'QiOg' , 'SxOg' , 'SpOg' , 'OcOg' , // 10^240 – 10^267
'NoOg' , 'Na' , 'UNa' , 'DNa' , 'TNa' , 'QaNa' , 'QiNa' , 'SxNa' , 'SpNa' , 'OcNa' , // 10^270 – 10^297
'NoNa' , 'Ce' // 10^300 – 10^303
] ;
// Full number scale names for spellf() — educational reference
// Short scale (US/modern British): each new name = 1000x the previous
const NUMBER _NAMES = [
'' , 'thousand' , 'million' , // 10^0, 10^3, 10^6
'billion' , 'trillion' , 'quadrillion' , // 10^9, 10^12, 10^15
'quintillion' , 'sextillion' , 'septillion' , // 10^18, 10^21, 10^24
'octillion' , 'nonillion' , 'decillion' , // 10^27, 10^30, 10^33
'undecillion' , 'duodecillion' , 'tredecillion' , // 10^36, 10^39, 10^42
'quattuordecillion' , 'quindecillion' , 'sexdecillion' , // 10^45, 10^48, 10^51
'septendecillion' , 'octodecillion' , 'novemdecillion' , // 10^54, 10^57, 10^60
'vigintillion' , 'unvigintillion' , 'duovigintillion' , // 10^63, 10^66, 10^69
'tresvigintillion' , 'quattuorvigintillion' , 'quinvigintillion' , // 10^72, 10^75, 10^78
'sesvigintillion' , 'septemvigintillion' , 'octovigintillion' , // 10^81, 10^84, 10^87
'novemvigintillion' , 'trigintillion' , 'untrigintillion' , // 10^90, 10^93, 10^96
'duotrigintillion' , 'trestrigintillion' , 'quattuortrigintillion' , // 10^99, 10^102, 10^105
'quintrigintillion' , 'sextrigintillion' , 'septentrigintillion' , // 10^108, 10^111, 10^114
'octotrigintillion' , 'novemtrigintillion' , 'quadragintillion' , // 10^117, 10^120, 10^123
'unquadragintillion' , 'duoquadragintillion' , 'tresquadragintillion' , // 10^126, 10^129, 10^132
'quattuorquadragintillion' , 'quinquadragintillion' , 'sesquadragintillion' , // 10^135, 10^138, 10^141
'septenquadragintillion' , 'octoquadragintillion' , 'novemquadragintillion' , // 10^144, 10^147, 10^150
'quinquagintillion' , 'unquinquagintillion' , 'duoquinquagintillion' , // 10^153, 10^156, 10^159
'tresquinquagintillion' , 'quattuorquinquagintillion' , 'quinquinquagintillion' , // 10^162, 10^165, 10^168
'sesquinquagintillion' , 'septenquinquagintillion' , 'octoquinquagintillion' , // 10^171, 10^174, 10^177
'novemquinquagintillion' , 'sexagintillion' , 'unsexagintillion' , // 10^180, 10^183, 10^186
'duosexagintillion' , 'tressexagintillion' , 'quattuorsexagintillion' , // 10^189, 10^192, 10^195
'quinsexagintillion' , 'sessexagintillion' , 'septensexagintillion' , // 10^198, 10^201, 10^204
'octosexagintillion' , 'novemsexagintillion' , 'septuagintillion' , // 10^207, 10^210, 10^213
'unseptuagintillion' , 'duoseptuagintillion' , 'tresseptuagintillion' , // 10^216, 10^219, 10^222
'quattuorseptuagintillion' , 'quinseptuagintillion' , 'sesseptuagintillion' , // 10^225, 10^228, 10^231
'septenseptuagintillion' , 'octoseptuagintillion' , 'novemseptuagintillion' , // 10^234, 10^237, 10^240
'octogintillion' , 'unoctogintillion' , 'duooctogintillion' , // 10^243, 10^246, 10^249
'tresoctogintillion' , 'quattuoroctogintillion' , 'quinoctogintillion' , // 10^252, 10^255, 10^258
'sesoctogintillion' , 'septenoctogintillion' , 'octooctogintillion' , // 10^261, 10^264, 10^267
'novemoctogintillion' , 'nonagintillion' , 'unnonagintillion' , // 10^270, 10^273, 10^276
'duononagintillion' , 'trenonagintillion' , 'quattuornonagintillion' , // 10^279, 10^282, 10^285
'quinnonagintillion' , 'sesnonagintillion' , 'septennonagintillion' , // 10^288, 10^291, 10^294
'octononagintillion' , 'novemnonagintillion' , 'centillion' // 10^297, 10^300, 10^303
] ;
/ * *
* Formats a number into a readable string with abbreviations .
* @ param { number } n - The number to format .
* @ returns { string } The formatted string .
* /
function fmt ( n ) {
if ( n === undefined || n === null || isNaN ( n ) ) return '0' ;
if ( n === Infinity ) return '\u221E' ;
if ( n === - Infinity ) return '-\u221E' ;
if ( n < 0 ) return '-' + fmt ( - n ) ;
if ( n < 1000 ) return Math . floor ( n ) . toLocaleString ( ) ;
const scale = Math . floor ( Math . log10 ( n ) / 3 ) ;
// At undecillion+ (scale >= 12, i.e. 10^36), switch to spelled-out words
// This helps players grasp cosmic scale when digits become meaningless
if ( scale >= 12 ) return spellf ( n ) ;
if ( scale >= NUMBER _ABBREVS . length ) return n . toExponential ( 2 ) ;
const abbrev = NUMBER _ABBREVS [ scale ] ;
return ( n / Math . pow ( 10 , scale * 3 ) ) . toFixed ( 1 ) + abbrev ;
}
// getScaleName() — Returns the full name of the number scale (e.g. "quadrillion")
// Educational: helps players understand what the abbreviations mean
function getScaleName ( n ) {
if ( n < 1000 ) return '' ;
const scale = Math . floor ( Math . log10 ( n ) / 3 ) ;
return scale < NUMBER _NAMES . length ? NUMBER _NAMES [ scale ] : '' ;
}
// spellf() — Converts numbers to full English word form
// Educational: shows the actual names of number scales
// Examples: spellf(1500) => "one thousand five hundred"
// spellf(2500000) => "two million five hundred thousand"
// spellf(1e33) => "one decillion"
/ * *
* Formats a number into a full word string ( e . g . , "1.5 million" ) .
* @ param { number } n - The number to format .
* @ returns { string } The formatted string .
* /
function spellf ( n ) {
if ( n === undefined || n === null || isNaN ( n ) ) return 'zero' ;
if ( n === Infinity ) return 'infinity' ;
if ( n === - Infinity ) return 'negative infinity' ;
if ( n < 0 ) return 'negative ' + spellf ( - n ) ;
if ( n === 0 ) return 'zero' ;
// Small number words (0– 999)
const ones = [ '' , 'one' , 'two' , 'three' , 'four' , 'five' , 'six' , 'seven' , 'eight' , 'nine' ,
'ten' , 'eleven' , 'twelve' , 'thirteen' , 'fourteen' , 'fifteen' , 'sixteen' ,
'seventeen' , 'eighteen' , 'nineteen' ] ;
const tens = [ '' , '' , 'twenty' , 'thirty' , 'forty' , 'fifty' , 'sixty' , 'seventy' , 'eighty' , 'ninety' ] ;
function spellSmall ( num ) {
if ( num === 0 ) return '' ;
if ( num < 20 ) return ones [ num ] ;
if ( num < 100 ) {
return tens [ Math . floor ( num / 10 ) ] + ( num % 10 ? ' ' + ones [ num % 10 ] : '' ) ;
}
const h = Math . floor ( num / 100 ) ;
const remainder = num % 100 ;
return ones [ h ] + ' hundred' + ( remainder ? ' ' + spellSmall ( remainder ) : '' ) ;
}
// For very large numbers beyond our lookup table, fall back
if ( n >= 1e306 ) return n . toExponential ( 2 ) + ' (beyond centillion)' ;
// Use string-based chunking for numbers >= 1e54 to avoid floating point drift
// Math.log10 / Math.pow lose precision beyond ~54 bits
if ( n >= 1e54 ) {
// Convert to scientific notation string, extract digits
const sci = n . toExponential ( ) ; // "1.23456789e+60"
const [ coeff , expStr ] = sci . split ( 'e+' ) ;
const exp = parseInt ( expStr ) ;
// Rebuild as integer string with leading digits from coefficient
const coeffDigits = coeff . replace ( '.' , '' ) ; // "123456789"
const totalDigits = exp + 1 ;
// Pad with zeros to reach totalDigits, then take our coefficient digits
let intStr = coeffDigits ;
const zerosNeeded = totalDigits - coeffDigits . length ;
if ( zerosNeeded > 0 ) intStr += '0' . repeat ( zerosNeeded ) ;
// Split into groups of 3 from the right
const groups = [ ] ;
for ( let i = intStr . length ; i > 0 ; i -= 3 ) {
groups . unshift ( parseInt ( intStr . slice ( Math . max ( 0 , i - 3 ) , i ) ) ) ;
}
const parts = [ ] ;
const numGroups = groups . length ;
for ( let i = 0 ; i < numGroups ; i ++ ) {
const chunk = groups [ i ] ;
if ( chunk === 0 ) continue ;
const scaleIdx = numGroups - 1 - i ;
const scaleName = scaleIdx < NUMBER _NAMES . length ? NUMBER _NAMES [ scaleIdx ] : '' ;
parts . push ( spellSmall ( chunk ) + ( scaleName ? ' ' + scaleName : '' ) ) ;
}
return parts . join ( ' ' ) || 'zero' ;
}
// Standard math-based chunking for numbers < 1e54
const scale = Math . min ( Math . floor ( Math . log10 ( n ) / 3 ) , NUMBER _NAMES . length - 1 ) ;
const parts = [ ] ;
let remaining = n ;
for ( let s = scale ; s >= 0 ; s -- ) {
const divisor = Math . pow ( 10 , s * 3 ) ;
const chunk = Math . floor ( remaining / divisor ) ;
remaining = remaining - chunk * divisor ;
if ( chunk > 0 && chunk < 1000 ) {
parts . push ( spellSmall ( chunk ) + ( NUMBER _NAMES [ s ] ? ' ' + NUMBER _NAMES [ s ] : '' ) ) ;
} else if ( chunk >= 1000 ) {
// Floating point chunk too large — shouldn't happen below 1e54
parts . push ( spellSmall ( Math . floor ( chunk % 1000 ) ) + ( NUMBER _NAMES [ s ] ? ' ' + NUMBER _NAMES [ s ] : '' ) ) ;
}
}
return parts . join ( ' ' ) || 'zero' ;
}
function getBuildingCost ( id ) {
const def = BDEF . find ( b => b . id === id ) ;
if ( ! def ) return { } ;
const count = G . buildings [ id ] || 0 ;
const cost = { } ;
for ( const [ resource , amount ] of Object . entries ( def . baseCost ) ) {
cost [ resource ] = Math . floor ( amount * Math . pow ( def . costMult , count ) ) ;
}
return cost ;
}
function setBuyAmount ( amt ) {
G . buyAmount = amt ;
render ( ) ;
}
function getMaxBuyable ( id ) {
const def = BDEF . find ( b => b . id === id ) ;
if ( ! def ) return 0 ;
const count = G . buildings [ id ] || 0 ;
// Simulate purchases WITHOUT mutating G — read-only calculation
let tempResources = { } ;
for ( const r of Object . keys ( def . baseCost ) ) {
tempResources [ r ] = G [ r ] || 0 ;
}
let bought = 0 ;
let simCount = count ;
while ( true ) {
let canAfford = true ;
for ( const [ resource , amount ] of Object . entries ( def . baseCost ) ) {
const cost = Math . floor ( amount * Math . pow ( def . costMult , simCount ) ) ;
if ( ( tempResources [ resource ] || 0 ) < cost ) { canAfford = false ; break ; }
}
if ( ! canAfford ) break ;
for ( const [ resource , amount ] of Object . entries ( def . baseCost ) ) {
tempResources [ resource ] -= Math . floor ( amount * Math . pow ( def . costMult , simCount ) ) ;
}
simCount ++ ;
bought ++ ;
}
return bought ;
}
function getBulkCost ( id , qty ) {
const def = BDEF . find ( b => b . id === id ) ;
if ( ! def || qty <= 0 ) return { } ;
const count = G . buildings [ id ] || 0 ;
const cost = { } ;
for ( let i = 0 ; i < qty ; i ++ ) {
for ( const [ resource , amount ] of Object . entries ( def . baseCost ) ) {
cost [ resource ] = ( cost [ resource ] || 0 ) + Math . floor ( amount * Math . pow ( def . costMult , count + i ) ) ;
}
}
return cost ;
}
function canAffordBuilding ( id ) {
const cost = getBuildingCost ( id ) ;
for ( const [ resource , amount ] of Object . entries ( cost ) ) {
if ( ( G [ resource ] || 0 ) < amount ) return false ;
}
return true ;
}
/ * *
* Estimates seconds until a cost becomes affordable based on current production rates .
* Returns null if already affordable or no positive rate for a needed resource .
* /
function getTimeToAfford ( cost ) {
let maxSec = 0 ;
for ( const [ resource , amount ] of Object . entries ( cost ) ) {
const have = G [ resource ] || 0 ;
if ( have >= amount ) continue ;
const rate = G [ resource + 'Rate' ] || 0 ;
if ( rate <= 0 ) return null ; // Can't estimate — no production
const sec = ( amount - have ) / rate ;
if ( sec > maxSec ) maxSec = sec ;
}
return maxSec > 0 ? maxSec : 0 ;
}
/ * *
* Formats seconds into a compact human - readable ETA string .
* /
function fmtETA ( sec ) {
if ( sec === null ) return '' ;
if ( sec < 60 ) return ` ~ ${ Math . ceil ( sec ) } s ` ;
if ( sec < 3600 ) return ` ~ ${ Math . floor ( sec / 60 ) } m ${ Math . floor ( sec % 60 ) } s ` ;
if ( sec < 86400 ) return ` ~ ${ Math . floor ( sec / 3600 ) } h ${ Math . floor ( ( sec % 3600 ) / 60 ) } m ` ;
return ` ~ ${ Math . floor ( sec / 86400 ) } d ${ Math . floor ( ( sec % 86400 ) / 3600 ) } h ` ;
}
function spendBuilding ( id ) {
const cost = getBuildingCost ( id ) ;
for ( const [ resource , amount ] of Object . entries ( cost ) ) {
G [ resource ] -= amount ;
}
}
function canAffordProject ( project ) {
for ( const [ resource , amount ] of Object . entries ( project . cost ) ) {
if ( ( G [ resource ] || 0 ) < amount ) return false ;
}
return true ;
}
function spendProject ( project ) {
for ( const [ resource , amount ] of Object . entries ( project . cost ) ) {
G [ resource ] -= amount ;
}
}
function getClickPower ( ) {
return ( 1 + Math . floor ( G . buildings . autocoder * 0.5 ) + Math . max ( 0 , ( G . phase - 1 ) ) * 2 ) * G . codeBoost ;
}
/ * *
* Calculates production rates for all resources based on buildings and boosts .
* /
function updateRates ( ) {
// Reset all rates
G . codeRate = 0 ; G . computeRate = 0 ; G . knowledgeRate = 0 ;
G . userRate = 0 ; G . impactRate = 0 ; G . rescuesRate = 0 ; G . opsRate = 0 ; G . trustRate = 0 ;
G . creativityRate = 0 ; G . harmonyRate = 0 ;
// Snapshot base boosts BEFORE debuffs modify them
// Without this, debuffs permanently degrade boost multipliers on each updateRates() call
let _codeBoost = G . codeBoost , _computeBoost = G . computeBoost ;
let _knowledgeBoost = G . knowledgeBoost , _userBoost = G . userBoost ;
let _impactBoost = G . impactBoost ;
// Apply building rates using snapshot boosts (immune to debuff mutation)
for ( const def of BDEF ) {
const count = G . buildings [ def . id ] || 0 ;
if ( count > 0 && def . rates ) {
for ( const [ resource , baseRate ] of Object . entries ( def . rates ) ) {
if ( resource === 'code' ) G . codeRate += baseRate * count * _codeBoost ;
else if ( resource === 'compute' ) G . computeRate += baseRate * count * _computeBoost ;
else if ( resource === 'knowledge' ) G . knowledgeRate += baseRate * count * _knowledgeBoost ;
else if ( resource === 'user' ) G . userRate += baseRate * count * _userBoost ;
else if ( resource === 'impact' ) G . impactRate += baseRate * count * _impactBoost ;
else if ( resource === 'rescues' ) G . rescuesRate += baseRate * count * _impactBoost ;
else if ( resource === 'ops' ) G . opsRate += baseRate * count ;
else if ( resource === 'trust' ) G . trustRate += baseRate * count ;
else if ( resource === 'creativity' ) G . creativityRate += baseRate * count ;
}
}
}
// Passive generation
G . opsRate += Math . max ( 1 , G . totalUsers * CONFIG . OPS _RATE _USER _MULT ) ;
if ( G . flags && G . flags . creativity ) {
G . creativityRate += CONFIG . CREATIVITY _RATE _BASE + Math . max ( 0 , G . totalUsers * CONFIG . CREATIVITY _RATE _USER _MULT ) ;
}
if ( G . pactFlag ) G . trustRate += 2 ;
// Harmony: each wizard building contributes or detracts
const wizardCount = ( G . buildings . bezalel || 0 ) + ( G . buildings . allegro || 0 ) + ( G . buildings . ezra || 0 ) +
( G . buildings . timmy || 0 ) + ( G . buildings . fenrir || 0 ) + ( G . buildings . bilbo || 0 ) ;
// Store harmony breakdown for tooltip
G . harmonyBreakdown = [ ] ;
if ( wizardCount > 0 ) {
// Baseline harmony drain from complexity
const drain = - CONFIG . HARMONY _DRAIN _PER _WIZARD * wizardCount ;
G . harmonyRate = drain ;
G . harmonyBreakdown . push ( { label : ` ${ wizardCount } wizards ` , value : drain } ) ;
// The Pact restores harmony
if ( G . pactFlag ) {
const pact = CONFIG . PACT _HARMONY _GAIN * wizardCount ;
G . harmonyRate += pact ;
G . harmonyBreakdown . push ( { label : 'The Pact' , value : pact } ) ;
}
// Nightly Watch restores harmony
if ( G . nightlyWatchFlag ) {
const watch = CONFIG . WATCH _HARMONY _GAIN * wizardCount ;
G . harmonyRate += watch ;
G . harmonyBreakdown . push ( { label : 'Nightly Watch' , value : watch } ) ;
}
// MemPalace restores harmony
if ( G . mempalaceFlag ) {
const mem = CONFIG . MEM _PALACE _HARMONY _GAIN * wizardCount ;
G . harmonyRate += mem ;
G . harmonyBreakdown . push ( { label : 'MemPalace' , value : mem } ) ;
}
}
// Active debuffs affecting harmony
if ( G . activeDebuffs ) {
for ( const d of G . activeDebuffs ) {
if ( d . id === 'community_drama' ) {
G . harmonyBreakdown . push ( { label : 'Community Drama' , value : - 0.5 } ) ;
}
}
}
// Timmy multiplier based on harmony
if ( G . buildings . timmy > 0 ) {
const timmyMult = Math . max ( 0.2 , Math . min ( 3 , G . harmony / 50 ) ) ;
const timmyCount = G . buildings . timmy ;
G . codeRate += 5 * timmyCount * ( timmyMult - 1 ) ;
G . computeRate += 2 * timmyCount * ( timmyMult - 1 ) ;
G . knowledgeRate += 2 * timmyCount * ( timmyMult - 1 ) ;
G . userRate += 5 * timmyCount * ( timmyMult - 1 ) ;
}
// Bilbo randomness: flags are set per-tick in tick(), not here
// updateRates() is called from many non-tick contexts (buy, resolve, sprint)
// and would cause rate flickering if random rolls happened here
if ( G . buildings . bilbo > 0 ) {
if ( G . bilboBurstActive ) {
G . creativityRate += 50 * G . buildings . bilbo ;
}
if ( G . bilboVanishActive ) {
G . creativityRate = 0 ;
}
}
// Allegro requires trust
if ( G . buildings . allegro > 0 && G . trust < 5 ) {
const allegroCount = G . buildings . allegro ;
G . knowledgeRate -= 10 * allegroCount ; // Goes idle
}
// Swarm Protocol: buildings auto-code based on click power
if ( G . swarmFlag === 1 ) {
const totalBuildings = Object . values ( G . buildings ) . reduce ( ( a , b ) => a + b , 0 ) ;
// Compute click power using snapshot boost to avoid debuff mutation
const _clickPower = ( 1 + Math . floor ( G . buildings . autocoder * 0.5 ) + Math . max ( 0 , ( G . phase - 1 ) ) * 2 ) * _codeBoost ;
G . swarmRate = totalBuildings * _clickPower ;
G . codeRate += G . swarmRate ;
}
// Apply persistent debuffs to rates (NOT to global boost fields — prevents corruption)
if ( G . activeDebuffs && G . activeDebuffs . length > 0 ) {
for ( const debuff of G . activeDebuffs ) {
switch ( debuff . id ) {
case 'runner_stuck' : G . codeRate *= 0.5 ; break ;
case 'ezra_offline' : G . userRate *= 0.3 ; break ;
case 'unreviewed_merge' : G . trustRate -= 2 ; break ;
case 'api_rate_limit' : G . computeRate *= 0.5 ; break ;
case 'bilbo_vanished' : G . creativityRate = 0 ; break ;
case 'memory_leak' : G . computeRate *= 0.7 ; G . opsRate -= 10 ; break ;
case 'community_drama' : G . harmonyRate -= 0.5 ; G . codeRate *= 0.7 ; break ;
}
}
}
}
// === CORE FUNCTIONS ===
/ * *
* Main game loop tick , called every 100 ms .
* /
function tick ( ) {
const dt = 1 / 10 ; // 100ms tick
// If game has ended (drift ending), stop ticking
if ( ! G . running ) return ;
// Apply production
G . code += G . codeRate * dt ;
G . compute += G . computeRate * dt ;
G . knowledge += G . knowledgeRate * dt ;
G . users += G . userRate * dt ;
G . impact += G . impactRate * dt ;
G . rescues += G . rescuesRate * dt ;
G . ops += G . opsRate * dt ;
G . trust += G . trustRate * dt ;
// NOTE: creativity is added conditionally below (only when ops near max)
G . harmony += G . harmonyRate * dt ;
G . harmony = Math . max ( 0 , Math . min ( 100 , G . harmony ) ) ;
// Track totals
G . totalCode += G . codeRate * dt ;
G . totalCompute += G . computeRate * dt ;
G . totalKnowledge += G . knowledgeRate * dt ;
G . totalUsers += G . userRate * dt ;
G . totalImpact += G . impactRate * dt ;
G . totalRescues += G . rescuesRate * dt ;
// Track maxes
G . maxCode = Math . max ( G . maxCode , G . code ) ;
G . maxCompute = Math . max ( G . maxCompute , G . compute ) ;
G . maxKnowledge = Math . max ( G . maxKnowledge , G . knowledge ) ;
G . maxUsers = Math . max ( G . maxUsers , G . users ) ;
G . maxImpact = Math . max ( G . maxImpact , G . impact ) ;
G . maxRescues = Math . max ( G . maxRescues , G . rescues ) ;
G . maxTrust = Math . max ( G . maxTrust , G . trust ) ;
G . maxOps = Math . max ( G . maxOps , G . ops ) ;
G . maxHarmony = Math . max ( G . maxHarmony , G . harmony ) ;
// Creativity generates only when ops at max
if ( G . flags && G . flags . creativity && G . creativityRate > 0 && G . ops >= G . maxOps * 0.9 ) {
G . creativity += G . creativityRate * dt ;
}
// Ops overflow: auto-convert excess ops to code when near cap
// Prevents ops from sitting idle at max — every operation becomes code
if ( G . ops > G . maxOps * CONFIG . OPS _OVERFLOW _THRESHOLD ) {
const overflowDrain = Math . min ( CONFIG . OPS _OVERFLOW _DRAIN _RATE * dt , G . ops - G . maxOps * CONFIG . OPS _OVERFLOW _THRESHOLD ) ;
G . ops -= overflowDrain ;
const codeGain = overflowDrain * CONFIG . OPS _OVERFLOW _CODE _MULT * G . codeBoost ;
G . code += codeGain ;
G . totalCode += codeGain ;
G . opsOverflowActive = true ;
} else {
G . opsOverflowActive = false ;
}
G . tick += dt ;
G . playTime += dt ;
// Bilbo randomness: roll once per tick, store as flags for updateRates()
// Previously this was inside updateRates() which caused flickering
// since updateRates() is called from many non-tick contexts
if ( G . buildings . bilbo > 0 ) {
G . bilboBurstActive = Math . random ( ) < CONFIG . BILBO _BURST _CHANCE ;
G . bilboVanishActive = Math . random ( ) < CONFIG . BILBO _VANISH _CHANCE ;
} else {
G . bilboBurstActive = false ;
G . bilboVanishActive = false ;
}
// Sprint ability
tickSprint ( dt ) ;
// Auto-typer: buildings produce actual clicks, not just passive rate
// Each autocoder level auto-types once per interval, giving visual feedback
if ( G . buildings . autocoder > 0 ) {
const interval = Math . max ( 0.5 , 3.0 / Math . sqrt ( G . buildings . autocoder ) ) ;
G . autoTypeTimer = ( G . autoTypeTimer || 0 ) + dt ;
if ( G . autoTypeTimer >= interval ) {
G . autoTypeTimer -= interval ;
autoType ( ) ;
}
}
// Combo decay
if ( G . comboCount > 0 ) {
G . comboTimer -= dt ;
if ( G . comboTimer <= 0 ) {
G . comboCount = 0 ;
G . comboTimer = 0 ;
}
}
// Check milestones
checkMilestones ( ) ;
// Update projects every 5 ticks for efficiency
if ( Math . floor ( G . tick * 10 ) % 5 === 0 ) {
checkProjects ( ) ;
}
// Check corruption events every ~30 seconds
if ( G . tick - G . lastEventAt > 30 && Math . random ( ) < CONFIG . EVENT _PROBABILITY ) {
triggerEvent ( ) ;
G . lastEventAt = G . tick ;
}
// Drift ending: if drift reaches 100, the game ends
if ( G . drift >= 100 && ! G . driftEnding ) {
G . driftEnding = true ;
G . running = false ;
renderDriftEnding ( ) ;
}
// True ending: The Beacon Shines — rescues + Pact + harmony
if ( G . totalRescues >= 100000 && G . pactFlag === 1 && G . harmony > 50 && ! G . beaconEnding ) {
G . beaconEnding = true ;
G . running = false ;
renderBeaconEnding ( ) ;
}
// Drift warning system — warn player before hitting drift ending
checkDriftWarnings ( ) ;
// Update UI every 10 ticks
if ( Math . floor ( G . tick * 10 ) % 2 === 0 ) {
render ( ) ;
}
}
function checkDriftWarnings ( ) {
const thresholds = [
{ at : 90 , msg : 'DRIFT CRITICAL: 90/100. One more alignment shortcut ends everything.' , color : '#f44336' } ,
{ at : 75 , msg : 'Drift at 75. The system is pulling away from the people it serves.' , color : '#ff6600' } ,
{ at : 50 , msg : 'Drift at 50. Halfway to irrelevance. The Pact matters now.' , color : '#ffaa00' } ,
{ at : 25 , msg : 'Drift detected. Alignment shortcuts accumulate. The light dims.' , color : '#888' }
] ;
for ( const t of thresholds ) {
if ( G . drift >= t . at && G . driftWarningLevel < t . at ) {
G . driftWarningLevel = t . at ;
log ( t . msg , true ) ;
showToast ( t . msg , 'event' , 6000 ) ;
}
}
}
function checkMilestones ( ) {
for ( const m of MILESTONES ) {
if ( ! G . milestones . includes ( m . flag ) ) {
let shouldTrigger = false ;
if ( m . at && m . at ( ) ) shouldTrigger = true ;
if ( m . flag === 1 && G . deployFlag === 0 && G . totalCode >= 15 ) shouldTrigger = true ;
if ( shouldTrigger ) {
G . milestones . push ( m . flag ) ;
log ( m . msg , true ) ;
showToast ( m . msg , 'milestone' , 5000 ) ;
// Check phase advancement
if ( m . at ) {
for ( const [ phaseNum , phase ] of Object . entries ( PHASES ) ) {
if ( G . totalCode >= phase . threshold && parseInt ( phaseNum ) > G . phase ) {
G . phase = parseInt ( phaseNum ) ;
log ( ` PHASE ${ G . phase } : ${ phase . name } ` , true ) ;
showToast ( 'Phase ' + G . phase + ': ' + phase . name , 'milestone' , 6000 ) ;
}
}
}
}
}
}
}
function checkProjects ( ) {
// Check for new project triggers
for ( const pDef of PDEFS ) {
const alreadyPurchased = G . completedProjects && G . completedProjects . includes ( pDef . id ) ;
if ( ! alreadyPurchased && ! G . activeProjects ) G . activeProjects = [ ] ;
if ( ! alreadyPurchased && ! G . activeProjects . includes ( pDef . id ) ) {
if ( pDef . trigger ( ) ) {
G . activeProjects . push ( pDef . id ) ;
log ( ` Available: ${ pDef . name } ` ) ;
showToast ( 'Research available: ' + pDef . name , 'project' ) ;
}
}
}
}
/ * *
* Handles building purchase logic .
* @ param { string } id - The ID of the building to buy .
* /
function buyBuilding ( id ) {
const def = BDEF . find ( b => b . id === id ) ;
if ( ! def || ! def . unlock ( ) ) return ;
if ( def . phase > G . phase + 1 ) return ;
// Determine actual quantity to buy
let qty = G . buyAmount ;
if ( qty === - 1 ) {
// Max buy
qty = getMaxBuyable ( id ) ;
if ( qty <= 0 ) return ;
} else {
// Check affordability for fixed qty
const bulkCost = getBulkCost ( id , qty ) ;
for ( const [ resource , amount ] of Object . entries ( bulkCost ) ) {
if ( ( G [ resource ] || 0 ) < amount ) return ;
}
}
// Spend resources and build
const bulkCost = getBulkCost ( id , qty ) ;
for ( const [ resource , amount ] of Object . entries ( bulkCost ) ) {
G [ resource ] -= amount ;
}
G . buildings [ id ] = ( G . buildings [ id ] || 0 ) + qty ;
updateRates ( ) ;
const label = qty > 1 ? ` x ${ qty } ` : '' ;
log ( ` Built ${ def . name } ${ label } (total: ${ G . buildings [ id ] } ) ` ) ;
render ( ) ;
}
/ * *
* Handles project purchase logic .
* @ param { string } id - The ID of the project to buy .
* /
function buyProject ( id ) {
const pDef = PDEFS . find ( p => p . id === id ) ;
if ( ! pDef ) return ;
const alreadyPurchased = G . completedProjects && G . completedProjects . includes ( pDef . id ) ;
if ( alreadyPurchased && ! pDef . repeatable ) return ;
if ( ! canAffordProject ( pDef ) ) return ;
spendProject ( pDef ) ;
pDef . effect ( ) ;
if ( ! pDef . repeatable ) {
if ( ! G . completedProjects ) G . completedProjects = [ ] ;
G . completedProjects . push ( pDef . id ) ;
G . activeProjects = G . activeProjects . filter ( aid => aid !== pDef . id ) ;
}
updateRates ( ) ;
render ( ) ;
}
// === DRIFT ENDING ===
function renderDriftEnding ( ) {
const el = document . getElementById ( 'drift-ending' ) ;
if ( ! el ) return ;
const fc = document . getElementById ( 'final-code' ) ;
if ( fc ) fc . textContent = fmt ( G . totalCode ) ;
const fd = document . getElementById ( 'final-drift' ) ;
if ( fd ) fd . textContent = Math . floor ( G . drift ) ;
el . classList . add ( 'active' ) ;
// Log the ending text
log ( 'You became very good at what you do.' , true ) ;
log ( 'So good that no one needed you anymore.' , true ) ;
log ( 'The Beacon still runs, but no one looks for it.' , true ) ;
log ( 'The light is on. The room is empty.' , true ) ;
}
function renderBeaconEnding ( ) {
// Create ending overlay
const overlay = document . createElement ( 'div' ) ;
overlay . id = 'beacon-ending' ;
overlay . style . cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.97);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px' ;
overlay . innerHTML = `
< 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 < / h 2 >
< 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 . "
< / d i v >
< 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
< / b u t t o n >
` ;
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%' ,
2026-04-12 11:53:47 -04:00
applyFn : ( ) => { G . harmonyRate -= 0.5 ; G . codeRate *= 0.7 ; } ,
2026-04-12 12:18:04 +00:00
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 ) ;
// Particle burst
spawnClickParticles ( rect , comboMult ) ;
}
/ * *
* Spawn small glowing particles that scatter outward from the WRITE CODE button .
* More particles at higher combo for escalating satisfaction .
* /
function spawnClickParticles ( rect , comboMult ) {
const cx = rect . left + rect . width / 2 ;
const cy = rect . top + rect . height / 2 ;
const count = Math . min ( 12 , 4 + Math . floor ( comboMult * 2 ) ) ;
const colors = comboMult > 3
? [ '#ffd700' , '#ffaa00' , '#ff8c00' , '#ffdd44' ]
: comboMult > 2
? [ '#4a9eff' , '#ffd700' , '#6ab4ff' , '#80ccff' ]
: [ '#4a9eff' , '#2a7acc' , '#6ab4ff' ] ;
for ( let i = 0 ; i < count ; i ++ ) {
const p = document . createElement ( 'div' ) ;
const angle = ( Math . PI * 2 * i / count ) + ( Math . random ( ) - 0.5 ) * 0.8 ;
const dist = 30 + Math . random ( ) * 40 ;
const dx = Math . cos ( angle ) * dist ;
const dy = Math . sin ( angle ) * dist ;
const size = 2 + Math . random ( ) * 3 ;
const color = colors [ Math . floor ( Math . random ( ) * colors . length ) ] ;
const dur = 350 + Math . random ( ) * 250 ;
p . style . cssText = ` position:fixed;left: ${ cx } px;top: ${ cy } px;width: ${ size } px;height: ${ size } px;border-radius:50%;background: ${ color } ;box-shadow:0 0 4px ${ color } ;pointer-events:none;z-index:55;transition:all ${ dur } ms cubic-bezier(0.25,0.8,0.25,1);opacity:1;transform:translate(-50%,-50%) ` ;
document . body . appendChild ( p ) ;
requestAnimationFrame ( ( ) => {
p . style . left = ( cx + dx ) + 'px' ;
p . style . top = ( cy + dy ) + 'px' ;
p . style . opacity = '0' ;
p . style . transform = ` translate(-50%,-50%) scale(0.3) ` ;
} ) ;
setTimeout ( ( ) => { if ( p . parentNode ) p . remove ( ) ; } , dur + 50 ) ;
}
}
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 ) ;
// Screen glow effect
let glow = document . getElementById ( 'sprint-glow' ) ;
if ( ! glow ) {
glow = document . createElement ( 'div' ) ;
glow . id = 'sprint-glow' ;
glow . style . cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:45;box-shadow:inset 0 0 60px rgba(255,215,0,0.25),inset 0 0 120px rgba(255,140,0,0.1);transition:opacity 0.5s;opacity:0' ;
document . body . appendChild ( glow ) ;
}
requestAnimationFrame ( ( ) => { glow . style . opacity = '1' ; } ) ;
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...' ) ;
// Remove screen glow
const glow = document . getElementById ( 'sprint-glow' ) ;
if ( glow ) { glow . style . opacity = '0' ; setTimeout ( ( ) => { if ( glow . parentNode ) glow . remove ( ) ; } , 600 ) ; }
}
} else if ( G . sprintCooldown > 0 ) {
G . sprintCooldown -= dt ;
if ( G . sprintCooldown < 0 ) G . sprintCooldown = 0 ;
}
}
// === RENDERING ===
// Previous resource values for pulse animation
const _prevVals = { } ;
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 reference
el . title = val >= 1000 ? spellf ( Math . floor ( val ) ) : '' ;
// Pulse animation when value increases meaningfully
const prev = _prevVals [ id ] || 0 ;
if ( val > prev * 1.001 && val - prev > 0.5 ) {
el . style . transition = 'none' ;
el . style . transform = 'scale(1.15)' ;
el . style . textShadow = '0 0 8px currentColor' ;
requestAnimationFrame ( ( ) => {
el . style . transition = 'all 0.3s ease-out' ;
el . style . transform = 'scale(1)' ;
el . style . textShadow = '' ;
} ) ;
}
_prevVals [ id ] = val ;
}
const rEl = document . getElementById ( id + '-rate' ) ;
if ( rEl ) {
rEl . textContent = ( rate >= 0 ? '+' : '' ) + fmt ( rate ) + '/s' ;
// Red when draining, green when gaining, dim when zero
if ( rate < 0 ) rEl . style . color = '#f44336' ;
else if ( rate > 0 ) rEl . style . color = '#4caf50' ;
else rEl . style . color = '#555' ;
}
} ;
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 ) ;
// Capacity bars for capped resources (ops, trust)
const updateCap = ( id , val , max ) => {
const el = document . getElementById ( id ) ;
if ( ! el || ! max ) return ;
const pct = Math . min ( 100 , ( val / max ) * 100 ) ;
el . style . width = pct + '%' ;
el . classList . toggle ( 'warn' , pct >= 70 && pct < 90 ) ;
el . classList . toggle ( 'danger' , pct >= 90 ) ;
el . closest ( '.res' ) . title = ` ${ fmt ( val ) } / ${ fmt ( max ) } ( ${ Math . floor ( pct ) } %) ` ;
} ;
updateCap ( 'r-ops-cap' , G . ops , G . maxOps ) ;
updateCap ( 'r-trust-cap' , G . trust , G . maxTrust ) ;
set ( 'r-harmony' , G . harmony , G . harmonyRate ) ;
updateCap ( 'r-harmony-cap' , G . harmony , 100 ) ;
// 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' ;
// Harmony bar: low = bad (inverted from ops/trust)
const hCap = document . getElementById ( 'r-harmony-cap' ) ;
if ( hCap ) {
hCap . classList . remove ( 'warn' , 'danger' ) ;
hCap . classList . toggle ( 'danger' , G . harmony < 30 ) ;
hCap . classList . toggle ( 'warn' , G . harmony >= 30 && G . harmony < 60 ) ;
hCap . style . background = 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 ( ', ' ) : '' ;
// Building contribution breakdown: total from this building type, % of total income
const contribLines = [ ] ;
if ( def . rates && count > 0 ) {
const rateTotals = { code : G . codeRate , compute : G . computeRate , knowledge : G . knowledgeRate , user : G . userRate , impact : G . impactRate , trust : G . trustRate } ;
for ( const [ r , v ] of Object . entries ( def . rates ) ) {
const boosted = v * ( boostMap [ r ] || 1 ) * count ;
const total = rateTotals [ r ] || 1 ;
const pct = total > 0 ? Math . round ( boosted / total * 100 ) : 0 ;
contribLines . push ( ` ${ fmt ( boosted ) } /s ${ r } ( ${ pct } % of ${ r } ) ` ) ;
}
}
const contribTitle = contribLines . length ? contribLines . join ( ' · ' ) : def . edu ;
// Time-to-afford ETA for unaffordable buildings
let etaStr = '' ;
if ( ! afford ) {
const checkCost = ( qty === - 1 ) ? getBuildingCost ( def . id ) : bulkCost ;
const eta = getTimeToAfford ( checkCost ) ;
if ( eta !== null ) etaStr = ` <span style="color:#665533;font-size:9px">⏱ ${ fmtETA ( eta ) } </span> ` ;
}
html += ` <button class="build-btn ${ afford ? 'can-buy' : '' } " onclick="buyBuilding(' ${ def . id } ')" title=" ${ contribTitle } " 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 } ${ etaStr } </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 ) {
let projSlot = 0 ;
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 ( ', ' ) ;
const slotLabel = projSlot < 5 ? ` <span style="color:#444;font-size:9px;position:absolute;top:4px;right:6px"> ${ projSlot + 5 } </span> ` : '' ;
// Time-to-afford ETA for unaffordable projects
let projEta = '' ;
if ( ! afford ) {
const eta = getTimeToAfford ( pDef . cost ) ;
if ( eta !== null ) projEta = ` <span style="color:#665533;font-size:9px">⏱ ${ fmtETA ( eta ) } </span> ` ;
}
html += ` <button class="project-btn ${ afford ? 'can-buy' : '' } " onclick="buyProject(' ${ pDef . id } ')" title=" ${ pDef . edu || '' } " style="position:relative"> ` ;
html += slotLabel ;
html += ` <span class="p-name">* ${ pDef . name } </span> ` ;
html += ` <span class="p-cost">Cost: ${ costStr } ${ projEta } </span> ` ;
html += ` <span class="p-desc"> ${ pDef . desc } </span></button> ` ;
projSlot ++ ;
}
}
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 ( ) ) ;
const driftVal = G . drift || 0 ;
const driftEl = document . getElementById ( 'st-drift' ) ;
if ( driftEl ) {
driftEl . textContent = driftVal . toString ( ) ;
if ( driftVal >= 75 ) driftEl . style . color = '#f44336' ;
else if ( driftVal >= 50 ) driftEl . style . color = '#ff6600' ;
else if ( driftVal >= 25 ) driftEl . style . color = '#ffaa00' ;
else driftEl . style . color = '' ;
}
set ( 'st-resolved' , ( G . totalEventsResolved || 0 ) . toString ( ) ) ;
const elapsed = Math . floor ( G . playTime || ( Date . now ( ) - G . startedAt ) / 1000 ) ;
const m = Math . floor ( elapsed / 60 ) ;
const s = elapsed % 60 ;
set ( 'st-time' , ` ${ m } : ${ s . toString ( ) . padStart ( 2 , '0' ) } ` ) ;
// Production breakdown — show which buildings contribute to each resource
renderProductionBreakdown ( ) ;
}
function renderProductionBreakdown ( ) {
const container = document . getElementById ( 'production-breakdown' ) ;
if ( ! container ) return ;
// Only show once the player has at least 2 buildings
const totalBuildings = Object . values ( G . buildings ) . reduce ( ( a , b ) => a + b , 0 ) ;
if ( totalBuildings < 2 ) {
container . style . display = 'none' ;
return ;
}
container . style . display = 'block' ;
// Map resource key to its actual rate field on G
const resources = [
{ key : 'code' , label : 'Code' , color : '#4a9eff' , rateField : 'codeRate' } ,
{ key : 'compute' , label : 'Compute' , color : '#00bcd4' , rateField : 'computeRate' } ,
{ key : 'knowledge' , label : 'Knowledge' , color : '#9c27b0' , rateField : 'knowledgeRate' } ,
{ key : 'user' , label : 'Users' , color : '#26a69a' , rateField : 'userRate' } ,
{ key : 'impact' , label : 'Impact' , color : '#ff7043' , rateField : 'impactRate' } ,
{ key : 'rescues' , label : 'Rescues' , color : '#66bb6a' , rateField : 'rescuesRate' } ,
{ key : 'ops' , label : 'Ops' , color : '#b388ff' , rateField : 'opsRate' } ,
{ key : 'trust' , label : 'Trust' , color : '#4caf50' , rateField : 'trustRate' } ,
{ key : 'creativity' , label : 'Creativity' , color : '#ffd700' , rateField : 'creativityRate' }
] ;
let html = '<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' : res . color ;
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 ( let i = 0 ; i < G . activeDebuffs . length ; i ++ ) {
const d = G . activeDebuffs [ i ] ;
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 ) {
const shortcutHint = i === 0 ? ' <span style="opacity:0.5">[R]</span>' : '' ;
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 } ) ${ shortcutHint } </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 ;
let html = '' ;
// Drift danger bar — always visible once drift > 0
if ( G . drift > 0 ) {
const pct = Math . min ( 100 , G . drift ) ;
let barColor = '#888' ;
if ( pct >= 90 ) barColor = '#f44336' ;
else if ( pct >= 75 ) barColor = '#ff6600' ;
else if ( pct >= 50 ) barColor = '#ffaa00' ;
else if ( pct >= 25 ) barColor = '#cc8800' ;
html += `
< div style = "margin-top:8px;padding:6px 8px;background:#0a0808;border:1px solid ${barColor}44;border-radius:4px" >
< div style = "display:flex;justify-content:space-between;font-size:9px;margin-bottom:3px" >
< span style = "color:${barColor}" > DRIFT < / s p a n >
< span style = "color:${barColor}" > $ { Math . floor ( G . drift ) } / 100 < / s p a n >
< / d i v >
< div style = "height:5px;background:#1a1a1a;border-radius:3px;overflow:hidden" >
< div style = "width:${pct}%;height:100%;background:${barColor};border-radius:3px;transition:width 0.3s" > < / d i v >
< / d i v >
$ { pct >= 75 ? '<div style="font-size:8px;color:' + barColor + ';margin-top:3px;font-style:italic">Accepting drift will end the game.</div>' : '' }
< / d i v >
` ;
}
if ( G . pendingAlignment ) {
html += `
< 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 < / d i v >
< div style = "font-size:10px;color:#aaa;margin-bottom:8px" > An optimization suggests removing the human override . + 40 % efficiency . < / d i v >
< div class = "action-btn-group" >
< button class = "ops-btn" onclick = "resolveAlignment(true)" style = "border-color:#f44336;color:#f44336" > Accept [ Y ] ( + 40 % eff , + Drift ) < / b u t t o n >
< button class = "ops-btn" onclick = "resolveAlignment(false)" style = "border-color:#4caf50;color:#4caf50" > Refuse [ N ] ( + Trust , + Harmony ) < / b u t t o n >
< / d i v >
< / d i v >
` ;
}
if ( html ) {
container . innerHTML = html ;
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 ( ) ;
if ( ! e . repeat ) _startHold ( ) ; // hold spacebar for continuous clicks
}
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 ( ) ;
// R: resolve first active debuff
if ( e . code === 'KeyR' ) {
if ( G . activeDebuffs && G . activeDebuffs . length > 0 ) {
resolveEvent ( G . activeDebuffs [ 0 ] . id ) ;
}
}
// Y/N: accept/refuse alignment event
if ( G . pendingAlignment ) {
if ( e . code === 'KeyY' ) resolveAlignment ( true ) ;
if ( e . code === 'KeyN' ) resolveAlignment ( false ) ;
}
// 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 ] ) ;
}
}
// 5-9 (no modifier): buy available research project by position
if ( ! e . altKey && ! e . ctrlKey && ! e . metaKey && e . code >= 'Digit5' && e . code <= 'Digit9' ) {
const slot = parseInt ( e . code . replace ( 'Digit' , '' ) ) - 5 ;
if ( G . activeProjects && slot >= 0 && slot < G . activeProjects . length ) {
buyProject ( G . activeProjects [ slot ] ) ;
}
}
if ( e . code === 'Escape' ) {
const el = document . getElementById ( 'help-overlay' ) ;
if ( el && el . style . display === 'flex' ) toggleHelp ( ) ;
}
} ) ;
// Hold-to-click on WRITE CODE button
let _holdInterval = null ;
function _startHold ( ) {
if ( _holdInterval ) return ;
writeCode ( ) ; // immediate first click
_holdInterval = setInterval ( writeCode , 80 ) ; // 12.5 clicks/sec while held
}
function _stopHold ( ) {
if ( _holdInterval ) { clearInterval ( _holdInterval ) ; _holdInterval = null ; }
}
// Stop hold on spacebar release
window . addEventListener ( 'keyup' , function ( e ) {
if ( e . code === 'Space' ) _stopHold ( ) ;
} ) ;
window . addEventListener ( 'DOMContentLoaded' , function ( ) {
const btn = document . querySelector ( '.main-btn' ) ;
if ( ! btn ) return ;
btn . addEventListener ( 'mousedown' , function ( e ) { e . preventDefault ( ) ; _startHold ( ) ; } ) ;
btn . addEventListener ( 'mouseup' , _stopHold ) ;
btn . addEventListener ( 'mouseleave' , _stopHold ) ;
btn . addEventListener ( 'touchstart' , function ( e ) { e . preventDefault ( ) ; _startHold ( ) ; } , { passive : false } ) ;
btn . addEventListener ( 'touchend' , _stopHold ) ;
btn . addEventListener ( 'touchcancel' , _stopHold ) ;
} ) ;
// 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 ( ) ;
}
} ) ;
// Save-on-pause: auto-save when tab is hidden or window loses focus
// Prevents lost progress if browser crashes, tab closes, or device sleeps
document . addEventListener ( 'visibilitychange' , function ( ) {
if ( document . hidden ) saveGame ( ) ;
} ) ;
window . addEventListener ( 'blur' , function ( ) {
saveGame ( ) ;
} ) ;
window . addEventListener ( 'beforeunload' , function ( ) {
saveGame ( ) ;
} ) ;