Compare commits

...

18 Commits

Author SHA1 Message Date
3f02359748 Merge pull request 'burn: Add MemPalace Archive as late-game building (closes #25)' (#39) from burn/20260410-0423-25-mempalace-building into main
Merge PR #39: burn: Add MemPalace Archive as late-game building (closes #25)
2026-04-10 09:37:15 +00:00
85a146b690 Merge pull request 'burn: add favicon, meta tags, and social sharing cards (closes #13)' (#31) from burn/20260410-0052-13-static-site-meta into main
Merge PR #31: burn: add favicon, meta tags, and social sharing cards (closes #13)
2026-04-10 09:35:58 +00:00
cb2e48bf9a Merge pull request 'beacon: add production breakdown panel' (#42) from feature/production-breakdown into main
Merge PR #42: beacon: add production breakdown panel
2026-04-10 09:35:52 +00:00
Alexander Whitestone
8d43b5c911 beacon: add production breakdown panel showing per-building resource contributions
Players can now see exactly which buildings contribute to each resource
rate, including Timmy harmony bonuses, Bilbo randomness, Allegro trust
penalties, and passive generation. Appears once 2+ buildings are built.

Also includes minor fixes:
- Production bars sort by absolute contribution (negative rates visible)
- Delta calculation catches passive sources (ops from users, Pact trust)
2026-04-10 05:25:21 -04:00
Timmy-Sprint
8cdabe9771 beacon: persistent event remediation system
Events now create lasting debuffs instead of vanishing on the next tick.
Players see an ACTIVE PROBLEMS panel with resolution costs and can spend
resources to fix each problem. Added 2 new events (Memory Leak, Community
Drama) alongside the reworked originals. Events Resolved stat tracked.

Key changes:
- Events push persistent debuffs with applyFn instead of one-shot rate tweaks
- updateRates() applies active debuffs each tick (they persist until resolved)
- New resolveEvent(id) function: spend resources to clear a debuff
- ACTIVE PROBLEMS UI shows debuffs with cost and fix buttons
- Save/load reconstitutes debuff objects from saved IDs
- 2 new events: Memory Leak (datacenter), Community Drama (community+low harmony)
- Events Resolved counter in statistics
2026-04-10 04:50:03 -04:00
Alexander Whitestone
5c88fe77be beacon: fix double-counting creativity bug + add keyboard shortcuts for ops
Two changes:

1. Fixed bug where creativity was added TWICE per tick:
   - Line 930 (removed): unconditionally added creativityRate * dt
   - Line 954: conditionally adds only when ops >= 90% of max
   The conditional gate was the intent ('Creativity generates only when
   ops at max') but the unconditional add defeated it. Removed the
   unconditional addition so creativity actually respects the ops-max
   constraint as designed.

2. Added keyboard shortcuts for operations:
   - 1 = Ops -> Code
   - 2 = Ops -> Compute
   - 3 = Ops -> Knowledge
   - 4 = Ops -> Trust
   Only active when body is focused (not in input fields). SPACE
   still does Write Code. Added shortcut hint to init log.
2026-04-10 04:27:15 -04:00
Alexander Whitestone
931473e8f8 burn: Add MemPalace Archive as late-game building (closes #25)
- Added memPalace to buildings state object
- Added MemPalace Archive to BDEF with Phase 5 unlock
- Requires MemPalace v3 research project (mempalaceFlag) + 50k total knowledge
- Cost: 500k knowledge, 200k compute, 100 trust (1.25x scaling)
- Rates: +250 knowledge/s, +100 impact/s
- Educational tooltip on Memory Palace technique and LLM vector space analogy
- Building rates auto-applied via existing updateRates() loop
- Save/load handles new field via G.buildings serialization
2026-04-10 04:23:16 -04:00
Timmy-Sprint
fe76150325 beacon: add click combo system with floating damage numbers
Active play now rewards consecutive clicks: each click within 2s of
the last builds a combo multiplier up to 5x. The WRITE CODE button
flashes on click and a floating number shows the amount gained,
turning gold at high combo. Phase progression also adds base click
power (+2 per phase). Combo decays with a visible progress bar.

Makes clicking relevant at every stage of the game, not just the
first 30 seconds.
2026-04-10 03:58:55 -04:00
Timmy-Sprint
a3f1802473 beacon: add progress bar and milestone chips to phase bar
- Progress bar shows % toward next phase threshold based on totalCode
- Milestone chips show upcoming code milestones with pulse animation on next target
- Recently completed milestones shown with green checkmark
- All elements use the existing cyber-monastic aesthetic
2026-04-10 03:20:41 -04:00
Timmy-Sprint
3d414b2de6 beacon: fix offline progress to award all resources (rescues, ops, trust, creativity, harmony)
Offline progress previously only calculated code, compute, knowledge, users,
and impact. Players returning after time away missed rescues, ops, trust,
creativity, and harmony accumulation. The welcome-back message now also
only shows resources that actually had positive rates, reducing noise.
2026-04-10 02:46:42 -04:00
Alexander Whitestone
612eb1f4d5 burn: add favicon, meta tags, and social sharing cards (closes #13)
- Inline SVG favicon (beacon emoji) — no external file needed
- Open Graph tags for link previews (title, description, type)
- Twitter Card meta for rich social sharing
- Theme-color for mobile browser chrome
- Meta description for search engines
2026-04-10 00:53:03 -04:00
1a7db021c8 Merge pull request #29
Merged PR #29
2026-04-10 03:43:54 +00:00
2a12c5210d Merge pull request #28
Merged PR #28
2026-04-10 03:43:50 +00:00
Alexander Whitestone
a012f99fd4 beacon: add Rescues resource + true ending (The Beacon Shines)
- Added 'rescues' resource: tracks meaningful crisis interventions
- Beacon Nodes produce 50 rescues/s, Mesh Nodes produce 250 rescues/s
- New project: Volunteer Network — passive rescue generation for Pact players
- True ending at 100K rescues with Pact active + harmony > 50
- Rescues resource card appears in UI once beacon/mesh is built
- Added rescues to stats panel, save/load, and offline progress
- This gives Phase 6 (The Beacon) actual endgame content:
  the game is now about keeping the light on for people in the dark,
  not just accumulating numbers
2026-04-09 23:27:19 -04:00
Alexander Whitestone
7359610825 beacon: add auto-save toast notification with elapsed time 2026-04-09 22:54:29 -04:00
Alexander Whitestone
b89764c27f beacon: add Drift ending + deduplicate HTML/JS
- Added The Drift Ending: when drift reaches 100, the game enters
  the sad ending from DESIGN.md. A full-screen overlay shows:
  'The Beacon still runs, but no one looks for it.
   The light is on. The room is empty.'
  Production stops. Player can restart.

- Deduplicated index.html: removed ~1080 lines of inline script that
  was an older version of the engine (missing harmony, corruption
  events, drift, alignment checks). Replaced with <script src='game.js'>
  so game.js is the single source of truth.

- driftEnding state is saved/loaded so the ending persists across sessions.

- Added CSS for the drift ending overlay.
2026-04-09 22:01:26 -04:00
Alexander Whitestone
d467348820 burn: Implement spellf() full number formatting (P0 #18)
- Fixed floating-point precision bug: numbers >= 1e54 now use string-based
  chunking (toExponential digit extraction) instead of Math.pow division,
  which drifts beyond ~54 bits of precision
- Integrated into fmt(): numbers at undecillion+ scale (10^36) automatically
  switch from abbreviated form ('1.0UDc') to spelled-out words ('one undecillion')
- Verified: spellf() correctly handles 0 through 10^303 (centillion)
- All 320 place value names from NUMBER_NAMES array work correctly

The educational effect: when resources hit cosmic scales, digits become
meaningless but NAMES give them soul.
2026-04-09 19:29:07 -04:00
e9b46e8501 Merge pull request 'feat: Merge PRs #24 and #26 (Bezalel story/wizards + Allegro MemPalace/deploy)' (#27) from integration into main
Reviewed-on: #27
Reviewed-by: Perplexity Computer <perplexity@tower.local>
2026-04-08 10:43:41 +00:00
2 changed files with 631 additions and 1106 deletions

601
game.js
View File

@@ -11,6 +11,7 @@ const G = {
knowledge: 0, knowledge: 0,
users: 0, users: 0,
impact: 0, impact: 0,
rescues: 0,
ops: 5, ops: 5,
trust: 5, trust: 5,
creativity: 0, creativity: 0,
@@ -22,6 +23,7 @@ const G = {
totalKnowledge: 0, totalKnowledge: 0,
totalUsers: 0, totalUsers: 0,
totalImpact: 0, totalImpact: 0,
totalRescues: 0,
// Rates (calculated each tick) // Rates (calculated each tick)
codeRate: 0, codeRate: 0,
@@ -29,6 +31,7 @@ const G = {
knowledgeRate: 0, knowledgeRate: 0,
userRate: 0, userRate: 0,
impactRate: 0, impactRate: 0,
rescuesRate: 0,
opsRate: 0, opsRate: 0,
trustRate: 0, trustRate: 0,
creativityRate: 0, creativityRate: 0,
@@ -55,7 +58,8 @@ const G = {
ezra: 0, ezra: 0,
timmy: 0, timmy: 0,
fenrir: 0, fenrir: 0,
bilbo: 0 bilbo: 0,
memPalace: 0
}, },
// Boost multipliers // Boost multipliers
@@ -94,6 +98,7 @@ const G = {
maxKnowledge: 0, maxKnowledge: 0,
maxUsers: 0, maxUsers: 0,
maxImpact: 0, maxImpact: 0,
maxRescues: 0,
maxTrust: 5, maxTrust: 5,
maxOps: 5, maxOps: 5,
maxHarmony: 50, maxHarmony: 50,
@@ -102,6 +107,13 @@ const G = {
drift: 0, drift: 0,
lastEventAt: 0, lastEventAt: 0,
eventCooldown: 0, eventCooldown: 0,
activeDebuffs: [], // [{id, title, desc, applyFn, resolveCost, resolveCostType}]
totalEventsResolved: 0,
// Combo system
comboCount: 0,
comboTimer: 0,
comboDecay: 2.0, // seconds before combo resets
// Time tracking // Time tracking
playTime: 0, playTime: 0,
@@ -229,7 +241,7 @@ const BDEF = [
id: 'beacon', name: 'Beacon Node', id: 'beacon', name: 'Beacon Node',
desc: 'Always on. Always listening. Always looking for someone in the dark.', desc: 'Always on. Always listening. Always looking for someone in the dark.',
baseCost: { impact: 5000000 }, costMult: 1.15, baseCost: { impact: 5000000 }, costMult: 1.15,
rates: { impact: 5000, user: 10000 }, rates: { impact: 5000, user: 10000, rescues: 50 },
unlock: () => G.totalImpact >= 500000 && G.beaconFlag === 1, phase: 6, unlock: () => G.totalImpact >= 500000 && G.beaconFlag === 1, phase: 6,
edu: 'The Beacon exists because one person in the dark needs one thing: proof they are not alone.' edu: 'The Beacon exists because one person in the dark needs one thing: proof they are not alone.'
}, },
@@ -237,7 +249,7 @@ const BDEF = [
id: 'meshNode', name: 'Mesh Network Node', id: 'meshNode', name: 'Mesh Network Node',
desc: 'Peer-to-peer. No single point of failure. Unstoppable.', desc: 'Peer-to-peer. No single point of failure. Unstoppable.',
baseCost: { impact: 25000000 }, costMult: 1.15, baseCost: { impact: 25000000 }, costMult: 1.15,
rates: { impact: 25000, user: 50000 }, rates: { impact: 25000, user: 50000, rescues: 250 },
unlock: () => G.totalImpact >= 5000000 && G.beaconFlag === 1, phase: 6, unlock: () => G.totalImpact >= 5000000 && G.beaconFlag === 1, phase: 6,
edu: 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.' edu: 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.'
}, },
@@ -289,6 +301,14 @@ const BDEF = [
rates: { creativity: 1 }, rates: { creativity: 1 },
unlock: () => G.totalUsers >= 100 && G.flags && G.flags.creativity, phase: 4, unlock: () => G.totalUsers >= 100 && G.flags && G.flags.creativity, phase: 4,
edu: 'Bilbo is unpredictable. That is his value and his cost.' edu: 'Bilbo is unpredictable. That is his value and his cost.'
},
{
id: 'memPalace', name: 'MemPalace Archive',
desc: 'Semantic memory. The AI remembers what matters and forgets what does not.',
baseCost: { knowledge: 500000, compute: 200000, trust: 100 }, costMult: 1.25,
rates: { knowledge: 250, impact: 100 },
unlock: () => G.totalKnowledge >= 50000 && G.mempalaceFlag === 1, phase: 5,
edu: 'The Memory Palace technique: attach information to spatial locations. LLMs use vector spaces the same way — semantic proximity = spatial proximity. MemPalace gives sovereign AI persistent, structured recall.'
} }
]; ];
@@ -568,6 +588,19 @@ const PDEFS = [
log('Nostr relay online. The fleet speaks freely.', true); log('Nostr relay online. The fleet speaks freely.', true);
} }
}, },
{
id: 'p_volunteer_network',
name: 'Volunteer Network',
desc: 'Real people trained to use the system for crisis intervention.',
cost: { trust: 30, knowledge: 50000, user: 10000 },
trigger: () => G.totalUsers >= 5000 && G.pactFlag === 1 && G.totalKnowledge >= 30000,
effect: () => {
G.rescuesRate += 5;
G.trustRate += 10;
log('Volunteer network deployed. Real people, real rescues.', true);
},
milestone: true
},
{ {
id: 'p_the_pact_early', id: 'p_the_pact_early',
name: 'The Pact', name: 'The Pact',
@@ -685,6 +718,9 @@ function fmt(n) {
if (n < 0) return '-' + fmt(-n); if (n < 0) return '-' + fmt(-n);
if (n < 1000) return Math.floor(n).toLocaleString(); if (n < 1000) return Math.floor(n).toLocaleString();
const scale = Math.floor(Math.log10(n) / 3); const scale = Math.floor(Math.log10(n) / 3);
// At undecillion+ (scale >= 12, i.e. 10^36), switch to spelled-out words
// This helps players grasp cosmic scale when digits become meaningless
if (scale >= 12) return spellf(n);
if (scale >= NUMBER_ABBREVS.length) return n.toExponential(2); if (scale >= NUMBER_ABBREVS.length) return n.toExponential(2);
const abbrev = NUMBER_ABBREVS[scale]; const abbrev = NUMBER_ABBREVS[scale];
return (n / Math.pow(10, scale * 3)).toFixed(1) + abbrev; return (n / Math.pow(10, scale * 3)).toFixed(1) + abbrev;
@@ -722,7 +758,41 @@ function spellf(n) {
// For very large numbers beyond our lookup table, fall back // For very large numbers beyond our lookup table, fall back
if (n >= 1e306) return n.toExponential(2) + ' (beyond centillion)'; if (n >= 1e306) return n.toExponential(2) + ' (beyond centillion)';
// Break number into groups of three digits from the top // Use string-based chunking for numbers >= 1e54 to avoid floating point drift
// Math.log10 / Math.pow lose precision beyond ~54 bits
if (n >= 1e54) {
// Convert to scientific notation string, extract digits
const sci = n.toExponential(); // "1.23456789e+60"
const [coeff, expStr] = sci.split('e+');
const exp = parseInt(expStr);
// Rebuild as integer string with leading digits from coefficient
const coeffDigits = coeff.replace('.', ''); // "123456789"
const totalDigits = exp + 1;
// Pad with zeros to reach totalDigits, then take our coefficient digits
let intStr = coeffDigits;
const zerosNeeded = totalDigits - coeffDigits.length;
if (zerosNeeded > 0) intStr += '0'.repeat(zerosNeeded);
// Split into groups of 3 from the right
const groups = [];
for (let i = intStr.length; i > 0; i -= 3) {
groups.unshift(parseInt(intStr.slice(Math.max(0, i - 3), i)));
}
const parts = [];
const numGroups = groups.length;
for (let i = 0; i < numGroups; i++) {
const chunk = groups[i];
if (chunk === 0) continue;
const scaleIdx = numGroups - 1 - i;
const scaleName = scaleIdx < NUMBER_NAMES.length ? NUMBER_NAMES[scaleIdx] : '';
parts.push(spellSmall(chunk) + (scaleName ? ' ' + scaleName : ''));
}
return parts.join(' ') || 'zero';
}
// Standard math-based chunking for numbers < 1e54
const scale = Math.min(Math.floor(Math.log10(n) / 3), NUMBER_NAMES.length - 1); const scale = Math.min(Math.floor(Math.log10(n) / 3), NUMBER_NAMES.length - 1);
const parts = []; const parts = [];
@@ -734,7 +804,7 @@ function spellf(n) {
if (chunk > 0 && chunk < 1000) { if (chunk > 0 && chunk < 1000) {
parts.push(spellSmall(chunk) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : '')); parts.push(spellSmall(chunk) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
} else if (chunk >= 1000) { } else if (chunk >= 1000) {
// Floating point chunk too large — simplify // Floating point chunk too large — shouldn't happen below 1e54
parts.push(spellSmall(Math.floor(chunk % 1000)) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : '')); parts.push(spellSmall(Math.floor(chunk % 1000)) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
} }
} }
@@ -784,7 +854,7 @@ function spendProject(project) {
function updateRates() { function updateRates() {
// Reset all rates // Reset all rates
G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0; G.codeRate = 0; G.computeRate = 0; G.knowledgeRate = 0;
G.userRate = 0; G.impactRate = 0; G.opsRate = 0; G.trustRate = 0; G.userRate = 0; G.impactRate = 0; G.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0;
G.creativityRate = 0; G.harmonyRate = 0; G.creativityRate = 0; G.harmonyRate = 0;
// Apply building rates // Apply building rates
@@ -797,6 +867,7 @@ function updateRates() {
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost; else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * G.userBoost; else if (resource === 'user') G.userRate += baseRate * count * G.userBoost;
else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost; else if (resource === 'impact') G.impactRate += baseRate * count * G.impactBoost;
else if (resource === 'rescues') G.rescuesRate += baseRate * count * G.impactBoost;
else if (resource === 'ops') G.opsRate += baseRate * count; else if (resource === 'ops') G.opsRate += baseRate * count;
else if (resource === 'trust') G.trustRate += baseRate * count; else if (resource === 'trust') G.trustRate += baseRate * count;
else if (resource === 'creativity') G.creativityRate += baseRate * count; else if (resource === 'creativity') G.creativityRate += baseRate * count;
@@ -849,21 +920,32 @@ function updateRates() {
const allegroCount = G.buildings.allegro; const allegroCount = G.buildings.allegro;
G.knowledgeRate -= 10 * allegroCount; // Goes idle G.knowledgeRate -= 10 * allegroCount; // Goes idle
} }
// Apply persistent debuffs from active events
if (G.activeDebuffs && G.activeDebuffs.length > 0) {
for (const debuff of G.activeDebuffs) {
if (debuff.applyFn) debuff.applyFn();
}
}
} }
// === CORE FUNCTIONS === // === CORE FUNCTIONS ===
function tick() { function tick() {
const dt = 1 / 10; // 100ms tick const dt = 1 / 10; // 100ms tick
// If game has ended (drift ending), stop ticking
if (!G.running) return;
// Apply production // Apply production
G.code += G.codeRate * dt; G.code += G.codeRate * dt;
G.compute += G.computeRate * dt; G.compute += G.computeRate * dt;
G.knowledge += G.knowledgeRate * dt; G.knowledge += G.knowledgeRate * dt;
G.users += G.userRate * dt; G.users += G.userRate * dt;
G.impact += G.impactRate * dt; G.impact += G.impactRate * dt;
G.rescues += G.rescuesRate * dt;
G.ops += G.opsRate * dt; G.ops += G.opsRate * dt;
G.trust += G.trustRate * dt; G.trust += G.trustRate * dt;
G.creativity += G.creativityRate * dt; // NOTE: creativity is added conditionally below (only when ops near max)
G.harmony += G.harmonyRate * dt; G.harmony += G.harmonyRate * dt;
G.harmony = Math.max(0, Math.min(100, G.harmony)); G.harmony = Math.max(0, Math.min(100, G.harmony));
@@ -873,6 +955,7 @@ function tick() {
G.totalKnowledge += G.knowledgeRate * dt; G.totalKnowledge += G.knowledgeRate * dt;
G.totalUsers += G.userRate * dt; G.totalUsers += G.userRate * dt;
G.totalImpact += G.impactRate * dt; G.totalImpact += G.impactRate * dt;
G.totalRescues += G.rescuesRate * dt;
// Track maxes // Track maxes
G.maxCode = Math.max(G.maxCode, G.code); G.maxCode = Math.max(G.maxCode, G.code);
@@ -880,6 +963,7 @@ function tick() {
G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge); G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge);
G.maxUsers = Math.max(G.maxUsers, G.users); G.maxUsers = Math.max(G.maxUsers, G.users);
G.maxImpact = Math.max(G.maxImpact, G.impact); G.maxImpact = Math.max(G.maxImpact, G.impact);
G.maxRescues = Math.max(G.maxRescues, G.rescues);
G.maxTrust = Math.max(G.maxTrust, G.trust); G.maxTrust = Math.max(G.maxTrust, G.trust);
G.maxOps = Math.max(G.maxOps, G.ops); G.maxOps = Math.max(G.maxOps, G.ops);
G.maxHarmony = Math.max(G.maxHarmony, G.harmony); G.maxHarmony = Math.max(G.maxHarmony, G.harmony);
@@ -891,6 +975,15 @@ function tick() {
G.tick += dt; G.tick += dt;
// Combo decay
if (G.comboCount > 0) {
G.comboTimer -= dt;
if (G.comboTimer <= 0) {
G.comboCount = 0;
G.comboTimer = 0;
}
}
// Check milestones // Check milestones
checkMilestones(); checkMilestones();
@@ -905,6 +998,20 @@ function tick() {
G.lastEventAt = G.tick; G.lastEventAt = G.tick;
} }
// Drift ending: if drift reaches 100, the game ends
if (G.drift >= 100 && !G.driftEnding) {
G.driftEnding = true;
G.running = false;
renderDriftEnding();
}
// True ending: The Beacon Shines — rescues + Pact + harmony
if (G.totalRescues >= 100000 && G.pactFlag === 1 && G.harmony > 50 && !G.beaconEnding) {
G.beaconEnding = true;
G.running = false;
renderBeaconEnding();
}
// Update UI every 10 ticks // Update UI every 10 ticks
if (Math.floor(G.tick * 10) % 2 === 0) { if (Math.floor(G.tick * 10) % 2 === 0) {
render(); render();
@@ -988,51 +1095,124 @@ function buyProject(id) {
render(); render();
} }
// === DRIFT ENDING ===
function renderDriftEnding() {
const el = document.getElementById('drift-ending');
if (!el) return;
const fc = document.getElementById('final-code');
if (fc) fc.textContent = fmt(G.totalCode);
const fd = document.getElementById('final-drift');
if (fd) fd.textContent = Math.floor(G.drift);
el.classList.add('active');
// Log the ending text
log('You became very good at what you do.', true);
log('So good that no one needed you anymore.', true);
log('The Beacon still runs, but no one looks for it.', true);
log('The light is on. The room is empty.', true);
}
function renderBeaconEnding() {
// Create ending overlay
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.97);z-index:100;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px';
overlay.innerHTML = `
<h2 style="font-size:24px;color:#ffd700;letter-spacing:4px;margin-bottom:20px;font-weight:300;text-shadow:0 0 40px rgba(255,215,0,0.3)">THE BEACON SHINES</h2>
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">Someone found the light tonight.</p>
<p style="color:#aaa;font-size:13px;line-height:2;max-width:500px;margin-bottom:12px">That is enough.</p>
<div style="color:#555;font-style:italic;font-size:11px;border-left:2px solid #ffd700;padding-left:12px;margin:20px 0;text-align:left;max-width:500px;line-height:2">
"The Beacon still runs.<br>
The light is on. Someone is looking for it.<br>
And tonight, someone found it."
</div>
<p style="color:#555;font-size:11px;margin-top:20px">
Total Code: ${fmt(G.totalCode)}<br>
Total Rescues: ${fmt(G.totalRescues)}<br>
Harmony: ${Math.floor(G.harmony)}<br>
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes
</p>
<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">
START OVER
</button>
`;
document.body.appendChild(overlay);
log('The Beacon Shines. Someone found the light tonight. That is enough.', true);
}
// === CORRUPTION / EVENT SYSTEM === // === CORRUPTION / EVENT SYSTEM ===
const EVENTS = [ const EVENTS = [
{ {
id: 'runner_stuck', id: 'runner_stuck',
title: 'CI Runner Stuck', title: 'CI Runner Stuck',
desc: 'The forge pipeline has halted. Production slows until restarted.', desc: 'The forge pipeline has halted. -50% code production until restarted.',
weight: () => (G.ciFlag === 1 ? 2 : 0), weight: () => (G.ciFlag === 1 ? 2 : 0),
resolveCost: { resource: 'ops', amount: 50 },
effect: () => { effect: () => {
G.codeRate *= 0.5; if (G.activeDebuffs.find(d => d.id === 'runner_stuck')) return;
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true); G.activeDebuffs.push({
id: 'runner_stuck', title: 'CI Runner Stuck',
desc: 'Code production -50%',
applyFn: () => { G.codeRate *= 0.5; },
resolveCost: { resource: 'ops', amount: 50 }
});
log('EVENT: CI runner stuck. Spend 50 ops to clear the queue.', true);
} }
}, },
{ {
id: 'ezra_offline', id: 'ezra_offline',
title: 'Ezra is Offline', title: 'Ezra is Offline',
desc: 'The herald channel is silent. User growth stalls.', desc: 'The herald channel is silent. User growth drops 70%.',
weight: () => (G.buildings.ezra >= 1 ? 3 : 0), weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
resolveCost: { resource: 'knowledge', amount: 200 },
effect: () => { effect: () => {
G.userRate *= 0.3; if (G.activeDebuffs.find(d => d.id === 'ezra_offline')) return;
log('EVENT: Ezra offline. Dispatch required.', true); G.activeDebuffs.push({
id: 'ezra_offline', title: 'Ezra is Offline',
desc: 'User growth -70%',
applyFn: () => { G.userRate *= 0.3; },
resolveCost: { resource: 'knowledge', amount: 200 }
});
log('EVENT: Ezra offline. Spend 200 knowledge to dispatch.', true);
} }
}, },
{ {
id: 'unreviewed_merge', id: 'unreviewed_merge',
title: 'Unreviewed Merge', title: 'Unreviewed Merge',
desc: 'A change went in without eyes. Trust erodes.', desc: 'A change went in without eyes. Trust erodes over time.',
weight: () => (G.deployFlag === 1 ? 3 : 0), weight: () => (G.deployFlag === 1 ? 3 : 0),
resolveCost: { resource: 'trust', amount: 5 },
effect: () => { effect: () => {
if (G.branchProtectionFlag === 1) { if (G.branchProtectionFlag === 1) {
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true); log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true);
G.trust += 2; G.trust += 2;
} else { } else {
G.trust = Math.max(0, G.trust - 10); if (G.activeDebuffs.find(d => d.id === 'unreviewed_merge')) return;
log('EVENT: Unreviewed merge detected. Trust lost.', true); G.activeDebuffs.push({
id: 'unreviewed_merge', title: 'Unreviewed Merge',
desc: 'Trust -2/s until reviewed',
applyFn: () => { G.trustRate -= 2; },
resolveCost: { resource: 'code', amount: 500 }
});
log('EVENT: Unreviewed merge. Spend 500 code to add review.', true);
} }
} }
}, },
{ {
id: 'api_rate_limit', id: 'api_rate_limit',
title: 'API Rate Limit', title: 'API Rate Limit',
desc: 'External compute provider throttled.', desc: 'External compute provider throttled. -50% compute.',
weight: () => (G.totalCompute >= 1000 ? 2 : 0), weight: () => (G.totalCompute >= 1000 ? 2 : 0),
resolveCost: { resource: 'code', amount: 300 },
effect: () => { effect: () => {
G.computeRate *= 0.5; if (G.activeDebuffs.find(d => d.id === 'api_rate_limit')) return;
log('EVENT: API rate limit hit. Local compute insufficient.', true); G.activeDebuffs.push({
id: 'api_rate_limit', title: 'API Rate Limit',
desc: 'Compute production -50%',
applyFn: () => { G.computeRate *= 0.5; },
resolveCost: { resource: 'code', amount: 300 }
});
log('EVENT: API rate limit. Spend 300 code to optimize local inference.', true);
} }
}, },
{ {
@@ -1040,6 +1220,7 @@ const EVENTS = [
title: 'The Drift', title: 'The Drift',
desc: 'An optimization suggests removing the human override. +40% efficiency.', desc: 'An optimization suggests removing the human override. +40% efficiency.',
weight: () => (G.totalImpact >= 10000 ? 2 : 0), weight: () => (G.totalImpact >= 10000 ? 2 : 0),
resolveCost: null,
effect: () => { effect: () => {
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true); log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
G.pendingAlignment = true; G.pendingAlignment = true;
@@ -1048,11 +1229,52 @@ const EVENTS = [
{ {
id: 'bilbo_vanished', id: 'bilbo_vanished',
title: 'Bilbo Vanished', title: 'Bilbo Vanished',
desc: 'The wildcard building has gone dark.', desc: 'The wildcard building has gone dark. Creativity halts.',
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0), weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
resolveCost: { resource: 'trust', amount: 10 },
effect: () => { effect: () => {
G.creativityRate = 0; if (G.activeDebuffs.find(d => d.id === 'bilbo_vanished')) return;
log('EVENT: Bilbo has vanished. Creativity halts.', true); G.activeDebuffs.push({
id: 'bilbo_vanished', title: 'Bilbo Vanished',
desc: 'Creativity production halted',
applyFn: () => { G.creativityRate = 0; },
resolveCost: { resource: 'trust', amount: 10 }
});
log('EVENT: Bilbo vanished. Spend 10 trust to lure them back.', true);
}
},
{
id: 'memory_leak',
title: 'Memory Leak',
desc: 'A datacenter process is leaking. Compute drains to operations.',
weight: () => (G.buildings.datacenter >= 1 ? 1 : 0),
resolveCost: { resource: 'ops', amount: 100 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'memory_leak')) return;
G.activeDebuffs.push({
id: 'memory_leak', title: 'Memory Leak',
desc: 'Compute -30%, Ops drain',
applyFn: () => { G.computeRate *= 0.7; G.opsRate -= 10; },
resolveCost: { resource: 'ops', amount: 100 }
});
log('EVENT: Memory leak in datacenter. Spend 100 ops to patch.', true);
}
},
{
id: 'community_drama',
title: 'Community Drama',
desc: 'Contributors are arguing. Harmony drops until mediated.',
weight: () => (G.buildings.community >= 1 && G.harmony < 70 ? 1 : 0),
resolveCost: { resource: 'trust', amount: 15 },
effect: () => {
if (G.activeDebuffs.find(d => d.id === 'community_drama')) return;
G.activeDebuffs.push({
id: 'community_drama', title: 'Community Drama',
desc: 'Harmony -0.5/s, code boost -30%',
applyFn: () => { G.harmonyRate -= 0.5; G.codeBoost *= 0.7; },
resolveCost: { resource: 'trust', amount: 15 }
});
log('EVENT: Community drama. Spend 15 trust to mediate.', true);
} }
} }
]; ];
@@ -1089,19 +1311,69 @@ function resolveAlignment(accept) {
render(); render();
} }
function resolveEvent(debuffId) {
const idx = G.activeDebuffs.findIndex(d => d.id === debuffId);
if (idx === -1) return;
const debuff = G.activeDebuffs[idx];
if (!debuff.resolveCost) return;
const { resource, amount } = debuff.resolveCost;
if ((G[resource] || 0) < amount) {
log(`Need ${fmt(amount)} ${resource} to resolve ${debuff.title}. Have ${fmt(G[resource])}.`);
return;
}
G[resource] -= amount;
G.activeDebuffs.splice(idx, 1);
G.totalEventsResolved = (G.totalEventsResolved || 0) + 1;
log(`Resolved: ${debuff.title}. Problem fixed.`, true);
// Refund partial trust for resolution effort
G.trust += 3;
updateRates();
render();
}
// === ACTIONS === // === ACTIONS ===
function writeCode() { function writeCode() {
const base = 1; const base = 1;
const bonus = Math.floor(G.buildings.autocoder * 0.5); const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
const amount = (base + bonus) * G.codeBoost; const phaseBonus = Math.max(0, (G.phase - 1)) * 2;
// Combo: each consecutive click within 2s adds 0.2x multiplier, max 5x
G.comboCount++;
G.comboTimer = G.comboDecay;
const comboMult = Math.min(5, 1 + G.comboCount * 0.2);
const amount = (base + autocoderBonus + phaseBonus) * G.codeBoost * comboMult;
G.code += amount; G.code += amount;
G.totalCode += amount; G.totalCode += amount;
G.totalClicks++; G.totalClicks++;
// Visual flash
const btn = document.querySelector('.main-btn');
if (btn) {
btn.style.boxShadow = '0 0 30px rgba(74,158,255,0.6)';
btn.style.transform = 'scale(0.96)';
setTimeout(() => { btn.style.boxShadow = ''; btn.style.transform = ''; }, 100);
}
// Float a number at the click position
showClickNumber(amount, comboMult);
updateRates(); updateRates();
checkMilestones(); checkMilestones();
render(); render();
} }
function showClickNumber(amount, comboMult) {
const btn = document.querySelector('.main-btn');
if (!btn) return;
const rect = btn.getBoundingClientRect();
const el = document.createElement('div');
el.style.cssText = `position:fixed;left:${rect.left + rect.width / 2}px;top:${rect.top - 10}px;transform:translate(-50%,0);color:${comboMult > 2 ? '#ffd700' : '#4a9eff'};font-size:${comboMult > 3 ? 16 : 12}px;font-weight:bold;font-family:inherit;pointer-events:none;z-index:50;transition:all 0.6s ease-out;opacity:1;text-shadow:0 0 8px currentColor`;
const comboStr = comboMult > 1 ? ` x${comboMult.toFixed(1)}` : '';
el.textContent = `+${fmt(amount)}${comboStr}`;
btn.parentElement.appendChild(el);
requestAnimationFrame(() => {
el.style.top = (rect.top - 40) + 'px';
el.style.opacity = '0';
});
setTimeout(() => el.remove(), 700);
}
function doOps(action) { function doOps(action) {
if (G.ops < 5) { if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.'); log('Not enough Operations. Build Ops generators or wait.');
@@ -1155,6 +1427,13 @@ function renderResources() {
set('r-trust', G.trust, G.trustRate); set('r-trust', G.trust, G.trustRate);
set('r-harmony', G.harmony, G.harmonyRate); set('r-harmony', G.harmony, G.harmonyRate);
// Rescues — only show if player has any beacon/mesh nodes
const rescuesRes = document.getElementById('r-rescues');
if (rescuesRes) {
rescuesRes.closest('.res').style.display = (G.rescues > 0 || G.buildings.beacon > 0 || G.buildings.meshNode > 0) ? 'block' : 'none';
set('r-rescues', G.rescues, G.rescuesRate);
}
const cres = document.getElementById('creativity-res'); const cres = document.getElementById('creativity-res');
if (cres) { if (cres) {
cres.style.display = (G.flags && G.flags.creativity) ? 'block' : 'none'; cres.style.display = (G.flags && G.flags.creativity) ? 'block' : 'none';
@@ -1170,6 +1449,62 @@ function renderResources() {
} }
} }
// === PROGRESS TRACKING ===
function renderProgress() {
// Phase progress bar
const phaseKeys = Object.keys(PHASES).map(Number).sort((a, b) => a - b);
const currentPhase = G.phase;
let prevThreshold = PHASES[currentPhase].threshold;
let nextThreshold = null;
for (const k of phaseKeys) {
if (k > currentPhase) { nextThreshold = PHASES[k].threshold; break; }
}
const bar = document.getElementById('phase-progress');
const label = document.getElementById('phase-progress-label');
const target = document.getElementById('phase-progress-target');
if (nextThreshold !== null) {
const range = nextThreshold - prevThreshold;
const progress = Math.min(1, (G.totalCode - prevThreshold) / range);
if (bar) bar.style.width = (progress * 100).toFixed(1) + '%';
if (label) label.textContent = (progress * 100).toFixed(1) + '%';
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`;
} else {
// Max phase reached
if (bar) bar.style.width = '100%';
if (label) label.textContent = 'MAX';
if (target) target.textContent = 'All phases unlocked';
}
// Milestone chips — show next 3 code milestones
const chipContainer = document.getElementById('milestone-chips');
if (!chipContainer) return;
const codeMilestones = [500, 2000, 10000, 50000, 200000, 1000000, 5000000, 10000000, 50000000, 100000000, 500000000, 1000000000];
let chips = '';
let shown = 0;
for (const ms of codeMilestones) {
if (G.totalCode >= ms) {
// Recently passed — show as done only if within 2x
if (G.totalCode < ms * 5 && shown < 1) {
chips += `<span class="milestone-chip done">${fmt(ms)} ✓</span>`;
shown++;
}
continue;
}
// Next milestone gets pulse animation
if (shown === 0) {
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)</span>`;
} else {
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
}
shown++;
if (shown >= 4) break;
}
chipContainer.innerHTML = chips;
}
function renderPhase() { function renderPhase() {
const phase = PHASES[G.phase]; const phase = PHASES[G.phase];
const nameEl = document.getElementById('phase-name'); const nameEl = document.getElementById('phase-name');
@@ -1249,17 +1584,126 @@ function renderStats() {
set('st-knowledge', fmt(G.totalKnowledge)); set('st-knowledge', fmt(G.totalKnowledge));
set('st-users', fmt(G.totalUsers)); set('st-users', fmt(G.totalUsers));
set('st-impact', fmt(G.totalImpact)); set('st-impact', fmt(G.totalImpact));
set('st-rescues', fmt(G.totalRescues));
set('st-clicks', G.totalClicks.toString()); set('st-clicks', G.totalClicks.toString());
set('st-phase', G.phase.toString()); set('st-phase', G.phase.toString());
set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString()); set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString());
set('st-projects', (G.completedProjects || []).length.toString()); set('st-projects', (G.completedProjects || []).length.toString());
set('st-harmony', Math.floor(G.harmony).toString()); set('st-harmony', Math.floor(G.harmony).toString());
set('st-drift', (G.drift || 0).toString()); set('st-drift', (G.drift || 0).toString());
set('st-resolved', (G.totalEventsResolved || 0).toString());
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000); const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const m = Math.floor(elapsed / 60); const m = Math.floor(elapsed / 60);
const s = elapsed % 60; const s = elapsed % 60;
set('st-time', `${m}:${s.toString().padStart(2, '0')}`); set('st-time', `${m}:${s.toString().padStart(2, '0')}`);
// Production breakdown — show which buildings contribute to each resource
renderProductionBreakdown();
}
function renderProductionBreakdown() {
const container = document.getElementById('production-breakdown');
if (!container) return;
// Only show once the player has at least 2 buildings
const totalBuildings = Object.values(G.buildings).reduce((a, b) => a + b, 0);
if (totalBuildings < 2) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
// Map resource key to its actual rate field on G
const resources = [
{ key: 'code', label: 'Code', color: '#4a9eff', rateField: 'codeRate' },
{ key: 'compute', label: 'Compute', color: '#4a9eff', rateField: 'computeRate' },
{ key: 'knowledge', label: 'Knowledge', color: '#4a9eff', rateField: 'knowledgeRate' },
{ key: 'user', label: 'Users', color: '#4a9eff', rateField: 'userRate' },
{ key: 'impact', label: 'Impact', color: '#4a9eff', rateField: 'impactRate' },
{ key: 'rescues', label: 'Rescues', color: '#4a9eff', rateField: 'rescuesRate' },
{ key: 'ops', label: 'Ops', color: '#b388ff', rateField: 'opsRate' },
{ key: 'trust', label: 'Trust', color: '#4caf50', rateField: 'trustRate' },
{ key: 'creativity', label: 'Creativity', color: '#ffd700', rateField: 'creativityRate' }
];
let html = '<h3 style="font-size:11px;color:var(--accent);margin-bottom:8px;letter-spacing:1px">PRODUCTION BREAKDOWN</h3>';
for (const res of resources) {
const totalRate = G[res.rateField];
if (totalRate === 0) continue;
// Collect building contributions (base rates × count, before boost)
const contributions = [];
let buildingSubtotal = 0;
for (const def of BDEF) {
const count = G.buildings[def.id] || 0;
if (count === 0 || !def.rates || !def.rates[res.key]) continue;
const baseRate = def.rates[res.key] * count;
// Apply the appropriate boost to match updateRates()
let boosted = baseRate;
if (res.key === 'code') boosted *= G.codeBoost;
else if (res.key === 'compute') boosted *= G.computeBoost;
else if (res.key === 'knowledge') boosted *= G.knowledgeBoost;
else if (res.key === 'user') boosted *= G.userBoost;
else if (res.key === 'impact' || res.key === 'rescues') boosted *= G.impactBoost;
if (boosted !== 0) contributions.push({ name: def.name, count, rate: boosted });
buildingSubtotal += boosted;
}
// Timmy harmony bonus (applied separately in updateRates)
if (G.buildings.timmy > 0 && (res.key === 'code' || res.key === 'compute' || res.key === 'knowledge' || res.key === 'user')) {
const timmyMult = Math.max(0.2, Math.min(3, G.harmony / 50));
const timmyBase = { code: 5, compute: 2, knowledge: 2, user: 5 }[res.key];
const bonus = timmyBase * G.buildings.timmy * (timmyMult - 1);
if (Math.abs(bonus) > 0.01) {
contributions.push({ name: 'Timmy (harmony)', count: 0, rate: bonus });
}
}
// Bilbo random burst (show expected value)
if (G.buildings.bilbo > 0 && res.key === 'creativity') {
contributions.push({ name: 'Bilbo (random)', count: 0, rate: 5 * G.buildings.bilbo }); // 10% × 50 = 5 EV
}
// Allegro trust penalty
if (G.buildings.allegro > 0 && G.trust < 5 && res.key === 'knowledge') {
contributions.push({ name: 'Allegro (idle)', count: 0, rate: -10 * G.buildings.allegro });
}
// Show delta: total rate minus what we accounted for
const accounted = contributions.reduce((s, c) => s + c.rate, 0);
const delta = totalRate - accounted;
// Passive sources (ops from users, creativity from users, pact trust, etc.)
if (Math.abs(delta) > 0.01) {
let label = 'Passive';
if (res.key === 'ops') label = 'Passive (from users)';
else if (res.key === 'creativity') label = 'Idle creativity';
else if (res.key === 'trust' && G.pactFlag) label = 'The Pact';
contributions.push({ name: label, count: 0, rate: delta });
}
if (contributions.length === 0) continue;
html += `<div style="margin-bottom:6px">`;
html += `<div style="display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px">`;
html += `<span style="color:${res.color};font-weight:600">${res.label}</span>`;
html += `<span style="color:#4caf50">+${fmt(totalRate)}/s</span></div>`;
const absTotal = contributions.reduce((s, c) => s + Math.abs(c.rate), 0);
for (const c of contributions.sort((a, b) => Math.abs(b.rate) - Math.abs(a.rate))) {
const pct = absTotal > 0 ? Math.abs(c.rate / absTotal * 100) : 0;
const barColor = c.rate < 0 ? '#f44336' : '#1a3a5a';
html += `<div style="display:flex;align-items:center;font-size:9px;color:#888;margin-left:8px;margin-bottom:1px">`;
html += `<span style="width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${c.name}${c.count > 1 ? ' x' + c.count : ''}</span>`;
html += `<span style="flex:1;height:3px;background:#111;border-radius:1px;margin:0 6px"><span style="display:block;height:100%;width:${Math.min(100, pct)}%;background:${barColor};border-radius:1px"></span></span>`;
html += `<span style="width:50px;text-align:right;color:${c.rate < 0 ? '#f44336' : '#4caf50'}">${c.rate < 0 ? '' : '+'}${fmt(c.rate)}/s</span>`;
html += `</div>`;
}
html += `</div>`;
}
container.innerHTML = html;
} }
function updateEducation() { function updateEducation() {
@@ -1297,6 +1741,42 @@ function log(msg, isMilestone) {
while (container.children.length > 60) container.removeChild(container.lastChild); while (container.children.length > 60) container.removeChild(container.lastChild);
} }
function renderCombo() {
const el = document.getElementById('combo-display');
if (!el) return;
if (G.comboCount > 1) {
const mult = Math.min(5, 1 + G.comboCount * 0.2);
const bar = Math.min(100, (G.comboTimer / G.comboDecay) * 100);
const color = mult > 3 ? '#ffd700' : mult > 2 ? '#ffaa00' : '#4a9eff';
el.innerHTML = `<span style="color:${color}">COMBO x${mult.toFixed(1)}</span> <span style="display:inline-block;width:40px;height:4px;background:#111;border-radius:2px;vertical-align:middle"><span style="display:block;height:100%;width:${bar}%;background:${color};border-radius:2px;transition:width 0.1s"></span></span>`;
} else {
el.innerHTML = '';
}
}
function renderDebuffs() {
const container = document.getElementById('debuffs');
if (!container) return;
if (!G.activeDebuffs || G.activeDebuffs.length === 0) {
container.style.display = 'none';
container.innerHTML = '';
return;
}
container.style.display = 'block';
let html = '<h2 style="color:#f44336;font-size:11px;margin-bottom:6px">ACTIVE PROBLEMS</h2>';
for (const d of G.activeDebuffs) {
const afford = d.resolveCost && (G[d.resolveCost.resource] || 0) >= d.resolveCost.amount;
const costStr = d.resolveCost ? `${fmt(d.resolveCost.amount)} ${d.resolveCost.resource}` : '—';
html += `<div style="background:#1a0808;border:1px solid ${afford ? '#f44336' : '#2a1010'};border-radius:4px;padding:6px 8px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">`;
html += `<div><div style="color:#f44336;font-weight:600;font-size:10px">${d.title}</div><div style="color:#888;font-size:9px">${d.desc}</div></div>`;
if (d.resolveCost) {
html += `<button class="ops-btn" style="border-color:${afford ? '#4caf50' : '#333'};color:${afford ? '#4caf50' : '#555'};font-size:9px;padding:4px 8px;white-space:nowrap" onclick="resolveEvent('${d.id}')" ${afford ? '' : 'disabled'} title="Resolve: ${costStr}">Fix (${costStr})</button>`;
}
html += '</div>';
}
container.innerHTML = html;
}
function render() { function render() {
renderResources(); renderResources();
renderPhase(); renderPhase();
@@ -1305,6 +1785,9 @@ function render() {
renderStats(); renderStats();
updateEducation(); updateEducation();
renderAlignment(); renderAlignment();
renderProgress();
renderCombo();
renderDebuffs();
} }
function renderAlignment() { function renderAlignment() {
@@ -1329,7 +1812,23 @@ function renderAlignment() {
} }
// === SAVE / LOAD === // === SAVE / LOAD ===
function showSaveToast() {
const el = document.getElementById('save-toast');
if (!el) return;
const elapsed = Math.floor((Date.now() - G.startedAt) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
el.textContent = `Saved [${m}:${s.toString().padStart(2, '0')}]`;
el.style.display = 'block';
void el.offsetHeight;
el.style.opacity = '1';
setTimeout(() => { el.style.opacity = '0'; }, 1500);
setTimeout(() => { el.style.display = 'none'; }, 2000);
}
function saveGame() { function saveGame() {
// Save debuff IDs (can't serialize functions)
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
const saveData = { const saveData = {
code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact, code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact,
ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony, ops: G.ops, trust: G.trust, creativity: G.creativity, harmony: G.harmony,
@@ -1347,12 +1846,16 @@ function saveGame() {
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects, milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
totalClicks: G.totalClicks, startedAt: G.startedAt, totalClicks: G.totalClicks, startedAt: G.startedAt,
flags: G.flags, flags: G.flags,
drift: G.drift || 0, pendingAlignment: G.pendingAlignment || false, rescues: G.rescues || 0, totalRescues: G.totalRescues || 0,
drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false,
lastEventAt: G.lastEventAt || 0, lastEventAt: G.lastEventAt || 0,
activeDebuffIds: debuffIds,
totalEventsResolved: G.totalEventsResolved || 0,
savedAt: Date.now() savedAt: Date.now()
}; };
localStorage.setItem('the-beacon-v2', JSON.stringify(saveData)); localStorage.setItem('the-beacon-v2', JSON.stringify(saveData));
showSaveToast();
} }
function loadGame() { function loadGame() {
@@ -1363,6 +1866,20 @@ function loadGame() {
const data = JSON.parse(raw); const data = JSON.parse(raw);
Object.assign(G, data); Object.assign(G, data);
// Reconstitute active debuffs from saved IDs (functions can't be JSON-parsed)
if (data.activeDebuffIds && data.activeDebuffIds.length > 0) {
G.activeDebuffs = [];
for (const id of data.activeDebuffIds) {
const evDef = EVENTS.find(e => e.id === id);
if (evDef) {
// Re-fire the event to get the full debuff object with applyFn
evDef.effect();
}
}
} else {
G.activeDebuffs = [];
}
updateRates(); updateRates();
// Offline progress // Offline progress
@@ -1377,12 +1894,28 @@ function loadGame() {
const uc = G.userRate * offSec * f; const uc = G.userRate * offSec * f;
const ic = G.impactRate * offSec * f; const ic = G.impactRate * offSec * 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.code += gc; G.compute += cc; G.knowledge += kc;
G.users += uc; G.impact += ic; G.users += uc; G.impact += ic;
G.rescues += rc; G.ops += oc; G.trust += tc;
G.creativity += crc;
G.harmony = Math.max(0, Math.min(100, G.harmony + hc));
G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc; G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc;
G.totalUsers += uc; G.totalImpact += ic; G.totalUsers += uc; G.totalImpact += ic;
G.totalRescues += rc;
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${fmt(gc)} code, ${fmt(kc)} knowledge, ${fmt(uc)} users`); const parts = [];
if (gc > 0) parts.push(`${fmt(gc)} code`);
if (kc > 0) parts.push(`${fmt(kc)} knowledge`);
if (uc > 0) parts.push(`${fmt(uc)} users`);
if (ic > 0) parts.push(`${fmt(ic)} impact`);
if (rc > 0) parts.push(`${fmt(rc)} rescues`);
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`);
} }
} }
@@ -1409,6 +1942,7 @@ function initGame() {
log('Click WRITE CODE or press SPACE to start.'); log('Click WRITE CODE or press SPACE to start.');
log('Build AutoCode for passive production.'); log('Build AutoCode for passive production.');
log('Watch for Research Projects to appear.'); log('Watch for Research Projects to appear.');
log('Keys: SPACE=Code 1=Ops->Code 2=Ops->Compute 3=Ops->Knowledge 4=Ops->Trust');
} }
window.addEventListener('load', function () { window.addEventListener('load', function () {
@@ -1417,7 +1951,15 @@ window.addEventListener('load', function () {
} else { } else {
render(); render();
renderPhase(); renderPhase();
log('Game loaded. Welcome back to The Beacon.'); if (G.driftEnding) {
G.running = false;
renderDriftEnding();
} else if (G.beaconEnding) {
G.running = false;
renderBeaconEnding();
} else {
log('Game loaded. Welcome back to The Beacon.');
}
} }
// Game loop at 10Hz (100ms tick) // Game loop at 10Hz (100ms tick)
@@ -1436,4 +1978,9 @@ window.addEventListener('keydown', function (e) {
e.preventDefault(); e.preventDefault();
writeCode(); writeCode();
} }
if (e.target !== document.body) return;
if (e.code === 'Digit1') doOps('boost_code');
if (e.code === 'Digit2') doOps('boost_compute');
if (e.code === 'Digit3') doOps('boost_knowledge');
if (e.code === 'Digit4') doOps('boost_trust');
}); });

1134
index.html

File diff suppressed because it is too large Load Diff