Compare commits

..

8 Commits

Author SHA1 Message Date
Alexander Whitestone
f362f30fd3 beacon: fix offline progress to award rescues, ops, and trust
Offline gains were missing rescues, ops, and trust — players with
beacon/mesh nodes lost half their progress on return. Now all active
resources are calculated at 50% offline efficiency.

Also includes event system overhaul: active events with durations,
resolve costs, auto-expiry, event UI rendering, and save/load support.
2026-04-10 01:21:05 -04:00
Alexander Whitestone
f97154c37a beacon: add unlock notification toasts for new buildings and projects
Players now get animated toast notifications (top-right) when:
- A new building becomes available to buy
- A new research project becomes available

Toasts are color-coded: blue for buildings, gold for projects.
Seen state is tracked and persisted in saves so toasts don't repeat.

This was the biggest UX gap in the game -- without notifications,
players had to manually scan the building/project lists to notice
new unlocks. Critical for idle games where pacing matters.
2026-04-10 00:25:40 -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
2 changed files with 378 additions and 1224 deletions

484
game.js
View File

@@ -11,6 +11,7 @@ const G = {
knowledge: 0,
users: 0,
impact: 0,
rescues: 0,
ops: 5,
trust: 5,
creativity: 0,
@@ -22,6 +23,7 @@ const G = {
totalKnowledge: 0,
totalUsers: 0,
totalImpact: 0,
totalRescues: 0,
// Rates (calculated each tick)
codeRate: 0,
@@ -29,6 +31,7 @@ const G = {
knowledgeRate: 0,
userRate: 0,
impactRate: 0,
rescuesRate: 0,
opsRate: 0,
trustRate: 0,
creativityRate: 0,
@@ -64,7 +67,6 @@ const G = {
knowledgeBoost: 1,
userBoost: 1,
impactBoost: 1,
creativityBoost: 1,
// Phase flags (mirroring Paperclips' milestoneFlag/compFlag/humanFlag system)
milestoneFlag: 0,
@@ -95,6 +97,7 @@ const G = {
maxKnowledge: 0,
maxUsers: 0,
maxImpact: 0,
maxRescues: 0,
maxTrust: 5,
maxOps: 5,
maxHarmony: 50,
@@ -103,6 +106,7 @@ const G = {
drift: 0,
lastEventAt: 0,
eventCooldown: 0,
activeEvents: [], // {id, expiresAt} — events auto-resolve after duration
// Time tracking
playTime: 0,
@@ -230,7 +234,7 @@ const BDEF = [
id: 'beacon', name: 'Beacon Node',
desc: 'Always on. Always listening. Always looking for someone in the dark.',
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,
edu: 'The Beacon exists because one person in the dark needs one thing: proof they are not alone.'
},
@@ -238,7 +242,7 @@ const BDEF = [
id: 'meshNode', name: 'Mesh Network Node',
desc: 'Peer-to-peer. No single point of failure. Unstoppable.',
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,
edu: 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.'
},
@@ -296,45 +300,6 @@ const BDEF = [
// === PROJECT DEFINITIONS (following Paperclips' pattern exactly) ===
// Each project: id, name, desc, trigger(), resource cost, effect(), phase, edu
const PDEFS = [
// === CREATIVE-TO-OPS CONVERSION (Creativity Engineering) ===
{
id: 'p_creativity_engineering',
name: 'Creativity Engineering',
desc: 'Channel creative sparks into operational fuel. Converts Creativity into Ops.',
cost: { ops: 2000, creativity: 50 },
trigger: () => G.flags && G.flags.creativity && G.creativity >= 20,
effect: () => {
G.flags.creativityEngineering = true;
G.creativityBoost = (G.creativityBoost || 1) + 0.5;
log('Creativity Engineering online. Ideas become infrastructure.', true);
},
milestone: true
},
{
id: 'p_creative_catalyst',
name: 'Creative Catalyst',
desc: 'Boosts conversion efficiency. Each creative idea produces 50% more Ops.',
cost: { ops: 5000, creativity: 200, knowledge: 3000 },
trigger: () => G.flags && G.flags.creativityEngineering && G.creativity >= 100,
effect: () => {
G.creativityBoost = (G.creativityBoost || 1) + 0.5;
log('Creative Catalyst installed. The pipeline flows faster.');
}
},
{
id: 'p_inspiration_engine',
name: 'Inspiration Engine',
desc: 'Automated creative synthesis. Creativity now generates passively from fleet harmony.',
cost: { ops: 15000, creativity: 500, knowledge: 10000, trust: 25 },
trigger: () => G.flags && G.flags.creativityEngineering && G.creativity >= 300 && G.harmony >= 40,
effect: () => {
G.flags.inspirationEngine = true;
G.creativityRate += 2;
log('Inspiration Engine active. The fleet dreams together.', true);
},
milestone: true
},
// PHASE 1: Manual -> Automation
{
id: 'p_improved_autocoder',
@@ -608,6 +573,19 @@ const PDEFS = [
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',
name: 'The Pact',
@@ -658,8 +636,7 @@ const EDU_FACTS = [
{ title: "Recursive Self-Improvement", text: "The system that improves itself must improve toward good. This is both the dream and the danger.", phase: 5 },
{ title: "The Beacon", text: "The Beacon exists because one person in the dark needs one thing: proof they are not alone. It costs nothing to be there.", phase: 6 },
{ title: "Mesh Network", text: "Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal. No single point of failure.", phase: 6 },
{ title: "The Value of a Life", text: "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page.", phase: 6 },
{ title: "Creativity Engineering", text: "Creative output is not waste — it is raw potential. The art of creativity engineering is channeling sparks of insight into operational infrastructure. An idea that cannot be executed is a dream. An operation without ideas is a machine.", phase: 3 }
{ title: "The Value of a Life", text: "An AI must never compute the value of a human life. Every person alive is alive by mercy. If God has not ended the story, no algorithm gets to write the last page.", phase: 6 }
];
// === UTILITY FUNCTIONS ===
@@ -726,6 +703,9 @@ function fmt(n) {
if (n < 0) return '-' + fmt(-n);
if (n < 1000) return Math.floor(n).toLocaleString();
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);
const abbrev = NUMBER_ABBREVS[scale];
return (n / Math.pow(10, scale * 3)).toFixed(1) + abbrev;
@@ -763,7 +743,41 @@ function spellf(n) {
// For very large numbers beyond our lookup table, fall back
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 parts = [];
@@ -775,7 +789,7 @@ function spellf(n) {
if (chunk > 0 && chunk < 1000) {
parts.push(spellSmall(chunk) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
} 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] : ''));
}
}
@@ -825,7 +839,7 @@ function spendProject(project) {
function updateRates() {
// Reset all rates
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;
// Apply building rates
@@ -838,6 +852,7 @@ function updateRates() {
else if (resource === 'knowledge') G.knowledgeRate += baseRate * count * G.knowledgeBoost;
else if (resource === 'user') G.userRate += baseRate * count * G.userBoost;
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 === 'trust') G.trustRate += baseRate * count;
else if (resource === 'creativity') G.creativityRate += baseRate * count;
@@ -850,11 +865,8 @@ function updateRates() {
if (G.flags && G.flags.creativity) {
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
}
// Inspiration Engine: harmony boosts passive creativity
if (G.flags && G.flags.inspirationEngine) {
G.creativityRate += (G.harmony / 50) * (G.buildings.bilbo || 1);
}
if (G.pactFlag) G.trustRate += 2;
if (G.branchProtectionFlag) G.trustRate += 3; // branch protection actively builds trust
// Harmony: each wizard building contributes or detracts
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
@@ -900,12 +912,16 @@ function updateRates() {
function tick() {
const dt = 1 / 10; // 100ms tick
// If game has ended (drift ending), stop ticking
if (!G.running) return;
// Apply production
G.code += G.codeRate * dt;
G.compute += G.computeRate * dt;
G.knowledge += G.knowledgeRate * dt;
G.users += G.userRate * dt;
G.impact += G.impactRate * dt;
G.rescues += G.rescuesRate * dt;
G.ops += G.opsRate * dt;
G.trust += G.trustRate * dt;
G.creativity += G.creativityRate * dt;
@@ -918,6 +934,7 @@ function tick() {
G.totalKnowledge += G.knowledgeRate * dt;
G.totalUsers += G.userRate * dt;
G.totalImpact += G.impactRate * dt;
G.totalRescues += G.rescuesRate * dt;
// Track maxes
G.maxCode = Math.max(G.maxCode, G.code);
@@ -925,6 +942,7 @@ function tick() {
G.maxKnowledge = Math.max(G.maxKnowledge, G.knowledge);
G.maxUsers = Math.max(G.maxUsers, G.users);
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.maxOps = Math.max(G.maxOps, G.ops);
G.maxHarmony = Math.max(G.maxHarmony, G.harmony);
@@ -950,6 +968,34 @@ function tick() {
G.lastEventAt = G.tick;
}
// Check event expiry
checkEventExpiry();
// Re-apply active event rate penalties (updateRates rebuilds from scratch)
for (const ae of G.activeEvents) {
switch (ae.id) {
case 'runner_stuck': G.codeRate *= 0.5; break;
case 'ezra_offline': G.userRate *= 0.3; break;
case 'unreviewed_merge': /* trust penalty is one-shot */ break;
case 'api_rate_limit': G.computeRate *= 0.5; break;
case 'bilbo_vanished': G.creativityRate = 0; break;
}
}
// 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
if (Math.floor(G.tick * 10) % 2 === 0) {
render();
@@ -1033,51 +1079,99 @@ function buyProject(id) {
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 ===
const EVENTS = [
{
id: 'runner_stuck',
title: 'CI Runner Stuck',
desc: 'The forge pipeline has halted. Production slows until restarted.',
desc: 'The forge pipeline has halted. Code production slowed.',
weight: () => (G.ciFlag === 1 ? 2 : 0),
duration: 45, // seconds
resolveCost: { ops: 10 },
effect: () => {
G.codeRate *= 0.5;
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true);
log('EVENT: CI runner stuck. Click to spend 10 Ops to clear, or wait ~45s.', true);
}
},
{
id: 'ezra_offline',
title: 'Ezra is Offline',
desc: 'The herald channel is silent. User growth stalls.',
desc: 'The herald channel is silent. User growth stalled.',
weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
duration: 60,
resolveCost: { ops: 5, trust: 2 },
effect: () => {
G.userRate *= 0.3;
log('EVENT: Ezra offline. Dispatch required.', true);
log('EVENT: Ezra offline. Click to dispatch, or wait ~60s.', true);
}
},
{
id: 'unreviewed_merge',
title: 'Unreviewed Merge',
desc: 'A change went in without eyes. Trust erodes.',
weight: () => (G.deployFlag === 1 ? 3 : 0),
weight: () => (G.deployFlag === 1 && G.branchProtectionFlag !== 1 ? 3 : 0),
duration: 30,
resolveCost: { ops: 8 },
effect: () => {
if (G.branchProtectionFlag === 1) {
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true);
G.trust += 2;
} else {
G.trust = Math.max(0, G.trust - 10);
log('EVENT: Unreviewed merge detected. Trust lost.', true);
}
G.trust = Math.max(0, G.trust - 10);
log('EVENT: Unreviewed merge detected. Trust lost. Click to revert.', true);
}
},
{
id: 'api_rate_limit',
title: 'API Rate Limit',
desc: 'External compute provider throttled.',
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
weight: () => (G.totalCompute >= 1000 && G.sovereignFlag !== 1 ? 2 : 0),
duration: 40,
resolveCost: { compute: 200 },
effect: () => {
G.computeRate *= 0.5;
log('EVENT: API rate limit hit. Local compute insufficient.', true);
log('EVENT: API rate limit. Click to provision local compute, or wait ~40s.', true);
}
},
{
@@ -1085,6 +1179,7 @@ const EVENTS = [
title: 'The Drift',
desc: 'An optimization suggests removing the human override. +40% efficiency.',
weight: () => (G.totalImpact >= 10000 ? 2 : 0),
duration: 0, // alignment events don't auto-resolve
effect: () => {
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
G.pendingAlignment = true;
@@ -1095,9 +1190,11 @@ const EVENTS = [
title: 'Bilbo Vanished',
desc: 'The wildcard building has gone dark.',
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
duration: 50,
resolveCost: { trust: 3 },
effect: () => {
G.creativityRate = 0;
log('EVENT: Bilbo has vanished. Creativity halts.', true);
log('EVENT: Bilbo vanished. Click to search, or wait ~50s.', true);
}
}
];
@@ -1111,12 +1208,53 @@ function triggerEvent() {
for (const ev of available) {
roll -= ev.weight();
if (roll <= 0) {
// Don't fire duplicate active events
if (G.activeEvents.some(ae => ae.id === ev.id)) return;
ev.effect();
if (ev.duration > 0) {
G.activeEvents.push({ id: ev.id, expiresAt: G.tick + ev.duration });
}
return;
}
}
}
function checkEventExpiry() {
for (let i = G.activeEvents.length - 1; i >= 0; i--) {
if (G.tick >= G.activeEvents[i].expiresAt) {
const ae = G.activeEvents[i];
G.activeEvents.splice(i, 1);
updateRates(); // recalculate without the penalty
const evDef = EVENTS.find(e => e.id === ae.id);
log(`Event resolved: ${evDef ? evDef.title : ae.id}`, true);
}
}
}
function hasActiveEvent(id) {
return G.activeEvents.some(ae => ae.id === id);
}
function resolveEvent(id) {
const evDef = EVENTS.find(e => e.id === id);
if (!evDef || !evDef.resolveCost) return;
if (!hasActiveEvent(id)) return;
if (!canAffordProject(evDef)) {
log('Not enough resources to resolve this event.');
return;
}
spendProject(evDef);
G.activeEvents = G.activeEvents.filter(ae => ae.id !== id);
updateRates();
// Small bonus for manually resolving
G.trust += 2;
log(`Resolved: ${evDef.title}. Trust +2.`, true);
render();
}
function resolveAlignment(accept) {
if (!G.pendingAlignment) return;
if (accept) {
@@ -1182,63 +1320,6 @@ function doOps(action) {
render();
}
// === CREATIVE-TO-OPS CONVERSION ===
function convertCreativity() {
if (!G.flags || !G.flags.creativityEngineering) {
log('Unlock Creativity Engineering first.');
return;
}
if (G.creativity < 10) {
log('Not enough Creativity. Need at least 10.');
return;
}
const boost = G.creativityBoost || 1;
const spent = Math.min(G.creativity, 100);
const opsGain = spent * 10 * boost;
const knowledgeGain = spent * 2 * (G.knowledgeBoost || 1);
G.creativity -= spent;
G.ops += opsGain;
G.knowledge += knowledgeGain;
G.totalKnowledge += knowledgeGain;
// Harmony bonus: high harmony improves conversion
if (G.harmony >= 60) {
const harmonyBonus = Math.floor(G.harmony / 20);
G.ops += harmonyBonus * spent;
log(`Creative -> Ops: +${fmt(opsGain)} ops, +${fmt(knowledgeGain)} knowledge (+${harmonyBonus * spent} harmony bonus)`);
} else {
log(`Creative -> Ops: +${fmt(opsGain)} ops, +${fmt(knowledgeGain)} knowledge`);
}
updateRates();
render();
}
function convertOpsToCreativity() {
if (!G.flags || !G.flags.creativityEngineering) {
log('Unlock Creativity Engineering first.');
return;
}
if (G.ops < 50) {
log('Not enough Ops. Need at least 50.');
return;
}
const boost = G.creativityBoost || 1;
const spent = Math.min(G.ops, 500);
const creativityGain = (spent / 50) * 5 * boost;
G.ops -= spent;
G.creativity += creativityGain;
log(`Ops -> Creative: +${fmt(creativityGain)} creativity from ${fmt(spent)} ops`);
updateRates();
render();
}
// === RENDERING ===
function renderResources() {
const set = (id, val, rate) => {
@@ -1257,6 +1338,13 @@ function renderResources() {
set('r-trust', G.trust, G.trustRate);
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');
if (cres) {
cres.style.display = (G.flags && G.flags.creativity) ? 'block' : 'none';
@@ -1351,6 +1439,7 @@ function renderStats() {
set('st-knowledge', fmt(G.totalKnowledge));
set('st-users', fmt(G.totalUsers));
set('st-impact', fmt(G.totalImpact));
set('st-rescues', fmt(G.totalRescues));
set('st-clicks', G.totalClicks.toString());
set('st-phase', G.phase.toString());
set('st-buildings', Object.values(G.buildings).reduce((a, b) => a + b, 0).toString());
@@ -1406,18 +1495,9 @@ function render() {
renderProjects();
renderStats();
updateEducation();
renderCreativityOps();
renderAlignment();
}
function renderCreativityOps() {
const container = document.getElementById('creativity-ops-ui');
if (!container) return;
if (G.flags && G.flags.creativityEngineering) {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
renderActiveEvents();
checkUnlocks();
}
function renderAlignment() {
@@ -1441,7 +1521,112 @@ function renderAlignment() {
}
}
function renderActiveEvents() {
const container = document.getElementById('events-ui');
if (!container) return;
if (G.activeEvents.length === 0) {
container.innerHTML = '';
container.style.display = 'none';
return;
}
let html = '';
for (const ae of G.activeEvents) {
const evDef = EVENTS.find(e => e.id === ae.id);
if (!evDef) continue;
const remaining = Math.max(0, Math.ceil(ae.expiresAt - G.tick));
const costStr = evDef.resolveCost ? Object.entries(evDef.resolveCost).map(([r, a]) => `${a} ${r}`).join(', ') : '';
const canResolve = evDef.resolveCost && canAffordProject(evDef);
html += `<div style="background:#1a1008;border:1px solid #ff8800;padding:8px;border-radius:4px;margin-bottom:6px">`;
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
html += `<span style="color:#ff8800;font-weight:bold;font-size:10px">${evDef.title}</span>`;
html += `<span style="color:#666;font-size:9px">${remaining}s</span>`;
html += `</div>`;
html += `<div style="font-size:9px;color:#888;margin:3px 0">${evDef.desc}</div>`;
if (evDef.resolveCost) {
html += `<button class="ops-btn" onclick="resolveEvent('${evDef.id}')" ${canResolve ? '' : 'disabled'} style="font-size:9px;padding:3px 8px;margin-top:2px;border-color:#ff8800;color:#ff8800">Resolve (${costStr})</button>`;
}
html += `</div>`;
}
container.innerHTML = html;
container.style.display = 'block';
}
// === UNLOCK NOTIFICATIONS ===
function showUnlockToast(type, name) {
const container = document.getElementById('unlock-toast');
if (!container) return;
const el = document.createElement('div');
el.className = `unlock-toast-item ${type}`;
const icon = type === 'building' ? 'BUILDING' : type === 'project' ? 'RESEARCH' : 'MILESTONE';
el.innerHTML = `<span style="font-weight:600">${icon}:</span> ${name}`;
container.appendChild(el);
// Trigger reflow, then show
void el.offsetHeight;
el.classList.add('show');
// Auto-remove after 4 seconds
setTimeout(() => {
el.classList.remove('show');
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 400);
}, 4000);
}
// Track what the player has already seen (so we don't re-notify)
function ensureSeenSets() {
if (!G._seenBuildings) G._seenBuildings = [];
if (!G._seenProjects) G._seenProjects = [];
}
function checkUnlocks() {
ensureSeenSets();
// Check for newly visible buildings
for (const def of BDEF) {
if (!def.unlock()) continue;
if (def.phase > G.phase + 1) continue;
if (G._seenBuildings.includes(def.id)) continue;
G._seenBuildings.push(def.id);
// Don't notify on the very first building (autocoder) — player just started
if (G.totalCode > 10) {
showUnlockToast('building', def.name);
}
}
// Check for newly available projects
if (G.activeProjects) {
for (const id of G.activeProjects) {
if (G._seenProjects.includes(id)) continue;
G._seenProjects.push(id);
const pDef = PDEFS.find(p => p.id === id);
if (pDef) {
showUnlockToast('project', pDef.name);
}
}
}
}
// === 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() {
const saveData = {
code: G.code, compute: G.compute, knowledge: G.knowledge, users: G.users, impact: G.impact,
@@ -1450,7 +1635,7 @@ function saveGame() {
totalUsers: G.totalUsers, totalImpact: G.totalImpact,
buildings: G.buildings,
codeBoost: G.codeBoost, computeBoost: G.computeBoost, knowledgeBoost: G.knowledgeBoost,
userBoost: G.userBoost, impactBoost: G.impactBoost, creativityBoost: G.creativityBoost || 1,
userBoost: G.userBoost, impactBoost: G.impactBoost,
milestoneFlag: G.milestoneFlag, phase: G.phase,
deployFlag: G.deployFlag, sovereignFlag: G.sovereignFlag, beaconFlag: G.beaconFlag,
memoryFlag: G.memoryFlag, pactFlag: G.pactFlag,
@@ -1460,12 +1645,17 @@ function saveGame() {
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
totalClicks: G.totalClicks, startedAt: G.startedAt,
flags: G.flags,
drift: G.drift || 0, pendingAlignment: G.pendingAlignment || false,
_seenBuildings: G._seenBuildings || [],
_seenProjects: G._seenProjects || [],
activeEvents: G.activeEvents || [],
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,
savedAt: Date.now()
};
localStorage.setItem('the-beacon-v2', JSON.stringify(saveData));
showSaveToast();
}
function loadGame() {
@@ -1490,12 +1680,21 @@ function loadGame() {
const uc = G.userRate * 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;
G.code += gc; G.compute += cc; G.knowledge += kc;
G.users += uc; G.impact += ic;
G.rescues += rc; G.ops += oc; G.trust += tc;
G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc;
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 = [`${fmt(gc)} code`, `${fmt(kc)} knowledge`, `${fmt(uc)} users`];
if (rc > 0.1) parts.push(`${fmt(rc)} rescues`);
if (oc > 0.1) parts.push(`${fmt(oc)} ops`);
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`);
}
}
@@ -1514,7 +1713,6 @@ function initGame() {
G.deployFlag = 0;
G.sovereignFlag = 0;
G.beaconFlag = 0;
G.creativityBoost = 1;
updateRates();
render();
renderPhase();
@@ -1531,7 +1729,15 @@ window.addEventListener('load', function () {
} else {
render();
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)

1118
index.html

File diff suppressed because it is too large Load Diff