Files
siege-protocol/js/renderer-world.js
44r0n7 626879ed0c Add freshness bar, enhance overlays and renderers
- Add enemy freshness tracking (novelty bonus for repeated deploys)
- Add freshness bar to sidepanel enemy cards with penalty indicator
- Major overhaul of renderer-overlays.js (790+ lines for UI polish)
- Enhanced combat log, shop overlays, and inventory UI
- Improved weapon/upgrade display with partial ownership colors
- Added element icons and weakness/resistance indicators to cards
- Enhanced radial menu and tooltip system
- Add "stale/%" penalty text when freshness depleted
- Update play link to ffazeshift.net in index.html
2026-06-17 11:58:17 -04:00

208 lines
6.6 KiB
JavaScript

// ═══ 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;
// per-weapon range rings drawn on alt-hold
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);
_bgW = W; _bgH = H;
}
function drawGrid(cx, cy) {
const gs = 48;
const x0 = Math.floor((cx - ARENA_RADIUS - gs) / gs) * gs;
const x1 = Math.ceil((cx + ARENA_RADIUS + gs) / gs) * gs;
const y0 = Math.floor((cy - ARENA_RADIUS - gs) / gs) * gs;
const y1 = Math.ceil((cy + ARENA_RADIUS + gs) / gs) * gs;
ctx.strokeStyle = '#0a1520';
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = x0; x <= x1; x += gs) { ctx.moveTo(x, y0); ctx.lineTo(x, y1); }
for (let y = y0; y <= y1; y += gs) { ctx.moveTo(x0, y); ctx.lineTo(x1, y); }
ctx.stroke();
}
// ── 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 ─────────────────────────────────────
// Per-weapon range rings — visible while Alt is held
const _RANGE_COLORS = ['#00d4ff', '#ff6b35', '#a855f7', '#22c55e', '#f59e0b', '#ec4899'];
function drawWeaponRanges(cx, cy) {
if (!_altHeld) return;
const weapons = (G.weapons || []).filter(w => w);
if (!weapons.length) return;
const zoom = G?.camera?.zoom ?? 1.0;
weapons.forEach((w, i) => {
const r = w.range ?? 0;
if (!r || r >= 9000) return;
const color = _RANGE_COLORS[i % _RANGE_COLORS.length];
const label = (getWeaponDef(w)?.name ?? w.defId).toUpperCase();
// Ring drawn in world space — scales naturally with zoom
ctx.save();
ctx.strokeStyle = color;
ctx.globalAlpha = 0.55;
ctx.lineWidth = 1.5;
ctx.setLineDash([8, 10]);
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
// Label in screen space — constant size regardless of zoom
// cx == ARENA_CX so screen x == cx; screen y = ARENA_CY - (r+6)*zoom
const sx = cx;
const sy = ARENA_CY - (r + 6) * zoom;
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.font = 'bold 11px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
const tw = ctx.measureText(label).width;
ctx.fillStyle = 'rgba(2,6,14,0.78)';
ctx.fillRect(sx - tw / 2 - 4, sy - 13, tw + 8, 14);
ctx.fillStyle = color;
ctx.globalAlpha = 0.95;
ctx.fillText(label, sx, sy);
ctx.restore();
});
}
// Dark overlay outside the arena circle
function drawArenaMask(W, H, cx, cy, zoom = 1) {
ctx.fillStyle = '#010407';
ctx.beginPath();
ctx.rect(0, 0, W, H);
ctx.arc(cx, cy, ARENA_RADIUS * zoom, 0, Math.PI * 2);
ctx.fill('evenodd');
}
// Subtle glowing border ring at arena edge
function drawArenaRing(cx, cy, zoom = 1) {
const r = ARENA_RADIUS * zoom;
ctx.save();
ctx.strokeStyle = 'rgba(0,100,180,0.14)';
ctx.lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, r - 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, r, 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.textBaseline = 'middle';
ctx.fillText(text, cx, by + bh / 2);
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();
}
}