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
+192
View File
@@ -0,0 +1,192 @@
// ═══ renderer-sidepanel.js ═══
// ============================================================
// RENDERER SIDEPANEL — deploy controls and combat log
// ============================================================
// ── SIDE PANEL ────────────────────────────────────────────────
const SP_W = 270;
const SP_DEPLOY_HDR_H = 48;
const SP_LOG_HDR_H = 28;
const SP_LOG_AREA_H = 150;
const SP_ENEMY_CARD_H = 74;
const SP_ENEMY_CARD_GAP = 6;
const SP_QTY_STEPS = [1, 5, 10, 25, 50];
let _sidePanelScrollY = 0;
let _sidePanelScrollMax = 0;
let _logScrollY = 0;
let _logScrollMax = 0;
function drawSidePanel() {
const W = canvas.width, H = canvas.height;
const PX = W - SP_W; // 1330
const PY = HUD_H; // 64
const PH = H - HUD_H; // 836
const ENEMY_AREA_H = PH - SP_DEPLOY_HDR_H - SP_LOG_HDR_H - SP_LOG_AREA_H;
const ENEMY_Y = PY + SP_DEPLOY_HDR_H; // 112
const LOG_HDR_Y = ENEMY_Y + ENEMY_AREA_H; // 634
const LOG_Y = LOG_HDR_Y + SP_LOG_HDR_H; // 662
// Background + left border
ctx.fillStyle = 'rgba(6,14,22,0.92)';
ctx.fillRect(PX, PY, SP_W, PH);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(PX, PY); ctx.lineTo(PX, PY + PH); ctx.stroke();
// ── Deploy header ─────────────────────────────────────────
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(PX, PY + SP_DEPLOY_HDR_H); ctx.lineTo(PX + SP_W, PY + SP_DEPLOY_HDR_H); ctx.stroke();
ctx.font = '11px Orbitron, monospace'; ctx.letterSpacing = '3px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#3a6080';
ctx.fillText('DEPLOY ENEMIES', PX + 14, PY + 10);
ctx.letterSpacing = '0px';
// Qty badge
const QB_W = 54, QB_H = 22;
const QB_X = PX + SP_W - 14 - QB_W, QB_Y = PY + 8;
ctx.fillStyle = '#0d1e30'; ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 1;
ctx.fillRect(QB_X, QB_Y, QB_W, QB_H); ctx.strokeRect(QB_X, QB_Y, QB_W, QB_H);
ctx.font = '13px Orbitron, monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#ffd700';
ctx.fillText('×' + G.sendQuantity, QB_X + QB_W / 2, QB_Y + QB_H / 2);
addHitRegion(QB_X, QB_Y, QB_W, QB_H, () => {
const idx = SP_QTY_STEPS.indexOf(G.sendQuantity);
G.sendQuantity = SP_QTY_STEPS[(idx + 1) % SP_QTY_STEPS.length];
});
ctx.font = '9px "Share Tech Mono", monospace'; ctx.letterSpacing = '2px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#1a3048';
ctx.fillText('scroll to set qty', PX + 14, PY + 30);
ctx.letterSpacing = '0px';
// ── Enemy cards (scrollable, clipped) ─────────────────────
ctx.save();
ctx.beginPath(); ctx.rect(PX, ENEMY_Y, SP_W, ENEMY_AREA_H); ctx.clip();
const qty = G.sendQuantity;
const bonusMult = qty >= 50 ? 1.3 : qty >= 25 ? 1.2 : qty >= 10 ? 1.12 : qty >= 5 ? 1.05 : 1.0;
for (let i = 0; i < ENEMY_DEFS.length; i++) {
const def = ENEMY_DEFS[i];
const totalCost = def.cost * qty;
const canDeploy = G.credits >= totalCost && !G.gameOver;
const cardY = ENEMY_Y + i * (SP_ENEMY_CARD_H + SP_ENEMY_CARD_GAP) - _sidePanelScrollY;
const CX = PX + 8, CW = SP_W - 16;
if (cardY + SP_ENEMY_CARD_H < ENEMY_Y || cardY > ENEMY_Y + ENEMY_AREA_H) continue;
const hov = canDeploy && isHovered(CX, cardY, CW, SP_ENEMY_CARD_H);
const elColor = def.element ? (ELEMENTS[def.element]?.color || '#1a3048') : '#1a3048';
ctx.save();
if (!canDeploy) ctx.globalAlpha = 0.32;
ctx.fillStyle = hov ? '#0c1828' : '#080f18';
ctx.strokeStyle = hov ? '#ffd700' : '#1a3048';
ctx.lineWidth = 1;
ctx.fillRect(CX, cardY, CW, SP_ENEMY_CARD_H);
ctx.strokeRect(CX, cardY, CW, SP_ENEMY_CARD_H);
// Element accent bar
ctx.fillStyle = elColor;
ctx.fillRect(CX, cardY + 5, 3, SP_ENEMY_CARD_H - 10);
// Freshness bar (5px strip at card top)
const fresh = G.enemyFreshness[def.id] || 0;
const freshPct = Math.max(0, 1 - fresh / 17);
if (freshPct > 0) {
const fbG = ctx.createLinearGradient(CX, 0, CX + CW * freshPct, 0);
fbG.addColorStop(0, '#00d4ff'); fbG.addColorStop(1, '#0055cc');
ctx.fillStyle = fbG;
ctx.fillRect(CX, cardY, CW * freshPct, 5);
}
// Hotkey
const hotkey = i < 9 ? String(i + 1) : i === 9 ? '0' : '';
ctx.font = '10px Orbitron, monospace'; ctx.letterSpacing = '0px';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#3a6080';
ctx.fillText('[' + hotkey + ']', CX + 6, cardY + 8);
// Name
ctx.font = '13px Orbitron, "Share Tech Mono", monospace'; ctx.letterSpacing = '0.5px';
ctx.fillStyle = def.color || '#b8d8e8';
ctx.fillText(def.name, CX + 30, cardY + 7);
ctx.letterSpacing = '0px';
// Cost
ctx.font = '13px Orbitron, monospace';
ctx.textAlign = 'right'; ctx.fillStyle = '#ffd700';
ctx.fillText(totalCost + '¢', CX + CW - 4, cardY + 7);
// Stats row
const statParts = ['HP ' + def.hp, 'SPD ' + def.speed];
if (def.armor) statParts.push('ARM ' + def.armor);
if (def.evasion) statParts.push('EVA ' + Math.round(def.evasion * 100) + '%');
if (def.count > 1) statParts.push('×' + def.count);
const elIcon = def.element ? (ELEMENTS[def.element]?.icon || '') : '';
if (elIcon) statParts.push(elIcon);
ctx.save();
ctx.beginPath(); ctx.rect(CX + 6, cardY + 26, CW - 12, 16); ctx.clip();
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = '#3a6080';
ctx.fillText(statParts.join(' · '), CX + 6, cardY + 26);
ctx.restore();
// Reward + profit
const rewardPerUnit = Math.round(def.reward * bonusMult);
const profit = rewardPerUnit - def.cost;
const profitStr = (profit >= 0 ? '+' : '') + profit + '¢';
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'bottom'; ctx.fillStyle = '#00ff88';
ctx.fillText('↑ ' + rewardPerUnit + '¢', CX + 6, cardY + SP_ENEMY_CARD_H - 6);
ctx.fillStyle = profit >= 0 ? '#00ff88' : '#ff3355';
ctx.fillText('(' + profitStr + ')', CX + 60, cardY + SP_ENEMY_CARD_H - 6);
if (bonusMult > 1) {
ctx.fillStyle = '#ffd700'; ctx.font = '9px Orbitron, monospace';
ctx.fillText('+' + Math.round((bonusMult - 1) * 100) + '%', CX + 114, cardY + SP_ENEMY_CARD_H - 6);
}
ctx.restore();
if (canDeploy) {
const dId = def.id;
addHitRegion(CX, cardY, CW, SP_ENEMY_CARD_H, () => deployEnemy(dId, G.sendQuantity));
}
}
const totalCardH = ENEMY_DEFS.length * (SP_ENEMY_CARD_H + SP_ENEMY_CARD_GAP);
_sidePanelScrollMax = Math.max(0, totalCardH - ENEMY_AREA_H + 4);
ctx.restore(); // end enemy clip
// ── Log section header ─────────────────────────────────────
ctx.fillStyle = '#060e18';
ctx.fillRect(PX, LOG_HDR_Y, SP_W, SP_LOG_HDR_H);
ctx.strokeStyle = '#1a3048'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(PX, LOG_HDR_Y); ctx.lineTo(PX + SP_W, LOG_HDR_Y); ctx.stroke();
ctx.beginPath(); ctx.moveTo(PX, LOG_HDR_Y + SP_LOG_HDR_H); ctx.lineTo(PX + SP_W, LOG_HDR_Y + SP_LOG_HDR_H); ctx.stroke();
ctx.font = '11px Orbitron, monospace'; ctx.letterSpacing = '3px';
ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#3a6080';
ctx.fillText('COMBAT LOG', PX + 14, LOG_HDR_Y + SP_LOG_HDR_H / 2);
ctx.letterSpacing = '0px';
// ── Log entries (scrollable, clipped) ─────────────────────
ctx.save();
ctx.beginPath(); ctx.rect(PX, LOG_Y, SP_W, SP_LOG_AREA_H); ctx.clip();
const LOG_ENTRY_H = 18;
const logColorMap = { win: '#00ff88', lose: '#ff3355', info: '#00d4ff' };
const logLines = G.logLines || [];
for (let i = 0; i < logLines.length; i++) {
const ly = LOG_Y + i * LOG_ENTRY_H - _logScrollY;
if (ly + LOG_ENTRY_H < LOG_Y || ly > LOG_Y + SP_LOG_AREA_H) continue;
ctx.save();
ctx.beginPath(); ctx.rect(PX + 10, ly, SP_W - 16, LOG_ENTRY_H); ctx.clip();
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'left'; ctx.textBaseline = 'top';
ctx.fillStyle = logColorMap[logLines[i].type] || '#3a6080';
ctx.fillText(' ' + logLines[i].text, PX + 10, ly + 2);
ctx.restore();
}
_logScrollMax = Math.max(0, logLines.length * LOG_ENTRY_H - SP_LOG_AREA_H + 4);
ctx.restore(); // end log clip
}