Skip to content
Open
3 changes: 3 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [20.4.0] (melonJS 2) - _unreleased_

### Added
- `level.load()`, `reload()`, `next()` and `previous()` take an `async` option: set it and the call hands back a promise that settles once the level is actually in the world, instead of the boolean it has always returned. `options.onLoaded` still fires either way, so the two forms mix freely, and omitting the flag changes nothing — the existing signatures are preserved as TypeScript overloads, so `const ok: boolean = level.load("map1")` still compiles. Running out of levels still reports `false` rather than rejecting, and an unknown level id throws synchronously in both forms: that is a typo, not a load failure, and it should not need `await` to surface ([#1646](https://github.com/melonjs/melonJS/issues/1646))
- **Soft transparency for the 3D tier** ([#1516](https://github.com/melonjs/melonJS/issues/1516)): a mesh now fades when you fade it. Draws that resolve to fractional alpha go into a **transparent pass**, replayed back-to-front after the opaque one — blending, writing no depth but still depth-tested, so transparent objects composite with each other and stay correctly hidden behind opaque geometry. Blending honours the renderable's existing `blendMode`, so `"additive"` gives glows. `transparent: true` opts in a soft-alpha *texture* the automatic check cannot see into — a glTF `alphaMode: "BLEND"` material, a glow sprite — and `transparent: false` pins the opaque path. Sorting is per object, so intersecting transparent meshes remain order-dependent. Needs a GPU backend and a `Camera3d`; a scene with no transparent objects never enters the queue
- **Distance fog for the 3D tier** ([#1622](https://github.com/melonjs/melonJS/issues/1622)): `camera.setFog({ mode, near, far, density, color })` fades mesh geometry toward a colour with distance — `"linear"` between two distances, or `"exp2"` from a single density. Every parameter is optional and the omitted ones resolve **live**: the distances track the camera's own clip planes, so fog cannot silently disagree with them after a later `setClipPlanes`, and the colour tracks `renderer.backgroundColor`, so geometry dissolves into the sky you already set. Measured radially and applied per fragment, so it neither slides as the camera turns nor bands across large triangles. Fog belongs to the camera, so split-screen and minimap views fog independently and a `Camera2d` never fogs; a mesh opts out with `fog: false`. **Off by default**, and compiled out on both backends rather than skipped at runtime
- **Height falloff for distance fog** ([#1633](https://github.com/melonjs/melonJS/issues/1633)): `camera.setFog({ …, fogHeight, heightFalloff })` makes fog density drop with altitude, so mist pools in low ground instead of hanging as thickly over a ridge as over the valley floor. `heightFalloff` defaults to `0`, which is not a special case but the same integral with the dial at zero, so a scene that omits it renders exactly as before. Costs one `exp` per vertex: an exponential integrates analytically along a straight segment, so there is no ray marching and no volume texture. Render space is **Y-down**, so `fogHeight` is the floor and density rises below it
Expand All @@ -11,6 +12,8 @@
- Docs: the API reference carries the engine's own identity — logo, brand palette and favicon — and the header links out to the site, the wiki, the repository and Discord. A **Copy page** control hands the page you are reading to an assistant: it copies the page as Markdown with its canonical URL attached, or opens it directly in a chat. The landing page also gained a short section on using the reference with an AI assistant

### Fixed
- Level: `level.reload()` was documented as returning `object` — "the current level" — but it returns whatever `level.load()` returns, which is `true`. The declared type has been wrong for the method's whole life: the 2011 original returned nothing at all. `getCurrentLevel()` is the call that hands back the level object. This corrects the emitted type from `object` to `boolean`, so a `const lvl: object = level.reload()` that compiled while receiving `true` now fails to compile, at the site that was already wrong
- Level: a level load could sit for a second or more before starting when the tab was in the background. `level.load()` deferred its work with a timer so the current frame could unwind before the world is reset — necessary, since it is routinely called from a trigger handler mid-loop — but browsers clamp a timer to at least a second in a background tab. It now defers with a microtask, which unwinds the frame just the same and is not clamped
- Lit meshes: specular highlights sat in the wrong place under a scaled ancestor ([#1636](https://github.com/melonjs/melonJS/issues/1636)). The camera position was derived from the view as `-Rᵀ·t`, which is only the right point when the upper 3×3 is orthonormal — and `Container.draw` folds every ancestor into that matrix. It is now the translation column of the view's inverse
- Lit meshes: specular lighting, and a mesh's alpha-map cutout, were wrong on whichever tier drew second in a frame. The instanced and non-instanced tiers are two programs sharing one batcher, and its skip-the-redundant-upload cache was not dropped when the program changed under it — so an instanced set behind a lit prop at the same shininess lost its specular outright, and instanced foliage rendered as opaque rectangles. Present since 20.0.0
- Ground shadows: a scene could lose every blob it drew. The queue drained on any batcher switch, including inside the screen-projection window `Container.draw` opens around a `floating` child — so a single HUD deleted every ground shadow — and mid-scene whenever anything non-mesh sorted there. It now drains only where the world draw is finished
Expand Down
20 changes: 15 additions & 5 deletions packages/melonjs/skills/melonjs-3d-assets/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: melonjs-3d-assets
description: "Use this skill when loading 3D models into melonJS — glTF and GLB scenes, OBJ/MTL models, materials, imported lights, node animation, ground shadows and GPU instancing. Covers level.load options, the rightHanded conversion, and exactly what the loader does and does not support. Triggers on: glTF, gltf, glb, OBJ, MTL, 3D model, getGLTF, getOBJ, getMTL, GLTFModel, GLTFScene, level.load glb, rightHanded, lightIntensityScale, castGroundShadow, shadowGroundY, EXT_mesh_gpu_instancing, KHR_lights_punctual, skinning, Blender export, 3D asset."
description: "Use this skill when loading 3D models into melonJS — glTF and GLB scenes, OBJ/MTL models, materials, imported lights, node animation, ground shadows and GPU instancing. Covers level.load options including the async flag, the rightHanded conversion, and exactly what the loader does and does not support. Triggers on: glTF, gltf, glb, OBJ, MTL, 3D model, getGLTF, getOBJ, getMTL, GLTFModel, GLTFScene, level.load glb, rightHanded, lightIntensityScale, castGroundShadow, shadowGroundY, EXT_mesh_gpu_instancing, KHR_lights_punctual, skinning, Blender export, 3D asset."
license: MIT
---

Expand Down Expand Up @@ -34,17 +34,27 @@ or it renders flat. See `melonjs-3d` for the camera.
| `scale` | `1` | pixels per glTF unit, applied to the whole scene. Blender's metre-scale export usually needs 20–100. |
| `container` | `game.world` | where the nodes are added |
| `onLoaded` | `app.onLevelLoaded` | called with the **level id**, not the scene |
| `async` | `false` | return a promise that settles once the scene is in the world, instead of a boolean |
| `rightHanded` | `true` | see below |
| `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |
| `lightIntensityScale` | — | keep authored intensity ratios instead of normalising every light to 1 |
| `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |
| `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |

`onLoaded` receives the level id — it is a "done" signal, not a handle on the
scene. You need it: with the game loop running, `level.load` stops the loop and
defers the actual load to the next tick, so it returns *before* anything is in
the world. To get at what was loaded, load into a container you own, or look the
nodes up by their authored names:
scene. You need it, or `async`: with the game loop running, `level.load` stops
the loop and defers the actual load to a microtask, so by default it returns
*before* anything is in the world.

```js
await level.load("diorama", { scale: 50, async: true });
// the scene is in the world here
```

Note `await level.load("diorama")` without the flag does not await the load — the
call returns a boolean, and `await true` resolves immediately. To get at what was
loaded,
load into a container you own, or look the nodes up by their authored names:

```js
level.load("diorama", { scale: 50, onLoaded: () => {
Expand Down
43 changes: 32 additions & 11 deletions packages/melonjs/skills/melonjs-tilemaps/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: melonjs-tilemaps
description: "Use this skill for Tiled maps in melonJS — loading TMX/TSX levels, spawning entities from Tiled objects, collision shapes authored in Tiled, isometric and hexagonal maps, and image layers. Covers the pool.register name contract, camera bounds, compressed maps needing the inflate plugin, and the level director API. Triggers on: Tiled, TMX, TSX, tilemap, level.load, tileset, ImageLayer, isometric, hexagonal, staggered, pool.register, Collectable, Trigger, object layer, collision layer, parallax."
description: "Use this skill for Tiled maps in melonJS — loading TMX/TSX levels, spawning entities from Tiled objects, collision shapes authored in Tiled, isometric and hexagonal maps, and image layers. Covers the pool.register name contract, camera bounds, compressed maps needing the inflate plugin, and the level director API. Triggers on: Tiled, TMX, TSX, tilemap, level.load, level.load async, await level.load, tileset, ImageLayer, isometric, hexagonal, staggered, pool.register, Collectable, Trigger, object layer, collision layer, parallax."
license: MIT
---

Expand Down Expand Up @@ -33,18 +33,38 @@ also skip `src` and pass the map inline via `data` (with `format: "json"` or
`"xml"`).

`level.load(levelId, options)` accepts `container` (default `game.world`),
`onLoaded` (default `game.onLevelLoaded`), `flatten` (default `game.mergeGroup`)
and `setViewportBounds` (default **`true`**). It throws `level <id> not found`
for an unknown id.
`onLoaded` (default `game.onLevelLoaded`), `flatten` (default `game.mergeGroup`),
`setViewportBounds` (default **`true`**) and `async` (default `false`). It throws
`level <id> not found` for an unknown id — synchronously, in both forms, because
that is a typo rather than a load failure.

**`level.load` is deferred while the game loop is running.** It calls
`state.stop()` and finishes the load in a `setTimeout`, so it returns `true`
before anything is in the world. Do follow-up work from the `onLoaded` callback
or an `event.LEVEL_LOADED` listener, not on the next line.
`state.stop()` and finishes the load in a microtask, so by default it returns
`true` before anything is in the world. Two ways to sequence work after it:

`level.reload()`, `level.next()`, `level.previous()`, `level.getCurrentLevelId()`
and `level.levelCount()` round out the namespace. `flatten: false` wraps each
Tiled object group in its own `Container` named after the group.
```js
// await it
await level.load("map1", { async: true });
// the world is populated here

// ...or use the callback / event, which fire in both forms
level.load("map1", { onLoaded: () => this.spawnPlayer() });
```

`async: true` is the only thing that changes the return value — everything else
behaves identically, `onLoaded` included. Without it the call returns a boolean,
so `await level.load("map1")` is not an error and does not await the load:
`await true` resolves immediately. (The load does finish first today, because the
deferral is a single microtask queued ahead of the await's continuation — but
that is incidental ordering, not a contract.) Pass the flag when you mean to
await.

`level.reload()`, `level.next()` and `level.previous()` take the same `async`
option and resolve the same value they return — so `if (level.next())` becomes
`if (await level.next({ async: true }))`. Running out of levels reports `false`
either way rather than throwing. `level.getCurrentLevelId()` and
`level.levelCount()` round out the namespace. `flatten: false` wraps each Tiled
object group in its own `Container` named after the group.

## Spawning entities from Tiled objects

Expand Down Expand Up @@ -177,7 +197,8 @@ unanimated layer into the offscreen-bake path instead.
| symptom | cause |
|---|---|
| a Tiled object becomes a plain shape with no behaviour | its class/name does not match any registered factory, or it was registered after `level.load` |
| the world is still empty right after `level.load` | the load is deferred via `setTimeout` while the loop runs — use `onLoaded` / `LEVEL_LOADED` |
| the world is still empty right after `level.load` | the load is deferred to a microtask while the loop runs — `await level.load(id, { async: true })`, or use `onLoaded` / `LEVEL_LOADED` |
| `await level.load(id)` returned `true` rather than a promise | without `async: true` the call returns a boolean; `await true` resolves immediately. The load happens to finish first today by microtask ordering, but that is incidental — pass the flag when you mean to await |
| `level <id> not found` | the map was never preloaded, or the asset `name` differs from the id passed to `load` |
| `unknown or invalid resource type` | asset `type` set to `"tmj"` / `"tsj"` — use `"tmx"` / `"tsx"` with the `.tmj` / `.tsj` file |
| camera will not scroll | `setViewportBounds: false`, or the map was added with `addTo()` (which defaults to `false`) |
Expand Down
1 change: 1 addition & 0 deletions packages/melonjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export { Sphere } from "./geometries/sphere.ts";
export * as input from "./input/input.ts";
// Backward compatibility for deprecated method or properties
export * from "./lang/deprecated.js";
export type { LevelLoadOptions } from "./level/level.js";
export { level } from "./level/level.js";
export {
registerTiledObjectClass,
Expand Down
Loading
Loading