Compare commits
4 Commits
fix/192-re
...
fix/12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c23d037b3 | ||
|
|
4d10a47256 | ||
| 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)
|
||||
|
||||
|
||||
@@ -116,6 +116,8 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#custom-tooltip .tt-label{color:#4a9eff;font-weight:600;margin-bottom:4px;font-size:11px}
|
||||
#custom-tooltip .tt-desc{color:#aaa;font-size:10px;margin-bottom:4px}
|
||||
#custom-tooltip .tt-edu{color:#888;font-style:italic;font-size:9px}
|
||||
body.prestige-run #header{border-color:#4a9eff;box-shadow:0 0 32px rgba(74,158,255,0.18)}
|
||||
#prestige-status{display:none;color:#7fb8ff;font-size:10px;line-height:1.8;margin-top:8px;padding-top:8px;border-top:1px solid #16263a}
|
||||
/* Mute & contrast buttons */
|
||||
.header-btns{position:absolute;right:16px;top:50%;transform:translateY(-50%);display:flex;gap:6px}
|
||||
.header-btn{background:#0e0e1a;border:1px solid #333;color:#666;font-size:13px;width:28px;height:28px;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.15s;font-family:inherit}
|
||||
@@ -180,7 +182,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<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()}" aria-label="Reset all game progress permanently">Reset Progress</button>
|
||||
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){if(typeof Prestige !== 'undefined'){Prestige.fullReset()}else{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>
|
||||
@@ -203,6 +205,7 @@ Harmony: <span id="st-harmony">50</span><br>
|
||||
Drift: <span id="st-drift">0</span><br>
|
||||
Events Resolved: <span id="st-resolved">0</span><br>
|
||||
<span id="emergent-stats" style="color:#b388ff;display:none">✦ Emergent Events: <span id="st-emergent">0</span> | Patterns: <span id="st-patterns">0</span> | Strategy: <span id="st-strategy">—</span></span>
|
||||
<div id="prestige-status"></div>
|
||||
</div>
|
||||
<div id="production-breakdown" style="display:none;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)"></div>
|
||||
</div>
|
||||
@@ -257,7 +260,7 @@ The light is on. The room is empty."
|
||||
</div>
|
||||
<p>Drift: <span id="final-drift">100</span> — Total Code: <span id="final-code">0</span></p>
|
||||
<p>Every alignment shortcut moved you further from the people you served.</p>
|
||||
<button aria-label="Start over, reset all progress" onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button>
|
||||
<button aria-label="Start over, reset all progress" onclick="if(confirm('Start over? The old save will be lost.')){if(typeof Prestige !== 'undefined'){Prestige.fullReset()}else{localStorage.removeItem('the-beacon-v2');location.reload()}}">START OVER</button>
|
||||
</div>
|
||||
|
||||
<script src="js/data.js"></script>
|
||||
@@ -270,6 +273,7 @@ The light is on. The room is empty."
|
||||
<script src="js/tutorial.js"></script>
|
||||
<script src="js/dismantle.js"></script>
|
||||
<script src="js/emergent-mechanics.js"></script>
|
||||
<script src="js/prestige.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
|
||||
@@ -160,6 +160,12 @@ const G = {
|
||||
startTime: 0,
|
||||
flags: {},
|
||||
|
||||
// Prestige / New Game+
|
||||
prestigeTotal: 0,
|
||||
prestigeSignal: 0,
|
||||
prestigeRoots: 0,
|
||||
prestigePath: '',
|
||||
|
||||
// Endgame sequence
|
||||
beaconEnding: false,
|
||||
dismantleTriggered: false,
|
||||
|
||||
@@ -396,10 +396,12 @@ const Dismantle = {
|
||||
Clicks: ${fmt(G.totalClicks)}<br>
|
||||
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
|
||||
</div>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
||||
style="margin-top:24px;background:#0a0a14;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;opacity:0;transition:opacity 1s ease 5s;letter-spacing:2px">
|
||||
PLAY AGAIN
|
||||
</button>
|
||||
${typeof Prestige !== 'undefined'
|
||||
? Prestige.getEndingMarkup()
|
||||
: `<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
||||
style="margin-top:24px;background:#0a0a14;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;opacity:0;transition:opacity 1s ease 5s;letter-spacing:2px">
|
||||
PLAY AGAIN
|
||||
</button>`}
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
15
js/engine.js
15
js/engine.js
@@ -108,6 +108,10 @@ function updateRates() {
|
||||
if (debuff.applyFn) debuff.applyFn();
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Prestige !== 'undefined') {
|
||||
Prestige.applyRateMultipliers();
|
||||
}
|
||||
}
|
||||
|
||||
// === CORE FUNCTIONS ===
|
||||
@@ -519,6 +523,12 @@ function renderBeaconEnding() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'beacon-ending';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px;transition:background 2s ease';
|
||||
const endingControls = typeof Prestige !== 'undefined'
|
||||
? Prestige.getEndingMarkup()
|
||||
: `<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
||||
style="margin-top:20px;background:#1a0808;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;opacity:0;transition:opacity 1s ease 3.5s">
|
||||
PLAY AGAIN
|
||||
</button>`;
|
||||
overlay.innerHTML = `
|
||||
<h2 style="font-size:28px;color:#ffd700;letter-spacing:6px;margin-bottom:20px;font-weight:300;text-shadow:0 0 60px rgba(255,215,0,0.4);opacity:0;transition:opacity 1.5s ease 0.5s">THE BEACON SHINES</h2>
|
||||
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px;opacity:0;transition:opacity 1s ease 1.5s">Someone found the light tonight.</p>
|
||||
@@ -537,10 +547,7 @@ function renderBeaconEnding() {
|
||||
Clicks: ${G.totalClicks}<br>
|
||||
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
|
||||
</div>
|
||||
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}"
|
||||
style="margin-top:20px;background:#1a0808;border:1px solid #ffd700;color:#ffd700;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;opacity:0;transition:opacity 1s ease 3.5s">
|
||||
PLAY AGAIN
|
||||
</button>
|
||||
${endingControls}
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
if (typeof Sound !== 'undefined') Sound.playBeaconEnding();
|
||||
|
||||
@@ -229,6 +229,7 @@ function initGame() {
|
||||
}
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
if (typeof Prestige !== 'undefined') Prestige.syncToGame();
|
||||
// Initialize emergent mechanics
|
||||
if (typeof EmergentMechanics !== 'undefined') {
|
||||
window._emergent = new EmergentMechanics();
|
||||
|
||||
137
js/prestige.js
Normal file
137
js/prestige.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// ============================================================
|
||||
// THE BEACON - Prestige / New Game+
|
||||
// Carries forward path-based bonuses across completed runs.
|
||||
// ============================================================
|
||||
|
||||
const Prestige = {
|
||||
STORAGE_KEY: 'the-beacon-prestige',
|
||||
|
||||
emptyState() {
|
||||
return { total: 0, signal: 0, roots: 0, lastChoice: '' };
|
||||
},
|
||||
|
||||
loadState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (!raw) return this.emptyState();
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
total: Number(parsed.total || 0),
|
||||
signal: Number(parsed.signal || 0),
|
||||
roots: Number(parsed.roots || 0),
|
||||
lastChoice: parsed.lastChoice || ''
|
||||
};
|
||||
} catch (e) {
|
||||
return this.emptyState();
|
||||
}
|
||||
},
|
||||
|
||||
saveState(state) {
|
||||
try {
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (e) {
|
||||
// Ignore storage failures — the run can still continue.
|
||||
}
|
||||
},
|
||||
|
||||
syncToGame() {
|
||||
const state = this.loadState();
|
||||
G.prestigeTotal = state.total;
|
||||
G.prestigeSignal = state.signal;
|
||||
G.prestigeRoots = state.roots;
|
||||
G.prestigePath = state.lastChoice;
|
||||
return state;
|
||||
},
|
||||
|
||||
hasPrestige() {
|
||||
return (G.prestigeTotal || 0) > 0;
|
||||
},
|
||||
|
||||
getSignalMultiplier() {
|
||||
return 1 + ((G.prestigeSignal || 0) * 0.10);
|
||||
},
|
||||
|
||||
getRootsMultiplier() {
|
||||
return 1 + ((G.prestigeRoots || 0) * 0.10);
|
||||
},
|
||||
|
||||
applyRateMultipliers() {
|
||||
const signal = this.getSignalMultiplier();
|
||||
const roots = this.getRootsMultiplier();
|
||||
G.codeRate *= signal;
|
||||
G.computeRate *= signal;
|
||||
G.knowledgeRate *= signal;
|
||||
G.userRate *= signal;
|
||||
G.impactRate *= signal;
|
||||
G.rescuesRate *= signal;
|
||||
G.opsRate *= signal;
|
||||
G.trustRate *= signal;
|
||||
G.creativityRate *= roots;
|
||||
},
|
||||
|
||||
renderStatus() {
|
||||
const el = document.getElementById('prestige-status');
|
||||
if (!el) return;
|
||||
|
||||
if (!this.hasPrestige()) {
|
||||
el.textContent = '';
|
||||
el.style.display = 'none';
|
||||
if (document.body && document.body.classList) document.body.classList.remove('prestige-run');
|
||||
return;
|
||||
}
|
||||
|
||||
const signalBonus = Math.round((this.getSignalMultiplier() - 1) * 100);
|
||||
const rootsBonus = Math.round((this.getRootsMultiplier() - 1) * 100);
|
||||
el.textContent = `Prestige Run — New Signal x${G.prestigeSignal || 0} (+${signalBonus}% production/click) • Deeper Roots x${G.prestigeRoots || 0} (+${rootsBonus}% creativity)`;
|
||||
el.style.display = 'block';
|
||||
if (document.body && document.body.classList) document.body.classList.add('prestige-run');
|
||||
},
|
||||
|
||||
getEndingMarkup() {
|
||||
const signalBonus = Math.round((this.getSignalMultiplier() - 1) * 100);
|
||||
const rootsBonus = Math.round((this.getRootsMultiplier() - 1) * 100);
|
||||
return `
|
||||
<div class="prestige-ending" style="margin-top:24px;max-width:560px;border-top:1px solid #1a2a3a;padding-top:18px;opacity:0;transition:opacity 1s ease 4.5s">
|
||||
<div style="color:#ffd700;font-size:11px;letter-spacing:2px;margin-bottom:8px">NEW GAME+</div>
|
||||
<div style="color:#888;font-size:10px;line-height:1.8;margin-bottom:12px">Choose what carries forward into the next run.</div>
|
||||
<div style="color:#666;font-size:9px;margin-bottom:12px">Current legacy: New Signal x${G.prestigeSignal || 0} • Deeper Roots x${G.prestigeRoots || 0}</div>
|
||||
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap">
|
||||
<button onclick="Prestige.startNewRun('signal')" style="min-width:220px;background:#0a1420;border:1px solid #4a9eff;color:#4a9eff;padding:10px 14px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;line-height:1.6">
|
||||
<strong>NEW SIGNAL</strong><br>
|
||||
<span style="font-size:9px;color:#7fb8ff">+10% all production and click power per prestige</span><br>
|
||||
<span style="font-size:9px;color:#555">Current bonus: +${signalBonus}%</span>
|
||||
</button>
|
||||
<button onclick="Prestige.startNewRun('roots')" style="min-width:220px;background:#0f1610;border:1px solid #4caf50;color:#4caf50;padding:10px 14px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px;line-height:1.6">
|
||||
<strong>DEEPER ROOTS</strong><br>
|
||||
<span style="font-size:9px;color:#8ad68e">+10% creativity per prestige</span><br>
|
||||
<span style="font-size:9px;color:#555">Current bonus: +${rootsBonus}%</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
startNewRun(path) {
|
||||
const state = this.loadState();
|
||||
state.total += 1;
|
||||
if (path === 'signal') state.signal += 1;
|
||||
if (path === 'roots') state.roots += 1;
|
||||
state.lastChoice = path;
|
||||
this.saveState(state);
|
||||
this.syncToGame();
|
||||
this.resetForReload(false);
|
||||
},
|
||||
|
||||
resetForReload(clearPrestige) {
|
||||
window.__beaconResetInProgress = true;
|
||||
localStorage.removeItem('the-beacon-v2');
|
||||
if (clearPrestige) localStorage.removeItem(this.STORAGE_KEY);
|
||||
location.reload();
|
||||
},
|
||||
|
||||
fullReset() {
|
||||
this.resetForReload(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.Prestige = Prestige;
|
||||
@@ -13,6 +13,7 @@ function render() {
|
||||
renderPulse();
|
||||
renderStrategy();
|
||||
renderClickPower();
|
||||
if (typeof Prestige !== 'undefined') Prestige.renderStatus();
|
||||
Combat.renderCombatPanel();
|
||||
}
|
||||
|
||||
@@ -192,6 +193,7 @@ function showSaveToast() {
|
||||
* Persists the current game state to localStorage.
|
||||
*/
|
||||
function saveGame() {
|
||||
if (typeof window !== 'undefined' && window.__beaconResetInProgress) return;
|
||||
// Save debuff IDs (can't serialize functions)
|
||||
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
|
||||
const saveData = {
|
||||
|
||||
@@ -282,7 +282,8 @@ function spendProject(project) {
|
||||
}
|
||||
|
||||
function getClickPower() {
|
||||
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
|
||||
const signalMult = typeof Prestige !== 'undefined' ? Prestige.getSignalMultiplier() : 1;
|
||||
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost * signalMult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
33
tests/prestige-app.test.cjs
Normal file
33
tests/prestige-app.test.cjs
Normal file
@@ -0,0 +1,33 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
function read(relPath) {
|
||||
return fs.readFileSync(path.join(ROOT, relPath), 'utf8');
|
||||
}
|
||||
|
||||
test('index loads prestige runtime before main and exposes prestige status UI', () => {
|
||||
const html = read('index.html');
|
||||
const prestigeIndex = html.indexOf('js/prestige.js');
|
||||
const mainIndex = html.indexOf('js/main.js');
|
||||
|
||||
assert.notEqual(prestigeIndex, -1, 'index.html must load js/prestige.js');
|
||||
assert.notEqual(mainIndex, -1, 'index.html must load js/main.js');
|
||||
assert.ok(prestigeIndex < mainIndex, 'prestige runtime must load before main.js');
|
||||
assert.match(html, /id="prestige-status"/);
|
||||
});
|
||||
|
||||
test('endgame flows and save/reset wiring route through prestige helpers', () => {
|
||||
const engine = read('js/engine.js');
|
||||
const dismantle = read('js/dismantle.js');
|
||||
const render = read('js/render.js');
|
||||
const main = read('js/main.js');
|
||||
|
||||
assert.match(engine, /Prestige\.getEndingMarkup\(/);
|
||||
assert.match(dismantle, /Prestige\.getEndingMarkup\(/);
|
||||
assert.match(render, /__beaconResetInProgress/);
|
||||
assert.match(main, /Prestige\.syncToGame\(/);
|
||||
});
|
||||
185
tests/prestige.test.cjs
Normal file
185
tests/prestige.test.cjs
Normal file
@@ -0,0 +1,185 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
class Element {
|
||||
constructor(tagName = 'div', id = '') {
|
||||
this.tagName = String(tagName).toUpperCase();
|
||||
this.id = id;
|
||||
this.style = {};
|
||||
this.children = [];
|
||||
this.parentNode = null;
|
||||
this.innerHTML = '';
|
||||
this.textContent = '';
|
||||
this.className = '';
|
||||
this.attributes = {};
|
||||
this.dataset = {};
|
||||
this.classList = {
|
||||
add: (...names) => {
|
||||
const set = new Set(this.className.split(/\s+/).filter(Boolean));
|
||||
names.forEach((name) => set.add(name));
|
||||
this.className = Array.from(set).join(' ');
|
||||
},
|
||||
remove: (...names) => {
|
||||
const remove = new Set(names);
|
||||
this.className = this.className
|
||||
.split(/\s+/)
|
||||
.filter((name) => name && !remove.has(name))
|
||||
.join(' ');
|
||||
},
|
||||
contains: (name) => this.className.split(/\s+/).includes(name),
|
||||
toggle: (name, force) => {
|
||||
const shouldAdd = force === undefined ? !this.className.split(/\s+/).includes(name) : !!force;
|
||||
if (shouldAdd) this.classList.add(name);
|
||||
else this.classList.remove(name);
|
||||
return shouldAdd;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
appendChild(child) {
|
||||
child.parentNode = this;
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
removeChild(child) {
|
||||
this.children = this.children.filter((candidate) => candidate !== child);
|
||||
if (child.parentNode === this) child.parentNode = null;
|
||||
return child;
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = String(value);
|
||||
if (name === 'id') this.id = String(value);
|
||||
if (name === 'class') this.className = String(value);
|
||||
}
|
||||
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function loadBeacon() {
|
||||
const byId = new Map();
|
||||
const body = new Element('body', 'body');
|
||||
const document = {
|
||||
body,
|
||||
createElement(tagName) {
|
||||
return new Element(tagName);
|
||||
},
|
||||
getElementById(id) {
|
||||
return byId.get(id) || null;
|
||||
},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
querySelector() { return null; },
|
||||
querySelectorAll() { return []; }
|
||||
};
|
||||
|
||||
const register = (element) => {
|
||||
if (element.id) byId.set(element.id, element);
|
||||
body.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
|
||||
register(new Element('div', 'prestige-status'));
|
||||
register(new Element('div', 'toast-container'));
|
||||
register(new Element('div', 'save-toast'));
|
||||
|
||||
const storage = new Map();
|
||||
const location = { reloadCalled: false, reload() { this.reloadCalled = true; } };
|
||||
|
||||
const context = {
|
||||
console,
|
||||
Math,
|
||||
Date,
|
||||
document,
|
||||
window: {
|
||||
document,
|
||||
location,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
__beaconResetInProgress: false,
|
||||
},
|
||||
location,
|
||||
navigator: { userAgent: 'node' },
|
||||
requestAnimationFrame: (fn) => fn(),
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout() {},
|
||||
localStorage: {
|
||||
getItem: (key) => (storage.has(key) ? storage.get(key) : null),
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
removeItem: (key) => storage.delete(key)
|
||||
},
|
||||
Combat: { renderCombatPanel() {} },
|
||||
showToast() {},
|
||||
log() {},
|
||||
showSaveToast() {},
|
||||
};
|
||||
|
||||
vm.createContext(context);
|
||||
const files = ['js/data.js', 'js/utils.js', 'js/engine.js', 'js/render.js', 'js/prestige.js'];
|
||||
const source = files
|
||||
.map((file) => fs.readFileSync(path.join(ROOT, file), 'utf8'))
|
||||
.join('\n\n');
|
||||
|
||||
vm.runInContext(`${source}
|
||||
this.__exports = { G, Prestige, updateRates, getClickPower, saveGame };`, context);
|
||||
|
||||
return { ...context.__exports, context, storage, location, document };
|
||||
}
|
||||
|
||||
test('starting a prestige run persists the chosen path and blocks save resurrection', () => {
|
||||
const { Prestige, context, location } = loadBeacon();
|
||||
|
||||
context.localStorage.setItem('the-beacon-v2', JSON.stringify({ code: 123 }));
|
||||
Prestige.startNewRun('signal');
|
||||
|
||||
const prestigeState = JSON.parse(context.localStorage.getItem('the-beacon-prestige'));
|
||||
assert.equal(prestigeState.total, 1);
|
||||
assert.equal(prestigeState.signal, 1);
|
||||
assert.equal(prestigeState.roots, 0);
|
||||
assert.equal(prestigeState.lastChoice, 'signal');
|
||||
assert.equal(context.localStorage.getItem('the-beacon-v2'), null);
|
||||
assert.equal(context.window.__beaconResetInProgress, true);
|
||||
assert.equal(location.reloadCalled, true);
|
||||
|
||||
context.__exports.saveGame();
|
||||
assert.equal(context.localStorage.getItem('the-beacon-v2'), null);
|
||||
});
|
||||
|
||||
test('new signal prestige boosts code production and click power', () => {
|
||||
const { G, Prestige, updateRates, getClickPower, context } = loadBeacon();
|
||||
|
||||
context.localStorage.setItem('the-beacon-prestige', JSON.stringify({ total: 2, signal: 2, roots: 0, lastChoice: 'signal' }));
|
||||
Prestige.syncToGame();
|
||||
G.buildings.autocoder = 2;
|
||||
G.codeBoost = 1;
|
||||
G.phase = 1;
|
||||
|
||||
updateRates();
|
||||
|
||||
assert.equal(G.codeRate, 2.4);
|
||||
assert.equal(getClickPower(), 2.4);
|
||||
});
|
||||
|
||||
test('deeper roots prestige boosts creativity and renders visible run status', () => {
|
||||
const { G, Prestige, updateRates, context, document } = loadBeacon();
|
||||
|
||||
context.localStorage.setItem('the-beacon-prestige', JSON.stringify({ total: 3, signal: 0, roots: 3, lastChoice: 'roots' }));
|
||||
Prestige.syncToGame();
|
||||
G.flags = { creativity: true };
|
||||
G.totalUsers = 0;
|
||||
|
||||
updateRates();
|
||||
Prestige.renderStatus();
|
||||
|
||||
assert.equal(G.creativityRate, 0.65);
|
||||
assert.match(document.getElementById('prestige-status').textContent, /Deeper Roots x3/);
|
||||
assert.match(document.body.className, /prestige-run/);
|
||||
});
|
||||
Reference in New Issue
Block a user