// ═══════════════════════════════════════════════════════════════════════════════ // GunCircle.io — Math Utilities // ═══════════════════════════════════════════════════════════════════════════════ import type { Vec2 } from './types.js'; // ─── Vector Operations ─────────────────────────────────────────────────────── export const vec2 = { zero: (): Vec2 => ({ x: 0, y: 0 }), fromAngle: (angle: number, magnitude: number = 1): Vec2 => ({ x: Math.cos(angle) * magnitude, y: Math.sin(angle) * magnitude, }), add: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y }), sub: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y }), mul: (v: Vec2, s: number): Vec2 => ({ x: v.x * s, y: v.y * s }), div: (v: Vec2, s: number): Vec2 => ({ x: v.x / s, y: v.y / s }), len: (v: Vec2): number => Math.sqrt(v.x * v.x + v.y * v.y), lenSq: (v: Vec2): number => v.x * v.x + v.y * v.y, normalize: (v: Vec2): Vec2 => { const l = Math.sqrt(v.x * v.x + v.y * v.y); return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l }; }, dot: (a: Vec2, b: Vec2): number => a.x * b.x + a.y * b.y, dist: (a: Vec2, b: Vec2): number => { const dx = a.x - b.x; const dy = a.y - b.y; return Math.sqrt(dx * dx + dy * dy); }, distSq: (a: Vec2, b: Vec2): number => { const dx = a.x - b.x; const dy = a.y - b.y; return dx * dx + dy * dy; }, clamp: (v: Vec2, maxLen: number): Vec2 => { const l = Math.sqrt(v.x * v.x + v.y * v.y); if (l <= maxLen) return v; const scale = maxLen / l; return { x: v.x * scale, y: v.y * scale }; }, angle: (v: Vec2): number => Math.atan2(v.y, v.x), angleBetween: (a: Vec2, b: Vec2): number => Math.atan2(b.y - a.y, b.x - a.x), lerp: (a: Vec2, b: Vec2, t: number): Vec2 => ({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, }), copy: (v: Vec2): Vec2 => ({ x: v.x, y: v.y }), equals: (a: Vec2, b: Vec2, eps: number = 0.001): boolean => Math.abs(a.x - b.x) < eps && Math.abs(a.y - b.y) < eps, }; // ─── Scalar Utilities ──────────────────────────────────────────────────────── export const lerp = (a: number, b: number, t: number): number => a + (b - a) * t; export const lerpAngle = (a: number, b: number, t: number): number => { let diff = b - a; while (diff > Math.PI) diff -= Math.PI * 2; while (diff < -Math.PI) diff += Math.PI * 2; return a + diff * t; }; export const clamp = (v: number, min: number, max: number): number => Math.max(min, Math.min(max, v)); export const clamp01 = (v: number): number => clamp(v, 0, 1); export const remap = ( v: number, inMin: number, inMax: number, outMin: number, outMax: number ): number => outMin + ((v - inMin) / (inMax - inMin)) * (outMax - outMin); /** Return random float in [min, max) */ export const randFloat = (min: number, max: number): number => min + Math.random() * (max - min); /** Return random int in [min, max) */ export const randInt = (min: number, max: number): number => Math.floor(randFloat(min, max)); /** Return 1 or -1 randomly */ export const randSign = (): number => (Math.random() < 0.5 ? 1 : -1); /** Check if circle a overlaps circle b */ export const circleOverlap = ( ax: number, ay: number, ar: number, bx: number, by: number, br: number ): boolean => { const dx = ax - bx; const dy = ay - by; const r = ar + br; return dx * dx + dy * dy < r * r; }; /** Solve circle-circle collision: push a out of b. Returns correction vector for a. */ export const circleCollisionResolve = ( ax: number, ay: number, ar: number, bx: number, by: number, br: number ): Vec2 | null => { const dx = ax - bx; const dy = ay - by; const distSq = dx * dx + dy * dy; const r = ar + br; if (distSq >= r * r || distSq === 0) return null; const dist = Math.sqrt(distSq); const overlap = r - dist; return { x: (dx / dist) * overlap, y: (dy / dist) * overlap, }; }; // ─── Spatial Hash Grid ─────────────────────────────────────────────────────── export class SpatialHashGrid { private cells = new Map(); private itemToCell = new Map(); constructor(private cellSize: number) {} private hash(cx: number, cy: number): string { return `${cx},${cy}`; } insert(item: T): void { const cx = Math.floor(item.x / this.cellSize); const cy = Math.floor(item.y / this.cellSize); const key = this.hash(cx, cy); let cell = this.cells.get(key); if (!cell) { cell = []; this.cells.set(key, cell); } cell.push(item); this.itemToCell.set(item.id, key); } remove(item: T): void { const key = this.itemToCell.get(item.id); if (!key) return; const cell = this.cells.get(key); if (!cell) return; const idx = cell.findIndex((i) => i.id === item.id); if (idx >= 0) cell.splice(idx, 1); this.itemToCell.delete(item.id); } update(item: T): void { this.remove(item); this.insert(item); } query(x: number, y: number, radius: number): T[] { const results: T[] = []; const minCx = Math.floor((x - radius) / this.cellSize); const maxCx = Math.floor((x + radius) / this.cellSize); const minCy = Math.floor((y - radius) / this.cellSize); const maxCy = Math.floor((y + radius) / this.cellSize); const rSq = radius * radius; for (let cx = minCx; cx <= maxCx; cx++) { for (let cy = minCy; cy <= maxCy; cy++) { const key = this.hash(cx, cy); const cell = this.cells.get(key); if (!cell) continue; for (const item of cell) { const dx = item.x - x; const dy = item.y - y; if (dx * dx + dy * dy <= rSq) { results.push(item); } } } } return results; } queryRect(x: number, y: number, w: number, h: number): T[] { const results: T[] = []; const minCx = Math.floor(x / this.cellSize); const maxCx = Math.floor((x + w) / this.cellSize); const minCy = Math.floor(y / this.cellSize); const maxCy = Math.floor((y + h) / this.cellSize); for (let cx = minCx; cx <= maxCx; cx++) { for (let cy = minCy; cy <= maxCy; cy++) { const key = this.hash(cx, cy); const cell = this.cells.get(key); if (!cell) continue; for (const item of cell) { if (item.x >= x && item.x <= x + w && item.y >= y && item.y <= y + h) { results.push(item); } } } } return results; } clear(): void { this.cells.clear(); this.itemToCell.clear(); } }