Rapier 2D (@dimforge/rapier2d-compat 0.20) comes in as the physics engine: the editor loads it once in the background right after opening a project from its own chunk, so the main bundle grows by 0.19 KB gzip and the first frame does not wait; loadPhysics, physicsStatus and the 100 px = 1 m conversion live in src/runtime/physics/engine.ts, __gm.physics() reports the state, unit tests pin free fall and a box resting on a box, and e2e module 41 pins that Rapier is ready from its own chunk without console noise (#145)
game-maker / gate (push) Successful in 1m46s
game-maker / suite (push) Successful in 6m12s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-18 16:56:47 +02:00
1 parent 6d7d0005cf
commit d02bcca266
9 files changed
+143 -1

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';
import {
loadPhysics,
PIXELS_PER_METRE,
physicsStatus,
type Rapier,
toMetres,
toPixels,
} from '../../src/runtime/physics/engine';
const STEP = 1 / 60;
const GRAVITY = 980;
/** A 400 × 20 px floor whose top is at y = 290, and a 32 px crate dropped from y = 0. */
function crateOverFloor(rapier: Rapier) {
const world = new rapier.World({ x: 0, y: toMetres(GRAVITY) });
world.timestep = STEP;
const floor = world.createRigidBody(rapier.RigidBodyDesc.fixed().setTranslation(0, toMetres(300)));
world.createCollider(rapier.ColliderDesc.cuboid(toMetres(200), toMetres(10)), floor);
const crate = world.createRigidBody(rapier.RigidBodyDesc.dynamic().setTranslation(0, 0));
world.createCollider(rapier.ColliderDesc.cuboid(toMetres(16), toMetres(16)), crate);
const run = (steps: number) => {
for (let i = 0; i < steps; i++) world.step();
};
const y = () => toPixels(crate.translation().y);
return { world, crate, run, y };
}
describe('physics engine', () => {
it('loads once and reports the Rapier version', async () => {
const first = loadPhysics();
expect(loadPhysics()).toBe(first);
const rapier = await first;
expect(rapier.version()).toBe('0.20.0');
expect(physicsStatus()).toMatchObject({ state: 'ready', version: '0.20.0' });
});
it('converts at 100 px to the metre', () => {
expect(PIXELS_PER_METRE).toBe(100);
expect(toMetres(980)).toBe(9.8);
expect(toPixels(toMetres(123.5))).toBeCloseTo(123.5, 10);
});
it('lets a crate fall freely in pixels per second squared', async () => {
const { run, y, crate } = crateOverFloor(await loadPhysics());
run(30);
// ½ g t² = 122.5 px after half a second; the integrator lands about a pixel past it.
expect(Math.abs(y() - 0.5 * GRAVITY * 0.25)).toBeLessThan(1.5);
expect(toPixels(crate.linvel().y)).toBeCloseTo(GRAVITY * 0.5, 3);
});
it('keeps a crate resting on the floor', async () => {
const { run, y, crate } = crateOverFloor(await loadPhysics());
run(180);
expect(Math.abs(y() - 274)).toBeLessThan(0.5);
expect(Math.abs(toPixels(crate.linvel().y))).toBeLessThan(0.5);
run(120);
expect(Math.abs(y() - 274)).toBeLessThan(0.5);
});
});