From 3d414b2de6292d58f1ffcb554c7b5a83109a806b Mon Sep 17 00:00:00 2001 From: Timmy-Sprint Date: Fri, 10 Apr 2026 02:46:42 -0400 Subject: [PATCH 1/6] beacon: fix offline progress to award all resources (rescues, ops, trust, creativity, harmony) Offline progress previously only calculated code, compute, knowledge, users, and impact. Players returning after time away missed rescues, ops, trust, creativity, and harmony accumulation. The welcome-back message now also only shows resources that actually had positive rates, reducing noise. --- game.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/game.js b/game.js index 94d1af7..3869dd6 100644 --- a/game.js +++ b/game.js @@ -1521,12 +1521,28 @@ function loadGame() { const uc = G.userRate * offSec * f; const ic = G.impactRate * offSec * f; + const rc = G.rescuesRate * offSec * f; + const oc = G.opsRate * offSec * f; + const tc = G.trustRate * offSec * f; + const crc = G.creativityRate * offSec * f; + const hc = G.harmonyRate * offSec * f; + G.code += gc; G.compute += cc; G.knowledge += kc; G.users += uc; G.impact += ic; + G.rescues += rc; G.ops += oc; G.trust += tc; + G.creativity += crc; + G.harmony = Math.max(0, Math.min(100, G.harmony + hc)); G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc; G.totalUsers += uc; G.totalImpact += ic; + G.totalRescues += rc; - log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${fmt(gc)} code, ${fmt(kc)} knowledge, ${fmt(uc)} users`); + const parts = []; + if (gc > 0) parts.push(`${fmt(gc)} code`); + if (kc > 0) parts.push(`${fmt(kc)} knowledge`); + if (uc > 0) parts.push(`${fmt(uc)} users`); + if (ic > 0) parts.push(`${fmt(ic)} impact`); + if (rc > 0) parts.push(`${fmt(rc)} rescues`); + log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`); } } -- 2.43.0 From a3f1802473e680b1edaa9532400cae2f3fd2b465 Mon Sep 17 00:00:00 2001 From: Timmy-Sprint Date: Fri, 10 Apr 2026 03:20:41 -0400 Subject: [PATCH 2/6] beacon: add progress bar and milestone chips to phase bar - Progress bar shows % toward next phase threshold based on totalCode - Milestone chips show upcoming code milestones with pulse animation on next target - Recently completed milestones shown with green checkmark - All elements use the existing cyber-monastic aesthetic --- game.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 11 +++++++++++ 2 files changed, 68 insertions(+) diff --git a/game.js b/game.js index 3869dd6..c2f269b 100644 --- a/game.js +++ b/game.js @@ -1297,6 +1297,62 @@ function renderResources() { } } +// === 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'); @@ -1433,6 +1489,7 @@ function render() { renderStats(); updateEducation(); renderAlignment(); + renderProgress(); } function renderAlignment() { diff --git a/index.html b/index.html index 6f6a2e8..e8b03cf 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,14 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code #phase-bar{text-align:center;padding:10px;margin:12px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px} #phase-bar .phase-name{font-size:14px;font-weight:700;color:var(--gold);letter-spacing:2px} #phase-bar .phase-desc{font-size:10px;color:var(--dim);margin-top:4px;font-style:italic} +.progress-wrap{margin-top:8px;height:6px;background:#111;border-radius:3px;overflow:hidden;position:relative} +.progress-fill{height:100%;border-radius:3px;transition:width 0.5s ease;background:linear-gradient(90deg,#1a3a5a,var(--accent))} +.progress-label{font-size:9px;color:var(--dim);margin-top:4px;display:flex;justify-content:space-between} +.milestone-row{display:flex;gap:6px;margin-top:6px;justify-content:center;flex-wrap:wrap} +.milestone-chip{font-size:9px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--dim);background:#0a0a14} +.milestone-chip.next{border-color:var(--accent);color:var(--accent);animation:pulse-chip 2s ease-in-out infinite} +.milestone-chip.done{border-color:#2a4a2a;color:var(--green);opacity:0.6} +@keyframes pulse-chip{0%,100%{box-shadow:0 0 0 rgba(74,158,255,0)}50%{box-shadow:0 0 8px rgba(74,158,255,0.3)}} #resources{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:6px;margin:12px 16px} .res{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;text-align:center} .res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px} @@ -69,6 +77,9 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
PHASE 1: THE FIRST LINE
Write code. Automate. Build the foundation.
+
+
0%Next: Phase 2 (2,000 code)
+
Code
0
+0/s
-- 2.43.0 From fe7615032590d1a6c4545748b41dc76f70d1646f Mon Sep 17 00:00:00 2001 From: Timmy-Sprint Date: Fri, 10 Apr 2026 03:58:55 -0400 Subject: [PATCH 3/6] 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. --- game.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- index.html | 1 + 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/game.js b/game.js index c2f269b..39645a4 100644 --- a/game.js +++ b/game.js @@ -107,6 +107,11 @@ const G = { lastEventAt: 0, eventCooldown: 0, + // Combo system + comboCount: 0, + comboTimer: 0, + comboDecay: 2.0, // seconds before combo resets + // Time tracking playTime: 0, startTime: 0 @@ -952,6 +957,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(); @@ -1212,16 +1226,46 @@ function resolveAlignment(accept) { // === 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.'); @@ -1481,6 +1525,19 @@ 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 = `COMBO x${mult.toFixed(1)} `; + } else { + el.innerHTML = ''; + } +} + function render() { renderResources(); renderPhase(); @@ -1490,6 +1547,7 @@ function render() { updateEducation(); renderAlignment(); renderProgress(); + renderCombo(); } function renderAlignment() { diff --git a/index.html b/index.html index e8b03cf..d93e8dd 100644 --- a/index.html +++ b/index.html @@ -97,6 +97,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code

ACTIONS

+
-- 2.43.0 From 5c88fe77be53abfc702beff0b32a89949ac01991 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Fri, 10 Apr 2026 04:27:15 -0400 Subject: [PATCH 4/6] 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. --- game.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/game.js b/game.js index 39645a4..9191bf4 100644 --- a/game.js +++ b/game.js @@ -927,7 +927,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)); @@ -1684,6 +1684,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 () { @@ -1719,4 +1720,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'); }); -- 2.43.0 From 8cdabe9771215efa182cbcf4d6c369041ca5a162 Mon Sep 17 00:00:00 2001 From: Timmy-Sprint Date: Fri, 10 Apr 2026 04:50:03 -0400 Subject: [PATCH 5/6] 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 --- game.js | 172 ++++++++++++++++++++++++++++++++++++++++++++++++----- index.html | 4 +- 2 files changed, 160 insertions(+), 16 deletions(-) diff --git a/game.js b/game.js index 9191bf4..631e7f5 100644 --- a/game.js +++ b/game.js @@ -106,6 +106,8 @@ const G = { drift: 0, lastEventAt: 0, eventCooldown: 0, + activeDebuffs: [], // [{id, title, desc, applyFn, resolveCost, resolveCostType}] + totalEventsResolved: 0, // Combo system comboCount: 0, @@ -909,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 === @@ -1127,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); } }, { @@ -1174,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; @@ -1182,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); } } ]; @@ -1223,6 +1302,26 @@ 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; @@ -1483,6 +1582,7 @@ 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); @@ -1538,6 +1638,29 @@ function renderCombo() { } } +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 render() { renderResources(); renderPhase(); @@ -1548,6 +1671,7 @@ function render() { renderAlignment(); renderProgress(); renderCombo(); + renderDebuffs(); } function renderAlignment() { @@ -1587,6 +1711,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, @@ -1607,6 +1733,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() }; @@ -1622,6 +1750,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 diff --git a/index.html b/index.html index d93e8dd..a701ffb 100644 --- a/index.html +++ b/index.html @@ -98,6 +98,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code

ACTIONS

+
@@ -128,7 +129,8 @@ Projects Done: 0
Time Played: 0:00
Clicks: 0
Harmony: 50
-Drift: 0 +Drift: 0
+Events Resolved: 0
-- 2.43.0 From 8d43b5c91150a5b93a1568a17f3b19d984ce53d2 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Fri, 10 Apr 2026 05:25:21 -0400 Subject: [PATCH 6/6] 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) --- game.js | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 1 + 2 files changed, 108 insertions(+) diff --git a/game.js b/game.js index 631e7f5..cc9c6b9 100644 --- a/game.js +++ b/game.js @@ -1588,6 +1588,113 @@ function renderStats() { 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); + 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 += `
`; + 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() { diff --git a/index.html b/index.html index a701ffb..ac72747 100644 --- a/index.html +++ b/index.html @@ -132,6 +132,7 @@ Harmony: 50
Drift: 0
Events Resolved: 0
+
-- 2.43.0