beacon: fix getMaxBuyable state mutation bug + add keyboard shortcuts help overlay

Fix: getMaxBuyable() was mutating G (subtracting/re-adding resources) during
render cycles in renderBuildings(). Now uses a read-only simulation with a
temporary resource copy. This prevented phantom resource fluctuations when
MAX buy was selected.

Feature: Press ? or click the ? button for a keyboard shortcuts overlay.
All keybindings listed in one place. Closes with ?, Esc, or clicking outside.
Added ? to the init log hint line.
This commit is contained in:
Timmy-Sprint
2026-04-10 17:44:33 -04:00
parent 24940fe465
commit 8d51349e64
2 changed files with 58 additions and 20 deletions

57
game.js
View File

@@ -866,32 +866,26 @@ function setBuyAmount(amt) {
function getMaxBuyable(id) {
const def = BDEF.find(b => b.id === id);
if (!def) return 0;
let count = G.buildings[id] || 0;
const count = G.buildings[id] || 0;
// Simulate purchases WITHOUT mutating G — read-only calculation
let tempResources = {};
for (const r of Object.keys(def.baseCost)) {
tempResources[r] = G[r] || 0;
}
let bought = 0;
let simCount = count;
while (true) {
const cost = {};
for (const [resource, amount] of Object.entries(def.baseCost)) {
cost[resource] = Math.floor(amount * Math.pow(def.costMult, count));
}
let canAfford = true;
for (const [resource, amount] of Object.entries(cost)) {
if ((G[resource] || 0) < amount) { canAfford = false; break; }
for (const [resource, amount] of Object.entries(def.baseCost)) {
const cost = Math.floor(amount * Math.pow(def.costMult, simCount));
if ((tempResources[resource] || 0) < cost) { canAfford = false; break; }
}
if (!canAfford) break;
// Spend from temporary copy
for (const [resource, amount] of Object.entries(cost)) {
G[resource] -= amount;
}
count++;
bought++;
}
// Refund: add back what we spent
let count2 = G.buildings[id] || 0;
for (let i = 0; i < bought; i++) {
for (const [resource, amount] of Object.entries(def.baseCost)) {
G[resource] += Math.floor(amount * Math.pow(def.costMult, count2));
tempResources[resource] -= Math.floor(amount * Math.pow(def.costMult, simCount));
}
count2++;
simCount++;
bought++;
}
return bought;
}
@@ -2417,7 +2411,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 S=Sprint 1-4=Ops B=Buy x1/10/MAX E=Export I=Import Ctrl+S=Save');
log('Keys: SPACE=Code S=Sprint 1-4=Ops B=Buy x1/10/MAX E=Export I=Import Ctrl+S=Save ?=Help');
log('Tip: Click fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code.');
}
@@ -2448,8 +2442,27 @@ window.addEventListener('load', function () {
setInterval(updateEducation, 10000);
});
// Help overlay
function toggleHelp() {
const el = document.getElementById('help-overlay');
if (!el) return;
const isOpen = el.style.display === 'flex';
el.style.display = isOpen ? 'none' : 'flex';
}
// Keyboard shortcuts
window.addEventListener('keydown', function (e) {
// Help toggle (? or /) — works even in input fields
if (e.key === '?' || e.key === '/') {
// Only trigger ? when not typing in an input
if (e.target === document.body || e.key === '?') {
if (e.key === '?' || (e.key === '/' && e.target === document.body)) {
e.preventDefault();
toggleHelp();
return;
}
}
}
if (e.code === 'Space' && e.target === document.body) {
e.preventDefault();
writeCode();
@@ -2468,6 +2481,10 @@ window.addEventListener('keydown', function (e) {
if (e.code === 'KeyS') activateSprint();
if (e.code === 'KeyE') exportSave();
if (e.code === 'KeyI') importSave();
if (e.code === 'Escape') {
const el = document.getElementById('help-overlay');
if (el && el.style.display === 'flex') toggleHelp();
}
});
// Ctrl+S to save (must be on keydown to preventDefault)

View File

@@ -170,6 +170,27 @@ Events Resolved: <span id="st-resolved">0</span>
<div id="log-entries"></div>
</div>
<div id="save-toast" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div>
<div id="help-btn" onclick="toggleHelp()" style="position:fixed;bottom:16px;right:16px;width:28px;height:28px;background:#0e0e1a;border:1px solid #333;color:#555;font-size:14px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:50;font-family:inherit;transition:all 0.2s" title="Keyboard shortcuts (?)">?</div>
<div id="help-overlay" onclick="if(event.target===this)toggleHelp()" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:80;justify-content:center;align-items:center;flex-direction:column;padding:40px">
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:420px;width:100%">
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px;text-align:center">KEYBOARD SHORTCUTS</h3>
<div style="font-size:11px;line-height:2.2;color:#aaa">
<div style="display:flex;justify-content:space-between"><span style="color:#555">Write Code</span><span style="color:#4a9eff;font-family:monospace">SPACE</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Code Sprint</span><span style="color:#ffd700;font-family:monospace">S</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Code</span><span style="color:#b388ff;font-family:monospace">1</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Compute</span><span style="color:#b388ff;font-family:monospace">2</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Knowledge</span><span style="color:#b388ff;font-family:monospace">3</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Ops → Trust</span><span style="color:#b388ff;font-family:monospace">4</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Cycle Buy Amount (x1/x10/MAX)</span><span style="color:#4a9eff;font-family:monospace">B</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Save Game</span><span style="color:#4a9eff;font-family:monospace">Ctrl+S</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Export Save</span><span style="color:#4a9eff;font-family:monospace">E</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:#555">Import Save</span><span style="color:#4a9eff;font-family:monospace">I</span></div>
<div style="display:flex;justify-content:space-between;border-top:1px solid #1a1a2e;padding-top:8px;margin-top:4px"><span style="color:#555">This Help</span><span style="color:#555;font-family:monospace">? or /</span></div>
</div>
<div style="text-align:center;margin-top:16px;font-size:9px;color:#444">Click WRITE CODE fast for combo bonuses! 10x=ops, 20x=knowledge, 30x+=bonus code</div>
<button onclick="toggleHelp()" style="display:block;margin:16px auto 0;background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:6px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Close [?]</button>
</div>
</div>
<div id="drift-ending">
<h2>THE DRIFT</h2>
<p>You became very good at what you do.</p>