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
+232
View File
@@ -0,0 +1,232 @@
// ═══ renderer-hud.js ═══
// ============================================================
// RENDERER HUD — top HUD, warnings
// ============================================================
// ============================================================
// RENDERER HUD — top HUD, warnings
// ============================================================
// ── HUD ───────────────────────────────────────────────────────
const HUD_H = 64;
// Right-section column centers (canvas x coords, 1600px wide)
const _HUD_KILLS_CX = 1542;
const _HUD_SCORE_CX = 1468;
const _HUD_DIV1_X = 1424;
const _HUD_RSV_CX = 1318;
const _HUD_DIV2_X = 1204;
const _HUD_CRED_CX = 1118;
function drawHUD() {
const W = canvas.width;
const cheapest = cheapestEnemyCost();
// ── Background strip ─────────────────────────────────────────
ctx.fillStyle = 'rgba(6,14,22,0.95)';
ctx.fillRect(0, 0, W, HUD_H);
ctx.strokeStyle = '#122030';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, HUD_H); ctx.lineTo(W, HUD_H); ctx.stroke();
ctx.save();
// ── LEFT: title ──────────────────────────────────────────────
ctx.font = '900 15px Orbitron, "Share Tech Mono", monospace';
ctx.letterSpacing = '5px';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#00d4ff';
ctx.shadowColor = 'rgba(0,212,255,0.35)';
ctx.shadowBlur = 14;
ctx.fillText('SIEGE PROTOCOL', 20, HUD_H / 2);
ctx.shadowBlur = 0;
const titleW = ctx.measureText('SIEGE PROTOCOL').width;
ctx.letterSpacing = '0px';
// ── LEFT: shop button ────────────────────────────────────────
const SBX = 20 + titleW + 16;
const SBY = 14;
const SBH = 36;
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
const shopLabel = '⚙ SHOP [Space]';
const SBW = ctx.measureText(shopLabel).width + 32;
const shopHov = isHovered(SBX, SBY, SBW, SBH);
ctx.fillStyle = shopHov ? '#00d4ff' : 'transparent';
ctx.strokeStyle = '#00d4ff';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(SBX, SBY, SBW, SBH); ctx.fill(); ctx.stroke();
if (shopHov) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 16; }
ctx.fillStyle = shopHov ? '#000000' : '#00d4ff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(shopLabel, SBX + SBW / 2, SBY + SBH / 2);
ctx.shadowBlur = 0;
addHitRegion(SBX, SBY, SBW, SBH, () => G.shopOpen ? closeShop() : openShop());
// ── CENTER: HP bar ───────────────────────────────────────────
const CX = W / 2;
const hpPct = G.tower.hp / G.tower.maxHp;
const hpColor = hpPct > 0.5 ? '#00ff88' : hpPct > 0.25 ? '#ffd700' : '#ff3355';
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText('TOWER INTEGRITY', CX, 7);
const BAR_X = CX - 180, BAR_Y = 24, BAR_W = 360, BAR_H = 14;
ctx.fillStyle = '#0a1520';
ctx.strokeStyle = '#1a3048';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(BAR_X, BAR_Y, BAR_W, BAR_H); ctx.fill(); ctx.stroke();
if (hpPct > 0) {
const grad = ctx.createLinearGradient(BAR_X, 0, BAR_X + BAR_W, 0);
if (hpPct > 0.5) { grad.addColorStop(0, '#00ff88'); grad.addColorStop(1, '#00ffaa'); }
else if (hpPct > 0.25) { grad.addColorStop(0, '#ffd700'); grad.addColorStop(1, '#ffaa00'); }
else { grad.addColorStop(0, '#ff3355'); grad.addColorStop(1, '#ff0033'); }
ctx.fillStyle = grad;
ctx.shadowColor = hpColor;
ctx.shadowBlur = 8;
ctx.fillRect(BAR_X, BAR_Y, BAR_W * Math.max(0, hpPct), BAR_H);
ctx.shadowBlur = 0;
}
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = hpColor;
ctx.fillText(`${G.tower.hp} / ${G.tower.maxHp} HP`, CX, HUD_H - 6);
// ── RIGHT helpers ────────────────────────────────────────────
function hudStat(label, value, valColor, cx) {
ctx.textAlign = 'center';
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText(label, cx, 7);
ctx.font = '700 18px Orbitron, "Share Tech Mono", monospace';
ctx.textBaseline = 'bottom';
ctx.fillStyle = valColor;
ctx.fillText(String(value), cx, HUD_H - 6);
}
function hudDivider(x) {
ctx.strokeStyle = '#1a3048';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(x, 12); ctx.lineTo(x, HUD_H - 12); ctx.stroke();
}
// KILLS / SCORE
hudStat('KILLS', G.totalKills, '#00d4ff', _HUD_KILLS_CX);
hudStat('SCORE', G.score, '#b8d8e8', _HUD_SCORE_CX);
hudDivider(_HUD_DIV1_X);
// ── RESERVE ──────────────────────────────────────────────────
const RSV = _HUD_RSV_CX;
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText('RESERVE', RSV, 7);
const RBW = 22, RBH = 22, RBY = 22;
const rDownX = RSV - 49;
const rUpX = RSV + 27;
const rDownDis = G.creditReserve <= cheapest;
const rUpDis = G.creditReserve >= G.credits;
function reserveBtn(bx, label, disabled) {
const hov = isHovered(bx, RBY, RBW, RBH);
ctx.strokeStyle = disabled ? '#aaaaff22' : (hov ? '#aaaaff' : '#aaaaff44');
ctx.fillStyle = (hov && !disabled) ? '#aaaaff22' : 'transparent';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(bx, RBY, RBW, RBH); ctx.fill(); ctx.stroke();
ctx.font = '13px Orbitron, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = disabled ? '#aaaaff40' : '#aaaaff';
ctx.fillText(label, bx + RBW / 2, RBY + RBH / 2);
}
reserveBtn(rDownX, '', rDownDis);
reserveBtn(rUpX, '+', rUpDis);
ctx.font = '700 14px Orbitron, "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#aaaaff';
ctx.fillText(G.creditReserve + '¢', RSV, RBY + RBH / 2);
// Spendable line (two-color)
const sp = Math.max(0, G.credits - G.creditReserve);
const pfx = 'spendable: ';
const spv = sp + '¢';
ctx.font = '10px "Share Tech Mono", monospace';
const pfxW = ctx.measureText(pfx).width;
const spvW = ctx.measureText(spv).width;
const spX = RSV - (pfxW + spvW) / 2;
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#3a6080';
ctx.fillText(pfx, spX, HUD_H - 6);
ctx.fillStyle = sp <= 0 ? '#ff3355' : '#ffd700';
ctx.fillText(spv, spX + pfxW, HUD_H - 6);
if (!rDownDis) addHitRegion(rDownX, RBY, RBW, RBH, () => adjustReserve(-10));
if (!rUpDis) addHitRegion(rUpX, RBY, RBW, RBH, () => adjustReserve(10));
hudDivider(_HUD_DIV2_X);
// ── CREDITS ──────────────────────────────────────────────────
const CRED = _HUD_CRED_CX;
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText('CREDITS', CRED, 7);
const credColor = G.credits === 0 ? '#ff3355' : G.credits < 50 ? '#ff8844' : '#ffd700';
ctx.font = '900 26px Orbitron, "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = credColor;
ctx.shadowColor = 'rgba(255,215,0,0.6)';
ctx.shadowBlur = 16;
ctx.fillText(G.credits + '¢', CRED, HUD_H - 6);
ctx.shadowBlur = 0;
ctx.restore();
}
// ── BROKE WARNING ─────────────────────────────────────────────
function drawBrokeWarning() {
const cheapest = cheapestEnemyCost();
if (G.credits >= cheapest || G.gameOver) return;
const W = canvas.width, H = canvas.height;
const msg = '⚠ LOW CREDITS — if enemies breach you may go bankrupt';
ctx.save();
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
const msgW = ctx.measureText(msg).width;
const BW = msgW + 44, BH = 30;
const BX = (W - BW) / 2;
const BY = H - 90;
const alpha = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(G.frame * 0.08));
ctx.globalAlpha = alpha;
ctx.fillStyle = 'rgba(4,10,16,0.92)';
ctx.strokeStyle = '#ffd700';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(BX, BY, BW, BH); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#ffd700';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(msg, W / 2, BY + BH / 2);
ctx.restore();
}