Compare commits

..

6 Commits

Author SHA1 Message Date
Alexander Whitestone
f0b894a2b6 wip: fix debuff corruption bug — debuffs no longer degrade boost multipliers on each updateRates() call; persist playTime across sessions
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 3s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 01:24:32 -04:00
Alexander Whitestone
bea958f723 wip: add ETA estimates to phase progress bar and milestone chips
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 2s
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-11 00:00:42 -04:00
Alexander Whitestone
b98cf38992 wip: add Alt+1-9 keyboard shortcuts for buying buildings 2026-04-10 22:26:33 -04:00
Alexander Whitestone
7cd47d1159 wip: show click power on WRITE CODE button
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 3s
Smoke Test / smoke (pull_request) Failing after 4s
2026-04-10 22:00:31 -04:00
Alexander Whitestone
a9e4889d88 wip: show boosted rates on buildings instead of raw base rates 2026-04-10 21:59:23 -04:00
Timmy-Sprint
970f3be00f beacon: add Fleet Status panel showing wizard health and production
Some checks failed
Smoke Test / smoke (push) Failing after 3s
New panel in the stats area displays each owned wizard building with:
- Name and current status (Active/Idle/Stressed/Offline/Vanished/Present)
- Color-coded indicator: green=healthy, amber=reduced, red=problem
- Production contribution breakdown per wizard
- Timmy shows effectiveness % scaled by harmony
- Allegro shows idle warning when trust < 5
- Ezra shows offline status when debuff is active
- Bilbo shows vanished status when debuff is active
- Harmony summary bar at the bottom

Makes the harmony/wizard interaction system visible and actionable.
Players can now see at a glance which wizards need attention.
2026-04-10 21:32:04 -04:00
2 changed files with 232 additions and 40 deletions

230
game.js
View File

@@ -1017,17 +1017,23 @@ function updateRates() {
G.userRate = 0; G.impactRate = 0; G.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0;
G.creativityRate = 0; G.harmonyRate = 0;
// Apply building rates
// Snapshot base boosts BEFORE debuffs modify them
// Without this, debuffs permanently degrade boost multipliers on each updateRates() call
let _codeBoost = G.codeBoost, _computeBoost = G.computeBoost;
let _knowledgeBoost = G.knowledgeBoost, _userBoost = G.userBoost;
let _impactBoost = G.impactBoost;
// Apply building rates using snapshot boosts (immune to debuff mutation)
for (const def of BDEF) {
const count = G.buildings[def.id] || 0;
if (count > 0 && def.rates) {
for (const [resource, baseRate] of Object.entries(def.rates)) {
if (resource === 'code') G.codeRate += baseRate * count * G.codeBoost;
else if (resource === 'compute') G.computeRate += baseRate * count * G.computeBoost;
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * G.userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost;
else if (resource === 'rescues') G.rescuesRate += baseRate * count * G.impactBoost;
if (resource === 'code') G.codeRate += baseRate * count * _codeBoost;
else if (resource === 'compute') G.computeRate += baseRate * count * _computeBoost;
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * _knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * _userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * _impactBoost;
else if (resource === 'rescues') G.rescuesRate += baseRate * count * _impactBoost;
else if (resource === 'ops') G.opsRate += baseRate * count;
else if (resource === 'trust') G.trustRate += baseRate * count;
else if (resource === 'creativity') G.creativityRate += baseRate * count;
@@ -1108,15 +1114,24 @@ function updateRates() {
// Swarm Protocol: buildings auto-code based on click power
if (G.swarmFlag === 1) {
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
const clickPower = getClickPower();
G.swarmRate = totalBuildings * clickPower;
// Compute click power using snapshot boost to avoid debuff mutation
const _clickPower = (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * _codeBoost;
G.swarmRate = totalBuildings * _clickPower;
G.codeRate += G.swarmRate;
}
// Apply persistent debuffs from active events
// Apply persistent debuffs to rates (NOT to global boost fields — prevents corruption)
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
for (const debuff of G.activeDebuffs) {
if (debuff.applyFn) debuff.applyFn();
switch (debuff.id) {
case 'runner_stuck': G.codeRate *= 0.5; break;
case 'ezra_offline': G.userRate *= 0.3; break;
case 'unreviewed_merge': G.trustRate -= 2; break;
case 'api_rate_limit': G.computeRate *= 0.5; break;
case 'bilbo_vanished': G.creativityRate = 0; break;
case 'memory_leak': G.computeRate *= 0.7; G.opsRate -= 10; break;
case 'community_drama': G.harmonyRate -= 0.5; G.codeRate *= 0.7; break;
}
}
}
}
@@ -1182,6 +1197,7 @@ function tick() {
}
G.tick += dt;
G.playTime += dt;
// Sprint ability
tickSprint(dt);
@@ -1828,7 +1844,16 @@ function renderProgress() {
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)`;
// ETA to next phase
let etaStr = '';
if (G.codeRate > 0) {
const remaining = nextThreshold - G.totalCode;
const secs = remaining / G.codeRate;
if (secs < 60) etaStr = `${Math.ceil(secs)}s`;
else if (secs < 3600) etaStr = `${Math.floor(secs / 60)}m ${Math.floor(secs % 60)}s`;
else etaStr = `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
}
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)${etaStr}`;
} else {
// Max phase reached
if (bar) bar.style.width = '100%';
@@ -1854,7 +1879,14 @@ function renderProgress() {
}
// Next milestone gets pulse animation
if (shown === 0) {
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)</span>`;
let etaStr = '';
if (G.codeRate > 0) {
const secs = (ms - G.totalCode) / G.codeRate;
if (secs < 60) etaStr = ` ~${Math.ceil(secs)}s`;
else if (secs < 3600) etaStr = ` ~${Math.floor(secs / 60)}m`;
else etaStr = ` ~${Math.floor(secs / 3600)}h`;
}
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)${etaStr}</span>`;
} else {
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
}
@@ -1887,6 +1919,7 @@ function renderBuildings() {
html += '</div>';
let visibleCount = 0;
let slotIndex = 0;
for (const def of BDEF) {
const isUnlocked = def.unlock();
@@ -1907,6 +1940,10 @@ function renderBuildings() {
continue;
}
// Slot number for keyboard shortcut (Alt+1-9)
const slotLabel = slotIndex < 9 ? `<span style="color:#444;font-size:9px;position:absolute;top:4px;right:6px">${slotIndex + 1}</span>` : '';
slotIndex++;
// Calculate bulk cost display
let qty = G.buyAmount;
let afford = false;
@@ -1932,9 +1969,16 @@ function renderBuildings() {
if (qty > 1) costStr = `x${qty}: ${costStr}`;
}
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${v}/${r}/s`).join(', ') : '';
// Show boosted (actual) rate per building, not raw base rate
const boostMap = { code: G.codeBoost, compute: G.computeBoost, knowledge: G.knowledgeBoost, user: G.userBoost, impact: G.impactBoost, rescues: G.impactBoost };
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => {
const boosted = v * (boostMap[r] || 1);
const label = boosted >= 1000 ? fmt(boosted) : boosted.toFixed(boosted % 1 === 0 ? 0 : 1);
return `+${label}/${r}/s`;
}).join(', ') : '';
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" aria-label="Buy ${def.name}, cost ${costStr}">`;
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" style="position:relative">`;
html += slotLabel;
html += `<span class="b-name">${def.name}</span>`;
if (count > 0) html += `<span class="b-count">x${count}</span>`;
html += `<span class="b-cost">Cost: ${costStr}</span>`;
@@ -1977,7 +2021,7 @@ function renderProjects() {
const afford = canAffordProject(pDef);
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}">`;
html += `<span class="p-name">* ${pDef.name}</span>`;
html += `<span class="p-cost">Cost: ${costStr}</span>`;
html += `<span class="p-desc">${pDef.desc}</span></button>`;
@@ -2019,7 +2063,7 @@ function renderStats() {
set('st-drift', (G.drift || 0).toString());
set('st-resolved', (G.totalEventsResolved || 0).toString());
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const elapsed = Math.floor(G.playTime || (Date.now() - G.startedAt) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
@@ -2137,6 +2181,118 @@ function renderProductionBreakdown() {
container.innerHTML = html;
}
// === FLEET STATUS PANEL ===
function renderFleetStatus() {
const container = document.getElementById('fleet-status');
if (!container) return;
// Only show once player has at least one wizard building
const wizardDefs = BDEF.filter(b =>
['bezalel','allegro','ezra','timmy','fenrir','bilbo'].includes(b.id)
);
const owned = wizardDefs.filter(d => (G.buildings[d.id] || 0) > 0);
if (owned.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
const h = G.harmony;
const timmyEff = Math.max(20, Math.min(300, (h / 50) * 100));
let html = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">FLEET STATUS</h3>';
html += '<div style="display:flex;gap:6px;flex-wrap:wrap">';
for (const def of owned) {
const count = G.buildings[def.id] || 0;
let status, color, detail;
switch (def.id) {
case 'bezalel':
status = 'Active';
color = '#4caf50';
detail = `+${fmt(50 * count * G.codeBoost)} code/s, +${2 * count} ops/s`;
break;
case 'allegro':
if (G.trust < 5) {
status = 'IDLE';
color = '#f44336';
detail = 'Needs trust ≥5 to function';
} else {
status = 'Active';
color = '#4caf50';
detail = `+${fmt(10 * count * G.knowledgeBoost)} knowledge/s`;
}
break;
case 'ezra':
const ezraDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'ezra_offline');
if (ezraDebuff) {
status = 'OFFLINE';
color = '#f44336';
detail = 'Channel down — users -70%';
} else {
status = 'Active';
color = '#4caf50';
detail = `+${fmt(25 * count * G.userBoost)} users/s, +${(0.5 * count).toFixed(1)} trust/s`;
}
break;
case 'timmy':
if (h < 20) {
status = 'STRESSED';
color = '#f44336';
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony critical`;
} else if (h < 50) {
status = 'Reduced';
color = '#ffaa00';
detail = `Effectiveness: ${Math.floor(timmyEff)}% — harmony low`;
} else {
status = 'Healthy';
color = '#4caf50';
detail = `Effectiveness: ${Math.floor(timmyEff)}% — all production boosted`;
}
break;
case 'fenrir':
status = 'Watching';
color = '#4a9eff';
detail = `+${2 * count} trust/s, -${1 * count} ops/s (security cost)`;
break;
case 'bilbo':
const bilboDebuff = G.activeDebuffs && G.activeDebuffs.find(d => d.id === 'bilbo_vanished');
if (bilboDebuff) {
status = 'VANISHED';
color = '#f44336';
detail = 'Creativity halted — spend trust to lure back';
} else {
status = 'Present';
color = Math.random() < 0.1 ? '#ffd700' : '#b388ff'; // occasional gold flash
detail = `+${count} creativity/s (10% burst chance, 5% vanish chance)`;
}
break;
}
html += `<div style="flex:1;min-width:140px;background:#0c0c18;border:1px solid ${color}33;border-radius:4px;padding:6px 8px;border-left:2px solid ${color}">`;
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
html += `<span style="color:${color};font-weight:600;font-size:10px">${def.name.split(' — ')[0]}</span>`;
html += `<span style="font-size:8px;color:${color};opacity:0.8;padding:1px 4px;border:1px solid ${color}44;border-radius:2px">${status}</span>`;
html += `</div>`;
html += `<div style="font-size:9px;color:#888;margin-top:2px">${detail}</div>`;
if (count > 1) html += `<div style="font-size:8px;color:#555;margin-top:1px">x${count}</div>`;
html += `</div>`;
}
html += '</div>';
// Harmony summary bar
const harmonyColor = h > 60 ? '#4caf50' : h > 30 ? '#ffaa00' : '#f44336';
html += `<div style="margin-top:8px;display:flex;align-items:center;gap:6px">`;
html += `<span style="font-size:9px;color:#666;min-width:60px">Harmony</span>`;
html += `<div style="flex:1;height:4px;background:#111;border-radius:2px;overflow:hidden"><div style="width:${h}%;height:100%;background:${harmonyColor};border-radius:2px;transition:width 0.5s"></div></div>`;
html += `<span style="font-size:9px;color:${harmonyColor};min-width:35px;text-align:right">${Math.floor(h)}%</span>`;
html += `</div>`;
container.innerHTML = html;
}
function updateEducation() {
const container = document.getElementById('education-text');
if (!container) return;
@@ -2374,6 +2530,14 @@ function renderPulse() {
label.style.color = textColor;
}
function renderClickPower() {
const btn = document.querySelector('.main-btn');
if (!btn) return;
const power = getClickPower();
const label = power >= 1000 ? fmt(power) : power.toFixed(power % 1 === 0 ? 0 : 1);
btn.textContent = `WRITE CODE (+${label})`;
}
function render() {
renderResources();
renderPhase();
@@ -2388,6 +2552,8 @@ function render() {
renderSprint();
renderBulkOps();
renderPulse();
renderFleetStatus();
renderClickPower();
}
function renderAlignment() {
@@ -2451,8 +2617,7 @@ function exportSave() {
const ts = new Date().toISOString().slice(0, 10);
a.download = `beacon-save-${ts}.json`;
a.click();
// Delay revoke to let browser finish download
setTimeout(() => URL.revokeObjectURL(url), 1000);
URL.revokeObjectURL(url);
log('Save exported to file.');
}
@@ -2536,6 +2701,7 @@ function saveGame() {
swarmRate: G.swarmRate || 0,
strategicFlag: G.strategicFlag || 0,
projectsCollapsed: G.projectsCollapsed !== false,
playTime: G.playTime || 0,
savedAt: Date.now()
};
@@ -2567,7 +2733,8 @@ function loadGame() {
'drift', 'driftEnding', 'beaconEnding', 'pendingAlignment',
'lastEventAt', 'totalEventsResolved', 'buyAmount',
'sprintActive', 'sprintTimer', 'sprintCooldown',
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed'
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed',
'playTime'
];
G.isLoading = true;
@@ -2739,6 +2906,20 @@ function toggleHelp() {
el.style.display = isOpen ? 'none' : 'flex';
}
/**
* Returns ordered list of currently visible (unlocked) building IDs.
* Used for keyboard shortcut mapping (Alt+1-9).
*/
function getVisibleBuildingIds() {
const ids = [];
for (const def of BDEF) {
if (def.unlock()) {
ids.push(def.id);
}
}
return ids;
}
// Keyboard shortcuts
window.addEventListener('keydown', function (e) {
// Help toggle (? or /) — works even in input fields
@@ -2774,6 +2955,15 @@ window.addEventListener('keydown', function (e) {
if (e.code === 'KeyS') activateSprint();
if (e.code === 'KeyE') exportSave();
if (e.code === 'KeyI') importSave();
// Alt+1-9: buy building by slot position
if (e.altKey && e.code >= 'Digit1' && e.code <= 'Digit9') {
e.preventDefault();
const slot = parseInt(e.code.replace('Digit', '')) - 1;
const visible = getVisibleBuildingIds();
if (slot < visible.length) {
buyBuilding(visible[slot]);
}
}
if (e.code === 'Escape') {
const el = document.getElementById('help-overlay');
if (el && el.style.display === 'flex') toggleHelp();

View File

@@ -114,7 +114,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
<div class="milestone-row" id="milestone-chips"></div>
</div>
<div id="resources" role="region" aria-label="Resources" aria-live="polite">
<div id="resources">
<div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
<div class="res"><div class="r-label">Compute</div><div class="r-val" id="r-compute">0</div><div class="r-rate" id="r-compute-rate">+0/s</div></div>
<div class="res"><div class="r-label">Knowledge</div><div class="r-val" id="r-knowledge">0</div><div class="r-rate" id="r-knowledge-rate">+0/s</div></div>
@@ -127,40 +127,40 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
<div class="res"><div class="r-label">Harmony</div><div class="r-val" id="r-harmony">50</div><div class="r-rate" id="r-harmony-rate">+0/s</div></div>
</div>
<div id="main">
<div class="panel" id="action-panel" role="region" aria-label="Actions">
<div class="panel" id="action-panel">
<h2>ACTIONS</h2>
<div class="action-btn-group"><button class="main-btn" onclick="writeCode()" aria-label="Write code, generates code resource">WRITE CODE</button></div>
<div id="combo-display" role="status" aria-live="polite" style="text-align:center;font-size:10px;color:var(--dim);height:14px;margin-bottom:4px;transition:all 0.2s"></div>
<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')" aria-label="Convert 1 ops to code boost">Ops -&gt; Code</button>
<button class="ops-btn" onclick="doOps('boost_compute')" aria-label="Convert 1 ops to compute boost">Ops -&gt; Compute</button>
<button class="ops-btn" onclick="doOps('boost_code')">Ops -&gt; Code</button>
<button class="ops-btn" onclick="doOps('boost_compute')">Ops -&gt; Compute</button>
</div>
<div class="action-btn-group">
<button class="ops-btn" onclick="doOps('boost_knowledge')" aria-label="Convert 1 ops to knowledge boost">Ops -&gt; Knowledge</button>
<button class="ops-btn" onclick="doOps('boost_trust')" aria-label="Convert 1 ops to trust boost">Ops -&gt; Trust</button>
<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 class="action-btn-group" id="bulk-ops-row" style="display:none">
<button class="ops-btn" onclick="doOps('boost_code', 50)" aria-label="Convert 50 ops to code boost" style="border-color:#555;color:#888">50→Code</button>
<button class="ops-btn" onclick="doOps('boost_compute', 50)" aria-label="Convert 50 ops to compute boost" style="border-color:#555;color:#888">50→Compute</button>
<button class="ops-btn" onclick="doOps('boost_knowledge', 50)" aria-label="Convert 50 ops to knowledge boost" style="border-color:#555;color:#888">50→Knowledge</button>
<button class="ops-btn" onclick="doOps('boost_code', 50)" style="border-color:#555;color:#888">50→Code</button>
<button class="ops-btn" onclick="doOps('boost_compute', 50)" style="border-color:#555;color:#888">50→Compute</button>
<button class="ops-btn" onclick="doOps('boost_knowledge', 50)" style="border-color:#555;color:#888">50→Knowledge</button>
</div>
<div id="sprint-container" style="display:none;margin-top:6px">
<button id="sprint-btn" class="main-btn" onclick="activateSprint()" aria-label="Activate code sprint, 10x code production for 10 seconds" style="font-size:11px;padding:8px 10px;border-color:#ffd700;color:#ffd700;width:100%">⚡ CODE SPRINT — 10x Code for 10s</button>
<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()" aria-label="Save game progress">Save Game [Ctrl+S]</button>
<button class="save-btn" onclick="saveGame()">Save Game [Ctrl+S]</button>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="save-btn" onclick="exportSave()" aria-label="Export save to file" style="flex:1">Export [E]</button>
<button class="save-btn" onclick="importSave()" aria-label="Import save from file" style="flex:1">Import [I]</button>
<button class="save-btn" onclick="exportSave()" style="flex:1">Export [E]</button>
<button class="save-btn" onclick="importSave()" style="flex:1">Import [I]</button>
</div>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}" aria-label="Reset all game progress permanently">Reset Progress</button>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
<h2>BUILDINGS</h2>
<div id="buildings"></div>
</div>
<div class="panel" id="project-panel" role="region" aria-label="Research Projects and Statistics">
<div class="panel" id="project-panel">
<h2>RESEARCH PROJECTS</h2>
<div id="projects"></div>
<h2>STATISTICS</h2>
@@ -180,17 +180,18 @@ 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 id="fleet-status" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
</div>
</div>
<div id="edu-panel" role="region" aria-label="Educational Content">
<div id="edu-panel">
<h3>WHAT YOU ARE LEARNING</h3>
<div id="education-text"><p class="dim">Education facts appear as you play...</p></div>
</div>
<div id="log" role="log" aria-label="System Log" aria-live="polite">
<div id="log">
<h2>SYSTEM LOG</h2>
<div id="log-entries"></div>
</div>
<div id="save-toast" role="status" aria-live="polite" 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="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%">
@@ -203,6 +204,7 @@ Events Resolved: <span id="st-resolved">0</span>
<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">Buy Building (by slot)</span><span style="color:#4a9eff;font-family:monospace">Alt+1..9</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>