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

169 lines
5.0 KiB
JavaScript

import * as THREE from 'three';
const EGA = {
GREEN: new THREE.Color(0.20, 0.67, 0.13),
DARK_GREEN: new THREE.Color(0.15, 0.40, 0.10),
BROWN: new THREE.Color(0.45, 0.30, 0.15),
TAN: new THREE.Color(0.55, 0.45, 0.30),
WHITE: new THREE.Color(1, 1, 1),
BLUE: new THREE.Color(0, 0, 0.67),
};
export class Terrain {
constructor(size = 2000, segments = 100) {
this.size = size;
this.segments = segments;
this.heightData = null;
this.cellSize = size / segments;
}
hash(x, z) {
const n = Math.sin(x * 127.1 + z * 311.7) * 43758.5453;
return n - Math.floor(n);
}
smoothNoise(x, z) {
const ix = Math.floor(x);
const iz = Math.floor(z);
const fx = x - ix;
const fz = z - iz;
const sfx = fx * fx * (3 - 2 * fx);
const sfz = fz * fz * (3 - 2 * fz);
const n00 = this.hash(ix, iz);
const n10 = this.hash(ix + 1, iz);
const n01 = this.hash(ix, iz + 1);
const n11 = this.hash(ix + 1, iz + 1);
const nx0 = n00 + (n10 - n00) * sfx;
const nx1 = n01 + (n11 - n01) * sfx;
return nx0 + (nx1 - nx0) * sfz;
}
getHeightRaw(x, z) {
let h = 0;
h += this.smoothNoise(x * 0.002, z * 0.002) * 50;
h += this.smoothNoise(x * 0.006, z * 0.006) * 20;
h += this.smoothNoise(x * 0.015, z * 0.015) * 8;
return Math.max(0, h - 15);
}
buildHeightData() {
const { segments: seg, size } = this;
this.heightData = new Float32Array((seg + 1) * (seg + 1));
const half = size / 2;
for (let iz = 0; iz <= seg; iz++) {
for (let ix = 0; ix <= seg; ix++) {
const x = -half + ix * this.cellSize;
const z = -half + iz * this.cellSize;
this.heightData[iz * (seg + 1) + ix] = this.getHeightRaw(x, z);
}
}
}
getHeightAt(x, z) {
if (!this.heightData) return this.getHeightRaw(x, z);
const { size, segments: seg } = this;
const half = size / 2;
const gx = ((x + half) / size) * seg;
const gz = ((z + half) / size) * seg;
const ix = Math.floor(gx);
const iz = Math.floor(gz);
if (ix < 0 || ix >= seg || iz < 0 || iz >= seg) return 0;
const fx = gx - ix;
const fz = gz - iz;
const row = seg + 1;
const h00 = this.heightData[iz * row + Math.min(ix, seg)];
const h10 = this.heightData[iz * row + Math.min(ix + 1, seg)];
const h01 = this.heightData[(iz + 1) * row + Math.min(ix, seg)];
const h11 = this.heightData[(iz + 1) * row + Math.min(ix + 1, seg)];
const nx0 = h00 + (h10 - h00) * fx;
const nx1 = h01 + (h11 - h01) * fx;
return nx0 + (nx1 - nx0) * fz;
}
flattenAirport(radius) {
if (!this.heightData) return;
const { segments: seg } = this;
const row = seg + 1;
const center = seg / 2;
for (let iz = 0; iz <= seg; iz++) {
for (let ix = 0; ix <= seg; ix++) {
const dx = ix - center;
const dz = iz - center;
const dist = Math.sqrt(dx * dx + dz * dz) * this.cellSize;
if (dist < radius) {
const fade = 1 - Math.min(1, dist / radius);
this.heightData[iz * row + ix] *= (1 - fade * 0.95);
}
}
}
}
createMesh() {
this.buildHeightData();
this.flattenAirport(200);
const { segments: seg, size } = this;
const geometry = new THREE.PlaneGeometry(size, size, seg, seg);
geometry.rotateX(-Math.PI / 2);
const positions = geometry.attributes.position.array;
const colors = new Float32Array(positions.length);
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const z = positions[i + 2];
const h = this.getHeightAt(x, z);
positions[i + 1] = h;
if (h < 3) {
colors[i] = EGA.GREEN.r;
colors[i + 1] = EGA.GREEN.g;
colors[i + 2] = EGA.GREEN.b;
} else if (h < 12) {
colors[i] = EGA.DARK_GREEN.r;
colors[i + 1] = EGA.DARK_GREEN.g;
colors[i + 2] = EGA.DARK_GREEN.b;
} else if (h < 30) {
colors[i] = EGA.BROWN.r;
colors[i + 1] = EGA.BROWN.g;
colors[i + 2] = EGA.BROWN.b;
} else if (h < 45) {
colors[i] = EGA.TAN.r;
colors[i + 1] = EGA.TAN.g;
colors[i + 2] = EGA.TAN.b;
} else {
const mix = Math.min(1, (h - 45) / 20);
colors[i] = EGA.TAN.r + (EGA.WHITE.r - EGA.TAN.r) * mix;
colors[i + 1] = EGA.TAN.g + (EGA.WHITE.g - EGA.TAN.g) * mix;
colors[i + 2] = EGA.TAN.b + (EGA.WHITE.b - EGA.TAN.b) * mix;
}
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.computeVertexNormals();
const material = new THREE.MeshLambertMaterial({
vertexColors: true,
flatShading: true,
});
return new THREE.Mesh(geometry, material);
}
createWater() {
const geometry = new THREE.PlaneGeometry(this.size * 2, this.size * 2);
geometry.rotateX(-Math.PI / 2);
const material = new THREE.MeshLambertMaterial({
color: EGA.BLUE,
transparent: true,
opacity: 0.7,
flatShading: true,
});
const water = new THREE.Mesh(geometry, material);
water.position.y = 0.5;
return water;
}
}