// ═══ 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(); }