// ═══ utils.js ═══ // ============================================================ // UTILS — shared pure helpers // ============================================================ function distSq(ax, ay, bx, by) { const dx = ax - bx; const dy = ay - by; return dx * dx + dy * dy; } function normalizeAngle(angle) { while (angle > Math.PI) angle -= Math.PI * 2; while (angle < -Math.PI) angle += Math.PI * 2; return angle; } function shortestAngleDelta(from, to) { return normalizeAngle(to - from); } function stepTowardAngle(current, desired, maxStep) { const delta = shortestAngleDelta(current, desired); if (Math.abs(delta) <= maxStep) return normalizeAngle(desired); return normalizeAngle(current + Math.sign(delta) * maxStep); } function compactLiveArray(items, isLive) { let write = 0; for (let i = 0; i < items.length; i++) { const item = items[i]; if (isLive(item)) items[write++] = item; } items.length = write; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function cheapestEnemyCost() { const tier = G?.difficultyTier ?? 0; const prestige = G?.prestigeLevel ?? 0; return ENEMY_DEFS .filter(d => (d.minTier ?? 0) <= tier && (d.minPrestige ?? 0) <= prestige) .reduce((min, d) => Math.min(min, d.cost), Infinity); } function countAliveEnemies() { let count = 0; for (const e of G.enemies) if (e.alive) count++; return count; } function getEquippedWeapons() { return G.weapons.slice(0, G.tower.weaponSlots).filter(Boolean); } function findWeaponInstance(instanceId) { for (const w of G.weapons) if (w && w.instanceId === instanceId) return w; return null; }