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:
2026-06-16 11:36:53 -04:00
commit 622a9fd170
31 changed files with 6164 additions and 0 deletions
+208
View File
@@ -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();
}