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>
212 lines
6.3 KiB
JavaScript
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;
|
|
});
|
|
}
|