Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
|
||||||
|
- name: Syntax and structure checks
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
.claude/
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Wings of the 90s - Flight Simulator
|
||||||
|
|
||||||
|
A period-accurate 3D flight simulator inspired by early 90s Amiga/DOS games like *Comanche*, *F-15 Strike Eagle*, and *After Burner*. Runs in any modern browser.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Authentic retro aesthetic** — 320×200 VGA Mode X resolution, CRT post-processing (barrel distortion, scanlines, color quantization, chromatic aberration, vignette), EGA 16-color palette
|
||||||
|
- **Loading screen** with progress bar and cycling messages
|
||||||
|
- **Main menu** with keyboard navigation
|
||||||
|
- **Procedural terrain** — 2000m heightmap with vertex-colored hills, water plane, low-poly clouds
|
||||||
|
- **Airport** — Runway with markings, taxiway, control tower, hangars, fuel truck, windsock
|
||||||
|
- **Low-poly Cessna-style aircraft** with animated propeller and retractable landing gear
|
||||||
|
- **6-DOF flight physics** — Thrust, drag, lift, gravity, ground friction, auto-leveling
|
||||||
|
- **Flight HUD** — Artificial horizon, airspeed, altimeter, heading tape, vertical speed indicator, throttle bar
|
||||||
|
- **Sound** — Engine drone tied to throttle, menu beeps
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `W` / `S` | Throttle up / down |
|
||||||
|
| `↑` / `↓` | Pitch |
|
||||||
|
| `←` / `→` | Roll |
|
||||||
|
| `Q` / `E` | Yaw (rudder) |
|
||||||
|
| `C` | Toggle cockpit / chase camera |
|
||||||
|
| `Esc` | Pause / resume |
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:5173` in your browser.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- Three.js r160 for 3D rendering
|
||||||
|
- Vite for dev server and builds
|
||||||
|
- Vanilla JS, no frameworks
|
||||||
|
- Web Audio API for sound
|
||||||
+2
-1
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build"
|
"build": "vite build",
|
||||||
|
"test": "node scripts/check-syntax.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { readFileSync, readdirSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const root = join(__dirname, '..');
|
||||||
|
const jsDir = join(root, 'js');
|
||||||
|
|
||||||
|
const files = readdirSync(jsDir).filter(f => f.endsWith('.js'));
|
||||||
|
|
||||||
|
let errors = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
const path = join(jsDir, file);
|
||||||
|
const source = readFileSync(path, 'utf-8');
|
||||||
|
// Basic sanity: file is non-empty and has balanced braces
|
||||||
|
if (source.length < 10) {
|
||||||
|
console.log(` FAIL ${file}: too small`);
|
||||||
|
errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const openBraces = (source.match(/{/g) || []).length;
|
||||||
|
const closeBraces = (source.match(/}/g) || []).length;
|
||||||
|
if (openBraces !== closeBraces) {
|
||||||
|
console.log(` FAIL ${file}: unbalanced braces (${openBraces} open, ${closeBraces} close)`);
|
||||||
|
errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const openParens = (source.match(/\(/g) || []).length;
|
||||||
|
const closeParens = (source.match(/\)/g) || []).length;
|
||||||
|
if (openParens !== closeParens) {
|
||||||
|
console.log(` FAIL ${file}: unbalanced parentheses`);
|
||||||
|
errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log(` PASS ${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = readFileSync(join(root, 'index.html'), 'utf-8');
|
||||||
|
if (html.includes('<canvas') && html.includes('<script')) {
|
||||||
|
console.log(' PASS index.html');
|
||||||
|
} else {
|
||||||
|
console.log(' FAIL index.html missing required elements');
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const css = readFileSync(join(root, 'css', 'style.css'), 'utf-8');
|
||||||
|
if (css.includes('#game-canvas') && css.includes('#loading-screen')) {
|
||||||
|
console.log(' PASS css/style.css');
|
||||||
|
} else {
|
||||||
|
console.log(' FAIL css/style.css missing required selectors');
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const required = ['main', 'flight-model', 'terrain', 'airport', 'aircraft', 'hud', 'ui', 'retro', 'sound'];
|
||||||
|
for (const mod of required) {
|
||||||
|
const source = readFileSync(join(jsDir, `${mod}.js`), 'utf-8');
|
||||||
|
if (source.includes('export class') || source.includes('export default')) {
|
||||||
|
console.log(` PASS js/${mod}.js exports correctly`);
|
||||||
|
} else {
|
||||||
|
console.log(` WARN js/${mod}.js missing export`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${errors === 0 ? 'All checks passed!' : `${errors} error(s) found.`}`);
|
||||||
|
if (errors > 0) process.exit(1);
|
||||||
Reference in New Issue
Block a user