beacon: add ops overflow auto-conversion to code

When Operations exceed 80% of max capacity, excess ops automatically
drain into Code at 2 ops/sec (10:1 ratio with code boost). This prevents
ops from sitting idle at the cap and gives the early game smoother flow.

Visual indicator shows 'overflow -> code' in the ops rate display when
active. No log spam - just works silently in the background.
This commit is contained in:
Timmy-Sprint
2026-04-10 14:44:54 -04:00
parent 6081844387
commit 9403f700d2
2 changed files with 136 additions and 1 deletions

132
game.js
View File

@@ -118,6 +118,14 @@ const G = {
// Bulk buy multiplier (1, 10, or -1 for max)
buyAmount: 1,
// Code Sprint ability
sprintActive: false,
sprintTimer: 0, // seconds remaining on active sprint
sprintCooldown: 0, // seconds until sprint available again
sprintDuration: 10, // seconds of boost
sprintCooldownMax: 60,// seconds cooldown
sprintMult: 10, // code multiplier during sprint
// Time tracking
playTime: 0,
startTime: 0
@@ -1027,8 +1035,24 @@ function tick() {
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 * 0.8) {
const overflowDrain = Math.min(2 * dt, G.ops - G.maxOps * 0.8);
G.ops -= overflowDrain;
const codeGain = overflowDrain * 10 * G.codeBoost;
G.code += codeGain;
G.totalCode += codeGain;
G.opsOverflowActive = true;
} else {
G.opsOverflowActive = false;
}
G.tick += dt;
// Sprint ability
tickSprint(dt);
// Combo decay
if (G.comboCount > 0) {
G.comboTimer -= dt;
@@ -1479,6 +1503,33 @@ function doOps(action) {
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) => {
@@ -1494,6 +1545,11 @@ function renderResources() {
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);
@@ -1891,6 +1947,53 @@ function renderDebuffs() {
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');
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 render() {
renderResources();
renderPhase();
@@ -1902,6 +2005,7 @@ function render() {
renderProgress();
renderCombo();
renderDebuffs();
renderSprint();
}
function renderAlignment() {
@@ -1995,6 +2099,9 @@ function saveGame() {
activeDebuffIds: debuffIds,
totalEventsResolved: G.totalEventsResolved || 0,
buyAmount: G.buyAmount || 1,
sprintActive: G.sprintActive || false,
sprintTimer: G.sprintTimer || 0,
sprintCooldown: G.sprintCooldown || 0,
savedAt: Date.now()
};
@@ -2010,6 +2117,28 @@ function loadGame() {
const data = JSON.parse(raw);
Object.assign(G, data);
// Restore sprint state properly
// codeBoost was saved with the sprint multiplier baked in
if (data.sprintActive) {
// Sprint was active when saved — check if it expired during offline time
const offSec = data.savedAt ? (Date.now() - data.savedAt) / 1000 : 0;
const remaining = (data.sprintTimer || 0) - offSec;
if (remaining > 0) {
// Sprint still going — keep boost, update timer
G.sprintActive = true;
G.sprintTimer = remaining;
G.sprintCooldown = 0;
} else {
// Sprint expired during offline — remove boost, start cooldown
G.sprintActive = false;
G.sprintTimer = 0;
G.codeBoost /= G.sprintMult;
const cdRemaining = G.sprintCooldownMax + remaining; // remaining is negative
G.sprintCooldown = Math.max(0, cdRemaining);
}
}
// If not sprintActive at save time, codeBoost is correct as-is
// Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed)
if (data.activeDebuffIds && data.activeDebuffIds.length > 0) {
G.activeDebuffs = [];
@@ -2109,7 +2238,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 B=Buy x1/x10/MAX');
log('Keys: SPACE=Code S=Code Sprint 1=Ops->Code 2=Ops->Compute 3=Ops->Knowledge 4=Ops->Trust B=Buy x1/x10/MAX');
}
window.addEventListener('load', function () {
@@ -2156,4 +2285,5 @@ window.addEventListener('keydown', function (e) {
else if (G.buyAmount === 10) setBuyAmount(-1);
else setBuyAmount(1);
}
if (e.code === 'KeyS') activateSprint();
});

View File

@@ -124,6 +124,11 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -&gt; Knowledge</button>
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -&gt; Trust</button>
</div>
<div id="sprint-container" style="display:none;margin-top:6px">
<button id="sprint-btn" class="main-btn" onclick="activateSprint()" style="font-size:11px;padding:8px 10px;border-color:#ffd700;color:#ffd700;width:100%">⚡ CODE SPRINT — 10x Code for 10s</button>
<div id="sprint-bar-wrap" style="display:none;margin-top:4px;height:4px;background:#111;border-radius:2px;overflow:hidden"><div id="sprint-bar" style="height:100%;background:linear-gradient(90deg,#ffd700,#ff8c00);border-radius:2px;transition:width 0.1s"></div></div>
<div id="sprint-label" style="font-size:9px;color:#666;margin-top:2px;text-align:center"></div>
</div>
<div id="alignment-ui" style="display:none"></div>
<button class="save-btn" onclick="saveGame()">Save Game</button>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>