Files
the-beacon/js/engine.js

1355 lines
54 KiB
JavaScript
Raw Normal View History

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