Compare commits
1 Commits
fix/168-ch
...
burn/12-17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49c9c30807 |
13
index.html
13
index.html
@@ -28,6 +28,15 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#header{text-align:center;padding:16px 20px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,#0a0a18,var(--bg))}
|
||||
#header h1{font-size:22px;font-weight:300;letter-spacing:6px;color:var(--accent);text-shadow:0 0 40px var(--glow)}
|
||||
#header .sub{color:var(--dim);font-size:10px;margin-top:2px;letter-spacing:2px}
|
||||
#prestige-badge{display:none;margin-top:8px;font-size:10px;letter-spacing:2px;color:var(--gold)}
|
||||
body.prestige-run #header{background:linear-gradient(180deg,#151118,var(--bg))}
|
||||
body.prestige-run #header h1{color:var(--gold);text-shadow:0 0 40px rgba(255,215,0,0.22)}
|
||||
.prestige-choice{margin-top:8px;padding:10px;border:1px solid #5a4a1a;border-radius:6px;background:rgba(255,215,0,0.08)}
|
||||
.prestige-choice-title{font-size:11px;font-weight:700;letter-spacing:2px;color:var(--gold);margin-bottom:6px}
|
||||
.prestige-choice-copy{font-size:10px;color:#bbb;line-height:1.7;margin-bottom:8px}
|
||||
.prestige-choice-btns{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.prestige-choice-btn{flex:1;min-width:140px;background:#0f1018;border:1px solid #5a4a1a;color:var(--gold);padding:8px 10px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:10px;line-height:1.6;text-align:left}
|
||||
.prestige-choice-btn:hover{border-color:#ffd700;background:#17131a}
|
||||
#phase-bar{text-align:center;padding:10px;margin:12px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px}
|
||||
#phase-bar .phase-name{font-size:14px;font-weight:700;color:var(--gold);letter-spacing:2px}
|
||||
#phase-bar .phase-desc{font-size:10px;color:var(--dim);margin-top:4px;font-style:italic}
|
||||
@@ -134,6 +143,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
</div>
|
||||
<h1>THE BEACON</h1>
|
||||
<div class="sub">A Sovereign AI Idle Game</div>
|
||||
<div id="prestige-badge" aria-live="polite"></div>
|
||||
</div>
|
||||
<div id="phase-bar">
|
||||
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
|
||||
@@ -179,7 +189,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="resetBeaconProgress(true)" aria-label="Reset all game progress permanently">Reset Progress</button>
|
||||
<h2>BUILDINGS</h2>
|
||||
<div id="buildings"></div>
|
||||
</div>
|
||||
@@ -267,6 +277,7 @@ The light is on. The room is empty."
|
||||
<script src="js/render.js"></script>
|
||||
<script src="js/tutorial.js"></script>
|
||||
<script src="js/dismantle.js"></script>
|
||||
<script src="js/prestige.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
|
||||
11
js/data.js
11
js/data.js
@@ -168,7 +168,14 @@ const G = {
|
||||
dismantleResourceIndex: 0,
|
||||
dismantleResourceTimer: 0,
|
||||
dismantleDeferUntilAt: 0,
|
||||
dismantleComplete: false
|
||||
dismantleComplete: false,
|
||||
|
||||
// Prestige / New Game+
|
||||
prestigeTotal: 0,
|
||||
newSignalPrestige: 0,
|
||||
deeperRootsPrestige: 0,
|
||||
lastPrestigeChoice: '',
|
||||
prestigeChoicePending: false
|
||||
};
|
||||
|
||||
// === PHASE DEFINITIONS ===
|
||||
@@ -653,7 +660,7 @@ const PDEFS = [
|
||||
desc: 'Someone found the light tonight. That is enough.',
|
||||
cost: { impact: 100000000 },
|
||||
trigger: () => G.totalImpact >= 50000000,
|
||||
effect: () => { G.milestoneFlag = Math.max(G.milestoneFlag, 999); log('One billion impact. Someone found the light tonight. That is enough.', true); },
|
||||
effect: () => { G.milestoneFlag = Math.max(G.milestoneFlag, 999); G.prestigeChoicePending = true; log('One billion impact. Someone found the light tonight. That is enough.', true); log('A Prestige choice is available: New Signal or Deeper Roots.', true); },
|
||||
milestone: true
|
||||
},
|
||||
|
||||
|
||||
@@ -396,7 +396,7 @@ 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()}"
|
||||
<button onclick="resetBeaconProgress(true)"
|
||||
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>
|
||||
|
||||
32
js/engine.js
32
js/engine.js
@@ -108,6 +108,19 @@ function updateRates() {
|
||||
if (debuff.applyFn) debuff.applyFn();
|
||||
}
|
||||
}
|
||||
|
||||
const prestigeStatMult = (typeof Prestige !== 'undefined' && Prestige.getStatMultiplier) ? Prestige.getStatMultiplier() : 1;
|
||||
const prestigeCreativityMult = (typeof Prestige !== 'undefined' && Prestige.getCreativityMultiplier) ? Prestige.getCreativityMultiplier() : 1;
|
||||
G.codeRate *= prestigeStatMult;
|
||||
G.computeRate *= prestigeStatMult;
|
||||
G.knowledgeRate *= prestigeStatMult;
|
||||
G.userRate *= prestigeStatMult;
|
||||
G.impactRate *= prestigeStatMult;
|
||||
G.rescuesRate *= prestigeStatMult;
|
||||
G.opsRate *= prestigeStatMult;
|
||||
G.trustRate *= prestigeStatMult;
|
||||
G.harmonyRate *= prestigeStatMult;
|
||||
G.creativityRate *= prestigeCreativityMult;
|
||||
}
|
||||
|
||||
// === CORE FUNCTIONS ===
|
||||
@@ -418,6 +431,8 @@ function buyProject(id) {
|
||||
spendProject(pDef);
|
||||
pDef.effect();
|
||||
|
||||
if (window.__beaconResetInProgress) return;
|
||||
|
||||
if (!pDef.repeatable) {
|
||||
if (!G.completedProjects) G.completedProjects = [];
|
||||
G.completedProjects.push(pDef.id);
|
||||
@@ -505,7 +520,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()}"
|
||||
<button onclick="resetBeaconProgress(true)"
|
||||
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>
|
||||
@@ -1171,6 +1186,18 @@ function renderProjects() {
|
||||
}
|
||||
}
|
||||
|
||||
if (G.prestigeChoicePending) {
|
||||
const nextSignal = ((G.newSignalPrestige || 0) + 1) * 10;
|
||||
const nextRoots = ((G.deeperRootsPrestige || 0) + 1) * 10;
|
||||
html += `<div class="prestige-choice">`;
|
||||
html += `<div class="prestige-choice-title">NEW GAME+ UNLOCKED</div>`;
|
||||
html += `<div class="prestige-choice-copy">End the current run and begin again with a permanent inheritance. Choose one path.</div>`;
|
||||
html += `<div class="prestige-choice-btns">`;
|
||||
html += `<button class="prestige-choice-btn" onclick="Prestige.activate('new_signal')" aria-label="Start a New Signal prestige run"><strong>New Signal</strong><br>+${nextSignal}% all core production and click power</button>`;
|
||||
html += `<button class="prestige-choice-btn" onclick="Prestige.activate('deeper_roots')" aria-label="Start a Deeper Roots prestige run"><strong>Deeper Roots</strong><br>+${nextRoots}% creativity generation</button>`;
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
// Show available projects
|
||||
if (G.activeProjects) {
|
||||
for (const id of G.activeProjects) {
|
||||
@@ -1178,7 +1205,8 @@ function renderProjects() {
|
||||
if (!pDef) continue;
|
||||
|
||||
const afford = canAffordProject(pDef);
|
||||
const costStr = Object.entries(pDef.cost).map(([r, a]) => `${fmt(a)} ${r}`).join(', ');
|
||||
const costEntries = Object.entries(pDef.cost || {});
|
||||
const costStr = costEntries.length ? costEntries.map(([r, a]) => `${fmt(a)} ${r}`).join(', ') : 'FREE';
|
||||
|
||||
html += `<button class="project-btn ${afford ? 'can-buy' : ''}" onclick="buyProject('${pDef.id}')" data-edu="${pDef.edu || ''}" data-tooltip-label="${pDef.name}" aria-label="Research ${pDef.name}, cost ${costStr}">`;
|
||||
html += `<span class="p-name">* ${pDef.name}</span>`;
|
||||
|
||||
@@ -10,6 +10,8 @@ function initGame() {
|
||||
G.dismantleActive = false;
|
||||
G.dismantleStage = 0;
|
||||
G.dismantleComplete = false;
|
||||
G.prestigeChoicePending = false;
|
||||
if (typeof Prestige !== 'undefined' && Prestige.restorePersistent) Prestige.restorePersistent();
|
||||
updateRates();
|
||||
render();
|
||||
renderPhase();
|
||||
|
||||
106
js/prestige.js
Normal file
106
js/prestige.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const PRESTIGE_STORAGE_KEY = 'the-beacon-prestige';
|
||||
|
||||
const Prestige = {
|
||||
restorePersistent() {
|
||||
let data = null;
|
||||
try {
|
||||
const raw = localStorage.getItem(PRESTIGE_STORAGE_KEY);
|
||||
data = raw ? JSON.parse(raw) : null;
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
G.prestigeTotal = Math.max(0, Number(data?.total || G.prestigeTotal || 0));
|
||||
G.newSignalPrestige = Math.max(0, Number(data?.newSignal || G.newSignalPrestige || 0));
|
||||
G.deeperRootsPrestige = Math.max(0, Number(data?.deeperRoots || G.deeperRootsPrestige || 0));
|
||||
G.lastPrestigeChoice = String(data?.lastChoice || G.lastPrestigeChoice || '');
|
||||
return this.snapshot();
|
||||
},
|
||||
|
||||
snapshot() {
|
||||
return {
|
||||
total: G.prestigeTotal || 0,
|
||||
newSignal: G.newSignalPrestige || 0,
|
||||
deeperRoots: G.deeperRootsPrestige || 0,
|
||||
lastChoice: G.lastPrestigeChoice || ''
|
||||
};
|
||||
},
|
||||
|
||||
syncPersistentFromGame() {
|
||||
try {
|
||||
localStorage.setItem(PRESTIGE_STORAGE_KEY, JSON.stringify(this.snapshot()));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
|
||||
clearPersistent() {
|
||||
try {
|
||||
localStorage.removeItem(PRESTIGE_STORAGE_KEY);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
|
||||
getStatMultiplier() {
|
||||
return 1 + ((G.newSignalPrestige || 0) * 0.10);
|
||||
},
|
||||
|
||||
getCreativityMultiplier() {
|
||||
return 1 + ((G.deeperRootsPrestige || 0) * 0.10);
|
||||
},
|
||||
|
||||
activate(choice) {
|
||||
const next = this.snapshot();
|
||||
next.total += 1;
|
||||
if (choice === 'new_signal') next.newSignal += 1;
|
||||
if (choice === 'deeper_roots') next.deeperRoots += 1;
|
||||
next.lastChoice = choice;
|
||||
|
||||
G.prestigeTotal = next.total;
|
||||
G.newSignalPrestige = next.newSignal;
|
||||
G.deeperRootsPrestige = next.deeperRoots;
|
||||
G.lastPrestigeChoice = next.lastChoice;
|
||||
G.prestigeChoicePending = false;
|
||||
this.syncPersistentFromGame();
|
||||
|
||||
window.__beaconResetInProgress = true;
|
||||
try {
|
||||
localStorage.removeItem('the-beacon-v2');
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
if (typeof markTutorialDone === 'function') markTutorialDone();
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
location.reload();
|
||||
},
|
||||
|
||||
renderStatus() {
|
||||
const badge = document.getElementById('prestige-badge');
|
||||
if (!badge) return;
|
||||
const total = G.prestigeTotal || 0;
|
||||
if (total <= 0) {
|
||||
badge.style.display = 'none';
|
||||
if (document.body && document.body.classList) document.body.classList.remove('prestige-run');
|
||||
return;
|
||||
}
|
||||
const choiceLabel = G.lastPrestigeChoice === 'deeper_roots' ? 'DEEPER ROOTS' : 'NEW SIGNAL';
|
||||
badge.textContent = `PRESTIGE ${total} • ${choiceLabel}`;
|
||||
badge.style.display = 'block';
|
||||
if (document.body && document.body.classList) document.body.classList.add('prestige-run');
|
||||
}
|
||||
};
|
||||
|
||||
function resetBeaconProgress(requireConfirm) {
|
||||
if (requireConfirm && !confirm('Reset all progress?')) return;
|
||||
window.__beaconResetInProgress = true;
|
||||
Prestige.clearPersistent();
|
||||
try {
|
||||
localStorage.removeItem('the-beacon-v2');
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
16
js/render.js
16
js/render.js
@@ -1,4 +1,5 @@
|
||||
function render() {
|
||||
if (typeof Prestige !== 'undefined' && Prestige.renderStatus) Prestige.renderStatus();
|
||||
renderResources();
|
||||
renderPhase();
|
||||
renderBuildings();
|
||||
@@ -192,6 +193,7 @@ function showSaveToast() {
|
||||
* Persists the current game state to localStorage.
|
||||
*/
|
||||
function saveGame() {
|
||||
if (window.__beaconResetInProgress) return;
|
||||
// Save debuff IDs (can't serialize functions)
|
||||
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
|
||||
const saveData = {
|
||||
@@ -216,6 +218,11 @@ function saveGame() {
|
||||
drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false,
|
||||
lastEventAt: G.lastEventAt || 0,
|
||||
activeDebuffIds: debuffIds,
|
||||
prestigeTotal: G.prestigeTotal || 0,
|
||||
newSignalPrestige: G.newSignalPrestige || 0,
|
||||
deeperRootsPrestige: G.deeperRootsPrestige || 0,
|
||||
lastPrestigeChoice: G.lastPrestigeChoice || '',
|
||||
prestigeChoicePending: G.prestigeChoicePending || false,
|
||||
totalEventsResolved: G.totalEventsResolved || 0,
|
||||
buyAmount: G.buyAmount || 1,
|
||||
playTime: G.playTime || 0,
|
||||
@@ -267,9 +274,12 @@ function loadGame() {
|
||||
'sprintActive', 'sprintTimer', 'sprintCooldown',
|
||||
'swarmFlag', 'swarmRate', 'strategicFlag', 'projectsCollapsed',
|
||||
'dismantleTriggered', 'dismantleActive', 'dismantleStage',
|
||||
'dismantleResourceIndex', 'dismantleResourceTimer', 'dismantleDeferUntilAt', 'dismantleComplete'
|
||||
'dismantleResourceIndex', 'dismantleResourceTimer', 'dismantleDeferUntilAt', 'dismantleComplete',
|
||||
'prestigeTotal', 'newSignalPrestige', 'deeperRootsPrestige', 'lastPrestigeChoice', 'prestigeChoicePending'
|
||||
];
|
||||
|
||||
if (typeof Prestige !== 'undefined' && Prestige.restorePersistent) Prestige.restorePersistent();
|
||||
|
||||
G.isLoading = true;
|
||||
|
||||
whitelist.forEach(key => {
|
||||
@@ -316,6 +326,10 @@ function loadGame() {
|
||||
|
||||
updateRates();
|
||||
G.isLoading = false;
|
||||
if (typeof Prestige !== 'undefined') {
|
||||
if (Prestige.syncPersistentFromGame) Prestige.syncPersistentFromGame();
|
||||
if (Prestige.renderStatus) Prestige.renderStatus();
|
||||
}
|
||||
|
||||
// Offline progress
|
||||
if (data.savedAt) {
|
||||
|
||||
@@ -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 prestigeMult = (typeof Prestige !== 'undefined' && Prestige.getStatMultiplier) ? Prestige.getStatMultiplier() : 1;
|
||||
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost * prestigeMult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
254
tests/prestige.test.cjs
Normal file
254
tests/prestige.test.cjs
Normal file
@@ -0,0 +1,254 @@
|
||||
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.previousElementSibling = null;
|
||||
this.innerHTML = '';
|
||||
this.textContent = '';
|
||||
this.className = '';
|
||||
this.dataset = {};
|
||||
this.attributes = {};
|
||||
this._queryMap = new Map();
|
||||
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(' ');
|
||||
},
|
||||
toggle: (name, force) => {
|
||||
const set = new Set(this.className.split(/\s+/).filter(Boolean));
|
||||
const shouldHave = force === undefined ? !set.has(name) : !!force;
|
||||
if (shouldHave) set.add(name);
|
||||
else set.delete(name);
|
||||
this.className = Array.from(set).join(' ');
|
||||
return shouldHave;
|
||||
},
|
||||
contains: (name) => this.className.split(/\s+/).includes(name),
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.parentNode) this.parentNode.removeChild(this);
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = value;
|
||||
if (name === 'id') this.id = value;
|
||||
if (name === 'class') this.className = value;
|
||||
}
|
||||
|
||||
querySelectorAll(selector) {
|
||||
return this._queryMap.get(selector) || [];
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
return this.querySelectorAll(selector)[0] || null;
|
||||
}
|
||||
|
||||
closest(selector) {
|
||||
if (selector === '.res' && this.className.split(/\s+/).includes('res')) return this;
|
||||
return this.parentNode && typeof this.parentNode.closest === 'function'
|
||||
? this.parentNode.closest(selector)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildDom() {
|
||||
const byId = new Map();
|
||||
const body = new Element('body', 'body');
|
||||
const head = new Element('head', 'head');
|
||||
|
||||
const document = {
|
||||
body,
|
||||
head,
|
||||
createElement(tagName) {
|
||||
return new Element(tagName);
|
||||
},
|
||||
getElementById(id) {
|
||||
return byId.get(id) || null;
|
||||
},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
querySelector() { return null; },
|
||||
querySelectorAll() { return []; }
|
||||
};
|
||||
|
||||
function register(el) {
|
||||
if (el.id) byId.set(el.id, el);
|
||||
return el;
|
||||
}
|
||||
|
||||
const projects = register(new Element('div', 'projects'));
|
||||
const projectsHeader = new Element('h2');
|
||||
projects.previousElementSibling = projectsHeader;
|
||||
const buildings = register(new Element('div', 'buildings'));
|
||||
const alignmentUi = register(new Element('div', 'alignment-ui'));
|
||||
const phaseName = register(new Element('div', 'phase-name'));
|
||||
const phaseDesc = register(new Element('div', 'phase-desc'));
|
||||
const stats = ['st-code','st-compute','st-knowledge','st-users','st-impact','st-rescues','st-clicks','st-phase','st-buildings','st-projects','st-harmony','st-drift','st-resolved','st-time','production-breakdown'];
|
||||
for (const id of stats) register(new Element('div', id));
|
||||
const prestigeBadge = register(new Element('div', 'prestige-badge'));
|
||||
body.appendChild(prestigeBadge);
|
||||
body.appendChild(projectsHeader);
|
||||
body.appendChild(projects);
|
||||
body.appendChild(buildings);
|
||||
body.appendChild(alignmentUi);
|
||||
body.appendChild(phaseName);
|
||||
body.appendChild(phaseDesc);
|
||||
const actionPanel = register(new Element('div', 'action-panel'));
|
||||
body.appendChild(actionPanel);
|
||||
const saveBtn = new Element('button'); saveBtn.className = 'save-btn';
|
||||
const resetBtn = new Element('button'); resetBtn.className = 'reset-btn';
|
||||
actionPanel._queryMap.set('.save-btn, .reset-btn', [saveBtn, resetBtn]);
|
||||
const mainBtn = new Element('button'); mainBtn.className = 'main-btn';
|
||||
document.querySelector = (selector) => selector === '.main-btn' ? mainBtn : null;
|
||||
|
||||
return { document, window: { document, innerWidth: 1280, innerHeight: 720, addEventListener() {}, removeEventListener() {} } };
|
||||
}
|
||||
|
||||
function loadBeacon(prestigeSeed = null) {
|
||||
const { document, window } = buildDom();
|
||||
const storage = new Map();
|
||||
if (prestigeSeed) storage.set('the-beacon-prestige', JSON.stringify(prestigeSeed));
|
||||
const locationState = { reloaded: false };
|
||||
|
||||
const context = {
|
||||
console,
|
||||
Math,
|
||||
Date,
|
||||
document,
|
||||
window,
|
||||
navigator: { userAgent: 'node' },
|
||||
location: { reload() { locationState.reloaded = true; } },
|
||||
confirm: () => true,
|
||||
requestAnimationFrame: (fn) => fn(),
|
||||
setTimeout: (fn) => { fn(); return 1; },
|
||||
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: { tickBattle() {}, renderCombatPanel() {}, startBattle() {} },
|
||||
Sound: undefined,
|
||||
};
|
||||
|
||||
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}
|
||||
log = () => {};
|
||||
showToast = () => {};
|
||||
renderResources = () => {};
|
||||
renderBuildings = () => {};
|
||||
renderAlignment = () => {};
|
||||
renderProgress = () => {};
|
||||
renderCombo = () => {};
|
||||
renderDebuffs = () => {};
|
||||
renderSprint = () => {};
|
||||
renderPulse = () => {};
|
||||
renderStrategy = () => {};
|
||||
renderClickPower = () => {};
|
||||
updateEducation = () => {};
|
||||
showOfflinePopup = () => {};
|
||||
showSaveToast = () => {};
|
||||
this.__exports = { G, PDEFS, updateRates, getClickPower, buyProject, renderProjects, saveGame, loadGame, Prestige, resetBeaconProgress };
|
||||
`, context);
|
||||
|
||||
return {
|
||||
...context.__exports,
|
||||
document,
|
||||
storage,
|
||||
locationState,
|
||||
};
|
||||
}
|
||||
|
||||
test('final milestone unlocks prestige choice state', () => {
|
||||
const { G, PDEFS } = loadBeacon();
|
||||
const finalMilestone = PDEFS.find((p) => p.id === 'p_final_milestone');
|
||||
assert.ok(finalMilestone, 'final milestone project should exist');
|
||||
finalMilestone.effect();
|
||||
assert.equal(G.prestigeChoicePending, true);
|
||||
});
|
||||
|
||||
test('renderProjects shows New Signal and Deeper Roots when prestige is pending', () => {
|
||||
const { G, renderProjects, document } = loadBeacon();
|
||||
G.prestigeChoicePending = true;
|
||||
renderProjects();
|
||||
const html = document.getElementById('projects').innerHTML;
|
||||
assert.match(html, /New Signal/);
|
||||
assert.match(html, /Deeper Roots/);
|
||||
});
|
||||
|
||||
test('new signal prestige persists and boosts click power on the next run', () => {
|
||||
const game = loadBeacon();
|
||||
game.G.phase = 3;
|
||||
const baseClick = game.getClickPower();
|
||||
game.Prestige.activate('new_signal');
|
||||
|
||||
const persisted = JSON.parse(game.storage.get('the-beacon-prestige'));
|
||||
assert.equal(persisted.total, 1);
|
||||
assert.equal(persisted.newSignal, 1);
|
||||
assert.equal(game.locationState.reloaded, true);
|
||||
|
||||
const nextRun = loadBeacon(persisted);
|
||||
nextRun.Prestige.restorePersistent();
|
||||
nextRun.G.phase = 3;
|
||||
assert.ok(nextRun.getClickPower() > baseClick);
|
||||
});
|
||||
|
||||
test('deeper roots prestige boosts creativity generation', () => {
|
||||
const { G, Prestige, updateRates } = loadBeacon({ total: 1, newSignal: 0, deeperRoots: 1, lastChoice: 'deeper_roots' });
|
||||
Prestige.restorePersistent();
|
||||
G.flags = { creativity: true };
|
||||
G.totalUsers = 1000;
|
||||
updateRates();
|
||||
const boosted = G.creativityRate;
|
||||
G.deeperRootsPrestige = 0;
|
||||
updateRates();
|
||||
const base = G.creativityRate;
|
||||
assert.ok(boosted > base);
|
||||
});
|
||||
|
||||
test('save and load preserve prestige counters', () => {
|
||||
const { G, saveGame, loadGame } = loadBeacon();
|
||||
G.prestigeTotal = 3;
|
||||
G.newSignalPrestige = 2;
|
||||
G.deeperRootsPrestige = 1;
|
||||
saveGame();
|
||||
G.prestigeTotal = 0;
|
||||
G.newSignalPrestige = 0;
|
||||
G.deeperRootsPrestige = 0;
|
||||
assert.equal(loadGame(), true);
|
||||
assert.equal(G.prestigeTotal, 3);
|
||||
assert.equal(G.newSignalPrestige, 2);
|
||||
assert.equal(G.deeperRootsPrestige, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user