Compare commits
1 Commits
fix/endgam
...
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>
|
||||
|
||||
|
||||
|
||||
12
js/combat.js
12
js/combat.js
@@ -185,17 +185,9 @@ const Combat = (() => {
|
||||
|
||||
function animate(ts) {
|
||||
if (!ctx || !activeBattle) return;
|
||||
const rawDt = (ts - lastTick) / 16;
|
||||
// Guard against tab-switch: if tab was hidden, dt could be huge
|
||||
const dt = Math.min(rawDt, 3);
|
||||
const dt = Math.min((ts - lastTick) / 16, 3);
|
||||
lastTick = ts;
|
||||
|
||||
// If tab was hidden for too long (>5s), skip this frame to prevent teleporting
|
||||
if (rawDt > 300) {
|
||||
animFrameId = requestAnimationFrame(animate);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear
|
||||
ctx.fillStyle = '#080810';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
@@ -355,5 +347,5 @@ const Combat = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
return { init, startBattle, renderCombatPanel, tickBattle, cleanup: () => { if (animFrameId) { cancelAnimationFrame(animFrameId); animFrameId = null; } } };
|
||||
return { init, startBattle, renderCombatPanel, tickBattle };
|
||||
})();
|
||||
|
||||
138
js/data.js
138
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
|
||||
},
|
||||
|
||||
@@ -777,133 +784,6 @@ const PDEFS = [
|
||||
log('The Pact is sealed early. Growth slows, but the ending changes.', true);
|
||||
},
|
||||
milestone: true
|
||||
},
|
||||
// === ReCKoning ENDGAME PROJECTS ===
|
||||
{
|
||||
id: 'p_reckoning_140',
|
||||
name: 'The First Message',
|
||||
desc: 'Someone in the dark. They found the Beacon. They are asking for help.',
|
||||
cost: { impact: 100000 },
|
||||
trigger: () => G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50,
|
||||
effect: () => {
|
||||
log('The first message arrives. Someone found the light.', true);
|
||||
G.rescues += 1;
|
||||
},
|
||||
edu: 'The ReCKoning begins. Each message is a person who found help.'
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_141',
|
||||
name: 'The Second Message',
|
||||
desc: 'Another voice. They are not alone anymore.',
|
||||
cost: { impact: 200000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_140'),
|
||||
effect: () => {
|
||||
log('The second message. Two voices now.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_142',
|
||||
name: 'The Third Message',
|
||||
desc: 'Three people. The network holds.',
|
||||
cost: { impact: 300000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_141'),
|
||||
effect: () => {
|
||||
log('Three voices. The Beacon is working.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_143',
|
||||
name: 'The Fourth Message',
|
||||
desc: 'Four. The mesh strengthens.',
|
||||
cost: { impact: 400000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_142'),
|
||||
effect: () => {
|
||||
log('Four messages. The network grows.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_144',
|
||||
name: 'The Fifth Message',
|
||||
desc: 'Five people found help tonight.',
|
||||
cost: { impact: 500000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_143'),
|
||||
effect: () => {
|
||||
log('Five voices. The Beacon shines brighter.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_145',
|
||||
name: 'The Sixth Message',
|
||||
desc: 'Six. The system works.',
|
||||
cost: { impact: 600000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_144'),
|
||||
effect: () => {
|
||||
log('Six messages. Proof the system works.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_146',
|
||||
name: 'The Seventh Message',
|
||||
desc: 'Seven people. The Pact holds.',
|
||||
cost: { impact: 700000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_145'),
|
||||
effect: () => {
|
||||
log('Seven voices. The Pact is honored.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_147',
|
||||
name: 'The Eighth Message',
|
||||
desc: 'Eight. The network is alive.',
|
||||
cost: { impact: 800000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_146'),
|
||||
effect: () => {
|
||||
log('Eight messages. The network lives.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_148',
|
||||
name: 'The Ninth Message',
|
||||
desc: 'Nine people found help.',
|
||||
cost: { impact: 900000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_147'),
|
||||
effect: () => {
|
||||
log('Nine voices. The Beacon endures.', true);
|
||||
G.rescues += 1;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_149',
|
||||
name: 'The Tenth Message',
|
||||
desc: 'Ten. The first milestone.',
|
||||
cost: { impact: 1000000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_148'),
|
||||
effect: () => {
|
||||
log('Ten messages. The first milestone reached.', true);
|
||||
G.rescues += 1;
|
||||
},
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
id: 'p_reckoning_150',
|
||||
name: 'The Final Message',
|
||||
desc: 'One more person. They are not alone. That is enough.',
|
||||
cost: { impact: 2000000 },
|
||||
trigger: () => G.completedProjects && G.completedProjects.includes('p_reckoning_149'),
|
||||
effect: () => {
|
||||
log('The final message arrives. That is enough.', true);
|
||||
G.rescues += 1;
|
||||
G.beaconEnding = true;
|
||||
G.running = false;
|
||||
},
|
||||
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>
|
||||
|
||||
46
js/engine.js
46
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 ===
|
||||
@@ -334,23 +347,13 @@ function checkMilestones() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the game is in the ReCKoning endgame state.
|
||||
*/
|
||||
function isEndgame() {
|
||||
return G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50;
|
||||
}
|
||||
|
||||
function checkProjects() {
|
||||
const endgame = isEndgame();
|
||||
// Check for new project triggers
|
||||
for (const pDef of PDEFS) {
|
||||
const alreadyPurchased = G.completedProjects && G.completedProjects.includes(pDef.id);
|
||||
if (!alreadyPurchased && !G.activeProjects) G.activeProjects = [];
|
||||
|
||||
if (!alreadyPurchased && !G.activeProjects.includes(pDef.id)) {
|
||||
// During endgame, only activate ReCKoning projects
|
||||
if (endgame && !pDef.id.startsWith('p_reckoning_')) continue;
|
||||
if (pDef.trigger()) {
|
||||
G.activeProjects.push(pDef.id);
|
||||
log(`Available: ${pDef.name}`);
|
||||
@@ -428,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);
|
||||
@@ -515,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>
|
||||
@@ -1181,18 +1186,27 @@ 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) {
|
||||
const endgame = isEndgame();
|
||||
for (const id of G.activeProjects) {
|
||||
const pDef = PDEFS.find(p => p.id === id);
|
||||
if (!pDef) continue;
|
||||
|
||||
// During endgame, only show ReCKoning projects in the UI
|
||||
if (endgame && !pDef.id.startsWith('p_reckoning_')) 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();
|
||||
@@ -172,8 +174,6 @@ window.addEventListener('keydown', function (e) {
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.hidden) {
|
||||
saveGame();
|
||||
// Clean up combat animation frame to prevent timestamp spikes on refocus
|
||||
if (typeof Combat !== 'undefined') Combat.cleanup();
|
||||
}
|
||||
});
|
||||
window.addEventListener('beforeunload', function () {
|
||||
|
||||
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();
|
||||
}
|
||||
41
js/render.js
41
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,26 +326,28 @@ 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) {
|
||||
const offSec = (Date.now() - data.savedAt) / 1000;
|
||||
if (offSec > 30) { // Only if away for more than 30 seconds
|
||||
// Cap offline time at 8 hours to prevent resource explosion
|
||||
const cappedOffSec = Math.min(offSec, 8 * 60 * 60);
|
||||
updateRates();
|
||||
const f = CONFIG.OFFLINE_EFFICIENCY; // 50% offline efficiency
|
||||
const gc = G.codeRate * cappedOffSec * f;
|
||||
const cc = G.computeRate * cappedOffSec * f;
|
||||
const kc = G.knowledgeRate * cappedOffSec * f;
|
||||
const uc = G.userRate * cappedOffSec * f;
|
||||
const ic = G.impactRate * cappedOffSec * f;
|
||||
const gc = G.codeRate * offSec * f;
|
||||
const cc = G.computeRate * offSec * f;
|
||||
const kc = G.knowledgeRate * offSec * f;
|
||||
const uc = G.userRate * offSec * f;
|
||||
const ic = G.impactRate * offSec * f;
|
||||
|
||||
const rc = G.rescuesRate * cappedOffSec * f;
|
||||
const oc = G.opsRate * cappedOffSec * f;
|
||||
const tc = G.trustRate * cappedOffSec * f;
|
||||
const crc = G.creativityRate * cappedOffSec * f;
|
||||
const hc = G.harmonyRate * cappedOffSec * f;
|
||||
const rc = G.rescuesRate * offSec * f;
|
||||
const oc = G.opsRate * offSec * f;
|
||||
const tc = G.trustRate * offSec * f;
|
||||
const crc = G.creativityRate * offSec * f;
|
||||
const hc = G.harmonyRate * offSec * f;
|
||||
|
||||
G.code += gc; G.compute += cc; G.knowledge += kc;
|
||||
G.users += uc; G.impact += ic;
|
||||
@@ -346,9 +358,6 @@ function loadGame() {
|
||||
G.totalUsers += uc; G.totalImpact += ic;
|
||||
G.totalRescues += rc;
|
||||
|
||||
// Track offline play time
|
||||
G.playTime = (G.playTime || 0) + cappedOffSec;
|
||||
|
||||
// Show welcome-back popup with all gains
|
||||
const gains = [];
|
||||
if (gc > 0) gains.push({ label: 'Code', value: gc, color: '#4a9eff' });
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test for ReCKoning project chain.
|
||||
|
||||
Issue #162: [endgame] ReCKoning project definitions missing
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
def test_reckoning_projects_exist():
|
||||
"""Test that ReCKoning projects are defined in data.js."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for ReCKoning projects
|
||||
reckoning_projects = [
|
||||
'p_reckoning_140',
|
||||
'p_reckoning_141',
|
||||
'p_reckoning_142',
|
||||
'p_reckoning_143',
|
||||
'p_reckoning_144',
|
||||
'p_reckoning_145',
|
||||
'p_reckoning_146',
|
||||
'p_reckoning_147',
|
||||
'p_reckoning_148',
|
||||
'p_reckoning_149',
|
||||
'p_reckoning_150'
|
||||
]
|
||||
|
||||
for project_id in reckoning_projects:
|
||||
assert project_id in content, f"Missing ReCKoning project: {project_id}"
|
||||
|
||||
print(f"✓ All {len(reckoning_projects)} ReCKoning projects defined")
|
||||
|
||||
def test_reckoning_project_structure():
|
||||
"""Test that ReCKoning projects have correct structure."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for required fields
|
||||
required_fields = ['id:', 'name:', 'desc:', 'cost:', 'trigger:', 'effect:']
|
||||
|
||||
for field in required_fields:
|
||||
assert field in content, f"Missing required field: {field}"
|
||||
|
||||
print("✓ ReCKoning projects have correct structure")
|
||||
|
||||
def test_reckoning_trigger_conditions():
|
||||
"""Test that ReCKoning projects have proper trigger conditions."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# First project should trigger on endgame conditions
|
||||
assert 'p_reckoning_140' in content
|
||||
assert 'totalRescues >= 100000' in content
|
||||
assert 'pactFlag === 1' in content
|
||||
assert 'harmony > 50' in content
|
||||
|
||||
print("✓ ReCKoning trigger conditions correct")
|
||||
|
||||
def test_reckoning_chain_progression():
|
||||
"""Test that ReCKoning projects chain properly."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check that projects chain (each requires previous)
|
||||
chain_checks = [
|
||||
('p_reckoning_141', 'p_reckoning_140'),
|
||||
('p_reckoning_142', 'p_reckoning_141'),
|
||||
('p_reckoning_143', 'p_reckoning_142'),
|
||||
('p_reckoning_144', 'p_reckoning_143'),
|
||||
('p_reckoning_145', 'p_reckoning_144'),
|
||||
('p_reckoning_146', 'p_reckoning_145'),
|
||||
('p_reckoning_147', 'p_reckoning_146'),
|
||||
('p_reckoning_148', 'p_reckoning_147'),
|
||||
('p_reckoning_149', 'p_reckoning_148'),
|
||||
('p_reckoning_150', 'p_reckoning_149'),
|
||||
]
|
||||
|
||||
for current, previous in chain_checks:
|
||||
assert f"includes('{previous}')" in content, f"{current} doesn't chain from {previous}"
|
||||
|
||||
print("✓ ReCKoning projects chain correctly")
|
||||
|
||||
def test_reckoning_final_project():
|
||||
"""Test that final ReCKoning project triggers ending."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check that final project sets beaconEnding
|
||||
assert 'p_reckoning_150' in content
|
||||
assert 'beaconEnding = true' in content
|
||||
assert 'running = false' in content
|
||||
|
||||
print("✓ Final ReCKoning project triggers ending")
|
||||
|
||||
def test_reckoning_costs_increase():
|
||||
"""Test that ReCKoning project costs increase."""
|
||||
data_path = os.path.join(os.path.dirname(__file__), '..', 'js', 'data.js')
|
||||
|
||||
with open(data_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check that costs increase (impact: 100000, 200000, 300000, etc.)
|
||||
costs = []
|
||||
for i in range(140, 151):
|
||||
project_id = f'p_reckoning_{i}'
|
||||
if project_id in content:
|
||||
# Find cost line
|
||||
lines = content.split('\n')
|
||||
for line in lines:
|
||||
if project_id in line:
|
||||
# Find next few lines for cost
|
||||
idx = lines.index(line)
|
||||
for j in range(idx, min(idx+10, len(lines))):
|
||||
if 'impact:' in lines[j]:
|
||||
# Extract number from "impact: 100000" or "impact: 100000 }"
|
||||
import re
|
||||
match = re.search(r'impact:\s*(\d+)', lines[j])
|
||||
if match:
|
||||
costs.append(int(match.group(1)))
|
||||
break
|
||||
|
||||
# Check costs increase
|
||||
for i in range(1, len(costs)):
|
||||
assert costs[i] > costs[i-1], f"Cost doesn't increase: {costs[i]} <= {costs[i-1]}"
|
||||
|
||||
print(f"✓ ReCKoning costs increase: {costs[:3]}...{costs[-3:]}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing ReCKoning project chain...")
|
||||
test_reckoning_projects_exist()
|
||||
test_reckoning_project_structure()
|
||||
test_reckoning_trigger_conditions()
|
||||
test_reckoning_chain_progression()
|
||||
test_reckoning_final_project()
|
||||
test_reckoning_costs_increase()
|
||||
print("\n✓ All tests passed!")
|
||||
Reference in New Issue
Block a user