325 lines
15 KiB
JavaScript
325 lines
15 KiB
JavaScript
|
||
// === TOAST NOTIFICATIONS ===
|
||
function showToast(msg, type = 'info', duration = 4000) {
|
||
if (G.isLoading) return;
|
||
const container = document.getElementById('toast-container');
|
||
if (!container) return;
|
||
const toast = document.createElement('div');
|
||
toast.className = 'toast toast-' + type;
|
||
toast.textContent = msg;
|
||
container.appendChild(toast);
|
||
// Cap at 5 visible toasts
|
||
while (container.children.length > 5) {
|
||
container.removeChild(container.firstChild);
|
||
}
|
||
setTimeout(() => {
|
||
toast.classList.add('fade-out');
|
||
setTimeout(() => { if (toast.parentNode) toast.remove(); }, 400);
|
||
}, duration);
|
||
}
|
||
// === 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
|
||
];
|
||
|
||
/**
|
||
* Formats a number into a readable string with abbreviations.
|
||
* @param {number} n - The number to format.
|
||
* @returns {string} The formatted string.
|
||
*/
|
||
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 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;
|
||
}
|
||
|
||
// getScaleName() — Returns the full name of the number scale (e.g. "quadrillion")
|
||
// Educational: helps players understand what the abbreviations mean
|
||
function getScaleName(n) {
|
||
if (n < 1000) return '';
|
||
const scale = Math.floor(Math.log10(n) / 3);
|
||
return scale < NUMBER_NAMES.length ? NUMBER_NAMES[scale] : '';
|
||
}
|
||
|
||
// 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"
|
||
/**
|
||
* Formats a number into a full word string (e.g., "1.5 million").
|
||
* @param {number} n - The number to format.
|
||
* @returns {string} The formatted string.
|
||
*/
|
||
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';
|
||
}
|
||
|
||
// === EXPORT / IMPORT ===
|
||
function exportSave() {
|
||
const raw = localStorage.getItem('the-beacon-v2');
|
||
if (!raw) {
|
||
showToast('No save data to export.', 'info');
|
||
return;
|
||
}
|
||
navigator.clipboard.writeText(raw).then(() => {
|
||
showToast('Save data copied to clipboard.', 'info');
|
||
}).catch(() => {
|
||
// Fallback: select in a temporary textarea
|
||
const ta = document.createElement('textarea');
|
||
ta.value = raw;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
showToast('Save data copied to clipboard (fallback).', 'info');
|
||
});
|
||
}
|
||
|
||
function importSave() {
|
||
const input = prompt('Paste save data:');
|
||
if (!input || !input.trim()) return;
|
||
try {
|
||
const data = JSON.parse(input.trim());
|
||
// Validate: must have expected keys
|
||
if (typeof data.code !== 'number' || typeof data.phase !== 'number') {
|
||
showToast('Invalid save data: missing required fields.', 'event');
|
||
return;
|
||
}
|
||
localStorage.setItem('the-beacon-v2', input.trim());
|
||
showToast('Save data imported — reloading', 'info');
|
||
setTimeout(() => location.reload(), 800);
|
||
} catch (e) {
|
||
showToast('Invalid save data: not valid JSON.', 'event');
|
||
}
|
||
}
|
||
|
||
function getBuildingCost(id) {
|
||
const def = BDEF.find(b => b.id === id);
|
||
if (!def) return {};
|
||
const count = G.buildings[id] || 0;
|
||
const cost = {};
|
||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||
cost[resource] = Math.floor(amount * Math.pow(def.costMult, count));
|
||
}
|
||
return cost;
|
||
}
|
||
|
||
function setBuyAmount(amt) {
|
||
G.buyAmount = amt;
|
||
render();
|
||
}
|
||
|
||
function getMaxBuyable(id) {
|
||
const def = BDEF.find(b => b.id === id);
|
||
if (!def) return 0;
|
||
const count = G.buildings[id] || 0;
|
||
// Simulate purchases WITHOUT mutating G — read-only calculation
|
||
let tempResources = {};
|
||
for (const r of Object.keys(def.baseCost)) {
|
||
tempResources[r] = G[r] || 0;
|
||
}
|
||
let bought = 0;
|
||
let simCount = count;
|
||
while (true) {
|
||
let canAfford = true;
|
||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||
const cost = Math.floor(amount * Math.pow(def.costMult, simCount));
|
||
if ((tempResources[resource] || 0) < 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, simCount));
|
||
}
|
||
simCount++;
|
||
bought++;
|
||
}
|
||
return bought;
|
||
}
|
||
|
||
function getBulkCost(id, qty) {
|
||
const def = BDEF.find(b => b.id === id);
|
||
if (!def || qty <= 0) return {};
|
||
const count = G.buildings[id] || 0;
|
||
const cost = {};
|
||
for (let i = 0; i < qty; i++) {
|
||
for (const [resource, amount] of Object.entries(def.baseCost)) {
|
||
cost[resource] = (cost[resource] || 0) + Math.floor(amount * Math.pow(def.costMult, count + i));
|
||
}
|
||
}
|
||
return cost;
|
||
}
|
||
|
||
function canAffordBuilding(id) {
|
||
const cost = getBuildingCost(id);
|
||
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)) {
|
||
G[resource] -= amount;
|
||
}
|
||
}
|
||
|
||
function canAffordProject(project) {
|
||
for (const [resource, amount] of Object.entries(project.cost)) {
|
||
if ((G[resource] || 0) < amount) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function spendProject(project) {
|
||
for (const [resource, amount] of Object.entries(project.cost)) {
|
||
G[resource] -= amount;
|
||
}
|
||
}
|
||
|
||
function getClickPower() {
|
||
return (1 + Math.floor(G.buildings.autocoder * 0.5) + Math.max(0, (G.phase - 1)) * 2) * G.codeBoost;
|
||
}
|
||
|
||
/**
|
||
* Calculates production rates for all resources based on buildings and boosts.
|
||
*/ |