// ═══ 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 }, } ); } }