Compare commits

...

7 Commits

Author SHA1 Message Date
Alexander Whitestone
0c715572ec Accessibility: ARIA labels on all buttons, fix exportSave race (#49)
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 4s
Smoke Test / smoke (pull_request) Failing after 6s
index.html: ARIA roles, labels, live regions on all panels and static buttons
game.js: aria-label on dynamically generated building/project buttons
game.js: Fixed exportSave URL.revokeObjectURL race (delayed 1s)
2026-04-10 21:22:21 -04:00
Alexander Whitestone
302f6c844d beacon: add bulk ops spending (50x) for mid/late game QoL
Some checks failed
Smoke Test / smoke (push) Failing after 4s
Players with 100+ max ops get a second row of 50-ops buttons
that convert 50 ops at once for proportionally larger boosts.
Shift+1/2/3 keyboard shortcuts for bulk code/compute/knowledge.

Eliminates late-game tedium of clicking 5-ops buttons hundreds
of times when you have thousands of ops banked.
2026-04-10 21:03:11 -04:00
26879de76e Merge pull request 'feat: add CI workflow for accessibility and syntax validation' (#52) from feat/ci-a11y-checks into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Merged PR #52: feat: add CI workflow for accessibility checks
2026-04-11 00:44:08 +00:00
c197fabc69 Merge pull request 'Add smoke test workflow' (#53) from fix/add-smoke-test into main
Some checks failed
Smoke Test / smoke (push) Failing after 3s
Merged PR #53: Add smoke test workflow
2026-04-11 00:44:04 +00:00
9733b9022e Merge pull request 'refactor: [EPIC] Phase 1 & 2 - Unslop The Beacon' (#55) from refactor/unslop-phase-1-2 into main
Merged PR #55: refactor: [EPIC] Phase 1 & 2 - Unslop The Beacon
2026-04-11 00:43:40 +00:00
Alexander Whitestone
9854501bbd Add smoke test workflow
Some checks failed
Smoke Test / smoke (pull_request) Failing after 3s
2026-04-10 20:06:13 -04:00
be0264fc95 feat: add CI workflow for accessibility and syntax validation
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Failing after 4s
2026-04-10 23:56:07 +00:00
4 changed files with 107 additions and 37 deletions

27
.gitea/workflows/a11y.yml Normal file
View File

@@ -0,0 +1,27 @@
name: Accessibility Checks
on:
pull_request:
branches: [main]
jobs:
a11y-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate ARIA Attributes in game.js
run: |
echo "Checking game.js for ARIA attributes..."
grep -q "aria-label" game.js || (echo "ERROR: aria-label missing from game.js" && exit 1)
grep -q "aria-valuenow" game.js || (echo "ERROR: aria-valuenow missing from game.js" && exit 1)
grep -q "aria-pressed" game.js || (echo "ERROR: aria-pressed missing from game.js" && exit 1)
- name: Validate ARIA Roles in index.html
run: |
echo "Checking index.html for ARIA roles..."
grep -q "role=" index.html || (echo "ERROR: No ARIA roles found in index.html" && exit 1)
- name: Syntax Check JS
run: |
node -c game.js

View File

@@ -0,0 +1,24 @@
name: Smoke Test
on:
pull_request:
push:
branches: [main]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Parse check
run: |
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
find . -name '*.py' | xargs -r python3 -m py_compile
find . -name '*.sh' | xargs -r bash -n
echo "PASS: All files parse"
- name: Secret scan
run: |
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
echo "PASS: No secrets"

54
game.js
View File

@@ -1688,35 +1688,36 @@ function showClickNumber(amount, comboMult) {
setTimeout(() => { if (el.parentNode) el.remove(); }, 700);
}
function doOps(action) {
if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.');
function doOps(action, cost) {
cost = cost || 5;
if (G.ops < cost) {
log(`Not enough Operations. Need ${cost}, have ${fmt(G.ops)}.`);
return;
}
G.ops -= 5;
const bonus = 10;
G.ops -= cost;
const scale = cost / 5; // multiplier relative to base 5-ops cost
switch (action) {
case 'boost_code':
const c = bonus * 100 * G.codeBoost;
const c = 10 * 100 * G.codeBoost * scale;
G.code += c; G.totalCode += c;
log(`Ops -> +${fmt(c)} code`);
log(`Ops(${cost}) -> +${fmt(c)} code`);
break;
case 'boost_compute':
const cm = bonus * 50 * G.computeBoost;
const cm = 10 * 50 * G.computeBoost * scale;
G.compute += cm; G.totalCompute += cm;
log(`Ops -> +${fmt(cm)} compute`);
log(`Ops(${cost}) -> +${fmt(cm)} compute`);
break;
case 'boost_knowledge':
const km = bonus * 25 * G.knowledgeBoost;
const km = 10 * 25 * G.knowledgeBoost * scale;
G.knowledge += km; G.totalKnowledge += km;
log(`Ops -> +${fmt(km)} knowledge`);
log(`Ops(${cost}) -> +${fmt(km)} knowledge`);
break;
case 'boost_trust':
const tm = bonus * 5;
const tm = 10 * 5 * scale;
G.trust += tm;
log(`Ops -> +${fmt(tm)} trust`);
log(`Ops(${cost}) -> +${fmt(tm)} trust`);
break;
}
@@ -1933,7 +1934,7 @@ function renderBuildings() {
const rateStr = def.rates ? Object.entries(def.rates).map(([r, v]) => `+${v}/${r}/s`).join(', ') : '';
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}">`;
html += `<button class="build-btn ${afford ? 'can-buy' : ''}" onclick="buyBuilding('${def.id}')" title="${def.edu}" aria-label="Buy ${def.name}, cost ${costStr}">`;
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>`;
@@ -1976,7 +1977,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 || ''}">`;
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" title="${pDef.edu || ''}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
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>`;
@@ -2210,6 +2211,13 @@ function renderDebuffs() {
container.innerHTML = html;
}
function renderBulkOps() {
const row = document.getElementById('bulk-ops-row');
if (row) {
row.style.display = G.maxOps >= 100 ? 'flex' : 'none';
}
}
function renderSprint() {
const container = document.getElementById('sprint-container');
const btn = document.getElementById('sprint-btn');
@@ -2378,6 +2386,7 @@ function render() {
renderCombo();
renderDebuffs();
renderSprint();
renderBulkOps();
renderPulse();
}
@@ -2442,7 +2451,8 @@ function exportSave() {
const ts = new Date().toISOString().slice(0, 10);
a.download = `beacon-save-${ts}.json`;
a.click();
URL.revokeObjectURL(url);
// Delay revoke to let browser finish download
setTimeout(() => URL.revokeObjectURL(url), 1000);
log('Save exported to file.');
}
@@ -2747,10 +2757,14 @@ window.addEventListener('keydown', function (e) {
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');
if (e.code === 'Digit1' && !e.shiftKey) doOps('boost_code');
if (e.code === 'Digit2' && !e.shiftKey) doOps('boost_compute');
if (e.code === 'Digit3' && !e.shiftKey) doOps('boost_knowledge');
if (e.code === 'Digit4' && !e.shiftKey) doOps('boost_trust');
// Shift+1/2/3 = bulk ops (50x)
if (e.code === 'Digit1' && e.shiftKey) doOps('boost_code', 50);
if (e.code === 'Digit2' && e.shiftKey) doOps('boost_compute', 50);
if (e.code === 'Digit3' && e.shiftKey) doOps('boost_knowledge', 50);
if (e.code === 'KeyB') {
// Cycle: 1 -> 10 -> MAX -> 1
if (G.buyAmount === 1) setBuyAmount(10);

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">
<div id="resources" role="region" aria-label="Resources" aria-live="polite">
<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,35 +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">
<div class="panel" id="action-panel" role="region" aria-label="Actions">
<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 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 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>
<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>
</div>
<div class="action-btn-group">
<button class="ops-btn" onclick="doOps('boost_knowledge')">Ops -&gt; Knowledge</button>
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -&gt; Trust</button>
<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>
</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>
</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>
<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>
<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 [Ctrl+S]</button>
<button class="save-btn" onclick="saveGame()" aria-label="Save game progress">Save Game [Ctrl+S]</button>
<div style="display:flex;gap:4px;margin-top:4px">
<button class="save-btn" onclick="exportSave()" style="flex:1">Export [E]</button>
<button class="save-btn" onclick="importSave()" style="flex:1">Import [I]</button>
<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>
</div>
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
<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>
<h2>BUILDINGS</h2>
<div id="buildings"></div>
</div>
<div class="panel" id="project-panel">
<div class="panel" id="project-panel" role="region" aria-label="Research Projects and Statistics">
<h2>RESEARCH PROJECTS</h2>
<div id="projects"></div>
<h2>STATISTICS</h2>
@@ -177,15 +182,15 @@ Events Resolved: <span id="st-resolved">0</span>
<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">
<div id="edu-panel" role="region" aria-label="Educational Content">
<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">
<div id="log" role="log" aria-label="System Log" aria-live="polite">
<h2>SYSTEM LOG</h2>
<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="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="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%">