626879ed0c
- 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
237 lines
11 KiB
JavaScript
237 lines
11 KiB
JavaScript
// ═══ 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
|
||
}
|