Files
siege-protocol/js/renderer-tower.js
44r0n7 622a9fd170 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>
2026-06-16 11:36:53 -04:00

215 lines
6.9 KiB
JavaScript

// ═══ renderer-tower.js ═══
// ============================================================
// RENDERER TOWER — tower hardpoints and shields
// ============================================================
// ── TOWER ─────────────────────────────────────────────────────
function drawTower(x, y) {
const t = G.frame;
const hpRatio = G.tower.hp / G.tower.maxHp;
const color = hpRatio > 0.5 ? '#00d4ff' : hpRatio > 0.25 ? '#ffd700' : '#ff3355';
const pulse = 0.6 + 0.4 * Math.sin(t * 0.05);
const totalSlots = Math.max(1, G.tower.weaponSlots);
const hardpointOrbit = getHardpointOrbit(totalSlots);
// Pulse ring
ctx.strokeStyle = color + '44';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, 36 + pulse * 4 + (hardpointOrbit - 20) * 0.45, 0, Math.PI*2);
ctx.stroke();
// Base chassis
ctx.fillStyle = '#050a0e';
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(x, y, 24, 0, Math.PI*2); ctx.fill(); ctx.stroke();
// Inner glow
const grd = ctx.createRadialGradient(x, y, 0, x, y, 20);
grd.addColorStop(0, color + '44');
grd.addColorStop(1, 'transparent');
ctx.fillStyle = grd;
ctx.beginPath(); ctx.arc(x, y, 20, 0, Math.PI*2); ctx.fill();
// Hardpoint ring
ctx.strokeStyle = color + '2d';
ctx.lineWidth = 1;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.arc(x, y, hardpointOrbit, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// Draw all unlocked mount sockets (filled when weapon installed)
for (let slotIndex = 0; slotIndex < totalSlots; slotIndex++) {
const mount = getSlotHardpoint(slotIndex, x, y, totalSlots);
const installed = !!G.weapons[slotIndex];
ctx.fillStyle = installed ? '#0d1f2a' : '#061018';
ctx.strokeStyle = installed ? color + '88' : '#1a3240';
ctx.lineWidth = installed ? 1.8 : 1.2;
ctx.beginPath();
ctx.arc(mount.x, mount.y, installed ? 5.4 : 4.2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
// Weapon hardpoints and independent turrets
const hardpoints = getWeaponHardpoints(x, y);
ctx.shadowBlur = 7;
for (const hp of hardpoints) {
const w = hp.weapon;
const def = getWeaponDef(w);
if (!def) continue;
const wColor = ELEMENTS[w.elements[0]]?.color || def.color || '#c8c8c8';
const wGlow = ELEMENTS[w.elements[0]]?.glow || wColor;
const aim = typeof w.aimAngle === 'number' ? w.aimAngle : hp.mountAngle;
const barrelLen = getWeaponBarrelLength(w);
const recoilPx = (w.recoil || 0) * 3.2;
// Brace arm
ctx.shadowBlur = 0;
ctx.strokeStyle = '#10293a';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(hp.x, hp.y);
ctx.stroke();
// Mount cap
ctx.fillStyle = '#07141d';
ctx.strokeStyle = wColor;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(hp.x, hp.y, 5.8, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
// Turret barrel
ctx.shadowBlur = 8;
ctx.shadowColor = wGlow;
ctx.fillStyle = wColor;
ctx.save();
ctx.translate(hp.x, hp.y);
ctx.rotate(aim);
const barrelDrawLen = Math.max(7, barrelLen - recoilPx);
let flashX = barrelDrawLen + 2;
let flashY = 0;
switch (def.type) {
case 'beam':
ctx.fillRect(0, -3.2, barrelDrawLen, 1.9);
ctx.fillRect(0, 1.3, barrelDrawLen, 1.9);
ctx.fillStyle = '#9fe6ff';
ctx.fillRect(barrelDrawLen - 3, -1.1, 3, 2.2);
break;
case 'cone':
ctx.fillRect(0, -2.8, Math.max(7, barrelDrawLen * 0.58), 5.6);
ctx.beginPath();
ctx.moveTo(barrelDrawLen * 0.56, -4.6);
ctx.lineTo(barrelDrawLen + 1, 0);
ctx.lineTo(barrelDrawLen * 0.56, 4.6);
ctx.closePath();
ctx.fill();
break;
case 'mortar':
ctx.fillRect(-1, -4.8, Math.max(6, barrelDrawLen * 0.72), 9.6);
ctx.beginPath();
ctx.arc(3, 0, 4.8, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#dff6ff';
ctx.fillRect(Math.max(6, barrelDrawLen * 0.7) - 2, -1.6, 2, 3.2);
flashX = Math.max(6, barrelDrawLen * 0.72) + 1.5;
break;
case 'multi':
ctx.fillRect(0, -4.2, barrelDrawLen, 2.6);
ctx.fillRect(0, 1.6, barrelDrawLen, 2.6);
ctx.fillStyle = '#dff6ff';
ctx.fillRect(barrelDrawLen - 2, -4.1, 2, 2.4);
ctx.fillRect(barrelDrawLen - 2, 1.7, 2, 2.4);
break;
case 'chain':
ctx.fillRect(0, -2.7, barrelDrawLen - 1, 5.4);
ctx.beginPath();
ctx.moveTo(barrelDrawLen - 2, -2.7);
ctx.lineTo(barrelDrawLen + 1.8, -4.8);
ctx.lineTo(barrelDrawLen - 0.1, -0.7);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(barrelDrawLen - 2, 2.7);
ctx.lineTo(barrelDrawLen + 1.8, 4.8);
ctx.lineTo(barrelDrawLen - 0.1, 0.7);
ctx.closePath();
ctx.fill();
break;
default:
ctx.fillRect(0, -2.9, barrelDrawLen, 5.8);
ctx.fillStyle = '#dff6ff';
ctx.fillRect(barrelDrawLen - 2, -1.4, 2, 2.8);
break;
}
if ((w.muzzleFlash || 0) > 0) {
const flashAlpha = Math.min(1, w.muzzleFlash / 4);
ctx.globalAlpha = flashAlpha;
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(flashX, flashY, 2.5 + flashAlpha * 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
ctx.restore();
}
ctx.shadowBlur = 0;
// Center hub
ctx.fillStyle = '#050a0e';
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.textAlign = 'left';
}
// ── SHIELD ────────────────────────────────────────────────────
function drawShield(x, y) {
if (!G.tower.shield || G.tower.shieldHp <= 0) return;
const alpha = G.tower.shieldHp / G.tower.shieldMaxHp;
ctx.save();
if (G.tower.shield === 'dome') {
const grd = ctx.createRadialGradient(x, y, 20, x, y, 50);
grd.addColorStop(0, 'transparent');
grd.addColorStop(1, '#00d4ff');
ctx.globalAlpha = 0.15 + alpha * 0.2;
ctx.fillStyle = grd;
ctx.beginPath(); ctx.arc(x, y, 46, 0, Math.PI*2); ctx.fill();
ctx.globalAlpha = alpha * 0.7;
ctx.strokeStyle = '#00d4ff'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(x, y, 46, 0, Math.PI*2); ctx.stroke();
} else if (G.tower.shield === 'directional') {
const arc = G.tower.shieldArcWidth ?? (Math.PI * 0.6);
const angle = G.tower.shieldAngle;
ctx.globalAlpha = alpha * 0.7;
ctx.fillStyle = '#00d4ff22';
ctx.beginPath();
ctx.moveTo(x, y);
ctx.arc(x, y, 50, angle - arc/2, angle + arc/2);
ctx.closePath(); ctx.fill();
ctx.strokeStyle = '#00d4ff'; ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(x, y, 50, angle - arc/2, angle + arc/2);
ctx.stroke();
}
ctx.restore();
}