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>
255 lines
8.7 KiB
JavaScript
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 },
|
|
}
|
|
);
|
|
}
|
|
}
|