Initial commit: GunCircle.io multiplayer arena shooter
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// 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<T extends { x: number; y: number; id: number }> {
|
||||
private cells = new Map<string, T[]>();
|
||||
private itemToCell = new Map<number, string>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user