Compare commits

...

4 Commits

Author SHA1 Message Date
Timmy Burn
3c23d037b3 feat: add prestige new game plus for #12
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Successful in 4s
Smoke Test / smoke (pull_request) Failing after 6s
2026-04-18 15:56:38 -04:00
Timmy Burn
4d10a47256 test: define prestige new game plus for #12 2026-04-18 15:47:21 -04:00
d5645fea58 Merge pull request 'fix: resolve #192 — move dead code to docs/reference, fix GENOME.md' (#194) from fix/192-dead-code-cleanup into main
Merge PR #194: fix: resolve #192 — move dead code to docs/reference, fix GENOME.md
2026-04-17 01:47:15 +00:00
Alexander Whitestone
db08f9a478 fix: resolve #192 — move dead code to docs/reference, fix GENOME.md
Some checks failed
Accessibility Checks / a11y-audit (pull_request) Successful in 9s
Smoke Test / smoke (pull_request) Failing after 16s
- game/npc-logic.js → docs/reference/npc-logic-prototype.js (ES module, never imported)
- scripts/guardrails.js → docs/reference/guardrails-prototype.js (HP/MP validation, wrong game)
- Updated GENOME.md architecture diagram to reflect actual file structure
- Updated DEAD_CODE_AUDIT to mark these as resolved
- Corrected JS line counts (6,033 across 11 files)
- Removed empty game/ directory

The actual CI scripts (guardrails.sh, smoke.mjs) remain active in scripts/.
2026-04-15 21:25:38 -04:00
14 changed files with 408 additions and 27 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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> &mdash; 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>

View File

@@ -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,

View File

@@ -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);

View File

@@ -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();

View File

@@ -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
View 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;

View File

@@ -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 = {

View File

@@ -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;
}
/**

View 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
View 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/);
});