Compare commits

..

9 Commits

Author SHA1 Message Date
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
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 331 additions and 1211 deletions

421
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,
@@ -104,6 +107,11 @@ const G = {
lastEventAt: 0,
eventCooldown: 0,
// Combo system
comboCount: 0,
comboTimer: 0,
comboDecay: 2.0, // seconds before combo resets
// Time tracking
playTime: 0,
startTime: 0
@@ -230,7 +238,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 +246,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 +304,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 +577,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 +640,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 +707,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 +747,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 +793,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 +843,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 +856,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,10 +869,6 @@ 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;
// Harmony: each wizard building contributes or detracts
@@ -900,12 +915,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 +937,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 +945,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);
@@ -936,6 +957,15 @@ function tick() {
G.tick += dt;
// Combo decay
if (G.comboCount > 0) {
G.comboTimer -= dt;
if (G.comboTimer <= 0) {
G.comboCount = 0;
G.comboTimer = 0;
}
}
// Check milestones
checkMilestones();
@@ -950,6 +980,20 @@ function 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
if (Math.floor(G.tick * 10) % 2 === 0) {
render();
@@ -1033,6 +1077,51 @@ 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 = [
{
@@ -1137,16 +1226,46 @@ function resolveAlignment(accept) {
// === ACTIONS ===
function writeCode() {
const base = 1;
const bonus = Math.floor(G.buildings.autocoder * 0.5);
const amount = (base + bonus) * G.codeBoost;
const autocoderBonus = Math.floor(G.buildings.autocoder * 0.5);
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.totalCode += amount;
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();
checkMilestones();
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) {
if (G.ops < 5) {
log('Not enough Operations. Build Ops generators or wait.');
@@ -1182,63 +1301,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 +1319,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';
@@ -1272,6 +1341,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() {
const phase = PHASES[G.phase];
const nameEl = document.getElementById('phase-name');
@@ -1351,6 +1476,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());
@@ -1399,6 +1525,19 @@ function log(msg, isMilestone) {
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 render() {
renderResources();
renderPhase();
@@ -1406,18 +1545,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';
}
renderProgress();
renderCombo();
}
function renderAlignment() {
@@ -1442,6 +1572,20 @@ function renderAlignment() {
}
// === 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 +1594,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 +1604,14 @@ 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,
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 +1636,28 @@ 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;
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;
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.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(', ')}`);
}
}
@@ -1514,7 +1676,6 @@ function initGame() {
G.deployFlag = 0;
G.sovereignFlag = 0;
G.beaconFlag = 0;
G.creativityBoost = 1;
updateRates();
render();
renderPhase();
@@ -1531,7 +1692,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)

1121
index.html

File diff suppressed because it is too large Load Diff