// ═══ 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 }