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