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
+163
View File
@@ -0,0 +1,163 @@
// ═══ renderer-shop-overlay.js ═══
// ============================================================
// RENDERER SHOP — canvas armory overlay
// ============================================================
// ── SHOP OVERLAY ──────────────────────────────────────────────
const _SH_HDR_H = 56;
const _SH_TAB_H = 38;
const _SH_BODY_Y = _SH_HDR_H + _SH_TAB_H; // 94
const _SH_PAD = 24;
const _SH_UPG_W = 130;
const _SH_UPG_H = 78;
const _SH_ARR_W = 26;
let _shopScrollY = 0;
let _shopScrollMax = 0;
const _shopRightClick = [];
function _shopWrapText(text, maxW, maxLines) {
if (!text) return [];
const words = text.split(' ');
const lines = [];
let cur = '';
for (const w of words) {
const test = cur ? cur + ' ' + w : w;
if (ctx.measureText(test).width <= maxW) { cur = test; }
else { if (cur) lines.push(cur); cur = w; if (lines.length >= maxLines) break; }
}
if (cur && lines.length < maxLines) lines.push(cur);
return lines;
}
function _shopUpgNode(sx, screenY, upg, bought, locked, cantAfford, onBuy, onRefund) {
const hov = !bought && !locked && !cantAfford && isHovered(sx, screenY, _SH_UPG_W, _SH_UPG_H);
const border = bought ? '#1a5030' : locked ? '#0e1e28' : cantAfford ? '#1a2030' : hov ? '#ffd700' : '#1a3048';
ctx.save();
ctx.globalAlpha = bought ? 0.75 : locked ? 0.32 : cantAfford ? 0.55 : 1;
ctx.fillStyle = bought ? '#071410' : '#060e18';
ctx.strokeStyle = border; ctx.lineWidth = 1;
ctx.fillRect(sx, screenY, _SH_UPG_W, _SH_UPG_H);
ctx.strokeRect(sx, screenY, _SH_UPG_W, _SH_UPG_H);
ctx.save();
ctx.beginPath(); ctx.rect(sx + 5, screenY + 5, _SH_UPG_W - 10, 16); ctx.clip();
ctx.font = '11px Orbitron, "Share Tech Mono", monospace'; ctx.letterSpacing = '0.5px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
ctx.fillStyle = bought ? '#00ff88' : '#b8d8e8';
ctx.fillText(upg.label, sx + 5, screenY + 5);
ctx.restore();
ctx.save();
ctx.beginPath(); ctx.rect(sx + 5, screenY + 22, _SH_UPG_W - 10, 30); ctx.clip();
ctx.font = '10px "Share Tech Mono", monospace'; ctx.letterSpacing = '0px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#3a6080';
const lines = _shopWrapText(upg.desc, _SH_UPG_W - 12, 2);
for (let li = 0; li < lines.length; li++) ctx.fillText(lines[li], sx + 5, screenY + 22 + li * 14);
ctx.restore();
ctx.font = '10px "Share Tech Mono", monospace'; ctx.letterSpacing = '0px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
if (bought) { ctx.fillStyle = '#1a4030'; ctx.fillText('✓ right-click refund', sx + 5, screenY + 58); }
else if (locked) { ctx.fillStyle = '#1a2838'; ctx.fillText('🔒 locked', sx + 5, screenY + 58); }
else { ctx.fillStyle = cantAfford ? '#ff3355' : '#ffd700'; ctx.fillText(upg.cost + '¢', sx + 5, screenY + 58); }
ctx.restore();
if (onBuy) addHitRegion(sx, screenY, _SH_UPG_W, _SH_UPG_H, onBuy);
if (onRefund) _shopRightClick.push({ x: sx, y: screenY, w: _SH_UPG_W, h: _SH_UPG_H, action: onRefund });
}
function drawShopOverlay() {
if (!G.shopOpen) return;
_shopRightClick.length = 0;
const W = canvas.width, H = canvas.height;
const BODY_H = H - _SH_BODY_Y;
ctx.fillStyle = 'rgba(2,8,14,0.97)';
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.fillStyle = '#040c14';
ctx.fillRect(0, 0, W, _SH_HDR_H);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, _SH_HDR_H); ctx.lineTo(W, _SH_HDR_H); ctx.stroke();
ctx.font = '900 15px Orbitron, "Share Tech Mono", monospace'; ctx.letterSpacing = '6px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
ctx.fillStyle = '#00d4ff'; ctx.shadowColor = '#00d4ff44'; ctx.shadowBlur = 14;
ctx.fillText('ARMORY', _SH_PAD, _SH_HDR_H / 2);
ctx.shadowBlur = 0; ctx.letterSpacing = '0px';
ctx.font = '18px Orbitron, "Share Tech Mono", monospace';
ctx.textAlign = 'center'; ctx.fillStyle = '#ffd700';
ctx.fillText('💰 ' + G.credits + '¢', W / 2, _SH_HDR_H / 2);
const CBW = 160, CBH = 32;
const CBX = W - _SH_PAD - CBW, CBY = (_SH_HDR_H - CBH) / 2;
const cbHov = isHovered(CBX, CBY, CBW, CBH);
ctx.fillStyle = cbHov ? '#3d0808' : 'transparent';
ctx.strokeStyle = '#ff3355'; ctx.lineWidth = 1;
ctx.fillRect(CBX, CBY, CBW, CBH); ctx.strokeRect(CBX, CBY, CBW, CBH);
ctx.font = '11px Orbitron, monospace'; ctx.letterSpacing = '2px';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#ff3355';
ctx.fillText('✕ CLOSE [Esc]', CBX + CBW / 2, CBY + CBH / 2);
ctx.letterSpacing = '0px';
addHitRegion(CBX, CBY, CBW, CBH, closeShop);
ctx.fillStyle = '#030a12';
ctx.fillRect(0, _SH_HDR_H, W, _SH_TAB_H);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, _SH_BODY_Y); ctx.lineTo(W, _SH_BODY_Y); ctx.stroke();
const equippedWeapons = getEquippedWeapons();
const tabDefs = [
{ id: 'tower', label: '🏰 TOWER' },
{ id: 'weapons', label: '🔧 BUY WEAPON' },
...equippedWeapons.map(w => {
const def = getWeaponDef(w);
return { id: w.instanceId, label: (def?.icon || '?') + ' ' + (def?.name || '?') };
}),
];
ctx.font = '11px Orbitron, "Share Tech Mono", monospace'; ctx.letterSpacing = '1.5px';
let tx = _SH_PAD;
const TB_Y = _SH_HDR_H + 4, TB_H = _SH_TAB_H - 4;
for (const tab of tabDefs) {
const tw = Math.ceil(ctx.measureText(tab.label).width) + 32;
const active = G.shopTab === tab.id;
const tabHov = isHovered(tx, TB_Y, tw, TB_H);
if (active) {
ctx.fillStyle = '#060e18'; ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.fillRect(tx, TB_Y, tw, TB_H + 2);
ctx.strokeRect(tx, TB_Y, tw, TB_H);
}
ctx.fillStyle = active ? '#00d4ff' : (tabHov ? '#b8d8e8' : '#3a6080');
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(tab.label, tx + tw / 2, TB_Y + TB_H / 2);
addHitRegion(tx, TB_Y, tw, TB_H, ((tid) => () => setShopTab(tid))(tab.id));
tx += tw + 4;
}
ctx.letterSpacing = '0px';
ctx.save();
ctx.beginPath(); ctx.rect(0, _SH_BODY_Y, W, BODY_H); ctx.clip();
const bodyCX = _SH_PAD, bodyCW = W - _SH_PAD * 2;
let yOff = _SH_PAD;
if (G.shopTab === 'tower') {
yOff = _shopDrawTowerContent(yOff, bodyCX, bodyCW, H);
} else if (G.shopTab === 'weapons') {
yOff = _shopDrawBuyContent(yOff, bodyCX, bodyCW, H);
} else {
const w = equippedWeapons.find(w => w.instanceId === G.shopTab);
if (w) yOff = _shopDrawWeaponContent(yOff, bodyCX, bodyCW, H, w);
}
_shopScrollMax = Math.max(0, yOff - BODY_H + _SH_PAD);
ctx.restore();
ctx.restore();
}