| 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); |
| } |
|
|
| |
| export function seededRandom(seed) { |
| let s = seed; |
| return function () { |
| s = (s * 1664525 + 1013904223) & 0xffffffff; |
| return (s >>> 0) / 0xffffffff; |
| }; |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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]; |
| } |
| } |
|
|
| |
| 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 }, |
| }; |
|
|
| |
| 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(); |
| } |
|
|
| |
| export function wrap(value, min, max) { |
| const range = max - min; |
| return ((((value - min) % range) + range) % range) + min; |
| } |
|
|