diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..2f1c088 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,286 @@ + +// === 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'; +} + +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. + */ \ No newline at end of file