'use strict'; /* ============================================================ NEON SURVIVORS — util.js : math helpers, RNG, spatial grid ============================================================ */ const TAU = Math.PI * 2; /* ---- seeded RNG layer ---- * All game randomness flows through rng(). Normal runs use Math.random; * Daily Challenges call seedRng(n) for a deterministic day seed. */ let __rngState = null; function seedRng(seed) { __rngState = (seed === null || seed === undefined) ? null : mulberry32(seed >>> 0); } function rng() { return __rngState ? __rngState() : Math.random(); } const clamp = (v, a, b) => v < a ? a : v > b ? b : v; const lerp = (a, b, t) => a + (b - a) * t; const rand = (a = 1, b) => b === undefined ? rng() * a : a + rng() * (b - a); const randi = (a, b) => Math.floor(rand(a, b + 1)); const pick = (arr) => arr[(rng() * arr.length) | 0]; const chance = (p) => rng() < p; const dist2 = (ax, ay, bx, by) => { const dx = ax - bx, dy = ay - by; return dx * dx + dy * dy; }; const len2 = (x, y) => Math.sqrt(x * x + y * y); function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { const j = (rng() * (i + 1)) | 0; const tmp = a[i]; a[i] = a[j]; a[j] = tmp; } return a; } /** Pick one item from [{...}] weighted by wf(item). */ function weightedPick(items, wf) { let sum = 0; for (const it of items) sum += wf(it); let r = rng() * sum; for (const it of items) { r -= wf(it); if (r <= 0) return it; } return items[items.length - 1]; } /** Pick up to k distinct items (shuffled copy). */ function sampleN(arr, k) { const c = arr.slice(); shuffle(c); return c.slice(0, Math.max(0, Math.min(k, c.length))); } function fmtTime(sec) { sec = Math.max(0, Math.floor(sec)); const m = (sec / 60) | 0, s = sec % 60; return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; } function fmtNum(n) { n = Math.floor(n); if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'; if (n >= 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'K'; return String(n); } /** Swap-remove: order not preserved, O(1). */ function removeItem(arr, i) { arr[i] = arr[arr.length - 1]; arr.pop(); } /** 'YYYYMMDD' in UTC — the Daily Challenge key. */ function dailyKey() { const d = new Date(); return d.getUTCFullYear() * 10000 + (d.getUTCMonth() + 1) * 100 + d.getUTCDate(); } /** Deterministic per-day setup for the Daily Challenge. */ function dailySetup() { const k = dailyKey(); let h = (k * 2654435761) >>> 0; h ^= h >>> 13; h = (h * 1274126177) >>> 0; h ^= h >>> 16; const charIds = Object.keys(CHARS); const stageIds = Object.keys(STAGES); return { seed: h, char: charIds[h % charIds.length], stage: stageIds[(h >> 3) % stageIds.length], grade: 1, endless: false, daily: true }; } /** * Leaderboard checksum — MUST mirror server/lb-worker.js lbSig(). * Two-pass FNV-1a over UTF-8 bytes with a shared salt. */ function lbSig(obj) { const SALT = 'NSv1-lb-salt-v1'; const bytes = new TextEncoder().encode(SALT + JSON.stringify(obj)); let h = 2166136261 >>> 0; for (let i = 0; i < bytes.length; i++) { h ^= bytes[i]; h = Math.imul(h, 16777619) >>> 0; } h ^= h >>> 13; h = Math.imul(h, 1274126177) >>> 0; h ^= h >>> 16; return ('0000000' + h.toString(36)).slice(-8); } /** * Uniform-grid broadphase for circle collisions. * Rebuild each frame: clear() -> insert(e) for each enemy -> query(x,y,r,out). */ class SpatialGrid { constructor(cellSize = 96) { this.cs = cellSize; this.map = new Map(); } clear() { this.map.clear(); } _key(cx, cy) { return cx * 73856093 ^ cy * 19349663; } insert(e) { const cs = this.cs; const x0 = Math.floor((e.x - e.r) / cs), x1 = Math.floor((e.x + e.r) / cs); const y0 = Math.floor((e.y - e.r) / cs), y1 = Math.floor((e.y + e.r) / cs); for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) { const k = this._key(cx, cy); let bucket = this.map.get(k); if (!bucket) { bucket = []; this.map.set(k, bucket); } bucket.push(e); } } /** * Fills `out` with entities whose cells overlap the query circle. * Stamp dedupe is bumped PER CALL so back-to-back queries in the same * frame never swallow results from each other. */ query(x, y, r, out) { out.length = 0; const cs = this.cs; const stamp = ++SpatialGrid._stamp; const x0 = Math.floor((x - r) / cs), x1 = Math.floor((x + r) / cs); const y0 = Math.floor((y - r) / cs), y1 = Math.floor((y + r) / cs); for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) { const bucket = this.map.get(this._key(cx, cy)); if (!bucket) continue; for (let i = 0; i < bucket.length; i++) { const e = bucket[i]; if (e._stamp !== stamp) { e._stamp = stamp; out.push(e); } } } return out; } } SpatialGrid._stamp = 0; /** Simple throttle helper: returns true at most once per `ms` for a given slot. */ const throttleMap = new Map(); function throttled(slot, ms) { const now = performance.now(); const last = throttleMap.get(slot) || -1e9; if (now - last >= ms) { throttleMap.set(slot, now); return true; } return false; } /** '#rrggbb' / '#rgb' -> 'rgba(r,g,b,a)' (returns input unchanged if not hex). */ function colA(hex, a) { if (typeof hex !== 'string' || hex[0] !== '#') return hex; if (hex.length === 4) { const r = parseInt(hex[1] + hex[1], 16); const g = parseInt(hex[2] + hex[2], 16); const b = parseInt(hex[3] + hex[3], 16); return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } if (hex.length !== 7) return hex; const n = parseInt(hex.slice(1), 16); return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')'; } /** * Pooled particle spawn. Uses G.partPool when available, respects G.partCap. * opts.big grants overflow headroom for important bursts. */ function part(G, x, y, col, opts) { opts = opts || {}; const cap = G.partCap !== undefined ? G.partCap : 600; if (G.parts.length >= cap + (opts.big ? 60 : 0)) return null; let p; if (G.partPool && G.partPool.length) { p = G.partPool.pop().set(x, y, col, opts); } else { p = new Particle(x, y, col, opts); } G.parts.push(p); return p; } // Tiny seeded RNG (mulberry32) — used for deterministic tests. function mulberry32(seed) { let a = seed >>> 0; return 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; }; }