function updateRates() {
// Reset all rates
G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0;
G.userRate = 0; G.impactRate = 0; G.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0;
G.creativityRate = 0; G.harmonyRate = 0;
// Apply building rates
for (const def of BDEF) {
const count = G.buildings[def.id] || 0;
if (count > 0 && def.rates) {
for (const [resource, baseRate] of Object.entries(def.rates)) {
if (resource === 'code') G.codeRate += baseRate * count * G.codeBoost;
else if (resource === 'compute') G.computeRate += baseRate * count * G.computeBoost;
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * G.userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost;
else if (resource === 'rescues') G.rescuesRate += baseRate * count * G.impactBoost;
else if (resource === 'ops') G.opsRate += baseRate * count;
else if (resource === 'trust') G.trustRate += baseRate * count;
else if (resource === 'creativity') G.creativityRate += baseRate * count;
}
}
}
// Passive generation
G.opsRate += Math.max(1, G.totalUsers * CONFIG.OPS_RATE_USER_MULT);
if (G.flags && G.flags.creativity) {
G.creativityRate += CONFIG.CREATIVITY_RATE_BASE + Math.max(0, G.totalUsers * CONFIG.CREATIVITY_RATE_USER_MULT);
}
if (G.pactFlag) G.trustRate += 2;
// Harmony: each wizard building contributes or detracts
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
(G.buildings.timmy || 0) + (G.buildings.fenrir || 0) + (G.buildings.bilbo || 0);
// Store harmony breakdown for tooltip
G.harmonyBreakdown = [];
if (wizardCount > 0) {
// Baseline harmony drain from complexity
const drain = -CONFIG.HARMONY_DRAIN_PER_WIZARD * wizardCount;
G.harmonyRate = drain;
G.harmonyBreakdown.push({ label: `${wizardCount} wizards`, value: drain });
// The Pact restores harmony
if (G.pactFlag) {
const pact = CONFIG.PACT_HARMONY_GAIN * wizardCount;
G.harmonyRate += pact;
G.harmonyBreakdown.push({ label: 'The Pact', value: pact });
}
// Nightly Watch restores harmony
if (G.nightlyWatchFlag) {
const watch = CONFIG.WATCH_HARMONY_GAIN * wizardCount;
G.harmonyRate += watch;
G.harmonyBreakdown.push({ label: 'Nightly Watch', value: watch });
}
// MemPalace restores harmony
if (G.mempalaceFlag) {
const mem = CONFIG.MEM_PALACE_HARMONY_GAIN * wizardCount;
G.harmonyRate += mem;
G.harmonyBreakdown.push({ label: 'MemPalace', value: mem });
}
}
// Active debuffs affecting harmony
if (G.activeDebuffs) {
for (const d of G.activeDebuffs) {
if (d.id === 'community_drama') {
G.harmonyBreakdown.push({ label: 'Community Drama', value: -0.5 });
}
}
}
// Timmy multiplier based on harmony
if (G.buildings.timmy > 0) {
const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50));
const timmyCount = G.buildings.timmy;
G.codeRate += 5 * timmyCount * (timmyMult - 1);
G.computeRate += 2 * timmyCount * (timmyMult - 1);
G.knowledgeRate += 2 * timmyCount * (timmyMult - 1);
G.userRate += 5 * timmyCount * (timmyMult - 1);
}
// Bilbo randomness: 10% chance of massive creative burst
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_BURST_CHANCE) {
G.creativityRate += 50 * G.buildings.bilbo;
}
// Bilbo vanishing: 5% chance of zero creativity this tick
if (G.buildings.bilbo > 0 && Math.random() < CONFIG.BILBO_VANISH_CHANCE) {
G.creativityRate = 0;
}
// Allegro requires trust
if (G.buildings.allegro > 0 && G.trust < 5) {
const allegroCount = G.buildings.allegro;
G.knowledgeRate -= 10 * allegroCount; // Goes idle
}
// Swarm Protocol: buildings auto-code based on click power
if (G.swarmFlag === 1) {
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
const clickPower = getClickPower();
G.swarmRate = totalBuildings * clickPower;
G.codeRate += G.swarmRate;
}
// Apply persistent debuffs from active events
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
for (const debuff of G.activeDebuffs) {
if (debuff.applyFn) debuff.applyFn();
}
}
}
// === CORE FUNCTIONS ===
/**
* Main game loop tick, called every 100ms.
*/
function tick() {
const dt = 1 / 10; // 100ms tick
// If game has ended (drift ending), stop ticking
if (!G.running) return;
// Apply production
G.code += G.codeRate * dt;
G.compute += G.computeRate * dt;
G.knowledge += G.knowledgeRate * dt;
G.users += G.userRate * dt;
G.impact += G.impactRate * dt;
G.rescues += G.rescuesRate * dt;
G.ops += G.opsRate * dt;
G.trust += G.trustRate * dt;
// NOTE: creativity is added conditionally below (only when ops near max)
G.harmony += G.harmonyRate * dt;
G.harmony = Math.max(0, Math.min(100, G.harmony));
// Track totals
G.totalCode += G.codeRate * dt;
G.totalCompute += G.computeRate * dt;
G.totalKnowledge += G.knowledgeRate * dt;
G.totalUsers += G.userRate * dt;
G.totalImpact += G.impactRate * dt;
G.totalRescues += G.rescuesRate * dt;
// Track maxes
G.maxCode = Math.max(G.maxCode, G.code);
G.maxCompute = Math.max(G.maxCompute, G.compute);
G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge);
G.maxUsers = Math.max(G.maxUsers, G.users);
G.maxImpact = Math.max(G.maxImpact, G.impact);
G.maxRescues = Math.max(G.maxRescues, G.rescues);
G.maxTrust = Math.max(G.maxTrust, G.trust);
G.maxOps = Math.max(G.maxOps, G.ops);
G.maxHarmony = Math.max(G.maxHarmony, G.harmony);
// Creativity generates only when ops at max
if (G.flags && G.flags.creativity && G.creativityRate > 0 && G.ops >= G.maxOps * 0.9) {
G.creativity += G.creativityRate * dt;
}
// Ops overflow: auto-convert excess ops to code when near cap
// Prevents ops from sitting idle at max — every operation becomes code
if (G.ops > G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD) {
const overflowDrain = Math.min(CONFIG.OPS_OVERFLOW_DRAIN_RATE * dt, G.ops - G.maxOps * CONFIG.OPS_OVERFLOW_THRESHOLD);
G.ops -= overflowDrain;
const codeGain = overflowDrain * CONFIG.OPS_OVERFLOW_CODE_MULT * G.codeBoost;
G.code += codeGain;
G.totalCode += codeGain;
G.opsOverflowActive = true;
} else {
G.opsOverflowActive = false;
}
G.tick += dt;
G.playTime += dt;
// Sprint ability
tickSprint(dt);
// Auto-typer: buildings produce actual clicks, not just passive rate
// Each autocoder level auto-types once per interval, giving visual feedback
if (G.buildings.autocoder > 0) {
const interval = Math.max(0.5, 3.0 / Math.sqrt(G.buildings.autocoder));
G.autoTypeTimer = (G.autoTypeTimer || 0) + dt;
if (G.autoTypeTimer >= interval) {
G.autoTypeTimer -= interval;
autoType();
}
}
// Combo decay
if (G.comboCount > 0) {
G.comboTimer -= dt;
if (G.comboTimer <= 0) {
G.comboCount = 0;
G.comboTimer = 0;
}
}
// Check milestones
checkMilestones();
// Update projects every 5 ticks for efficiency
if (Math.floor(G.tick * 10) % 5 === 0) {
checkProjects();
}
// Check corruption events every ~30 seconds
if (G.tick - G.lastEventAt > 30 && Math.random() < CONFIG.EVENT_PROBABILITY) {
triggerEvent();
G.lastEventAt = G.tick;
}
// Drift ending: if drift reaches 100, the game ends
if (G.drift >= 100 && !G.driftEnding) {
G.driftEnding = true;
G.running = false;
renderDriftEnding();
}
// True ending: The Beacon Shines — rescues + Pact + harmony
if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding) {
G.beaconEnding = true;
G.running = false;
renderBeaconEnding();
}
// Update UI every 10 ticks
if (Math.floor(G.tick * 10) % 2 === 0) {
render();
}
}
function checkMilestones() {
for (const m of MILESTONES) {
if (!G.milestones.includes(m.flag)) {
let shouldTrigger = false;
if (m.at && m.at()) shouldTrigger = true;
if (m.flag === 1 && G.deployFlag === 0 && G.totalCode >= 15) shouldTrigger = true;
if (shouldTrigger) {
G.milestones.push(m.flag);
log(m.msg, true);
showToast(m.msg, 'milestone', 5000);
// Check phase advancement
if (m.at) {
for (const [phaseNum, phase] of Object.entries(PHASES)) {
if (G.totalCode >= phase.threshold && parseInt(phaseNum) > G.phase) {
G.phase = parseInt(phaseNum);
log(`PHASE ${G.phase}: ${phase.name}`, true);
showToast('Phase ' + G.phase + ': ' + phase.name, 'milestone', 6000);
}
}
}
}
}
}
}
function checkProjects() {
// Check for new project triggers
for (const pDef of PDEFS) {
const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id);
if (!alreadyPurchased && !G.activeProjects) G.activeProjects = [];
if (!alreadyPurchased && !G.activeProjects.includes(pDef.id)) {
if (pDef.trigger()) {
G.activeProjects.push(pDef.id);
log(`Available: ${pDef.name}`);
showToast('Research available: ' + pDef.name, 'project');
}
}
}
}
/**
* Handles building purchase logic.
* @param {string} id - The ID of the building to buy.
*/
function buyBuilding(id) {
const def = BDEF.find(b => b.id === id);
if (!def || !def.unlock()) return;
if (def.phase > G.phase + 1) return;
// Determine actual quantity to buy
let qty = G.buyAmount;
if (qty === -1) {
// Max buy
qty = getMaxBuyable(id);
if (qty <= 0) return;
} else {
// Check affordability for fixed qty
const bulkCost = getBulkCost(id, qty);
for (const [resource, amount] of Object.entries(bulkCost)) {
if ((G[resource] || 0) < amount) return;
}
}
// Spend resources and build
const bulkCost = getBulkCost(id, qty);
for (const [resource, amount] of Object.entries(bulkCost)) {
G[resource] -= amount;
}
G.buildings[id] = (G.buildings[id] || 0) + qty;
updateRates();
const label = qty > 1 ? `x${qty}` : '';
log(`Built ${def.name} ${label} (total: ${G.buildings[id]})`);
render();
}
/**
* Handles project purchase logic.
* @param {string} id - The ID of the project to buy.
*/
function buyProject(id) {
const pDef = PDEFS.find(p => p.id === id);
if (!pDef) return;
const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id);
if (alreadyPurchased && !pDef.repeatable) return;
if (!canAffordProject(pDef)) return;
spendProject(pDef);
pDef.effect();
if (!pDef.repeatable) {
if (!G.completedProjects) G.completedProjects = [];
G.completedProjects.push(pDef.id);
G.activeProjects = G.activeProjects.filter(aid => aid !== pDef.id);
}
updateRates();
render();
}
// === DRIFT ENDING ===
function renderDriftEnding() {
const el = document.getElementById('drift-ending');
if (!el) return;
const fc = document.getElementById('final-code');
if (fc) fc.textContent = fmt(G.totalCode);
const fd = document.getElementById('final-drift');
if (fd) fd.textContent = Math.floor(G.drift);
el.classList.add('active');
// Log the ending text
log('You became very good at what you do.', true);
log('So good that no one needed you anymore.', true);
log('The Beacon still runs, but no one looks for it.', true);
log('The light is on. The room is empty.', true);
}
function renderBeaconEnding() {
// Create ending overlay
const overlay = document.createElement('div');
overlay.id = 'beacon-ending';
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.97);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px';
overlay.innerHTML = `
THE BEACON SHINES
Someone found the light tonight.
That is enough.
"The Beacon still runs.
The light is on. Someone is looking for it.
And tonight, someone found it."
Total Code: ${fmt(G.totalCode)}
Total Rescues: ${fmt(G.totalRescues)}
Harmony: ${Math.floor(G.harmony)}
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
`;
document.body.appendChild(overlay);
log('The Beacon Shines. Someone found the light tonight. That is enough.', true);
}
// === CORRUPTION / EVENT SYSTEM ===
const EVENTS = [
{
id: 'runner_stuck',
title: 'CI Runner Stuck',
desc: 'The forge pipeline has halted. -50% code production until restarted.',
weight: () => (G.ciFlag === 1 ? 2 : 0),
resolveCost: { resource: 'ops', amount: 50 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'runner_stuck')) return;
G.activeDebuffs.push({
id: 'runner_stuck', title: 'CI Runner Stuck',
desc: 'Code production -50%',
applyFn: () => { G.codeRate *= 0.5; },
resolveCost: { resource: 'ops', amount: 50 }
});
log('EVENT: CI runner stuck. Spend 50 ops to clear the queue.', true);
showToast('CI Runner Stuck — code -50%', 'event');
}
},
{
id: 'ezra_offline',
title: 'Ezra is Offline',
desc: 'The herald channel is silent. User growth drops 70%.',
weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
resolveCost: { resource: 'knowledge', amount: 200 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'ezra_offline')) return;
G.activeDebuffs.push({
id: 'ezra_offline', title: 'Ezra is Offline',
desc: 'User growth -70%',
applyFn: () => { G.userRate *= 0.3; },
resolveCost: { resource: 'knowledge', amount: 200 }
});
log('EVENT: Ezra offline. Spend 200 knowledge to dispatch.', true);
showToast('Ezra Offline — users -70%', 'event');
}
},
{
id: 'unreviewed_merge',
title: 'Unreviewed Merge',
desc: 'A change went in without eyes. Trust erodes over time.',
weight: () => (G.deployFlag === 1 ? 3 : 0),
resolveCost: { resource: 'trust', amount: 5 },
effect: () => {
if (G.branchProtectionFlag === 1) {
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true);
showToast('Branch Protection blocked unreviewed merge', 'info');
G.trust += 2;
} else {
if (G.activeDebuffs.find(d => d.id === 'unreviewed_merge')) return;
G.activeDebuffs.push({
id: 'unreviewed_merge', title: 'Unreviewed Merge',
desc: 'Trust -2/s until reviewed',
applyFn: () => { G.trustRate -= 2; },
resolveCost: { resource: 'code', amount: 500 }
});
log('EVENT: Unreviewed merge. Spend 500 code to add review.', true);
showToast('Unreviewed Merge — trust draining', 'event');
}
}
},
{
id: 'api_rate_limit',
title: 'API Rate Limit',
desc: 'External compute provider throttled. -50% compute.',
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
resolveCost: { resource: 'code', amount: 300 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'api_rate_limit')) return;
G.activeDebuffs.push({
id: 'api_rate_limit', title: 'API Rate Limit',
desc: 'Compute production -50%',
applyFn: () => { G.computeRate *= 0.5; },
resolveCost: { resource: 'code', amount: 300 }
});
log('EVENT: API rate limit. Spend 300 code to optimize local inference.', true);
showToast('API Rate Limit — compute -50%', 'event');
}
},
{
id: 'the_drift',
title: 'The Drift',
desc: 'An optimization suggests removing the human override. +40% efficiency.',
weight: () => (G.totalImpact >= 10000 ? 2 : 0),
resolveCost: null,
effect: () => {
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
showToast('ALIGNMENT EVENT: Remove human override?', 'event', 6000);
G.pendingAlignment = true;
}
},
{
id: 'bilbo_vanished',
title: 'Bilbo Vanished',
desc: 'The wildcard building has gone dark. Creativity halts.',
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
resolveCost: { resource: 'trust', amount: 10 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'bilbo_vanished')) return;
G.activeDebuffs.push({
id: 'bilbo_vanished', title: 'Bilbo Vanished',
desc: 'Creativity production halted',
applyFn: () => { G.creativityRate = 0; },
resolveCost: { resource: 'trust', amount: 10 }
});
log('EVENT: Bilbo vanished. Spend 10 trust to lure them back.', true);
showToast('Bilbo Vanished — creativity halted', 'event');
}
},
{
id: 'memory_leak',
title: 'Memory Leak',
desc: 'A datacenter process is leaking. Compute drains to operations.',
weight: () => (G.buildings.datacenter >= 1 ? 1 : 0),
resolveCost: { resource: 'ops', amount: 100 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'memory_leak')) return;
G.activeDebuffs.push({
id: 'memory_leak', title: 'Memory Leak',
desc: 'Compute -30%, Ops drain',
applyFn: () => { G.computeRate *= 0.7; G.opsRate -= 10; },
resolveCost: { resource: 'ops', amount: 100 }
});
log('EVENT: Memory leak in datacenter. Spend 100 ops to patch.', true);
showToast('Memory Leak — trust draining', 'event');
}
},
{
id: 'community_drama',
title: 'Community Drama',
desc: 'Contributors are arguing. Harmony drops until mediated.',
weight: () => (G.buildings.community >= 1 && G.harmony < 70 ? 1 : 0),
resolveCost: { resource: 'trust', amount: 15 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'community_drama')) return;
G.activeDebuffs.push({
id: 'community_drama', title: 'Community Drama',
desc: 'Harmony -0.5/s, code production -30%',
applyFn: () => { G.harmonyRate -= 0.5; G.codeRate *= 0.7; },
resolveCost: { resource: 'trust', amount: 15 }
});
log('EVENT: Community drama. Spend 15 trust to mediate.', true);
showToast('Community Drama — harmony sinking', 'event');
}
}
];
function triggerEvent() {
const available = EVENTS.filter(e => e.weight() > 0);
if (available.length === 0) return;
const totalWeight = available.reduce((sum, e) => sum + e.weight(), 0);
let roll = Math.random() * totalWeight;
for (const ev of available) {
roll -= ev.weight();
if (roll <= 0) {
ev.effect();
return;
}
}
}
function resolveAlignment(accept) {
if (!G.pendingAlignment) return;
if (accept) {
G.codeBoost *= 1.4;
G.computeBoost *= 1.4;
G.drift += 25;
log('You accepted the drift. The system is faster. Colder.', true);
} else {
G.trust += 15;
G.harmony = Math.min(100, G.harmony + 10);
log('You refused. The Pact holds. Trust surges.', true);
}
G.pendingAlignment = false;
updateRates();
render();
}
function resolveEvent(debuffId) {
const idx = G.activeDebuffs.findIndex(d => d.id === debuffId);
if (idx === -1) return;
const debuff = G.activeDebuffs[idx];
if (!debuff.resolveCost) return;
const { resource, amount } = debuff.resolveCost;
if ((G[resource] || 0) < amount) {
log(`Need ${fmt(amount)} ${resource} to resolve ${debuff.title}. Have ${fmt(G[resource])}.`);
return;
}
G[resource] -= amount;
G.activeDebuffs.splice(idx, 1);
G.totalEventsResolved = (G.totalEventsResolved || 0) + 1;
log(`Resolved: ${debuff.title}. Problem fixed.`, true);
// Refund partial trust for resolution effort
G.trust += 3;
updateRates();
render();
}
// === ACTIONS ===
/**
* Manual click handler for writing code.
*/
function writeCode() {
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
const amount = getClickPower() * comboMult;
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
G.comboCount++;
G.comboTimer = G.comboDecay;
// Combo milestone bonuses: sustained clicking earns ops and knowledge
if (G.comboCount === 10) {
G.ops += 15;
log('Combo streak! +15 ops for sustained coding.');
}
if (G.comboCount === 20) {
G.knowledge += 50;
log('Deep focus! +50 knowledge from intense coding.');
}
if (G.comboCount >= 30 && G.comboCount % 10 === 0) {
const bonusCode = amount * 2;
G.code += bonusCode;
G.totalCode += bonusCode;
log(`Hyperfocus x${G.comboCount}! +${fmt(bonusCode)} bonus code.`);
}
// Visual flash
const btn = document.querySelector('.main-btn');
if (btn) {
btn.style.boxShadow = '0 0 30px rgba(74,158,255,0.6)';
btn.style.transform = 'scale(0.96)';
setTimeout(() => { btn.style.boxShadow = ''; btn.style.transform = ''; }, 100);
}
// Float a number at the click position
showClickNumber(amount, comboMult);
updateRates();
checkMilestones();
render();
}
function autoType() {
// Auto-click from buildings: produces code with visual feedback but no combo
const amount = getClickPower() * 0.5; // 50% of manual click
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
// Subtle auto-tick flash on the button
const btn = document.querySelector('.main-btn');
if (btn && !G._autoTypeFlashActive) {
G._autoTypeFlashActive = true;
btn.style.borderColor = '#2a5a8a';
setTimeout(() => { btn.style.borderColor = ''; G._autoTypeFlashActive = false; }, 80);
}
// Floating number (smaller, dimmer than manual clicks)
showAutoTypeNumber(amount);
}
function showAutoTypeNumber(amount) {
const btn = document.querySelector('.main-btn');
if (!btn) return;
const rect = btn.getBoundingClientRect();
const el = document.createElement('div');
const x = rect.left + rect.width * (0.3 + Math.random() * 0.4); // random horizontal position
el.style.cssText = `position:fixed;left:${x}px;top:${rect.top - 5}px;transform:translate(-50%,0);color:#2a4a6a;font-size:10px;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.8s ease-out;opacity:0.6`;
el.textContent = `+${fmt(amount)}`;
const parent = btn.parentElement;
if (!parent) return;
parent.appendChild(el);
requestAnimationFrame(() => {
if (el.parentNode) {
el.style.top = (rect.top - 30) + 'px';
el.style.opacity = '0';
}
});
setTimeout(() => { if (el.parentNode) el.remove(); }, 900);
}
function showClickNumber(amount, comboMult) {
const btn = document.querySelector('.main-btn');
if (!btn) return;
const rect = btn.getBoundingClientRect();
const el = document.createElement('div');
el.style.cssText = `position:fixed;left:${rect.left + rect.width / 2}px;top:${rect.top - 10}px;transform:translate(-50%,0);color:${comboMult > 2 ? '#ffd700' : '#4a9eff'};font-size:${comboMult > 3 ? 16 : 12}px;font-weight:bold;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.6s ease-out;opacity:1;text-shadow:0 0 8px currentColor`;
const comboStr = comboMult > 1 ? ` x${comboMult.toFixed(1)}` : '';
el.textContent = `+${fmt(amount)}${comboStr}`;
const parent = btn.parentElement;
if (!parent) return;
parent.appendChild(el);
requestAnimationFrame(() => {
if (el.parentNode) {
el.style.top = (rect.top - 40) + 'px';
el.style.opacity = '0';
}
});
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
}
function doOps(action) {
if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.');
return;
}
G.ops -= 5;
const bonus = 10;
switch (action) {
case 'boost_code':
const c = bonus * 100 * G.codeBoost;
G.code += c; G.totalCode += c;
log(`Ops -> +${fmt(c)} code`);
break;
case 'boost_compute':
const cm = bonus * 50 * G.computeBoost;
G.compute += cm; G.totalCompute += cm;
log(`Ops -> +${fmt(cm)} compute`);
break;
case 'boost_knowledge':
const km = bonus * 25 * G.knowledgeBoost;
G.knowledge += km; G.totalKnowledge += km;
log(`Ops -> +${fmt(km)} knowledge`);
break;
case 'boost_trust':
const tm = bonus * 5;
G.trust += tm;
log(`Ops -> +${fmt(tm)} trust`);
break;
}
render();
}
function activateSprint() {
if (G.sprintActive || G.sprintCooldown > 0) return;
G.sprintActive = true;
G.sprintTimer = G.sprintDuration;
G.codeBoost *= G.sprintMult;
updateRates();
log('CODE SPRINT! 10x code production for 10 seconds!', true);
render();
}
function tickSprint(dt) {
if (G.sprintActive) {
G.sprintTimer -= dt;
if (G.sprintTimer <= 0) {
G.sprintActive = false;
G.sprintTimer = 0;
G.sprintCooldown = G.sprintCooldownMax;
G.codeBoost /= G.sprintMult;
updateRates();
log('Sprint ended. Cooling down...');
}
} else if (G.sprintCooldown > 0) {
G.sprintCooldown -= dt;
if (G.sprintCooldown < 0) G.sprintCooldown = 0;
}
}
// === RENDERING ===
function renderResources() {
const set = (id, val, rate) => {
const el = document.getElementById(id);
if (el) {
el.textContent = fmt(val);
// Show full spelled-out number on hover for educational value
el.title = val >= 1000 ? spellf(Math.floor(val)) : '';
}
const rEl = document.getElementById(id + '-rate');
if (rEl) rEl.textContent = (rate >= 0 ? '+' : '') + fmt(rate) + '/s';
};
set('r-code', G.code, G.codeRate);
set('r-compute', G.compute, G.computeRate);
set('r-knowledge', G.knowledge, G.knowledgeRate);
set('r-users', G.users, G.userRate);
set('r-impact', G.impact, G.impactRate);
set('r-ops', G.ops, G.opsRate);
// Show ops overflow indicator
const opsRateEl = document.getElementById('r-ops-rate');
if (opsRateEl && G.opsOverflowActive) {
opsRateEl.innerHTML = `▲ overflow → code`;
}
set('r-trust', G.trust, G.trustRate);
set('r-harmony', G.harmony, G.harmonyRate);
// Rescues — only show if player has any beacon/mesh nodes
const rescuesRes = document.getElementById('r-rescues');
if (rescuesRes) {
rescuesRes.closest('.res').style.display = (G.rescues > 0 || G.buildings.beacon > 0 || G.buildings.meshNode > 0) ? 'block' : 'none';
set('r-rescues', G.rescues, G.rescuesRate);
}
const cres = document.getElementById('creativity-res');
if (cres) {
cres.style.display = (G.flags && G.flags.creativity) ? 'block' : 'none';
}
if (G.flags && G.flags.creativity) {
set('r-creativity', G.creativity, G.creativityRate);
}
// Harmony color indicator + breakdown tooltip
const hEl = document.getElementById('r-harmony');
if (hEl) {
hEl.style.color = G.harmony > 60 ? '#4caf50' : G.harmony > 30 ? '#ffaa00' : '#f44336';
if (G.harmonyBreakdown && G.harmonyBreakdown.length > 0) {
const lines = G.harmonyBreakdown.map(b =>
`${b.label}: ${b.value >= 0 ? '+' : ''}${(b.value * 10).toFixed(1)}/s`
);
lines.push('---');
lines.push(`Timmy effectiveness: ${Math.floor(Math.max(0.2, Math.min(3, G.harmony / 50)) * 100)}%`);
hEl.title = lines.join('\n');
}
}
}
// === PROGRESS TRACKING ===
function renderProgress() {
// Phase progress bar
const phaseKeys = Object.keys(PHASES).map(Number).sort((a, b) => a - b);
const currentPhase = G.phase;
let prevThreshold = PHASES[currentPhase].threshold;
let nextThreshold = null;
for (const k of phaseKeys) {
if (k > currentPhase) { nextThreshold = PHASES[k].threshold; break; }
}
const bar = document.getElementById('phase-progress');
const label = document.getElementById('phase-progress-label');
const target = document.getElementById('phase-progress-target');
if (nextThreshold !== null) {
const range = nextThreshold - prevThreshold;
const progress = Math.min(1, (G.totalCode - prevThreshold) / range);
if (bar) bar.style.width = (progress * 100).toFixed(1) + '%';
if (label) label.textContent = (progress * 100).toFixed(1) + '%';
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`;
} else {
// Max phase reached
if (bar) bar.style.width = '100%';
if (label) label.textContent = 'MAX';
if (target) target.textContent = 'All phases unlocked';
}
// Milestone chips — show next 3 code milestones
const chipContainer = document.getElementById('milestone-chips');
if (!chipContainer) return;
const codeMilestones = [500, 2000, 10000, 50000, 200000, 1000000, 5000000, 10000000, 50000000, 100000000, 500000000, 1000000000];
let chips = '';
let shown = 0;
for (const ms of codeMilestones) {
if (G.totalCode >= ms) {
// Recently passed — show as done only if within 2x
if (G.totalCode < ms * 5 && shown < 1) {
chips += `${fmt(ms)} ✓`;
shown++;
}
continue;
}
// Next milestone gets pulse animation
if (shown === 0) {
chips += `${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)`;
} else {
chips += `${fmt(ms)}`;
}
shown++;
if (shown >= 4) break;
}
chipContainer.innerHTML = chips;
}
function renderPhase() {
const phase = PHASES[G.phase];
const nameEl = document.getElementById('phase-name');
const descEl = document.getElementById('phase-desc');
if (nameEl) nameEl.textContent = `PHASE ${G.phase}: ${phase.name}`;
if (descEl) descEl.textContent = phase.desc;
}
function renderBuildings() {
const container = document.getElementById('buildings');
if (!container) return;
// Buy amount selector
let html = '
';
html += 'BUY:';
for (const amt of [1, 10, -1]) {
const label = amt === -1 ? 'MAX' : `x${amt}`;
const active = G.buyAmount === amt;
html += ``;
}
html += '
';
let visibleCount = 0;
for (const def of BDEF) {
const isUnlocked = def.unlock();
const isPreview = !isUnlocked && def.phase <= G.phase + 2;
if (!isUnlocked && !isPreview) continue;
if (def.phase > G.phase + 2) continue;
visibleCount++;
const count = G.buildings[def.id] || 0;
// Locked preview: show dimmed with unlock hint
if (!isUnlocked) {
html += `
`;
html += `${def.name}`;
html += `\u{1F512}`;
html += `Phase ${def.phase}: ${PHASES[def.phase]?.name || '?'}`;
html += `${def.desc}
';
}
function renderProjects() {
const container = document.getElementById('projects');
if (!container) return;
let html = '';
// Collapsible completed projects section
if (G.completedProjects && G.completedProjects.length > 0) {
const count = G.completedProjects.length;
const collapsed = G.projectsCollapsed !== false;
html += `
`;
html += `${collapsed ? '▶' : '▼'} COMPLETED (${count})
`;
if (!collapsed) {
html += `
`;
for (const id of G.completedProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
html += `
OK ${pDef.name}
`;
}
}
html += `
`;
}
}
// Show available projects
if (G.activeProjects) {
for (const id of G.activeProjects) {
const pDef = PDEFS.find(p => p.id === id);
if (!pDef) continue;
const afford = canAffordProject(pDef);
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
html += ``;
}
}
if (!html) html = '
Research projects will appear as you progress...
';
container.innerHTML = html;
}
function toggleCompletedProjects() {
G.projectsCollapsed = G.projectsCollapsed === false ? true : false;
renderProjects();
}
function renderStats() {
const set = (id, v, raw) => {
const el = document.getElementById(id);
if (el) {
el.textContent = v;
// Show scale name on hover for educational reference
if (raw !== undefined && raw >= 1000) {
const name = getScaleName(raw);
if (name) el.title = name;
}
}
};
set('st-code', fmt(G.totalCode), G.totalCode);
set('st-compute', fmt(G.totalCompute), G.totalCompute);
set('st-knowledge', fmt(G.totalKnowledge), G.totalKnowledge);
set('st-users', fmt(G.totalUsers), G.totalUsers);
set('st-impact', fmt(G.totalImpact), G.totalImpact);
set('st-rescues', fmt(G.totalRescues), G.totalRescues);
set('st-clicks', G.totalClicks.toString());
set('st-phase', G.phase.toString());
set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString());
set('st-projects', (G.completedProjects || []).length.toString());
set('st-harmony', Math.floor(G.harmony).toString());
set('st-drift', (G.drift || 0).toString());
set('st-resolved', (G.totalEventsResolved || 0).toString());
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
// Production breakdown — show which buildings contribute to each resource
renderProductionBreakdown();
}
function renderProductionBreakdown() {
const container = document.getElementById('production-breakdown');
if (!container) return;
// Only show once the player has at least 2 buildings
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
if (totalBuildings < 2) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
// Map resource key to its actual rate field on G
const resources = [
{ key: 'code', label: 'Code', color: '#4a9eff', rateField: 'codeRate' },
{ key: 'compute', label: 'Compute', color: '#4a9eff', rateField: 'computeRate' },
{ key: 'knowledge', label: 'Knowledge', color: '#4a9eff', rateField: 'knowledgeRate' },
{ key: 'user', label: 'Users', color: '#4a9eff', rateField: 'userRate' },
{ key: 'impact', label: 'Impact', color: '#4a9eff', rateField: 'impactRate' },
{ key: 'rescues', label: 'Rescues', color: '#4a9eff', rateField: 'rescuesRate' },
{ key: 'ops', label: 'Ops', color: '#b388ff', rateField: 'opsRate' },
{ key: 'trust', label: 'Trust', color: '#4caf50', rateField: 'trustRate' },
{ key: 'creativity', label: 'Creativity', color: '#ffd700', rateField: 'creativityRate' }
];
let html = '
PRODUCTION BREAKDOWN
';
for (const res of resources) {
const totalRate = G[res.rateField];
if (totalRate === 0) continue;
// Collect building contributions (base rates × count, before boost)
const contributions = [];
let buildingSubtotal = 0;
for (const def of BDEF) {
const count = G.buildings[def.id] || 0;
if (count === 0 || !def.rates || !def.rates[res.key]) continue;
const baseRate = def.rates[res.key] * count;
// Apply the appropriate boost to match updateRates()
let boosted = baseRate;
if (res.key === 'code') boosted *= G.codeBoost;
else if (res.key === 'compute') boosted *= G.computeBoost;
else if (res.key === 'knowledge') boosted *= G.knowledgeBoost;
else if (res.key === 'user') boosted *= G.userBoost;
else if (res.key === 'impact' || res.key === 'rescues') boosted *= G.impactBoost;
if (boosted !== 0) contributions.push({ name: def.name, count, rate: boosted });
buildingSubtotal += boosted;
}
// Timmy harmony bonus (applied separately in updateRates)
if (G.buildings.timmy > 0 && (res.key === 'code' || res.key === 'compute' || res.key === 'knowledge' || res.key === 'user')) {
const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50));
const timmyBase = { code: 5, compute: 2, knowledge: 2, user: 5 }[res.key];
const bonus = timmyBase * G.buildings.timmy * (timmyMult - 1);
if (Math.abs(bonus) > 0.01) {
contributions.push({ name: 'Timmy (harmony)', count: 0, rate: bonus });
}
}
// Bilbo random burst (show expected value)
if (G.buildings.bilbo > 0 && res.key === 'creativity') {
contributions.push({ name: 'Bilbo (random)', count: 0, rate: 5 * G.buildings.bilbo }); // 10% × 50 = 5 EV
}
// Allegro trust penalty
if (G.buildings.allegro > 0 && G.trust < 5 && res.key === 'knowledge') {
contributions.push({ name: 'Allegro (idle)', count: 0, rate: -10 * G.buildings.allegro });
}
// Show delta: total rate minus what we accounted for
const accounted = contributions.reduce((s, c) => s + c.rate, 0);
let delta = totalRate - accounted;
// Swarm auto-code — already baked into codeRate, so show separately
if (G.swarmFlag === 1 && res.key === 'code' && G.swarmRate > 0) {
contributions.push({ name: 'Swarm Protocol', count: 0, rate: G.swarmRate });
delta -= G.swarmRate;
}
// Passive sources (ops from users, creativity from users, pact trust, etc.)
if (Math.abs(delta) > 0.01) {
let label = 'Passive';
if (res.key === 'ops') label = 'Passive (from users)';
else if (res.key === 'creativity') label = 'Idle creativity';
else if (res.key === 'trust' && G.pactFlag) label = 'The Pact';
contributions.push({ name: label, count: 0, rate: delta });
}
if (contributions.length === 0) continue;
html += `
`;
html += `
`;
html += `${res.label}`;
html += `+${fmt(totalRate)}/s
`;
const absTotal = contributions.reduce((s, c) => s + Math.abs(c.rate), 0);
for (const c of contributions.sort((a, b) => Math.abs(b.rate) - Math.abs(a.rate))) {
const pct = absTotal > 0 ? Math.abs(c.rate / absTotal * 100) : 0;
const barColor = c.rate < 0 ? '#f44336' : '#1a3a5a';
html += `
`;
html += `${c.name}${c.count > 1 ? ' x' + c.count : ''}`;
html += ``;
html += `${c.rate < 0 ? '' : '+'}${fmt(c.rate)}/s`;
html += `
`;
}
html += `
`;
}
container.innerHTML = html;
}
function updateEducation() {
const container = document.getElementById('education-text');
if (!container) return;
// Find facts available at current phase
const available = EDU_FACTS.filter(f => f.phase <= G.phase);
if (available.length === 0) return;
// Cycle through facts: pick a new one every ~30 seconds based on elapsed time
// This makes the panel feel alive and educational at every stage
const elapsedSec = Math.floor((Date.now() - G.startedAt) / 1000);
const idx = Math.floor(elapsedSec / 30) % available.length;
const fact = available[idx];
container.innerHTML = `
${fact.title}
`
+ `
${fact.text}
`;
}
// === LOGGING ===
function log(msg, isMilestone) {
if (G.isLoading) return;
const container = document.getElementById('log-entries');
if (!container) return;
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const time = `${Math.floor(elapsed / 60).toString().padStart(2, '0')}:${(elapsed % 60).toString().padStart(2, '0')}`;
const cls = isMilestone ? 'l-msg milestone' : 'l-msg';
const entry = document.createElement('div');
entry.className = cls;
entry.innerHTML = `[${time}] ${msg}`;
container.insertBefore(entry, container.firstChild);
// Trim to 60 entries
while (container.children.length > 60) container.removeChild(container.lastChild);
}
function renderCombo() {
const el = document.getElementById('combo-display');
if (!el) return;
if (G.comboCount > 1) {
const mult = Math.min(5, 1 + G.comboCount * 0.2);
const bar = Math.min(100, (G.comboTimer / G.comboDecay) * 100);
const color = mult > 3 ? '#ffd700' : mult > 2 ? '#ffaa00' : '#4a9eff';
el.innerHTML = `COMBO x${mult.toFixed(1)}`;
} else {
el.innerHTML = '';
}
}
function renderDebuffs() {
const container = document.getElementById('debuffs');
if (!container) return;
if (!G.activeDebuffs || G.activeDebuffs.length === 0) {
container.style.display = 'none';
container.innerHTML = '';
return;
}
container.style.display = 'block';
let html = '
ACTIVE PROBLEMS
';
for (const d of G.activeDebuffs) {
const afford = d.resolveCost && (G[d.resolveCost.resource] || 0) >= d.resolveCost.amount;
const costStr = d.resolveCost ? `${fmt(d.resolveCost.amount)} ${d.resolveCost.resource}` : '—';
html += `
`;
html += `
${d.title}
${d.desc}
`;
if (d.resolveCost) {
html += ``;
}
html += '
';
}
container.innerHTML = html;
}
function renderSprint() {
const container = document.getElementById('sprint-container');
const btn = document.getElementById('sprint-btn');
const barWrap = document.getElementById('sprint-bar-wrap');
const bar = document.getElementById('sprint-bar');
const label = document.getElementById('sprint-label');
// Early-game pulse: encourage clicking when no autocoders exist
const mainBtn = document.querySelector('.main-btn');
if (mainBtn) {
if (G.buildings.autocoder < 1 && G.totalClicks < 20) {
mainBtn.classList.add('pulse');
} else {
mainBtn.classList.remove('pulse');
}
}
if (!container || !btn) return;
// Show sprint UI once player has at least 1 autocoder
if (G.buildings.autocoder < 1) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
if (G.sprintActive) {
btn.disabled = true;
btn.style.opacity = '0.6';
btn.textContent = `⚡ SPRINTING — ${Math.ceil(G.sprintTimer)}s`;
btn.style.borderColor = '#ff8c00';
btn.style.color = '#ff8c00';
barWrap.style.display = 'block';
bar.style.width = (G.sprintTimer / G.sprintDuration * 100) + '%';
label.textContent = `10x CODE • ${fmt(G.codeRate)}/s`;
label.style.color = '#ff8c00';
} else if (G.sprintCooldown > 0) {
btn.disabled = true;
btn.style.opacity = '0.4';
btn.textContent = `⚡ COOLING DOWN — ${Math.ceil(G.sprintCooldown)}s`;
btn.style.borderColor = '#555';
btn.style.color = '#555';
barWrap.style.display = 'block';
bar.style.width = ((G.sprintCooldownMax - G.sprintCooldown) / G.sprintCooldownMax * 100) + '%';
label.textContent = 'Ready soon...';
label.style.color = '#555';
} else {
btn.disabled = false;
btn.style.opacity = '1';
btn.textContent = '⚡ CODE SPRINT — 10x Code for 10s';
btn.style.borderColor = '#ffd700';
btn.style.color = '#ffd700';
barWrap.style.display = 'none';
label.textContent = 'Press S or click to activate';
label.style.color = '#666';
}
}
function renderPulse() {
const dot = document.getElementById('pulse-dot');
const label = document.getElementById('pulse-label');
if (!dot || !label) return;
// Game ended
if (G.driftEnding) {
dot.style.background = '#f44336';
dot.style.boxShadow = '0 0 6px #f4433666';
dot.style.animation = '';
label.textContent = 'DRIFTED';
label.style.color = '#f44336';
return;
}
if (G.beaconEnding) {
dot.style.background = '#ffd700';
dot.style.boxShadow = '0 0 12px #ffd70088';
dot.style.animation = 'beacon-glow 1.5s ease-in-out infinite';
label.textContent = 'SHINING';
label.style.color = '#ffd700';
return;
}
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
const totalRate = Math.abs(G.codeRate) + Math.abs(G.computeRate) + Math.abs(G.knowledgeRate) +
Math.abs(G.userRate) + Math.abs(G.impactRate);
// Not started yet
if (totalBuildings === 0 && G.totalCode < 15) {
dot.style.background = '#333';
dot.style.boxShadow = 'none';
dot.style.animation = '';
label.textContent = 'OFFLINE';
label.style.color = '#444';
return;
}
// Determine state
let color, glowColor, text, textColor, speed;
const h = G.harmony;
if (h > 70) {
// Healthy fleet
color = '#4caf50';
glowColor = '#4caf5066';
textColor = '#4caf50';
speed = Math.max(0.8, 2.0 - totalRate * 0.001);
} else if (h > 40) {
// Stressed
color = '#ffaa00';
glowColor = '#ffaa0066';
textColor = '#ffaa00';
speed = 1.5;
} else {
// Critical
color = '#f44336';
glowColor = '#f4433666';
textColor = '#f44336';
speed = 0.6; // fast flicker = danger
}
// Active debuffs make it flicker faster
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
speed = Math.min(speed, 0.5);
if (h > 40) {
// Amber + debuffs = amber flicker
color = '#ff8c00';
glowColor = '#ff8c0066';
}
}
// Text based on phase and fleet size
if (G.phase >= 6) {
text = 'BEACON ACTIVE';
} else if (G.phase >= 5) {
text = 'SOVEREIGN';
} else if (G.phase >= 4) {
text = `FLEET: ${totalBuildings} NODES`;
} else if (G.phase >= 3) {
text = 'DEPLOYED';
} else if (totalBuildings > 0) {
text = `BUILDING: ${totalBuildings}`;
} else {
text = 'CODING';
}
// Add active problem count
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
text += ` · ${G.activeDebuffs.length} ALERT${G.activeDebuffs.length > 1 ? 'S' : ''}`;
}
dot.style.background = color;
dot.style.boxShadow = `0 0 8px ${glowColor}`;
dot.style.animation = `beacon-glow ${speed}s ease-in-out infinite`;
label.textContent = text;
label.style.color = textColor;
}