Files
siege-protocol/js/renderer-hud.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

410 lines
17 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-hud.js ═══
// ============================================================
// RENDERER HUD — top HUD, warnings
// ============================================================
// ============================================================
// RENDERER HUD — top HUD, warnings
// ============================================================
// ── HUD ───────────────────────────────────────────────────────
const HUD_H = 64;
// ── TOOLTIP ───────────────────────────────────────────────────
const _TT_DELAY = 700; // ms of continuous hover before tooltip shows
let _ttTarget = null; // button key currently hovered
let _ttHoverMs = 0; // timestamp when hover on _ttTarget began
let _ttBx = 0, _ttBy = 0, _ttBw = 0, _ttBh = 0;
function _tickTooltip(key, x, y, w, h) {
if (isHovered(x, y, w, h)) {
if (_ttTarget !== key) { _ttTarget = key; _ttHoverMs = Date.now(); _ttBx = x; _ttBy = y; _ttBw = w; _ttBh = h; }
} else if (_ttTarget === key) {
_ttTarget = null;
}
}
function _drawHudTooltip(key, W, H) {
// [description, hotkey | null]
const tips = {
armory: ['Buy and upgrade weapons', 'Space'],
command: ['Upgrade tower hull, armor & more', 'C'],
threat: ['Change enemy difficulty tier', null],
prestige: ['Reset for permanent bonuses', null],
inventory: ['Mount and manage weapons', 'I'],
rdown: ['Decrease credit reserve', null],
rup: ['Increase credit reserve', null],
qty: ['Click to cycle qty · scroll adjust', null],
};
const tip = tips[key];
if (!tip) return;
const [desc, hotkey] = tip;
ctx.save();
ctx.letterSpacing = '0px';
ctx.font = '11px "Share Tech Mono", monospace';
const descW = ctx.measureText(desc).width;
const KEY_PAD = 10, KEY_H = 18;
let keyBadgeW = 0;
if (hotkey) {
ctx.font = '10px Orbitron, monospace';
keyBadgeW = ctx.measureText(hotkey).width + KEY_PAD * 2;
}
const H_PAD = 12, GAP = hotkey ? 8 : 0;
const TW = H_PAD * 2 + descW + GAP + keyBadgeW;
const TH = 28;
const isBottom = _ttBy > H / 2;
const tx = Math.min(Math.max(_ttBx + _ttBw / 2 - TW / 2, 8), W - TW - 8);
const ty = isBottom ? _ttBy - TH - 6 : _ttBy + _ttBh + 6;
// Background pill
ctx.fillStyle = 'rgba(4,10,18,0.97)';
ctx.strokeStyle = '#1a4060';
ctx.lineWidth = 1;
ctx.fillRect(tx, ty, TW, TH);
ctx.strokeRect(tx, ty, TW, TH);
// Description
ctx.font = '11px "Share Tech Mono", monospace';
ctx.fillStyle = '#b8d8e8';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(desc, tx + H_PAD, ty + TH / 2);
// Keycap badge
if (hotkey) {
const bx = tx + H_PAD + descW + GAP;
const by = ty + (TH - KEY_H) / 2;
ctx.fillStyle = '#060e18';
ctx.strokeStyle = '#00d4ff66';
ctx.lineWidth = 1;
ctx.fillRect(bx, by, keyBadgeW, KEY_H);
ctx.strokeRect(bx, by, keyBadgeW, KEY_H);
// Inner highlight line at top (keycap bevel)
ctx.strokeStyle = '#00d4ff33';
ctx.beginPath(); ctx.moveTo(bx + 2, by + 2); ctx.lineTo(bx + keyBadgeW - 2, by + 2); ctx.stroke();
ctx.font = '10px Orbitron, monospace';
ctx.fillStyle = '#00d4ff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(hotkey, bx + keyBadgeW / 2, by + KEY_H / 2);
}
ctx.restore();
}
// Right-section column centers (canvas x coords, 1600px wide)
const _HUD_KILLS_CX = 1542;
const _HUD_SCORE_CX = 1468;
const _HUD_DIV1_X = 1424;
const _HUD_RSV_CX = 1318;
const _HUD_DIV2_X = 1204;
const _HUD_CRED_CX = 1118;
function drawHUD() {
const W = canvas.width, H = canvas.height;
const cheapest = cheapestEnemyCost();
// ── Background strip ─────────────────────────────────────────
ctx.fillStyle = 'rgba(6,14,22,0.95)';
ctx.fillRect(0, 0, W, HUD_H);
ctx.strokeStyle = '#122030';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, HUD_H); ctx.lineTo(W, HUD_H); ctx.stroke();
ctx.save();
// ── LEFT: title ──────────────────────────────────────────────
ctx.font = '900 15px Orbitron, "Share Tech Mono", monospace';
ctx.letterSpacing = '5px';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#00d4ff';
ctx.shadowColor = 'rgba(0,212,255,0.35)';
ctx.shadowBlur = 14;
ctx.fillText('SIEGE PROTOCOL', 20, HUD_H / 2);
ctx.shadowBlur = 0;
const titleW = ctx.measureText('SIEGE PROTOCOL').width;
ctx.letterSpacing = '0px';
// ── LEFT: THREAT, PRESTIGE buttons ───────────────────────────
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
const tierDef = DIFFICULTY_TIERS[G.difficultyTier || 0];
const tierName = tierDef?.name ?? 'NORMAL';
const prestige = G.prestigeLevel || 0;
const tierActive = (G.difficultyTier || 0) > 0;
// THREAT button
const threatLabel = '⚠ ' + tierName;
const TH_BW = Math.ceil(ctx.measureText(threatLabel).width) + 28;
const TH_BX = 20 + titleW + 16;
const TH_BY = 14, TH_BH = 36;
const threatHov = isHovered(TH_BX, TH_BY, TH_BW, TH_BH);
const threatOn = G.threatOpen;
ctx.fillStyle = (threatHov || threatOn) ? (tierActive ? '#ff6b35' : '#00d4ff') : 'transparent';
ctx.strokeStyle = tierActive ? '#ff6b35' : (threatOn ? '#00d4ff' : '#3a5060');
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(TH_BX, TH_BY, TH_BW, TH_BH); ctx.fill(); ctx.stroke();
if (threatHov || threatOn) { ctx.shadowColor = tierActive ? '#ff6b35' : '#00d4ff'; ctx.shadowBlur = 12; }
ctx.fillStyle = (threatHov || threatOn) ? '#000000' : (tierActive ? '#ff6b35' : '#3a6080');
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(threatLabel, TH_BX + TH_BW / 2, TH_BY + TH_BH / 2);
ctx.shadowBlur = 0;
addHitRegion(TH_BX, TH_BY, TH_BW, TH_BH, () => G.threatOpen ? closeThreatPanel() : openThreatPanel());
_tickTooltip('threat', TH_BX, TH_BY, TH_BW, TH_BH);
// PRESTIGE button
const prestigeLabel = prestige > 0 ? ('✶P' + prestige + ' PRESTIGE') : 'PRESTIGE';
const PS_BX = TH_BX + TH_BW + 8;
const PS_BW = Math.ceil(ctx.measureText(prestigeLabel).width) + 28;
const PS_BY = 14, PS_BH = 36;
const prestigeHov = isHovered(PS_BX, PS_BY, PS_BW, PS_BH);
const prestigeOn = G.prestigeOpen;
ctx.fillStyle = (prestigeHov || prestigeOn) ? '#c77dff' : 'transparent';
ctx.strokeStyle = prestige > 0 ? '#c77dff' : (prestigeOn ? '#c77dff' : '#3a4060');
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(PS_BX, PS_BY, PS_BW, PS_BH); ctx.fill(); ctx.stroke();
if (prestigeHov || prestigeOn) { ctx.shadowColor = '#c77dff'; ctx.shadowBlur = 12; }
ctx.fillStyle = (prestigeHov || prestigeOn) ? '#000000' : (prestige > 0 ? '#c77dff' : '#3a4060');
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(prestigeLabel, PS_BX + PS_BW / 2, PS_BY + PS_BH / 2);
ctx.shadowBlur = 0;
addHitRegion(PS_BX, PS_BY, PS_BW, PS_BH, () => G.prestigeOpen ? closePrestigeDialog() : openPrestigeDialog());
_tickTooltip('prestige', PS_BX, PS_BY, PS_BW, PS_BH);
// ── CENTER: HP bar ───────────────────────────────────────────
const CX = W / 2;
const hpPct = G.tower.hp / G.tower.maxHp;
const hpColor = hpPct > 0.5 ? '#00ff88' : hpPct > 0.25 ? '#ffd700' : '#ff3355';
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText('TOWER INTEGRITY', CX, 7);
const BAR_X = CX - 180, BAR_Y = 24, BAR_W = 360, BAR_H = 14;
ctx.fillStyle = '#0a1520';
ctx.strokeStyle = '#1a3048';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(BAR_X, BAR_Y, BAR_W, BAR_H); ctx.fill(); ctx.stroke();
if (hpPct > 0) {
const grad = ctx.createLinearGradient(BAR_X, 0, BAR_X + BAR_W, 0);
if (hpPct > 0.5) { grad.addColorStop(0, '#00ff88'); grad.addColorStop(1, '#00ffaa'); }
else if (hpPct > 0.25) { grad.addColorStop(0, '#ffd700'); grad.addColorStop(1, '#ffaa00'); }
else { grad.addColorStop(0, '#ff3355'); grad.addColorStop(1, '#ff0033'); }
ctx.fillStyle = grad;
ctx.shadowColor = hpColor;
ctx.shadowBlur = 8;
ctx.fillRect(BAR_X, BAR_Y, BAR_W * Math.max(0, hpPct), BAR_H);
ctx.shadowBlur = 0;
}
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = hpColor;
ctx.fillText(`${G.tower.hp} / ${G.tower.maxHp} HP`, CX, HUD_H - 6);
// ── RIGHT helpers ────────────────────────────────────────────
function hudStat(label, value, valColor, cx) {
ctx.textAlign = 'center';
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText(label, cx, 7);
ctx.font = '700 18px Orbitron, "Share Tech Mono", monospace';
ctx.textBaseline = 'bottom';
ctx.fillStyle = valColor;
ctx.fillText(String(value), cx, HUD_H - 6);
}
function hudDivider(x) {
ctx.strokeStyle = '#1a3048';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(x, 12); ctx.lineTo(x, HUD_H - 12); ctx.stroke();
}
// KILLS / SCORE
hudStat('KILLS', G.totalKills, '#00d4ff', _HUD_KILLS_CX);
hudStat('SCORE', G.score, '#b8d8e8', _HUD_SCORE_CX);
hudDivider(_HUD_DIV1_X);
// ── RESERVE ──────────────────────────────────────────────────
const RSV = _HUD_RSV_CX;
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText('RESERVE', RSV, 7);
const RBW = 22, RBH = 22, RBY = 22;
const rDownX = RSV - 49;
const rUpX = RSV + 27;
const rDownDis = G.creditReserve <= cheapest;
const rUpDis = G.creditReserve >= G.credits;
function reserveBtn(bx, label, disabled) {
const hov = isHovered(bx, RBY, RBW, RBH);
ctx.strokeStyle = disabled ? '#aaaaff22' : (hov ? '#aaaaff' : '#aaaaff44');
ctx.fillStyle = (hov && !disabled) ? '#aaaaff22' : 'transparent';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(bx, RBY, RBW, RBH); ctx.fill(); ctx.stroke();
ctx.font = '13px Orbitron, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = disabled ? '#aaaaff40' : '#aaaaff';
ctx.fillText(label, bx + RBW / 2, RBY + RBH / 2);
}
reserveBtn(rDownX, '', rDownDis);
reserveBtn(rUpX, '+', rUpDis);
ctx.font = '700 14px Orbitron, "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = '#aaaaff';
ctx.fillText(G.creditReserve + '¢', RSV, RBY + RBH / 2);
// Spendable line (two-color)
const sp = Math.max(0, G.credits - G.creditReserve);
const pfx = 'spendable: ';
const spv = sp + '¢';
ctx.font = '10px "Share Tech Mono", monospace';
const pfxW = ctx.measureText(pfx).width;
const spvW = ctx.measureText(spv).width;
const spX = RSV - (pfxW + spvW) / 2;
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#3a6080';
ctx.fillText(pfx, spX, HUD_H - 6);
ctx.fillStyle = sp <= 0 ? '#ff3355' : '#ffd700';
ctx.fillText(spv, spX + pfxW, HUD_H - 6);
if (!rDownDis) addHitRegion(rDownX, RBY, RBW, RBH, () => adjustReserve(-10));
if (!rUpDis) addHitRegion(rUpX, RBY, RBW, RBH, () => adjustReserve(10));
_tickTooltip('rdown', rDownX, RBY, RBW, RBH);
_tickTooltip('rup', rUpX, RBY, RBW, RBH);
hudDivider(_HUD_DIV2_X);
// ── CREDITS ──────────────────────────────────────────────────
const CRED = _HUD_CRED_CX;
ctx.font = '11px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = '#3a6080';
ctx.fillText('CREDITS', CRED, 7);
const credColor = G.credits === 0 ? '#ff3355' : G.credits < 50 ? '#ff8844' : '#ffd700';
ctx.font = '900 26px Orbitron, "Share Tech Mono", monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = credColor;
ctx.shadowColor = 'rgba(255,215,0,0.6)';
ctx.shadowBlur = 16;
ctx.fillText(G.credits + '¢', CRED, HUD_H - 6);
ctx.shadowBlur = 0;
// ── BOTTOM-LEFT: ARMORY button ─────────────────────────────
const BTN_H = 34;
const BTN_Y = H - 14 - BTN_H;
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
const armoryLabel = '⚙ ARMORY';
const ARM_BW = Math.ceil(ctx.measureText(armoryLabel).width) + 32;
const ARM_BX = 20, ARM_BY = BTN_Y;
const armoryHov = isHovered(ARM_BX, ARM_BY, ARM_BW, BTN_H);
const armoryOn = G.armoryOpen;
ctx.fillStyle = (armoryHov || armoryOn) ? '#00d4ff' : 'transparent';
ctx.strokeStyle = '#00d4ff'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(ARM_BX, ARM_BY, ARM_BW, BTN_H); ctx.fill(); ctx.stroke();
if (armoryHov || armoryOn) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 16; }
ctx.fillStyle = (armoryHov || armoryOn) ? '#000000' : '#00d4ff';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(armoryLabel, ARM_BX + ARM_BW / 2, ARM_BY + BTN_H / 2);
ctx.shadowBlur = 0;
// Don't register when open — overlay is full-screen and its regions must take priority
if (!armoryOn) addHitRegion(ARM_BX, ARM_BY, ARM_BW, BTN_H, openArmory);
_tickTooltip('armory', ARM_BX, ARM_BY, ARM_BW, BTN_H);
// ── BOTTOM-LEFT: COMMAND button ────────────────────────────
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
const cmdLabel = '🏰 COMMAND';
const CMD_BW = Math.ceil(ctx.measureText(cmdLabel).width) + 28;
const CMD_BX = ARM_BX + ARM_BW + 8, CMD_BY = BTN_Y;
const cmdHov = isHovered(CMD_BX, CMD_BY, CMD_BW, BTN_H);
const cmdOn = G.commandOpen;
ctx.fillStyle = (cmdHov || cmdOn) ? '#00d4ff' : 'transparent';
ctx.strokeStyle = cmdOn ? '#00d4ff' : '#1a4060'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(CMD_BX, CMD_BY, CMD_BW, BTN_H); ctx.fill(); ctx.stroke();
if (cmdHov || cmdOn) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 16; }
ctx.fillStyle = (cmdHov || cmdOn) ? '#000000' : '#3a8090';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(cmdLabel, CMD_BX + CMD_BW / 2, CMD_BY + BTN_H / 2);
ctx.shadowBlur = 0;
if (!cmdOn) addHitRegion(CMD_BX, CMD_BY, CMD_BW, BTN_H, openCommand);
_tickTooltip('command', CMD_BX, CMD_BY, CMD_BW, BTN_H);
// ── BOTTOM-RIGHT: INVENTORY button ─────────────────────────
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
const invLabel = 'INVENTORY';
const INV_BW = Math.ceil(ctx.measureText(invLabel).width) + 28;
const INV_BH = BTN_H;
const INV_BX = (W - SP_W) - 14 - INV_BW;
const INV_BY = BTN_Y;
const invOpen = document.body.classList.contains('inventory-open');
const invHov = isHovered(INV_BX, INV_BY, INV_BW, INV_BH);
ctx.fillStyle = (invHov || invOpen) ? '#00d4ff' : 'transparent';
ctx.strokeStyle = invOpen ? '#00d4ff' : '#1a4060'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(INV_BX, INV_BY, INV_BW, INV_BH); ctx.fill(); ctx.stroke();
if (invHov || invOpen) { ctx.shadowColor = '#00d4ff'; ctx.shadowBlur = 12; }
ctx.fillStyle = (invHov || invOpen) ? '#000000' : '#3a6080';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(invLabel, INV_BX + INV_BW / 2, INV_BY + INV_BH / 2);
ctx.shadowBlur = 0;
if (!invOpen) addHitRegion(INV_BX, INV_BY, INV_BW, INV_BH, () => openWeaponPicker(-1));
_tickTooltip('inventory', INV_BX, INV_BY, INV_BW, INV_BH);
ctx.restore();
}
// Called last in render() so tooltips always appear on top
function drawTooltips() {
if (_ttTarget && Date.now() - _ttHoverMs >= _TT_DELAY) {
_drawHudTooltip(_ttTarget, canvas.width, canvas.height);
}
}
// ── BROKE WARNING ─────────────────────────────────────────────
function drawBrokeWarning() {
const cheapest = cheapestEnemyCost();
if (G.credits >= cheapest || G.gameOver) return;
const W = canvas.width, H = canvas.height;
const msg = '⚠ LOW CREDITS — if enemies breach you may go bankrupt';
ctx.save();
ctx.font = '11px Orbitron, "Share Tech Mono", monospace';
const msgW = ctx.measureText(msg).width;
const BW = msgW + 44, BH = 30;
const BX = (W - BW) / 2;
const BY = H - 90;
const alpha = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(G.frame * 0.08));
ctx.globalAlpha = alpha;
ctx.fillStyle = 'rgba(4,10,16,0.92)';
ctx.strokeStyle = '#ffd700';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.rect(BX, BY, BW, BH); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#ffd700';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(msg, W / 2, BY + BH / 2);
ctx.restore();
}