8.3 KiB
TODO — Wings of the 90s
Working notes from the 6 Oct 2026 audit. Everything below was measured by running the
code, not inferred from the README. Upstream: mac-ai-playground/retro-flight-sim
(read-only for kevinhermes); work happens in this fork.
Status check (verified)
npm install+npm run build— clean, 13 modules, 512 kB bundle, 581 ms.npm test— passes, but see T4: it only counts braces.- Terrain is deterministic across instances (
getHeightAt(317,-812)identical in two separateTerrainobjects) — the "persistent world" claim holds. - Rendering verified in software (SwiftShader, see "How to run it headless" below):
WebGL 2 context, 60 fps, 143 distinct colours, 63,792/64,000 non-black pixels, HUD
canvas drawing 6,905 lit pixels. No GPU is involved — this LXC has no
/dev/driand no/dev/nvidia*, so all rendering is CPU-rasterised. - T8 found during that verification: the CRT post-processing shader does not compile, so the 3D canvas renders pure black on any machine. See T8.
T1 — Takeoff does not work (highest priority)
Simulated 40 s at full throttle against the real Terrain heightmap:
| t (s) | knots | alt (ft) | onGround |
|---|---|---|---|
| 0 | 0.3 | 19.7 | false |
| 1 | 23.2 | 2.8 | false |
| 2 | 49.3 | 0 | true |
| 6 | 340.2 | 12.2 | false |
| 9 | 542.5 | 105.2 | false |
| 10 | 564.6 | 0 | true |
| 39 | 2,543.3 | 0 | true |
The aircraft becomes briefly airborne, then never leaves the ground and accelerates to Mach 4.1. Causes:
js/flight-model.js:77— ground friction is-hSpeed * 20, i.e. 20 N per m/s, againstmaxThrust = 50000N on a 1200 kg airframe. Friction loses every argument.js/flight-model.js:60-64— lift is applied whileonGround, scaled only byGROUND_LIFT_REDUCTION, so the plane floats off the runway at arbitrary speed and then the ground-collision clamp at:95slams it back down.- No weight-on-wheels state:
onGroundis a position test, not a contact state, so there is no normal force and no way to model a proper rotation/tail-up. PITCH_LIMIT = 85°and the auto-levelling at:134-135(x *= 0.995,z *= 0.985) fight the player continuously.
Acceptance criteria: full throttle from rest → airborne within a plausible takeoff roll (order 200–400 m), climb sustained, stall if nose raised too high, and speed bounded near a sane VMO instead of 2,500 kt.
T2 — No WebGL fallback
js/main.js:36-43 constructs THREE.WebGLRenderer with no try/catch. On a machine
without WebGL the page throws an uncaught Error creating WebGL context and the loading
screen never advances — a blank screen with no message.
Fix: wrap renderer init, show a period-appropriate failure card ("GRAPHICS CARD NOT DETECTED"), and keep the loading/menu screens usable without 3D.
T3 — Stall speed is defined but unused
STALL_SPEED = 25 (js/flight-model.js:4) is used only to scale lift. Nothing in
hud.js or sound.js references stalling. A period-accurate sim of this era should
shove a warning in your face.
Fix: stall flag on the flight model → HUD warning + stick shaker / audio cue.
T4 — The test suite is theatre
scripts/check-syntax.js checks that each file is >10 chars and that { and } counts
match. It cannot catch T1. CI (.gitea/workflows/*.yml) runs it on every push, so the
green tick currently means nothing.
Fix: real unit tests on FlightModel (takeoff profile, stall, ground contact) and
Terrain (determinism, airport flattening radius). Node has node:test built in — no
new dependency needed. Add a headless smoke test that asserts the game reaches the
FLIGHT state.
T5 — No LICENSE
README and repo carry no licence. Decide one (MIT matches the sibling wings88) and add
the file.
T6 — No save/load
The sibling project's brief promised persistent worlds. There is no persistence here —
no localStorage, no world seed stored. Terrain is deterministic per process, which is
not the same thing.
Fix: persist the world seed + aircraft state; add a save/load menu entry.
T8 — The CRT shader does not compile, so the 3D canvas is black
Found by running the game under software WebGL, not by reading the code. js/retro.js:29:
gl_Position = vec4(position, 0.0, 1.0);
In three.js position is a vec3, so this asks for vec4(vec3, float, float) — five
components into a four-component vector. Invalid GLSL ES on both WebGL 1 and 2:
THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
Program Info Log: Vertex shader is not compiled.
ERROR: 0:71: 'constructor' : too many arguments
The whole post-processing pass therefore fails and the game canvas reads back as one
colour, 0/64,000 non-black pixels. Verified in isolation: vec4(vec3, float) compiles,
vec4(vec3, float, float) does not, on both WebGL 1 and WebGL 2.
Fix (verified): gl_Position = vec4(position.xy, 0.0, 1.0); — compiles clean, and the
canvas then reports 143 distinct colours, 63,792/64,000 non-black pixels, scanline banding
visible every 25 rows, and the scene hash changing frame to frame once the aircraft moves.
Applied locally during the audit to confirm it, then reverted — not committed.
Note this is the kind of bug the current test suite cannot see (see T4): braces balance perfectly.
T7 — Minor / housekeeping
index.htmlrequests/favicon.ico→ 404 on every load. Add one, or drop the request.- Bundle is 512 kB (132 kB gzipped); Vite warns about the 500 kB chunk. Fine for now,
worth a
manualChunkssplit if it grows. package-lock.jsonis 31 kB for a two-dependency project — expected, just noting it.- No FPS cap. The README brags about a retro feel;
wings88's design notes suggest a configurable 20 FPS cap, which would be cheap to add and very on-theme.
How to run it headless (no GPU, verified working)
This LXC has no GPU access — lspci/lsmod inside an LXC report the Proxmox host's
hardware (an RTX 3090 shows up), but the container has no /dev/dri and no /dev/nvidia*,
so nothing is usable. Software rendering is the only option and it does work.
The combination that works:
CHROME=/root/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome
$CHROME --headless=new --no-sandbox --disable-gpu-sandbox \
--use-angle=swiftshader --enable-unsafe-swiftshader \
--remote-debugging-port=9223 --user-data-dir=/tmp/probe --no-first-run
# then attach with playwright-core: chromium.connectOverCDP('http://127.0.0.1:9223')
Gotchas, all learned the hard way:
- Playwright's own launcher defeats this.
chromium.launch()injects defaults (--disable-gpuamong them) that silently kill the GPU process; every attempt vialaunch()returnedwebgl: NULLor crashed the page. Launch Chrome directly and attach over CDP instead. --use-gl=swiftshader(legacy path) crashes the page. Use--use-angle=swiftshader.--use-angle=vulkan+ Mesa lavapipe (VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json) brings the GPU process up (GPU.EstablishGpuChannelrecords samples) but still yields no WebGL context. SwiftShader is the path that works.- Headful under
xvfb-runcrashes the page.--headless=newis required. --dump-domraces the page script — it captures before the probe writes its result.- WebGL clears the drawing buffer after each frame (
preserveDrawingBuffer: false), so a synchronousreadPixelsreturns all-black. Sample inside arequestAnimationFramecallback, or the measurement is meaningless. readPixelsorigin is bottom-left; flip rows before reporting them as screen rows.- Screenshots via CDP fail here (
Unable to capture screenshot), so verification is by pixel readback and colour histograms rather than images.
Suggested order
T1 → T4 (tests first would be better, but T1 is the reason the project is worth keeping) → T2 → T3 → T5 → T6 → T7.
Notes on provenance
Two commits, both authored by mac-container-dev on 5 May 2026. Not a fork of wings88
(fork: false, no cross-references in code) — an independent take on the same brief.
wings88 has 214 source lines and no Three.js usage at all; its useful parts are the
Jest/Playwright setup and the design guidelines (16–32 colour palette, no anti-aliasing,
20 FPS cap), which would slot in here.