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:
+208
@@ -0,0 +1,208 @@
|
||||
// ═══ 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();
|
||||
}
|
||||
Reference in New Issue
Block a user