export const TAU = Math.PI * 2; export function lerp(a, b, t) { return a + (b - a) * t; } export function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } export function distance(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } export function distanceSq(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; return dx * dx + dy * dy; } export function randomRange(min, max) { return min + Math.random() * (max - min); } export function randomInt(min, max) { return Math.floor(randomRange(min, max + 1)); } export function randomGaussian(mean = 0, std = 1) { let u = 0, v = 0; while (u === 0) u = Math.random(); while (v === 0) v = Math.random(); return mean + std * Math.sqrt(-2.0 * Math.log(u)) * Math.cos(TAU * v); } export function randomChoice(arr) { return arr[Math.floor(Math.random() * arr.length)]; } export function normalize(x, y) { const len = Math.sqrt(x * x + y * y); if (len === 0) return { x: 0, y: 0 }; return { x: x / len, y: y / len }; } export function angleBetween(x1, y1, x2, y2) { return Math.atan2(y2 - y1, x2 - x1); } export function hslToHex(h, s, l) { s /= 100; l /= 100; const a = s * Math.min(l, 1 - l); const f = (n) => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color) .toString(16) .padStart(2, "0"); }; return `#${f(0)}${f(8)}${f(4)}`; } export function hexToRgb(hex) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return { r, g, b }; } export function rgbToString(r, g, b, a = 1) { return a < 1 ? `rgba(${r},${g},${b},${a})` : `rgb(${r},${g},${b})`; } export function smoothstep(edge0, edge1, x) { const t = clamp((x - edge0) / (edge1 - edge0), 0, 1); return t * t * (3 - 2 * t); } /** Simple seeded pseudo-random for reproducible sequences */ export function seededRandom(seed) { let s = seed; return function () { s = (s * 1664525 + 1013904223) & 0xffffffff; return (s >>> 0) / 0xffffffff; }; } /** Object pool for reducing GC pressure */ export class ObjectPool { constructor(factory, reset, initialSize = 100) { this._factory = factory; this._reset = reset; this._pool = []; for (let i = 0; i < initialSize; i++) { this._pool.push(factory()); } } acquire() { if (this._pool.length > 0) { return this._pool.pop(); } return this._factory(); } release(obj) { this._reset(obj); this._pool.push(obj); } get available() { return this._pool.length; } } /** Spatial hash grid for efficient proximity queries */ export class SpatialGrid { constructor(width, height, cellSize) { this.cellSize = cellSize; this.cols = Math.ceil(width / cellSize); this.rows = Math.ceil(height / cellSize); this.cells = new Array(this.cols * this.rows); this.clear(); } clear() { for (let i = 0; i < this.cells.length; i++) { if (this.cells[i]) { this.cells[i].length = 0; } else { this.cells[i] = []; } } } _key(col, row) { return row * this.cols + col; } insert(item, x, y) { const col = Math.floor(x / this.cellSize); const row = Math.floor(y / this.cellSize); if (col >= 0 && col < this.cols && row >= 0 && row < this.rows) { this.cells[this._key(col, row)].push(item); } } query(x, y, radius) { const results = []; const minCol = Math.max(0, Math.floor((x - radius) / this.cellSize)); const maxCol = Math.min( this.cols - 1, Math.floor((x + radius) / this.cellSize), ); const minRow = Math.max(0, Math.floor((y - radius) / this.cellSize)); const maxRow = Math.min( this.rows - 1, Math.floor((y + radius) / this.cellSize), ); const rSq = radius * radius; for (let row = minRow; row <= maxRow; row++) { for (let col = minCol; col <= maxCol; col++) { const cell = this.cells[this._key(col, row)]; for (let i = 0; i < cell.length; i++) { const item = cell[i]; const dx = item.x - x; const dy = item.y - y; if (dx * dx + dy * dy <= rSq) { results.push(item); } } } } return results; } } /** Exponential moving average for smooth telemetry */ export class EMA { constructor(alpha = 0.1) { this.alpha = alpha; this.value = null; } update(v) { if (this.value === null) { this.value = v; } else { this.value = this.alpha * v + (1 - this.alpha) * this.value; } return this.value; } reset() { this.value = null; } } /** Ring buffer for fixed-size history */ export class RingBuffer { constructor(capacity) { this.capacity = capacity; this.data = new Array(capacity); this.head = 0; this.size = 0; } push(value) { this.data[this.head] = value; this.head = (this.head + 1) % this.capacity; if (this.size < this.capacity) this.size++; } toArray() { const arr = new Array(this.size); for (let i = 0; i < this.size; i++) { arr[i] = this.data[(this.head - this.size + i + this.capacity) % this.capacity]; } return arr; } get last() { if (this.size === 0) return undefined; return this.data[(this.head - 1 + this.capacity) % this.capacity]; } } /** Color constants for entity types */ export const COLORS = { gatherer: { hex: "#00ff88", r: 0, g: 255, b: 136 }, predator: { hex: "#ff3366", r: 255, g: 51, b: 102 }, builder: { hex: "#3399ff", r: 51, g: 153, b: 255 }, explorer: { hex: "#ffaa00", r: 255, g: 170, b: 0 }, hybrid: { hex: "#cc66ff", r: 204, g: 102, b: 255 }, food: { hex: "#00ffaa", r: 0, g: 255, b: 170 }, energy: { hex: "#ffee44", r: 255, g: 238, b: 68 }, cyan: { hex: "#00f0ff", r: 0, g: 240, b: 255 }, }; /** Format a number for display (e.g., 1234 -> 1.2k) */ export function formatNumber(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"; if (n >= 1000) return (n / 1000).toFixed(1) + "k"; return Math.round(n).toString(); } /** Wrap value within bounds */ export function wrap(value, min, max) { const range = max - min; return ((((value - min) % range) + range) % range) + min; }