/** * VehicleHud — HUD водителя (задача 14): круглый спидометр со стрелкой, * передача (D/R/N), подсказки клавиш. DOM-оверлей поверх canvas (как * ShopInventoryUi). Показывается пока игрок за рулём. * * Фича-парность: идентичный модуль в rublox-player/src/engine/. */ export class VehicleHud { constructor(scene3d) { this.s = scene3d; this.root = null; this.needle = null; this.speedText = null; this.gearText = null; this._maxKmh = 80; } show(maxKmh) { this.remove(); this._maxKmh = Math.max(20, Math.round((maxKmh || 14) * 3.6 / 10) * 10 + 10); const parent = (this.s.canvas && this.s.canvas.parentElement) || document.body; try { if (getComputedStyle(parent).position === 'static') parent.style.position = 'relative'; } catch { /* ignore */ } const root = document.createElement('div'); root.className = 'kbn-veh-hud'; root.style.cssText = 'position:absolute;left:24px;bottom:22px;z-index:45;width:160px;height:160px;' + 'pointer-events:none;font-family:system-ui,"Segoe UI",sans-serif;user-select:none;'; // SVG-циферблат. const R = 70, CX = 80, CY = 80; const startA = 135, endA = 405; // дуга 270° const ticks = []; const N = 8; for (let i = 0; i <= N; i++) { const a = (startA + (endA - startA) * i / N) * Math.PI / 180; const x1 = CX + Math.cos(a) * (R - 4), y1 = CY + Math.sin(a) * (R - 4); const x2 = CX + Math.cos(a) * (R - 14), y2 = CY + Math.sin(a) * (R - 14); ticks.push(``); const lx = CX + Math.cos(a) * (R - 26), ly = CY + Math.sin(a) * (R - 26) + 4; const val = Math.round(this._maxKmh * i / N); ticks.push(`${val}`); } root.innerHTML = `` + `` + ticks.join('') + `` + `` + `0` + `км/ч` + `N` + ``; parent.appendChild(root); this.root = root; this.needle = root.querySelector('#kbn-veh-needle'); this.speedText = root.querySelector('#kbn-veh-speed'); this.gearText = root.querySelector('#kbn-veh-gear'); this._CX = CX; this._CY = CY; // Подсказки клавиш справа снизу. const keys = document.createElement('div'); keys.className = 'kbn-veh-keys'; keys.style.cssText = 'position:absolute;right:24px;bottom:28px;z-index:45;pointer-events:none;' + 'color:#cfd6e0;font:600 14px/1.6 system-ui,sans-serif;text-align:right;' + 'text-shadow:0 1px 3px rgba(0,0,0,0.7);'; keys.innerHTML = '
WASD — руль
V — камера
E — выйти
'; parent.appendChild(keys); this._keys = keys; } /** Обновить стрелку/число/передачу. speed — м/с (signed). */ update(speedMs) { if (!this.needle) return; const kmh = Math.abs(speedMs) * 3.6; const frac = Math.max(0, Math.min(1, kmh / this._maxKmh)); const ang = -135 + 270 * frac; // -135°..+135° this.needle.setAttribute('transform', `rotate(${ang.toFixed(1)} ${this._CX} ${this._CY})`); if (this.speedText) this.speedText.textContent = String(Math.round(kmh)); if (this.gearText) { const g = speedMs < -0.3 ? 'R' : (Math.abs(speedMs) < 0.3 ? 'N' : 'D'); this.gearText.textContent = g; this.gearText.setAttribute('fill', g === 'R' ? '#ff7a5a' : g === 'N' ? '#9aa6b8' : '#7fe0a0'); } } remove() { if (this.root) { try { this.root.remove(); } catch { /* ignore */ } this.root = null; } if (this._keys) { try { this._keys.remove(); } catch { /* ignore */ } this._keys = null; } this.needle = this.speedText = this.gearText = null; } dispose() { this.remove(); } }