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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user