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:
2026-06-16 11:36:53 -04:00
commit 622a9fd170
31 changed files with 6164 additions and 0 deletions
+310
View File
@@ -0,0 +1,310 @@
// ═══ 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;
}
default:
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2); ctx.fill();
}
}
// ── ENEMIES ───────────────────────────────────────────────────
function drawEnemies() {
const enemies = G.enemies;
// 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();
}
}
// 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';
}