npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gama3d

v0.53.0

Published

GAMA — Gaming And Motion Agent: a 3D game development library built on top of three.js, focused on game loops, entities, steering-based motion agents, animation, cameras and input.

Readme

GAMA — Gaming And Motion Agent

CI npm

GAMA is a 3D game development library built on top of three.js. It gives you the pieces three.js deliberately leaves out — a game loop, entities and components, input, cameras, tweening, gameplay collisions — and its signature feature: motion agents, a composable steering-behavior system for bringing NPCs, enemies, flocks and companions to life.

three.js renders. GAMA makes it a game.

Vision

Most web games start the same way: a requestAnimationFrame loop, a pile of ad-hoc update() calls, keyboard flags scattered across event listeners, and enemy movement written as one-off vector math. GAMA packages those patterns into a small, typed, tree-shakeable library so you start at the gameplay layer, not the plumbing layer.

Design principles:

  • three.js-native, not a wrapper. A GameObject is a THREE.Object3D. Anything from the three.js ecosystem — loaders, materials, postprocessing — works unchanged. GAMA never hides the renderer or the scene graph from you.
  • Motion is the product. Character movement, steering-driven AI, animation cross-fades, camera rigs and tweens are first-class, because motion is what makes a 3D scene feel like a game.
  • Composable behaviors over inheritance trees. An enemy is a GameObject + MotionAgent + a few weighted steering behaviors + a StateMachine. Swap behaviors at runtime to change how it moves.
  • Small and honest scope. GAMA ships gameplay-level collisions (spheres, boxes, triggers) in core, not a physics engine. When you need rigid-body dynamics, the optional gama3d/rapier adapter binds rapier bodies and a stair-climbing character controller to GameObjects — core stays dependency-free either way.

Install

npm install gama3d three

Start from a template

The fastest way to a game that is actually a game — title screen, settings that persist, pause, results, best score, phone controls and a deployable build, all already wired:

node scripts/new-game.mjs my-game     # or --template courier
cd my-game && npm install && npm run dev

| template | what you get | |---|---| | starter | A complete small game (find five markers before the clock runs out) with the whole shell wired. Delete the round, keep the rest. | | courier | Havenbrook Courier — a generated village, a delivery loop, townsfolk in the way, day turning to dusk. A worked example; the docs site serves it at /play/. |

Both depend on the published packages rather than this repo, so what you scaffold is exactly what an outside developer gets. See the shell & templates.

Quick start

import { Mesh, BoxGeometry, MeshStandardMaterial, AmbientLight, Vector3 } from 'three';
import { Game, MotionAgent, Seek, CharacterController, FollowCamera } from 'gama3d';

const game = new Game();
game.world.scene.add(new AmbientLight(0xffffff, 1));

// A keyboard-driven player
const player = game.world.spawn('player');
player.add(new Mesh(new BoxGeometry(), new MeshStandardMaterial({ color: 0x60a5fa })));
player.addComponent(new CharacterController(game.input, { speed: 8 }));

// An enemy that chases the player
const enemy = game.world.spawn('enemy');
enemy.add(new Mesh(new BoxGeometry(), new MeshStandardMaterial({ color: 0xf87171 })));
enemy.position.set(10, 0, 10);
enemy
  .addComponent(new MotionAgent({ maxSpeed: 5, planar: true }))
  .addBehavior(new Seek(player.position));

// A camera that follows the player
const cam = new FollowCamera(game.camera, player, { offset: new Vector3(0, 8, 12) });
game.onUpdate((time) => cam.update(time.delta));

game.start();

Run the bundled demos:

npm install
npm run dev          # player + pursuing chasers + flock + pickups (F3 = debug overlay)
npm run dev:flock    # 400 boids: spatial hashing, obstacle avoidance, containment
npm run dev:navmesh  # click-to-move: A* + funnel pathfinding around walls
npm run dev:physics  # rapier: stairs, ramps, crate pyramid, jumping character
npm run dev:ai       # behavior-tree guards: patrol → chase → give up
npm run dev:navgen   # navmesh BAKED from level boxes + orbit camera
npm run dev:react    # react-three-fiber: 120 boids as declarative JSX

Documentation

Docs site with live playground — run it locally with npm run site:dev, or npm run site:publish to build it and push it to the docs branch for GitHub Pages (Settings → Pages → Deploy from a branch → docs, / (root)). It includes every guide below plus a dozen editable, runnable examples of steering, flocking, navmesh baking, behavior trees and more.

Flow fields that are not 8% wrong. One flood from the goal, any number of agents reading it — but the usual eight-neighbour Dijkstra is not exact, because the path is still made of eight directions. A staircase at 22.5° is longer than the line it approximates by exactly √(4 − 2√2) = 8.24%, and it is a bias: refine the grid three times and the error does not move, because halving the cell gives you the same staircase twice as often. FlowField solves the eikonal equation |∇φ| = cost by fast marching instead — Pythagoras rather than a staircase — and its error does converge (3.67% → 2.57% → 1.66% as the cell quarters). What an agent does with the difference: the eight-way field points it up to 21° off the true bearing on open ground, the eikonal one 2.3°. Both solvers ship, because a number that is only ever right is a number nobody has checked against the alternative. Gated by npm run flow, which measures both against a Euclidean distance neither is ever shown.

A voice that is a tube and the speed of sound. Giving an NPC a voice normally means shipping audio files, calling a text-to-speech service, or playing beeps. Speech is a SOURCE through a FILTER (Fant, 1960): the folds carry pitch, the tract carries the vowel, and they are independent — which is why you can sing "ah" on any note, and why a whisper, with no folds in it at all, stays intelligible. A tract is a tube closed at one end, so its resonances are (2n−1)c/4L: 490 / 1470 / 2450 Hz for 17.5 cm, against a textbook neutral vowel of 500 / 1500 / 2500. And the IPA vowel chart is the formant table — F1 ranks against the chart's own vertical axis at ρ = 1.000, because a phonetician's ear in 1888 and a spectrograph in 1952 were measuring the same axis. renderVoice() returns a Float32Array; no assets, no network, no WebAudio. Gated by npm run voice, which renders a woman and a child from the adult-male table and one length and checks them against Peterson & Barney's own women's and children's rows — 5.0% and 5.4% off, against a floor of 4.9% and 4.3% that is the best any single ratio can do when it is allowed to see the answer.

Prosody, against a number measured on people. A correct vocal tract still sounds like a machine, and not because of its timbre — because the pitch is a constant and the syllables are evenly spaced. Rhythm is Klatt's duration rules (1979), whose form is the claim: shortening applies to the duration above a floor, so no stack of rules can squeeze a syllable to nothing. Grabe & Low (2002) then put a number on "stress-timed vs syllable-timed" — the normalized Pairwise Variability Index, measured off recordings of humans: English 57.2, Dutch 65.5, French 43.5, Spanish 29.7. This model was built from none of it and comes out at 63.8, inside the span the stress-timed languages cover; drop the stress reduction and it falls to 50.2, out of that group entirely, which is what "stress-timed" means. Take Klatt's floor out and it goes to 85.0, past every language ever measured. Melody is done in semitones, because that is the finding rather than a unit convention: a man, a woman and a child saying the same sentence differ by 70 Hz and agree to 3e-15 semitones. Gated by npm run prosody, which reads the pitch back out of the rendered samples by autocorrelation.

Consonants, and a consonant is mostly not a sound. Delattre, Liberman and Cooper cut the bursts off synthetic syllables at Haskins in 1955 and listeners still heard /b/, /d/ and /g/: what identifies a stop is where F2 is heading. The same /d/ runs 2067 → 2290 Hz before /i/ and 1378 → 870 before /u/ — opposite directions, one consonant, and nothing specifies that. /p/ vs /b/ is not a sound either but a duration, the voice onset time Lisker and Abramson measured in 1964, and the gate reads it back out of the audio against a budget of two pitch periods because voicing can only start when the folds next close. And a nasal needs a zero: the closed mouth hangs off the path as a dead end and subtracts a frequency, which no cascade of resonators can do at any setting — npm run consonants renders the same phone all-pole as the control and requires the notch to vanish.

And an NPC that talks. speak('the traveller stopped at the gate') returns a Float32Array — no assets, no network, no service. Under it: a vocal tract that is a tube, Klatt's durations and a semitone pitch contour, consonants with loci and voice onset times, and a 216-word pronunciation dictionary, because English spelling is not a function of its letters. Gated on two axes: 96% of 96 vowels identified by a template matcher given the vowel table and nothing else (against a 10% chance floor and 15% for the same audio cut from the wrong places) — that measures the filter, on whispered vowels — and separately the voiced-against-unvoiced balance against Fletcher's 1953 relative phonetic powers, which is what decides whether a line is audible at all and which nothing measured until a listener said they could not hear a word. And the handshake the trilogy exists for: visemeOf returns the mouth ANIMA draws, the two agree at r = 0.832 against 0.392 for a face 100 ms out of step, and neither package imports the other — because F1 is mouth opening, in the geometry and in the air.

Utility AI with a unit. A utility system's response curves and weights exist to map metres, hit points and cooldowns onto an invented 0..1 axis. Don't invent the axis: an action is worth something and it costs seconds, so utility is value / seconds — a rate, which compares, with nothing to shape or balance. And the harder half, knowing when to stop, is Charnov's marginal value theorem (1976) rather than a threshold somebody picked: leave when this patch's instantaneous rate has fallen to the average rate available elsewhere. Forager runs it with the environment's rate measured off its own life, so it has no parameters at all, and npm run forage checks it against an exhaustive sweep of 6000 fixed leaving times — landing within 0.35% of a rule it was never given, reproducing both of Charnov's predictions (longer travel → longer stay; a richer world → an earlier exit), and showing what the alternative costs: a depletion threshold tuned optimally for a 2 s walk loses 29% of the harvest rate on a 40 s one.

  • Getting started
  • The shell & templates — the part of a game that is not the game
  • Levels: prefabs & format — a scene as data, and the round trip back
  • The editor — Editor + gama3d/editor: selection, snapped edits, undo that merges a drag into one step, and mountEditor for a whole tool in one call
  • The asset pipeline — a generated manifest (keys, byte sizes, groups, hashes), AssetLibrary (byte-weighted progress, shared instances, reference-counted release) and a --check gate
  • Networking — gama3d/net: an authoritative server, client-side prediction, reconciliation, entity interpolation, delta snapshots, and a simulated link that makes all of it testable without a socket
  • Dialogue — conversations as JSON so lintDialogue can read them: dangling links, unreachable lines and misspelt variables found before a player finds them; hidden vs locked choices, mid-conversation saves, exact counters
  • Replay & determinism — a run is its seed plus its inputs, a few hundred bytes: Recorder, replay, and a per-tick worldChecksum that names the FIRST tick two runs disagree on. It found that createFlock could not be replayed at all — Math.random in the scatter and in every boid's Wander — and brought GAMA a seeded Rng to fix it with
  • Utility AI — utility is a RATE (value per second), and Charnov's marginal value theorem (1976) says when to leave a patch, checked against an exhaustive sweep of 6000 fixed leaving times
  • Flow fields — one flood, any number of agents, and the eight-way grid is √(4−2√2) = 8.24% wrong at 22.5° in a way a finer grid does not fix
  • Voice — a formant synthesizer: a tract is a tube, so its resonances are (2n−1)c/4L, and the IPA vowel chart IS the formant table (ρ = 1.000). Checked against Peterson & Barney's 1952 women's and children's rows, which build nothing
  • Prosody — Klatt's duration rules and a semitone pitch contour: English rhythm measured out of the model at nPVI 63.8 against a published 57.2, with the stress reduction removed as the contrast and the floor removed as the control
  • Consonants — loci and the transition that identifies a stop, voice onset time against 1964, and the antiformant that made this the one rung needing a new filter rather than new numbers
  • Diction — text to speech: a pronunciation dictionary, the maximal onset principle, and an intelligibility gate whose budget is a ratio to its own control
  • The perf gate — npm run perf: calibration-relative timing with honest noise handling, exact work counters, render budgets in headless Chromium — and the deliberate regressions it was made to fail on
  • Using all three libraries — the catalog seam: how GAMA, SCENA and ANIMA compose into one game without importing each other
  • Characters — templates: third-person/top-down players, guards, companions, flocks, locomotion
  • Motion agents & steering — behaviors, flocking at scale, avoidance, state machines, tuning
  • Core — loop, fixed timestep, entities, events, pooling
  • Animation — tweens, easing, clip cross-fades
  • Gameplay — input & actions, camera, collisions, audio, assets
  • Audio — Soundboard: procedural sound from a seed — footsteps, impacts, engines, weather, crowds, captions
  • Game feel & HUD — GameFeel (trauma shake, hit-stop, slow-mo, rumble) and Hud (score, hearts, banner, prompt, captions, radar)
  • The pickup loop — Collector (sweep, values, respawn timers) and CheckpointRun (order enforced, laps, progress) over SCENA-shaped props
  • Stakes — Health (i-frames, death as an edge, knockback vectors) and Projectiles (pooled instanced shots, teams, arcs)
  • Opposition — Harass (ring-keeping, seeded strafe) and WaveDirector (staggered waves, rest, rubber-band pressure)
  • Retention — GameFlow (title/playing/paused/results, gate() as the pause), Objectives, SaveSlot (versioned, corruption-safe), ghosts (GhostRecorder/Ghost — race yesterday's you)
  • Platformer — PlatformerController: gravity, coyote time, jump buffering, variable height, moving-platform carry — and the coin-run payoff demo
  • Light as gameplay — Illumination (the how-lit-am-I field over structural sources), Flashlight (battery, gutter, cone reveal test), MoodGrade (lerped lighting moods per game state)
  • Flight — FlightController: arcade-honest fixed-wing flight (bank-to-turn, stall, taxi/takeoff/touchdown events) bridging to SCENA airframes via aircraftInput
  • Rail — RailController: the vehicle that does not steer. A schedule, a stopping curve, honest ETAs, and an overrun you can read — driving a scalar along anything with a length
  • Physics — the optional rapier adapter: rigid bodies & character controller
  • React — the optional react-three-fiber bindings: <Entity>, useComponent, flock hooks

Architecture

┌───────────────────────────────────────────────────────────┐
│ Game       loop · fixed timestep · renderer · input       │
├───────────────────────────────────────────────────────────┤
│ World      THREE.Scene + GameObject registry              │
│ GameObject extends THREE.Object3D + components + events   │
│ Component  onAttach / update / fixedUpdate / onDetach     │
├────────────────────┬───────────────┬──────────────────────┤
│ Motion             │ Animation     │ Gameplay             │
│ MotionAgent        │ Tween/Tweens  │ Input + ActionMap    │
│  Seek/Flee/Arrive  │ easing        │  (keyboard, gamepad) │
│  Pursue/Evade      │ Animator      │ CharacterController  │
│  Wander            │  (mixer +     │ FollowCamera         │
│  Separation        │   crossfade)  │ OrbitRig/ShoulderRig │
│  (cont.)           │               │ Sphere/BoxCollider   │
│  Alignment         │               │ CollisionSystem      │
│  Cohesion          │               │  (enter/exit events) │
│  FollowPath        │               │ Pool                 │
│  ObstacleAvoidance │               │ AudioManager         │
│  Containment       │               │ Assets               │
│ SpatialGrid        │               │ DebugOverlay         │
│ NavMesh (A*+funnel)│               │                      │
│ NavMeshAgent.goTo  │               │                      │
│ generateNavMesh    │               │                      │
│ StateMachine       │               │                      │
│ BehaviorTree       │               │                      │
│ Forager (Charnov)  │               │                      │
│ FlowField (eikonal)│               │                      │
├────────────────────┴───────────────┴──────────────────────┤
│ gama3d/rapier (optional entry point, peer dep on rapier)    │
│ PhysicsWorld · RigidBody · PhysicsCharacterController     │
├───────────────────────────────────────────────────────────┤
│ gama3d/react (optional entry point, peer deps react + r3f)  │
│ GamaProvider · Entity · useComponent · useFlockGrid       │
├───────────────────────────────────────────────────────────┤
│ gama3d/templates (characters in one call)                   │
│ createThirdPersonCharacter · createTopDownCharacter       │
│ createGuard · createCompanion · createFlock · Locomotion  │
└───────────────────────────────────────────────────────────┘
                          three.js

Motion agents

A MotionAgent is a component that steers its GameObject by summing weighted forces from classic Reynolds steering behaviors. Behaviors take fixed points, live Vector3 references, getters, or other agents — so targets can move.

import { MotionAgent, Pursue, Evade, Wander, Separation, Alignment, Cohesion, FollowPath, Path, StateMachine } from 'gama3d';

// A guard that patrols, then chases when the player gets close
const agent = guard.addComponent(new MotionAgent({ maxSpeed: 4, planar: true }));
const patrol = new FollowPath(new Path([wp1, wp2, wp3], true));
const chase = new Pursue(playerAgent);

const brain = guard.addComponent(
  new StateMachine({ agent })
);
brain
  .addState({
    name: 'patrol',
    enter: ({ agent }) => { agent.clearBehaviors(); agent.addBehavior(patrol); },
    update: ({ agent }) => {
      if (agent.position.distanceTo(player.position) < 8) brain.setState('chase');
    },
  })
  .addState({
    name: 'chase',
    enter: ({ agent }) => { agent.clearBehaviors(); agent.addBehavior(chase); },
    update: ({ agent }) => {
      if (agent.position.distanceTo(player.position) > 15) brain.setState('patrol');
    },
  });
brain.setState('patrol');

// A flock, in four lines
boid.addComponent(new MotionAgent({ maxSpeed: 3 }))
  .addBehavior(new Separation(() => flock, 1.5), 1.6)
  .addBehavior(new Alignment(() => flock, 5))
  .addBehavior(new Cohesion(() => flock, 6), 0.8);

Available behaviors: Seek, Flee, Arrive, Pursue, Evade, Wander, Separation, Alignment, Cohesion, FollowPath, ObstacleAvoidance, Containment. Implement the one-method SteeringBehavior interface to add your own.

Flocks scale with SpatialGrid, a spatial hash for near-O(n) neighbor queries — examples/flock runs 400 boids with obstacle avoidance:

const grid = new SpatialGrid(5);
game.onUpdate(() => grid.rebuild(flock));
agent.addBehavior(new Separation(grid.near(agent, 5), 1.5), 1.8);

Measured — npm run bench:throughput — the grid overtakes a plain array at around 500 agents (1000 agents: 8.8 ms/frame vs 25.7; 2000: 19.8 vs 91.9). Below that the array is faster and simpler, and the docs say so rather than assuming the fancy structure wins.

Navigation

Point-to-point movement through a level is one call — NavMesh runs A* over triangle adjacency and string-pulls the result with the funnel algorithm:

const nav = NavMesh.fromGeometry(floorGeometry);
enemy.addComponent(new MotionAgent({ maxSpeed: 5, planar: true }));
const navAgent = enemy.addComponent(new NavMeshAgent(nav));

navAgent.goTo(clickPoint);
enemy.events.on('nav-arrived', () => attack());

Physics (optional)

Real rigid-body dynamics via the rapier adapter — a separate entry point, so core gama stays dependency-free:

import { PhysicsWorld, RigidBody, PhysicsCharacterController } from 'gama3d/rapier';

const physics = await PhysicsWorld.create();
physics.attach(game); // steps at the game's fixed rate

crate.addComponent(new RigidBody(physics, {
  collider: { shape: 'box', halfExtents: new Vector3(0.5, 0.5, 0.5) },
}));
const controller = player.addComponent(
  new PhysicsCharacterController(physics, game.input, { speed: 7 })
); // slopes, stairs, snap-to-ground, gravity, jump()

Seeing what agents think

new DebugOverlay(game); // press F3

Velocity arrows (cyan), steering-force arrows (magenta), collider wireframes, and an FPS/entity/draw-call panel — steering bugs stop being invisible.

Animation

import { Tweens, easing, Animator } from 'gama3d';

const tweens = new Tweens();
game.onUpdate((t) => tweens.update(t.delta));
tweens.to(door.position, { y: 4 }, { duration: 0.6, easing: easing.cubicInOut });

// Skinned characters: named clips with cross-fades
const animator = hero.addComponent(new Animator(gltf.animations, gltf.scene));
animator.play('idle');
// later, when the player moves:
animator.play('run', 0.2);

Assets

import { Assets } from 'gama3d';

const assets = new Assets();
assets.onProgress = (loaded, total) => hud.setProgress(loaded / total);
const gltf = await assets.gltf('models/hero.glb'); // cached; repeated calls are free

Roadmap

  • [x] Spatial hashing for Separation/Alignment/Cohesion neighbor queries at scale
  • [x] Obstacle-avoidance (ObstacleAvoidance) and bounds (Containment) steering behaviors
  • [x] Gamepad support and named-action input mapping
  • [x] Audio manager (one-shots, positional audio, music cross-fade)
  • [x] Debug overlay (steering/velocity arrows, collider wireframes, stats)
  • [x] Object pooling and collision enter/exit events
  • [x] Fixed-timestep simulation option
  • [x] Navmesh pathfinding: NavMesh (A* + funnel) and NavMeshAgent.goTo(point)
  • [x] Navmesh generation from level geometry (generateNavMesh: grid sampling, slope/step/radius rules)
  • [x] Rapier adapter (gama3d/rapier): rigid bodies + physics character controller
  • [x] Behavior trees (reactive composites, decorators, typed contexts)
  • [x] Orbit and shoulder camera rigs (drag-orbit + pointer-lock mouse look with occlusion)
  • [x] React-three-fiber bindings (gama3d/react): Entity/GameObject bridge, component hooks, flock grid
  • [x] Documentation site with live, editable playground (npm run site:dev)
  • [x] Character templates (gama3d/templates): third-person & top-down players, guard/companion/flock NPCs, Locomotion animation glue
  • [x] Game templates (gama3d/templates): createRace (whole racer), CricketMatch (ball flight, timing window, laws-accurate scoring)
  • [x] Procedural audio (Soundboard): sample-free synthesized SFX, engines/weather/crowd beds, buses & ducking, captions, offline-render verification
  • [x] Game feel (GameFeel): trauma-squared screen shake, hit-stop, slow-mo with ease-back, haptic rumble
  • [x] HUD (Hud): DOM overlay — score/timer/hearts, banner, objective, prompt, Soundboard caption line, canvas radar
  • [x] Pickup loop: Collector (structural pickups/fields, respawn scheduling) and CheckpointRun (ordered gates, laps, setState painting)
  • [x] Stakes: Health (i-frames, one-shot death edge, revive with mercy window, computed knockback) and Projectiles (pooled tracers, structural targets, team filtering)
  • [x] Opposition: Harass steering (ring + band + seeded strafe) and WaveDirector (trickle spawns, rest, escalation under a ceiling, rubber-band pressure)
  • [x] Retention: GameFlow (legal-move state machine, gate() pause), Objectives (clamped progress, once-only completion), SaveSlot (versioned envelope, null-means-null), GhostRecorder/GhostTape/Ghost (fixed-interval tapes, seam-safe yaw playback)
  • [x] Platformer: PlatformerController (sub-stepped gravity, walls/ceilings, coyote time, jump buffer, variable jump height, moving-platform carry) + the coin-run payoff example
  • [x] Light as gameplay: Illumination field (structural sources, live litness, pure math), Flashlight (battery drama, seeded gutter, cone reveal with angular slack), MoodGrade (structural rig targets, seamless interrupted blends)
  • [x] Flight: FlightController (throttle→speed→lift, bank-to-turn, stall as a state, taxi/rotate/flare/touchdown with sink-rate events, apply() + aircraftInput bridges)
  • [x] Hover: HoverController (collective/cyclic/pedals, rotor spool inertia, seeded hover breath, sink-rate touchdowns, helicopterInput bridge) + rotorVoicing/RotorSound (blade-pass tremolo — the wop-wop is amplitude, not pitch)
  • [x] Rail: RailController (position is one scalar; the √(2·brake·remaining) stopping curve, exact landing at any step size, run-through when a mark was booked inside the braking distance, loop-aware schedules, ETAs that integrate the curve they drive)
  • [x] Dialogue: conversations as data (Dialogue, defineDialogue) with a JSON condition/effect vocabulary chosen so lintDialogue can statically find dangling links, unreachable nodes, strandable choice lists and undeclared variables; hidden vs locked choices, save/restore mid-line, exact counts
  • [x] Air combat: Missiles (lead pursuit under a hard turn-rate limit with speed-bleed — evadability as physics; seeded one-chance flare seduction; pooled instanced) + LockOn (cone/range/time, no credit for past devotion)
  • [ ] Multi-layer navmesh generation (Recast-style voxelization)

Development

npm install
npm test          # 524 vitest unit tests, no browser
npm run typecheck
npm run build     # tsup → dist (ESM + CJS + d.ts)
npm run dev       # vite dev server for examples/basic

And the things that need a browser or a socket, none of which a unit test can stand in for:

npm run verify:playgrounds   # every playground example, headless, pixel-checked
npm run verify:editor        # the editor driven for real, 20 checks
npm run net:check            # two clients over real WebSockets, 10 checks
npm run perf                 # timing + exact counters + render budgets

All of the above run in CI on every push (.github/workflows/ci.yml) — a pipeline that ran npm test and stopped would pass while the playground rendered black, while the editor's raycast missed, and while the netcode dropped a handshake. Each of those was a real bug, and none of them was found by a unit test.

Release notes live in CHANGELOG.md.

License

MIT