Files
siege-protocol/js/renderer-sidepanel.js
T
44r0n7 626879ed0c Add freshness bar, enhance overlays and renderers
- Add enemy freshness tracking (novelty bonus for repeated deploys)
- Add freshness bar to sidepanel enemy cards with penalty indicator
- Major overhaul of renderer-overlays.js (790+ lines for UI polish)
- Enhanced combat log, shop overlays, and inventory UI
- Improved weapon/upgrade display with partial ownership colors
- Added element icons and weakness/resistance indicators to cards
- Enhanced radial menu and tooltip system
- Add "stale/%" penalty text when freshness depleted
- Update play link to ffazeshift.net in index.html
2026-06-17 11:58:17 -04:00

237 lines
11 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);
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
}