melonjs 20.13.0 → 20.14.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 (120) hide show
  1. package/README.md +65 -22
  2. package/build/application/application.d.ts +6 -1
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/header.d.ts.map +1 -1
  5. package/build/application/resize.d.ts.map +1 -1
  6. package/build/application/settings.d.ts +13 -8
  7. package/build/application/settings.d.ts.map +1 -1
  8. package/build/audio/audio.d.ts +8 -4
  9. package/build/audio/audio.d.ts.map +1 -1
  10. package/build/audio/backend/engine.d.ts.map +1 -1
  11. package/build/audio/playback.d.ts +2 -2
  12. package/build/audio/playback.d.ts.map +1 -1
  13. package/build/audio/spatial.d.ts.map +1 -1
  14. package/build/audio/types.d.ts +15 -5
  15. package/build/audio/types.d.ts.map +1 -1
  16. package/build/camera/camera2d.d.ts +10 -9
  17. package/build/camera/camera2d.d.ts.map +1 -1
  18. package/build/camera/camera3d.d.ts +11 -8
  19. package/build/camera/camera3d.d.ts.map +1 -1
  20. package/build/geometries/path2d.d.ts +9 -8
  21. package/build/geometries/path2d.d.ts.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +40993 -39738
  25. package/build/index.js.map +4 -4
  26. package/build/input/keyboard.d.ts.map +1 -1
  27. package/build/input/pointerevent.d.ts.map +1 -1
  28. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  29. package/build/level/level.d.ts.map +1 -1
  30. package/build/level/tiled/TMXLayer.d.ts +2 -1
  31. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  32. package/build/level/tiled/TMXTileMap.d.ts +13 -3
  33. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  34. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  35. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  36. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  37. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -0
  38. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  39. package/build/level/tiled/renderer/TMXRenderer.d.ts +7 -3
  40. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  41. package/build/lighting/light3d.d.ts +55 -0
  42. package/build/lighting/light3d.d.ts.map +1 -1
  43. package/build/loader/cache.d.ts +146 -97
  44. package/build/loader/cache.d.ts.map +1 -1
  45. package/build/loader/internal-types.d.ts +69 -0
  46. package/build/loader/internal-types.d.ts.map +1 -0
  47. package/build/loader/loader.d.ts +584 -292
  48. package/build/loader/loader.d.ts.map +1 -1
  49. package/build/loader/parsers/audio.d.ts.map +1 -1
  50. package/build/loader/parsers/fontface.d.ts.map +1 -1
  51. package/build/loader/parsers/gltf.d.ts.map +1 -1
  52. package/build/loader/parsers/tmx.d.ts.map +1 -1
  53. package/build/math/color.d.ts +4 -0
  54. package/build/math/color.d.ts.map +1 -1
  55. package/build/math/noise.d.ts +16 -16
  56. package/build/math/noise.d.ts.map +1 -1
  57. package/build/physics/adapter.d.ts +64 -25
  58. package/build/physics/adapter.d.ts.map +1 -1
  59. package/build/physics/builtin/body.d.ts +9 -0
  60. package/build/physics/builtin/body.d.ts.map +1 -1
  61. package/build/physics/builtin/builtin-adapter.d.ts +25 -3
  62. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  63. package/build/physics/builtin/detector.d.ts.map +1 -1
  64. package/build/physics/builtin/raycast.d.ts.map +1 -1
  65. package/build/physics/world.d.ts +40 -10
  66. package/build/physics/world.d.ts.map +1 -1
  67. package/build/renderable/container.d.ts +1 -0
  68. package/build/renderable/container.d.ts.map +1 -1
  69. package/build/renderable/draggable.d.ts.map +1 -1
  70. package/build/renderable/frameAnimation.d.ts.map +1 -1
  71. package/build/renderable/instanced_mesh.d.ts +16 -14
  72. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  73. package/build/renderable/mesh.d.ts +62 -5
  74. package/build/renderable/mesh.d.ts.map +1 -1
  75. package/build/renderable/trigger.d.ts +5 -0
  76. package/build/renderable/trigger.d.ts.map +1 -1
  77. package/build/renderable/ui/uibaseelement.d.ts +8 -1
  78. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  79. package/build/renderable/ui/uicheckbox.d.ts.map +1 -1
  80. package/build/system/device.d.ts +11 -2
  81. package/build/system/device.d.ts.map +1 -1
  82. package/build/system/fullscreen.d.ts.map +1 -1
  83. package/build/system/pool.d.ts.map +1 -1
  84. package/build/utils/utils.d.ts.map +1 -1
  85. package/build/video/canvas/canvas_renderer.d.ts +16 -14
  86. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  87. package/build/video/canvas_factory.d.ts.map +1 -1
  88. package/build/video/effects/shadereffect.d.ts +127 -0
  89. package/build/video/effects/shadereffect.d.ts.map +1 -1
  90. package/build/video/renderer.d.ts.map +1 -1
  91. package/build/video/webgl/batchers/lit_mesh_batcher.d.ts.map +1 -1
  92. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  93. package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
  94. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/skills/melonjs/SKILL.md +38 -14
  97. package/skills/melonjs-20-migration/SKILL.md +50 -7
  98. package/skills/melonjs-3d/SKILL.md +138 -38
  99. package/skills/melonjs-3d-assets/SKILL.md +83 -33
  100. package/skills/melonjs-audio/SKILL.md +55 -14
  101. package/skills/melonjs-camera-and-drawing/SKILL.md +95 -8
  102. package/skills/melonjs-deployment/SKILL.md +92 -14
  103. package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
  104. package/skills/melonjs-effects-and-shaders/SKILL.md +46 -15
  105. package/skills/melonjs-events/SKILL.md +3 -2
  106. package/skills/melonjs-getting-started/SKILL.md +5 -0
  107. package/skills/melonjs-input/SKILL.md +65 -15
  108. package/skills/melonjs-lighting/SKILL.md +13 -1
  109. package/skills/melonjs-loading-assets/SKILL.md +202 -27
  110. package/skills/melonjs-math/SKILL.md +12 -5
  111. package/skills/melonjs-particles-and-trails/SKILL.md +7 -3
  112. package/skills/melonjs-performance/SKILL.md +18 -8
  113. package/skills/melonjs-physics/SKILL.md +385 -127
  114. package/skills/melonjs-plugins/SKILL.md +45 -11
  115. package/skills/melonjs-renderables/SKILL.md +24 -7
  116. package/skills/melonjs-renderer-backends/SKILL.md +38 -17
  117. package/skills/melonjs-scenes-and-state/SKILL.md +30 -5
  118. package/skills/melonjs-sprites-and-animation/SKILL.md +23 -2
  119. package/skills/melonjs-tilemaps/SKILL.md +56 -18
  120. package/skills/melonjs-ui-and-text/SKILL.md +120 -25
@@ -144,6 +144,11 @@ await loader.preload([
144
144
  ]);
145
145
  ```
146
146
 
147
+ A flat list suits a single-screen game. A game with several levels exports a
148
+ record of bundles instead — `{ common: [...], level1: [...], level2: [...] }`,
149
+ passed to the same `loader.preload()` — so each level's assets can later be
150
+ freed and loaded again by id; see `melonjs-loading-assets`.
151
+
147
152
  Two ordering rules that produce confusing failures when missed:
148
153
 
149
154
  - **`audio.init()` must run before any audio asset is preloaded**, and names the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-input
3
- description: "Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, virtual gamepad, touch controls, joystick, gamepad-plugin, floating stick, floating button, floating joystick, two thumbs, tap to fire, hideAtRest, mobile controls, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click."
3
+ description: "Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, bindPointer, mouse button, vibrateGamepad, rumble, activeGamepad, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, virtual gamepad, touch controls, joystick, gamepad-plugin, floating stick, floating button, floating joystick, two thumbs, tap to fire, hideAtRest, mobile controls, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -54,12 +54,37 @@ narrows it; it also repeats while the key is held, so guard anything that must
54
54
  happen once. Keep polling for movement and for a menu cursor, where one step
55
55
  per press and a key held across a state change are the things that matter.
56
56
 
57
+ When the window loses the focus or the page is hidden, every key and
58
+ `bindPointer` button still held is released, since its real release goes to
59
+ another window: polling sees it up when the player comes back, and a key still
60
+ held then is pressed again by its next `keydown`. No `KEYUP` is sent for it,
61
+ so code moved by `KEYDOWN` / `KEYUP` rather than by polling should also listen
62
+ to `event.BLUR`. A gamepad's presses are left to the gamepad, which is read
63
+ every frame.
64
+
57
65
  The full signature is `bindKey(keyCode, action, lock?, preventDefault?)`;
58
66
  `preventDefault` defaults to `input.preventDefault` (itself `true`). Pass the
59
67
  fourth argument per binding, or change the global default with
60
68
  `input.setPreventDefault(false)` — **assigning `input.preventDefault = false`
61
69
  does not work**, it is a read-only module binding.
62
70
 
71
+ ### A mouse button as a key
72
+
73
+ `bindPointer` makes a mouse button press a bound key, so a click fires the same
74
+ action and `isKeyPressed` sees it:
75
+
76
+ ```js
77
+ input.bindKey(input.KEY.X, "fire");
78
+ input.bindPointer(input.KEY.X); // the left button
79
+ input.bindPointer(input.pointer.RIGHT, input.KEY.X); // or another button
80
+ input.unbindPointer(input.pointer.RIGHT); // LEFT when omitted
81
+ ```
82
+
83
+ As with `bindGamepad`, the key needs its `bindKey` first, or `bindPointer`
84
+ throws `no action defined for keycode N`. Turning the wheel neither presses
85
+ nor releases a bound button's key (before 20.14 it released the left
86
+ button's).
87
+
63
88
  ## Pointer events need `isKinematic = false`
64
89
 
65
90
  **This is the trap.** `Renderable.isKinematic` defaults to `true`, and both the
@@ -82,9 +107,11 @@ class Button extends Renderable {
82
107
  ```
83
108
 
84
109
  `Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*` elements set it
85
- to `false` for you, and so does attaching a physics body (`Body`'s constructor
86
- sets `ancestor.isKinematic = false`) — so plain `Renderable` and `Sprite`
87
- subclasses with no body are the ones that catch people.
110
+ to `false` for you, and so does giving it a physics body (a `bodyDef` on any
111
+ adapter, or a `Body`) — so plain `Renderable` and `Sprite` subclasses with no
112
+ body are the ones that catch people. Before melonJS 20.14, matter-adapter
113
+ 1.6.0 and planck-adapter 1.7.0, a `bodyDef` under matter or planck left
114
+ `isKinematic` at `true`, so set it yourself there.
88
115
 
89
116
  Register in `onActivateEvent` and release in `onDeactivateEvent`, not in the
90
117
  constructor — pooled objects are reused and would otherwise accumulate handlers.
@@ -103,6 +130,11 @@ throw `invalid event type`. melonJS maps the canonical name onto whichever
103
130
  mouse/touch events the device actually supports, so you always register the
104
131
  pointer name.
105
132
 
133
+ A region can listen to `"pointerup"` alone, to act on release: it gets the
134
+ release of a press made on it. With several fingers on one region, each finger
135
+ that pressed it gets its own `pointerup` / `pointercancel`; `pointerenter` and
136
+ `pointerleave` still follow one pointer per region.
137
+
106
138
  ## Which object gets the event
107
139
 
108
140
  The dispatcher collects the regions near the pointer from the broadphase,
@@ -182,9 +214,10 @@ globalThis.addEventListener("pointerup", onRelease);
182
214
 
183
215
  For plain drag-and-drop, `Draggable` / `DropTarget` already wire all of this up
184
216
  (`pointerdown` / `pointerup` / `pointercancel` regions plus a global
185
- `POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`. `DRAGEND`
186
- still originates from a region handler, so it only fires when the release lands
187
- on the draggable itself.
217
+ `POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`, and
218
+ follow the pointer under any camera, scrolled or zoomed. `DRAGEND` still
219
+ originates from a region handler, so it only fires when the release lands on
220
+ the draggable itself.
188
221
 
189
222
  ## Gamepad
190
223
 
@@ -252,10 +285,10 @@ input.hasGamepadVibration(index); // for an options-screen toggle
252
285
 
253
286
  Safe to call blind: no pad, no motors, or no Gamepad API at all resolves
254
287
  `false` and does nothing, so game code never branches on support. Magnitudes
255
- default to **full** — the underlying web API defaults every parameter to zero,
256
- which makes a bare `playEffect` a silent no-op. A second call preempts the
257
- first on the same pad, so a rapid series of hits reads as a rapid series of
258
- hits rather than as a queue. For a SUSTAINED rumble issue one effect with a
288
+ default to **full** and `duration` to 200 ms — the underlying web API defaults
289
+ every parameter to zero, which makes a bare `playEffect` a silent no-op. A
290
+ second call preempts the first on the same pad, so a rapid series of hits
291
+ reads as a rapid series of hits rather than as a queue. For a SUSTAINED rumble issue one effect with a
259
292
  long `duration` instead of calling every frame.
260
293
 
261
294
  Hot-plugging needs no polling of your own — the engine listens for the browser's
@@ -270,12 +303,15 @@ event.on(event.GAMEPAD_DISCONNECTED, (gamepad) => { /* pause, show a prompt */ }
270
303
 
271
304
  Bindings survive a disconnect: they are attached to the index, not to the
272
305
  device, so a pad unplugged and plugged back into the same slot keeps working.
306
+ A pad unplugged with a button or the stick held lets go of the keys bound to
307
+ them, as releasing them would.
273
308
 
274
309
  ## Virtual controls
275
310
 
276
- For on-screen touch controls, use `@melonjs/gamepad-plugin` rather than
277
- writing them: sticks and buttons drawn over the game and reported as a pad in
278
- a gamepad slot, so the `bindGamepad` bindings above answer to them unchanged.
311
+ For on-screen touch controls, use `@melonjs/gamepad-plugin` (it needs melonJS
312
+ 20.12 or later) rather than writing them: sticks and buttons drawn over the
313
+ game and reported as a pad in a gamepad slot, so the `bindGamepad` bindings
314
+ above answer to them unchanged.
279
315
 
280
316
  ```js
281
317
  import { GamepadPlugin } from "@melonjs/gamepad-plugin";
@@ -326,6 +362,12 @@ Every press lasts at least one frame, so a 15 ms thumb tap still reaches
326
362
  `isKeyPressed`, and Safari's pinch gesture is cancelled while the controls
327
363
  show, so a second thumb works on iOS.
328
364
 
365
+ To match the game's artwork (1.2), pass an already-loaded `TextureAtlas` as
366
+ `skin` to `GamepadPlugin`, with `base` / `thumb` region names on sticks
367
+ and `region` / optional `pressed` names on buttons. Omitted parts keep
368
+ their shapes. Regions scale to the controls' sizes without changing hit areas;
369
+ labels, floating controls, opacity, hiding and dimming keep working.
370
+
329
371
  A single key can also be pressed directly, reaching the same actions:
330
372
 
331
373
  ```js
@@ -338,19 +380,27 @@ input.triggerKeyEvent(input.KEY.LEFT, false); // release
338
380
  | symptom | cause |
339
381
  |---|---|
340
382
  | pointer handler never fires | `isKinematic` left at its `true` default |
383
+ | touches never arrive where there is no DOM (nx.js, a Web Worker) | melonJS older than 20.14 listened for mouse events there; update. Touch is then read from `TouchEvent`s dispatched on the canvas given as `settings.canvas` |
341
384
  | handlers fire twice, or on dead objects | registered in the constructor instead of `onActivateEvent` |
342
385
  | camera drag jumps around | using world coordinates instead of `gameScreenX`/`gameScreenY` |
343
386
  | `gameX` holds screen coords where world coords were expected | read on `event.POINTERMOVE`, or on a floating region — use `gameWorldX`/`gameWorldY` |
344
387
  | drag stops when leaving the object | region events end at the region — use `event.POINTERMOVE` |
345
388
  | release never detected outside the region | no global pointer-up event — add a raw listener |
346
389
  | jump fires every frame while held | `bindKey` without its third `lock` argument |
390
+ | the hero keeps walking after an Alt-Tab, until the key is pressed again (before 20.14) | the key's release went to another window: update, held keys are now released on blur |
391
+ | a region with only a `"pointerup"` callback never gets it, or the first finger's release is lost with two on one region (before 20.14) | the release was only delivered to the last pointer to press the region: update |
392
+ | a key bound with `bindPointer` never fires once the mouse has entered the canvas (before 20.12) | the pointer's button bindings were replaced on each event: update |
393
+ | firing (a key bound with `bindPointer`) stops while the button is held and the wheel turns (before 20.14) | a wheel event released the main button's key: update |
394
+ | clicks land off target after browser zoom or a move to another screen (before 20.14) | the pointer was scaled by the live pixel ratio against a canvas sized with the old one: update |
395
+ | a `Draggable` jumps away when grabbed, and stays stuck dragging, once the camera has moved (before 20.14) | it mixed screen and world coordinates: update |
347
396
  | `invalid event type` thrown at registration | non-pointer name (`"click"`, `"mousedown"`, …) passed to `registerPointerEvent` |
348
- | `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |
397
+ | `no action defined for keycode N` | `bindGamepad` or `bindPointer` called before `bindKey` for that keycode |
349
398
  | floating stick ignores the right half of the screen | its `area` defaults to `"half"`, its own side — use `area: "screen"` |
350
399
  | `area: "screen"` stick still ignores part of the screen | a floating button's area comes first there — the overlap order is fixed buttons, fixed sticks, floating buttons, floating sticks |
351
400
  | a quick tap on a stick moves instead of acting | it leaned past `dpadThreshold` or outlasted `tap.time` before lifting |
352
401
  | stick has to be pushed well past its `threshold` before it registers | `threshold` is ADDED to the deadzone, not used instead of it |
353
402
  | one half of a stick does nothing | each half is a separate `bindGamepad`, chosen by the sign of `threshold` |
403
+ | a key stays pressed after its pad is unplugged, or a stick bound on one side holds its key after coming back to the centre (before 20.12) | update: an unplugged pad lets go of what it held, and a stick lets go when it returns to rest, even just past the centre |
354
404
  | a widget under an overlapping panel still lights up | the panel consumes no hover: return `false` from its `onOver` (presses already stop at UI elements; `UIPanel` with `blocking` does both) |
355
405
  | a widget stays highlighted after the pointer moves onto a panel over it | the panel consumes no `pointermove`, so nothing takes the pointer off the widget |
356
406
  | a child's big `z` does not put it in front of another container | `pos.z` is container-local — use `ancestor.moveToTop()` on the container |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-lighting
3
- description: "Use this skill for 2D dynamic lighting in melonJS — Light2d, ambient light on a Stage, multiple lights, and per-pixel normal-mapped sprites authored in SpriteIlluminator. Covers specular highlights via shininess, the ambient-cutout model, which parts need a GPU backend, and why Light2d does not survive a Camera3d. Triggers on: Light2d, specular, shininess, highlight, glint, lighting, ambientLight, ambientLightingColor, normalMap, SpriteIlluminator, per-pixel lighting, drawLight, torch, glow, darkness."
3
+ description: "Use this skill for 2D dynamic lighting in melonJS — Light2d, ambient light on a Stage, multiple lights, and per-pixel normal-mapped sprites authored in SpriteIlluminator. Covers specular highlights via shininess, the ambient-cutout model, which parts need a GPU backend, and why Light2d does not survive a Camera3d. Triggers on: Light2d, softness, soft light, lightHeight, setRadii, specular, shininess, highlight, glint, lighting, ambientLight, ambientLightingColor, normalMap, SpriteIlluminator, per-pixel lighting, drawLight, torch, glow, darkness."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -31,6 +31,11 @@ intensity = 0.7)`, and `pos` is the light's **centre** — its `anchorPoint` is
31
31
  `(0.5, 0.5)`, and `centerX` / `centerY` are overridden to return `pos` directly.
32
32
  Give `radiusY` a different value for a stretched, elliptical light.
33
33
 
34
+ Resize a light with `light.setRadii(radiusX, radiusY)`, which resizes its
35
+ bounds too. Writing `radiusX` alone changes nothing you can see: the glow, the
36
+ hole in the darkness and the normal-map shading are all sized from the bounds,
37
+ on every backend.
38
+
34
39
  A `Light2d` added to the world **registers itself with the stage's ambient
35
40
  overlay on activate**, so you add it like any other renderable and it takes part
36
41
  in the lighting pass. It draws additively (`blendMode = "lighter"`); set
@@ -105,6 +110,11 @@ const hero = new Sprite(x, y, {
105
110
  canvas, `ImageBitmap`, a `Texture2d`). Assigning anything else — including an
106
111
  `HTMLVideoElement`, which would freeze on frame 0 — throws a `TypeError`.
107
112
 
113
+ How grazing the light is comes from `light.lightHeight`, its height above the
114
+ sprite plane in the same units as its radius (default `max(radiusX, radiusY) *
115
+ 0.075`). Lower it for light raking across the surface, which shows the relief
116
+ off; raise it for flatter, more even shading.
117
+
108
118
  Normal maps are typically authored in **SpriteIlluminator**. The sidecar pattern
109
119
  above — one `_n` image beside each diffuse image — is the simplest setup, but an
110
120
  atlas can carry one too: `new TextureAtlas(json, image, { normalMap })` pairs a
@@ -209,6 +219,8 @@ See the `melonjs-3d` skill.
209
219
  | sprite not shaded by a light, only glowed over | no `normalMap` on the sprite, or the Canvas backend is active |
210
220
  | normal-mapped sprite is pure black where unlit | `Stage.ambientLightingColor` left at black |
211
221
  | the 33rd light shades nothing | `MAX_LIGHTS = 32` for the normal-map pipeline |
222
+ | changing `radiusX` / `radiusY` does nothing | everything is sized from the bounds, which `setRadii()` updates and a bare field write does not |
223
+ | normal-mapped relief looks flat under a light | `lightHeight` is high relative to the radius; lower it for a grazing light |
212
224
  | `TypeError` assigning `normalMap` | not an image-like value (a video element is rejected outright) |
213
225
  | lighting misaligned under a 3D camera | the 2D light pass is not projection-aware — use `Light3d` |
214
226
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-loading-assets
3
- description: "Use this skill for loading and managing game assets in melonJS — the resource descriptor format, the full asset type list, preload versus load, retrieving loaded assets, base URLs and cross-origin settings, and the loading screen. Triggers on: loader, loader.preload, loader.load, resources, getImage, getJSON, getTMX, getGLTF, getVideo, getFont, setBaseURL, setOptions, crossOrigin, withCredentials, credentials, cookies, authenticated assets, CORS, asset, preload, LOADER_COMPLETE, loading screen, unload, setParser, custom asset type, generated texture, procedural texture."
3
+ description: "Use this skill for loading and managing game assets in melonJS — the resource descriptor format, the full asset type list, organising a multi-level game's assets as bundles (an assets file exporting common plus one bundle per level, passed to loader.preload), loading and freeing a level's bundle, preload versus load, retrieving loaded assets, base URLs and cross-origin settings, and the loading screen. Triggers on: loader, loader.preload, loader.load, resources, resources.js, assets file, getImage, getJSON, getTMX, getGLTF, getVideo, getFont, setBaseURL, setOptions, crossOrigin, withCredentials, credentials, cookies, authenticated assets, CORS, asset, preload, LOADER_COMPLETE, loading screen, unload, unloadAll, bundle, asset bundle, setBundle, getBundle, isLoaded, several levels, per-level assets, free memory between levels, setParser, custom asset type, generated texture, procedural texture."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -8,26 +8,13 @@ license: MIT
8
8
 
9
9
  ## The resource descriptor
10
10
 
11
- Every asset is `{ name, type, src }` plus type-specific extras:
11
+ Every asset is `{ name, type, src }` plus type-specific extras. The name is
12
+ what the asset is read back by; the type picks the parser. Type and name
13
+ together identify an asset, so an atlas's image and its JSON share a name:
12
14
 
13
15
  ```js
14
- import { loader } from "melonjs";
15
-
16
- await loader.preload([
17
- { name: "tiles", type: "image", src: "data/img/tiles.png" },
18
- { name: "map1", type: "tmx", src: "data/map/map1.tmx" },
19
- { name: "tileset", type: "tsx", src: "data/map/tileset.tsx" },
20
- { name: "sfx", type: "audio", src: "data/sfx/" },
21
- { name: "font", type: "fontface", src: "data/font/PressStart2P.ttf" },
22
- { name: "atlas", type: "json", src: "data/img/atlas.json" },
23
- ]);
24
- ```
25
-
26
- `preload()` has been awaitable since 19.8. The callback form still works and
27
- takes a third argument to suppress the built-in loading screen:
28
-
29
- ```js
30
- loader.preload(resources, onLoaded, /* switchToLoadState */ false);
16
+ { name: "texture", type: "image", src: "data/img/texture.png" }
17
+ { name: "texture", type: "json", src: "data/img/texture.json" }
31
18
  ```
32
19
 
33
20
  ## The type list
@@ -36,7 +23,7 @@ The type strings are melonJS-specific — this is not a generic loader:
36
23
 
37
24
  | type | for |
38
25
  |---|---|
39
- | `"image"` | textures, sprite sheets, normal maps |
26
+ | `"image"` | textures, sprite sheets, normal maps; `src` may be an array tried in order, such as compressed formats (`.ktx`, `.ktx2`, `.dds`, `.pvr`, `.pkm`) by priority with a PNG last |
40
27
  | `"json"` | data, texture-atlas descriptors, collision shapes from a shape editor |
41
28
  | `"tmx"` / `"tsx"` | Tiled maps and external tilesets |
42
29
  | `"audio"` | sound — **`src` is a directory**, see below |
@@ -50,6 +37,128 @@ The type strings are melonJS-specific — this is not a generic loader:
50
37
  | `"js"` | scripts |
51
38
  | any type you register | your own formats, and things you generate rather than download — see [Custom asset types](#custom-asset-types) |
52
39
 
40
+ ## Organise the assets: `common` plus one bundle per level
41
+
42
+ For any game with **more than one level or screen**, the assets file exports a
43
+ **record of bundles**: what every level uses under `common`, and each level's
44
+ own assets in a bundle of its own. The boot call takes the record as it is:
45
+
46
+ ```js
47
+ // resources.js
48
+ export const resources = {
49
+ common: [ // everything every level uses
50
+ { name: "player", type: "image", src: "data/img/player.png" },
51
+ { name: "tileset", type: "image", src: "data/img/tileset.png" },
52
+ { name: "tileset", type: "tsx", src: "data/map/tileset.tsx" },
53
+ { name: "music", type: "audio", src: "data/bgm/" },
54
+ { name: "jump", type: "audio", src: "data/sfx/" },
55
+ ],
56
+ level1: [
57
+ { name: "level1", type: "tmx", src: "data/map/level1.tmx" },
58
+ { name: "hills", type: "image", src: "data/img/hills.png" },
59
+ ],
60
+ level2: [
61
+ { name: "level2", type: "tmx", src: "data/map/level2.tmx" },
62
+ { name: "hills", type: "image", src: "data/img/hills.png" }, // shared: listed again
63
+ { name: "lava", type: "image", src: "data/img/lava.png" },
64
+ ],
65
+ };
66
+ ```
67
+
68
+ ```js
69
+ // game.js
70
+ import { audio, loader, state } from "melonjs";
71
+ import { resources } from "./resources.js";
72
+
73
+ audio.init("mp3,ogg"); // before any audio is loaded
74
+ await loader.preload(resources); // every bundle, behind the loading screen
75
+ state.change(state.PLAY);
76
+ ```
77
+
78
+ `preload(record)` declares each key as a bundle and loads them all, with one
79
+ progress bar; an asset several bundles list is fetched once. Nothing else
80
+ changes: a bundle is a lifetime, not a namespace, so assets keep their own
81
+ names, and `getImage("hills")`, `new Sprite(x, y, { image: "player" })` and
82
+ `level.load("level1")` work as with a flat list. What bundles add is that a
83
+ level can later be freed and loaded again **by its id** (next section). Starting
84
+ with bundles costs nothing even if the game never unloads anything.
85
+
86
+ - **Name a level's bundle after its map.** Bundle ids and asset names are
87
+ separate, so `"level1"` can be both, and `level.load(id)` and
88
+ `loader.load(id)` then take the same string.
89
+ - **List a shared asset in every bundle that uses it.** It is still fetched
90
+ once, and it is what keeps the asset alive when one of those bundles is
91
+ unloaded.
92
+ - **A map's dependencies go with it**: its `.tsx` and tileset images in the
93
+ same bundle, or in `common`.
94
+ - **Effects that outlive a level go in `common`** (a HUD's or a camera's
95
+ shader): unloading the level's bundle would destroy them under whatever
96
+ still draws with them.
97
+
98
+ **A flat array is the simplest form**, and is fine for a single-screen game, a
99
+ prototype or a jam game. It works as it always has, and everything in it stays
100
+ in memory for the whole game:
101
+
102
+ ```js
103
+ await loader.preload([
104
+ { name: "tiles", type: "image", src: "data/img/tiles.png" },
105
+ { name: "map1", type: "tmx", src: "data/map/map1.tmx" },
106
+ { name: "sfx", type: "audio", src: "data/sfx/" },
107
+ ]);
108
+ ```
109
+
110
+ Moving to bundles later is a change to the assets file alone: wrap the list in
111
+ `{ common: [ … ] }` and split the levels out.
112
+
113
+ Either way, `preload()` can be awaited (since 19.8) or given a callback, and a
114
+ third argument suppresses the built-in loading screen:
115
+
116
+ ```js
117
+ loader.preload(resources, onLoaded, /* switchToLoadState */ false);
118
+ ```
119
+
120
+ ## Level by level: load before, unload after
121
+
122
+ With every bundle preloaded at boot, nothing more is needed to play. To keep
123
+ memory bounded in a game whose levels are many or large:
124
+
125
+ ```js
126
+ import { level, loader } from "melonjs";
127
+
128
+ // leaving level1 for good: load the next, switch, then free the previous
129
+ await loader.load("level2"); // fetches nothing if still in memory
130
+ await level.load("level2", { async: true });
131
+ loader.unload("level1"); // "hills" stays: level2 lists it too
132
+
133
+ // going back to a level that was unloaded
134
+ await loader.load("level1"); // fetches what unloading freed
135
+ await level.load("level1", { async: true });
136
+ ```
137
+
138
+ - **`await loader.load(id)` before entering a level that may not be in
139
+ memory.** When the bundle is still loaded it resolves at once with no
140
+ request, so it is cheap to call every time. It does not show the loading
141
+ screen (the game keeps running); use `loader.preload([id])` for that, or
142
+ `loader.isLoaded(id)` to decide.
143
+ - **`loader.unload(id)` for a level the player will not come back to soon,
144
+ after the next level is in.** It frees what no other loaded bundle lists.
145
+ Unloading the level being played leaves its world as it is, but with no
146
+ current level, so `level.reload()` has nothing to reload.
147
+
148
+ **When not to unload:**
149
+
150
+ - **Levels that loop back** — a hub, a retry after game over, a last level
151
+ whose exit leads to the first. Unloading one means fetching it again, and a
152
+ hitch, every time the player returns.
153
+ - **A small game.** If every level fits comfortably in memory, keep
154
+ everything loaded. The platformer example does exactly this: `resources.ts`
155
+ exports `{ common, map1, map2 }`, `preload(resources)` loads it all at boot,
156
+ and since map2's exit leads back to map1, nothing is ever unloaded.
157
+
158
+ Unload when the levels are large (big tilesets and backgrounds, per-level
159
+ music, 3D scenes), numerous, or linear (a level behind the player for good),
160
+ and on memory-constrained targets such as phones and mini-game platforms.
161
+
53
162
  ## What belongs in the manifest — and what cannot
54
163
 
55
164
  Preloading is not only for things with a URL. Three tiers, and the middle one
@@ -117,8 +226,8 @@ const app = new Application(1280, 720, {
117
226
 
118
227
  **You do not call this yourself.** `preload()` calls `renderer.prewarm()` once
119
228
  the assets are in and **before** `LOADER_COMPLETE` fires, so it happens while
120
- the progress bar is still up. Every `preload()` warms, not just the first, so a
121
- per-level preload warms whatever that level brought with it.
229
+ the progress bar is still up. Every `preload()` warms, not just the first, and
230
+ so does `load(id)`, so loading a level's bundle warms whatever it brought.
122
231
 
123
232
  The one precondition for this part: **a game that never calls
124
233
  `loader.preload()` does not get it**. If you load assets another way, call
@@ -361,15 +470,71 @@ still loading joins it, and the value covers both until everything in flight
361
470
  has settled; the next preload starts from zero again. A failed asset keeps
362
471
  its preload short of `1`, and only that preload.
363
472
 
364
- ## Unloading
473
+ ## Bundle and unloading reference
474
+
475
+ Bundles can also be declared one by one, which is what `preload(record)` does
476
+ for each key:
365
477
 
366
478
  ```js
367
- loader.unload({ name: "map1", type: "tmx" });
368
- loader.unloadAll();
479
+ loader.setBundle("level3", [
480
+ { name: "level3", type: "tmx", src: "data/map/level3.tmx" },
481
+ { name: "hills", type: "image", src: "data/img/hills.png" },
482
+ ]);
483
+ await loader.preload(["common", "level3"]); // ids and descriptors mix freely
369
484
  ```
370
485
 
371
- `unloadAll()` also releases audio and GPU resources such as shader programs, so
372
- it is the right teardown for switching between large levels.
486
+ - **Strings are bundles, objects are assets.** `loader.unload("map1")`
487
+ releases the bundle `"map1"`; `loader.unload({ name: "map1", type: "tmx" })`
488
+ frees that one asset at once, whoever holds it (the bundles listing it then
489
+ report `isLoaded(id) === false`, and loading one again fetches it again).
490
+ - **`load(id)`** goes through the same path as `preload()`: `LOADER_PROGRESS`
491
+ for the bundle's own assets, the shader warm-up, then `LOADER_COMPLETE`. It
492
+ does not switch to the loading screen, and always returns a promise
493
+ (`onload` / `onerror` callbacks are optional extras).
494
+ - **`load(id, assets)` declares and loads in one call** — `setBundle(id, assets)`
495
+ then `load(id)`. Called again with the same assets (type, name, src) it just
496
+ loads; with different ones it redefines the bundle, which throws while that
497
+ bundle is loaded. Handy where a level's assets are declared next to the code
498
+ that enters it.
499
+ - **`preload(record)` again** fetches nothing already in memory. A bundle
500
+ already declared with the same assets is left as it is. A single descriptor
501
+ passed by mistake throws: wrap it in an array.
502
+ - **Progress:** each asset of the bundle is one step; one already in memory
503
+ counts as a step done at once. An asset listed twice is one asset, and an
504
+ empty bundle is allowed (it is always loaded).
505
+ - **Nothing is fetched twice.** An asset already in memory is not requested
506
+ again, however it got there, and two bundles loading the same asset at once
507
+ share one request.
508
+ - **Ownership.** Each loaded bundle listing an asset holds it; `unload(id)`
509
+ frees the assets no other loaded bundle lists. An asset loaded the old way —
510
+ a flat `preload` list, or `load(asset)` — is **pinned**: no bundle unload
511
+ frees it, only `unload(asset)` or `unloadAll()`.
512
+ - **`isLoaded(id)`** is true when every asset of the bundle is in memory,
513
+ however it got there. `getBundle(id)` returns a copy of the list, or
514
+ `undefined` for an id never declared.
515
+ - **Freeing a map unregisters its level.** After `unload(id)`, `level.load` of
516
+ a map (or glTF scene) that bundle freed throws "level … not found" until the
517
+ bundle is loaded again, which registers it again.
518
+ - **Freeing releases the GPU side too:** an image's texture (since 20.14), a
519
+ shader's program, a font's place in `document.fonts`. `getShader()` hands out
520
+ the loader's shared instance and does not track who took it, hence
521
+ "effects that outlive a level go in `common`".
522
+ - **A failing asset rejects `load(id)`**, like `preload`; the assets that did
523
+ load stay, held by the bundle, and `loader.reload(src)` completes it.
524
+ - **Errors are synchronous.** An unknown id throws in `load`, `unload`,
525
+ `preload` and `isLoaded`. Redefining a bundle with `setBundle` throws while
526
+ it is loading or loaded: unload it first. A bundle lists descriptors only,
527
+ not other bundle ids.
528
+ - **`unloadAll()`** frees every asset, audio included, whoever holds it. The
529
+ bundles stay declared, all back to not loaded, ready to be loaded again.
530
+ Levels stay registered (unlike a bundle unload), so load a level's bundle
531
+ again before `level.load` of it.
532
+ - **`load(asset)`, with a descriptor**, loads one asset while the game runs:
533
+ no loading screen, progress or loader event, only its promise (or the
534
+ `onload` / `onerror` callbacks). It is pinned. One already in memory
535
+ resolves at once with no request (the callback form returns `0` and never
536
+ calls `onload`), except a `"binary"`, `"fontface"`, `"aseprite"` or `"js"`
537
+ asset, which is fetched again.
373
538
 
374
539
  ## Loading between scenes
375
540
 
@@ -459,6 +624,16 @@ throws, the same trap as the removed `loader.onload` / `onProgress`.
459
624
  | `load : unknown or invalid resource type` | a custom type in the manifest before its `loader.setParser` call |
460
625
  | black or frozen screen when a stage is first entered, the profile shows your own code (noise, a canvas bake) | a costly derived asset built on stage entry — bake it inside the preload as a [custom asset type](#custom-asset-types) |
461
626
  | a custom asset preloads but the result is nowhere to be found | `onload` takes no value and the loader stores nothing for custom types — keep the result in your own module |
627
+ | `level X not found` after switching levels with bundles | that map's bundle was unloaded (it unregisters the level), or never loaded: `await loader.load(id)` before `level.load` |
628
+ | `loader: unknown bundle "…"` | a typo, or `setBundle` not called yet; ids and asset names are separate, a bundle is not found by an asset's name |
629
+ | `setBundle(…): the bundle is loaded` | redefining a loaded bundle, by `setBundle` or by `load(id, assets)` with a different list: `loader.unload(id)` first |
630
+ | a shared image disappears (blank layer) after unloading the previous level's bundle | the next level's bundle does not list it: list a shared asset in every bundle that uses it |
631
+ | an asset survives `unload(id)` and memory stays up | it was also loaded the old way (a flat `preload` list, or `load(asset)`), which pins it; load it only through bundles, or `unload(asset)` |
632
+ | a HUD or camera effect goes blank, or throws, after a level's bundle is unloaded | its shader came from that bundle; effects that outlive a level belong in a bundle that outlives it (`"common"`) |
633
+ | `level.reload()` throws `level undefined not found` | the current level's own bundle was unloaded; unload the previous level's bundle after the next level is in |
634
+ | a pause and new network requests every time the player goes back to an earlier level | that level's bundle is unloaded on leaving it although the game loops back to it; keep it loaded (see [When not to unload](#level-by-level-load-before-unload-after)) |
635
+ | memory grows level after level although assets are unloaded (before 20.14) | unloading an image left its GPU texture in the renderer; update |
636
+ | an asset given as a `data:` URI fails, or the default loading screen shows no logo, in a sandboxed embed or under a strict Content-Security-Policy (before 20.11) | the `data:` URI was fetched, which a `connect-src` without `data:` refuses; update, it is decoded in place |
462
637
 
463
638
  ## Related skills
464
639
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-math
3
- description: "Use this skill for vector and matrix maths in melonJS — Vector2d, Vector3d, their observable variants, Matrix2d, Matrix3d, and the math utility functions. Covers which methods mutate and which allocate, the vector pool and when clone() is the wrong tool, interpolation with lerp, slerp, damp and moveTowards, and the argument defaults that silently drop a component. Triggers on: Vector2d, Vector3d, ObservableVector2d, ObservableVector3d, Matrix2d, Matrix3d, vector, vector maths, dot, cross, normalize, length, distance, angle, lerp, slerp, damp, moveTowards, project, clamp, degToRad, radToDeg, clone, vector pool, vector3dPool, setMuted, setCallback, my vector is wrong, z is zero, scale ignores z., moveTowards ignores z, projectN, currentTransform, setTransform, matrix, transform matrix, column-major, val, Float32Array, basis matrix, orientation matrix, Noise, getNoise1d, flicker, wobble."
3
+ description: "Use this skill for vector and matrix maths in melonJS — Vector2d, Vector3d, their observable variants, Matrix2d, Matrix3d, and the math utility functions. Covers which methods mutate and which allocate, the vector pool and when clone() is the wrong tool, interpolation with lerp, slerp, damp and moveTowards, and the argument defaults that silently drop a component. Triggers on: Vector2d, Vector3d, ObservableVector2d, ObservableVector3d, Matrix2d, Matrix3d, vector, vector maths, dot, cross, normalize, length, distance, angle, lerp, slerp, damp, moveTowards, project, clamp, degToRad, radToDeg, clone, vector pool, vector3dPool, setMuted, setCallback, my vector is wrong, z is zero, scale ignores z, moveTowards ignores z, setPosition, projectN, currentTransform, setTransform, matrix, transform matrix, column-major, val, Float32Array, basis matrix, orientation matrix, Noise, getNoise1d, flicker, wobble."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -97,6 +97,9 @@ v.scale(2, 2, 2); // (1,2,3) -> (2,4,6) what you meant
97
97
  v.set(10, 20); // z is now 0, whatever it was
98
98
  ```
99
99
 
100
+ To move a renderable, `setPosition(x, y)` (20.8) is the call that keeps its
101
+ depth: leave out `z` and it stays as it was, where `pos.set(x, y)` zeroes it.
102
+
100
103
  **`Vector3d.pos` on a renderable is typed `Vector2d`.** `Renderable.pos` is an
101
104
  `ObservableVector3d` at run time but is declared through `Polygon` as a
102
105
  `Vector2d`, so `.z`, `.setMuted` and `.setCallback` need a cast from
@@ -117,7 +120,7 @@ The 2D one is the z component of the 3D cross, which is the useful scalar for
117
120
  ```js
118
121
  a.lerp(b, t); // straight line, t in 0..1
119
122
  a.slerp(b, t); // along the ARC, constant angular rate
120
- a.damp(b, lambda, dt); // frame-rate independent ease toward b
123
+ a.damp(b, lambda, dt); // frame-rate independent ease toward b (dt in seconds)
121
124
  a.moveTowards(b, step); // fixed distance per call, never overshoots
122
125
  ```
123
126
 
@@ -292,11 +295,13 @@ math.clamp(v, lo, hi);
292
295
  math.lerp(a, b, t); // scalars
293
296
  math.damp(a, b, lambda, dt); // frame-rate independent
294
297
  math.degToRad(deg); math.radToDeg(rad);
295
- math.random(lo, hi); // integer
298
+ math.random(lo, hi); // integer, hi EXCLUDED: random(1, 6) never gives 6
296
299
  math.randomFloat(lo, hi);
297
- math.weightedRandom(lo, hi); // integer, biased toward LO
300
+ math.weightedRandom(lo, hi); // integer, biased toward LO, hi excluded
298
301
  math.round(n, decimals); // nearest; a tie goes up, as Math.round: -2.5 → -2
299
302
  math.pow(n); // n squared
303
+ math.lerpArray([1, 0.5, 0], t); // a value along evenly spaced samples, t in 0..1
304
+ math.isPowerOfTwo(n); math.nextPowerOfTwo(n);
300
305
  math.DEG_TO_RAD math.RAD_TO_DEG math.TAU math.ETA math.EPSILON
301
306
  ```
302
307
 
@@ -324,11 +329,13 @@ and `Math.random()` every frame jitters instead of drifting.
324
329
  | symptom | cause |
325
330
  |---|---|
326
331
  | z unchanged after scaling | `scale(s)` leaves z alone; pass `scale(s, s, s)` |
327
- | z became 0 for no reason | `set(x, y)` defaults z to 0 |
332
+ | z became 0 for no reason | `set(x, y)` defaults z to 0; move a renderable with `setPosition(x, y)` |
328
333
  | the original vector changed | the method mutates; `copy()` into something first |
329
334
  | a position callback fires twice and sees a half-updated vector, or `pos.floorSelf()` returns something that is not `pos` | before 20.13 the observable `negateSelf` / `floorSelf` / `ceilSelf` / `clampSelf` wrote component by component; upgrade |
330
335
  | `math.round(-2.7)` is -2, or a large value comes back negative | before 20.13 `round` truncated toward zero through 32-bit integers; upgrade |
336
+ | `math.isPowerOfTwo(0)` is true, or a fraction passes | before 20.13 it accepted zero, negative and fractional values; upgrade |
331
337
  | memory climbs in a loop | `clone()` or `negate()`/`floor()`/`ceil()` per frame |
338
+ | memory taken by dropped `clone()`s never comes back | before 20.9 the pool kept every instance it handed out; upgrade, and release what you clone |
332
339
  | two helper results are the same object | a shared scratch vector was returned |
333
340
  | `.z` is a type error | `Renderable.pos` is declared `Vector2d` (#1510) |
334
341
  | rotation eases oddly between directions | `lerp` on directions; use `slerp` |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-particles-and-trails
3
- description: "Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, 3D particle motion through elevation and minSpread/maxSpread in 20.8, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: Trail3d, ribbon, exhaust trail, speed streak, ski track, tyre mark, ribbonNormal, ParticleEmitter, particles, particle editor, tune particles, spawn depth, volume, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, elevation, minSpread, maxSpread, spawn area, Trail, explosion, smoke, sparks, debris, ring, exhaust, emitter, tintOverLife, colour over life, color over life, flame, fire."
3
+ description: "Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, 3D particle motion through elevation and minSpread/maxSpread in 20.8, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: Trail3d, ribbon, exhaust trail, speed streak, ski track, tyre mark, ribbonNormal, ParticleEmitter, particles, particle editor, tune particles, spawn depth, volume, burstParticles, streamParticles, stopStream, autoDestroyOnComplete, onComplete, sprite sheet particles, framewidth, randomFrame, cone, sphere, referenceSpace, ParticleEmitterSettings, elevation, minSpread, maxSpread, spawn area, Trail, explosion, smoke, sparks, debris, ring, exhaust, emitter, tintOverLife, colour over life, color over life, flame, fire."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -64,7 +64,8 @@ It covers every setting that changes how one emitter looks, but not:
64
64
  the one drawn on top last (or with a higher `z`).
65
65
  - **Trails.** `Trail` and `Trail3d` are separate renderables; see below.
66
66
  - **Code-only settings.** `onComplete`, `autoDestroyOnComplete`,
67
- `accurateBounds`, and a `Container` as `referenceSpace` (it offers
67
+ `accurateBounds` (deprecated since 20.2, and does nothing), and a
68
+ `Container` as `referenceSpace` (it offers
68
69
  `"local"` and `"world"`). `textureAdditive` is left out: `blendMode`
69
70
  replaces it.
70
71
  - **Changes at run time.** Calling `burstParticles()` when something
@@ -258,7 +259,9 @@ debris.
258
259
 
259
260
  For one-shot effects, `autoDestroyOnComplete: true` removes the emitter once its
260
261
  particles have finished. The `onComplete` callback fires at that same moment
261
- either way.
262
+ either way. A stream finishes only once it stops emitting: one left on the
263
+ default `duration` (`Infinity`) never completes until you call `stopStream()`,
264
+ so give a stream that should clean up after itself a `duration`.
262
265
 
263
266
  For repeated effects (muzzle flashes, impacts) keep **one** long-lived emitter
264
267
  and re-aim it per shot, rather than allocating an emitter per event.
@@ -392,6 +395,7 @@ answer when you want discrete puffs rather than a continuous ribbon.
392
395
  | the cloud follows a moving emitter | default `referenceSpace: "local"` — use `"world"` for trails |
393
396
  | particles do not travel far enough | settings tuned pre-20.2, when drawn travel was doubled |
394
397
  | one-shot emitters accumulate | missing `autoDestroyOnComplete: true` |
398
+ | a streaming emitter with `autoDestroyOnComplete` never goes away | the stream runs on the default `duration` of `Infinity`: give it a `duration`, or call `stopStream()` |
395
399
  | every particle of an emitter is the same colour all its life | `tint` is one value for the emitter: use `tintOverLife` for a colour that changes as it ages |
396
400
  | a `tintOverLife` colour's transparency has no effect | the list sets the colour only; opacity is the particle's fade |
397
401
  | `tintOverLife` colours look muddy | the texture is coloured; author it white so the tints come through as given |