// ═══ renderer-world.js ═══ // ============================================================ // RENDERER WORLD — background, arena, fog, portals // ============================================================ // ── OFFSCREEN BACKGROUND CACHE ──────────────────────────────── // Drawn once, re-rendered only on resize — never recomputed each frame let _bgCanvas = null; let _bgW = 0, _bgH = 0; let _fogCanvas = null; let _fogW = 0, _fogH = 0, _fogRange = 0; const _mountDropZones = []; const _bagDropZones = []; const _dragRegions = []; function buildBackground(W, H) { _bgCanvas = document.createElement('canvas'); _bgCanvas.width = W; _bgCanvas.height = H; const c = _bgCanvas.getContext('2d'); const grad = c.createRadialGradient(W/2, H/2, 0, W/2, H/2, Math.max(W,H) * 0.7); grad.addColorStop(0, '#060d14'); grad.addColorStop(1, '#020508'); c.fillStyle = grad; c.fillRect(0, 0, W, H); c.strokeStyle = '#0a1520'; c.lineWidth = 1; const gs = 48; for (let x = 0; x < W; x += gs) { c.beginPath(); c.moveTo(x, 0); c.lineTo(x, H); c.stroke(); } for (let y = 0; y < H; y += gs) { c.beginPath(); c.moveTo(0, y); c.lineTo(W, y); c.stroke(); } _bgW = W; _bgH = H; } // ── CACHED ELEMENT GLOW GRADIENTS ──────────────────────────── // Keyed by "elementId:x:y:radius" — cleared each frame since positions change, // but the gradient itself is reused within a single frame for same-element enemies. // In practice we cache by element type at a unit radius and scale at draw time. const _elemGradCache = {}; function getElemGrad(el, x, y, r) { const grd = ctx.createRadialGradient(x, y, 0, x, y, r); grd.addColorStop(0, el.glow + '55'); grd.addColorStop(1, 'transparent'); return grd; } // ── ARENA OVERLAY HELPERS ───────────────────────────────────── // Subtle dashed ring at tower's current vision range function drawArenaRangeLine(cx, cy) { const r = G.tower.range; ctx.save(); ctx.strokeStyle = 'rgba(0,180,255,0.07)'; ctx.lineWidth = 1; ctx.setLineDash([6, 12]); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); ctx.restore(); } // Fog of war: sharp transition at tower range, flat dark zone beyond it function drawFog(cx, cy) { const r = G.tower.range; const W = canvas.width; const H = canvas.height; if (!_fogCanvas || _fogW !== W || _fogH !== H || _fogRange !== r) { _fogCanvas = document.createElement('canvas'); _fogCanvas.width = W; _fogCanvas.height = H; const c = _fogCanvas.getContext('2d'); // ponytail: cache the identical fog gradient; rebuild only when range/canvas changes. const fog = c.createRadialGradient(cx, cy, r * 0.94, cx, cy, r * 1.12); fog.addColorStop(0, 'rgba(2,6,12,0)'); fog.addColorStop(1, 'rgba(2,6,12,0.62)'); c.fillStyle = fog; c.beginPath(); c.arc(cx, cy, ARENA_RADIUS, 0, Math.PI * 2); c.fill(); _fogW = W; _fogH = H; _fogRange = r; } ctx.drawImage(_fogCanvas, 0, 0); } // Dark overlay outside the arena circle function drawArenaMask(W, H, cx, cy) { ctx.fillStyle = '#010407'; ctx.beginPath(); ctx.rect(0, 0, W, H); ctx.arc(cx, cy, ARENA_RADIUS, 0, Math.PI * 2); // same dir as rect → evenodd cancels inside ctx.fill('evenodd'); } // Subtle glowing border ring at arena edge function drawArenaRing(cx, cy) { ctx.save(); ctx.strokeStyle = 'rgba(0,100,180,0.14)'; ctx.lineWidth = 10; ctx.beginPath(); ctx.arc(cx, cy, ARENA_RADIUS - 5, 0, Math.PI * 2); ctx.stroke(); ctx.strokeStyle = 'rgba(0,180,255,0.22)'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(cx, cy, ARENA_RADIUS, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } // Streak indicator drawn on canvas; reads G.streak directly function drawStreak(cx, cy) { const streak = G.streak; const framesSince = G.frame - streak.lastKillFrame; if (streak.count < 3 || framesSince > 180) return; const alpha = Math.min(1, Math.min(framesSince / 8, (180 - framesSince) / 20)); if (alpha <= 0) return; const high = streak.count >= 10; const color = high ? '#ffd700' : '#00d4ff'; const text = `x${streak.count} STREAK`; ctx.save(); ctx.globalAlpha = alpha; ctx.font = 'bold 18px Orbitron, "Share Tech Mono", monospace'; ctx.textAlign = 'center'; const tw = ctx.measureText(text).width; const pad = 14; const bw = tw + pad * 2; const bh = 30; const bx = cx - bw / 2; const by = cy - 350; ctx.fillStyle = 'rgba(2,6,14,0.9)'; ctx.fillRect(bx, by, bw, bh); ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.strokeRect(bx, by, bw, bh); ctx.fillStyle = color; ctx.shadowColor = color; ctx.shadowBlur = 10; ctx.fillText(text, cx, by + 21); ctx.shadowBlur = 0; ctx.restore(); } // ── PORTALS ─────────────────────────────────────────────────── function drawPortals() { for (const p of G.portals) { const alpha = Math.min(1, p.life / 30); const fadeOut = Math.min(1, (p.life / p.maxLife) * 3); const a = Math.min(alpha, fadeOut); if (a <= 0) continue; ctx.save(); // Glow blob (no shadow — use radial gradient instead) const grd = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 30); grd.addColorStop(0, p.color + '66'); grd.addColorStop(1, 'transparent'); ctx.globalAlpha = a; ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(p.x, p.y, 30, 0, Math.PI * 2); ctx.fill(); // Outer ring ctx.globalAlpha = a * 0.7; ctx.strokeStyle = p.color; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(p.x, p.y, 22 + Math.sin(G.frame * 0.08) * 3, 0, Math.PI * 2); ctx.stroke(); // Spinning arc ctx.translate(p.x, p.y); ctx.rotate(p.angle); ctx.globalAlpha = a * 0.9; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(0, 0, 14, 0, Math.PI * 1.5); ctx.stroke(); ctx.restore(); } }