Building Drift Well: A Physics Game as an npm Package
Why I Built Drift Well
After building Signal Hop, I wanted a game that felt fundamentally different. Signal Hop is about discrete choices on a graph. Drift Well is continuous: you push a particle and watch it curve through invisible gravity fields. The inspiration came from orbital mechanics simulations, where small velocity changes create dramatically different trajectories.
The core mechanic: you cannot see the gravity wells. You only see your particle and the checkpoint you need to reach. Every push reveals something about the invisible force fields based on how your particle curves. Over time, you build a mental model of the level's gravitational landscape.
Game Design
Drift Well is a physics puzzle game. You start each level with a particle, a set of invisible gravity wells, and a sequence of checkpoint rings to reach. Tap and drag to aim, release to push your particle. The particle drifts with momentum, pulled and deflected by gravity wells you cannot see.
Each push costs energy. Hitting walls drains more. Run out of energy and it is game over. This creates a tension: aggressive pushes reach checkpoints faster but waste energy and risk wall collisions. Gentle pushes let gravity do the work but require more precise aim.
Levels are procedurally generated. The number of gravity wells, their strength, and their positions all increase with level number. Early levels might have one or two gentle wells. Later levels have five or six wells creating complex gravitational corridors.
Physics Simulation
The physics engine runs as a pure TypeScript module with no DOM dependencies. Each tick computes gravitational acceleration from all wells, applies it to the particle's velocity, then updates position. The acceleration from a well follows an inverse-square law: stronger at close range, negligible at distance.
function computeGravity(particle: Vec2, wells: GravityWell[]): Vec2 {
let ax = 0, ay = 0;
for (const well of wells) {
const dx = well.x - particle.x;
const dy = well.y - particle.y;
const distSq = dx * dx + dy * dy;
const dist = Math.sqrt(distSq);
if (dist < MIN_DISTANCE) continue;
const force = well.strength / distSq;
ax += (dx / dist) * force;
ay += (dy / dist) * force;
}
return { x: ax, y: ay };
}A minimum distance threshold prevents division-by-near-zero when the particle passes very close to a well center. The simulation runs at a fixed timestep of 16ms, accumulated and processed in chunks to maintain deterministic behavior regardless of frame rate variation.
Drag-to-Aim Controls
The input system uses pointer events for unified mouse and touch handling. When you press down on the particle, a drag line appears showing your aim direction. The length of the drag determines push strength. An arrow indicator and a dotted trajectory preview help you visualize the initial path.
The trajectory preview only shows a short segment, not the full path. This is intentional: if you could see the entire trajectory, the gravity wells would be immediately revealed. The short preview gives just enough information to aim without spoiling the discovery.
Level Generation
Each level is generated procedurally with constraints. Gravity wells are placed with minimum spacing to avoid creating singularity-like traps. Checkpoints are placed in positions that require navigating through gravitational influence zones, not in the dead space between wells. The generation algorithm verifies that a valid path exists by running a simulated test trajectory.
Difficulty scales along three axes: more wells, stronger wells, and tighter checkpoint placement. Level 1 has two weak wells and a single checkpoint. By level 10, you are threading through five strong wells with three checkpoints placed in gravitational corridors.
Rendering
Canvas 2D rendering draws the particle with a trailing motion blur, checkpoint rings with a gentle pulse animation, and the drag indicator when aiming. Gravity wells are invisible during gameplay but shown as faded circles on the game-over screen, letting you see what was pulling your particle all along.
The reveal on game over is one of the most satisfying moments: you see the wells overlaid on your trajectory and realize why your particle curved the way it did. It turns every loss into a learning moment.
Packaging and Testing
Like all games in this series, Drift Well ships as an npm package with subpath exports. The physics engine is pure TypeScript, making it easy to test: 50 tests cover gravity computation, collision detection, energy management, level generation, and the full game state machine. The React component is a thin wrapper that connects the engine to Canvas 2D rendering.
The separation between engine and rendering means the same game logic could power a WebGL version, a terminal-based version, or a headless simulation for testing level generation quality. The architecture mirrors the pattern established with Signal Hop.
Get more posts like this
I write about system design, backend engineering, and building npm packages from scratch. Follow along on Substack for new posts.
Subscribe on Substack →