// ============ util.js — shared helpers ============ export const clamp = (v, a, b) => v < a ? a : v > b ? b : v; export const lerp = (a, b, t) => a + (b - a) * t; export const dist2 = (x1, y1, x2, y2) => { const dx = x2 - x1, dy = y2 - y1; return dx * dx + dy * dy; }; export const dist = (x1, y1, x2, y2) => Math.sqrt(dist2(x1, y1, x2, y2)); let _uid = 1; export const uid = () => _uid++; export function resetUid(v) { _uid = v; } export const curUid = () => _uid; /** Mulberry32 seeded RNG */ export function makeRng(seed) { let a = seed >>> 0; const fn = function () { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; fn.getState = () => a; fn.setState = v => { a = v >>> 0; }; return fn; } export function fmtMoney(n, signed = false) { const neg = n < 0; let v = Math.abs(Math.round(n)); let s; if (v >= 1e9) s = (v / 1e9).toFixed(2) + 'B'; else if (v >= 1e6) s = (v / 1e6).toFixed(2) + 'M'; else s = v.toLocaleString('en-US'); return (neg ? '-$' : (signed && n > 0 ? '+$' : '$')) + s; } export function fmtNum(n) { return Math.round(n).toLocaleString('en-US'); } const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; export function fmtDate(t) { return `${MONTHS[t.month]} ${t.day}, Year ${t.year}`; } export function fmtClock(t) { const h = Math.floor(t.hour), m = Math.floor((t.hour - h) * 60); return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; } /** pick weighted item from [[item, weight], ...] or array with .w prop */ export function weightedPick(rng, items) { let total = 0; for (const it of items) total += (it.w !== undefined ? it.w : it[1]); let r = rng() * total; for (const it of items) { r -= (it.w !== undefined ? it.w : it[1]); if (r <= 0) return it.it !== undefined ? it.it : it[0]; } return items[items.length - 1]; } export const choice = (rng, arr) => arr[Math.floor(rng() * arr.length)]; export const chance = (rng, p) => rng() < p; /** DOM helper */ export function el(tag, attrs = {}, ...children) { const e = document.createElement(tag); for (const [k, v] of Object.entries(attrs)) { if (k === 'class') e.className = v; else if (k === 'html') e.innerHTML = v; else if (k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2).toLowerCase(), v); else if (v !== null && v !== undefined) e.setAttribute(k, v); } for (const c of children.flat()) { if (c === null || c === undefined) continue; e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); } return e; } export function download(filename, text) { try { const blob = new Blob([text], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 400); return true; } catch (e) { console.error('download failed', e); return false; } }