// ═══ renderer-combat.js ═══ // ============================================================ // RENDERER COMBAT — enemies, weapons, tower, particles // ============================================================ // ── AOE ZONES ───────────────────────────────────────────────── function drawAoeZones() { for (const z of G.aoeZones) { ctx.save(); ctx.globalAlpha = z.life * 0.25; ctx.fillStyle = z.color; ctx.beginPath(); ctx.arc(z.x, z.y, z.radius, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = z.life * 0.5; ctx.strokeStyle = z.color; ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); } } // ── ENEMY TRAILS ────────────────────────────────────────────── function drawEnemyTrails() { // Batch all trails in one pass — no save/restore per trail ctx.lineCap = 'round'; for (const e of G.enemies) { if (!e.alive || e.trail.length < 2) continue; ctx.strokeStyle = e.color + '22'; ctx.lineWidth = e.radius * 0.7; ctx.beginPath(); ctx.moveTo(e.trail[0].x, e.trail[0].y); for (let i = 1; i < e.trail.length; i++) ctx.lineTo(e.trail[i].x, e.trail[i].y); ctx.stroke(); } ctx.lineCap = 'butt'; } // ── ENEMY SHAPES ────────────────────────────────────────────── function drawEnemyShape(e, bodyColor) { const x = e.x, y = e.y, r = e.radius; const t = G.frame * 0.04; ctx.fillStyle = bodyColor; ctx.strokeStyle = bodyColor; switch (e.defId) { case 'grunt': ctx.beginPath(); ctx.rect(x - r, y - r, r * 2, r * 2); ctx.fill(); ctx.strokeStyle = '#ffffff33'; ctx.lineWidth = 1; ctx.stroke(); break; case 'runner': { const ang = Math.atan2(canvas.height/2 - y, canvas.width/2 - x); ctx.save(); ctx.translate(x, y); ctx.rotate(ang); ctx.beginPath(); ctx.moveTo(r*1.6,0); ctx.lineTo(0,r*0.65); ctx.lineTo(-r*0.8,0); ctx.lineTo(0,-r*0.65); ctx.closePath(); ctx.fill(); ctx.restore(); break; } case 'brute': ctx.beginPath(); for (let i = 0; i < 6; i++) { const a=(i/6)*Math.PI*2-Math.PI/6; i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r); } ctx.closePath(); ctx.fill(); ctx.strokeStyle='#ffffff33'; ctx.lineWidth=2; ctx.stroke(); break; case 'swarm': { const ang = Math.atan2(canvas.height/2 - y, canvas.width/2 - x); ctx.save(); ctx.translate(x,y); ctx.rotate(ang); ctx.beginPath(); ctx.moveTo(r*1.2,0); ctx.lineTo(-r*0.8,r*0.8); ctx.lineTo(-r*0.8,-r*0.8); ctx.closePath(); ctx.fill(); ctx.restore(); break; } case 'phantom': ctx.globalAlpha *= 0.85 + Math.sin(t*3.1 + e.x*0.1)*0.15; ctx.beginPath(); for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2,rad=i%2===0?r:r*0.45;i===0?ctx.moveTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad):ctx.lineTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad);} ctx.closePath(); ctx.fill(); break; case 'iceling': ctx.beginPath(); for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2+Math.PI/8;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);} ctx.closePath(); ctx.fill(); ctx.strokeStyle='#ffffff55'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(x-r*.5,y-r*.5); ctx.lineTo(x+r*.5,y+r*.5); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x+r*.5,y-r*.5); ctx.lineTo(x-r*.5,y+r*.5); ctx.stroke(); break; case 'sparkling': ctx.beginPath(); for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2,rad=(i%2===0?r:r*0.5)+r*0.12*Math.sin(t*5+i);i===0?ctx.moveTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad):ctx.lineTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad);} ctx.closePath(); ctx.fill(); break; case 'venom': ctx.beginPath(); for (let i=0;i<=7;i++){const a=(i/7)*Math.PI*2,rad=r*(1+0.22*Math.sin(t*2+i*1.3+e.x*0.05));i===0?ctx.moveTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad):ctx.lineTo(x+Math.cos(a)*rad,y+Math.sin(a)*rad);} ctx.closePath(); ctx.fill(); break; case 'titan': ctx.beginPath(); for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);} ctx.closePath(); ctx.fill(); ctx.beginPath(); for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2;i===0?ctx.moveTo(x+Math.cos(a)*r*.6,y+Math.sin(a)*r*.6):ctx.lineTo(x+Math.cos(a)*r*.6,y+Math.sin(a)*r*.6);} ctx.closePath(); ctx.strokeStyle='#ff000099'; ctx.lineWidth=2.5; ctx.stroke(); break; case 'wraith': { const pulseR = r + Math.sin(t*2 + e.x*0.05)*2; ctx.strokeStyle = bodyColor; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(x, y, pulseR, 0, Math.PI*2); ctx.stroke(); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(x, y, pulseR*0.5, 0, Math.PI*2); ctx.stroke(); break; } case 'commander': { // Pentagon body with rank stripes ctx.beginPath(); for (let i=0;i<5;i++){const a=(i/5)*Math.PI*2-Math.PI/2;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);} ctx.closePath(); ctx.fill(); ctx.strokeStyle='#ffffff44'; ctx.lineWidth=1.5; ctx.stroke(); // Rank stripes ctx.strokeStyle='#000000aa'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.moveTo(x-r*0.5,y-r*0.2); ctx.lineTo(x+r*0.5,y-r*0.2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x-r*0.35,y+r*0.1); ctx.lineTo(x+r*0.35,y+r*0.1); ctx.stroke(); break; } case 'juggernaut': { // Heavy octagon ctx.beginPath(); for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2;i===0?ctx.moveTo(x+Math.cos(a)*r,y+Math.sin(a)*r):ctx.lineTo(x+Math.cos(a)*r,y+Math.sin(a)*r);} ctx.closePath(); ctx.fill(); ctx.strokeStyle='#ff000066'; ctx.lineWidth=3; ctx.stroke(); ctx.strokeStyle='#ffffff22'; ctx.lineWidth=1; ctx.beginPath(); for (let i=0;i<8;i++){const a=(i/8)*Math.PI*2;i===0?ctx.moveTo(x+Math.cos(a)*r*0.55,y+Math.sin(a)*r*0.55):ctx.lineTo(x+Math.cos(a)*r*0.55,y+Math.sin(a)*r*0.55);} ctx.closePath(); ctx.stroke(); break; } case 'siegebreaker': { // Cross / plus shape (large, slow tank) const arm = r * 0.55; ctx.beginPath(); ctx.moveTo(x-arm,y-r); ctx.lineTo(x+arm,y-r); ctx.lineTo(x+arm,y-arm); ctx.lineTo(x+r,y-arm); ctx.lineTo(x+r,y+arm); ctx.lineTo(x+arm,y+arm); ctx.lineTo(x+arm,y+r); ctx.lineTo(x-arm,y+r); ctx.lineTo(x-arm,y+arm); ctx.lineTo(x-r,y+arm); ctx.lineTo(x-r,y-arm); ctx.lineTo(x-arm,y-arm); ctx.closePath(); ctx.fill(); ctx.strokeStyle='#ffffff33'; ctx.lineWidth=2; ctx.stroke(); // Regen pulse ring when regen is active if (!e.regenPausedUntil || G.frame >= e.regenPausedUntil) { const regenAlpha = 0.15 + 0.15 * Math.sin(t * 3); ctx.strokeStyle = `rgba(136,14,79,${regenAlpha})`; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(x, y, r * 1.4, 0, Math.PI*2); ctx.stroke(); } break; } case 'echo': { // Twin overlapping rings — split visual const off = r * 0.3; ctx.beginPath(); ctx.arc(x-off, y, r*0.8, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(x+off, y, r*0.8, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle='#ffffff44'; ctx.lineWidth=1; ctx.beginPath(); ctx.arc(x-off, y, r*0.8, 0, Math.PI*2); ctx.stroke(); ctx.beginPath(); ctx.arc(x+off, y, r*0.8, 0, Math.PI*2); ctx.stroke(); break; } case 'voidherald': { // Diamond with void inner ring ctx.beginPath(); ctx.moveTo(x, y-r); ctx.lineTo(x+r*0.8, y); ctx.lineTo(x, y+r); ctx.lineTo(x-r*0.8, y); ctx.closePath(); ctx.fill(); ctx.strokeStyle=bodyColor+'88'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(x, y, r*0.45, 0, Math.PI*2); ctx.stroke(); break; } default: ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2); ctx.fill(); } } // ── ENEMIES ─────────────────────────────────────────────────── function drawEnemies() { const enemies = G.enemies; // Pass 0: Commander plasma links + self-glow (drawn under all bodies) for (const e of enemies) { if (!e.alive || e.defId !== 'commander') continue; const auraRadSq = 80 * 80; const linked = enemies.filter(t => t.alive && t !== e && distSq(e.x, e.y, t.x, t.y) <= auraRadSq); const cap = Math.min(linked.length, 8); // Plasma links to each buffed enemy if (cap > 0) { ctx.save(); ctx.shadowColor = 'rgba(255,200,50,0.8)'; ctx.shadowBlur = 8; ctx.strokeStyle = 'rgba(255,200,50,0.55)'; ctx.lineWidth = 1; for (let i = 0; i < cap; i++) { const t = linked[i]; const mx = (e.x + t.x) / 2; const my = (e.y + t.y) / 2; const dx = t.x - e.x, dy = t.y - e.y; const len = Math.sqrt(dx * dx + dy * dy) || 1; const perp = Math.sin(G.frame * 0.04 + i * 1.3) * 18; const cpx = mx - (dy / len) * perp; const cpy = my + (dx / len) * perp; ctx.beginPath(); ctx.moveTo(e.x, e.y); ctx.quadraticCurveTo(cpx, cpy, t.x, t.y); ctx.stroke(); } ctx.shadowBlur = 0; ctx.restore(); } // Self-glow: pulses large and bright when alone, dims as links form const glowR = linked.length === 0 ? 40 : Math.max(14, 28 - linked.length * 2); const selfAlpha = linked.length === 0 ? 0.35 + 0.15 * Math.sin(G.frame * 0.05) : Math.max(0.05, 0.18 - linked.length * 0.02); const grd = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, glowR); grd.addColorStop(0, `rgba(255,200,50,${selfAlpha.toFixed(2)})`); grd.addColorStop(1, 'rgba(255,200,50,0)'); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(e.x, e.y, glowR, 0, Math.PI * 2); ctx.fill(); } // Pass 1: elemental glows (no shadow, just gradient blobs) for (const e of enemies) { if (!e.alive || !e.element) continue; const el = ELEMENTS[e.element]; if (!el) continue; ctx.save(); const grd = getElemGrad(el, e.x, e.y, e.radius * 2.2); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(e.x, e.y, e.radius * 2.2, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } // Pass 2: shadows ON — draw all glowing shapes together ctx.shadowBlur = 8; for (const e of enemies) { if (!e.alive) continue; ctx.save(); ctx.shadowColor = e.glowColor || e.color; let bodyColor = e.color; if (e.frozen > 0) bodyColor = '#7ecfff'; else if (e.hitFlash > 0) bodyColor = '#ffffff'; drawEnemyShape(e, bodyColor); ctx.restore(); } ctx.shadowBlur = 0; // turn off once for all // Pass 3: overlays (armor ring, freeze tint, DoT dots) — no shadow needed for (const e of enemies) { if (!e.alive) continue; if (e.armor > 0) { ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 1.5, 0, Math.PI * 2); ctx.strokeStyle = '#aaaaaa88'; ctx.lineWidth = Math.min(e.armor * 0.8, 3.5); ctx.stroke(); } if (e.frozen > 0) { ctx.globalAlpha = 0.35; ctx.fillStyle = '#7ecfff'; ctx.beginPath(); ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = 1; } if (e.slow && e.frozen <= 0) { ctx.strokeStyle = '#7ecfffaa'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 4, 0, Math.PI * 2); ctx.stroke(); } if (e.amplified > 0) { ctx.strokeStyle = '#ff77e9aa'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 7, 0, Math.PI * 2); ctx.stroke(); } if (e.dots && e.dots.length > 0) { for (let i = 0; i < e.dots.length; i++) { const angle = G.frame * 0.08 + i * (Math.PI * 2 / e.dots.length); ctx.fillStyle = e.dots[i].color; ctx.beginPath(); ctx.arc(e.x + Math.cos(angle)*(e.radius+5), e.y + Math.sin(angle)*(e.radius+5), 2, 0, Math.PI*2); ctx.fill(); } } // Void Herald shield bubble if (e.defId === 'voidherald') { const shieldActive = (e._voidHitsThisFrame || 0) >= 3; if (shieldActive) { ctx.save(); ctx.strokeStyle = '#c77dff'; ctx.lineWidth = 3; ctx.shadowColor = '#c77dff'; ctx.shadowBlur = 18; ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 5, 0, Math.PI*2); ctx.stroke(); ctx.globalAlpha = 0.15; ctx.fillStyle = '#c77dff'; ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 5, 0, Math.PI*2); ctx.fill(); ctx.restore(); } else { ctx.strokeStyle = 'rgba(124,77,255,0.22)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(e.x, e.y, e.radius + 4, 0, Math.PI*2); ctx.stroke(); } } // HP bar — only when damaged if (e.hp < e.maxHp) { const bw = e.radius * 2.4, bh = 3; const bx = e.x - bw/2, by = e.y - e.radius - 8; ctx.fillStyle = '#111'; ctx.fillRect(bx-1, by-1, bw+2, bh+2); ctx.fillStyle = '#333'; ctx.fillRect(bx, by, bw, bh); const pct = Math.max(0, e.hp / e.maxHp); ctx.fillStyle = pct > 0.5 ? '#00ff88' : pct > 0.25 ? '#ffd700' : '#ff3355'; ctx.fillRect(bx, by, bw * pct, bh); } } } // ── PROJECTILES ─────────────────────────────────────────────── function drawProjectiles() { // Batch all projectiles under one shadowBlur pass ctx.shadowBlur = 10; for (const p of G.projectiles) { if (p.life <= 0) continue; ctx.shadowColor = p.glow; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius ?? 4, 0, Math.PI * 2); ctx.fill(); } ctx.shadowBlur = 0; } // ── BEAMS ───────────────────────────────────────────────────── function drawChainArcs() { if (!G.chainArcs || G.chainArcs.length === 0) return; for (const arc of G.chainArcs) { if (arc.life <= 0 || arc.pts.length < 2) continue; ctx.save(); ctx.lineJoin = 'round'; ctx.lineCap = 'round'; // Outer glow ctx.globalAlpha = arc.life * 0.7; ctx.strokeStyle = arc.color; ctx.lineWidth = 5; ctx.shadowColor = arc.color; ctx.shadowBlur = 22; ctx.beginPath(); ctx.moveTo(arc.pts[0].x, arc.pts[0].y); for (let i = 1; i < arc.pts.length; i++) ctx.lineTo(arc.pts[i].x, arc.pts[i].y); ctx.stroke(); // Bright white core ctx.globalAlpha = arc.life; ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1.5; ctx.shadowColor = '#ffffff'; ctx.shadowBlur = 8; ctx.beginPath(); ctx.moveTo(arc.pts[0].x, arc.pts[0].y); for (let i = 1; i < arc.pts.length; i++) ctx.lineTo(arc.pts[i].x, arc.pts[i].y); ctx.stroke(); ctx.restore(); } ctx.shadowBlur = 0; } function drawBeams() { const cx = ARENA_CX, cy = ARENA_CY; ctx.shadowBlur = 15; for (const b of G.beams) { ctx.save(); ctx.globalAlpha = b.life; ctx.shadowColor = b.glow; // Glow stroke ctx.strokeStyle = b.color; ctx.lineWidth = 3 * b.life; ctx.beginPath(); ctx.moveTo(b.x1, b.y1); ctx.lineTo(b.x1 + Math.cos(b.angle)*b.length, b.y1 + Math.sin(b.angle)*b.length); ctx.stroke(); // Bright core ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.globalAlpha = b.life * 0.5; ctx.beginPath(); ctx.moveTo(b.x1, b.y1); ctx.lineTo(b.x1 + Math.cos(b.angle)*b.length, b.y1 + Math.sin(b.angle)*b.length); ctx.stroke(); ctx.restore(); } ctx.shadowBlur = 0; } // ── PARTICLES ───────────────────────────────────────────────── function drawParticles() { // All particles in one pass — no save/restore for (const p of G.particles) { const alpha = (p.life * 255 | 0).toString(16).padStart(2, '0'); ctx.fillStyle = p.color + alpha; ctx.beginPath(); ctx.arc(p.x, p.y, p.radius * p.life, 0, Math.PI * 2); ctx.fill(); } } // ── FLOATERS ────────────────────────────────────────────────── function drawFloaters() { ctx.textAlign = 'center'; for (const f of G.floaters) { const alpha = (f.life * 255 | 0).toString(16).padStart(2, '0'); ctx.font = `bold ${Math.round(11 * f.scale)}px "Share Tech Mono", monospace`; ctx.fillStyle = f.color + alpha; ctx.fillText(f.text, f.x, f.y); } ctx.textAlign = 'left'; }