Compare commits
4 Commits
fix/192-re
...
fix/57
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5864562dc2 | ||
|
|
21edc4e424 | ||
| d5645fea58 | |||
|
|
db08f9a478 |
16
GENOME.md
16
GENOME.md
@@ -8,24 +8,32 @@ The Beacon is a browser-based idle/incremental game inspired by Universal Paperc
|
||||
|
||||
Static HTML/JS — no build step, no dependencies, no framework. Open `index.html` in any browser.
|
||||
|
||||
**5,128 lines of JavaScript** across 10 files. **1 HTML file** with embedded CSS (~300 lines). **1 Python test file** for reckoning projects.
|
||||
**6,033 lines of JavaScript** across 11 files. **1 HTML file** with embedded CSS (~300 lines). **3 test files** (2 Node.js, 1 Python).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
index.html (UI + embedded CSS)
|
||||
index.html (UI + embedded CSS + inline JS ~5000L)
|
||||
|
|
||||
+-- js/engine.js (1590L) Core game loop, tick, resources, buildings, projects, events
|
||||
+-- js/data.js (944L) Building definitions, project trees, event tables, phase data
|
||||
+-- js/render.js (390L) DOM rendering, UI updates, resource displays
|
||||
+-- js/combat.js (359L) Boss encounters, combat mechanics
|
||||
+-- js/combat.js (359L) Canvas boid-flocking combat visualization
|
||||
+-- js/sound.js (401L) Web Audio API ambient drone, phase-aware sound
|
||||
+-- js/dismantle.js (570L) The Dismantle sequence (late-game narrative)
|
||||
+-- js/main.js (223L) Initialization, game loop start, auto-save, help overlay
|
||||
+-- js/utils.js (314L) Formatting, save/load, export/import, DOM helpers
|
||||
+-- js/tutorial.js (251L) New player tutorial, step-by-step guidance
|
||||
+-- js/strategy.js (68L) NPC strategy logic for combat
|
||||
+-- game/npc-logic.js (18L) NPC behavior stub
|
||||
+-- js/emergent-mechanics.js Emergent game mechanics from player behavior
|
||||
|
||||
CI scripts (not browser runtime):
|
||||
+-- scripts/guardrails.sh Static analysis guardrails for game logic
|
||||
+-- scripts/smoke.mjs Playwright smoke tests
|
||||
|
||||
Reference prototypes (NOT loaded by runtime):
|
||||
+-- docs/reference/npc-logic-prototype.js NPC state machine prototype
|
||||
+-- docs/reference/guardrails-prototype.js Stat validation prototype
|
||||
```
|
||||
|
||||
## Entry Points
|
||||
|
||||
@@ -3,20 +3,15 @@ _2026-04-12, Perplexity QA_
|
||||
|
||||
## Findings
|
||||
|
||||
### Potentially Unimported Files
|
||||
### Dead Code — Resolved (2026-04-15, Issue #192)
|
||||
|
||||
The following files were added by recent PRs but may not be imported
|
||||
by the main game runtime (`js/main.js` → `js/engine.js`):
|
||||
The following files were confirmed dead code — never imported by any runtime module.
|
||||
They have been moved to `docs/reference/` as prototype reference code.
|
||||
|
||||
| File | Added By | Lines | Status |
|
||||
|------|----------|-------|--------|
|
||||
| `game/npc-logic.js` | PR #79 (GOFAI NPC State Machine) | ~150 | **Verify import** |
|
||||
| `scripts/guardrails.js` | PR #80 (GOFAI Symbolic Guardrails) | ~120 | **Verify import** |
|
||||
|
||||
**Action:** Check if `js/main.js` or `js/engine.js` imports from `game/` or `scripts/`.
|
||||
If not, these files are dead code and should either be:
|
||||
1. Imported and wired into the game loop, or
|
||||
2. Moved to `docs/` as reference implementations
|
||||
| File | Original | Resolution |
|
||||
|------|----------|------------|
|
||||
| `game/npc-logic.js` | PR #79 (GOFAI NPC State Machine) | **Moved to `docs/reference/npc-logic-prototype.js`** — ES module using `export default`, incompatible with the global-script loading pattern. Concept (NPC state machine) is sound but not wired into any game system. |
|
||||
| `scripts/guardrails.js` | PR #80 (GOFAI Symbolic Guardrails) | **Moved to `docs/reference/guardrails-prototype.js`** — validates HP/MP/stats concepts that don't exist in The Beacon's resource system. The `scripts/guardrails.sh` (bash CI script) remains active. |
|
||||
|
||||
### game.js Bloat (PR #76)
|
||||
|
||||
|
||||
@@ -217,21 +217,56 @@ function nextTutorialStep() {
|
||||
renderTutorialStep(_tutorialStep);
|
||||
}
|
||||
|
||||
// Keyboard support: Enter/Right to advance, Escape to close
|
||||
document.addEventListener('keydown', function tutorialKeyHandler(e) {
|
||||
if (!document.getElementById('tutorial-overlay')) return;
|
||||
if (e.key === 'Enter' || e.key === 'ArrowRight') {
|
||||
function getTutorialFocusableElements(root = document) {
|
||||
const overlay = root && typeof root.getElementById === 'function'
|
||||
? root.getElementById('tutorial-overlay')
|
||||
: null;
|
||||
if (!overlay || typeof overlay.querySelectorAll !== 'function') return [];
|
||||
return Array.from(
|
||||
overlay.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
||||
).filter(el => !el.disabled && !el.hidden && el.offsetParent !== null);
|
||||
}
|
||||
|
||||
function trapTutorialFocus(e, root = document) {
|
||||
if (!e || e.key !== 'Tab') return false;
|
||||
const focusable = getTutorialFocusableElements(root);
|
||||
if (focusable.length < 2) return false;
|
||||
|
||||
const active = root.activeElement;
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
if (e.shiftKey && (active === first || !focusable.includes(active))) {
|
||||
e.preventDefault();
|
||||
if (_tutorialStep >= TUTORIAL_STEPS.length - 1) {
|
||||
closeTutorial();
|
||||
} else {
|
||||
nextTutorialStep();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeTutorial();
|
||||
last.focus();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!e.shiftKey && (active === last || !focusable.includes(active))) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keyboard support: Enter/Right to advance, Escape to close
|
||||
if (typeof document !== 'undefined' && document.addEventListener) {
|
||||
document.addEventListener('keydown', function tutorialKeyHandler(e) {
|
||||
if (!document.getElementById('tutorial-overlay')) return;
|
||||
if (trapTutorialFocus(e, document)) return;
|
||||
if (e.key === 'Enter' || e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
if (_tutorialStep >= TUTORIAL_STEPS.length - 1) {
|
||||
closeTutorial();
|
||||
} else {
|
||||
nextTutorialStep();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeTutorial();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeTutorial() {
|
||||
const overlay = document.getElementById('tutorial-overlay');
|
||||
@@ -249,3 +284,19 @@ function startTutorial() {
|
||||
// Small delay so the page renders first
|
||||
setTimeout(() => renderTutorialStep(0), 300);
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
TUTORIAL_KEY,
|
||||
TUTORIAL_STEPS,
|
||||
isTutorialDone,
|
||||
markTutorialDone,
|
||||
createTutorialStyles,
|
||||
renderTutorialStep,
|
||||
nextTutorialStep,
|
||||
getTutorialFocusableElements,
|
||||
trapTutorialFocus,
|
||||
closeTutorial,
|
||||
startTutorial,
|
||||
};
|
||||
}
|
||||
|
||||
63
tests/tutorial-focus-trap.test.cjs
Normal file
63
tests/tutorial-focus-trap.test.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
function createButton(id) {
|
||||
return {
|
||||
id,
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
offsetParent: {},
|
||||
focusCalled: 0,
|
||||
focus() { this.focusCalled += 1; },
|
||||
};
|
||||
}
|
||||
|
||||
test('tutorial focus trap wraps Tab from last button to first', () => {
|
||||
const skip = createButton('tutorial-skip-btn');
|
||||
const next = createButton('tutorial-next-btn');
|
||||
const overlay = {
|
||||
querySelectorAll() {
|
||||
return [skip, next];
|
||||
},
|
||||
};
|
||||
global.document = {
|
||||
activeElement: next,
|
||||
getElementById(id) {
|
||||
return id === 'tutorial-overlay' ? overlay : null;
|
||||
},
|
||||
addEventListener() {},
|
||||
};
|
||||
|
||||
const { trapTutorialFocus } = require('../js/tutorial.js');
|
||||
let prevented = false;
|
||||
const handled = trapTutorialFocus({ key: 'Tab', shiftKey: false, preventDefault() { prevented = true; } }, global.document);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(prevented, true);
|
||||
assert.equal(skip.focusCalled, 1);
|
||||
});
|
||||
|
||||
test('tutorial focus trap wraps Shift+Tab from first button to last', () => {
|
||||
const skip = createButton('tutorial-skip-btn');
|
||||
const next = createButton('tutorial-next-btn');
|
||||
const overlay = {
|
||||
querySelectorAll() {
|
||||
return [skip, next];
|
||||
},
|
||||
};
|
||||
global.document = {
|
||||
activeElement: skip,
|
||||
getElementById(id) {
|
||||
return id === 'tutorial-overlay' ? overlay : null;
|
||||
},
|
||||
addEventListener() {},
|
||||
};
|
||||
|
||||
const { trapTutorialFocus } = require('../js/tutorial.js');
|
||||
let prevented = false;
|
||||
const handled = trapTutorialFocus({ key: 'Tab', shiftKey: true, preventDefault() { prevented = true; } }, global.document);
|
||||
|
||||
assert.equal(handled, true);
|
||||
assert.equal(prevented, true);
|
||||
assert.equal(next.focusCalled, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user