// ═══ 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(); }