// Small formatting helpers shared by the UI, ported from // scripts/classes/text_format.gd. // Groups a number's digits in threes. The separators are non-breaking spaces, // so a money or resource figure never wraps in the middle of a number. export function groupDigits(value) { const truncated = Math.trunc(value); const digits = String(Math.abs(truncated)); let grouped = ""; let count = 0; for (let i = digits.length - 1; i >= 0; i--) { grouped = digits[i] + grouped; count += 1; if (count % 3 === 0 && i > 0) grouped = "\u00a0" + grouped; } return (truncated < 0 ? "-" : "") + grouped; } // The canonical number for a variable display: digits grouped in threes with // non-breaking spaces and, when `signed`, an explicit plus or minus. Callers // pass an unsigned figure for a stock; a flow passes `signed` so it reads as a // movement. This is what every money, resource and stat figure renders through. export function formatNumber(value, { decimals = 0, signed = false } = {}) { const amount = Number(value); const safe = Number.isFinite(amount) ? amount : 0; const negative = safe < 0; const sign = signed ? (negative ? "\u2212" : "+") : (negative ? "\u2212" : ""); const fixed = Math.abs(safe).toFixed(decimals); const [whole, fraction] = fixed.split("."); const grouped = groupDigits(Number(whole)); return sign + (fraction ? `${grouped}.${fraction}` : grouped); } // Splits a compact figure into its mantissa and power-of-ten suffix, so the // suffix can sit apart from the number in the unified display. `-1.5M` becomes // the number `-1.5` and the suffix `M`. export function splitCompact(value) { const text = compact(value); const match = /^(-?[0-9.]+)([KMBT]?)$/.exec(text); if (!match) return { number: text, suffix: "" }; return { number: match[1].replace("-", "\u2212"), suffix: match[2] }; } function threeSignificant(value) { const digits = Math.floor(Math.log(value) / Math.log(10)) + 1; const decimals = Math.max(0, 3 - digits); const factor = Math.pow(10, decimals); const rounded = Math.round(value * factor) / factor; let text = rounded.toFixed(decimals); if (text.includes(".")) text = text.replace(/0+$/, "").replace(/\.$/, ""); return text; } export function compact(value) { if (Math.abs(value) < 1e-9) return "0"; const sign = value < 0 ? "-" : ""; let amount = Math.abs(value); const suffixes = ["", "K", "M", "B", "T"]; let index = 0; while (amount >= 1000 && index < suffixes.length - 1) { amount /= 1000; index += 1; } let text = threeSignificant(amount); if (text === "1000" && index < suffixes.length - 1) { index += 1; text = "1"; } return sign + text + suffixes[index]; } // A fraction as a whole-number percentage, the unit most descriptions quote: // 0.1 reads "10%". Callers pass the raw fraction the simulation applies so a // description can never drift from the rule it describes. export function formatPercent(fraction, { decimals = 0 } = {}) { const value = Number(fraction) * 100; const fixed = value.toFixed(decimals); const text = decimals === 0 ? fixed : fixed.replace(/\.?0+$/, ""); return `${text}%`; } export function improvements(list) { if (!list || list.length === 0) return "None"; return list.join(", "); } export function hours(value) { if (value < 1.0) return value.toFixed(2) + " h"; return value.toFixed(1) + " h"; }