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

307 lines
8.2 KiB
JavaScript

import * as THREE from 'three';
import { FlightModel } from './flight-model.js';
import { Terrain } from './terrain.js';
import { Airport } from './airport.js';
import { Aircraft } from './aircraft.js';
import { HUD } from './hud.js';
import { UI } from './ui.js';
import { RetroEffect } from './retro.js';
import { Sound } from './sound.js';
const GW = 320;
const GH = 200;
class Game {
constructor() {
this.state = 'LOADING';
this.clock = new THREE.Clock();
this.cockpitView = false;
this.fixedDelta = 1 / 60;
this.accumulator = 0;
this.initRenderer();
this.initScene();
this.initCamera();
this.initInput();
this.initComponents();
this.initUI();
// Start loading sequence
this.startLoading();
// Begin render loop
this.animate();
}
initRenderer() {
this.renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('game-canvas'),
antialias: false,
powerPreference: 'low-power',
});
this.renderer.setSize(GW, GH, false);
this.renderer.setPixelRatio(1);
}
initScene() {
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x4466AA);
this.scene.fog = new THREE.Fog(0x4466AA, 600, 2000);
// Hemisphere light (sky/ground)
const hemi = new THREE.HemisphereLight(0x8888CC, 0x444422, 0.6);
this.scene.add(hemi);
// Directional light (sun)
const sun = new THREE.DirectionalLight(0xFFFFDD, 1.2);
sun.position.set(200, 120, 100);
this.scene.add(sun);
// Ambient
this.scene.add(new THREE.AmbientLight(0x404050, 0.3));
// Low-poly clouds
this.buildClouds();
}
buildClouds() {
const cloudMat = new THREE.MeshLambertMaterial({
color: 0xDDDDDD,
flatShading: true,
transparent: true,
opacity: 0.85,
});
const cloudPositions = [
{ x: 300, z: 200, y: 180 },
{ x: -400, z: -100, y: 220 },
{ x: 500, z: -300, y: 160 },
{ x: -200, z: 400, y: 200 },
{ x: 100, z: -500, y: 190 },
{ x: -600, z: 300, y: 170 },
{ x: 400, z: 500, y: 210 },
{ x: -300, z: -400, y: 185 },
{ x: 700, z: 100, y: 195 },
{ x: -500, z: -200, y: 175 },
];
cloudPositions.forEach(({ x, y, z }) => {
const cloud = new THREE.Group();
const numPuffs = 3 + Math.floor(Math.random() * 4);
for (let i = 0; i < numPuffs; i++) {
const size = 10 + Math.random() * 20;
const puff = new THREE.Mesh(
new THREE.SphereGeometry(size, 5, 4),
cloudMat
);
puff.position.set(
(Math.random() - 0.5) * 30,
(Math.random() - 0.5) * 5,
(Math.random() - 0.5) * 20
);
puff.scale.y = 0.4;
cloud.add(puff);
}
cloud.position.set(x, y, z);
this.scene.add(cloud);
});
}
initCamera() {
this.camera = new THREE.PerspectiveCamera(75, GW / GH, 0.5, 3000);
this.camera.position.set(0, 15, 20);
}
initInput() {
this.keys = {};
window.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
this.handleKeydown(e.code);
});
window.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
}
initComponents() {
this.terrain = new Terrain(2000, 100);
this.airport = new Airport();
this.aircraft = new Aircraft();
this.flightModel = new FlightModel();
this.hud = new HUD();
this.retro = new RetroEffect(this.renderer, GW, GH);
this.sound = new Sound();
}
initUI() {
this.ui = new UI();
this.ui.onNewFlight = () => this.startFlight();
this.ui.onResume = () => this.resumeFlight();
this.ui.onMainMenu = () => this.returnToMenu();
this.ui.onQuit = () => this.quitGame();
}
handleKeydown(code) {
// Init audio on first keypress
this.sound.init();
if (this.ui.menuActive) {
if (this.ui.handleMenuInput(code)) return;
}
if (this.ui.controlsActive) {
if (this.ui.handleControlsInput()) return;
}
if (this.ui.pauseMenu.style.display !== 'none') {
if (this.ui.handlePauseInput(code)) return;
}
if (this.state === 'FLYING' && code === 'Escape') {
this.pauseFlight();
this.sound.beep(400, 60);
}
if (this.state === 'FLYING' && code === 'KeyC') {
this.cockpitView = !this.cockpitView;
}
}
getFlightInput() {
return {
throttleUp: !!this.keys['KeyW'],
throttleDown: !!this.keys['KeyS'],
pitch: this.keys['ArrowUp'] ? 1 : this.keys['ArrowDown'] ? -1 : 0,
roll: this.keys['ArrowLeft'] ? 1 : this.keys['ArrowRight'] ? -1 : 0,
yaw: this.keys['KeyQ'] ? -1 : this.keys['KeyE'] ? 1 : 0,
};
}
async startLoading() {
await this.ui.showLoading(async (step) => {
if (step === 0) {
// Generate terrain
const terrainMesh = this.terrain.createMesh();
this.scene.add(terrainMesh);
const water = this.terrain.createWater();
this.scene.add(water);
} else if (step === 1) {
// Build airport
this.airport.build();
this.scene.add(this.airport.getGroup());
} else if (step === 2) {
// Place aircraft
const aircraftGroup = this.aircraft.getGroup();
// Position on runway
const terrainH = this.terrain.getHeightAt(0, 50);
this.flightModel.position.set(0, terrainH + 2, 50);
this.flightModel.orientation.y = Math.PI; // Face down runway (south)
aircraftGroup.position.copy(this.flightModel.position);
aircraftGroup.rotation.copy(this.flightModel.orientation);
this.scene.add(aircraftGroup);
}
});
this.state = 'MENU';
}
startFlight() {
this.ui.hideMainMenu();
this.state = 'FLYING';
this.hud.show();
this.sound.takeoffSound();
}
pauseFlight() {
this.state = 'PAUSED';
this.ui.showPauseMenu();
}
resumeFlight() {
this.state = 'FLYING';
this.ui.hidePauseMenu();
}
returnToMenu() {
this.state = 'MENU';
this.ui.hidePauseMenu();
this.ui.showMainMenu();
this.hud.hide();
}
quitGame() {
this.state = 'QUIT';
this.ui.showQuitScreen();
this.hud.hide();
}
updateCamera() {
const acPos = this.flightModel.position;
const acRot = this.flightModel.orientation;
if (this.cockpitView) {
const forward = new THREE.Vector3(0, 0, -1).applyEuler(acRot);
const up = new THREE.Vector3(0, 1, 0).applyEuler(acRot);
this.camera.position.copy(acPos)
.add(forward.clone().multiplyScalar(3))
.add(up.clone().multiplyScalar(1.5));
this.camera.lookAt(acPos.clone().add(forward.clone().multiplyScalar(20)));
} else {
// Chase camera
const forward = new THREE.Vector3(0, 0, -1).applyEuler(acRot);
const up = new THREE.Vector3(0, 1, 0).applyEuler(acRot);
const desiredPos = acPos.clone()
.add(forward.multiplyScalar(-20))
.add(up.multiplyScalar(10));
this.camera.position.lerp(desiredPos, 0.06);
const lookTarget = acPos.clone().add(up.multiplyScalar(2));
this.camera.lookAt(lookTarget);
}
}
animate() {
requestAnimationFrame(() => this.animate());
const deltaRaw = this.clock.getDelta();
const delta = Math.min(deltaRaw, 0.1);
const elapsed = this.clock.getElapsedTime();
if (this.state === 'FLYING') {
this.accumulator += delta;
while (this.accumulator >= this.fixedDelta) {
const input = this.getFlightInput();
this.flightModel.update(
this.fixedDelta,
input,
(x, z) => this.terrain.getHeightAt(x, z)
);
// Sync aircraft mesh
const acGroup = this.aircraft.getGroup();
acGroup.position.copy(this.flightModel.position);
acGroup.rotation.copy(this.flightModel.orientation);
// Propeller
this.aircraft.updatePropeller(this.flightModel.thrust, this.fixedDelta);
// Landing gear
this.aircraft.retractGear(this.flightModel.onGround);
this.accumulator -= this.fixedDelta;
}
this.updateCamera();
this.hud.draw(this.flightModel);
this.sound.update(this.flightModel.thrust);
}
// Update retro effect
this.retro.update(elapsed);
// Render with retro post-processing
this.retro.render(this.scene, this.camera);
}
}
new Game();