Compare commits
1 Commits
feature/bu
...
bezalel/fl
| Author | SHA1 | Date | |
|---|---|---|---|
| 85a3e150f1 |
569
game.js
569
game.js
@@ -11,7 +11,6 @@ const G = {
|
||||
knowledge: 0,
|
||||
users: 0,
|
||||
impact: 0,
|
||||
rescues: 0,
|
||||
ops: 5,
|
||||
trust: 5,
|
||||
creativity: 0,
|
||||
@@ -23,7 +22,6 @@ const G = {
|
||||
totalKnowledge: 0,
|
||||
totalUsers: 0,
|
||||
totalImpact: 0,
|
||||
totalRescues: 0,
|
||||
|
||||
// Rates (calculated each tick)
|
||||
codeRate: 0,
|
||||
@@ -31,7 +29,6 @@ const G = {
|
||||
knowledgeRate: 0,
|
||||
userRate: 0,
|
||||
impactRate: 0,
|
||||
rescuesRate: 0,
|
||||
opsRate: 0,
|
||||
trustRate: 0,
|
||||
creativityRate: 0,
|
||||
@@ -85,7 +82,6 @@ const G = {
|
||||
tick: 0,
|
||||
saveTimer: 0,
|
||||
secTimer: 0,
|
||||
buyMode: 1, // 1, 10, or -1 (max)
|
||||
|
||||
// Systems
|
||||
projects: [],
|
||||
@@ -98,7 +94,6 @@ const G = {
|
||||
maxKnowledge: 0,
|
||||
maxUsers: 0,
|
||||
maxImpact: 0,
|
||||
maxRescues: 0,
|
||||
maxTrust: 5,
|
||||
maxOps: 5,
|
||||
maxHarmony: 50,
|
||||
@@ -107,7 +102,6 @@ const G = {
|
||||
drift: 0,
|
||||
lastEventAt: 0,
|
||||
eventCooldown: 0,
|
||||
activeEvents: [], // {id, expiresAt} — events auto-resolve after duration
|
||||
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
@@ -235,7 +229,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, rescues: 50 },
|
||||
rates: { impact: 5000, user: 10000 },
|
||||
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.'
|
||||
},
|
||||
@@ -243,7 +237,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, rescues: 250 },
|
||||
rates: { impact: 25000, user: 50000 },
|
||||
unlock: () => G.totalImpact >= 5000000 && G.beaconFlag === 1, phase: 6,
|
||||
edu: 'Decentralized means unstoppable. If one Beacon goes dark, a thousand more carry the signal.'
|
||||
},
|
||||
@@ -574,19 +568,6 @@ 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',
|
||||
@@ -641,161 +622,14 @@ const EDU_FACTS = [
|
||||
];
|
||||
|
||||
// === UTILITY FUNCTIONS ===
|
||||
|
||||
// Extended number scale abbreviations — covers up to centillion (10^303)
|
||||
// Inspired by Universal Paperclips' spellf() system
|
||||
const NUMBER_ABBREVS = [
|
||||
'', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', // 10^0 – 10^27
|
||||
'No', 'Dc', 'UDc', 'DDc', 'TDc', 'QaDc', 'QiDc', 'SxDc', 'SpDc', 'OcDc', // 10^30 – 10^57
|
||||
'NoDc', 'Vg', 'UVg', 'DVg', 'TVg', 'QaVg', 'QiVg', 'SxVg', 'SpVg', 'OcVg', // 10^60 – 10^87
|
||||
'NoVg', 'Tg', 'UTg', 'DTg', 'TTg', 'QaTg', 'QiTg', 'SxTg', 'SpTg', 'OcTg', // 10^90 – 10^117
|
||||
'NoTg', 'Qd', 'UQd', 'DQd', 'TQd', 'QaQd', 'QiQd', 'SxQd', 'SpQd', 'OcQd', // 10^120 – 10^147
|
||||
'NoQd', 'Qq', 'UQq', 'DQq', 'TQq', 'QaQq', 'QiQq', 'SxQq', 'SpQq', 'OcQq', // 10^150 – 10^177
|
||||
'NoQq', 'Sg', 'USg', 'DSg', 'TSg', 'QaSg', 'QiSg', 'SxSg', 'SpSg', 'OcSg', // 10^180 – 10^207
|
||||
'NoSg', 'St', 'USt', 'DSt', 'TSt', 'QaSt', 'QiSt', 'SxSt', 'SpSt', 'OcSt', // 10^210 – 10^237
|
||||
'NoSt', 'Og', 'UOg', 'DOg', 'TOg', 'QaOg', 'QiOg', 'SxOg', 'SpOg', 'OcOg', // 10^240 – 10^267
|
||||
'NoOg', 'Na', 'UNa', 'DNa', 'TNa', 'QaNa', 'QiNa', 'SxNa', 'SpNa', 'OcNa', // 10^270 – 10^297
|
||||
'NoNa', 'Ce' // 10^300 – 10^303
|
||||
];
|
||||
|
||||
// Full number scale names for spellf() — educational reference
|
||||
// Short scale (US/modern British): each new name = 1000x the previous
|
||||
const NUMBER_NAMES = [
|
||||
'', 'thousand', 'million', // 10^0, 10^3, 10^6
|
||||
'billion', 'trillion', 'quadrillion', // 10^9, 10^12, 10^15
|
||||
'quintillion', 'sextillion', 'septillion', // 10^18, 10^21, 10^24
|
||||
'octillion', 'nonillion', 'decillion', // 10^27, 10^30, 10^33
|
||||
'undecillion', 'duodecillion', 'tredecillion', // 10^36, 10^39, 10^42
|
||||
'quattuordecillion', 'quindecillion', 'sexdecillion', // 10^45, 10^48, 10^51
|
||||
'septendecillion', 'octodecillion', 'novemdecillion', // 10^54, 10^57, 10^60
|
||||
'vigintillion', 'unvigintillion', 'duovigintillion', // 10^63, 10^66, 10^69
|
||||
'tresvigintillion', 'quattuorvigintillion', 'quinvigintillion', // 10^72, 10^75, 10^78
|
||||
'sesvigintillion', 'septemvigintillion', 'octovigintillion', // 10^81, 10^84, 10^87
|
||||
'novemvigintillion', 'trigintillion', 'untrigintillion', // 10^90, 10^93, 10^96
|
||||
'duotrigintillion', 'trestrigintillion', 'quattuortrigintillion', // 10^99, 10^102, 10^105
|
||||
'quintrigintillion', 'sextrigintillion', 'septentrigintillion', // 10^108, 10^111, 10^114
|
||||
'octotrigintillion', 'novemtrigintillion', 'quadragintillion', // 10^117, 10^120, 10^123
|
||||
'unquadragintillion', 'duoquadragintillion', 'tresquadragintillion', // 10^126, 10^129, 10^132
|
||||
'quattuorquadragintillion', 'quinquadragintillion', 'sesquadragintillion', // 10^135, 10^138, 10^141
|
||||
'septenquadragintillion', 'octoquadragintillion', 'novemquadragintillion', // 10^144, 10^147, 10^150
|
||||
'quinquagintillion', 'unquinquagintillion', 'duoquinquagintillion', // 10^153, 10^156, 10^159
|
||||
'tresquinquagintillion', 'quattuorquinquagintillion','quinquinquagintillion', // 10^162, 10^165, 10^168
|
||||
'sesquinquagintillion', 'septenquinquagintillion', 'octoquinquagintillion', // 10^171, 10^174, 10^177
|
||||
'novemquinquagintillion', 'sexagintillion', 'unsexagintillion', // 10^180, 10^183, 10^186
|
||||
'duosexagintillion', 'tressexagintillion', 'quattuorsexagintillion', // 10^189, 10^192, 10^195
|
||||
'quinsexagintillion', 'sessexagintillion', 'septensexagintillion', // 10^198, 10^201, 10^204
|
||||
'octosexagintillion', 'novemsexagintillion', 'septuagintillion', // 10^207, 10^210, 10^213
|
||||
'unseptuagintillion', 'duoseptuagintillion', 'tresseptuagintillion', // 10^216, 10^219, 10^222
|
||||
'quattuorseptuagintillion', 'quinseptuagintillion', 'sesseptuagintillion', // 10^225, 10^228, 10^231
|
||||
'septenseptuagintillion', 'octoseptuagintillion', 'novemseptuagintillion', // 10^234, 10^237, 10^240
|
||||
'octogintillion', 'unoctogintillion', 'duooctogintillion', // 10^243, 10^246, 10^249
|
||||
'tresoctogintillion', 'quattuoroctogintillion', 'quinoctogintillion', // 10^252, 10^255, 10^258
|
||||
'sesoctogintillion', 'septenoctogintillion', 'octooctogintillion', // 10^261, 10^264, 10^267
|
||||
'novemoctogintillion', 'nonagintillion', 'unnonagintillion', // 10^270, 10^273, 10^276
|
||||
'duononagintillion', 'trenonagintillion', 'quattuornonagintillion', // 10^279, 10^282, 10^285
|
||||
'quinnonagintillion', 'sesnonagintillion', 'septennonagintillion', // 10^288, 10^291, 10^294
|
||||
'octononagintillion', 'novemnonagintillion', 'centillion' // 10^297, 10^300, 10^303
|
||||
];
|
||||
|
||||
function fmt(n) {
|
||||
if (n === undefined || n === null || isNaN(n)) return '0';
|
||||
if (n === Infinity) return '\u221E';
|
||||
if (n === -Infinity) return '-\u221E';
|
||||
if (n < 0) return '-' + fmt(-n);
|
||||
if (n < 1000) return Math.floor(n).toLocaleString();
|
||||
const units = ['', 'K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc', 'Ud', 'Dd', 'Td'];
|
||||
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;
|
||||
}
|
||||
|
||||
// spellf() — Converts numbers to full English word form
|
||||
// Educational: shows the actual names of number scales
|
||||
// Examples: spellf(1500) => "one thousand five hundred"
|
||||
// spellf(2500000) => "two million five hundred thousand"
|
||||
// spellf(1e33) => "one decillion"
|
||||
function spellf(n) {
|
||||
if (n === undefined || n === null || isNaN(n)) return 'zero';
|
||||
if (n === Infinity) return 'infinity';
|
||||
if (n === -Infinity) return 'negative infinity';
|
||||
if (n < 0) return 'negative ' + spellf(-n);
|
||||
if (n === 0) return 'zero';
|
||||
|
||||
// Small number words (0–999)
|
||||
const ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine',
|
||||
'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen',
|
||||
'seventeen', 'eighteen', 'nineteen'];
|
||||
const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
|
||||
|
||||
function spellSmall(num) {
|
||||
if (num === 0) return '';
|
||||
if (num < 20) return ones[num];
|
||||
if (num < 100) {
|
||||
return tens[Math.floor(num / 10)] + (num % 10 ? ' ' + ones[num % 10] : '');
|
||||
}
|
||||
const h = Math.floor(num / 100);
|
||||
const remainder = num % 100;
|
||||
return ones[h] + ' hundred' + (remainder ? ' ' + spellSmall(remainder) : '');
|
||||
}
|
||||
|
||||
// For very large numbers beyond our lookup table, fall back
|
||||
if (n >= 1e306) return n.toExponential(2) + ' (beyond centillion)';
|
||||
|
||||
// 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 = [];
|
||||
|
||||
let remaining = n;
|
||||
for (let s = scale; s >= 0; s--) {
|
||||
const divisor = Math.pow(10, s * 3);
|
||||
const chunk = Math.floor(remaining / divisor);
|
||||
remaining = remaining - chunk * divisor;
|
||||
if (chunk > 0 && chunk < 1000) {
|
||||
parts.push(spellSmall(chunk) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
|
||||
} else if (chunk >= 1000) {
|
||||
// Floating point chunk too large — shouldn't happen below 1e54
|
||||
parts.push(spellSmall(Math.floor(chunk % 1000)) + (NUMBER_NAMES[s] ? ' ' + NUMBER_NAMES[s] : ''));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' ') || 'zero';
|
||||
const unit = units[Math.min(scale, units.length - 1)] || 'e' + (scale * 3);
|
||||
if (scale >= units.length) return n.toExponential(2);
|
||||
return (n / Math.pow(10, scale * 3)).toFixed(1) + unit;
|
||||
}
|
||||
|
||||
function getBuildingCost(id) {
|
||||
@@ -809,42 +643,6 @@ function getBuildingCost(id) {
|
||||
return cost;
|
||||
}
|
||||
|
||||
function getBuildingBatchCost(id, count) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def || count <= 0) return {};
|
||||
const currentCount = G.buildings[id] || 0;
|
||||
const cost = {};
|
||||
for (let i = 0; i < count; i++) {
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
cost[resource] = (cost[resource] || 0) + Math.floor(amount * Math.pow(def.costMult, currentCount + i));
|
||||
}
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
function getMaxBuyable(id) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def) return 0;
|
||||
// Simulate buying one at a time until we can't afford
|
||||
let count = 0;
|
||||
const tempResources = {};
|
||||
for (const r of Object.keys(def.baseCost)) tempResources[r] = G[r] || 0;
|
||||
const currentCount = G.buildings[id] || 0;
|
||||
for (let i = 0; i < 1000; i++) { // cap at 1000
|
||||
let canAfford = true;
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
const cost = Math.floor(amount * Math.pow(def.costMult, currentCount + i));
|
||||
if (tempResources[resource] < cost) { canAfford = false; break; }
|
||||
}
|
||||
if (!canAfford) break;
|
||||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||||
tempResources[resource] -= Math.floor(amount * Math.pow(def.costMult, currentCount + i));
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function canAffordBuilding(id) {
|
||||
const cost = getBuildingCost(id);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
@@ -853,16 +651,6 @@ function canAffordBuilding(id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function canAffordBatch(id) {
|
||||
const count = G.buyMode === -1 ? getMaxBuyable(id) : G.buyMode;
|
||||
if (count <= 0) return false;
|
||||
const cost = getBuildingBatchCost(id, count);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
if ((G[resource] || 0) < amount) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function spendBuilding(id) {
|
||||
const cost = getBuildingCost(id);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
@@ -886,7 +674,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.rescuesRate = 0; G.opsRate = 0; G.trustRate = 0;
|
||||
G.userRate = 0; G.impactRate = 0; G.opsRate = 0; G.trustRate = 0;
|
||||
G.creativityRate = 0; G.harmonyRate = 0;
|
||||
|
||||
// Apply building rates
|
||||
@@ -899,7 +687,6 @@ 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;
|
||||
@@ -913,7 +700,6 @@ function updateRates() {
|
||||
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
|
||||
}
|
||||
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) +
|
||||
@@ -959,16 +745,12 @@ 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;
|
||||
@@ -981,7 +763,6 @@ 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);
|
||||
@@ -989,7 +770,6 @@ 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);
|
||||
@@ -1015,34 +795,6 @@ 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();
|
||||
@@ -1092,26 +844,15 @@ function checkProjects() {
|
||||
function buyBuilding(id) {
|
||||
const def = BDEF.find(b => b.id === id);
|
||||
if (!def || !def.unlock()) return;
|
||||
|
||||
if (def.phase > G.phase + 1) return;
|
||||
|
||||
const buyCount = G.buyMode === -1 ? getMaxBuyable(id) : G.buyMode;
|
||||
if (buyCount <= 0) return;
|
||||
if (!canAffordBuilding(id)) return;
|
||||
|
||||
if (buyCount === 1) {
|
||||
if (!canAffordBuilding(id)) return;
|
||||
spendBuilding(id);
|
||||
G.buildings[id] = (G.buildings[id] || 0) + 1;
|
||||
log(`Built ${def.name} (total: ${G.buildings[id]})`);
|
||||
} else {
|
||||
if (!canAffordBatch(id)) return;
|
||||
const cost = getBuildingBatchCost(id, buyCount);
|
||||
for (const [resource, amount] of Object.entries(cost)) {
|
||||
G[resource] -= amount;
|
||||
}
|
||||
G.buildings[id] = (G.buildings[id] || 0) + buyCount;
|
||||
log(`Built ${buyCount}x ${def.name} (total: ${G.buildings[id]})`);
|
||||
}
|
||||
spendBuilding(id);
|
||||
G.buildings[id] = (G.buildings[id] || 0) + 1;
|
||||
updateRates();
|
||||
log(`Built ${def.name} (total: ${G.buildings[id]})`);
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -1137,99 +878,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 = [
|
||||
{
|
||||
id: 'runner_stuck',
|
||||
title: 'CI Runner Stuck',
|
||||
desc: 'The forge pipeline has halted. Code production slowed.',
|
||||
desc: 'The forge pipeline has halted. Production slows until restarted.',
|
||||
weight: () => (G.ciFlag === 1 ? 2 : 0),
|
||||
duration: 45, // seconds
|
||||
resolveCost: { ops: 10 },
|
||||
effect: () => {
|
||||
G.codeRate *= 0.5;
|
||||
log('EVENT: CI runner stuck. Click to spend 10 Ops to clear, or wait ~45s.', true);
|
||||
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ezra_offline',
|
||||
title: 'Ezra is Offline',
|
||||
desc: 'The herald channel is silent. User growth stalled.',
|
||||
desc: 'The herald channel is silent. User growth stalls.',
|
||||
weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
|
||||
duration: 60,
|
||||
resolveCost: { ops: 5, trust: 2 },
|
||||
effect: () => {
|
||||
G.userRate *= 0.3;
|
||||
log('EVENT: Ezra offline. Click to dispatch, or wait ~60s.', true);
|
||||
log('EVENT: Ezra offline. Dispatch required.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'unreviewed_merge',
|
||||
title: 'Unreviewed Merge',
|
||||
desc: 'A change went in without eyes. Trust erodes.',
|
||||
weight: () => (G.deployFlag === 1 && G.branchProtectionFlag !== 1 ? 3 : 0),
|
||||
duration: 30,
|
||||
resolveCost: { ops: 8 },
|
||||
weight: () => (G.deployFlag === 1 ? 3 : 0),
|
||||
effect: () => {
|
||||
G.trust = Math.max(0, G.trust - 10);
|
||||
log('EVENT: Unreviewed merge detected. Trust lost. Click to revert.', true);
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'api_rate_limit',
|
||||
title: 'API Rate Limit',
|
||||
desc: 'External compute provider throttled.',
|
||||
weight: () => (G.totalCompute >= 1000 && G.sovereignFlag !== 1 ? 2 : 0),
|
||||
duration: 40,
|
||||
resolveCost: { compute: 200 },
|
||||
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
|
||||
effect: () => {
|
||||
G.computeRate *= 0.5;
|
||||
log('EVENT: API rate limit. Click to provision local compute, or wait ~40s.', true);
|
||||
log('EVENT: API rate limit hit. Local compute insufficient.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1237,7 +930,6 @@ 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;
|
||||
@@ -1248,11 +940,9 @@ 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 vanished. Click to search, or wait ~50s.', true);
|
||||
log('EVENT: Bilbo has vanished. Creativity halts.', true);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1266,53 +956,12 @@ 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) {
|
||||
@@ -1331,15 +980,6 @@ function resolveAlignment(accept) {
|
||||
}
|
||||
|
||||
// === ACTIONS ===
|
||||
function setBuyMode(mode) {
|
||||
G.buyMode = mode;
|
||||
// Update active button highlight
|
||||
document.querySelectorAll('.buy-mode-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', parseInt(btn.dataset.mode) === mode);
|
||||
});
|
||||
renderBuildings();
|
||||
}
|
||||
|
||||
function writeCode() {
|
||||
const base = 1;
|
||||
const bonus = Math.floor(G.buildings.autocoder * 0.5);
|
||||
@@ -1405,13 +1045,6 @@ 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';
|
||||
@@ -1506,7 +1139,6 @@ 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());
|
||||
@@ -1563,8 +1195,6 @@ function render() {
|
||||
renderStats();
|
||||
updateEducation();
|
||||
renderAlignment();
|
||||
renderActiveEvents();
|
||||
checkUnlocks();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -1588,112 +1218,7 @@ 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,
|
||||
@@ -1710,19 +1235,14 @@ function saveGame() {
|
||||
branchProtectionFlag: G.branchProtectionFlag || 0, nightlyWatchFlag: G.nightlyWatchFlag || 0,
|
||||
nostrFlag: G.nostrFlag || 0,
|
||||
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
|
||||
totalClicks: G.totalClicks, startedAt: G.startedAt, buyMode: G.buyMode,
|
||||
totalClicks: G.totalClicks, startedAt: G.startedAt,
|
||||
flags: G.flags,
|
||||
_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,
|
||||
drift: G.drift || 0, pendingAlignment: G.pendingAlignment || false,
|
||||
lastEventAt: G.lastEventAt || 0,
|
||||
savedAt: Date.now()
|
||||
};
|
||||
|
||||
localStorage.setItem('the-beacon-v2', JSON.stringify(saveData));
|
||||
showSaveToast();
|
||||
}
|
||||
|
||||
function loadGame() {
|
||||
@@ -1747,21 +1267,12 @@ 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;
|
||||
|
||||
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(', ')}`);
|
||||
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${fmt(gc)} code, ${fmt(kc)} knowledge, ${fmt(uc)} users`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1786,7 +1297,6 @@ function initGame() {
|
||||
|
||||
log('The screen is blank. Write your first line of code.', true);
|
||||
log('Click WRITE CODE or press SPACE to start.');
|
||||
log('Press B to toggle buy mode (x1 / x10 / MAX).');
|
||||
log('Build AutoCode for passive production.');
|
||||
log('Watch for Research Projects to appear.');
|
||||
}
|
||||
@@ -1797,20 +1307,9 @@ window.addEventListener('load', function () {
|
||||
} else {
|
||||
render();
|
||||
renderPhase();
|
||||
if (G.driftEnding) {
|
||||
G.running = false;
|
||||
renderDriftEnding();
|
||||
} else if (G.beaconEnding) {
|
||||
G.running = false;
|
||||
renderBeaconEnding();
|
||||
} else {
|
||||
log('Game loaded. Welcome back to The Beacon.');
|
||||
}
|
||||
log('Game loaded. Welcome back to The Beacon.');
|
||||
}
|
||||
|
||||
// Restore buy mode button highlight
|
||||
setBuyMode(G.buyMode || 1);
|
||||
|
||||
// Game loop at 10Hz (100ms tick)
|
||||
setInterval(tick, 100);
|
||||
|
||||
@@ -1827,10 +1326,4 @@ window.addEventListener('keydown', function (e) {
|
||||
e.preventDefault();
|
||||
writeCode();
|
||||
}
|
||||
if (e.code === 'KeyB' && e.target === document.body) {
|
||||
e.preventDefault();
|
||||
// Cycle: x1 -> x10 -> MAX -> x1
|
||||
const next = G.buyMode === 1 ? 10 : G.buyMode === 10 ? -1 : 1;
|
||||
setBuyMode(next);
|
||||
}
|
||||
});
|
||||
|
||||
1008
index.html
1008
index.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user