// ═══ 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); if (!G.armoryOpen && !G.commandOpen) 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]; }); _tickTooltip('qty', QB_X, QB_Y, QB_W, QB_H); 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; const tierDef = DIFFICULTY_TIERS[G.difficultyTier || 0]; const tierRewardMult = tierDef?.rewardMult ?? 1; const visibleDefs = ENEMY_DEFS.filter(d => (d.minTier ?? 0) <= (G.difficultyTier || 0) && (d.minPrestige ?? 0) <= (G.prestigeLevel || 0) ); for (let i = 0; i < visibleDefs.length; i++) { const def = visibleDefs[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) — shows novelty bonus remaining 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); } if (freshPct < 1) { // Red fill for depleted portion — always visible ctx.fillStyle = '#ff335555'; ctx.fillRect(CX + CW * freshPct, cardY, CW * (1 - freshPct), 5); // Penalty text inside the bar (only when meaningfully stale) if (fresh > 3) { const penalty = Math.round((1 - freshPct) * 35); ctx.font = 'bold 9px "Share Tech Mono", monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#ff3355dd'; ctx.fillText('-' + penalty + '%', CX + CW - 3, cardY + 2.5); ctx.textAlign = 'left'; } } // Hotkey — base 10 enemies keep 1-0, new enemies have no hotkey const baseIndex = ENEMY_DEFS.findIndex(d => d.id === def.id); const hotkey = baseIndex < 9 ? String(baseIndex + 1) : baseIndex === 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 - 70, 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(); // Elemental weakness/resistance icons (right side of stats row) ctx.font = '11px monospace'; ctx.textBaseline = 'top'; let iconX = CX + CW - 6; const weakEntries = Object.entries(def.weaknesses || {}).filter(([,m]) => m > 1); const resEntries = Object.entries(def.resistances || {}).filter(([,m]) => m < 1); for (const [elId] of weakEntries) { const elD = ELEMENTS[elId]; if (!elD) continue; iconX -= ctx.measureText(elD.icon).width + 2; ctx.fillStyle = '#ff6b35'; ctx.textAlign = 'left'; ctx.fillText(elD.icon, iconX, cardY + 26); } for (const [elId] of resEntries) { const elD = ELEMENTS[elId]; if (!elD) continue; iconX -= ctx.measureText(elD.icon).width + 2; ctx.fillStyle = '#1a4060'; ctx.textAlign = 'left'; ctx.fillText(elD.icon, iconX, cardY + 26); } // Reward + profit (tier-scaled) const rewardPerUnit = Math.round(def.reward * bonusMult * tierRewardMult); 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 + 62, cardY + SP_ENEMY_CARD_H - 6); if (tierRewardMult > 1) { ctx.fillStyle = '#ff6b35'; ctx.font = '9px Orbitron, monospace'; ctx.fillText('×' + tierRewardMult.toFixed(1), CX + 124, cardY + SP_ENEMY_CARD_H - 6); } else if (bonusMult > 1) { ctx.fillStyle = '#ffd700'; ctx.font = '9px Orbitron, monospace'; ctx.fillText('+' + Math.round((bonusMult - 1) * 100) + '%', CX + 124, cardY + SP_ENEMY_CARD_H - 6); } ctx.restore(); if (canDeploy && !G.armoryOpen && !G.commandOpen) { const dId = def.id; addHitRegion(CX, cardY, CW, SP_ENEMY_CARD_H, () => deployEnemy(dId, G.sendQuantity)); } } const totalCardH = visibleDefs.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'; const cnt = logLines[i].count || 1; ctx.fillText('› ' + logLines[i].text + (cnt > 1 ? ` ×${cnt}` : ''), 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 }