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>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
// ═══ 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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user