Files
mac-container-dev 76bad46d09 Initial commit: Retro 90s flight simulator MVP
Loading screen, main menu, 3D flight sim with CRT post-processing,
procedural terrain, airport with buildings, low-poly aircraft,
flight physics, HUD instruments, and sound.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 10:43:22 +00:00

275 lines
6.8 KiB
JavaScript

export class HUD {
constructor() {
this.canvas = document.getElementById('hud-canvas');
this.ctx = this.canvas.getContext('2d');
this.visible = false;
}
show() {
this.visible = true;
document.getElementById('hud-overlay').style.display = 'block';
}
hide() {
this.visible = false;
document.getElementById('hud-overlay').style.display = 'none';
}
clear() {
this.ctx.clearRect(0, 0, 320, 200);
}
draw(flightModel) {
if (!this.visible) return;
this.clear();
const ctx = this.ctx;
const speed = flightModel.getAirspeedKnots();
const altitude = flightModel.getAltitudeFeet();
const heading = flightModel.getHeadingDeg();
const vsi = flightModel.getVerticalSpeedFtMin();
const pitch = flightModel.getPitchDeg();
const roll = flightModel.getRollDeg();
const throttle = flightModel.thrust;
// Artificial horizon (center-bottom)
this.drawArtificialHorizon(ctx, pitch, roll);
// Heading tape (top center)
this.drawHeadingTape(ctx, heading);
// Airspeed indicator (left side)
this.drawAirspeed(ctx, speed);
// Altimeter (right side)
this.drawAltimeter(ctx, altitude);
// Vertical speed indicator (right side, below altimeter)
this.drawVSI(ctx, vsi);
// Throttle indicator (bottom left)
this.drawThrottle(ctx, throttle);
// Heading number
const cardinal = this.headingToCardinal(heading);
ctx.fillStyle = '#00FF00';
ctx.font = '8px "Press Start 2P", monospace';
ctx.textAlign = 'center';
ctx.fillText(`${Math.round(heading)}° ${cardinal}`, 160, 12);
}
drawArtificialHorizon(ctx, pitch, roll) {
const w = 100;
const h = 60;
const x = 110;
const y = 140;
ctx.save();
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.clip();
const cx = x + w / 2;
const cy = y + h / 2;
ctx.translate(cx, cy);
ctx.rotate(-roll * Math.PI / 180);
const pitchOffset = pitch * 1.5;
// Sky
ctx.fillStyle = '#0000AA';
ctx.fillRect(-60, -40 + pitchOffset, 120, 40);
// Ground
ctx.fillStyle = '#8B4513';
ctx.fillRect(-60, pitchOffset, 120, 40);
// Horizon line
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-60, pitchOffset);
ctx.lineTo(60, pitchOffset);
ctx.stroke();
// Pitch lines
ctx.strokeStyle = '#FFFFFF88';
ctx.lineWidth = 1;
for (let p = -20; p <= 20; p += 10) {
if (p === 0) continue;
const py = p * 1.5;
ctx.beginPath();
ctx.moveTo(-10, py);
ctx.lineTo(10, py);
ctx.stroke();
}
ctx.restore();
// Aircraft reference symbol
ctx.strokeStyle = '#FF8800';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx - 18, cy);
ctx.lineTo(cx - 6, cy);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx + 6, cy);
ctx.lineTo(cx + 18, cy);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx, cy + 3);
ctx.stroke();
// Border
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
}
drawHeadingTape(ctx, heading) {
const y = 22;
const ctx2 = ctx;
ctx2.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx2.fillRect(80, 14, 160, 18);
ctx2.strokeStyle = '#00FF00';
ctx2.lineWidth = 1;
ctx2.strokeRect(80, 14, 160, 18);
ctx2.font = '6px "Press Start 2P", monospace';
ctx2.textAlign = 'center';
ctx2.fillStyle = '#00FF00';
const cards = [
{ deg: 0, label: 'N' },
{ deg: 90, label: 'E' },
{ deg: 180, label: 'S' },
{ deg: 270, label: 'W' },
];
cards.forEach(({ deg, label }) => {
const diff = heading - deg;
const px = (diff / 30) * 8;
const lx = 160 + px;
if (lx > 85 && lx < 235) {
ctx2.fillText(label, lx, y);
}
});
}
drawAirspeed(ctx, speed) {
const x = 8;
const y = 40;
const w = 30;
const h = 100;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
// Speed bar
const fill = Math.min(1, speed / 200);
const barH = fill * (h - 10);
ctx.fillStyle = fill > 0.85 ? '#FF5555' : fill > 0.6 ? '#FFFF55' : '#00FF00';
ctx.fillRect(x + 2, y + h - 4 - barH, w - 4, barH);
// Speed ticks
ctx.fillStyle = '#00FF00';
ctx.font = '6px "Press Start 2P", monospace';
ctx.textAlign = 'left';
for (let s = 0; s <= 200; s += 50) {
const tickY = y + h - 4 - (s / 200) * (h - 10);
ctx.fillRect(x, tickY - 0.5, 4, 1);
}
// Current speed
ctx.textAlign = 'center';
ctx.font = '8px "Press Start 2P", monospace';
ctx.fillText(Math.round(speed), x + w / 2, y - 4);
ctx.font = '5px "Press Start 2P", monospace';
ctx.fillText('KT', x + w / 2, y + h + 10);
}
drawAltimeter(ctx, altitude) {
const x = 282;
const y = 40;
const w = 30;
const h = 100;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
// Altitude bar
const fill = Math.min(1, altitude / 15000);
const barH = fill * (h - 10);
ctx.fillStyle = '#00FF00';
ctx.fillRect(x + 2, y + h - 4 - barH, w - 4, barH);
// Current altitude
ctx.fillStyle = '#00FF00';
ctx.font = '8px "Press Start 2P", monospace';
ctx.textAlign = 'center';
ctx.fillText(Math.round(altitude), x + w / 2, y - 4);
ctx.font = '5px "Press Start 2P", monospace';
ctx.fillText('FT', x + w / 2, y + h + 10);
}
drawVSI(ctx, vsi) {
const x = 282;
const y = 155;
const w = 30;
const h = 35;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
ctx.fillStyle = '#00FF00';
ctx.font = '6px "Press Start 2P", monospace';
ctx.textAlign = 'center';
const vsiText = vsi >= 0 ? `+${Math.round(vsi)}` : Math.round(vsi);
ctx.fillText(vsiText, x + w / 2, y + 14);
ctx.font = '5px "Press Start 2P", monospace';
ctx.fillText('FT/M', x + w / 2, y + 26);
}
drawThrottle(ctx, throttle) {
const x = 8;
const y = 160;
const w = 40;
const h = 8;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, w, h);
const fill = throttle * (w - 2);
ctx.fillStyle = throttle > 0.8 ? '#FF5555' : '#00AAAA';
ctx.fillRect(x + 1, y + 1, fill, h - 2);
ctx.fillStyle = '#00FF00';
ctx.font = '5px "Press Start 2P", monospace';
ctx.textAlign = 'left';
ctx.fillText('THR', x, y - 3);
}
headingToCardinal(deg) {
const dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
const idx = Math.round(deg / 22.5) % 16;
return dirs[idx];
}
}