Files
siege-protocol/js/weapon-projectiles.js
T
44r0n7 622a9fd170 Initial commit: Siege Protocol
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>
2026-06-16 11:36:53 -04:00

212 lines
6.3 KiB
JavaScript

// ═══ weapon-projectiles.js ═══
// ============================================================
// WEAPON PROJECTILES — projectile, beam, AoE, and chain updates
// ============================================================
// ── UPDATE PROJECTILES ────────────────────────────────────────
function updateProjectiles() {
const W = canvas.width, H = canvas.height;
const cx = W / 2, cy = H / 2;
for (const p of G.projectiles) {
if (p.life <= 0) continue;
if (p.type === 'mortar') {
// Arc toward target
p.progress += (p.speed * 0.6) / Math.hypot(p.tx - p.sx, p.ty - p.sy);
p.progress = Math.min(p.progress, 1);
p.x = p.sx + (p.tx - p.sx) * p.progress;
p.y = p.sy + (p.ty - p.sy) * p.progress;
// Arc height
const arc = Math.sin(p.progress * Math.PI) * 60;
p.y -= arc;
if (p.progress >= 1) {
// Explode
explodeMortar(p);
p.life = 0;
}
continue;
}
// Homing
if (p.homing && p.targetId) {
let target = p.targetRef && p.targetRef.alive ? p.targetRef : null;
if (!target) {
for (const e of G.enemies) {
if (e.id === p.targetId && e.alive) {
target = e;
p.targetRef = e;
break;
}
}
}
if (target) {
const angle = Math.atan2(target.y - p.y, target.x - p.x);
const currentAngle = Math.atan2(p.vy, p.vx);
const delta = normalizeAngle(angle - currentAngle);
const newAngle = currentAngle + Math.sign(delta) * Math.min(Math.abs(delta), p.homingStrength);
const spd = Math.hypot(p.vx, p.vy);
p.vx = Math.cos(newAngle) * spd;
p.vy = Math.sin(newAngle) * spd;
}
}
p.x += p.vx;
p.y += p.vy;
// Out of bounds
if (p.x < -50 || p.x > W+50 || p.y < -50 || p.y > H+50) {
p.life = 0;
continue;
}
// Hit detection
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
const hitRadius = e.radius + p.radius;
if (distSq(p.x, p.y, e.x, e.y) < hitRadius * hitRadius) {
// Evasion check
if (e.evasion && Math.random() < e.evasion) {
spawnFloater(e.x, e.y - 14, 'DODGE', '#aaaaaa', 0.9);
p.life = 0;
break;
}
if (p.aoeRadius > 0) {
explodeProjectile(p);
} else {
dealDamage(e, p.damage, p.elements, true, p.critChance, p.armorShred);
if (e.hp <= 0) killEnemy(e, true);
}
if (p.pierceLeft > 0) {
p.pierceLeft--;
} else {
p.life = 0;
break;
}
}
}
}
compactLiveArray(G.projectiles, p => p.life > 0);
}
function explodeMortar(p) {
sfx_mortar_explode();
spawnParticleBurst(p.x, p.y, p.color, 20);
// Screen shake feel: big particle burst
spawnParticleBurst(p.x, p.y, '#ffffff', 6);
if (p.dotDamage > 0) {
// Lingering zone (poison cloud)
G.aoeZones.push({
id: uid(),
x: p.x, y: p.y,
radius: p.aoeRadius,
color: p.color,
life: 1,
duration: p.dotDuration || 180,
remaining: p.dotDuration || 180,
dotDamage: p.dotDamage,
dotInterval: p.dotInterval || 30,
dotTick: 0,
elements: p.elements,
freezeDuration: p.freezeDuration || 0,
});
return;
}
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
const hitRadius = p.aoeRadius + e.radius;
if (distSq(e.x, e.y, p.x, p.y) < hitRadius * hitRadius) {
dealDamage(e, p.damage, p.elements, false);
if (p.freezeDuration > 0) applyFreeze(e, p.freezeDuration);
if (e.hp <= 0) killEnemy(e, true);
}
}
}
function explodeProjectile(p) {
spawnParticleBurst(p.x, p.y, p.color, 24);
spawnParticleBurst(p.x, p.y, '#ffffff', 5);
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
const hitRadius = p.aoeRadius + e.radius;
if (distSq(e.x, e.y, p.x, p.y) < hitRadius * hitRadius) {
dealDamage(e, p.damage, p.elements, true, p.critChance, p.armorShred);
if (e.hp <= 0) killEnemy(e, true);
}
}
p.life = 0;
}
function updateBeams() {
compactLiveArray(G.beams, b => {
b.life -= b.decay;
return b.life > 0;
});
}
function updateAoeZones() {
for (const z of G.aoeZones) {
z.remaining--;
z.life = z.remaining / z.duration;
z.dotTick++;
if (z.dotTick >= z.dotInterval) {
z.dotTick = 0;
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
const hitRadius = z.radius + e.radius;
if (distSq(e.x, e.y, z.x, z.y) < hitRadius * hitRadius) {
dealDamage(e, z.dotDamage, z.elements, false);
if (e.hp <= 0) killEnemy(e, true);
}
}
}
}
compactLiveArray(G.aoeZones, z => z.remaining > 0);
}
// Chain arc visual helper — pushes a glowing zigzag bolt to G.chainArcs
function spawnChainArc(x1, y1, x2, y2, color) {
const dx = x2 - x1, dy = y2 - y1;
const len = Math.hypot(dx, dy) || 1;
const perpX = -dy / len, perpY = dx / len;
const steps = 10;
const pts = [{ x: x1, y: y1 }];
for (let i = 1; i < steps; i++) {
const t = i / steps;
const maxOff = 26 * Math.sin(t * Math.PI); // taper to 0 at both ends
const off = (Math.random() - 0.5) * 2 * maxOff;
pts.push({ x: x1 + dx * t + perpX * off, y: y1 + dy * t + perpY * off });
}
pts.push({ x: x2, y: y2 });
G.chainArcs.push({ pts, color, life: 1.0, decay: 0.16 });
// Short branch off the midpoint for realism
const mid = pts[Math.floor(steps / 2)];
const branchAngle = Math.atan2(dy, dx) + (Math.random() < 0.5 ? 1 : -1) * (Math.PI * 0.3 + Math.random() * 0.4);
const branchLen = len * 0.3;
const bpts = [{ x: mid.x, y: mid.y }];
const bSteps = 4;
for (let i = 1; i <= bSteps; i++) {
const t = i / bSteps;
const off = (Math.random() - 0.5) * 12 * Math.sin(t * Math.PI);
bpts.push({
x: mid.x + Math.cos(branchAngle) * branchLen * t + perpX * off,
y: mid.y + Math.sin(branchAngle) * branchLen * t + perpY * off,
});
}
G.chainArcs.push({ pts: bpts, color, life: 0.65, decay: 0.20 });
}
function updateChainArcs() {
compactLiveArray(G.chainArcs, a => {
a.life -= a.decay;
return a.life > 0;
});
}