// ═══ portals.js ═══ // ============================================================ // PORTALS — portal spawning and lifecycle // ============================================================ const PORTAL_COUNT = 3; const PORTAL_OPEN_TIME = 180; const PORTAL_COOLDOWN_MIN = 300; const PORTAL_COOLDOWN_MAX = 600; const PORTAL_SPAWN_INTERVAL = 18; const PORTAL_RADIUS = ARENA_RADIUS - 32; function splitInteger(total, parts) { const base = Math.floor(total / parts); let remainder = total - base * parts; const out = []; for (let i = 0; i < parts; i++) { out.push(base + (remainder > 0 ? 1 : 0)); if (remainder > 0) remainder--; } return out; } // ── PORTAL MANAGEMENT ───────────────────────────────────────── function updatePortals() { // Tick existing portals for (const p of G.portals) { p.life--; p.angle += 0.02; // Spawn enemies from open portals if (p.spawnQueue.length > 0 && p.life > 0) { // For swarm portals: burst spawn all at once in a ring if (p.isBurst && p.spawnTimer <= 0) { const total = p.spawnQueue.length; p.spawnQueue.forEach((entry, i) => { const angle = (i / total) * Math.PI * 2; if (entry?.def) { spawnEnemy(entry.def, p.x, p.y, entry.reward, angle, entry.cost, entry.breachRiskMult); } }); p.spawnQueue = []; } else { p.spawnTimer--; if (p.spawnTimer <= 0 && !p.isBurst) { p.spawnTimer = PORTAL_SPAWN_INTERVAL; const entry = p.spawnQueue.shift(); if (entry?.def) { spawnEnemy(entry.def, p.x, p.y, entry.reward, null, entry.cost, entry.breachRiskMult); } } } } } compactLiveArray(G.portals, p => p.life > 0); } function openPortal(enemyDef, count = 1, rewardPerUnit = null, isBurst = false, costPerUnit = null, breachRiskMult = 1, rewardSequence = null, fixedAngle = null) { const cx = ARENA_CX, cy = ARENA_CY; let x, y, angle, attempts = 0; do { angle = fixedAngle !== null ? fixedAngle : Math.random() * Math.PI * 2; x = cx + Math.cos(angle) * PORTAL_RADIUS; y = cy + Math.sin(angle) * PORTAL_RADIUS; attempts++; } while (attempts < 20 && fixedAngle === null && G.portals.some(p => distSq(p.x, p.y, x, y) < 80 * 80)); const queue = []; for (let i = 0; i < count; i++) { const reward = Array.isArray(rewardSequence) ? rewardSequence[i] : rewardPerUnit; queue.push({ def: enemyDef, reward, cost: costPerUnit, breachRiskMult }); } G.portals.push({ id: uid(), x, y, life: PORTAL_OPEN_TIME + (isBurst ? 10 : count * PORTAL_SPAWN_INTERVAL), maxLife: PORTAL_OPEN_TIME + (isBurst ? 10 : count * PORTAL_SPAWN_INTERVAL), angle: Math.random() * Math.PI * 2, spawnQueue: queue, spawnTimer: 30, isBurst, defId: enemyDef.id, color: enemyDef.color, }); sfx_portal_open(); }