Compare commits

..

4 Commits

Author SHA1 Message Date
Alexander Whitestone
8d43b5c911 beacon: add production breakdown panel showing per-building resource contributions
Players can now see exactly which buildings contribute to each resource
rate, including Timmy harmony bonuses, Bilbo randomness, Allegro trust
penalties, and passive generation. Appears once 2+ buildings are built.

Also includes minor fixes:
- Production bars sort by absolute contribution (negative rates visible)
- Delta calculation catches passive sources (ops from users, Pact trust)
2026-04-10 05:25:21 -04:00
Timmy-Sprint
8cdabe9771 beacon: persistent event remediation system
Events now create lasting debuffs instead of vanishing on the next tick.
Players see an ACTIVE PROBLEMS panel with resolution costs and can spend
resources to fix each problem. Added 2 new events (Memory Leak, Community
Drama) alongside the reworked originals. Events Resolved stat tracked.

Key changes:
- Events push persistent debuffs with applyFn instead of one-shot rate tweaks
- updateRates() applies active debuffs each tick (they persist until resolved)
- New resolveEvent(id) function: spend resources to clear a debuff
- ACTIVE PROBLEMS UI shows debuffs with cost and fix buttons
- Save/load reconstitutes debuff objects from saved IDs
- 2 new events: Memory Leak (datacenter), Community Drama (community+low harmony)
- Events Resolved counter in statistics
2026-04-10 04:50:03 -04:00
Alexander Whitestone
5c88fe77be beacon: fix double-counting creativity bug + add keyboard shortcuts for ops
Two changes:

1. Fixed bug where creativity was added TWICE per tick:
   - Line 930 (removed): unconditionally added creativityRate * dt
   - Line 954: conditionally adds only when ops >= 90% of max
   The conditional gate was the intent ('Creativity generates only when
   ops at max') but the unconditional add defeated it. Removed the
   unconditional addition so creativity actually respects the ops-max
   constraint as designed.

2. Added keyboard shortcuts for operations:
   - 1 = Ops -> Code
   - 2 = Ops -> Compute
   - 3 = Ops -> Knowledge
   - 4 = Ops -> Trust
   Only active when body is focused (not in input fields). SPACE
   still does Write Code. Added shortcut hint to init log.
2026-04-10 04:27:15 -04:00
Timmy-Sprint
fe76150325 beacon: add click combo system with floating damage numbers
Active play now rewards consecutive clicks: each click within 2s of
the last builds a combo multiplier up to 5x. The WRITE CODE button
flashes on click and a floating number shows the amount gained,
turning gold at high combo. Phase progression also adds base click
power (+2 per phase). Combo decays with a visible progress bar.

Makes clicking relevant at every stage of the game, not just the
first 30 seconds.
2026-04-10 03:58:55 -04:00
2 changed files with 336 additions and 19 deletions

349
game.js
View File

@@ -106,6 +106,13 @@ const G = {
drift: 0,
lastEventAt: 0,
eventCooldown: 0,
activeDebuffs: [], // [{id, title, desc, applyFn, resolveCost, resolveCostType}]
totalEventsResolved: 0,
// Combo system
comboCount: 0,
comboTimer: 0,
comboDecay: 2.0, // seconds before combo resets
// Time tracking
playTime: 0,
@@ -904,6 +911,13 @@ function updateRates() {
const allegroCount = G.buildings.allegro;
G.knowledgeRate -= 10 * allegroCount; // Goes idle
}
// 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 ===
@@ -922,7 +936,7 @@ function tick() {
G.rescues += G.rescuesRate * dt;
G.ops += G.opsRate * dt;
G.trust += G.trustRate * dt;
G.creativity += G.creativityRate * 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));
@@ -952,6 +966,15 @@ function tick() {
G.tick += dt;
// Combo decay
if (G.comboCount > 0) {
G.comboTimer -= dt;
if (G.comboTimer <= 0) {
G.comboCount = 0;
G.comboTimer = 0;
}
}
// Check milestones
checkMilestones();
@@ -1113,46 +1136,74 @@ const EVENTS = [
{
id: 'runner_stuck',
title: 'CI Runner Stuck',
desc: 'The forge pipeline has halted. Production slows until restarted.',
desc: 'The forge pipeline has halted. -50% code production until restarted.',
weight: () => (G.ciFlag === 1 ? 2 : 0),
resolveCost: { resource: 'ops', amount: 50 },
effect: () => {
G.codeRate *= 0.5;
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true);
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);
}
},
{
id: 'ezra_offline',
title: 'Ezra is Offline',
desc: 'The herald channel is silent. User growth stalls.',
desc: 'The herald channel is silent. User growth drops 70%.',
weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
resolveCost: { resource: 'knowledge', amount: 200 },
effect: () => {
G.userRate *= 0.3;
log('EVENT: Ezra offline. Dispatch required.', true);
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);
}
},
{
id: 'unreviewed_merge',
title: 'Unreviewed Merge',
desc: 'A change went in without eyes. Trust erodes.',
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);
G.trust += 2;
} else {
G.trust = Math.max(0, G.trust - 10);
log('EVENT: Unreviewed merge detected. Trust lost.', true);
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);
}
}
},
{
id: 'api_rate_limit',
title: 'API Rate Limit',
desc: 'External compute provider throttled.',
desc: 'External compute provider throttled. -50% compute.',
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
resolveCost: { resource: 'code', amount: 300 },
effect: () => {
G.computeRate *= 0.5;
log('EVENT: API rate limit hit. Local compute insufficient.', true);
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);
}
},
{
@@ -1160,6 +1211,7 @@ const EVENTS = [
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);
G.pendingAlignment = true;
@@ -1168,11 +1220,52 @@ const EVENTS = [
{
id: 'bilbo_vanished',
title: 'Bilbo Vanished',
desc: 'The wildcard building has gone dark.',
desc: 'The wildcard building has gone dark. Creativity halts.',
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
resolveCost: { resource: 'trust', amount: 10 },
effect: () => {
G.creativityRate = 0;
log('EVENT: Bilbo has vanished. Creativity halts.', true);
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);
}
},
{
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);
}
},
{
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);
}
}
];
@@ -1209,19 +1302,69 @@ function resolveAlignment(accept) {
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 ===
function writeCode() {
const base = 1;
const bonus = Math.floor(G.buildings.autocoder * 0.5);
const amount = (base + bonus) * G.codeBoost;
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
G.comboCount++;
G.comboTimer = G.comboDecay;
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * comboMult;
G.code += amount;
G.totalCode += amount;
G.totalClicks++;
// 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 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}`;
btn.parentElement.appendChild(el);
requestAnimationFrame(() => {
el.style.top = (rect.top - 40) + 'px';
el.style.opacity = '0';
});
setTimeout(() => el.remove(), 700);
}
function doOps(action) {
if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.');
@@ -1439,11 +1582,119 @@ function renderStats() {
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);
const delta = totalRate - accounted;
// 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() {
@@ -1481,6 +1732,42 @@ function log(msg, isMilestone) {
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 render() {
renderResources();
renderPhase();
@@ -1490,6 +1777,8 @@ function render() {
updateEducation();
renderAlignment();
renderProgress();
renderCombo();
renderDebuffs();
}
function renderAlignment() {
@@ -1529,6 +1818,8 @@ function showSaveToast() {
}
function saveGame() {
// Save debuff IDs (can't serialize functions)
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
const saveData = {
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,
@@ -1549,6 +1840,8 @@ function saveGame() {
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,
savedAt: Date.now()
};
@@ -1564,6 +1857,20 @@ function loadGame() {
const data = JSON.parse(raw);
Object.assign(G, data);
// 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();
// Offline progress
@@ -1626,6 +1933,7 @@ function initGame() {
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 1=Ops->Code 2=Ops->Compute 3=Ops->Knowledge 4=Ops->Trust');
}
window.addEventListener('load', function () {
@@ -1661,4 +1969,9 @@ window.addEventListener('keydown', function (e) {
e.preventDefault();
writeCode();
}
if (e.target !== document.body) return;
if (e.code === 'Digit1') doOps('boost_code');
if (e.code === 'Digit2') doOps('boost_compute');
if (e.code === 'Digit3') doOps('boost_knowledge');
if (e.code === 'Digit4') doOps('boost_trust');
});

View File

@@ -97,6 +97,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<div class="panel" id="action-panel">
<h2>ACTIONS</h2>
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()">WRITE CODE</button></div>
<div id="combo-display" style="text-align:center;font-size:10px;color:var(--dim);height:14px;margin-bottom:4px;transition:all 0.2s"></div>
<div id="debuffs" style="display:none;margin-top:8px"></div>
<div class="action-btn-group">
<button class="ops-btn" onclick="doOps('boost_code')">Ops -&gt; Code</button>
<button class="ops-btn" onclick="doOps('boost_compute')">Ops -&gt; Compute</button>
@@ -127,8 +129,10 @@ Projects Done: <span id="st-projects">0</span><br>
Time Played: <span id="st-time">0:00</span><br>
Clicks: <span id="st-clicks">0</span><br>
Harmony: <span id="st-harmony">50</span><br>
Drift: <span id="st-drift">0</span>
Drift: <span id="st-drift">0</span><br>
Events Resolved: <span id="st-resolved">0</span>
</div>
<div id="production-breakdown" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
</div>
</div>
<div id="edu-panel">