All checks were successful
Система транспорта для Рублокс-студии (задача 14 Недели 4): - VehicleManager — аркадная физика (газ/руль/тормоз/реверс), коллизия через physics.moveAABB; GLB-кузов Kenney car-kit (колёса в модели). - VehicleHud — графический спидометр-стрелка (SVG, 270° дуга) + передача D/R/N. - Вход hold-F / выход E; камера follow/капот/кинематографичная (V циклит). - game.scene.spawn(vehicle:car, opts) + onVehicleEnter/onVehicleExit. - Звук мотора: низкочастотный рокот (бас-пила + шум + LFO-пульсация тактов), pitch/громкость ∝ скорости — не воющий тон. - Авто оседает на землю при спавне (_settle + повторы при поздней готовности физики) — не висит/не тонет. - Водитель скрывается за рулём; падение в бездну → выход + респавн. - Производительность: addShadowCaster фильтрует мелкие/тонкие/огромный пол меши; InstancedMesh без receiveShadows (фикс тормозов 5→50 FPS). - Вики: карточка #61 «Такси-симулятор» + статья + 2 скриншота. - incrementPlay(id, userId) — передаём user_id для self/user-cooldown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
96 lines
5.0 KiB
JavaScript
96 lines
5.0 KiB
JavaScript
/**
|
||
* 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(`<line x1="${x1.toFixed(1)}" y1="${y1.toFixed(1)}" x2="${x2.toFixed(1)}" y2="${y2.toFixed(1)}" stroke="#c8d0dc" stroke-width="2"/>`);
|
||
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(`<text x="${lx.toFixed(1)}" y="${ly.toFixed(1)}" fill="#9aa6b8" font-size="9" text-anchor="middle">${val}</text>`);
|
||
}
|
||
root.innerHTML =
|
||
`<svg viewBox="0 0 160 160" width="160" height="160">` +
|
||
`<circle cx="${CX}" cy="${CY}" r="${R}" fill="rgba(16,20,32,0.82)" stroke="#3a4760" stroke-width="3"/>` +
|
||
ticks.join('') +
|
||
`<line id="kbn-veh-needle" x1="${CX}" y1="${CY}" x2="${CX}" y2="${CY - R + 18}" stroke="#ff5a3c" stroke-width="3.5" stroke-linecap="round" transform="rotate(-135 ${CX} ${CY})"/>` +
|
||
`<circle cx="${CX}" cy="${CY}" r="6" fill="#ff5a3c"/>` +
|
||
`<text id="kbn-veh-speed" x="${CX}" y="${CY + 30}" fill="#ffe44a" font-size="22" font-weight="800" text-anchor="middle">0</text>` +
|
||
`<text x="${CX}" y="${CY + 44}" fill="#9aa6b8" font-size="9" text-anchor="middle">км/ч</text>` +
|
||
`<text id="kbn-veh-gear" x="${CX}" y="${CY - 16}" fill="#7fe0a0" font-size="18" font-weight="900" text-anchor="middle">N</text>` +
|
||
`</svg>`;
|
||
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 = '<div><b>WASD</b> — руль</div><div><b>V</b> — камера</div><div><b>E</b> — выйти</div>';
|
||
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(); }
|
||
}
|