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,254 @@
|
||||
// ═══ 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 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user