Compare commits
12 Commits
integratio
...
fix/event-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cdabe9771 | ||
|
|
5c88fe77be | ||
|
|
fe76150325 | ||
|
|
a3f1802473 | ||
|
|
3d414b2de6 | ||
| 1a7db021c8 | |||
| 2a12c5210d | |||
|
|
a012f99fd4 | ||
|
|
7359610825 | ||
|
|
b89764c27f | ||
|
|
d467348820 | ||
| e9b46e8501 |
483
game.js
483
game.js
@@ -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,
|
||||
@@ -94,6 +97,7 @@ const G = {
|
||||
maxKnowledge: 0,
|
||||
maxUsers: 0,
|
||||
maxImpact: 0,
|
||||
maxRescues: 0,
|
||||
maxTrust: 5,
|
||||
maxOps: 5,
|
||||
maxHarmony: 50,
|
||||
@@ -102,6 +106,13 @@ const G = {
|
||||
drift: 0,
|
||||
lastEventAt: 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
|
||||
playTime: 0,
|
||||
@@ -229,7 +240,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.'
|
||||
},
|
||||
@@ -237,7 +248,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.'
|
||||
},
|
||||
@@ -568,6 +579,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',
|
||||
@@ -685,6 +709,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;
|
||||
@@ -722,7 +749,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 = [];
|
||||
|
||||
@@ -734,7 +795,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] : ''));
|
||||
}
|
||||
}
|
||||
@@ -784,7 +845,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
|
||||
@@ -797,6 +858,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;
|
||||
@@ -849,21 +911,32 @@ function updateRates() {
|
||||
const allegroCount = G.buildings.allegro;
|
||||
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 ===
|
||||
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;
|
||||
// NOTE: creativity is added conditionally below (only when ops near max)
|
||||
G.harmony += G.harmonyRate * dt;
|
||||
G.harmony = Math.max(0, Math.min(100, G.harmony));
|
||||
|
||||
@@ -873,6 +946,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);
|
||||
@@ -880,6 +954,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);
|
||||
@@ -891,6 +966,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();
|
||||
|
||||
@@ -905,6 +989,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();
|
||||
@@ -988,51 +1086,124 @@ 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. -50% code production until restarted.',
|
||||
weight: () => (G.ciFlag === 1 ? 2 : 0),
|
||||
resolveCost: { resource: 'ops', amount: 50 },
|
||||
effect: () => {
|
||||
G.codeRate *= 0.5;
|
||||
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true);
|
||||
if (G.activeDebuffs.find(d => d.id === 'runner_stuck')) return;
|
||||
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',
|
||||
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),
|
||||
resolveCost: { resource: 'knowledge', amount: 200 },
|
||||
effect: () => {
|
||||
G.userRate *= 0.3;
|
||||
log('EVENT: Ezra offline. Dispatch required.', true);
|
||||
if (G.activeDebuffs.find(d => d.id === 'ezra_offline')) return;
|
||||
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',
|
||||
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),
|
||||
resolveCost: { resource: 'trust', amount: 5 },
|
||||
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);
|
||||
if (G.activeDebuffs.find(d => d.id === 'unreviewed_merge')) return;
|
||||
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',
|
||||
title: 'API Rate Limit',
|
||||
desc: 'External compute provider throttled.',
|
||||
desc: 'External compute provider throttled. -50% compute.',
|
||||
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
|
||||
resolveCost: { resource: 'code', amount: 300 },
|
||||
effect: () => {
|
||||
G.computeRate *= 0.5;
|
||||
log('EVENT: API rate limit hit. Local compute insufficient.', true);
|
||||
if (G.activeDebuffs.find(d => d.id === 'api_rate_limit')) return;
|
||||
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 +1211,7 @@ const EVENTS = [
|
||||
title: 'The Drift',
|
||||
desc: 'An optimization suggests removing the human override. +40% efficiency.',
|
||||
weight: () => (G.totalImpact >= 10000 ? 2 : 0),
|
||||
resolveCost: null,
|
||||
effect: () => {
|
||||
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
|
||||
G.pendingAlignment = true;
|
||||
@@ -1048,11 +1220,52 @@ const EVENTS = [
|
||||
{
|
||||
id: '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),
|
||||
resolveCost: { resource: 'trust', amount: 10 },
|
||||
effect: () => {
|
||||
G.creativityRate = 0;
|
||||
log('EVENT: Bilbo has vanished. Creativity halts.', true);
|
||||
if (G.activeDebuffs.find(d => d.id === 'bilbo_vanished')) return;
|
||||
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 +1302,69 @@ function resolveAlignment(accept) {
|
||||
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 ===
|
||||
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.');
|
||||
@@ -1155,6 +1418,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';
|
||||
@@ -1170,6 +1440,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');
|
||||
@@ -1249,12 +1575,14 @@ 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());
|
||||
set('st-projects', (G.completedProjects || []).length.toString());
|
||||
set('st-harmony', Math.floor(G.harmony).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 m = Math.floor(elapsed / 60);
|
||||
@@ -1297,6 +1625,42 @@ 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 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() {
|
||||
renderResources();
|
||||
renderPhase();
|
||||
@@ -1305,6 +1669,9 @@ function render() {
|
||||
renderStats();
|
||||
updateEducation();
|
||||
renderAlignment();
|
||||
renderProgress();
|
||||
renderCombo();
|
||||
renderDebuffs();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -1329,7 +1696,23 @@ 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() {
|
||||
// Save debuff IDs (can't serialize functions)
|
||||
const debuffIds = (G.activeDebuffs || []).map(d => d.id);
|
||||
const saveData = {
|
||||
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,
|
||||
@@ -1347,12 +1730,16 @@ 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,
|
||||
activeDebuffIds: debuffIds,
|
||||
totalEventsResolved: G.totalEventsResolved || 0,
|
||||
savedAt: Date.now()
|
||||
};
|
||||
|
||||
localStorage.setItem('the-beacon-v2', JSON.stringify(saveData));
|
||||
showSaveToast();
|
||||
}
|
||||
|
||||
function loadGame() {
|
||||
@@ -1363,6 +1750,20 @@ function loadGame() {
|
||||
const data = JSON.parse(raw);
|
||||
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();
|
||||
|
||||
// Offline progress
|
||||
@@ -1377,12 +1778,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(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,6 +1826,7 @@ function initGame() {
|
||||
log('Click WRITE CODE or press SPACE to start.');
|
||||
log('Build AutoCode for passive production.');
|
||||
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 () {
|
||||
@@ -1417,7 +1835,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)
|
||||
@@ -1436,4 +1862,9 @@ window.addEventListener('keydown', function (e) {
|
||||
e.preventDefault();
|
||||
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');
|
||||
});
|
||||
|
||||
1118
index.html
1118
index.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user