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,159 @@
1
+ ---
2
+ name: melonjs-events
3
+ description: "Use this skill for the melonJS event bus and lifecycle hooks — subscribing to engine events, the per-frame and per-state event names, renderable lifecycle callbacks, and how to unsubscribe without leaking. Triggers on: event.on, event.off, event.once, event.emit, GAME_UPDATE, GAME_BEFORE_DRAW, LEVEL_LOADED, LOADER_COMPLETE, STATE_CHANGE, CANVAS_ONRESIZE, POINTERMOVE, WORLD_STEP, onActivateEvent, onDeactivateEvent, onDestroyEvent, lifecycle, subscribe, listener."
4
+ license: MIT
5
+ ---
6
+
7
+ # Events and lifecycle
8
+
9
+ ## The bus
10
+
11
+ ```js
12
+ import { event } from "melonjs";
13
+
14
+ const onUpdate = () => { /* … */ };
15
+ const unsubscribe = event.on(event.GAME_UPDATE, onUpdate); // on() returns its own off()
16
+ event.once(event.LEVEL_LOADED, () => { /* … */ });
17
+
18
+ // two ways to undo it — the same reference, or the returned function
19
+ event.off(event.GAME_UPDATE, onUpdate);
20
+ unsubscribe();
21
+ ```
22
+
23
+ `event.on`/`once`/`off`/`has` all take an optional third `context` argument,
24
+ invoked as the handler's `this`. **`off` matches on the pair — handler identity
25
+ AND context identity.** `on(EVT, fn, this)` followed by `off(EVT, fn)` removes
26
+ nothing, silently.
27
+
28
+ **Store the handler reference, or use the context argument.** An inline arrow or
29
+ a fresh `.bind(this)` in the `on` call can never be removed — that is the
30
+ standard way to leak listeners in melonJS.
31
+
32
+ ```js
33
+ // ✗ unremovable
34
+ event.on(event.GAME_UPDATE, this.tick.bind(this));
35
+
36
+ // ✓ keep the bound reference
37
+ this._tick = this.tick.bind(this);
38
+ event.on(event.GAME_UPDATE, this._tick);
39
+ // later: event.off(event.GAME_UPDATE, this._tick);
40
+
41
+ // ✓ or skip the bind entirely — the context is part of the identity
42
+ event.on(event.GAME_UPDATE, this.tick, this);
43
+ // later: event.off(event.GAME_UPDATE, this.tick, this);
44
+ ```
45
+
46
+ Custom event names work at runtime (`event.emit("my.custom.event", payload)`),
47
+ but `on`/`emit` are typed against a closed map of the engine's own events, so in
48
+ TypeScript a name outside that map is a compile error. Use your own emitter for
49
+ game events in a `.ts` project.
50
+
51
+ ## The events worth knowing
52
+
53
+ **Per frame**
54
+
55
+ | event | when |
56
+ |---|---|
57
+ | `GAME_BEFORE_UPDATE` / `GAME_UPDATE` / `GAME_AFTER_UPDATE` | around the world update — `GAME_UPDATE` is **not** emitted while paused |
58
+ | `GAME_BEFORE_DRAW` / `GAME_AFTER_DRAW` | around the draw pass |
59
+ | `WORLD_STEP` | after the physics step |
60
+ | `TICK` | the raw engine tick |
61
+
62
+ **Lifecycle**
63
+
64
+ | event | when |
65
+ |---|---|
66
+ | `BOOT`, `GAME_INIT`, `VIDEO_INIT` | engine start-up |
67
+ | `STATE_CHANGE`, `STATE_PAUSE`, `STATE_RESUME`, `STATE_STOP` | state machine |
68
+ | `STAGE_RESET` | a stage was reset |
69
+ | `LEVEL_LOADED` | a Tiled level finished loading |
70
+ | `LOADER_PROGRESS`, `LOADER_COMPLETE`, `LOADER_ERROR` | asset loading |
71
+ | `BLUR` / `FOCUS` | the page lost or regained focus |
72
+ | `CANVAS_ONRESIZE`, `VIEWPORT_ONRESIZE` | relayout hooks |
73
+ | `POINTERMOVE` | global pointer movement |
74
+
75
+ Use `LOADER_PROGRESS` / `LOADER_COMPLETE`. The `loader.onProgress` /
76
+ `loader.onload` / `loader.onError` properties were deprecated in 18.2.0 and
77
+ removed in 20.3: they were module-level `let` bindings on an ES module
78
+ namespace, so `loader.onProgress = fn` threw and the documented migration path
79
+ never worked. For completion, `loader.preload(assets, onloadcb)` also takes the
80
+ callback directly.
81
+
82
+ ## Renderable lifecycle callbacks
83
+
84
+ Prefer these over bus subscriptions where they fit — the engine calls them at the
85
+ right times and there is nothing to unsubscribe:
86
+
87
+ | callback | when |
88
+ |---|---|
89
+ | `onActivateEvent()` | `addChild`-ed into a container that is already attached to the world root (a `Container` forwards it to its children) |
90
+ | `onDeactivateEvent()` | removed from its container — including the automatic world clear on a state change |
91
+ | `onDestroyEvent()` | `destroy()` ran; called with no arguments from the container-removal path |
92
+ | `onCollisionStart` / `onCollisionActive` / `onCollisionEnd` | contacts |
93
+
94
+ Note the activate gate: adding a child to a *detached* container fires nothing.
95
+ The callback arrives later, when that container itself is added to the world.
96
+ And an `isPersistent` renderable survives the world clear between stages, so it
97
+ gets no fresh `onActivateEvent` on the next state.
98
+
99
+ **Pair subscriptions with activate/deactivate, not the constructor.** This is
100
+ the rule that keeps pooled objects working:
101
+
102
+ ```js
103
+ onActivateEvent() {
104
+ this._tick = this.update.bind(this);
105
+ event.on(event.GAME_UPDATE, this._tick);
106
+ input.registerPointerEvent("pointerdown", this, this._onDown);
107
+ }
108
+ onDeactivateEvent() {
109
+ event.off(event.GAME_UPDATE, this._tick);
110
+ input.releasePointerEvent("pointerdown", this);
111
+ }
112
+ ```
113
+
114
+ **Pool-recycled objects never fire `onDestroyEvent` on removal** — `removeChildNow`
115
+ tries `pool.push(child)` first and only calls `destroy()` if that fails, so a
116
+ class registered with `pool.register(name, Class, true)` goes back to the pool
117
+ instead. Anything you clean up there will not run. `destroy()` is also what calls
118
+ `releaseAllPointerEvents(this)`, which is why a pooled object must release its
119
+ own pointer registrations in `onDeactivateEvent`.
120
+
121
+ ## Guard callbacks that can fire during teardown
122
+
123
+ Events can arrive while a stage is being dismantled, when the current state is
124
+ already something else:
125
+
126
+ ```js
127
+ if (!state.isCurrent(state.PLAY)) return;
128
+ ```
129
+
130
+ Without this, a "children emptied" handler can reset the wrong stage.
131
+
132
+ ## Removing objects from a collision callback
133
+
134
+ Contact callbacks fire inline inside the physics step, mid-iteration over the
135
+ detector's pair list. `container.removeChild()` is safe there — it defers the
136
+ actual removal to the next tick, which is exactly why it defers. The immediate
137
+ forms are the dangerous ones: `removeChildNow()` and `destroy()` tear the object
138
+ down under the iteration (`destroy()` nulls `pos` and `body`). The builtin
139
+ detector guards the pair it is currently resolving, but nothing guarantees that
140
+ for the rest of the step or for another adapter.
141
+
142
+ Use `removeChild()`, or flag the work and drain it from `Stage.update`.
143
+
144
+ ## Symptom → cause
145
+
146
+ | symptom | cause |
147
+ |---|---|
148
+ | handler still fires after removing the object | `event.off` given a different function reference, or a different `context` than `on` got |
149
+ | handlers accumulate on pooled objects | subscribed in the constructor rather than `onActivateEvent` |
150
+ | cleanup never runs on a pooled object | pooled removal does not fire `onDestroyEvent` |
151
+ | crash during a stage switch | callback ran mid-teardown — guard with `state.isCurrent` |
152
+ | crash after a collision | `removeChildNow()` / `destroy()` inside a contact callback — use `removeChild()` |
153
+ | progress callbacks never fire | assigned `loader.onProgress` (removed in 20.3) instead of subscribing to `LOADER_PROGRESS` |
154
+ | `onActivateEvent` never runs | the child was added to a container not yet attached to the world |
155
+
156
+ ## Related skills
157
+
158
+ - `melonjs-scenes-and-state` — the state machine and stage lifecycle
159
+ - `melonjs-performance` — pooling, and why activate/deactivate pairing matters
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: melonjs-getting-started
3
+ description: "Use this skill when starting or bootstrapping a melonJS game, creating the Application, choosing a renderer, or setting up the first scene. Covers the mandatory async init, the world container, adding renderables, and the WebGPU/WebGL 2/Canvas fallback. Triggers on: melonjs, new Application, app.init, app.world, video.AUTO, video.WEBGL, video.CANVAS, Stage, state.change, getting started, bootstrap."
4
+ license: MIT
5
+ ---
6
+
7
+ # Getting started with melonJS
8
+
9
+ > Bootstrapping a melonJS 20.x game: the `Application` lifecycle, the mandatory
10
+ > `await app.init()`, the world container, and what the renderer fallback means
11
+ > for feature availability.
12
+
13
+ **Applies to:** melonJS 20.x. Older major versions differ — see the
14
+ `melonjs-20-migration` skill.
15
+
16
+ ## The bootstrap
17
+
18
+ `Application` construction is synchronous but **initialisation is not**. `init()`
19
+ returns a promise and must be awaited before building any renderable, loading
20
+ assets, or entering a scene.
21
+
22
+ ```js
23
+ import { Application, Text } from "melonjs";
24
+
25
+ const app = new Application(1218, 562, {
26
+ parent: "screen", // id of the DOM element to mount into
27
+ scale: "auto",
28
+ backgroundColor: "#202020",
29
+ });
30
+ await app.init(); // ← REQUIRED. Nothing below works without it.
31
+
32
+ app.world.addChild(
33
+ new Text(609, 281, {
34
+ font: "Arial",
35
+ size: 160,
36
+ fillStyle: "#FFFFFF",
37
+ textAlign: "center",
38
+ textBaseline: "middle",
39
+ text: "Hello World !",
40
+ }),
41
+ );
42
+ ```
43
+
44
+ **This is the single most common thing to get wrong.** On 19.x the constructor
45
+ called `init(width, height, options)` for you; on 20.x `init()` takes no
46
+ arguments and calling — and awaiting — it is your job. Nothing that follows
47
+ names the missing call. The constructor does build `app.world`, so adding a
48
+ plain renderable succeeds and simply never draws — no canvas is appended and the
49
+ app never subscribes to the frame tick — while constructing a `Sprite` or a
50
+ `Text` throws `Cannot read properties of undefined (reading 'renderer')`,
51
+ because the global `game` those two fall back on is only set once the first
52
+ `init()` resolves. Either way `app.renderer` and `app.viewport` stay `undefined`
53
+ and the page stays blank.
54
+
55
+ `init()` is asynchronous because acquiring a WebGPU device is asynchronous. There
56
+ is no synchronous alternative.
57
+
58
+ ## Top-level await
59
+
60
+ The bootstrap needs `await` at module scope. In an ES module that is fine. If the
61
+ surrounding tooling does not allow it, wrap it:
62
+
63
+ ```js
64
+ async function main() {
65
+ const app = new Application(800, 600, { parent: "screen" });
66
+ await app.init();
67
+ // …
68
+ }
69
+ void main();
70
+ ```
71
+
72
+ ## Choosing a renderer
73
+
74
+ `video.AUTO` (the default) tries **WebGPU → WebGL 2 → Canvas** and uses the first
75
+ that works.
76
+
77
+ ```js
78
+ import { video } from "melonjs";
79
+
80
+ const app = new Application(800, 600, {
81
+ parent: "screen",
82
+ renderer: video.AUTO, // default; or video.WEBGPU / video.WEBGL / video.CANVAS
83
+ });
84
+ ```
85
+
86
+ Two consequences worth knowing before writing rendering code:
87
+
88
+ - **Which backend you get is a runtime fact, not a build-time one.** The same
89
+ code runs on a different renderer depending on the machine. Do not assume
90
+ WebGL.
91
+ - **Not every feature exists on every backend.** Shader effects and post effects
92
+ need a programmable pipeline and are inert on Canvas. Where a feature degrades,
93
+ the engine warns once rather than throwing — so a missing effect is a console
94
+ warning, not a crash.
95
+
96
+ Pin the renderer explicitly (`video.WEBGL`) only when you have a reason to. It
97
+ costs you the WebGPU path and the Canvas fallback.
98
+
99
+ ## The scene graph
100
+
101
+ `app.world` is the root `Container`. Everything drawn is a descendant of it.
102
+
103
+ ```js
104
+ app.world.addChild(sprite, 10); // 10 is the z / draw order
105
+ ```
106
+
107
+ **Pass the z-order as the second argument to `addChild`** — it is the only form
108
+ that works. `renderable.z` does not exist as a property, setting `depth` before
109
+ `addChild` is overwritten by `autoDepth`, and setting it afterwards does not
110
+ resort. See the `melonjs-renderables` skill for the full explanation.
111
+
112
+ ## Scenes
113
+
114
+ For anything beyond a single screen, use `Stage` subclasses and the `state`
115
+ manager rather than mutating `app.world` directly:
116
+
117
+ ```js
118
+ import { Stage, state } from "melonjs";
119
+
120
+ class PlayScreen extends Stage {
121
+ onResetEvent() { /* build the scene */ }
122
+ onDestroyEvent() { /* tear it down */ }
123
+ }
124
+
125
+ state.set(state.PLAY, new PlayScreen());
126
+ state.change(state.PLAY);
127
+ ```
128
+
129
+ `onResetEvent` runs every time the state is entered, not once — build scene
130
+ content there, not in the constructor.
131
+
132
+ ## Loading assets
133
+
134
+ Assets are declared and preloaded through `loader`; audio needs `audio.init()`
135
+ with the formats you ship before any audio asset is preloaded.
136
+
137
+ ```js
138
+ import { audio, loader } from "melonjs";
139
+
140
+ loader.setOptions({ crossOrigin: "anonymous" }); // needed for remote assets
141
+ audio.init("mp3,ogg"); // BEFORE preloading audio
142
+ await loader.preload([
143
+ { name: "tileset", type: "image", src: "data/img/tileset.png" },
144
+ { name: "jump", type: "audio", src: "data/sfx/" }, // directory, not file
145
+ ]);
146
+ ```
147
+
148
+ Two ordering rules that produce confusing failures when missed:
149
+
150
+ - **`audio.init()` must run before any audio asset is preloaded**, and names the
151
+ formats you actually ship. Preloading first throws *"target audio extension(s)
152
+ should be set through me.audio.init() before calling the preloader"*.
153
+ - The audio `src` is a **directory**, not a file. The loader appends the asset
154
+ `name` plus each configured extension, so a full filename gives you a 404.
155
+
156
+ ## `anchorPoint` defaults to the centre
157
+
158
+ `Renderable.anchorPoint` is `(0.5, 0.5)`, so `pos` is an object's **centre**, not
159
+ its top-left. `Container` forces `(0, 0)` instead.
160
+
161
+ Writing top-left math against a centred anchor puts everything half a sprite off
162
+ with no error — it is the most common source of "close but subtly wrong"
163
+ placement.
164
+
165
+ ## Common mistakes
166
+
167
+ | symptom | cause |
168
+ |---|---|
169
+ | blank page, nothing drawn, or a `TypeError` reading `renderer` from `new Sprite` / `new Text` | missing `await app.init()` |
170
+ | objects draw in the wrong order | `.z`/`depth` set instead of `addChild(child, z)` — `.z` is not even a real property |
171
+ | shader/post effect does nothing, warning in console | running on the Canvas renderer |
172
+ | audio 404s | audio asset `src` given as a file rather than a directory |
173
+ | scene content appears once and never resets | scene built in the constructor instead of `onResetEvent` |
174
+ | everything offset by half a sprite | top-left math against the centred `anchorPoint` default |
175
+ | a `Tween` or `ParticleEmitter` does nothing | `.start()` / `streamParticles()` never called |
176
+ | cross-origin textures fail to load | missing `loader.setOptions({ crossOrigin: "anonymous" })` |
177
+
178
+ ## Related skills
179
+
180
+ - `melonjs-20-migration` — what changed in 20.x that older code and older
181
+ training data get wrong
182
+ - `melonjs-renderables` — custom `Renderable` subclasses, pointer events, post
183
+ effects and the traps in each
@@ -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