622a9fd170
Inverted tower-defense browser game — deploy enemies yourself, tower auto-kills them, pocket credits, upgrade weapons. HTML + Canvas + vanilla JS, no build step. Co-Authored-By: claude-flow <ruv@ruv.net>
91 lines
2.9 KiB
JavaScript
91 lines
2.9 KiB
JavaScript
// ═══ 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();
|
|
}
|
|
|