| | 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; |
| | } |
| |
|