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>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user