melonjs 20.2.0 → 20.3.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 (140) hide show
  1. package/README.md +86 -8
  2. package/build/application/settings.d.ts +3 -2
  3. package/build/application/settings.d.ts.map +1 -1
  4. package/build/audio/audio.d.ts +2 -2
  5. package/build/audio/audio.d.ts.map +1 -1
  6. package/build/audio/backend/buffer.d.ts +26 -0
  7. package/build/audio/backend/buffer.d.ts.map +1 -0
  8. package/build/audio/backend/cache.d.ts +2 -0
  9. package/build/audio/backend/cache.d.ts.map +1 -0
  10. package/build/audio/backend/core.d.ts +24 -0
  11. package/build/audio/backend/core.d.ts.map +1 -0
  12. package/build/audio/backend/engine.d.ts +54 -0
  13. package/build/audio/backend/engine.d.ts.map +1 -0
  14. package/build/audio/backend/sound.d.ts +79 -0
  15. package/build/audio/backend/sound.d.ts.map +1 -0
  16. package/build/audio/backend/spatial.d.ts +191 -0
  17. package/build/audio/backend/spatial.d.ts.map +1 -0
  18. package/build/audio/backend/types.d.ts +97 -0
  19. package/build/audio/backend/types.d.ts.map +1 -0
  20. package/build/audio/backend/voice.d.ts +33 -0
  21. package/build/audio/backend/voice.d.ts.map +1 -0
  22. package/build/audio/playback.d.ts +49 -4
  23. package/build/audio/playback.d.ts.map +1 -1
  24. package/build/audio/{backend.d.ts → state.d.ts} +13 -11
  25. package/build/audio/state.d.ts.map +1 -0
  26. package/build/audio/types.d.ts +93 -1
  27. package/build/audio/types.d.ts.map +1 -1
  28. package/build/camera/camera3d.d.ts +5 -5
  29. package/build/const.d.ts +2 -2
  30. package/build/index.d.ts.map +1 -1
  31. package/build/index.js +8793 -10171
  32. package/build/index.js.map +4 -4
  33. package/build/input/input.d.ts +19 -0
  34. package/build/input/input.d.ts.map +1 -1
  35. package/build/lang/deprecated.d.ts +4 -2
  36. package/build/lang/deprecated.d.ts.map +1 -1
  37. package/build/level/gltf/GLTFScene.d.ts +12 -0
  38. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  39. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  40. package/build/loader/loader.d.ts +4 -40
  41. package/build/loader/loader.d.ts.map +1 -1
  42. package/build/loader/parsers/audio.d.ts +31 -0
  43. package/build/loader/parsers/audio.d.ts.map +1 -0
  44. package/build/loader/parsers/shader.d.ts.map +1 -1
  45. package/build/physics/adapter.d.ts +3 -3
  46. package/build/physics/builtin/body.d.ts +23 -24
  47. package/build/physics/builtin/body.d.ts.map +1 -1
  48. package/build/physics/builtin/detector.d.ts +1 -1
  49. package/build/plugin/plugin.d.ts +9 -1
  50. package/build/plugin/plugin.d.ts.map +1 -1
  51. package/build/polyfill/index.d.ts +1 -4
  52. package/build/polyfill/index.d.ts.map +1 -1
  53. package/build/renderable/mesh.d.ts +16 -0
  54. package/build/renderable/mesh.d.ts.map +1 -1
  55. package/build/renderable/renderable.d.ts +16 -10
  56. package/build/renderable/renderable.d.ts.map +1 -1
  57. package/build/renderable/sprite.d.ts +7 -7
  58. package/build/renderable/sprite.d.ts.map +1 -1
  59. package/build/state/stage.d.ts +4 -2
  60. package/build/state/stage.d.ts.map +1 -1
  61. package/build/system/event.d.ts +1 -1
  62. package/build/system/platform.d.ts +21 -1
  63. package/build/system/platform.d.ts.map +1 -1
  64. package/build/utils/fetchdata.d.ts.map +1 -0
  65. package/build/video/blendmodes.d.ts +1 -1
  66. package/build/video/canvas/canvas_renderer.d.ts +5 -2
  67. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  68. package/build/video/effects/blur.d.ts +4 -4
  69. package/build/video/effects/blur.d.ts.map +1 -1
  70. package/build/video/effects/chromaticAberration.d.ts +5 -5
  71. package/build/video/effects/chromaticAberration.d.ts.map +1 -1
  72. package/build/video/effects/colorMatrix.d.ts +5 -5
  73. package/build/video/effects/colorMatrix.d.ts.map +1 -1
  74. package/build/video/effects/desaturate.d.ts +5 -5
  75. package/build/video/effects/desaturate.d.ts.map +1 -1
  76. package/build/video/effects/dissolve.d.ts +4 -4
  77. package/build/video/effects/dissolve.d.ts.map +1 -1
  78. package/build/video/effects/dropShadow.d.ts +5 -5
  79. package/build/video/effects/dropShadow.d.ts.map +1 -1
  80. package/build/video/effects/flash.d.ts +4 -4
  81. package/build/video/effects/flash.d.ts.map +1 -1
  82. package/build/video/effects/glow.d.ts +5 -5
  83. package/build/video/effects/glow.d.ts.map +1 -1
  84. package/build/video/effects/hologram.d.ts +4 -4
  85. package/build/video/effects/hologram.d.ts.map +1 -1
  86. package/build/video/effects/invert.d.ts +5 -5
  87. package/build/video/effects/invert.d.ts.map +1 -1
  88. package/build/video/effects/outline.d.ts +6 -6
  89. package/build/video/effects/outline.d.ts.map +1 -1
  90. package/build/video/effects/pixelate.d.ts +4 -4
  91. package/build/video/effects/pixelate.d.ts.map +1 -1
  92. package/build/video/effects/radialGradient.d.ts +2 -2
  93. package/build/video/effects/scanline.d.ts +6 -6
  94. package/build/video/effects/scanline.d.ts.map +1 -1
  95. package/build/video/effects/sepia.d.ts +5 -5
  96. package/build/video/effects/sepia.d.ts.map +1 -1
  97. package/build/video/effects/shadereffect.d.ts +12 -10
  98. package/build/video/effects/shadereffect.d.ts.map +1 -1
  99. package/build/video/effects/shine.d.ts +3 -3
  100. package/build/video/effects/shine.d.ts.map +1 -1
  101. package/build/video/effects/tintPulse.d.ts +4 -4
  102. package/build/video/effects/tintPulse.d.ts.map +1 -1
  103. package/build/video/effects/vignette.d.ts +6 -6
  104. package/build/video/effects/vignette.d.ts.map +1 -1
  105. package/build/video/effects/wave.d.ts +4 -4
  106. package/build/video/effects/wave.d.ts.map +1 -1
  107. package/build/video/texture/atlas.d.ts +1 -1
  108. package/build/video/webgl/glshader.d.ts +2 -2
  109. package/build/video/webgl/webgl_renderer.d.ts +2 -1
  110. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  111. package/build/video/webgpu/webgpu_renderer.d.ts +2 -1
  112. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  113. package/package.json +5 -9
  114. package/skills/AGENTS.md +67 -0
  115. package/skills/melonjs/SKILL.md +156 -0
  116. package/skills/melonjs-20-migration/SKILL.md +160 -0
  117. package/skills/melonjs-3d/SKILL.md +226 -0
  118. package/skills/melonjs-3d-assets/SKILL.md +239 -0
  119. package/skills/melonjs-audio/SKILL.md +215 -0
  120. package/skills/melonjs-camera-and-drawing/SKILL.md +171 -0
  121. package/skills/melonjs-deployment/SKILL.md +113 -0
  122. package/skills/melonjs-effects-and-shaders/SKILL.md +177 -0
  123. package/skills/melonjs-events/SKILL.md +159 -0
  124. package/skills/melonjs-getting-started/SKILL.md +183 -0
  125. package/skills/melonjs-input/SKILL.md +166 -0
  126. package/skills/melonjs-lighting/SKILL.md +139 -0
  127. package/skills/melonjs-loading-assets/SKILL.md +157 -0
  128. package/skills/melonjs-particles-and-trails/SKILL.md +136 -0
  129. package/skills/melonjs-performance/SKILL.md +178 -0
  130. package/skills/melonjs-physics/SKILL.md +321 -0
  131. package/skills/melonjs-plugins/SKILL.md +191 -0
  132. package/skills/melonjs-renderables/SKILL.md +214 -0
  133. package/skills/melonjs-renderer-backends/SKILL.md +173 -0
  134. package/skills/melonjs-scenes-and-state/SKILL.md +180 -0
  135. package/skills/melonjs-sprites-and-animation/SKILL.md +173 -0
  136. package/skills/melonjs-tilemaps/SKILL.md +193 -0
  137. package/skills/melonjs-ui-and-text/SKILL.md +175 -0
  138. package/build/audio/backend.d.ts.map +0 -1
  139. package/build/loader/parsers/fetchdata.d.ts.map +0 -1
  140. /package/build/{loader/parsers → utils}/fetchdata.d.ts +0 -0
@@ -0,0 +1,321 @@
1
+ ---
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."
4
+ license: MIT
5
+ ---
6
+
7
+ # Physics and collision
8
+
9
+ > melonJS has a built-in SAT collision world and two optional rigid-body
10
+ > adapters. Movement, collision callbacks and spatial queries all behave
11
+ > differently depending on which is active — and several of the differences fail
12
+ > silently.
13
+
14
+ ## Choosing a world
15
+
16
+ | | when |
17
+ |---|---|
18
+ | **built-in** (default) | Arcade-style: platformers, top-down games. Fast, position-based, not Newtonian. |
19
+ | **`@melonjs/planck-adapter`** | Full rigid-body dynamics — stacking, joints, realistic restitution. |
20
+ | **`@melonjs/matter-adapter`** | Same class of thing, backed by matter-js. |
21
+
22
+ An adapter is an `Application` setting, not a plugin:
23
+
24
+ ```js
25
+ import { PlanckAdapter } from "@melonjs/planck-adapter";
26
+
27
+ const app = new Application(800, 600, {
28
+ parent: "screen",
29
+ physic: new PlanckAdapter({
30
+ gravity: { x: 0, y: 320 }, // pixels/s², default {x: 0, y: 320}
31
+ pixelsPerMeter: 32, // default 32
32
+ subSteps: 2, // default 1
33
+ }),
34
+ });
35
+ await app.init();
36
+ ```
37
+
38
+ `PlanckAdapter` also takes `velocityIterations` (default `8`) and
39
+ `positionIterations` (default `3`). `MatterAdapter` takes a different set —
40
+ `gravity` (default `{x: 0, y: 1}`, matter's own convention), `subSteps` and
41
+ `matterEngineOptions`; it has no `pixelsPerMeter` because it already works in
42
+ pixels. `subSteps` is the anti-tunnelling knob when per-frame motion exceeds a
43
+ body radius. Pick **one** adapter per game; `physic: "none"` disables physics
44
+ entirely — `world.step` skips the simulation and the world behaves as a pure
45
+ scene graph.
46
+
47
+ `world.physic` carries the active adapter's label (`"builtin"`, `"planck"`,
48
+ `"matter"`, `"none"`), so game code can branch without importing the class.
49
+
50
+ ## Two ways to declare a body
51
+
52
+ **`bodyDef`** — declarative, portable, works on every adapter. Prefer it.
53
+
54
+ ```js
55
+ class Player extends Sprite {
56
+ constructor(x, y, settings) {
57
+ super(x, y, settings);
58
+ this.bodyDef = {
59
+ type: "dynamic",
60
+ shapes: [ /* … */ ],
61
+ collisionType: collision.types.PLAYER_OBJECT,
62
+ collisionMask: collision.types.WORLD_SHAPE | collision.types.ENEMY_OBJECT,
63
+ restitution: 0.2,
64
+ };
65
+ }
66
+ }
67
+ ```
68
+
69
+ The engine forwards it to the active adapter when the renderable is added with
70
+ `Container.addChild`, and only if that container is already attached to the root
71
+ world; the broadphase picks the renderable up on the next `world.update()`.
72
+ Calling `adapter.addBody()` yourself registers the body but leaves the
73
+ renderable out of the scene graph, so it integrates but never collides.
74
+
75
+ Other portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),
76
+ `friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,
77
+ `isSensor`, `userData`. Not every adapter honours every one — the built-in
78
+ adapter ignores `friction`, `fixedRotation` and `userData`, and maps
79
+ `frictionAir` onto its per-axis `body.friction` damping vector.
80
+
81
+ **`new Body(this, shape)`** — the built-in-native API. Fine, but ties the entity
82
+ to the built-in world.
83
+
84
+ ### Per-shape filtering and triggers
85
+
86
+ A shape in `shapes` may carry its own `collisionType`, `collisionMask`,
87
+ `isTrigger` (default `false`) and `isActive` (default `true`). A shape can only
88
+ **narrow** what its body allows — the body's type/mask are checked first.
89
+
90
+ ```js
91
+ const feet = new Rect(0, 24, 32, 8);
92
+ feet.collisionMask = collision.types.WORLD_SHAPE;
93
+ const torso = new Rect(0, 0, 32, 24);
94
+ torso.collisionMask = collision.types.ENEMY_OBJECT;
95
+ torso.isTrigger = true; // detects hits, never gets pushed by them
96
+ ```
97
+
98
+ `isActive: false` removes the shape from the narrow phase *and* from raycasts
99
+ without shrinking the body's bounds. Honoured by the built-in and planck
100
+ adapters; the matter adapter filters per body and ignores them. Under planck the
101
+ body-wide setters (`setCollisionType` / `setCollisionMask` / `setSensor`) write
102
+ every fixture and so overwrite per-shape values.
103
+
104
+ For contacts reported per *shape pair* rather than per body pair, define
105
+ `onShapeCollisionStart` / `onShapeCollisionActive` / `onShapeCollisionEnd`.
106
+ Declaring at least one of them opts into shape-pair enumeration; declaring none
107
+ costs nothing.
108
+
109
+ ## Move with forces, not by assigning position
110
+
111
+ ```js
112
+ // ✗ the adapters overwrite this from the engine body every step
113
+ this.pos.x += this.speed;
114
+
115
+ // ✓ portable — works on every adapter
116
+ this.body.applyForce(thrustX, 0);
117
+ this.body.setVelocity(vx, vy);
118
+
119
+ // ✓ built-in only — `force` and `maxVel` are BuiltinAdapter Body fields
120
+ this.body.force.x = this.body.maxVel.x;
121
+ ```
122
+
123
+ Under matter and planck, `syncFromPhysics()` copies the engine body's position
124
+ back onto `renderable.pos` after every step, so a direct write is simply erased
125
+ — no error. The built-in world integrates `pos` in place instead, so a write
126
+ survives but fights collision resolution and jitters. To teleport, go through
127
+ `adapter.setPosition(renderable, p)` so the engine body moves with it.
128
+
129
+ ## Collision callbacks — which one to use
130
+
131
+ The lifecycle is `onCollisionStart` (contact begins), `onCollisionActive` (every
132
+ step while it persists) and `onCollisionEnd` (contact breaks). None of them are
133
+ defined on `Renderable` — you just declare the ones you want and the dispatcher
134
+ checks `typeof`. The legacy `onCollision` still exists and has a trap:
135
+
136
+ > On the built-in world, `onCollision` fires **twice per frame** for
137
+ > dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT
138
+ > response with a fixed `a`/`b` per pair. On the adapters it is routed to their
139
+ > "still touching" phase and fires once per side.
140
+
141
+ The legacy dedupe idiom is `if (this !== response.a) return;`. Defining
142
+ `onCollisionActive` suppresses the legacy callback on that renderable (the
143
+ "supersedes" rule, applied per side so `a` and `b` can migrate independently),
144
+ and is deduped to once per pair per side per frame on every adapter.
145
+
146
+ **Return values only work on the legacy handler.** `return false` from
147
+ `onCollision` skips the built-in SAT push-out; what `onCollisionStart` /
148
+ `onCollisionActive` return is discarded. To opt a body out of the physical
149
+ response, set `bodyDef.isSensor` / `body.setSensor(true)`, or make the shape a
150
+ trigger. With no `onCollision` defined at all, push-out happens by default for
151
+ dynamic non-sensor bodies.
152
+
153
+ The two handler families do **not** get the same response object:
154
+
155
+ | | `onCollision` (legacy) | `onCollisionStart` / `Active` / `End` |
156
+ |---|---|---|
157
+ | `a` / `b` | fixed per pair | receiver-symmetric: `a === this`, `b === other` |
158
+ | overlap magnitude | `response.overlap` | `response.depth` |
159
+ | contact axis | `response.overlapN` / `overlapV` | `response.normal` |
160
+ | also carries | `aInB`, `bInA`, `indexShapeA/B`, `isTriggerContact` | `pair` (adapter-native, absent on built-in) |
161
+
162
+ On the modern response `overlap` / `overlapN` / `overlapV` are still present but
163
+ `@deprecated` (and `undefined` under the matter adapter) — use `depth` and
164
+ `normal`. `normal` is the direction the receiver must move to separate, in
165
+ canvas coordinates: `normal.y < -0.7` means "push me up", i.e. I landed on top
166
+ of `other`.
167
+
168
+ One portability wrinkle: the built-in adapter dispatches `onCollisionEnd` with
169
+ `undefined` as the response; the matter and planck adapters pass a real one.
170
+ Guard it.
171
+
172
+ ## Never mutate the world during contact dispatch
173
+
174
+ Collision callbacks are dispatched from inside the physics step on every
175
+ adapter, and each engine has its own tolerance for being mutated there. Flag the
176
+ work and drain it from your `Stage.update`:
177
+
178
+ ```js
179
+ onCollisionStart(response, other) {
180
+ this.pendingRemoval = true; // don't addChild / removeChildNow here
181
+ }
182
+ ```
183
+
184
+ What actually happens per adapter:
185
+
186
+ - **built-in** — callbacks fire inline during `step()`. `Container.removeChild`
187
+ is deferred, so it is safe; `removeChildNow()` and `destroy()` are immediate,
188
+ and the detector only survives them because it re-checks `body === undefined`
189
+ after every user callback.
190
+ - **planck** — `onCollisionStart` / `onCollisionEnd` are dispatched from
191
+ planck's `begin-contact` / `end-contact` events, which fire *while the world
192
+ is locked*. Adding a renderable with a `bodyDef` there makes planck's
193
+ `createBody` return `null` and the adapter then throws on the null handle.
194
+ `onCollisionActive` is dispatched after the step and is the safe one.
195
+ - **matter** — callbacks come from matter's `collisionStart` / `collisionActive`
196
+ / `collisionEnd` events during `Engine.update`.
197
+
198
+ ## Spatial queries
199
+
200
+ `raycast` and `queryAABB` are implemented by all three adapters:
201
+
202
+ ```js
203
+ import { Rect, Vector2d } from "melonjs";
204
+
205
+ // nearest hit only — returns null when nothing is hit
206
+ const hit = app.world.adapter.raycast(new Vector2d(x0, y0), new Vector2d(x1, y1));
207
+ // hit → { renderable, point, normal, fraction }
208
+
209
+ // takes a Rect, not a Bounds
210
+ const inArea = app.world.adapter.queryAABB(new Rect(x, y, w, h));
211
+ ```
212
+
213
+ `querySphere(centre, radius)` (or `querySphere(sphere)`) is **built-in only**,
214
+ and only active under a 3D broadphase — i.e. `world.sortOn === "depth"`, which
215
+ `Camera3d` sets. So is `raycast3d`. Check
216
+ `world.adapter.capabilities.raycasts3d` before calling either. `raycast` itself
217
+ is capability-gated by `capabilities.raycasts`.
218
+
219
+ The legacy `collision.rayCast(line, result)` is a different API: it takes a
220
+ `Line`, returns an **array** of every intersecting renderable, and only exists
221
+ on the built-in path.
222
+
223
+ Two prerequisites that produce empty results rather than errors:
224
+
225
+ - **`isKinematic` must be `false`** for a renderable to be in the broadphase at
226
+ all (a `Body` sets this for you).
227
+ - On the **built-in** adapter, the broadphase is cleared and rebuilt inside
228
+ `world.update()`, so querying before the first update returns nothing.
229
+
230
+ ## Built-in world quirks
231
+
232
+ These are specific to the default world and surprise people arriving from a
233
+ real rigid-body engine:
234
+
235
+ - **Dynamic-dynamic collision is position-based, not Newtonian.** Separation is
236
+ mass-proportional but the velocity response is per-body cancellation — two
237
+ equal-mass bodies do not exchange momentum the way you would expect.
238
+ - **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate
239
+ `app.world.adapter.gravity` at runtime, or pass
240
+ `physic: new BuiltinAdapter({ gravity })` to override it. `body.gravityScale`
241
+ (default `1`) scales it per body; `0` disables gravity for that body and is
242
+ the portable replacement for the deprecated `body.ignoreGravity`.
243
+ - **`applyForce(x, y)` is linear only** unless you pass the optional
244
+ `(pointX, pointY)` application point, which generates a torque
245
+ `τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.
246
+ `applyTorque(τ)` is the direct form. Note the rotation is visual: SAT
247
+ collisions stay axis-aligned.
248
+ - **Force accumulators reset at end-of-step** — for every body, including
249
+ static, paused and off-screen ones — so forces must be applied every frame to
250
+ persist.
251
+ - **`isGrounded` is flag-based, not contact-based** — the adapter returns
252
+ `!body.falling && !body.jumping`, flags updated by the last resolved
253
+ collision, not a live contact test.
254
+ - **`def.density` maps 1:1 to `body.mass`** (default `1`), and `def.friction` is
255
+ **ignored** entirely. `body.friction` is a per-axis `Vector2d` of per-step
256
+ velocity damping fed from `def.frictionAir`, not a surface coefficient.
257
+ - **`body.maxVel` defaults to `(490, 490)`** and hard-clamps velocity after
258
+ forces and friction. The canonical movement idiom is
259
+ `this.body.force.x = this.body.maxVel.x`.
260
+ - **Default `collisionType` is `ENEMY_OBJECT`**, and default `collisionMask` is
261
+ `ALL_OBJECT` — set both explicitly.
262
+ - **Integration is gated on `inViewport || alwaysUpdate`** — an off-screen body
263
+ stops simulating unless you set `alwaysUpdate = true`. It is also gated on
264
+ `updateWhenPaused` while the state manager is paused, and static bodies never
265
+ integrate.
266
+ - **`addBody` throws on double-registration** for a renderable that is already
267
+ adapter-managed.
268
+
269
+ ## Porting between adapters
270
+
271
+ Same call, different magnitude — these are the ones that waste an afternoon:
272
+
273
+ - **The portable API converts for you, the native handle does not.**
274
+ `adapter.setVelocity` / `body.setVelocity(x, y)` are in pixels on every
275
+ adapter; reaching for the raw handle (`body.setLinearVelocity` on planck) puts
276
+ you in metres per second, scaled by `pixelsPerMeter`.
277
+ - **Time base differs.** Built-in `body.vel` is pixels *per frame*; planck and
278
+ matter integrate in seconds. The same number is ~60× off.
279
+ - **Force magnitudes are not comparable** between adapters; re-tune rather than
280
+ reuse. `applyForce` is a per-step accumulator everywhere (cleared each step,
281
+ so call it every frame) — `applyImpulse` is the one-shot. Matter has no native
282
+ impulse, so the adapter emulates it as `Δv = J / mass` and ignores the
283
+ application point.
284
+ - **`body.position` is the centroid in matter**, while `renderable.pos` is
285
+ top-left in melonJS. The adapter stores a per-body offset so `renderable.pos`
286
+ stays correct — but read `body.position` directly and you get the centroid.
287
+ - **Degenerate (zero-area / collinear) polygons — a Tiled polyline, a `Line` —
288
+ cannot be built by matter and fall back to their axis-aligned bounding box.**
289
+ Any other shape type throws `unsupported shape type`.
290
+ - **Ellipses become circles on matter**, using the average of the two radii.
291
+ - **`isGrounded` is capability-gated** (`capabilities.isGrounded`) and computed
292
+ differently per adapter: a flag read on built-in, a live scan of active
293
+ contacts on matter.
294
+
295
+ Adapter-specific escapes exist (`(this.body as MatterAdapter.Body).frictionAir`,
296
+ `adapter.matter.Constraint.create(...)`) but are not portable — flag them if you
297
+ use them. `adapter.capabilities` (`constraints`,
298
+ `continuousCollisionDetection`, `sleepingBodies`, `raycasts`, `raycasts3d`,
299
+ `velocityLimit`, `isGrounded`) is the portable way to branch.
300
+
301
+ ## Symptom → cause
302
+
303
+ | symptom | cause |
304
+ |---|---|
305
+ | entity jitters or does not move | assigning `pos` instead of applying force |
306
+ | collision handler runs twice per frame | legacy `onCollision` on a dynamic pair (built-in) — use `onCollisionActive` |
307
+ | `return false` from a handler does nothing | return values are only honoured by legacy `onCollision` — use `isSensor` / `isTrigger` |
308
+ | crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |
309
+ | `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |
310
+ | `querySphere` / `raycast3d` is not a function | 2D adapter, or built-in without a 3D broadphase (`sortOn !== "depth"`) |
311
+ | `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |
312
+ | `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |
313
+ | off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |
314
+ | forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |
315
+ | `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, melonJS the top-left — the adapter offsets between them |
316
+ | a Tiled polyline becomes a solid box on matter | matter cannot build a zero-area polygon; the adapter falls back to its AABB |
317
+
318
+ ## Related skills
319
+
320
+ - `melonjs-renderables` — `isKinematic`, which also gates the broadphase
321
+ - `melonjs-tilemaps` — collision shapes authored in Tiled
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: melonjs-plugins
3
+ description: "Use this skill when adding a melonJS plugin or adapter — the debug panel, Spine skeletal animation, compressed Tiled map support, Capacitor packaging — or when writing your own plugin. Covers plugin.register and its version gate, plugin.cache, plugin.patch, and why physics adapters are not plugins. Triggers on: plugin, plugin.register, plugin.cache, plugin.get, plugin.patch, BasePlugin, debug-plugin, DebugPanelPlugin, SpinePlugin, TiledInflatePlugin, CapacitorPlugin, adapter, custom plugin, plugin version mismatch, already registered."
4
+ license: MIT
5
+ ---
6
+
7
+ # Plugins and adapters
8
+
9
+ ## Two different mechanisms
10
+
11
+ This trips people up, so it is worth stating first:
12
+
13
+ | | how it is installed |
14
+ |---|---|
15
+ | **Plugins** | `plugin.register(Class, name?, ...args)` — after `await app.init()` |
16
+ | **Physics adapters** | an `Application` **setting** (`physic:`), before `init()` |
17
+
18
+ ```js
19
+ // the adapter is chosen at construction …
20
+ const app = new Application(800, 600, {
21
+ parent: "screen",
22
+ physic: new PlanckAdapter({ gravity, pixelsPerMeter }),
23
+ });
24
+
25
+ // … the plugin is registered once the renderer exists
26
+ await app.init();
27
+ plugin.register(DebugPanelPlugin, "debugPanel");
28
+ ```
29
+
30
+ ## `register` in detail
31
+
32
+ ```js
33
+ plugin.register(PluginClass, "name", ...constructorArgs);
34
+ plugin.cache.name; // the instance
35
+ plugin.get(PluginClass); // by class — or by registered name string
36
+ ```
37
+
38
+ - **The name is optional** — it falls back to the class name.
39
+ - **The registry is global, not per-application.** It is a module-level object,
40
+ so registering the same name twice throws `plugin <name> already registered`.
41
+ A plugin *instance* does hold an `app` reference, but it defaults to `game` —
42
+ **whichever Application most recently finished `init()`**, since that is where
43
+ `game` is assigned. On a multi-application page, pass the app explicitly
44
+ through the constructor args:
45
+ `plugin.register(DebugPanelPlugin, "debugPanel", input.KEY.S, myApp)`.
46
+ - **`BasePlugin.version` is a floor, not an identity.** It declares the
47
+ *minimum melonJS version the plugin needs*, and `register` throws
48
+ `Plugin version mismatch, expected: <floor>, got: <engine>` when the running
49
+ engine is older. That gate is the real enforcement — a peer-dependency range
50
+ only warns at install time, and not at all for CDN or pre-bundled use. If you
51
+ see that error, upgrade the engine or pin an older plugin. The check runs
52
+ *after* the instance is constructed, so a plugin whose constructor has side
53
+ effects will already have run them when it throws.
54
+
55
+ Register after `await app.init()`: a plugin binds to an Application and reads
56
+ renderer and frame state that does not exist before init.
57
+
58
+ ## The official plugins
59
+
60
+ ### `@melonjs/debug-plugin`
61
+
62
+ An in-game panel — frame time, draw and object counts — plus toggles for
63
+ hitbox/bounding-box overlays, velocity vectors and quadtree visualisation. The
64
+ first thing to reach for when something is slow or a collision is not landing
65
+ where you expect.
66
+
67
+ ```js
68
+ import { DebugPanelPlugin } from "@melonjs/debug-plugin";
69
+ plugin.register(DebugPanelPlugin, "debugPanel"); // default toggle key: S
70
+ ```
71
+
72
+ ### `@melonjs/tiled-inflate-plugin`
73
+
74
+ **Required**, not optional, for Tiled maps using gzip, zlib or zstd layer
75
+ compression. It installs the inflate hook `TMXUtils` calls; without it a
76
+ compressed `.tmx` fails to parse — a map that opens fine in Tiled and refuses to
77
+ load in melonJS is nearly always this.
78
+
79
+ ```js
80
+ import { TiledInflatePlugin } from "@melonjs/tiled-inflate-plugin";
81
+ plugin.register(TiledInflatePlugin);
82
+ ```
83
+
84
+ ### `@melonjs/spine-plugin`
85
+
86
+ Spine 4.x skeletal animation as a melonJS renderable, on all three backends.
87
+ This is the answer for deforming animated characters — glTF import does not read
88
+ vertex skinning. It also registers a Tiled object class, so Spine skeletons can
89
+ be placed from a map.
90
+
91
+ ### `@melonjs/capacitor-plugin`
92
+
93
+ Bridges Capacitor's native lifecycle and hardware-back events into the engine:
94
+
95
+ ```js
96
+ import { CapacitorPlugin } from "@melonjs/capacitor-plugin";
97
+ plugin.register(CapacitorPlugin, "capacitor", options);
98
+ // plugin.cache.capacitor.teardown() — detach every listener
99
+ ```
100
+
101
+ It also exports helpers usable on their own: `onBackButton`, `bindStageBack`,
102
+ `lockOrientation` / `unlockOrientation`, `hideSplash`. See `melonjs-deployment`
103
+ for the wider packaging picture.
104
+
105
+ ## Physics adapters
106
+
107
+ `@melonjs/planck-adapter` and `@melonjs/matter-adapter` swap the physics world
108
+ wholesale. Pick **one** per game. The portable surface — the declarative
109
+ `renderable.bodyDef`, `world.adapter.queryAABB(rect)` and
110
+ `world.adapter.raycast(from, to)` — works unchanged across the built-in world and
111
+ both adapters, so entity code does not have to branch.
112
+
113
+ Two queries are **not** portable: `querySphere` and `raycast3d` exist only on the
114
+ built-in adapter (planck and matter are 2D-only and deliberately omit them).
115
+ `queryAABB` is the one region query every adapter must implement, so call the
116
+ others optionally — `app.world.adapter.querySphere?.(center, radius)`. For rays
117
+ there is a real flag: `app.world.adapter.capabilities.raycasts3d`.
118
+
119
+ Behaviour genuinely differs: velocity units, force magnitudes and where a body's
120
+ position is measured from all change. See `melonjs-physics`.
121
+
122
+ ## Writing your own
123
+
124
+ There is **no `init()` lifecycle hook** — the constructor is where setup goes,
125
+ and `register` instantiates it for you.
126
+
127
+ ```js
128
+ import { plugin, event } from "melonjs";
129
+
130
+ class MyPlugin extends plugin.BasePlugin {
131
+ constructor(app = undefined) {
132
+ super(app); // stores it as this.app
133
+ this.version = "20.0.0"; // MINIMUM engine version this needs
134
+ event.on(event.GAME_UPDATE, () => { /* … */ });
135
+ }
136
+ }
137
+
138
+ plugin.register(MyPlugin, "myPlugin");
139
+ ```
140
+
141
+ `this.version` defaults to the running engine version, which means it always
142
+ passes — set it deliberately to the oldest version you actually support.
143
+
144
+ To wrap an existing engine method, `plugin.patch` keeps the original reachable
145
+ as `this._patched()`:
146
+
147
+ ```js
148
+ plugin.patch(app, "update", function (time) {
149
+ const result = this._patched(time);
150
+ return result;
151
+ });
152
+ ```
153
+
154
+ `patch` takes a class *or* an instance: given a constructor it patches the
155
+ prototype (that is how the debug plugin decorates `Renderable.postDraw`), given a
156
+ plain object it patches that object. `this._patched` is only bound for the
157
+ synchronous duration of your replacement — it is set before the call and nulled
158
+ straight after — so call it inline, never after an `await`.
159
+
160
+ ```js
161
+ plugin.patch(Renderable, "postDraw", function (renderer) {
162
+ this._patched(renderer);
163
+ // draw an overlay on top of every renderable
164
+ });
165
+ ```
166
+
167
+ A plugin is the right shape when you need to augment engine classes or hook the
168
+ engine lifecycle globally. For anything that is just game code, a `Renderable`
169
+ or a `Stage` is simpler and easier to test.
170
+
171
+ ## Symptom → cause
172
+
173
+ | symptom | cause |
174
+ |---|---|
175
+ | `plugin <name> already registered` | the registry is global; registered twice, often across hot reloads |
176
+ | `Plugin version mismatch` | the plugin's `version` floor is above the running engine |
177
+ | `Plugin should extend the BasePlugin Class !` | the class does not extend `plugin.BasePlugin` |
178
+ | the panel reports another app's numbers | `app` defaulted to `game`, the last Application to finish `init()` |
179
+ | `world.adapter.querySphere is not a function` | built-in adapter only; planck and matter are 2D-only |
180
+ | a Tiled map opens in Tiled but not melonJS | compressed layers, tiled-inflate plugin missing |
181
+ | plugin setup never runs | setup written in an `init()` method — there is no such hook |
182
+ | `plugin.register` for an adapter does nothing | adapters are a construction setting, not a plugin |
183
+ | physics behaves differently after adding an adapter | expected — units and force scales differ; re-tune |
184
+
185
+ ## Related skills
186
+
187
+ - `melonjs-physics` — the adapters and their behavioural differences
188
+ - `melonjs-tilemaps` — where the inflate plugin becomes mandatory
189
+ - `melonjs-performance` — using the debug panel to measure
190
+ - `melonjs-deployment` — Capacitor and native packaging
191
+ - `melonjs-3d-assets` — why skeletal characters go through Spine