melonjs 20.5.0 → 20.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/build/application/defaultApplicationSettings.d.ts +1 -0
  2. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  3. package/build/application/settings.d.ts +37 -0
  4. package/build/application/settings.d.ts.map +1 -1
  5. package/build/camera/camera2d.d.ts.map +1 -1
  6. package/build/geometries/polygon.d.ts.map +1 -1
  7. package/build/index.d.ts +1 -1
  8. package/build/index.d.ts.map +1 -1
  9. package/build/index.js +22428 -21236
  10. package/build/index.js.map +4 -4
  11. package/build/lang/deprecated.d.ts.map +1 -1
  12. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  13. package/build/level/level.d.ts +1 -0
  14. package/build/level/level.d.ts.map +1 -1
  15. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  16. package/build/lighting/light2d.d.ts +68 -2
  17. package/build/lighting/light2d.d.ts.map +1 -1
  18. package/build/lighting/light3d.d.ts.map +1 -1
  19. package/build/loader/loader.d.ts +54 -0
  20. package/build/loader/loader.d.ts.map +1 -1
  21. package/build/loader/loadingscreen.d.ts.map +1 -1
  22. package/build/loader/parsers/fontface.d.ts +2 -0
  23. package/build/loader/parsers/fontface.d.ts.map +1 -1
  24. package/build/loader/parsers/mtl.d.ts.map +1 -1
  25. package/build/physics/adapter.d.ts +143 -2
  26. package/build/physics/adapter.d.ts.map +1 -1
  27. package/build/physics/builtin/body.d.ts +6 -16
  28. package/build/physics/builtin/body.d.ts.map +1 -1
  29. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  30. package/build/physics/builtin/detector.d.ts.map +1 -1
  31. package/build/physics/builtin/sat.d.ts.map +1 -1
  32. package/build/physics/builtin/sat3d.d.ts.map +1 -1
  33. package/build/physics/physicseditor.d.ts +2 -0
  34. package/build/physics/physicseditor.d.ts.map +1 -0
  35. package/build/renderable/container.d.ts.map +1 -1
  36. package/build/renderable/entity/entity.d.ts +2 -0
  37. package/build/renderable/entity/entity.d.ts.map +1 -1
  38. package/build/renderable/mesh.d.ts +23 -1
  39. package/build/renderable/mesh.d.ts.map +1 -1
  40. package/build/renderable/renderable.d.ts +39 -1
  41. package/build/renderable/renderable.d.ts.map +1 -1
  42. package/build/renderable/sprite.d.ts +72 -0
  43. package/build/renderable/sprite.d.ts.map +1 -1
  44. package/build/renderable/trigger.d.ts +12 -9
  45. package/build/renderable/trigger.d.ts.map +1 -1
  46. package/build/state/stage.d.ts +22 -0
  47. package/build/state/stage.d.ts.map +1 -1
  48. package/build/video/buffer/vertex.d.ts.map +1 -1
  49. package/build/video/gpu/meshmaterial.d.ts +2 -0
  50. package/build/video/gpu/meshmaterial.d.ts.map +1 -0
  51. package/build/video/renderer.d.ts +44 -0
  52. package/build/video/renderer.d.ts.map +1 -1
  53. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  54. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -1
  55. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  56. package/build/video/webgl/batchers/mesh_batcher.d.ts +2 -0
  57. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  58. package/build/video/webgl/shaders/multitexture-lit.d.ts.map +1 -1
  59. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  60. package/build/video/webgpu/batchers/lit_quad_batcher.d.ts +17 -0
  61. package/build/video/webgpu/batchers/lit_quad_batcher.d.ts.map +1 -1
  62. package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
  63. package/build/video/webgpu/effect_binding.d.ts.map +1 -1
  64. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  65. package/build/video/webgpu/texture/store.d.ts.map +1 -1
  66. package/build/video/webgpu/webgpu_renderer.d.ts +1 -0
  67. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/skills/melonjs-3d/SKILL.md +16 -0
  70. package/skills/melonjs-3d-assets/SKILL.md +51 -8
  71. package/skills/melonjs-effects-and-shaders/SKILL.md +37 -0
  72. package/skills/melonjs-lighting/SKILL.md +45 -1
  73. package/skills/melonjs-loading-assets/SKILL.md +43 -9
  74. package/skills/melonjs-performance/SKILL.md +34 -6
  75. package/skills/melonjs-physics/SKILL.md +320 -3
  76. package/skills/melonjs-renderables/SKILL.md +13 -0
  77. package/skills/melonjs-scenes-and-state/SKILL.md +37 -0
  78. package/skills/melonjs-tilemaps/SKILL.md +9 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-physics
3
- description: "Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, and the behaviour differences between adapters. Triggers on: Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT."
3
+ description: "Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -19,6 +19,61 @@ license: MIT
19
19
  | **`@melonjs/planck-adapter`** | Full rigid-body dynamics — stacking, joints, realistic restitution. |
20
20
  | **`@melonjs/matter-adapter`** | Same class of thing, backed by matter-js. |
21
21
 
22
+ ### Picking one
23
+
24
+ Start with the built-in. It is the default for a reason: no dependency, the
25
+ smallest bundle, and its position-based model is what most 2D games actually
26
+ want — a platformer's jump arc is a designed curve, not a simulated one, and
27
+ tuning gravity against a solver you are fighting is a bad afternoon.
28
+
29
+ **Move to an adapter when the game needs something the builtin cannot express**,
30
+ and these are the usual triggers:
31
+
32
+ - **Rotation that collides.** The clearest signal. The builtin's SAT never reads
33
+ `body.angle`, so a spinning shape collides as though it never turned. See
34
+ the note in *Built-in world quirks* below.
35
+ - **Torque, angular velocity, or spin from an impact.** Anything that should
36
+ start turning *because* it was hit.
37
+ - **Joints and constraints** — ragdolls, chains, ropes, vehicles, hinged doors.
38
+ The builtin has no concept of them.
39
+ - **Stacking and resting contacts.** Boxes that pile up and stay put need an
40
+ iterative solver. The builtin resolves each contact once per step with no
41
+ iteration pass, which splits three ways. A dynamic body resting on STATIC
42
+ geometry is exact, to the pixel, which is the platformer case it exists for.
43
+ A body squeezed between a dynamic body and static geometry is exact too: the
44
+ immovable side gets the casting vote and the other body absorbs the whole
45
+ correction, so a crate shoved against a wall stops at the wall. But a body
46
+ squeezed between two DYNAMIC bodies has no immovable side to defer to, so
47
+ those per-pair corrections still compete and it comes to rest slightly
48
+ overlapping. Three stacked 44px boxes rest flush on the floor with about 2px
49
+ of overlap inside the pile, and stay there: a stable equilibrium, not jitter
50
+ you can wait out or tune away.
51
+ - **Believable restitution and friction**, where a ball's bounce height and roll
52
+ should follow from its material rather than from code you wrote per case.
53
+
54
+ **Stay on the built-in when** the game is grid or tile based, movement is
55
+ authored rather than simulated, collision is "did these two boxes touch" plus a
56
+ push-out, or you are shipping to a tight bundle budget. Tiled maps, `Trigger`
57
+ and `Collectable` all work on every backend, so this is not a fork in the road
58
+ for level content.
59
+
60
+ **planck or matter?** Both are real rigid-body engines and either will do; the
61
+ adapters expose the same portable API, so a game can switch with an import
62
+ change and a gravity re-tune.
63
+
64
+ - **planck** (a Box2D port) is the more accurate and the more predictable under
65
+ stress: better stacking, better joint behaviour, a continuous-collision
66
+ `bullet` mode for fast movers. It works in **metres**, so it has a
67
+ `pixelsPerMeter` scale to think about, and its tuning vocabulary is Box2D's.
68
+ - **matter-js** works directly in **pixels**, which makes it the gentler
69
+ introduction, and its compound bodies and constraints are pleasant to author.
70
+ It is the softer solver: stacks settle less crisply and fast bodies need
71
+ `subSteps`.
72
+
73
+ If you have no preference and the game leans on stacking, joints or fast
74
+ projectiles, take planck. If you want to be up and running with the least
75
+ conversion to think about, take matter.
76
+
22
77
  An adapter is an `Application` setting, not a plugin:
23
78
 
24
79
  ```js
@@ -44,6 +99,34 @@ body radius. Pick **one** adapter per game; `physic: "none"` disables physics
44
99
  entirely — `world.step` skips the simulation and the world behaves as a pure
45
100
  scene graph.
46
101
 
102
+ **Gravity is a different unit in every backend, and the defaults are NOT the
103
+ same speed.** Each is "Earth-like" in its own convention, but measured against
104
+ each other by timing a 200px free fall in one scene:
105
+
106
+ | backend | default | unit | measured fall |
107
+ |---|---|---|---|
108
+ | built-in | `0.98` | per FRAME, integrated against `timer.tick` | **~4450 px/s²** |
109
+ | matter | `{x: 0, y: 1}` | matter's own, after its internal `gravity.scale` | **~900 px/s²** |
110
+ | planck | `320` | pixels per second² | **~330 px/s²** |
111
+
112
+ That is a **thirteenfold spread**, so swapping the built-in world for an adapter
113
+ and keeping everything else the same makes the whole game feel different. It is
114
+ not a bug in either one: they simply do not share a unit.
115
+
116
+ To match a backend you are porting FROM, pick a target in px/s² and convert:
117
+ planck takes it directly; matter takes `target / 900`; the built-in takes
118
+ `target / 4450 * 0.98`, because it applies gravity per frame rather than per
119
+ second. Passing one number to two of them compares nothing: hand planck
120
+ matter's `{x: 0, y: 1}` and it runs at a three-hundredth of Earth gravity.
121
+
122
+ That mistake is quiet and it does not look like a gravity problem. Bodies fall
123
+ slowly enough that a spawner keeping a fixed interval starts constructing them
124
+ inside each other, and **a body born overlapping another is outside what a
125
+ solver promises to fix**: the contact normals of an interlocked pair can cancel
126
+ out, leaving the position solver with nowhere to push. The symptom is two
127
+ shapes permanently fused at rest, which reads as a broken collision shape
128
+ rather than as a bad gravity constant. Spawn clear of anything already there.
129
+
47
130
  `world.physic` carries the active adapter's label (`"builtin"`, `"planck"`,
48
131
  `"matter"`, `"none"`), so game code can branch without importing the class.
49
132
 
@@ -72,6 +155,19 @@ world; the broadphase picks the renderable up on the next `world.update()`.
72
155
  Calling `adapter.addBody()` yourself registers the body but leaves the
73
156
  renderable out of the scene graph, so it integrates but never collides.
74
157
 
158
+ **`fixedRotation` follows the engine, so bodies rotate.** Both matter and
159
+ planck default it to `false`, and so do the adapters from matter-adapter 1.4.0
160
+ and planck-adapter 1.5.0. Pass `fixedRotation: true` for anything that must
161
+ stay upright, such as a platformer actor. Earlier adapter versions locked
162
+ rotation unless told otherwise, which inverted the engine they wrap: bodies
163
+ landed holding whatever angle they were given and no collision could turn them,
164
+ while `setAngle()` still worked, so a body could look rotated while being
165
+ unable to rotate.
166
+
167
+ `frictionAir` damps **rotation as well as translation** on both adapters, so
168
+ it is also what settles a spin. Left at `0` a body that starts turning never
169
+ stops, which only became visible once rotation stopped being locked.
170
+
75
171
  Other portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),
76
172
  `friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,
77
173
  `isSensor`, `userData`. Not every adapter honours every one — the built-in
@@ -106,6 +202,166 @@ For contacts reported per *shape pair* rather than per body pair, define
106
202
  Declaring at least one of them opts into shape-pair enumeration; declaring none
107
203
  costs nothing.
108
204
 
205
+ ### Shapes drawn in a physics-shape editor
206
+
207
+ A visual collision-shape editor exports a JSON file holding a map of body name
208
+ to the fixtures authored for it. Preload it like any other JSON, then name it
209
+ from `bodyDef.shapes` with `id` naming the body to read:
210
+
211
+ ```js
212
+ // in the preloader
213
+ { name: "shapes", type: "json", src: "data/physics/shapes.json" }
214
+
215
+ // then, on the renderable
216
+ this.bodyDef = { type: "dynamic", shapes: "shapes", id: "hotdog" };
217
+ ```
218
+
219
+ This works identically on the built-in, matter and planck adapters — the file is
220
+ resolved into real shapes before any adapter is handed the definition. Circles
221
+ and polygons both load, and a concave outline (exported already decomposed into
222
+ convex pieces) becomes one `Polygon` per piece.
223
+
224
+ The density, friction and bounce the shapes were authored with are carried onto
225
+ the body definition too, each honoured by whichever backends support it — the
226
+ built-in adapter ignores contact `friction`, as it does for a hand-written
227
+ definition. The format carries these per fixture, but only planck could honour
228
+ that, so they reduce onto the body and a file whose fixtures disagree warns. An
229
+ explicit field on the definition always wins:
230
+
231
+ ```js
232
+ // bounces at 0.9 whatever the file says
233
+ this.bodyDef = { type: "dynamic", shapes: "shapes", id: "hotdog", restitution: 0.9 };
234
+ ```
235
+
236
+ The exporting tool's own filter block is **not** read, in any spelling. Its
237
+ category and mask bits are numbers in that tool's namespace rather than
238
+ {@link collision.types}, and a template writes its defaults for every fixture
239
+ whether or not you set anything — importing them would silently tag a shape as
240
+ something it is not and stop it colliding. Set `collisionType` / `collisionMask`
241
+ on the body definition instead. A file written by hand may carry those two names
242
+ per fixture, and `isSensor`, which mean the same thing here as there.
243
+
244
+ Authored shapes and imported ones can sit in the same list.
245
+
246
+ Two bodies must never share one `shapes` array: a body **owns** its shapes and
247
+ rotating it mutates them in place. Naming the file sidesteps this — every body
248
+ resolved from it mints its own shapes.
249
+
250
+ `Body#fromJSON()` and passing an exported list to `addShape()` are the old,
251
+ built-in-only spelling of this and are deprecated since 20.7.0.
252
+
253
+ ### Which shapes each backend can actually simulate
254
+
255
+ | shape | builtin | matter | planck |
256
+ |---|---|---|---|
257
+ | `Rect`, `Polygon` | exact | exact | exact, up to 12 vertices |
258
+ | `RoundRect` | exact | exact (decomposed) | sampled down to 12 vertices |
259
+ | `Ellipse` | exact | circle of the average radius | circle of the average radius |
260
+ | `Line` | exact | thin quad along the segment | thin quad along the segment |
261
+ | `Point`, `Box3d`, `Sphere` | supported | **throws** | **throws** |
262
+
263
+ Two of those are worth knowing before you author geometry. Box2D caps a
264
+ polygon's vertex count and says nothing when you go over: measured, a
265
+ 16-point outline comes back with 12 vertices and a 36-point one collapses
266
+ from 70x36 to 47x18. A `RoundRect` carries 36 points, four corners of nine
267
+ arc segments each, so it is the usual way to meet that cap; the adapter now
268
+ samples it down and keeps the extent. Anything the 2D backends cannot express
269
+ is refused loudly on both, rather than leaving a body with no collision
270
+ geometry that silently never collides.
271
+
272
+ Whatever a backend really simulates is what `getBodyShapes()` reports, so the
273
+ debug panel is the fastest way to see an approximation for what it is.
274
+
275
+ ### `Line` shapes, and why slopes need a rigid-body adapter
276
+
277
+ A `Line` is two points and no area, and it is what Tiled emits for every
278
+ polyline, so it is the natural way to author a slope or a strip of ground.
279
+ Neither rigid-body engine has a segment primitive, so both simulate one as a
280
+ thin quad following the segment (2px), which is what `getBodyShapes()` reports
281
+ back. Before 20.7 each substituted something of its own instead: matter the
282
+ line's bounding box, planck a one-metre square wherever the body happened to
283
+ be. Neither logged anything.
284
+
285
+ The builtin solver resolves a sloped segment too, and correctly: a body lands
286
+ on it and SLIDES down it, holding a constant distance from the surface while
287
+ its x advances. What it has no notion of is SURFACE friction. `def.friction`
288
+ is ignored entirely and `body.friction` is a per-step velocity damping vector
289
+ fed from `def.frictionAir`, so nothing removes the tangential component of
290
+ gravity. A body on an incline therefore accelerates down it forever and leaves
291
+ at the end, and in a valley of segments it swings from arm to arm for a while
292
+ before coming to rest on the flat. (It used to escape through an arm instead;
293
+ that was a separate defect in how a body was separated from a shape list, fixed
294
+ in 20.7.)
295
+
296
+ matter and planck both have real surface friction, so the same valley holds a
297
+ body still.
298
+
299
+ **Several shapes on one body.** A Tiled polyline arrives as ONE body carrying
300
+ one shape per segment, and until 20.7 a body resting on such a ground could
301
+ sink through one of the segments and drop out. The narrowphase reports the
302
+ FIRST overlapping shape pair it finds, and the compound resolution pass
303
+ re-ran that same first-hit-wins scan, so it kept re-resolving the pair it had
304
+ already resolved while a neighbour was penetrated unmeasured. Every pair is
305
+ enumerated now. If you are ever diagnosing something similar, the experiment
306
+ that localises it is to re-run the same geometry as SEVERAL single-shape
307
+ bodies: if that holds and one multi-shape body does not, the fault is in the
308
+ compound path rather than in the shape type.
309
+
310
+ Do not diagnose this by dropping a body and reading its final `y`. A body that
311
+ slid off the end of a ramp and a body that fell straight through it both end up
312
+ far below, and they look identical in that one number. Track the body against
313
+ the surface height at its CURRENT x instead: riding the slope shows a constant
314
+ offset while x advances.
315
+
316
+ So segments collide on every backend. If you need a body to come to REST on a
317
+ sloped one, use matter or planck, or damp it yourself: the builtin will slide
318
+ it down the incline indefinitely. The `Line Collision` example draws the
319
+ authored segment against the geometry each backend reports, which is the
320
+ quickest way to see what a backend is really simulating.
321
+
322
+ ### `anchorPoint` moves the collision shapes with the drawing
323
+
324
+ `anchorPoint` says where in its own bounds a renderable sits on its `pos`:
325
+ `updateBounds()` translates its bounds by `-width * anchorPoint.x, -height *
326
+ anchorPoint.y`, and `preDraw` shifts its rendering by the same amount. A
327
+ body's collision shapes are measured from that same frame, so a shape of
328
+ `new Rect(0, 0, width, height)` covers the sprite whatever the anchor is, and
329
+ a shape's own `pos` still offsets it inside that frame (a small hitbox at the
330
+ feet, a hurtbox at the head).
331
+
332
+ This was broken until 20.7: shapes were measured from `pos` regardless of the
333
+ anchor, so anything not anchored at its top-left corner collided where it was
334
+ not drawn, by half a body at the default centred anchor and by a full body
335
+ height at the bottom anchor a platformer actor uses. If you compensated by
336
+ offsetting your shapes by hand, remove those offsets. `Entity` and Tiled
337
+ objects set their own anchor to `(0, 0)`, so nothing built either of those
338
+ ways is affected.
339
+
340
+ The debug panel is the quickest check: green is the renderable's bounds, red
341
+ is what actually collides, and the two now agree.
342
+
343
+ One renderable does not take that offset at all: one that has cleared
344
+ `applyAnchorTransform`, the flag `preDraw` reads before shifting anything.
345
+ `GLTFModel` sets it `false` outright and `Mesh` clears it under a `Camera3d`,
346
+ because both emit world coordinates and pivot about their own model origin
347
+ rather than a bounds box, so they draw at `pos` whatever their `anchorPoint`
348
+ still holds. Their bodies are built there too, on every backend, and
349
+ `raycast3d` reports hits in that same frame. If you clear the flag on a
350
+ renderable of your own, the same applies: you place it, and your shapes are
351
+ measured from `pos`.
352
+
353
+ ### `autoTransform: false` opts out of rotated bounds
354
+
355
+ `updateBounds()` only applies `currentTransform` when `autoTransform` is
356
+ `true`. Turn it off and the renderable's bounds stay the UNROTATED frame
357
+ while the sprite turns inside them, which quietly feeds a wrong box to
358
+ frustum culling and to the broadphase. Measured on a 60x20 box at 90
359
+ degrees: `20x60` with `autoTransform` on, `60x20` with it off.
360
+
361
+ That flag means "I place myself", so it is a fair contract, but reach for it
362
+ only when you really are drawing in your own frame, and remember the bounds
363
+ are yours to keep honest then.
364
+
109
365
  ## Move with forces, not by assigning position
110
366
 
111
367
  ```js
@@ -311,6 +567,13 @@ real rigid-body engine:
311
567
  - **Dynamic-dynamic collision is position-based, not Newtonian.** Separation is
312
568
  mass-proportional but the velocity response is per-body cancellation — two
313
569
  equal-mass bodies do not exchange momentum the way you would expect.
570
+ - **An immovable contact wins the step.** Each step records, per body, which
571
+ directions it has already been pushed out of, and a later dynamic-dynamic
572
+ contact will not push it back in one of those directions: the other body
573
+ takes the whole correction instead. A light body shoving a heavy one against
574
+ a wall therefore stops dead at the wall rather than driving it through, and
575
+ the shover absorbs all of the separation. The record lasts one step and is
576
+ not contact state — it says nothing about what a body is touching.
314
577
  - **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate
315
578
  `app.world.adapter.gravity` at runtime, or pass
316
579
  `physic: new BuiltinAdapter({ gravity })` to override it. `body.gravityScale`
@@ -319,8 +582,7 @@ real rigid-body engine:
319
582
  - **`applyForce(x, y)` is linear only** unless you pass the optional
320
583
  `(pointX, pointY)` application point, which generates a torque
321
584
  `τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.
322
- `applyTorque(τ)` is the direct form. Note the rotation is visual: SAT
323
- collisions stay axis-aligned.
585
+ `applyTorque(τ)` is the direct form.
324
586
  - **Force accumulators reset at end-of-step** — for every body, including
325
587
  static, paused and off-screen ones — so forces must be applied every frame to
326
588
  persist.
@@ -342,6 +604,50 @@ real rigid-body engine:
342
604
  - **`addBody` throws on double-registration** for a renderable that is already
343
605
  adapter-managed.
344
606
 
607
+ ### Rotation collides on planck and matter, but not on the built-in solver
608
+
609
+ This is the sharpest behaviour difference between the adapters, and it is
610
+ silent — nothing warns you.
611
+
612
+ | adapter | a rotating body |
613
+ | --- | --- |
614
+ | **builtin** | rotates **visually only**. `body.angle`, `setAngle()` and angular integration write `renderable.currentTransform`; the collision shapes are never rotated, so a spinning sprite collides as if it were upright |
615
+ | **planck** / **matter** | rotates for real — the solver owns the body's transform and collision follows it |
616
+
617
+ So a turret, a swinging platform or a tumbling crate that must *collide*
618
+ rotated needs planck or matter. Under the builtin solver, approximate it with a
619
+ shape that is close enough at every angle (a circle, or a square for
620
+ 90° steps), or rotate the shapes yourself with `body.rotate(angle)` — which
621
+ does rotate them, unlike `setAngle()`.
622
+
623
+ `body.rotate()` applies a **delta** and does not update `body.angle`; the two
624
+ are unlinked, so a body driven by `rotate()` still reports an angle of `0`.
625
+ Track it yourself if you need to read it back. Before 20.7 it also threw on a
626
+ `Box3d` or `Point` shape, and did not tell its owner that the body's bounds had
627
+ grown.
628
+
629
+ `adapter.getBodyShapes(renderable)` reports whichever is true: the geometry
630
+ the engine is actually simulating on planck and matter (matter-adapter 1.3.0 /
631
+ planck-adapter 1.4.0 and later), unrotated shapes on the builtin solver because
632
+ there they genuinely are. That is what the debug overlay draws, so the hitbox
633
+ you see is the hitbox that collides — a sprite spinning inside a stationary red
634
+ box on the builtin is the solver telling you the truth, not a drawing bug.
635
+
636
+ On planck and matter that report comes from the engine rather than from your
637
+ definitions, so it shows the approximations they make: an `Ellipse` appears as
638
+ the circle of average radius it is simulated as, a concave polygon as the
639
+ convex pieces it was decomposed into, and a `Rect` as a `Polygon`. Your
640
+ authored shapes are untouched and still readable on `renderable.bodyDef.shapes`.
641
+
642
+ **If you need rotation to affect collision, use planck or matter.** That is the
643
+ dividing line: the builtin is a position-based solver whose SAT never reads
644
+ `body.angle`, so rotation there is a visual property. `body.rotate(angle)` is
645
+ the workaround, not a rotating body — it permanently moves the shapes, does not
646
+ track an angle, and gives you no angular velocity, torque or rotational
647
+ response from a collision. A game whose collision genuinely turns (a spinning
648
+ hazard, a swinging bridge, a car, anything with torque) wants a real rigid-body
649
+ adapter rather than a workaround.
650
+
345
651
  ## Porting between adapters
346
652
 
347
653
  Same call, different magnitude — these are the ones that waste an afternoon:
@@ -389,11 +695,22 @@ use them. `adapter.capabilities` (`constraints`,
389
695
  | `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |
390
696
  | `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |
391
697
  | off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |
698
+ | a body sinks through one shape of a multi-shape body | fixed in 20.7; before that the compound pass only ever re-resolved the first overlapping pair |
699
+ | a body will not stay put on a slope on the builtin | it is riding the slope and sliding: the builtin has no surface friction, only `frictionAir` damping. Damp it yourself, or use matter/planck |
700
+ | the sprite is drawn offset from its hitbox | pre-20.7 `anchorPoint` moved the drawing but not the collision shapes; upgrade, and drop any offsets you added to compensate |
701
+ | a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |
702
+ | a body in a pile sinks into the floor (built-in) | fixed in 20.7 for anything with an immovable side; bodies pinned only by other DYNAMIC bodies still overlap by a pixel or two, which is what planck/matter are for |
392
703
  | forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |
393
704
  | `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, melonJS the top-left — the adapter offsets between them |
705
+ | `bodyDef.shapes: "…"` throws "is not loaded" | the name must match the `name` the JSON was preloaded under, not its filename |
706
+ | everything falls far too fast or too slow after swapping backends | gravity is a different unit per backend; see the table above |
707
+ | debris lands holding its spawn angle and never tumbles | `fixedRotation: true`, or matter-adapter < 1.4.0 / planck-adapter < 1.5.0 |
708
+ | a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use planck/matter, or `body.rotate(angle)` |
709
+ | the debug hitbox does not follow a spinning body | pre-1.2.1 matter-adapter / pre-1.3.1 planck-adapter returned the authored shapes rather than the rotated ones |
394
710
  | a Tiled polyline becomes a solid box on matter | matter cannot build a zero-area polygon; the adapter falls back to its AABB |
395
711
 
396
712
  ## Related skills
397
713
 
398
714
  - `melonjs-renderables` — `isKinematic`, which also gates the broadphase
399
715
  - `melonjs-tilemaps` — collision shapes authored in Tiled
716
+ - `melonjs-loading-assets` — preloading a shape editor's export as JSON
@@ -215,6 +215,18 @@ sprite.alpha = 0; // hides it
215
215
 
216
216
  Use `alpha` for a blink, or remove the child for a long absence.
217
217
 
218
+ ## A mask belongs to you, not to the renderable
219
+
220
+ `renderable.mask` takes a shape you created, and `destroy()` releases the
221
+ reference without recycling it: the same instance may still be masking
222
+ something else, so the engine has no claim on it. If you pool masks, release
223
+ them yourself.
224
+
225
+ Before 20.7.0 `destroy()` tried to recycle it and **threw** for every type
226
+ `mask` accepts, because it used the legacy name-keyed pool and no geometry
227
+ class is registered there. It failed mid-teardown, so a masked renderable
228
+ could not be destroyed or removed without keepalive at all.
229
+
218
230
  ## Update and draw
219
231
 
220
232
  ```js
@@ -260,6 +272,7 @@ Hand-rolled equivalents miss the batching and the multi-backend support.
260
272
  | object draws at its parent's origin, ignores its position | `draw()` not reading `this.pos` |
261
273
  | pointer handler registered but never fires | `isKinematic` left at its `true` default |
262
274
  | wrong draw order | anything other than `addChild(child, z)` — see section 3 |
275
+ | `me.pool: object ... cannot be recycled` while destroying | a mask on a renderable, before 20.7.0 |
263
276
  | effect cannot be re-enabled after removal | `removePostEffect()` destroyed it |
264
277
  | effect silently does nothing | running on the Canvas renderer |
265
278
  | object appears frozen while its state changes | `update()` not returning `true` |
@@ -49,6 +49,43 @@ always drives the switch from its own completion callback.
49
49
  registered for that id, and `state.set` throws if the second argument is not a
50
50
  `Stage` instance. Neither fails quietly.
51
51
 
52
+ ### Always leave `state.LOADING` — it is transitional
53
+
54
+ `loader.preload()` switches to `state.LOADING` for you. Building your scene in
55
+ the preload callback and then *staying there* looks like it works, and is one
56
+ of the easiest mistakes to make:
57
+
58
+ ```js
59
+ // WRONG — the game now runs inside the loading stage, forever
60
+ loader.preload(assets, () => {
61
+ app.world.addChild(new Player(...));
62
+ });
63
+ ```
64
+
65
+ Nothing ever destroys that stage, so the built-in loading screen's logo and
66
+ progress bar are never removed and sit on top of your game. (Eight of this
67
+ repository's own examples did this before it was noticed, so it is not an
68
+ obscure trap.)
69
+
70
+ Give the scene a stage of its own and switch to it:
71
+
72
+ ```js
73
+ class PlayScreen extends Stage {
74
+ onResetEvent(app) {
75
+ app.world.addChild(new Player(...));
76
+ }
77
+ }
78
+
79
+ await loader.preload(assets);
80
+ state.set(state.PLAY, new PlayScreen());
81
+ state.change(state.PLAY);
82
+ ```
83
+
84
+ The awaited form reads better here than the callback, because "load, then
85
+ switch" is exactly what the code then says. If you genuinely want to keep using
86
+ the loading stage as your scene, `state.change(state.DEFAULT)` at least gets
87
+ you out of it — but a stage of your own is what you want.
88
+
52
89
  ## Persistent objects across levels
53
90
 
54
91
  ```js
@@ -100,6 +100,15 @@ melonJS pre-registers `Renderable`, `Sprite`, `NineSliceSprite`, `Text`,
100
100
  `BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable` and `Trigger`
101
101
  as Tiled classes, so `Collectable` and `Trigger` work out of the box.
102
102
 
103
+ A `Trigger` forwards a **fixed list** of settings to `level.load()` — not
104
+ whatever you pass it. Today that list is `container`, `onLoaded`, `flatten`,
105
+ `setViewportBounds` and the glTF scene options (`scale`, `rightHanded`,
106
+ `lights`, `lightIntensityScale`, `castGroundShadow`, `shadowGroundY`); see the
107
+ 3D assets skill. Anything outside it is **dropped silently**, which is worth
108
+ knowing before you spend an afternoon on a map that loads at the wrong scale.
109
+ `async` is excluded deliberately — the trigger needs to sequence its own
110
+ transition.
111
+
103
112
  ## Camera bounds
104
113
 
105
114
  `level.load` sets the viewport bounds to the map size for you —