chore(release): 19.5 prep — CHANGELOG date, README polish, planck test coverage#1457
Conversation
…st coverage - CHANGELOG: stamp `## [19.5.0]` heading with the 2026-05-20 release date - README: Features list — add raycast / queryAABB and collision lifecycle hooks; add pluggable PhysicsAdapter mention; cleanup of stale SAT-only framing - README: Physics Adapters section — fleshed out matter / planck descriptions with the showcase examples, the portable adapter API surface (raycast / queryAABB / lifecycle), and links to the three wiki pages (migration / switching / quirks) - planck-adapter: new `tests/parity.spec.ts` — cross-adapter parity vs BuiltinAdapter (mirror of matter-adapter's parity spec, 68 tests). Box2D's polygon `radius` adds ~0.32 px / edge through the meter conversion, so AABB-dimension assertions use a 1 px tolerance instead of the matter spec's tighter 0.05 px; planck factory uses a real gravity (320 px/s² ≈ 1g) so the 60-tick fall window actually produces a floor contact for the collision-lifecycle tests. - planck-adapter: new `tests/planck-adapter-stress.spec.ts` — lifecycle leak guards (mirror of matter's stress spec, 7 tests). Mirrors the same add/remove / updateShape / init→destroy→init invariants; walks `world.getBodyList()` to verify the underlying planck world body count tracks the adapter's internal maps. Planck test count: 43 → 118. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
926eefa to
c7f7bc4
Compare
There was a problem hiding this comment.
Pull request overview
Release-prep updates for melonJS 19.5.0, polishing top-level documentation and expanding @melonjs/planck-adapter test coverage to match the existing matter-adapter parity/stress suites.
Changes:
- Stamp
packages/melonjs/CHANGELOG.md19.5.0 section with the 2026-05-20 release date. - Update
README.mdfeature bullets (Sound/Physics) and expand the “Physics Adapters” section with clearer adapter descriptions + portability notes and wiki links. - Add new
@melonjs/planck-adaptertest suites: cross-adapter parity coverage and lifecycle leak stress guards.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| README.md | Updates feature highlights and expands Physics Adapters documentation/links for 19.5. |
| packages/planck-adapter/tests/planck-adapter-stress.spec.ts | Adds lifecycle/leak regression stress tests for PlanckAdapter internals and world-body tracking. |
| packages/planck-adapter/tests/parity.spec.ts | Adds BuiltinAdapter vs PlanckAdapter parity suite to pin shared PhysicsAdapter contract behavior. |
| packages/melonjs/CHANGELOG.md | Marks 19.5.0 as released on 2026-05-20. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…lities / isGrounded / updateShape / sensor-pushout
Surface a documentation audit finding — the wiki described `raycast` and
`queryAABB` as matter-only, but as of 19.5 both are portable across every
adapter. These tests pin that contract:
- **`raycast` portable hit shape** — `{ renderable, point, normal, fraction }`
with the expected per-adapter precision (planck's polygon-radius slop
≈ 0.32 px / edge sets the planck tolerance at `aabbPrecision: 0`).
- **`queryAABB` portable region query** — bounds-overlap, empty-on-miss,
multi-body span.
- **`adapter.capabilities` shape pin** — each adapter asserts the exact
capability set it advertises (builtin: raycasts/velocityLimit/isGrounded
only; matter/planck: all six).
- **`isGrounded` in-air parity** — only the half both adapters actually
agree on; the resting-on-floor case diverges by design (see the new
BuiltinAdapter Quirks #10 in the wiki) and is intentionally out of scope.
- **`updateShape` velocity preservation** — a moving body keeps its
linear velocity when its shape is swapped (matter / planck rebuild the
body and re-apply saved velocity; builtin mutates the shape list in
place — same observable outcome).
- **Sensor + push-out matrix** — the full 2×2 grid (dyn sensor × static
sensor) confirms push-out happens iff both bodies are solid.
Driving infrastructure additions:
- Both factories grow an `expectedCapabilities` pin (used by the
capabilities test).
- Matter factory gains `rayPrecision: 0 / 1` to mirror planck's existing
`aabbPrecision` — close-to tolerances differ per adapter.
- Tests use `world.update(16)` (engine convention is dt in ms, not
seconds) and `r.alwaysUpdate = true` because builtin's broadphase is
populated by `world.update`, and its `step()` gates integration on
`inViewport || alwaysUpdate` — both irrelevant to matter / planck but
required for builtin parity in a headless test environment.
Net: matter-adapter parity 70 → 96 (+26), planck-adapter parity 68 → 94
(+26). No adapter code changes — pure regression coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ition fix, BuiltinAdapter → TS
## matter-adapter bug fix
`MatterAdapter.setPosition` now zeroes `body.positionImpulse` after the
teleport. Matter's `Resolver.postSolvePosition` applies the cached
per-body position-correction every step via the warming mechanism,
independently of `pairs.list`. After a discontinuous teleport out of a
penetrating overlap, the cached impulse contained the OLD penetration
vector and yanked the body back ≈ penetration depth on the next step.
Builtin and planck handle this natively (planck inherits the correct
behaviour from Box2D's `b2Body::SetTransform`); matter doesn't, so the
adapter does it explicitly. Regression test in both parity specs pins
drift = 0 on every adapter.
## Cross-adapter parity coverage
Both `matter-adapter/tests/parity.spec.ts` and
`planck-adapter/tests/parity.spec.ts` grow a substantial block of new
tests, each running against BuiltinAdapter + the target adapter via the
existing factory pattern:
- **`adapter.capabilities` shape pin** — per-factory `expectedCapabilities`
asserts the exact flag set (catches accidental capability flips).
- **`raycast` / `queryAABB` portable hit shape** — pins the 19.5 contract
(`renderable`, `point`, `normal`, `fraction`); previously described as
matter-only in the wiki.
- **Raycast/queryAABB are geometric, not collision-filtered** — explicitly
pin that mask/type don't filter spatial-query results.
- **`isGrounded` in-air parity** — only the half adapters actually agree on;
the resting-on-floor case diverges by design (builtin flag-based vs.
matter/planck contact-based).
- **`updateShape` preserves linear velocity** — moving body's velocity
survives a shape swap on every adapter.
- **Sensor + push-out matrix** — full 2×2 grid (dyn sensor × static sensor),
push-out happens iff both are solid.
- **`maxVelocity` clamping under sustained force** — pins behavioural
clamping, not just config propagation.
Five adversarial common-gameplay-pattern tests added under
`describe("adversarial — common gameplay patterns")`:
1. **Deferred-removal pickup pattern** — `onCollisionStart` flags coin →
`removeChildNow` after step → `queryAABB` confirms gone, no double-fire.
2. **`setPosition` away from a resting body** — cutscene / level-transition
teleport survives one step without being dragged back to the floor.
3. **`setStatic` mid-collision** — body in active contact freezes in place,
no further drift.
4. **Nested-container `removeChildNow`** — body inside a sub-Container
de-registers from the adapter (level-cleanup pattern).
5. **`setSensor` toggle mid-flight** — one-way-platform pattern; sensor=true
passes through, sensor=false lands on subsequent contact.
Plus per-adapter behavioural fills surfaced by an audit of existing
coverage:
- **matter `gravityScale: 0` prevents motion** + **`gravityScale: 0.5` falls
half as fast** — pins the per-body counter-force emulation (matter-js
0.20 has no native per-body gravityScale; the adapter compensates in a
`beforeUpdate` hook). The stress spec already covered the bookkeeping
drainage; this covers the behavioural side.
- **planck `maxVelocity`** — config propagation, `setMaxVelocity`
override, sustained-force clamping. Was zero coverage before.
Final test counts: matter 161 → 173 (+12), planck 153 → 165 (+12).
## Physics layer JS → TS migration
Two brand-new-in-19.5 files that should match the rest of the new
adapter surface (which is all TS):
- `physics/builtin/builtin-adapter.js` → `.ts` (556 lines)
- `physics/builtin/raycast.js` → `.ts` (293 lines)
Both now `implements PhysicsAdapter` / type their exports against the
public adapter interface. `raycastQuery` returns `RaycastHit[]` directly
instead of the loose `{renderable: object, …}[]` it had as JSDoc,
which lets `BuiltinAdapter.raycast` drop its boundary cast.
Adjacent JS holdouts (`body.js`, `world.js`, `detector.js`, `aseprite.js`,
`shine.js`) deferred — body / world / detector are legacy classes with
substantial scope, aseprite is internal-only, shine matches other shader
effects which are all JS. Progressive migration, one user-facing class
at a time.
## Adapter source cleanup
Zero `as unknown as` left in either adapter's `src/index.ts`:
- **`positionImpulse`** access in matter-adapter — single intersection cast
(`as Matter.Body & { positionImpulse: Matter.Vector }`) where the field
exists on every runtime body but isn't in `@types/matter-js`.
- **Ellipse shape conversion** in both adapters — entire double-cast deleted;
the `instanceof Ellipse` already narrows the type and `radiusV` is
always initialized in the constructor, so the `?.` / `??` fallbacks were
unreachable.
- **`renderable.currentTransform`** access in `syncFromPhysics` (both adapters)
— the cast + per-method `typeof === "function"` runtime guards
contradicted the actual `currentTransform: Matrix3d` typed field. Use
it directly.
## Cycle clean-up
Two physics-local cycles addressed in `madge --circular --ts-config`:
- **`physics/bounds.ts` ↔ `physics/geometries/polygon.ts`** — fully
eliminated. `Bounds.toPolygon()` removed (added 2014 for the
`me.Rect → polygon for SAT` idiom, made obsolete years ago when the
shape API learned to handle Rect/Ellipse/Polygon natively, zero
callers across the entire monorepo for the last 5+ years).
- **`physics/adapter.ts` ↔ `physics/world.js`** — adapter.ts no longer
has inline `import("./world.js").default` / `import("../renderable/
renderable.js").default` in 30 type positions; collapsed to top-level
`import type World` / `import type Renderable` aliases. Runtime-erased
via `import type`, so the cycle is no longer a real one. (madge with
`--ts-config` may still flag it conservatively; the remaining trio
is static-analysis noise.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
packages/melonjs/src/physics/builtin/raycast.ts:261
raycastQueryassumes every body shape is either anEllipseor a polygon-like shape and routes all non-Ellipseshapes into_raycastPolygon. ButBody.addShape/Body.getShapesupportsLine(and historicallyPoint), so a body with aLineshape would hit theelsebranch and_raycastPolygonwill throw when it accessesshape.points/shape.normals. Consider switching onshapeB.type(Ellipse/Polygon/Line/Point) and either implement ray entry math for the extra types or explicitly skip unsupported types instead of treating them as polygons.
…yCount()
Copilot review caught that the stress-spec helper had stale commentary
("Planck has no getBodyCount getter; walk the body list directly") —
`world.getBodyCount()` is in planck's public API and is used elsewhere
in this package's tests (`planck-adapter.spec.ts:98, 111, 120, 122`).
Replace the manual `getBodyList()` linked-list walk with the direct
getter for clarity. Behaviour unchanged; stress spec still 7/7 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntract Three issues flagged by Copilot review on #1457: 1. **`Body.addShape(bounds)` was broken** by the previous removal of `Bounds.toPolygon()`. The legacy path at `body.js:592` handled both `Rect` and `Bounds` via `shape.toPolygon()`; with that method gone, passing a Bounds as a body shape would throw at runtime. Fix: inline the rectangle-from-AABB polygon construction in `body.js` (which already imports `polygonPool` and now `Vector2d`). Same geometry the old `Bounds.toPolygon()` produced; consumer-side, so bounds.ts → polygon.ts cycle stays broken. 2. **`getBodyAABB` / `getBodyShapes` violated the adapter contract** — the documented behaviour is "returns undefined / []" for an unregistered body, but the original implementation only checked `renderable.body !== undefined`. A dangling Body reference left on `renderable.body` after `removeBody` would silently return debug geometry. Gate both on `this.bodies.has(body)` (same predicate the `getVelocity` implementation already uses) so the debug surface matches the documented contract. 3. **Stale `./raycast.js` reference** in the `raycast` method comment — the shared implementation was renamed to `raycast.ts` in this PR. One-character fix. melonjs 3484 / 12 skipped, matter 173, planck 165 — all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y constructor The `Body.addShape(bounds)` regression that Copilot caught on #1457 slipped past CI because nothing in the test suite ever called addShape with a Bounds. body.js's `addShape` is documented to accept seven distinct shape types — `Rect | Ellipse | Polygon | Line | Point | Bounds | JSON object` — but only three of those branches (Rect / Ellipse / Polygon) had behavioural coverage. Adds explicit per-branch tests: - `addShape(Bounds)` — pins the bug fix from 27af71d (regression guard for the inlined AABB-to-polygon conversion that replaced the removed `Bounds.toPolygon` method). - `addShape(Line)` — `Line extends Polygon`, so it hits the Polygon branch; pin that the inheritance path stays intact. - `addShape(Point)` — single Point as a degenerate shape. - `addShape(JSON object)` — PhysicEditor-style `{ shape: [x0,y0, ...] }` array routed through `fromJSON` (catch-all `else` branch). - `new Body(r, Bounds)` — constructor path mirror of the Bounds case. - `new Body(r, mixed-array)` — compound body with one of every shape type at once; catches branch-ordering bugs. Every conditional in `body.js#addShape` is now exercised by at least one test. body.spec.js: 81 → 89 tests, full melonjs suite: 3484 → 3490. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hapes The debug-surface contract bug Copilot caught on #1457 (BuiltinAdapter returning AABB/shapes for a body unregistered via `removeBody`) slipped past CI because the existing parity tests only exercised the "no body at all" path. They never constructed a dangling-body state — the production scenario where `renderable.body` still points at an orphaned Body instance after `adapter.removeBody(r)` cleared the adapter's bookkeeping. Adds two regression tests per parity spec, exercised against every adapter pair (Builtin + Matter, Builtin + Planck) via the factory: - `getBodyAABB returns undefined for a body removed via adapter.removeBody` - `getBodyShapes returns [] for a body removed via adapter.removeBody` Each test adds a body to the world, calls `adapter.removeBody(r)`, and asserts the debug surface returns the contracted "unregistered" values — NOT the dangling Body's geometry. matter / planck already behaved correctly here (their `bodyMap.get(r)` returns undefined for the orphan); BuiltinAdapter now does too, gated on `this.bodies.has(body)` in 27af71d. matter parity 114 → 118, planck parity 112 → 116. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PR's apparent green check status was masking a real CI lint failure. GitHub Actions' path-filter strategy for the docs-only shim (main-docs.yml) operates on the whole PR diff, not the synchronize delta — so a mixed PR (some .md + some code) triggers both main.yml and main-docs.yml, and the faster shim wins the race via the shared concurrency group. The real lint job from main.yml was being cancelled silently while the shim reported success. The last actually-real lint run (26205443092 at commit 27af71d) caught 9 errors all from this PR: - matter-adapter/src/index.ts:675 — `Missing JSDoc @param "body"` on the new `invalidateContactsFor` method. Resolved by dropping the JSDoc tag block; the prose is now a `//` comment inside the method body (matter-adapter convention for trivial private helpers). - matter-adapter / planck-adapter parity specs (4 errors) — the new adversarial pickup-pattern test captured the `this` reference inside `Coin.onCollisionStart` (`pickedUp = this`), tripping `@typescript-eslint/no-this-alias`. Refactored to a boolean flag + the existing outer-scope `coin` reference; functionally equivalent, no lint disable needed. - matter / planck parity specs (2 errors) — the new dangling-body test used `adapter.getBodyShapes?.(r)` + `shapes!.length`, both flagged as unnecessary by `no-unnecessary-type-assertion` (the method is required on the interface). Dropped the optional chain and the bang. - planck-adapter spec (4 errors) — the maxVelocity tests used `adapter.getMaxVelocity?.(r)` + `cap!.x`/`cap!.y`. `getMaxVelocity` is required on PlanckAdapter; dropped both the `?.` and `!`. Local repro: `pnpm lint --force` from repo root. Now clean across all packages. Test counts unchanged — matter 177, planck 169, melonjs 3490. (The underlying CI partition bug — main-docs.yml shim firing on mixed PRs — should be tracked as a separate 19.6 follow-up; this commit only fixes the lint errors it was hiding.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two-workflow partition (main.yml runs lint+test for code changes,
main-docs.yml emits matching success checks for docs-only PRs) was
racy on mixed PRs.
GitHub Actions evaluates `paths` / `paths-ignore` against the **whole
PR diff**, not the synchronize delta. A PR that started with a
README.md tweak and later added .ts changes triggers BOTH workflows
on every subsequent push:
- main.yml fires (paths-ignore for *.md matches → still has non-md
files → triggers)
- main-docs.yml fires (paths *.md matches → still has md files →
triggers)
Both share `concurrency.group = ci-${{ github.ref }}` with
`cancel-in-progress: true`, so the faster shim (~13s) cancelled the
real lint+test (~1-2 min) and reported success. Branch protection
saw a green "lint" / "test" check name and let the PR pass, even
though real lint had 9 errors silently dropped on the floor.
#1457 (this PR) was the first PR where the bug surfaced because the
real lint actually failed — caught by a Copilot review that pointed
me at the cancelled run log.
Fix: drop main-docs.yml entirely. main.yml runs unconditionally on
every push/PR. Docs-only PRs pay ~2 minutes of CI cost as the
trade-off; cheaper than silently merging a lint failure. If the
docs-only fast-path becomes painful, the right replacement is a
single workflow with an internal docs-only detection step
(e.g. `dorny/paths-filter` action) that gates the heavy work — same
spirit, but only one workflow per push so no race is possible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same as the earlier fix in builtin-adapter.ts (27af71d), but in the detector's own rayCast JSDoc. Compile-time imports were already correct (line 3: import from './raycast.ts'); the stale string only existed in the prose comment above rayCast(), which the runtime / TS never read.
…elease date - melonjs 19.5.0: _2026-05-20_ → _2026-05-22_ (pushed back from the original planck-adapter merge date because the release-prep PR surfaced + fixed real bugs along the way — see #1457) - @melonjs/matter-adapter 1.0.0: _unreleased_ → _2026-05-22_ - @melonjs/planck-adapter 1.0.0: _2026-05-22_ (initial release) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
packages/melonjs/src/physics/builtin/raycast.ts:227
Body.addShapesupportsPointshapes, butraycastQueryassumes every body shape has a.posand is either anEllipseorPolygon(see thegetShape(i): Polygon | Ellipsecast and_computeShapeAbsPos). If a body contains aPoint, this will throw at runtime when accessingshape.pos. Consider widening the type to includePointand either implement point hits or explicitly skip unsupported shape types (e.g., ignoreshape.type === "Point").
Summary
Release-prep PR for 19.5.0 (ships alongside
melonjs,@melonjs/matter-adapter,@melonjs/planck-adapter).Planck spec count: 43 → 118.
Test plan
🤖 Generated with Claude Code