Compare commits

..

2 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
2 changed files with 268 additions and 16 deletions

279
game.js
View File

@@ -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,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() {
@@ -1538,6 +1745,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 = '<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();
@@ -1548,6 +1778,7 @@ function render() {
renderAlignment();
renderProgress();
renderCombo();
renderDebuffs();
}
function renderAlignment() {
@@ -1587,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,
@@ -1607,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()
};
@@ -1622,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

View File

@@ -98,6 +98,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<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>
@@ -128,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">