Files
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

255 lines
8.7 KiB
JavaScript

// ═══ weapon-fire.js ═══
// ============================================================
// WEAPON FIRE — dispatch and per-weapon shot handlers
// ============================================================
function pushStandardProjectile(w, target, ox, oy, vx, vy, opts = {}) {
const elements = opts.elements || getWeaponElements(w);
const color = opts.color || getWeaponColor(w);
G.projectiles.push({
id: uid(),
x: ox, y: oy,
vx, vy,
radius: opts.radius ?? w.projectileRadius ?? 4,
damage: opts.damage ?? w.damage,
elements,
targetId: target.id,
targetRef: target,
pierce: opts.pierce ?? w.pierce ?? 0,
pierceLeft: opts.pierceLeft ?? w.pierce ?? 0,
critChance: opts.critChance ?? w.critChance ?? 0,
armorShred: opts.armorShred ?? w.armorShred ?? 0,
color,
glow: ELEMENTS[elements[0]]?.glow || color,
life: 1,
weaponId: w.instanceId,
type: 'standard',
...opts.extra,
});
}
function fireWeapon(w, target, ox, oy, aimAngle, aimPoint = null) {
const def = getWeaponDef(w);
// Sound — throttle rapid-fire weapons
const throttleMs = (def.fireRate < 15) ? 80 : 0;
if (throttleMs) sfx_weapon_fire_throttled(def.id, throttleMs);
else sfx_weapon_fire(def.id);
switch (def.type) {
case 'projectile': fireProjectile(w, target, ox, oy, aimAngle); break;
case 'cone': fireCone(w, target, ox, oy, aimAngle); break;
case 'chain': fireChain(w, target, ox, oy); break;
case 'mortar': fireMortar(w, target, ox, oy, aimPoint); break;
case 'beam': fireBeam(w, target, ox, oy, aimAngle); break;
case 'multi': fireMulti(w, ox, oy); break;
}
}
// ── PROJECTILE ────────────────────────────────────────────────
function fireProjectile(w, target, ox, oy, aimAngle) {
const angle = typeof aimAngle === 'number' ? aimAngle : Math.atan2(target.y - oy, target.x - ox);
pushStandardProjectile(
w, target, ox, oy,
Math.cos(angle) * w.projectileSpeed,
Math.sin(angle) * w.projectileSpeed
);
}
// ── CONE (flamethrower) ───────────────────────────────────────
function fireCone(w, target, ox, oy, aimAngle) {
const baseAngle = typeof aimAngle === 'number' ? aimAngle : Math.atan2(target.y - oy, target.x - ox);
const halfCone = w.coneAngle / 2;
const range = w.range;
const elements = getWeaponElements(w);
// Check all enemies in cone
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
const ex = e.x - ox, ey = e.y - oy;
if (ex * ex + ey * ey > range * range) continue;
const angle = Math.atan2(ey, ex);
const diff = normalizeAngle(angle - baseAngle);
if (Math.abs(diff) > halfCone) continue;
dealDamage(e, w.damage, elements, false, 0, 0);
}
// Visual: layered flame particles
// Core jet — bright yellow, large, near barrel
for (let i = 0; i < 5; i++) {
const a = baseAngle + (Math.random() - 0.5) * halfCone * 1.1;
const dist = 10 + Math.random() * range * 0.55;
const t = dist / range;
const color = t < 0.3 ? '#ffee55' : t < 0.55 ? '#ffaa22' : '#ff6600';
spawnParticle(
ox + Math.cos(a) * dist, oy + Math.sin(a) * dist,
color,
Math.cos(a) * 0.6 + (Math.random()-0.5)*0.5,
Math.sin(a) * 0.6 + (Math.random()-0.5)*0.5 - 0.5,
0.85 + Math.random() * 0.15,
(1 - t) * 8 + 3 + Math.random() * 3,
0.06 + Math.random() * 0.04
);
}
// Body — orange, mid-range, billowing
for (let i = 0; i < 7; i++) {
const a = baseAngle + (Math.random() - 0.5) * halfCone * 2.4;
const dist = 20 + Math.random() * range * 0.80;
const t = dist / range;
const color = t < 0.45 ? '#ff8822' : t < 0.72 ? '#ff4400' : '#cc2200';
spawnParticle(
ox + Math.cos(a) * dist, oy + Math.sin(a) * dist,
color,
(Math.random()-0.5) * 1.1,
(Math.random()-0.5) * 0.8 - 0.7,
0.6 + Math.random() * 0.3,
(1 - t * 0.5) * 5 + 2 + Math.random() * 3,
0.05 + Math.random() * 0.03
);
}
// Embers / smoke — dark red and charcoal, drift upward
for (let i = 0; i < 3; i++) {
const a = baseAngle + (Math.random() - 0.5) * halfCone * 2.8;
const dist = range * 0.45 + Math.random() * range * 0.55;
const color = Math.random() < 0.55 ? '#ff3300' : '#332211';
spawnParticle(
ox + Math.cos(a) * dist, oy + Math.sin(a) * dist,
color,
(Math.random()-0.5) * 0.6,
-0.7 - Math.random() * 0.6,
0.5 + Math.random() * 0.3,
2 + Math.random() * 4,
0.03 + Math.random() * 0.02
);
}
}
// ── CHAIN LIGHTNING ───────────────────────────────────────────
function fireChain(w, target, ox, oy) {
const elements = getWeaponElements(w);
let current = target;
const hit = new Set([target.id]);
let dmg = w.damage;
let prev = { x: ox, y: oy };
for (let i = 0; i <= w.chains; i++) {
dealDamage(current, dmg, elements, false);
spawnChainArc(prev.x, prev.y, current.x, current.y, getWeaponColor(w));
prev = { x: current.x, y: current.y };
dmg *= 0.7;
let next = null;
let nextDist = w.chainRange * w.chainRange;
for (const e of G.enemies) {
if (!e.alive || hit.has(e.id)) continue;
const d = distSq(e.x, e.y, current.x, current.y);
if (d < nextDist) {
next = e;
nextDist = d;
}
}
if (!next) break;
hit.add(next.id);
current = next;
}
}
// ── MORTAR / FREEZE BOMB / POISON CLOUD ──────────────────────
function fireMortar(w, target, ox, oy, aimPoint = null) {
const tx = aimPoint?.x ?? target.x;
const ty = aimPoint?.y ?? target.y;
const elements = getWeaponElements(w);
const color = getWeaponColor(w);
G.projectiles.push({
id: uid(),
x: ox, y: oy,
tx, ty,
vx: 0, vy: 0,
radius: 6,
damage: w.damage,
elements,
aoeRadius: w.aoeRadius,
freezeDuration: w.freezeDuration ?? 0,
dotDamage: w.dotDamage ?? 0,
dotInterval: w.dotInterval ?? 0,
dotDuration: w.dotDuration ?? 0,
color,
glow: ELEMENTS[elements[0]]?.glow || color,
life: 1,
type: 'mortar',
weaponId: w.instanceId,
// Mortar arc
progress: 0,
speed: w.projectileSpeed ?? 3.5,
sx: ox, sy: oy,
peaked: false,
});
}
// ── BEAM (laser) ──────────────────────────────────────────────
function fireBeam(w, target, ox, oy, aimAngle) {
const angle = typeof aimAngle === 'number' ? aimAngle : Math.atan2(target.y - oy, target.x - ox);
const elements = getWeaponElements(w);
// Check all enemies along the beam line
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
if (isOnBeamLine(ox, oy, angle, e.x, e.y, e.radius + 8)) {
dealDamage(e, w.damage, elements, false);
}
}
// Visual beam flash
G.beams.push({
id: uid(),
x1: ox, y1: oy,
angle,
length: Math.max(canvas.width, canvas.height),
color: getWeaponColor(w),
glow: ELEMENTS[elements[0]]?.glow || getWeaponColor(w),
life: 1,
decay: 0.15,
});
}
function isOnBeamLine(ox, oy, angle, px, py, radius) {
const dx = Math.cos(angle), dy = Math.sin(angle);
const ex = px - ox, ey = py - oy;
const t = ex * dx + ey * dy;
if (t < 0) return false;
const closestX = ox + dx * t;
const closestY = oy + dy * t;
return distSq(closestX, closestY, px, py) < radius * radius;
}
// ── MULTI (missile pod) ───────────────────────────────────────
function fireMulti(w, ox, oy) {
const targets = [];
const maxTargets = Math.max(1, w.targets || 1);
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
let insertAt = targets.length;
while (insertAt > 0 && targets[insertAt - 1].hp > e.hp) insertAt--;
if (insertAt >= maxTargets) continue;
targets.splice(insertAt, 0, e);
if (targets.length > maxTargets) targets.length = maxTargets;
}
if (targets.length === 0) return;
const elements = getWeaponElements(w);
const color = getWeaponColor(w);
for (const t of targets) {
const angle = Math.atan2(t.y - oy, t.x - ox);
const spread = (Math.random() - 0.5) * 0.3;
pushStandardProjectile(
w, t, ox, oy,
Math.cos(angle + spread) * w.projectileSpeed,
Math.sin(angle + spread) * w.projectileSpeed,
{
radius: 4,
elements,
color,
pierce: 0,
pierceLeft: 0,
armorShred: 0,
extra: { homing: true, homingStrength: 0.12, aoeRadius: w.aoeRadius ?? 0 },
}
);
}
}