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,192 @@
|
||||
// ═══ 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user