Files
siege-protocol/js/renderer-sidepanel.js
T
44r0n7 622a9fd170 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>
2026-06-16 11:36:53 -04:00

193 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ═══ 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
}