melonjs 20.1.1 → 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 (165) hide show
  1. package/README.md +88 -10
  2. package/build/application/settings.d.ts +4 -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 +10048 -10341
  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/input/pointerevent.d.ts.map +1 -1
  36. package/build/lang/deprecated.d.ts +4 -2
  37. package/build/lang/deprecated.d.ts.map +1 -1
  38. package/build/level/gltf/GLTFScene.d.ts +12 -0
  39. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  40. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  41. package/build/loader/loader.d.ts +4 -40
  42. package/build/loader/loader.d.ts.map +1 -1
  43. package/build/loader/parsers/audio.d.ts +31 -0
  44. package/build/loader/parsers/audio.d.ts.map +1 -0
  45. package/build/loader/parsers/shader.d.ts.map +1 -1
  46. package/build/particles/emitter.d.ts +81 -3
  47. package/build/particles/emitter.d.ts.map +1 -1
  48. package/build/particles/particle.d.ts +62 -1
  49. package/build/particles/particle.d.ts.map +1 -1
  50. package/build/particles/settings.d.ts +50 -7
  51. package/build/particles/settings.d.ts.map +1 -1
  52. package/build/physics/adapter.d.ts +3 -3
  53. package/build/physics/builtin/body.d.ts +23 -24
  54. package/build/physics/builtin/body.d.ts.map +1 -1
  55. package/build/physics/builtin/detector.d.ts +1 -1
  56. package/build/plugin/plugin.d.ts +9 -1
  57. package/build/plugin/plugin.d.ts.map +1 -1
  58. package/build/polyfill/index.d.ts +1 -4
  59. package/build/polyfill/index.d.ts.map +1 -1
  60. package/build/renderable/container.d.ts +10 -0
  61. package/build/renderable/container.d.ts.map +1 -1
  62. package/build/renderable/entity/entity.d.ts +10 -0
  63. package/build/renderable/entity/entity.d.ts.map +1 -1
  64. package/build/renderable/mesh.d.ts +16 -0
  65. package/build/renderable/mesh.d.ts.map +1 -1
  66. package/build/renderable/renderable.d.ts +122 -32
  67. package/build/renderable/renderable.d.ts.map +1 -1
  68. package/build/renderable/sprite.d.ts +7 -7
  69. package/build/renderable/sprite.d.ts.map +1 -1
  70. package/build/state/stage.d.ts +4 -2
  71. package/build/state/stage.d.ts.map +1 -1
  72. package/build/system/event.d.ts +1 -1
  73. package/build/system/platform.d.ts +21 -1
  74. package/build/system/platform.d.ts.map +1 -1
  75. package/build/utils/fetchdata.d.ts.map +1 -0
  76. package/build/video/blendmodes.d.ts +107 -0
  77. package/build/video/blendmodes.d.ts.map +1 -0
  78. package/build/video/canvas/canvas_renderer.d.ts +31 -24
  79. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  80. package/build/video/effects/blendEffect.d.ts +57 -0
  81. package/build/video/effects/blendEffect.d.ts.map +1 -0
  82. package/build/video/effects/blur.d.ts +4 -4
  83. package/build/video/effects/blur.d.ts.map +1 -1
  84. package/build/video/effects/chromaticAberration.d.ts +5 -5
  85. package/build/video/effects/chromaticAberration.d.ts.map +1 -1
  86. package/build/video/effects/colorMatrix.d.ts +5 -5
  87. package/build/video/effects/colorMatrix.d.ts.map +1 -1
  88. package/build/video/effects/desaturate.d.ts +5 -5
  89. package/build/video/effects/desaturate.d.ts.map +1 -1
  90. package/build/video/effects/dissolve.d.ts +4 -4
  91. package/build/video/effects/dissolve.d.ts.map +1 -1
  92. package/build/video/effects/dropShadow.d.ts +5 -5
  93. package/build/video/effects/dropShadow.d.ts.map +1 -1
  94. package/build/video/effects/flash.d.ts +4 -4
  95. package/build/video/effects/flash.d.ts.map +1 -1
  96. package/build/video/effects/glow.d.ts +5 -5
  97. package/build/video/effects/glow.d.ts.map +1 -1
  98. package/build/video/effects/hologram.d.ts +4 -4
  99. package/build/video/effects/hologram.d.ts.map +1 -1
  100. package/build/video/effects/invert.d.ts +5 -5
  101. package/build/video/effects/invert.d.ts.map +1 -1
  102. package/build/video/effects/outline.d.ts +6 -6
  103. package/build/video/effects/outline.d.ts.map +1 -1
  104. package/build/video/effects/pixelate.d.ts +4 -4
  105. package/build/video/effects/pixelate.d.ts.map +1 -1
  106. package/build/video/effects/radialGradient.d.ts +2 -2
  107. package/build/video/effects/scanline.d.ts +6 -6
  108. package/build/video/effects/scanline.d.ts.map +1 -1
  109. package/build/video/effects/sepia.d.ts +5 -5
  110. package/build/video/effects/sepia.d.ts.map +1 -1
  111. package/build/video/effects/shadereffect.d.ts +12 -10
  112. package/build/video/effects/shadereffect.d.ts.map +1 -1
  113. package/build/video/effects/shine.d.ts +3 -3
  114. package/build/video/effects/shine.d.ts.map +1 -1
  115. package/build/video/effects/tintPulse.d.ts +4 -4
  116. package/build/video/effects/tintPulse.d.ts.map +1 -1
  117. package/build/video/effects/vignette.d.ts +6 -6
  118. package/build/video/effects/vignette.d.ts.map +1 -1
  119. package/build/video/effects/wave.d.ts +4 -4
  120. package/build/video/effects/wave.d.ts.map +1 -1
  121. package/build/video/renderer.d.ts +3 -4
  122. package/build/video/renderer.d.ts.map +1 -1
  123. package/build/video/rendertarget/canvasrendertarget.d.ts +1 -0
  124. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  125. package/build/video/texture/atlas.d.ts +1 -1
  126. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  127. package/build/video/webgl/glshader.d.ts +2 -2
  128. package/build/video/webgl/glshader.d.ts.map +1 -1
  129. package/build/video/webgl/utils/program.d.ts.map +1 -1
  130. package/build/video/webgl/webgl_renderer.d.ts +117 -29
  131. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  132. package/build/video/webgpu/batchers/quad_batcher.d.ts +7 -0
  133. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  134. package/build/video/webgpu/pipeline/cache.d.ts +0 -9
  135. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  136. package/build/video/webgpu/webgpu_renderer.d.ts +101 -6
  137. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  138. package/package.json +6 -10
  139. package/skills/AGENTS.md +67 -0
  140. package/skills/melonjs/SKILL.md +156 -0
  141. package/skills/melonjs-20-migration/SKILL.md +160 -0
  142. package/skills/melonjs-3d/SKILL.md +226 -0
  143. package/skills/melonjs-3d-assets/SKILL.md +239 -0
  144. package/skills/melonjs-audio/SKILL.md +215 -0
  145. package/skills/melonjs-camera-and-drawing/SKILL.md +171 -0
  146. package/skills/melonjs-deployment/SKILL.md +113 -0
  147. package/skills/melonjs-effects-and-shaders/SKILL.md +177 -0
  148. package/skills/melonjs-events/SKILL.md +159 -0
  149. package/skills/melonjs-getting-started/SKILL.md +183 -0
  150. package/skills/melonjs-input/SKILL.md +166 -0
  151. package/skills/melonjs-lighting/SKILL.md +139 -0
  152. package/skills/melonjs-loading-assets/SKILL.md +157 -0
  153. package/skills/melonjs-particles-and-trails/SKILL.md +136 -0
  154. package/skills/melonjs-performance/SKILL.md +178 -0
  155. package/skills/melonjs-physics/SKILL.md +321 -0
  156. package/skills/melonjs-plugins/SKILL.md +191 -0
  157. package/skills/melonjs-renderables/SKILL.md +214 -0
  158. package/skills/melonjs-renderer-backends/SKILL.md +173 -0
  159. package/skills/melonjs-scenes-and-state/SKILL.md +180 -0
  160. package/skills/melonjs-sprites-and-animation/SKILL.md +173 -0
  161. package/skills/melonjs-tilemaps/SKILL.md +193 -0
  162. package/skills/melonjs-ui-and-text/SKILL.md +175 -0
  163. package/build/audio/backend.d.ts.map +0 -1
  164. package/build/loader/parsers/fetchdata.d.ts.map +0 -1
  165. /package/build/{loader/parsers → utils}/fetchdata.d.ts +0 -0
@@ -0,0 +1,166 @@
1
+ ---
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, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, click, touch, drag."
4
+ license: MIT
5
+ ---
6
+
7
+ # Input: keyboard, pointer and gamepad
8
+
9
+ ## Keyboard goes through named actions
10
+
11
+ Do not poll key codes. Bind a key to an **action string**, then poll the action —
12
+ this is what lets gamepad and virtual controls map onto the same handling.
13
+
14
+ ```js
15
+ import { input } from "melonjs";
16
+
17
+ input.bindKey(input.KEY.LEFT, "left");
18
+ input.bindKey(input.KEY.RIGHT, "right");
19
+ input.bindKey(input.KEY.X, "jump", true); // true = locked: one press per keydown
20
+
21
+ // in update()
22
+ if (input.isKeyPressed("left")) { /* … */ }
23
+ if (input.isKeyPressed("jump")) { /* fires once per press, not per frame */ }
24
+ ```
25
+
26
+ The third argument to `bindKey` is `lock` (default `false`). Without it,
27
+ `isKeyPressed` stays true for as long as the key is held — correct for movement,
28
+ wrong for jump or fire. A locked action stays consumed until the key is released
29
+ or you call `input.unlockKey("jump")`. Use `input.keyStatus(action)` when you
30
+ want the raw held/not-held state regardless of locking.
31
+
32
+ The full signature is `bindKey(keyCode, action, lock?, preventDefault?)`;
33
+ `preventDefault` defaults to `input.preventDefault` (itself `true`). Pass the
34
+ fourth argument per binding, or change the global default with
35
+ `input.setPreventDefault(false)` — **assigning `input.preventDefault = false`
36
+ does not work**, it is a read-only module binding.
37
+
38
+ ## Pointer events need `isKinematic = false`
39
+
40
+ **This is the trap.** `Renderable.isKinematic` defaults to `true`, and both the
41
+ broadphase and the pointer dispatcher skip any candidate whose `isKinematic` is
42
+ still `true`. The handler registers successfully and simply never fires.
43
+
44
+ ```js
45
+ class Button extends Renderable {
46
+ constructor(x, y, w, h) {
47
+ super(x, y, w, h);
48
+ this.isKinematic = false; // ← required
49
+ }
50
+ onActivateEvent() {
51
+ input.registerPointerEvent("pointerdown", this, this.onDown.bind(this));
52
+ }
53
+ onDeactivateEvent() {
54
+ input.releasePointerEvent("pointerdown", this);
55
+ }
56
+ }
57
+ ```
58
+
59
+ `Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*` elements set it
60
+ to `false` for you, and so does attaching a physics body (`Body`'s constructor
61
+ sets `ancestor.isKinematic = false`) — so plain `Renderable` and `Sprite`
62
+ subclasses with no body are the ones that catch people.
63
+
64
+ Register in `onActivateEvent` and release in `onDeactivateEvent`, not in the
65
+ constructor — pooled objects are reused and would otherwise accumulate handlers.
66
+
67
+ `app.viewport` works as a whole-screen region when you want global clicks: the
68
+ dispatcher appends it to the candidate list on every event, so it is reachable
69
+ even when nothing else is under the pointer.
70
+
71
+ The accepted event names are exactly `"pointerdown"`, `"pointerup"`,
72
+ `"pointermove"`, `"pointercancel"`, `"pointerenter"`, `"pointerover"`,
73
+ `"pointerleave"` and `"wheel"`. Anything else — `"click"`, `"mousedown"`,
74
+ `"touchstart"` — makes both `registerPointerEvent` and `releasePointerEvent`
75
+ throw `invalid event type`. melonJS maps the canonical name onto whichever
76
+ mouse/touch events the device actually supports, so you always register the
77
+ pointer name.
78
+
79
+ ## World coordinates versus screen coordinates
80
+
81
+ The pointer carries several, and picking the wrong one produces a feedback loop:
82
+
83
+ | property | meaning | use for |
84
+ |---|---|---|
85
+ | `pointer.gameScreenX` / `gameScreenY` | canvas-relative position | anything that **moves the camera** |
86
+ | `pointer.gameWorldX` / `gameWorldY` | projected into world space | picking and spawning objects |
87
+ | `pointer.gameLocalX` / `gameLocalY` | relative to the region's ancestor container | hit-testing inside a container |
88
+ | `pointer.gameX` / `gameY` | **context-dependent** — see below | region handlers on non-floating regions |
89
+
90
+ `gameX`/`gameY` is not a fixed coordinate space. The dispatcher rewrites it per
91
+ candidate just before calling your handler: world coordinates for a normal
92
+ region, screen coordinates for a `isFloating === true` region. On the global
93
+ `event.POINTERMOVE` broadcast it is set to the **screen** coordinates. When you
94
+ need one specific space, read `gameWorldX`/`gameWorldY` or
95
+ `gameScreenX`/`gameScreenY` by name rather than relying on `gameX`.
96
+
97
+ Using world coordinates for a camera-orbit drag means the same pixel maps to a
98
+ different world point every frame as the camera moves — the drag jumps wildly.
99
+
100
+ ## Pointer move and release outside a region
101
+
102
+ Region handlers only fire while the pointer is over the region. For drag
103
+ tracking, listen globally:
104
+
105
+ ```js
106
+ import { event } from "melonjs";
107
+ event.on(event.POINTERMOVE, (pointer) => { /* … */ });
108
+ ```
109
+
110
+ There is **no global pointer-up system event** — `event.ts` publishes only
111
+ `POINTERMOVE`, `POINTERLOCKCHANGE`, `DRAGSTART` and `DRAGEND`. For "released
112
+ anywhere" you need a raw listener:
113
+
114
+ ```js
115
+ globalThis.addEventListener("pointerup", onRelease);
116
+ ```
117
+
118
+ For plain drag-and-drop, `Draggable` / `DropTarget` already wire all of this up
119
+ (`pointerdown` / `pointerup` / `pointercancel` regions plus a global
120
+ `POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`. `DRAGEND`
121
+ still originates from a region handler, so it only fires when the release lands
122
+ on the draggable itself.
123
+
124
+ ## Gamepad
125
+
126
+ Gamepad input maps onto the same action strings, so game code does not branch:
127
+
128
+ ```js
129
+ input.bindGamepad(0,
130
+ { type: "buttons", code: input.GAMEPAD.BUTTONS.FACE_1 },
131
+ input.KEY.X); // → the "jump" action bound above
132
+ ```
133
+
134
+ **Order matters:** `bindGamepad` throws `no action defined for keycode N` if the
135
+ keycode has not already been passed to `bindKey`. Axes use the same call with a
136
+ threshold: `{ type: "axes", code: input.GAMEPAD.AXES.LX, threshold: -0.5 }`.
137
+ `unbindGamepad(index, button)`, `setGamepadDeadzone(value)` and
138
+ `setGamepadMapping(id, mapping)` complete the surface.
139
+
140
+ ## Virtual controls
141
+
142
+ On-screen buttons synthesise key events, again reaching the same actions:
143
+
144
+ ```js
145
+ input.triggerKeyEvent(input.KEY.LEFT, true); // press
146
+ input.triggerKeyEvent(input.KEY.LEFT, false); // release
147
+ ```
148
+
149
+ ## Symptom → cause
150
+
151
+ | symptom | cause |
152
+ |---|---|
153
+ | pointer handler never fires | `isKinematic` left at its `true` default |
154
+ | handlers fire twice, or on dead objects | registered in the constructor instead of `onActivateEvent` |
155
+ | camera drag jumps around | using world coordinates instead of `gameScreenX`/`gameScreenY` |
156
+ | `gameX` holds screen coords where world coords were expected | read on `event.POINTERMOVE`, or on a floating region — use `gameWorldX`/`gameWorldY` |
157
+ | drag stops when leaving the object | region events end at the region — use `event.POINTERMOVE` |
158
+ | release never detected outside the region | no global pointer-up event — add a raw listener |
159
+ | jump fires every frame while held | `bindKey` without its third `lock` argument |
160
+ | `invalid event type` thrown at registration | non-pointer name (`"click"`, `"mousedown"`, …) passed to `registerPointerEvent` |
161
+ | `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |
162
+
163
+ ## Related skills
164
+
165
+ - `melonjs-renderables` — `isKinematic` also gates the physics broadphase
166
+ - `melonjs-ui-and-text` — `UISpriteElement` and `UITextButton` handle this for you
@@ -0,0 +1,139 @@
1
+ ---
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 the ambient-cutout model, which parts need a GPU backend, and why Light2d does not survive a Camera3d. Triggers on: Light2d, lighting, ambientLight, ambientLightingColor, normalMap, SpriteIlluminator, per-pixel lighting, drawLight, torch, glow, darkness."
4
+ license: MIT
5
+ ---
6
+
7
+ # 2D dynamic lighting
8
+
9
+ > `Light2d` is a world renderable that cuts through a stage's ambient darkness.
10
+ > The glow and the cutout work on every backend; only per-pixel normal-map
11
+ > shading needs a GPU one. It is 2D-only.
12
+
13
+ ## The model: ambient darkness plus cutouts
14
+
15
+ The `Stage` holds an ambient colour — a dark sheet painted over the scene after
16
+ the world is drawn — and each active `Light2d` punches a hole in it at its
17
+ visible area. The lights themselves are drawn additively on top.
18
+
19
+ ```js
20
+ class PlayScreen extends Stage {
21
+ onResetEvent(app) {
22
+ this.ambientLight.parseCSS("#1a1a2e"); // the darkness
23
+
24
+ app.world.addChild(new Light2d(x, y, radiusX, radiusY, "#ffddaa", 0.9));
25
+ }
26
+ }
27
+ ```
28
+
29
+ The constructor is `Light2d(x, y, radiusX, radiusY = radiusX, color = "#FFF",
30
+ intensity = 0.7)`, and `pos` is the light's **centre** — its `anchorPoint` is
31
+ `(0.5, 0.5)`, and `centerX` / `centerY` are overridden to return `pos` directly.
32
+ Give `radiusY` a different value for a stretched, elliptical light.
33
+
34
+ A `Light2d` added to the world **registers itself with the stage's ambient
35
+ overlay on activate**, so you add it like any other renderable and it takes part
36
+ in the lighting pass. It draws additively (`blendMode = "lighter"`); set
37
+ `light.illuminationOnly = true` to suppress the visible glow while still lighting
38
+ normal-mapped sprites.
39
+
40
+ `Stage.ambientLight` defaults to fully transparent black, and `drawLighting()`
41
+ returns immediately while its alpha is 0 — so with no ambient colour set there
42
+ is nothing to cut through and the lights just look like soft additive blobs.
43
+
44
+ ## Multiple lights
45
+
46
+ Add as many as you need — the glow quads and the ambient cutouts accumulate with
47
+ no fixed cap.
48
+
49
+ The **normal-map** pipeline is capped: lights are packed into a std140 uniform
50
+ block sized for `MAX_LIGHTS = 32`, and anything past the 32nd is silently dropped
51
+ from the per-pixel shading (it still draws its glow and punches its cutout). The
52
+ cap is an allocation, not a budget — the fragment loop still runs once per pixel
53
+ per live light.
54
+
55
+ Lights are positioned like any renderable, so they can follow an entity by
56
+ updating `pos`, or be added as children of one.
57
+
58
+ ## Per-pixel lighting with normal maps
59
+
60
+ For sprites that respond to light direction rather than just brightness, supply a
61
+ normal map alongside the diffuse texture:
62
+
63
+ ```js
64
+ await loader.preload([
65
+ { name: "hero", type: "image", src: "data/img/hero.png" },
66
+ { name: "hero_n", type: "image", src: "data/img/hero_n.png" },
67
+ ]);
68
+
69
+ const hero = new Sprite(x, y, {
70
+ image: "hero",
71
+ normalMap: "hero_n", // ← enables per-pixel lighting on this sprite
72
+ framewidth: 32, frameheight: 32,
73
+ });
74
+ ```
75
+
76
+ `normalMap` takes a loader key, or any image-like source (`HTMLImageElement`,
77
+ canvas, `ImageBitmap`, a `Texture2d`). Assigning anything else — including an
78
+ `HTMLVideoElement`, which would freeze on frame 0 — throws a `TypeError`.
79
+
80
+ Normal maps are typically authored in **SpriteIlluminator**. The sidecar pattern
81
+ above — one `_n` image beside each diffuse image — is the simplest setup, but an
82
+ atlas can carry one too: `new TextureAtlas(json, image, { normalMap })` pairs a
83
+ normal texture sharing the colour texture's UVs, and a `Sprite` built from that
84
+ atlas picks it up in preference to its own `settings.normalMap`.
85
+
86
+ Unlit areas of a normal-mapped sprite render pure black unless you raise
87
+ `Stage.ambientLightingColor` (default black) — that is the base level added to
88
+ every lit pixel, and it is a different knob from `Stage.ambientLight`.
89
+
90
+ Without a normal map a sprite is still lit, but flatly: brightness varies with
91
+ distance from the light, not with surface direction.
92
+
93
+ ## What needs a GPU backend, and what does not
94
+
95
+ Less than you would expect. `Light2d` is renderer-agnostic: it just calls
96
+ `renderer.drawLight(this)`, and every backend implements it — the GPU backends
97
+ through a shared procedural radial-falloff shader, the Canvas renderer by
98
+ rasterising a cached `Gradient` into one shared render target and compositing it
99
+ with `drawImage`. The ambient cutout pass is plain `setMask` + `fillRect`. So the
100
+ lit-spot look works everywhere, Canvas included.
101
+
102
+ What Canvas cannot do is **per-pixel normal-map lighting**:
103
+ `renderer.setLightUniforms()` is a no-op there, and the first frame that passes a
104
+ non-empty light list logs a one-shot warning. Normal-mapped sprites simply render
105
+ unlit — no error.
106
+
107
+ ```js
108
+ if (app.renderer.shaderLanguage === null) {
109
+ // Canvas fallback: lights and ambient still work, but `normalMap` is ignored
110
+ }
111
+ ```
112
+
113
+ ## Do not combine with `Camera3d`
114
+
115
+ `Camera3d` extends `Camera2d` and therefore still runs the 2D lighting pass, but
116
+ nothing in that pass is projection-aware: the ambient fill is a screen-space
117
+ `fillRect`, the cutout is a 2D `Ellipse` mask, and light positions are packed as
118
+ 2D world coordinates minus the camera translate. Under a perspective projection
119
+ none of that lines up with what the camera actually draws.
120
+
121
+ For 3D scenes use `Light3d` instead — it is the counterpart for the mesh
122
+ lighting path, with `"directional"`, `"ambient"`, `"point"` and `"spot"` types.
123
+ See the `melonjs-3d` skill.
124
+
125
+ ## Symptom → cause
126
+
127
+ | symptom | cause |
128
+ |---|---|
129
+ | lights added but the scene never darkens | no ambient colour set — `ambientLight` alpha is 0, so the pass returns early |
130
+ | sprite lit flatly, ignoring light direction | no `normalMap` on the sprite, or the Canvas backend is active |
131
+ | normal-mapped sprite is pure black where unlit | `Stage.ambientLightingColor` left at black |
132
+ | the 33rd light shades nothing | `MAX_LIGHTS = 32` for the normal-map pipeline |
133
+ | `TypeError` assigning `normalMap` | not an image-like value (a video element is rejected outright) |
134
+ | lighting misaligned under a 3D camera | the 2D light pass is not projection-aware — use `Light3d` |
135
+
136
+ ## Related skills
137
+
138
+ - `melonjs-3d` — `Light3d` for perspective scenes
139
+ - `melonjs-sprites-and-animation` — sprite settings and atlases
@@ -0,0 +1,157 @@
1
+ ---
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, asset, preload, LOADER_COMPLETE, loading screen, unload."
4
+ license: MIT
5
+ ---
6
+
7
+ # Loading assets
8
+
9
+ ## The resource descriptor
10
+
11
+ Every asset is `{ name, type, src }` plus type-specific extras:
12
+
13
+ ```js
14
+ import { loader } from "melonjs";
15
+
16
+ loader.setOptions({ crossOrigin: "anonymous" }); // needed for remote assets
17
+
18
+ await loader.preload([
19
+ { name: "tiles", type: "image", src: "data/img/tiles.png" },
20
+ { name: "map1", type: "tmx", src: "data/map/map1.tmx" },
21
+ { name: "tileset", type: "tsx", src: "data/map/tileset.tsx" },
22
+ { name: "sfx", type: "audio", src: "data/sfx/" },
23
+ { name: "font", type: "fontface", src: "data/font/PressStart2P.ttf" },
24
+ { name: "atlas", type: "json", src: "data/img/atlas.json" },
25
+ ]);
26
+ ```
27
+
28
+ `preload()` has been awaitable since 19.8. The callback form still works and
29
+ takes a third argument to suppress the built-in loading screen:
30
+
31
+ ```js
32
+ loader.preload(resources, onLoaded, /* switchToLoadState */ false);
33
+ ```
34
+
35
+ ## The type list
36
+
37
+ The type strings are melonJS-specific — this is not a generic loader:
38
+
39
+ | type | for |
40
+ |---|---|
41
+ | `"image"` | textures, sprite sheets, normal maps |
42
+ | `"json"` | data, texture-atlas descriptors |
43
+ | `"tmx"` / `"tsx"` | Tiled maps and external tilesets |
44
+ | `"audio"` | sound — **`src` is a directory**, see below |
45
+ | `"video"` | video textures |
46
+ | `"binary"` | raw data, bitmap font `.fnt` descriptors |
47
+ | `"fontface"` | web fonts (`.ttf`, `.woff`) |
48
+ | `"aseprite"` | Aseprite JSON + image |
49
+ | `"shader"` | GLSL/WGSL shader sources |
50
+ | `"obj"` / `"mtl"` | Wavefront models and materials |
51
+ | `"gltf"` / `"glb"` | glTF scenes |
52
+ | `"js"` | scripts |
53
+
54
+ ## Two conventions that produce 404s
55
+
56
+ **Audio `src` is a directory.** The loader appends the asset name plus each
57
+ format from `audio.init()`, and picks the first that decodes:
58
+
59
+ ```js
60
+ audio.init("mp3,ogg"); // before preloading audio
61
+ { name: "cling", type: "audio", src: "data/sfx/" } // → data/sfx/cling.mp3
62
+ ```
63
+
64
+ A full filename here gives you a 404. And `audio.init()` must run **before** any
65
+ audio asset is preloaded.
66
+
67
+ **A Tiled map needs its dependencies listed too** — the `.tmx`, any external
68
+ `.tsx`, and the tileset images. Listing only the map produces a blank level.
69
+
70
+ ## Retrieving loaded assets
71
+
72
+ ```js
73
+ loader.getImage("tiles");
74
+ loader.getJSON("atlas");
75
+ loader.getTMX("map1");
76
+ loader.getGLTF("diorama");
77
+ loader.getVideo("intro");
78
+ loader.getFont("font");
79
+ loader.getBinary("font-fnt");
80
+ ```
81
+
82
+ Most engine classes take the asset **name** directly, so you often do not need
83
+ these — `new Sprite(x, y, { image: "tiles" })` resolves it for you.
84
+
85
+ ## Base URLs
86
+
87
+ Set a prefix per asset type rather than repeating it:
88
+
89
+ ```js
90
+ loader.setBaseURL("image", "data/img/");
91
+ loader.setBaseURL("audio", "data/sfx/");
92
+ loader.setBaseURL("*", "https://cdn.example.com/"); // every 2D type at once
93
+ ```
94
+
95
+ The `"*"` wildcard deliberately **skips `obj`, `mtl`, `gltf` and `glb`**: those
96
+ resolve their own internal references (a `map_Kd` texture, a glTF `.bin`)
97
+ relative to their file, so a global prefix would double-prefix them. Set those
98
+ types individually.
99
+
100
+ ## Progress and completion
101
+
102
+ Use the events. The old callback properties (`loader.onload`, `onProgress`,
103
+ `onError`) were deprecated in 18.2 and **removed in 20.3** — being `let`
104
+ bindings on an ES module namespace they could never be assigned from outside in
105
+ the first place, so `loader.onProgress = fn` always threw a `TypeError`:
106
+
107
+ ```js
108
+ import { event } from "melonjs";
109
+
110
+ event.on(event.LOADER_PROGRESS, (progress) => { /* 0..1 */ });
111
+ event.on(event.LOADER_COMPLETE, () => { /* … */ });
112
+ event.on(event.LOADER_ERROR, (res) => { /* … */ });
113
+ ```
114
+
115
+ ## Unloading
116
+
117
+ ```js
118
+ loader.unload({ name: "map1", type: "tmx" });
119
+ loader.unloadAll();
120
+ ```
121
+
122
+ `unloadAll()` also releases audio and GPU resources such as shader programs, so
123
+ it is the right teardown for switching between large levels.
124
+
125
+ ## Loading between scenes
126
+
127
+ The default loading screen appears automatically when `preload` switches state.
128
+ For a custom one, register a `Stage` against `state.LOADING`:
129
+
130
+ ```js
131
+ state.set(state.LOADING, new MyLoadingScreen());
132
+ ```
133
+
134
+ If you preload without a loading stage and then change state immediately, pass
135
+ `forceChange`:
136
+
137
+ ```js
138
+ state.change(state.PLAY, true);
139
+ ```
140
+
141
+ ## Symptom → cause
142
+
143
+ | symptom | cause |
144
+ |---|---|
145
+ | audio 404s | `src` given as a file instead of a directory |
146
+ | audio fails, `preload()` rejects with "Failed loading resource" | `audio.init()` not called before preloading — the real message is replaced by the loader's |
147
+ | blank level, missing tiles | `.tsx` or tileset image not listed in the resources |
148
+ | cross-origin textures fail | missing `loader.setOptions({ crossOrigin: "anonymous" })` |
149
+ | text renders in a fallback font | web font not preloaded as `"fontface"` |
150
+ | `BitmapText` renders nothing | the `.fnt` (as `"binary"`) or its image is missing |
151
+ | `TypeError` assigning `loader.onProgress` / `onload` | removed in 20.3; they were never writable — use the `LOADER_*` events |
152
+
153
+ ## Related skills
154
+
155
+ - `melonjs-getting-started` — where preloading sits in the bootstrap
156
+ - `melonjs-tilemaps` — the full Tiled asset set
157
+ - `melonjs-audio` — formats and the directory convention
@@ -0,0 +1,136 @@
1
+ ---
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, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: ParticleEmitter, particles, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, Trail, explosion, smoke, sparks, exhaust, emitter."
4
+ license: MIT
5
+ ---
6
+
7
+ # Particles and trails
8
+
9
+ ## An emitter does nothing until you tell it to emit
10
+
11
+ Adding a `ParticleEmitter` to the world is not enough. Nothing appears, and
12
+ nothing warns you.
13
+
14
+ ```js
15
+ const emitter = new ParticleEmitter(x, y, {
16
+ image: loader.getImage("spark"),
17
+ totalParticles: 60,
18
+ speed: 4,
19
+ minLife: 200, maxLife: 800,
20
+ });
21
+
22
+ world.addChild(emitter);
23
+ emitter.streamParticles(); // continuous — or:
24
+ emitter.burstParticles(); // one shot
25
+ ```
26
+
27
+ `streamParticles(ms)` takes an optional run time; without one it uses the
28
+ `duration` setting, which defaults to `Infinity` — a stream runs until
29
+ `stopStream()`. `burstParticles(n)` launches `n`, or `totalParticles`.
30
+
31
+ For one-shot effects, `autoDestroyOnComplete: true` removes the emitter once its
32
+ particles have finished. The `onComplete` callback fires at that same moment
33
+ either way.
34
+
35
+ For repeated effects (muzzle flashes, impacts) keep **one** long-lived emitter
36
+ and re-aim it per shot, rather than allocating an emitter per event.
37
+
38
+ ## `referenceSpace` — which frame particles live in
39
+
40
+ Added in 20.2. This decides what a particle's position is measured *from*, and
41
+ it is the difference between an effect that trails behind a moving object and
42
+ one that drags along with it.
43
+
44
+ | value | particles are measured from | use for |
45
+ |---|---|---|
46
+ | `"local"` (default) | the emitter | flames, auras, anything welded to the object |
47
+ | `"world"` | the container the emitter sits in | **trails, smoke, exhaust, dust** |
48
+ | a `Container` | that container | a moving frame — snow inside a moving carriage |
49
+
50
+ ```js
51
+ // exhaust that stays where it was emitted, so the ship flies away from it
52
+ const exhaust = new ParticleEmitter(x, y, { referenceSpace: "world", /* … */ });
53
+ ```
54
+
55
+ With the default `"local"`, a moving emitter carries its whole cloud along —
56
+ which is right for a torch flame and wrong for a smoke trail. Changing it at
57
+ runtime re-bases live particles, so nothing jumps.
58
+
59
+ ## Particle tuning changed in 20.2
60
+
61
+ The 20.2 particle transform fix corrected a drift where drawn positions ran past
62
+ simulated ones. Particles now travel roughly **half as far** over their life as
63
+ they did before.
64
+
65
+ Consequence: **speed values tuned on 19.x or earlier look wrong on 20.2+.** If
66
+ you are copying emitter settings from older code or older tutorials, expect to
67
+ raise `speed` or `maxLife`.
68
+
69
+ ## Trails
70
+
71
+ `Trail` is a ribbon that follows a target — use it rather than hand-rolling a
72
+ position history. **Its constructor takes one argument, the options object** —
73
+ no `x, y` pair like every other renderable:
74
+
75
+ ```js
76
+ const trail = new Trail({
77
+ target: sprite, // a renderable or a plain Vector2d
78
+ length: 24, // max points kept (default 20)
79
+ width: 20, // max ribbon width in px (default 10)
80
+ lifetime: 500, // per-point lifetime in ms (default 500)
81
+ widthCurve: [1, 0], // 0 = head, 1 = tail
82
+ gradient: ["#fff", "#f80", "#f000"],
83
+ });
84
+ trail.blendMode = "additive";
85
+ world.addChild(trail, 5);
86
+ ```
87
+
88
+ Omit `target` and feed it yourself with `trail.addPoint(x, y)` — the mode for a
89
+ sword slash, where the ribbon follows a weapon tip rather than an object. Points
90
+ closer together than `minDistance` (default 4 px) are dropped.
91
+
92
+ **`Trail` is 2D.** `addPoint` takes `(x, y)`, points are stored without a `z`,
93
+ and the ribbon is drawn as connected screen-space quads. Under a `Camera3d` it
94
+ draws a flat ribbon that does not recede with perspective — so it is the wrong
95
+ tool for a trail behind an object moving *into* the scene. For that:
96
+
97
+ - **`ParticleEmitter` with `referenceSpace: "world"`** — particles stay where
98
+ they were dropped while the emitter moves on, and the emitter propagates its
99
+ depth to each particle so `Camera3d` projects them correctly. This is the
100
+ built-in answer for exhaust, ski tracks and speed trails in 3D.
101
+ - **A procedural `Mesh`** built from the object's recent path, with
102
+ `mesh.vertexColors` for a per-vertex gradient — more work, but a continuous
103
+ ribbon rather than discrete puffs.
104
+
105
+ ## Performance
106
+
107
+ - Particles come from a shared pool, so a steady-state emitter allocates
108
+ nothing. Each emitter is still a `Container` that updates and draws every
109
+ frame, so one busy emitter beats many idle ones.
110
+ - `totalParticles` (default 50) is a cap on how many are alive at once in stream
111
+ mode; `burstParticles()` launches that many in one go, or the count you pass.
112
+ - `framesToSkip` (default 0) skips n updates between simulation steps — the
113
+ cheapest lever on an emitter with many particles.
114
+ - Blending is per particle, set through the `blendMode` setting or by assigning
115
+ `emitter.blendMode` (which reaches the particles already alive as well as
116
+ later ones). Additive is cheap everywhere: fixed-function on both GPU
117
+ backends, native `lighter` on Canvas. The expensive modes are the advanced CSS
118
+ ones (`overlay`, `soft-light`, `color-dodge`, `darken`, …), which on WebGL 2
119
+ and WebGPU cost a destination capture plus a shader composite *per blended
120
+ draw*.
121
+
122
+ ## Symptom → cause
123
+
124
+ | symptom | cause |
125
+ |---|---|
126
+ | trail looks flat under a `Camera3d` | `Trail` is 2D — use a world-space emitter or a ribbon mesh |
127
+ | emitter added but nothing appears | `streamParticles()` / `burstParticles()` never called |
128
+ | the cloud follows a moving emitter | default `referenceSpace: "local"` — use `"world"` for trails |
129
+ | particles do not travel far enough | settings tuned pre-20.2, when drawn travel was doubled |
130
+ | one-shot emitters accumulate | missing `autoDestroyOnComplete: true` |
131
+ | frame rate drops with many effects | an emitter per event instead of one re-aimed emitter |
132
+
133
+ ## Related skills
134
+
135
+ - `melonjs-renderables` — draw order and blend modes
136
+ - `melonjs-effects-and-shaders` — blend mode cost on each backend