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>
209 lines
6.5 KiB
JavaScript
209 lines
6.5 KiB
JavaScript
// ═══ weapons.js ═══
|
|
// ============================================================
|
|
// WEAPONS.JS — Firing logic for all weapon types
|
|
// ============================================================
|
|
|
|
const AIM_THRESHOLD = 0.18; // radians - tighter aim requirement
|
|
const HARDPOINT_BASE_ANGLE = -Math.PI / 2;
|
|
|
|
function getHardpointOrbit(totalSlots = Math.max(1, G.tower.weaponSlots)) {
|
|
// Wider orbit as slots increase so each hardpoint stays readable.
|
|
return 21 + Math.min(12, totalSlots * 1.6);
|
|
}
|
|
|
|
function getSlotHardpoint(slotIndex, cx, cy, totalSlots = Math.max(1, G.tower.weaponSlots)) {
|
|
const orbit = getHardpointOrbit(totalSlots);
|
|
const mountAngle = HARDPOINT_BASE_ANGLE + (slotIndex / totalSlots) * Math.PI * 2;
|
|
return {
|
|
slotIndex,
|
|
totalSlots,
|
|
mountAngle,
|
|
x: cx + Math.cos(mountAngle) * orbit,
|
|
y: cy + Math.sin(mountAngle) * orbit,
|
|
orbit,
|
|
};
|
|
}
|
|
|
|
function getWeaponHardpoints(cx, cy) {
|
|
const totalSlots = Math.max(1, G.tower.weaponSlots);
|
|
const result = [];
|
|
for (let slotIndex = 0; slotIndex < totalSlots; slotIndex++) {
|
|
const weapon = G.weapons[slotIndex];
|
|
if (!weapon) continue;
|
|
const mount = getSlotHardpoint(slotIndex, cx, cy, totalSlots);
|
|
result.push({ ...mount, weapon });
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getWeaponBarrelLength(weapon) {
|
|
const type = getWeaponDef(weapon)?.type;
|
|
if (type === 'beam') return 24;
|
|
if (type === 'mortar') return 14;
|
|
if (type === 'cone') return 15;
|
|
if (type === 'multi') return 19;
|
|
if (type === 'chain') return 18;
|
|
return 17;
|
|
}
|
|
|
|
function getWeaponMuzzle(hardpoint) {
|
|
const w = hardpoint.weapon;
|
|
const aimAngle = typeof w.aimAngle === 'number' ? w.aimAngle : hardpoint.mountAngle;
|
|
const barrelLen = getWeaponBarrelLength(w);
|
|
const recoilOffset = (w.recoil || 0) * 3.2;
|
|
const reach = Math.max(7, barrelLen - recoilOffset);
|
|
return {
|
|
x: hardpoint.x + Math.cos(aimAngle) * reach,
|
|
y: hardpoint.y + Math.sin(aimAngle) * reach,
|
|
angle: aimAngle,
|
|
};
|
|
}
|
|
|
|
function clampAimPoint(x, y) {
|
|
const pad = 18;
|
|
return {
|
|
x: Math.max(pad, Math.min(canvas.width - pad, x)),
|
|
y: Math.max(pad, Math.min(canvas.height - pad, y)),
|
|
};
|
|
}
|
|
|
|
function predictEnemyFuturePoint(target, framesAhead) {
|
|
if (typeof predictEnemyPositionAlongPath === 'function') {
|
|
return predictEnemyPositionAlongPath(target, framesAhead);
|
|
}
|
|
return {
|
|
x: target.x + (target.vx || 0) * framesAhead,
|
|
y: target.y + (target.vy || 0) * framesAhead,
|
|
};
|
|
}
|
|
|
|
function predictInterceptPoint(ox, oy, target, projectileSpeed, maxLeadFrames = 180) {
|
|
const speed = Math.max(0.01, projectileSpeed);
|
|
|
|
let t = Math.hypot(target.x - ox, target.y - oy) / speed;
|
|
t = Math.max(0, Math.min(maxLeadFrames, t));
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const p = predictEnemyFuturePoint(target, t);
|
|
const dist = Math.hypot(p.x - ox, p.y - oy);
|
|
const nextT = Math.max(0, Math.min(maxLeadFrames, dist / speed));
|
|
if (Math.abs(nextT - t) < 0.2) {
|
|
t = nextT;
|
|
break;
|
|
}
|
|
t = nextT;
|
|
}
|
|
|
|
const predicted = predictEnemyFuturePoint(target, t);
|
|
return clampAimPoint(predicted.x, predicted.y);
|
|
}
|
|
|
|
function getLeadAimPoint(w, target, ox, oy) {
|
|
if (!w || !target) return null;
|
|
const def = getWeaponDef(w);
|
|
if (!def) return { x: target.x, y: target.y };
|
|
|
|
if (def.type === 'projectile') {
|
|
return predictInterceptPoint(ox, oy, target, Math.max(0.1, w.projectileSpeed || 0.1), 220);
|
|
}
|
|
|
|
if (def.type === 'mortar') {
|
|
// Mortar progress uses speed^2 * 0.6 in updateProjectiles.
|
|
const ps = Math.max(0.1, w.projectileSpeed || 0.1);
|
|
const planarSpeed = ps * ps * 0.6;
|
|
return predictInterceptPoint(ox, oy, target, planarSpeed, 260);
|
|
}
|
|
|
|
return { x: target.x, y: target.y };
|
|
}
|
|
|
|
function getWeaponColor(w) {
|
|
const el = getWeaponElements(w)[0];
|
|
return ELEMENTS[el]?.color || getWeaponDef(w)?.color || '#fff';
|
|
}
|
|
|
|
function getIdleScanAngle(hardpoint) {
|
|
const sweep = Math.PI / 4; // 90 degree cone, centered away from tower.
|
|
const phase = hardpoint.slotIndex * 1.7;
|
|
return normalizeAngle(hardpoint.mountAngle + Math.sin(G.frame * 0.0125 + phase) * sweep);
|
|
}
|
|
|
|
function updateWeapons() {
|
|
const cx = ARENA_CX, cy = ARENA_CY;
|
|
const hardpoints = getWeaponHardpoints(cx, cy);
|
|
let hasPrimaryAim = false;
|
|
|
|
for (const hardpoint of hardpoints) {
|
|
const w = hardpoint.weapon;
|
|
const mountAngle = hardpoint.mountAngle;
|
|
|
|
if (typeof w.aimAngle !== 'number') w.aimAngle = mountAngle;
|
|
if (typeof w.cooldown !== 'number') w.cooldown = 0;
|
|
if (typeof w.recoil !== 'number') w.recoil = 0;
|
|
if (typeof w.muzzleFlash !== 'number') w.muzzleFlash = 0;
|
|
|
|
w.recoil *= 0.68;
|
|
if (w.recoil < 0.01) w.recoil = 0;
|
|
if (w.muzzleFlash > 0) w.muzzleFlash--;
|
|
|
|
const target = pickTarget(w);
|
|
let aimPoint = null;
|
|
let desiredAngle = null;
|
|
if (target) {
|
|
aimPoint = getLeadAimPoint(w, target, hardpoint.x, hardpoint.y);
|
|
desiredAngle = Math.atan2(aimPoint.y - hardpoint.y, aimPoint.x - hardpoint.x);
|
|
w.aimAngle = stepTowardAngle(w.aimAngle, desiredAngle, G.tower.aimSpeed);
|
|
} else {
|
|
const scanDelta = shortestAngleDelta(w.aimAngle, mountAngle);
|
|
if (Math.abs(scanDelta) > Math.PI / 4) w.aimAngle = mountAngle;
|
|
desiredAngle = getIdleScanAngle(hardpoint);
|
|
w.aimAngle = stepTowardAngle(w.aimAngle, desiredAngle, G.tower.aimSpeed * 0.45);
|
|
}
|
|
|
|
if (!hasPrimaryAim) {
|
|
G.tower.cannonAngle = w.aimAngle;
|
|
hasPrimaryAim = true;
|
|
}
|
|
|
|
if (w.cooldown > 0) {
|
|
w.cooldown--;
|
|
continue;
|
|
}
|
|
if (!target) continue;
|
|
|
|
const dx = target.x - hardpoint.x;
|
|
const dy = target.y - hardpoint.y;
|
|
const dist = Math.hypot(dx, dy);
|
|
if (dist > w.range) continue;
|
|
|
|
let fireAngle = w.aimAngle;
|
|
|
|
// Aim check for directional weapons.
|
|
if (w.defId !== 'flamethrower' && w.defId !== 'laser') {
|
|
const aimDelta = shortestAngleDelta(w.aimAngle, desiredAngle);
|
|
if (Math.abs(aimDelta) > AIM_THRESHOLD) continue;
|
|
// Snap to exact target angle on shot so first-round accuracy is reliable.
|
|
fireAngle = desiredAngle;
|
|
w.aimAngle = desiredAngle;
|
|
} else if (desiredAngle != null) {
|
|
fireAngle = desiredAngle;
|
|
w.aimAngle = desiredAngle;
|
|
}
|
|
|
|
const muzzle = getWeaponMuzzle(hardpoint);
|
|
fireWeapon(w, target, muzzle.x, muzzle.y, fireAngle, aimPoint);
|
|
w.cooldown = Math.max(4, w.fireRate);
|
|
w.recoil = 1;
|
|
w.muzzleFlash = 4;
|
|
w.lastFireFrame = G.frame;
|
|
}
|
|
|
|
if (!hasPrimaryAim) G.tower.cannonAngle = normalizeAngle((G.tower.cannonAngle || 0) + 0.006);
|
|
|
|
// Update projectiles
|
|
updateProjectiles();
|
|
updateBeams();
|
|
updateChainArcs();
|
|
updateAoeZones();
|
|
}
|