No description
  • JavaScript 91.7%
  • CSS 5.6%
  • HTML 2.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Fabian Gillmann 640d5ebb7a Add unit tests for the physics core
The suite only covered whole-design behaviour and UI smoke. This adds direct
tests for orbit math (circular/elliptic/hyperbolic elements), the outcome()
classification incl. a real max-Q breakup, staging order and ignition, SAS
anti-windup bounds and the shape of the ascent programme.
2026-09-20 03:50:17 +02:00
src Move autopilot and control tuning into constants.js 2026-09-20 03:47:13 +02:00
test Add unit tests for the physics core 2026-09-20 03:50:17 +02:00
tools revert 5f11c317a6 2026-09-13 18:51:26 +02:00
.gitignore Remove agent workspace persona files accidentally committed; gitignore them 2026-09-14 11:00:06 +02:00
AGENTS.md Add AGENTS.md: push changes to origin, test-before-commit workflow 2026-09-13 00:44:26 +02:00
index.html revert 5f11c317a6 2026-09-13 18:51:26 +02:00
package-lock.json Finish Kestrel: fix throttle control, HUD gauges, dv/maxSpeed tracking 2026-09-12 23:53:14 +02:00
package.json Add unit tests for the physics core 2026-09-20 03:50:17 +02:00
README.md Finish Kestrel: fix throttle control, HUD gauges, dv/maxSpeed tracking 2026-09-12 23:53:14 +02:00
styles.css revert 5f11c317a6 2026-09-13 18:51:26 +02:00

Kestrel — Rocket Science

Build a rocket out of lumpy parts, light it, and try to put a periapsis above 50 km on a small planet where orbit only costs ~3.1 km/s. Bad designs do not just fail, they fail in specific, teachable ways: tumbling off the pad, breaking apart at max-Q, a 400 km suborbital arc with 40 m/s of dV left.

npm install
npm run serve        # http://localhost:8080

No build step — plain ES modules plus three.js from an import map.

Controls

key
Space stage
X cycle SAS: off → prograde → auto-ascent
↑ / ↓ throttle
, / . time warp
A / D pitch by hand (SAS off)
Esc abort

In the builder, click a part to bolt it on top of the stack, click a stack row to remove it, and load a blueprint to see what a working rocket looks like. The analysis panel reports liftoff mass, TWR, ideal Δv, max-Q limit, max-G limit and the CG/CP margin in calibres — the four numbers that decide whether a design can possibly work.

What the simulation models

  • Point-mass orbital mechanics on a 1000 km radius planet (mu = 9.8e12), circular orbit ≈ 3130 m/s, orbit achieved at periapsis ≥ 50 km.
  • Exponential atmosphere (6 km scale height, sea-level density 1.22) with q = ½ρv², a Mach-dependent drag curve and a sound-speed profile.
  • Structural limits. Every part carries a maxQ and a maxG; the stack inherits the worst of both. Thin long tanks cannot take the pressure, so throttle-down through max-Q is a real decision, not decoration.
  • Static stability. CP is integrated per part with a proper tail-as-centre-of-pressure model (parts ahead of the CG are destabilising); SAS can only correct within actuator authority, so an airframe that is more unstable than its fins can hold will depart control regardless of pilot input.
  • Gravity turn + real autopilot. AUTO pitches over on a rate-limited programme tied to dynamic pressure, then circularises near apoapsis. Because the command is rate limited, an over-powered-but-unstable rocket still departs control.
  • Staging, thrust curves and a g-limiter per stage, plus TWR-limited liftoff, so a rocket that cannot lift is caught on the pad.

Tools

npm run designs      # fly every blueprint headless, print the outcome table
npm run check        # regression: each design must fail/succeed for the right reason
npm test             # physics, builder UI, renderer, full game loop, camera framing
node tools/stats.mjs pencil    # mass properties, stability breakdown
node tools/trace.mjs fatboy 60 # frame-by-frame trace of one flight

Screenshots without a browser

Headless Chrome does not drive requestAnimationFrame, so the page accepts ?demo=<blueprint>&launch=1&warp=8&drive=1&shot=<frame>, which drives the loop from a timer and writes a canvas snapshot into the DOM:

node tools/serve.mjs 8099 &
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless=new --enable-unsafe-swiftshader --window-size=1280,760 \
  --virtual-time-budget=60000 --dump-dom \
  'http://localhost:8099/?demo=shrike&launch=1&warp=8&drive=1&shot=1200' \
  | perl -ne 'print $1 if /id="shot" src="data:image\/png;base64,([^"]+)"/' \
  | base64 -d > shot.png

Layout

src/constants.js   planet, atmosphere, control and guidance tuning
src/orbit.js       conic elements from state vectors
src/parts.js       the part catalogue: mass, Isp, thrust, maxQ, aerodynamics
src/rocket.js      stack → mass properties, staging plan, geometry
src/physics.js     FlightSim: forces, SAS, guidance, outcome classification
src/shapes.js      part meshes, built by lathing the part table
src/render.js      planet, sky, chase camera, staging debris
src/build.js       builder UI and its 3D preview
src/hud.js         flight readouts
src/main.js        game shell: builder → flight → debrief
test/              headless tests (three.js + jsdom)
tools/             headless flight, regression, framing and a static server

Known edges

The renderer keeps the scene in a vehicle-centred frame: at a 1000 km radius, float32 has about a decimetre of precision, which is not enough for a 10 m rocket. Everything fixed to the planet hangs off World.worldRoot, which is shifted to the vehicle every frame.