melonjs 20.4.0 → 20.6.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.
- package/build/application/defaultApplicationSettings.d.ts +1 -0
- package/build/application/defaultApplicationSettings.d.ts.map +1 -1
- package/build/application/settings.d.ts +37 -0
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/backend/sound.d.ts.map +1 -1
- package/build/audio/procedural.d.ts +7 -2
- package/build/audio/procedural.d.ts.map +1 -1
- package/build/audio/types.d.ts +22 -0
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera2d.d.ts +28 -0
- package/build/camera/camera2d.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts +24 -3
- package/build/camera/camera3d.d.ts.map +1 -1
- package/build/geometries/polygon.d.ts +7 -1
- package/build/geometries/polygon.d.ts.map +1 -1
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1379 -226
- package/build/index.js.map +4 -4
- package/build/level/gltf/GLTFModel.d.ts +50 -7
- package/build/level/gltf/GLTFModel.d.ts.map +1 -1
- package/build/lighting/light3d.d.ts +20 -47
- package/build/lighting/light3d.d.ts.map +1 -1
- package/build/loader/loader.d.ts +106 -6
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/loadingscreen.d.ts.map +1 -1
- package/build/renderable/animation.d.ts +21 -6
- package/build/renderable/animation.d.ts.map +1 -1
- package/build/renderable/container.d.ts +27 -5
- package/build/renderable/container.d.ts.map +1 -1
- package/build/renderable/instanced_mesh.d.ts +34 -9
- package/build/renderable/instanced_mesh.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +131 -94
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts +17 -0
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite3d.d.ts +12 -2
- package/build/renderable/sprite3d.d.ts.map +1 -1
- package/build/renderable/text/bitmaptext.d.ts +32 -7
- package/build/renderable/text/bitmaptext.d.ts.map +1 -1
- package/build/renderable/text/text.d.ts +78 -3
- package/build/renderable/text/text.d.ts.map +1 -1
- package/build/renderable/text/textmetrics.d.ts +2 -0
- package/build/renderable/text/textmetrics.d.ts.map +1 -1
- package/build/renderable/text/textstyle.d.ts.map +1 -1
- package/build/state/stage.d.ts +21 -0
- package/build/state/stage.d.ts.map +1 -1
- package/build/system/device.d.ts +1 -1
- package/build/system/device.d.ts.map +1 -1
- package/build/system/save.d.ts +44 -1
- package/build/system/save.d.ts.map +1 -1
- package/build/video/effects/glsl_realization.d.ts.map +1 -1
- package/build/video/effects/mesh_splice.d.ts +2 -0
- package/build/video/effects/mesh_splice.d.ts.map +1 -0
- package/build/video/effects/shadereffect.d.ts +12 -128
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/effects/wgsl_realization.d.ts.map +1 -1
- package/build/video/gradient.d.ts +19 -0
- package/build/video/gradient.d.ts.map +1 -1
- package/build/video/renderer.d.ts +44 -0
- package/build/video/renderer.d.ts.map +1 -1
- package/build/video/texture/noise_texture2d.d.ts +101 -34
- package/build/video/texture/noise_texture2d.d.ts.map +1 -1
- package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
- package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
- package/build/video/webgl/glshader.d.ts +7 -2
- package/build/video/webgl/glshader.d.ts.map +1 -1
- package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
- package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
- package/build/video/webgpu/batchers/mesh_batcher.d.ts +6 -1
- package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
- package/build/video/webgpu/effect_binding.d.ts.map +1 -1
- package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +1 -0
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/melonjs-3d/SKILL.md +152 -3
- package/skills/melonjs-3d-assets/SKILL.md +139 -0
- package/skills/melonjs-audio/SKILL.md +17 -0
- package/skills/melonjs-camera-and-drawing/SKILL.md +45 -0
- package/skills/melonjs-effects-and-shaders/SKILL.md +130 -1
- package/skills/melonjs-loading-assets/SKILL.md +131 -1
- package/skills/melonjs-performance/SKILL.md +83 -1
- package/skills/melonjs-physics/SKILL.md +78 -0
- package/skills/melonjs-renderables/SKILL.md +63 -1
- package/skills/melonjs-renderer-backends/SKILL.md +19 -0
- package/skills/melonjs-scenes-and-state/SKILL.md +93 -0
- package/skills/melonjs-ui-and-text/SKILL.md +124 -0
|
@@ -49,6 +49,43 @@ always drives the switch from its own completion callback.
|
|
|
49
49
|
registered for that id, and `state.set` throws if the second argument is not a
|
|
50
50
|
`Stage` instance. Neither fails quietly.
|
|
51
51
|
|
|
52
|
+
### Always leave `state.LOADING` — it is transitional
|
|
53
|
+
|
|
54
|
+
`loader.preload()` switches to `state.LOADING` for you. Building your scene in
|
|
55
|
+
the preload callback and then *staying there* looks like it works, and is one
|
|
56
|
+
of the easiest mistakes to make:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// WRONG — the game now runs inside the loading stage, forever
|
|
60
|
+
loader.preload(assets, () => {
|
|
61
|
+
app.world.addChild(new Player(...));
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Nothing ever destroys that stage, so the built-in loading screen's logo and
|
|
66
|
+
progress bar are never removed and sit on top of your game. (Eight of this
|
|
67
|
+
repository's own examples did this before it was noticed, so it is not an
|
|
68
|
+
obscure trap.)
|
|
69
|
+
|
|
70
|
+
Give the scene a stage of its own and switch to it:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
class PlayScreen extends Stage {
|
|
74
|
+
onResetEvent(app) {
|
|
75
|
+
app.world.addChild(new Player(...));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await loader.preload(assets);
|
|
80
|
+
state.set(state.PLAY, new PlayScreen());
|
|
81
|
+
state.change(state.PLAY);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The awaited form reads better here than the callback, because "load, then
|
|
85
|
+
switch" is exactly what the code then says. If you genuinely want to keep using
|
|
86
|
+
the loading stage as your scene, `state.change(state.DEFAULT)` at least gets
|
|
87
|
+
you out of it — but a stage of your own is what you want.
|
|
88
|
+
|
|
52
89
|
## Persistent objects across levels
|
|
53
90
|
|
|
54
91
|
```js
|
|
@@ -74,6 +111,45 @@ app.freeze(150); // brief hit-stop (proxies state.fr
|
|
|
74
111
|
`state.change`. The `"mask"` form needs its fourth argument, an `Ellipse` or
|
|
75
112
|
`Polygon`; without one it warns and falls back to a direct switch.
|
|
76
113
|
|
|
114
|
+
### A Stage's own `update()` is NOT paused
|
|
115
|
+
|
|
116
|
+
This one is easy to get wrong and hard to see. `Application.update` calls both
|
|
117
|
+
of these **unconditionally**:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
this.isDirty = this.world.update(this.updateDelta);
|
|
121
|
+
this.isDirty = state.current().update(this.updateDelta) || this.isDirty;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Only `Container.update` consults the pause — it reads `state.isPaused()` and
|
|
125
|
+
skips every child that is not `updateWhenPaused`. So world children (entities,
|
|
126
|
+
sprites, meshes, a `GLTFModel`) freeze correctly, while **the Stage subclass's
|
|
127
|
+
own `update()` keeps running**.
|
|
128
|
+
|
|
129
|
+
A game that keeps its logic in entities pauses for free and never notices. A
|
|
130
|
+
game that drives the simulation from `Stage.update` — common for an endless
|
|
131
|
+
runner, where one object moves and the world scrolls past it — keeps simulating
|
|
132
|
+
through `pauseOnBlur` with its world frozen. The signature is bizarre and very
|
|
133
|
+
misleading: the scenery scrolls on, the player character stands still in world
|
|
134
|
+
space and slides off the screen, and it all snaps back on focus. It reads as a
|
|
135
|
+
culling or animation bug, not a pause bug.
|
|
136
|
+
|
|
137
|
+
Guard it explicitly:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
update(dt) {
|
|
141
|
+
super.update(dt);
|
|
142
|
+
if (state.isPaused()) {
|
|
143
|
+
return true; // world children are already frozen for you
|
|
144
|
+
}
|
|
145
|
+
// …simulation…
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Worth knowing that `pauseOnBlur` (default `true`) fires on a window `blur`, not
|
|
150
|
+
only on tab `visibilitychange` — so clicking another window on the same screen
|
|
151
|
+
triggers it while the canvas is still fully visible.
|
|
152
|
+
|
|
77
153
|
## The update loop
|
|
78
154
|
|
|
79
155
|
There is no loop you own. Logic goes in `update(dt)` overrides, or a
|
|
@@ -160,6 +236,21 @@ save.add({ hiscore: 0 }); // once; idempotent, loads any stored value
|
|
|
160
236
|
save.hiscore = 1200; // plain assignment writes to localStorage
|
|
161
237
|
```
|
|
162
238
|
|
|
239
|
+
**In TypeScript, keep what `add()` returns.** The namespace carries a
|
|
240
|
+
`[key: string]: unknown` index for keys registered anywhere, so reading
|
|
241
|
+
`save.hiscore` off it gives you `unknown`. `add()` hands back the same
|
|
242
|
+
namespace typed with the keys you just registered, which is the handle to
|
|
243
|
+
keep:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const store = save.add({ hiscore: 0, lives: 3 });
|
|
247
|
+
store.hiscore = 1200; // number, no cast
|
|
248
|
+
if (score > store.hiscore) { /* ... */ }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Chained calls accumulate, so `save.add({ a: 0 }).add({ b: "" })` is typed with
|
|
252
|
+
both.
|
|
253
|
+
|
|
163
254
|
## Symptom → cause
|
|
164
255
|
|
|
165
256
|
| symptom | cause |
|
|
@@ -169,6 +260,8 @@ save.hiscore = 1200; // plain assignment writes to localStorage
|
|
|
169
260
|
| a tween finishes in 1000 ms whatever you pass | `to()` takes `{ duration }`, not a number |
|
|
170
261
|
| object frozen while its state updates | `update()` not returning `true` |
|
|
171
262
|
| timers fire behind a pause menu | `window.setTimeout` instead of `timer.setTimeout` |
|
|
263
|
+
| the world freezes on blur but the game keeps advancing | simulation lives in `Stage.update`, which the pause does not gate — only `Container.update` checks `state.isPaused()` |
|
|
264
|
+
| the player character is left behind and scrolls off after a lost window | same cause: the stage advanced the camera while its world children were paused |
|
|
172
265
|
| a tween does nothing | `.start()` never called |
|
|
173
266
|
| effect stops during a hit-stop | tween needs `updateWhenPaused = true` |
|
|
174
267
|
| crash after a stage switch | callback ran during teardown — guard with `state.isCurrent` |
|
|
@@ -6,6 +6,96 @@ license: MIT
|
|
|
6
6
|
|
|
7
7
|
# UI, HUD and text
|
|
8
8
|
|
|
9
|
+
## Outlined text: the stroke eats into the glyph
|
|
10
|
+
|
|
11
|
+
`Text` draws `fillText` and then `strokeText`, so the outline lands **on top of
|
|
12
|
+
the fill** and is centred on the glyph edge — half of it inward. On a small or
|
|
13
|
+
chunky face a `lineWidth` of 3 leaves the letters solid black. Keep it to 1,
|
|
14
|
+
and raise the font size rather than the stroke.
|
|
15
|
+
|
|
16
|
+
The stroke used to cost you the top row of pixels as well, and a display face
|
|
17
|
+
whose glyphs overshoot the nominal ascent did the same on its own: the render
|
|
18
|
+
box was sized from the line height alone, with no allowance for either. **That
|
|
19
|
+
is fixed** — the bake is padded by the ink's real extent and the blit shifts
|
|
20
|
+
back by the same amount, so nothing is clipped and the reported bounds are
|
|
21
|
+
unchanged. Write the string you mean:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
new Text(x, y, { font: "Display", lineWidth: 1, lineHeight: 1.45, text: "" });
|
|
25
|
+
hud.setText("SCORE 100");
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you have a label carrying a leading `\n` to buy headroom, that workaround is
|
|
29
|
+
now dead weight — drop it, and take the line height back off the position you
|
|
30
|
+
shifted it by.
|
|
31
|
+
|
|
32
|
+
## Gradient text
|
|
33
|
+
|
|
34
|
+
`fillStyle` takes a `Gradient` as well as a colour — the same object
|
|
35
|
+
`Renderer#setColor` accepts, built the way the canvas API builds one:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const ramp = renderer.createLinearGradient(0, 0, 0, 24); // top to bottom
|
|
39
|
+
ramp.addColorStop(0, "#fffdf0");
|
|
40
|
+
ramp.addColorStop(1, "#f0a020");
|
|
41
|
+
|
|
42
|
+
new Text(x, y, { font: "Display", size: 24, fillStyle: ramp });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Coordinates are the label's own bake: `(0, 0)` is the top-left of the render
|
|
46
|
+
box, so the ramp above runs down one line. A multi-line label **restarts it on
|
|
47
|
+
every line**, reading like one `Text` per line — you do not have to author the
|
|
48
|
+
gradient over the block height. Pass `gradientPerLine: false` for a single ramp
|
|
49
|
+
spanning the whole block, which is what a plain canvas does and what a
|
|
50
|
+
deliberate fade across a two-line title wants.
|
|
51
|
+
|
|
52
|
+
The ramp colours the **fill only** — `Text` strokes in a separate pass, so
|
|
53
|
+
an outline keeps its own colour without any luminance trickery — and it works on
|
|
54
|
+
Canvas2D, which a post effect does not. `fillStyle.alpha` still gates the fill,
|
|
55
|
+
and the property still reads back as a `Color`.
|
|
56
|
+
|
|
57
|
+
## BitmapText: `size` is a RATIO, not pixels
|
|
58
|
+
|
|
59
|
+
The trap when moving over from `Text`:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
new Text(x, y, { font: "Arial", size: 24, text: "SCORE" }); // 24 pixels
|
|
63
|
+
new BitmapText(x, y, { font: "arial", size: 24, text: "SCORE" }); // 24 TIMES
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`size` scales the font's authored size, so `1` is native and `2` is double —
|
|
67
|
+
`resize(scale)` and `set(textAlign, scale)` take the same ratio. Whole numbers
|
|
68
|
+
keep pixel art crisp; fractional ones resample the page image.
|
|
69
|
+
|
|
70
|
+
It has **no stroke** — there is no `strokeStyle` or `lineWidth` here, which is
|
|
71
|
+
part of why it stays sharp. Colour comes from the tint instead:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const score = new BitmapText(8, 8, {
|
|
75
|
+
font: "arial", text: "1000", fillStyle: "#ffd700",
|
|
76
|
+
});
|
|
77
|
+
score.fillStyle = "#ff4040"; // recolour at any time
|
|
78
|
+
score.fillStyle = new Color(255, 255, 255); // UNTINTED, not "white text"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`fillStyle` is `Renderable#tint` under another name: white is the *absence* of a
|
|
82
|
+
tint and every other colour tints away from it, so author the page in white to
|
|
83
|
+
keep every colour available to you.
|
|
84
|
+
|
|
85
|
+
Load the descriptor as `binary` and its page as `image` under the **same name**.
|
|
86
|
+
Both BMFont flavours — text (`.fnt`) and XML — are auto-detected, so an `.xml`
|
|
87
|
+
descriptor loads as-is:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
loader.preload([
|
|
91
|
+
{ name: "arial", type: "binary", src: "data/font/arial.fnt" },
|
|
92
|
+
{ name: "arial", type: "image", src: "data/font/arial.png" },
|
|
93
|
+
]);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Reach for it over `Text` when the text is mostly static, has to stay crisp at
|
|
97
|
+
integer scales, or wants recolouring without a re-bake.
|
|
98
|
+
|
|
9
99
|
## The HUD pattern
|
|
10
100
|
|
|
11
101
|
A HUD is a `floating` container at a high z, built once and re-added:
|
|
@@ -39,6 +129,40 @@ Three things matter here:
|
|
|
39
129
|
real accessor is `depth` (an alias for `pos.z`); `addChild(child, z)` sets it
|
|
40
130
|
for you.
|
|
41
131
|
|
|
132
|
+
### Buttons: extend the handlers, do not bind listeners
|
|
133
|
+
|
|
134
|
+
`UISpriteElement` is a `Sprite` that already registers itself for pointer
|
|
135
|
+
events, so a button is made by overriding methods rather than by wiring
|
|
136
|
+
`registerPointerEvent`:
|
|
137
|
+
|
|
138
|
+
| handler | fires | returns |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `onClick(event)` | pressed | `false` to stop the event propagating |
|
|
141
|
+
| `onRelease(event)` | pressed and released | `false` to stop propagating |
|
|
142
|
+
| `onOver(event)` | pointer enters | — |
|
|
143
|
+
| `onOut(event)` | pointer leaves | — |
|
|
144
|
+
| `onHold()` | pressed and held | — |
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
class MuteButton extends UISpriteElement {
|
|
148
|
+
constructor(x, y) {
|
|
149
|
+
super(x, y, { image: atlas, region: "speaker.png" });
|
|
150
|
+
this.setOpacity(0.5);
|
|
151
|
+
}
|
|
152
|
+
onOver() { this.setOpacity(1.0); }
|
|
153
|
+
onOut() { this.setOpacity(0.5); }
|
|
154
|
+
onClick() {
|
|
155
|
+
audio.muteAll();
|
|
156
|
+
return false; // consumed — do not fall through to the world
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The pointer still has to reach it: a renderable with `isKinematic = true` — the
|
|
162
|
+
default on a plain `Renderable` — is skipped by the broadphase and receives
|
|
163
|
+
nothing. `UISpriteElement` and `UIBaseElement` clear it for you; anything else
|
|
164
|
+
you make clickable has to clear it itself.
|
|
165
|
+
|
|
42
166
|
### In a 3D scene, a HUD needs a SMALL depth
|
|
43
167
|
|
|
44
168
|
`floating` opts a renderable out of the camera transform. It does **not** opt it
|