{"owner":"melonjs","repo":"melonJS","hasSkills":true,"totalSkillsCount":65,"totalTokensCount":81883,"categories":["anthropic-skill","root-instruction","claude-rule","gemini-rule","copilot-instructions","cursor-rule","windsurf-rule","cline-rule","mcp-config","marketplace","plugin-manifest"],"hasMcp":true,"mcpConfig":{"mcpServers":{"melonJS":{"command":"npx","args":["-y","@modelcontextprotocol/server-melonJS"]}}},"found":["packages/melonjs/skills/melonjs-3d/SKILL.md","packages/melonjs/skills/melonjs-getting-started/SKILL.md","packages/melonjs/skills/melonjs-20-migration/SKILL.md","packages/melonjs/skills/melonjs/SKILL.md","packages/melonjs/skills/melonjs-renderables/SKILL.md","packages/melonjs/skills/melonjs-3d-assets/SKILL.md","packages/melonjs/skills/melonjs-physics/SKILL.md","packages/melonjs/skills/melonjs-sprites-and-animation/SKILL.md","packages/melonjs/skills/melonjs-input/SKILL.md","packages/melonjs/skills/melonjs-tilemaps/SKILL.md","packages/melonjs/skills/melonjs-audio/SKILL.md","packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md","packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md","packages/melonjs/skills/melonjs-ui-and-text/SKILL.md","packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md","packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md","packages/melonjs/skills/melonjs-lighting/SKILL.md","packages/melonjs/skills/melonjs-math/SKILL.md","packages/melonjs/skills/melonjs-deployment/SKILL.md","packages/melonjs/skills/melonjs-loading-assets/SKILL.md","packages/melonjs/skills/melonjs-renderer-backends/SKILL.md","packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md","packages/melonjs/skills/melonjs-performance/SKILL.md","packages/melonjs/skills/melonjs-events/SKILL.md","packages/melonjs/skills/melonjs-plugins/SKILL.md","AGENTS.md","CLAUDE.md","GEMINI.md","COPILOT.md",".cursorrules",".windsurfrules",".clinerules","llms.txt","mcp.json","marketplace.json","plugin.json",".github/copilot-instructions.md",".cursor/mcp.json",".claude-plugin/marketplace.json",".claude-plugin/plugin.json","packages/melonjs/skills/melonjs/README.md","packages/melonjs/skills/melonjs-getting-started/README.md","packages/melonjs/skills/melonjs-20-migration/README.md","packages/melonjs/skills/melonjs-renderables/README.md","packages/melonjs/skills/melonjs-3d/README.md","packages/melonjs/skills/melonjs-3d-assets/README.md","packages/melonjs/skills/melonjs-physics/README.md","packages/melonjs/skills/melonjs-sprites-and-animation/README.md","packages/melonjs/skills/melonjs-input/README.md","packages/melonjs/skills/melonjs-effects-and-shaders/README.md","packages/melonjs/skills/melonjs-tilemaps/README.md","packages/melonjs/skills/melonjs-audio/README.md","packages/melonjs/skills/melonjs-scenes-and-state/README.md","packages/melonjs/skills/melonjs-ui-and-text/README.md","packages/melonjs/skills/melonjs-particles-and-trails/README.md","packages/melonjs/skills/melonjs-camera-and-drawing/README.md","packages/melonjs/skills/melonjs-lighting/README.md","packages/melonjs/skills/melonjs-math/README.md","packages/melonjs/skills/melonjs-deployment/README.md","packages/melonjs/skills/melonjs-loading-assets/README.md","packages/melonjs/skills/melonjs-renderer-backends/README.md","packages/melonjs/skills/melonjs-display-and-scaling/README.md","packages/melonjs/skills/melonjs-performance/README.md","packages/melonjs/skills/melonjs-events/README.md","packages/melonjs/skills/melonjs-plugins/README.md"],"skills":{"packages/melonjs/skills/melonjs-3d/SKILL.md":"---\nname: melonjs-3d\ndescription: \"Use this skill for anything 3D or 2.5D in melonJS — Camera3d, Mesh, InstancedMesh, Sprite3d billboards, Light3d, ground shadows, glTF/GLB scenes, and depth sorting. Covers the Y-down/+Z-forward convention that is the inverse of OpenGL, the cameraClass opt-in, clip planes, and what does not work on the Canvas fallback. Triggers on: Camera3d, Mesh, InstancedMesh, Sprite3d, Trail3d, ribbon, exhaust trail, Light3d, billboard, glTF, glb, 3D, 2.5D, depth, cameraClass, fov, setClipPlanes, setFog, fog, distance fog, height fog, heightFalloff, transparent, transparency, alpha, blendMode, fade, castGroundShadow, lit, setBasis, getBasis, lookAt, up vector, gimbal, slerp, great circle, turn a direction.\"\nlicense: MIT\n---\n\n# 3D and 2.5D in melonJS\n\n> melonJS is a 2D engine that grew a real 3D tier: perspective cameras, meshes,\n> instancing, glTF scenes and 3D lights. Most of what a model \"knows\" about 3D\n> graphics comes from the OpenGL convention, and **melonJS's is the inverse**.\n> Read the conventions section before writing any 3D code.\n\n## Coordinate conventions — the inverse of OpenGL\n\nThis is the single most important thing on this page.\n\n| | melonJS | OpenGL convention |\n|---|---|---|\n| vertical | **Y-down** — higher `y` is *lower* on screen | Y-up |\n| depth | **+Z forward** — higher `z` is *farther* away | −Z forward |\n\n`Camera3d` exposes three rotation angles: `camera.pitch`\n(X, look up/down), `camera.yaw` (Y, look left/right) and `camera.roll` (Z, bank\nthe horizon). The view is `R(yaw) ∘ R(pitch) ∘ R(roll)` inverted, and the frustum\nplanes come off that same matrix — so culling follows a banked view.\n\nThe basis those angles produce is **readable and writable**: `getBasis(right,\nup, forward)` reads it, `setBasis(right, up, forward)` poses the camera from\none.\n\n`camera.rotate()` is NOT the way to bank a 3D camera. It writes the inherited\n`currentTransform`, which a 3D view never reads, so the call is silently inert.\nSet `roll`. (On a `Camera2d` it is the other way round: roll IS that transform's\nrotation, which is why `worldToLocal` / `localToWorld` compensate for it.)\n\nThe payoff is that 2D code translates directly — anywhere you used `pos.x` /\n`pos.y`, add `pos.z` and the maths still holds. The cost is that every\nOpenGL-shaped instinct is backwards.\n\n**Consequence that bites immediately:** \"behind everything\" is the *largest*\ndepth when the camera looks along +Z. Put a backdrop on the wrong side and it\neither paints over the whole scene (it is nearer than everything else) or\ndisappears without trace (it falls in front of the near plane and the GPU clips\nit). Which sign means \"far\" depends on which side the camera sits, so check the\ncamera before picking a backdrop depth.\n\nglTF assets are authored Y-up right-handed. `level.load` converts them by\nrotation (`rightHanded: true` is its default), so you do not apply a flip\nyourself. The **`Mesh` constructor defaults `rightHanded` to `false`**, which\nbridges by mirroring instead — geometry you hand to a `Mesh` directly comes in\nflipped left/right unless you pass `rightHanded: true`. See\n`melonjs-3d-assets`.\n\n## Opting in\n\n3D starts with the camera class, and nothing else has to change. Set it once on\nthe `Application`:\n\n```js\nimport { Application, Camera3d } from \"melonjs\";\n\nconst app = new Application(1024, 768, {\n    parent: \"screen\",\n    cameraClass: Camera3d,      // ← the opt-in\n});\nawait app.init();\n```\n\nThat flips the world's sort to `\"depth\"`, switches the broadphase to a 3D index,\nand renders sprites and meshes with perspective projection. `Camera3d` extends\n`Camera2d`, so follow, fade, shake and post effects all still work.\n\n### Keep the camera, do not fish it back out\n\n`app.viewport` is the **active stage's** default camera, so it is typed\n`Camera2d`: a stage overrides the application's `cameraClass`, and the\nbuilt-in loading screen does exactly that. Reading your `Camera3d` back out of\nit therefore needs a cast, and a cast that is wrong while the loader is up:\n\n```js\nconst camera = app.viewport as Camera3d;   // works, until it does not\n```\n\nOwn it instead. Pass the camera in and keep the typed reference, and nothing\ndownstream ever narrows:\n\n```js\nclass GameStage extends Stage {\n    readonly camera: Camera3d;\n\n    constructor(app) {\n        const camera = new Camera3d(0, 0, app.renderer.width, app.renderer.height);\n        super({ cameras: [camera] });\n        this.camera = camera;\n    }\n}\n```\n\nA freshly built camera is named `\"default\"`, and a stage registers its\n`cameras` by name, so this IS the stage's default camera: `app.viewport`\nreturns the same instance.\n\n### The two patterns differ in LIFETIME, and that decides which you want\n\nThis is the part that bites. `cameraClass` builds a **fresh camera for each\nstage**, which the engine does deliberately so \"Camera3d state shouldn't\nbleed across stages\". An owned camera passed through `super({ cameras })` is\nbuilt once and lives as long as the stage object, which for a stage you\nregister with `state.set()` means as long as the application.\n\nSo anything you attach to the camera now outlives the visit:\n\n```js\n// in onResetEvent — runs EVERY time the stage is entered\napp.viewport.addPostEffect(bloom);\n```\n\nWith `cameraClass` that lands on a fresh camera each time and is harmless.\nWith an owned camera it stacks: a second bloom on the second visit, a third\non the third. The symptom is text and edges appearing to double or smear,\nbecause the scene is being post-processed once per accumulated effect.\n\nNote that `removePostEffect` DESTROYS the effect rather than detaching it, so\n\"just remove it on the way out\" is not a free undo.\n\n**Choose on lifetime, not on typing:**\n\n| your effects / camera state are | use |\n|---|---|\n| set up per stage visit, in `onResetEvent` | `cameraClass` — fresh camera per stage |\n| set up once, in the stage constructor | an owned camera in `super({ cameras })` |\n\nIf you want the typed reference but your setup is per-visit, keep\n`cameraClass` and narrow once on reset:\n\n```js\noverride onResetEvent(app) {\n    const viewport = app.viewport;\n    if (!(viewport instanceof Camera3d)) {\n        throw new Error(\"this stage needs a Camera3d\");\n    }\n    this.camera = viewport;   // typed, checked, and still fresh per visit\n}\n```\n\nThat is what the Earth Defender example does, and why: it attaches post\neffects on every reset and never removes them.\n\nNarrowing beats asserting wherever you read the shared slot: the language\nverifies `instanceof`, and fails loudly if the application was never given a\n`cameraClass`, where a cast fails silently at the first 3D call.\n\nPer-stage opt-in also works, which is useful when the loading screen is 2D:\n\n```js\nclass GameStage extends Stage {\n    constructor() {\n        super({ cameras: [new Camera3d(0, 0, 1024, 768, { fov: Math.PI / 3 })] });\n    }\n}\n```\n\n## Camera traps\n\n- **There is no sky.** The frame clears to `renderer.backgroundColor` (opaque\n  black unless the renderer was created transparent), and `world.backgroundColor`\n  clears over it when you set one — both are flat colours. Anything more is a\n  backdrop renderable, screen-fixed or placed at a far depth.\n- **Set the clip planes for your scene scale.** Defaults are near 0.1 / far 1000.\n  Objects beyond the far plane clip or project with bad w-divides; a `near` that\n  is too small wastes depth precision and distant props z-fight. Use\n  `camera.setClipPlanes(near, far)`.\n- **`camera.pos.set(x, y)` is 2-argument and zeroes z.** Use\n  `camera.setPosition(x, y, z)`, which leaves the depth untouched when `z` is\n  omitted; `camera.depth` is the z accessor if you only want that.\n- **`lookAt(target)` solves pitch and yaw only**, so on its own it cannot\n  express \"up is this direction\" and it leaves `roll` alone. Pass an up to\n  bank the horizon too: `camera.lookAt(target, up)`, where `up` is what should\n  appear UP ON SCREEN. This engine renders Y-down, so a level horizon is\n  `(0, -1, 0)`. There is deliberately no default, because giving one would\n  silently level every camera that had banked its own horizon.\n- **When the game already has all three axes, hand them over**:\n  `camera.setBasis(right, up, forward)`, the writable counterpart of\n  `getBasis`. The case that needs it is a view over a curved surface, where up\n  is the surface normal and differs at every point, so no pitch and yaw pair\n  expresses it and integrating towards one drifts and gimbals at the poles.\n  `setBasis` absorbs a triple that has drifted off orthonormal rather than\n  leaving the camera holding something that is not a rotation, and it does not\n  care what the axes are scaled to: a direction is a direction, so anything\n  from a denormal to 1e300 resolves the same way. Note its `up` is the camera's\n  local +Y column, which under Y-down points DOWN the screen: the opposite\n  sense to `lookAt`'s, and one negation apart.\n- **`worldToLocal` does not project.** It is a 2D camera's offset subtraction.\n  To pin a label or marker to a point in the scene use\n  `camera.worldToScreen(x, y, z)`, which applies the projection and returns\n  `null` behind the camera. See `melonjs-camera-and-drawing`.\n\n## Where an up vector earns its keep\n\nThree cases. In the first two, `lookAt(target)` on its own is already right and\nan up would be noise. The third is the one it exists for.\n\n```js\n// 1. ordinary third-person: world up never changes, so say nothing\ncamera.setPosition(x, y, z).lookAt(player);\n\n// 2. a deliberate dutch angle: this is a ROLL, not an up vector\ncamera.lookAt(player);\ncamera.roll = Math.PI / 32;\n\n// 3. a view over a curved surface: up is the surface normal, and it is\n//    different at every point on the globe. No pitch and yaw pair expresses\n//    it, and integrating towards it drifts and gimbals over the poles.\nconst up = player.pos.clone().sub(planetCentre).normalize();  // surface normal\ncamera.setPosition(eye.x, eye.y, eye.z).lookAt(player, up.negate());\n```\n\nNote the `negate()` in case 3. The surface normal points away from the planet,\nwhich is the direction the player's head points, and `up` wants what should\nappear UP ON SCREEN. This engine renders Y-down, so `(0, -1, 0)` is a level\nhorizon and a world-space \"up\" direction has to be handed over in that sense.\nGet it backwards and the view is upside down, which is the single most likely\nmistake with this argument.\n\nIf the game already computes all three axes, skip the solve entirely:\n\n```js\n// a surface frame the game is already maintaining\ncamera.setBasis(right, up, forward);\n```\n\n`setBasis` takes the camera's OWN columns, where `up` is local +Y and so points\ndown the screen: the opposite sense to `lookAt`'s, one negation apart. It also\nabsorbs a frame that has drifted off orthonormal, which one integrated over\nthousands of steps will have.\n\n**A camera posed this way and a world rotated into a fixed camera's frame are\nthe same picture.** If a game already transforms its world into view space by\nhand, there is nothing to gain by switching; the value of `setBasis` is not\nhaving to write that transform in the first place.\n\n## Turning a direction: `slerp`, not `lerp`\n\n`lerp` walks the straight line between the two tips, which passes INSIDE the\nsphere. The result points the right way at both ends, lingers near them, rushes\nthe middle, and comes out shorter than either input as it crosses the midpoint.\nOn a camera swing that reads as a lurch; on a body walking over a globe it dips\ninto the surface.\n\n`Vector3d#slerp` turns along the great circle instead, at a constant angular\nrate, and carries the length across linearly, so two unit vectors give back a\nunit vector and nothing needs renormalizing.\n\n```js\n// a turret, a camera, a ship: a facing chasing another facing\nfacing.slerp(toTarget, 0.25);\n\n// frame-rate independent, the same shape as math.damp\nfacing.slerp(toTarget, 1 - Math.exp((-6 * dt) / 1000));\n```\n\n`ObservableVector3d` has it too and fires its callback ONCE, so\n`renderable.pos.slerp(...)` is one notification rather than three.\n\n**`Vector2d` has the same method**, and it is not a cut-down copy: in 2D the\nwhole turn is one signed angle, so there is no sine to divide by and no\ndegenerate near-parallel case at all. What it buys a 2D game is the wrap: a\nheading damped as a NUMBER from 170 degrees to -170 degrees sweeps the long 340\ninstead of the short 20, and an arc between directions cannot do that.\n\nTwo degenerate pairs are handled inside rather than left to the caller, and\nboth turn up in ordinary game code:\n\n- **the same direction**, where every path between them is the same path.\n- **opposite directions**, where infinitely many great circles pass through\n  both and none is the shorter way round. The textbook formula divides by a\n  vanishing sine here and hands back `NaN`, which propagates into a position or\n  a matrix and the object disappears. `slerp` picks a stable perpendicular and\n  turns through half a circle instead.\n\nThat second case is why a hand-rolled version is a liability rather than a\nshortcut: anything orbiting a planet reaches the antipode of where it started,\nevery run.\n\n**`math.damp` for a scalar, `slerp` for a direction.** Same job, different\nspace; what `damp` cannot do is keep a vector on the sphere.\n\n## Depth sorting\n\nUnder `Camera3d` the world sorts on `\"depth\"` **every frame** (recursively), so\nunlike 2D you *can* assign `depth` after `addChild` and it will reorder. That is\nthe one place where the usual z-ordering rule is relaxed — see\n`melonjs-renderables` for the 2D rules, which still apply everywhere else.\n\nYou will often *have* to. A `Container` has `autoDepth: true` by default, so\n`addChild(child)` with no explicit z **overwrites `pos.z` with the child's\nindex** — a real world depth in a 3D scene, and never the one you wanted. Pass\nit (`world.addChild(mesh, z)`), or set `mesh.depth` afterwards. The glTF\nimporter turns `autoDepth` off on the container it loads into for this reason.\n\n**`floating` does not opt out of this sort.** It skips the camera *transform*,\nnot the depth *order*. A floating child is ordered by `|pos.z|` alone — its\n`pos.x/y` are screen pixels, and the camera does not move relative to it — so\nthe magnitude is the distance and the sign is ignored:\n\n```js\nworld.addChild(hud, -150);       // small -> nearer than anything -> on top\nworld.addChild(skybox, -10000);  // large -> farther -> behind everything\nworld.addChild(skybox, 100000);  // equally far: sign does not matter\n```\n\nBoth hold at any camera position. A HUD given the huge z that would put it on\ntop in 2D lands at the far end of the level instead, with the scenery drawing\nover it.\n\n## Transparency\n\nA mesh fades by setting its opacity — there is nothing else to switch on:\n\n```js\nghost.setOpacity(0.4);\n```\n\nMeshes render in two phases. The **opaque pass** writes depth in sort order; the\n**transparent pass** replays afterwards, back-to-front, blending and writing no\ndepth. A draw lands in the second whenever its alpha is fractional.\n\nThat default matters because the opaque path writes premultiplied colour with\nblending off, so before this a faded mesh came out **darkened toward black**\nrather than see-through — the background contributed nothing.\n\n**`transparent: true`** when the transparency is in the TEXTURE rather than the\nopacity — a soft-edged glow, smoke, a glTF material with `alphaMode: \"BLEND\"`.\nThe automatic check reads the draw's alpha and cannot see into a texture. Watch\n`alphaCutoff` here: it discards texels *before* blending sees them, so a soft\nedge needs a low cutoff (`Sprite3d` drops its own default to `1/255` when you\nset `transparent: true`). The cutoff thresholds the MATERIAL's alpha, not the\ndrawn alpha, so a fading cutout mesh keeps its shape instead of disappearing at\nits own threshold.\n\nThe glTF loader does **not** set this for you: one loaded mesh can merge several\nmaterials and the flag routes the whole mesh, so a `\"BLEND\"` material sharing\ngeometry with an opaque one would drag the opaque half into the transparent pass.\n\n**`transparent: false`** pins a mesh to the opaque pass however it is faded.\n\n### `depthTest: false` for something with no surface to be occluded on\n\n`transparent` decides the PASS, and the transparent pass turns depth *writing*\noff on its own so overlapping blended draws blend instead of fighting. Whether\ngeometry in front of a mesh **hides** it is a separate question, and\n`depthTest` is where you answer it. The default, `true`, is what you want for\nanything genuinely in the scene — a muzzle flash behind a wall should still be\nbehind the wall, and this is not a synonym for \"additive\".\n\nSet it `false` for a mesh standing in for a screen-space effect: an additive\nglow carrying a world position only so it can sort and move with the thing it\nbelongs to. A world-space caption billboarded over a prop wants it for the\nsame reason, since it has no surface for the prop's own foliage to be in front\nof. Left depth tested, a flat billboard is sliced along a hard straight\nline the moment any geometry is nearer at some pixel. That shows up worst\naround something round — a glow on a rock near a planet's limb gets cut,\nand no offset escapes it, because the near surface bulges further toward the\ncamera than any offset you would dare apply.\n\n```js\nconst halo = new Sprite3d(0, 0, {\n    image: glowTexture,\n    width: rock.radius * 3, height: rock.radius * 3,\n    billboard: \"spherical\",\n    blendMode: \"additive\",\n    transparent: true,     // the blended pass\n    alphaCutoff: 0,        // do not cut the soft falloff\n    depthTest: false,      // and do not let the world slice it\n});\n```\n\nHonoured in the transparent pass on both GPU backends; the Canvas renderer has\nno depth buffer and ignores it.\n\nBlending uses the renderable's existing `blendMode`, so a glow is one property.\nThe advanced modes (`\"overlay\"`, `\"difference\"`, and the rest that need a\ncompositing pass) fall back to `\"normal\"` here, on both backends:\n\n```js\nconst glow = new Mesh(0, 0, {\n    ...quad, texture: glowTexture,\n    transparent: true, blendMode: \"additive\", alphaCutoff: 0,\n});\n```\n\n| | |\n| --- | --- |\n| sorting | **per object**, by distance from the camera |\n| intersecting transparent meshes | may pop as the camera moves — split them, or accept it |\n| `InstancedMesh` | sorts as **one** object; instances draw in buffer order |\n| needs | a GPU backend and a `Camera3d` |\n\nGround shadows ride the same pass — a blob is a decal, and decals are its first\nclient rather than a feature of their own.\n\n**`Trail3d`** is the transparent pass's other obvious client: a camera-facing\nribbon for an exhaust streak, a speed trail or a tyre mark. It is a `Mesh`, so\nit sorts and fogs with everything else here, and like the rest of this tier it\nneeds a `Camera3d`. `Trail` is the 2D one and stays flat under a perspective\ncamera. See `melonjs-particles-and-trails`.\n\n## Distance fog\n\nOff until you ask for it, and one call on the camera:\n\n```js\ncamera.setFog({ near: 2000, far: 7000 });   // linear: name the two distances\ncamera.setFog({ mode: \"exp2\", density: 4e-4 }); // or one density\ncamera.setFog(null);                         // off\n```\n\nIt is the cheapest thing that stops a 3D scene reading as flat cut-outs, and it\nlets props arrive at the far plane without a visible edge.\n\n**Every parameter is optional, and the omitted ones track live.** Distances\ndefault to the camera's own clip planes, so fog cannot silently disagree with\nthem after a later `setClipPlanes`. The colour defaults to\n`renderer.backgroundColor` and follows it, so geometry dissolves into whatever\nsky you already set — including through a day/night fade. Pass `color` only\nwhen the fog should deliberately differ from the backdrop:\n\n```js\ncamera.setFog({ far: 5000, color: \"#8899aa\" });\n```\n\nA `Color` is held by reference, so mutating it animates the fog.\n\n**Height falloff** makes mist pool in low ground instead of hanging at every\naltitude equally — the difference between fog reading as weather and as a global\ndesaturation:\n\n```js\ncamera.setFog({ near: 1200, far: 7000, fogHeight: 0, heightFalloff: 0.0015 });\n```\n\n`heightFalloff` defaults to **0**, which is uniform fog — not a special case,\nthe same integral with the dial at zero, so leaving it out changes nothing.\nRender space is **Y-down**, so `fogHeight` is the floor and density rises\n*below* it; every published form of this formula assumes Y-up and has the\nopposite sign.\n\nFog is measured **radially** from the camera and applied **per fragment**, so\nit does not slide as the camera turns and does not band across large triangles.\nIt lives on the camera, so a split-screen or minimap view fogs independently —\nand a `Camera2d` never fogs at all.\n\n**Per object:** `fog: false` exempts a mesh however far away it is — for a\nwaypoint or objective marker that has to stay readable. It exempts the mesh and\nnot the ground shadow it casts: a blob is a mark on the floor and fogs with the\nfloor. Emissive surfaces fog\nlike everything else (light travelling through fog is attenuated too), so a\nneon sign that should punch through wants `fog: false`, not a brighter\nemissive.\n\nOnly meshes fog. 2D content, HUDs and `floating` renderables never reach the\nmesh shaders, so a screen-space overlay stays clean with no work.\n\n**A custom mesh shader is not fogged unless it asks to be.** Fog is compiled\ninto the engine's own mesh programs — `#define FOG` on WebGL, an `enable_fog`\noverridable constant on WebGPU — and a shader you supply is yours: the engine\nbinds it as written and never substitutes a fogged variant. So a mesh given its\nown `GLShader` keeps full contrast while the scene around it recedes. It is\nsafe — nothing throws, and the camera's fog is simply not applied — but it is\nusually surprising.\n\nA hosted `ShaderEffect` is the other case and needs nothing: its body is\nspliced into the engine's own mesh program as a colour hook, and fog is applied\nto the hook's result, so such a mesh fogs like any other.\n\nTo opt in, declare the same uniforms and the engine will feed them, because the\nfog values are pushed to any mesh program that declares them rather than only to\nthe built-in ones:\n\n```glsl\nuniform vec3 uFogColor;   // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams;  // x = mode (0 off / 1 linear / 2 exp2),\n                          // y = near, z = 1/(far - near), w = density\n```\n\nYour vertex stage computes the distance itself — `length((uViewMatrix *\nuModelMatrix * vec4(aVertex, 1.0)).xyz)`, radially so it does not swim as the\ncamera turns — and the blend must scale the fog colour by the fragment's own\nalpha, `mix(uFogColor * a, rgb, f)`, because `vColor` arrives premultiplied.\nMixing toward the unscaled colour haloes every alpha-cutout edge.\n\nThe flip side is the reason fog costs nothing when unused: with no camera fog,\nthe mesh programs are compiled without any of it, on both backends. It is not a\nbranch that is skipped at runtime — the code is not there.\n\n## Meshes\n\n```js\nconst cube = new Mesh(x, y, {\n    model: \"cube\",          // a preloaded OBJ name — no built-in primitives\n    texture: atlas,         // TextureAtlas, image, or a preloaded image name\n    width: 64, height: 64,\n    cullBackFaces: true,    // the default\n    lit: true,\n});\n```\n\n**A mesh pivots about its model origin `(0, 0, 0)`, not an anchor point.** Under\n`Camera3d` it opts out of the anchor offset entirely, so `anchorPoint` is inert\nthere — place the origin where you want the pivot at authoring time, or nest the\nmesh under a transformed parent. The anchor is only honoured on the legacy\n2D-camera path.\n\n### InstancedMesh, and when it is the wrong tool\n\n**`InstancedMesh`** draws one mesh many times in a single draw call — the\ndifference between a hundred trees and a hundred thousand. glTF scenes using\n`EXT_mesh_gpu_instancing` load as one automatically; by hand it is a `Mesh`\nwith a count:\n\n```js\nconst trees = new InstancedMesh(0, 0, { ...treeGeometry, instanceCount: 400 });\nconst at = new Matrix3d();               // one scratch, reused\nfor (let i = 0; i < trees.instanceCount; i++) {\n    at.identity().translate(x, y, z);\n    trees.setInstance(i, at);\n}\nworld.addChild(trees, 0);\n\ntrees.visibleInstanceCount = 120;        // draw fewer, without re-uploading\n```\n\nIt is not a free upgrade. One `InstancedMesh` is **one geometry and one\nmaterial**, and four things move from per-object to per-group:\n\n| | with `Mesh` | with `InstancedMesh` |\n| --- | --- | --- |\n| depth sort | each object sorts on its own `pos` | the whole set has **one** sort key |\n| ground shadow | one blob per object | one instanced draw for the set |\n| removal | `removeChild`, indices unaffected | `removeInstance(i)` swaps the **last** instance into the hole, so any index you were holding is now wrong |\n| colour | `tint` per object | needs `instanceColors: true` and `setInstanceColor(i, …)` |\n\nSo the question is not \"how many are there\" but **\"does the game address them\nindividually\"**:\n\n- **Scenery — instance it.** Trees, rocks, grass, debris: the game never asks\n  about one of them.\n- **Collision-tested props — still fine.** You test against positions you\n  already own; instancing only changes how they are *drawn*.\n- **Collectibles and enemies — usually not.** Anything removed one at a time\n  makes `removeInstance`'s swap your problem: you have to keep an index↔object\n  map and repair it on every removal. At small counts a pooled `Mesh` each is\n  less code and no slower.\n\nUnder a few hundred objects the draw-call saving is not what limits you\nanyway — reach for it when the count is in the thousands, or when the objects\nare pure scenery and it costs nothing to.\n\n## Normals are generated for you\n\nA `lit` mesh needs per-vertex normals for the shader to light with. Supply them\nif you have them; **omit them and the engine computes them from the geometry**:\n\n```js\nconst mesh = new Mesh(x, y, { vertices, uvs, indices, lit: true });\n// normals derived from the triangles — nothing else to do\n```\n\nYou do not choose flat or smooth, because the geometry already decides. Face\nnormals accumulate into their vertices weighted by area, so where faces **share**\na vertex they average and the surface shades smoothly, and where every triangle\ncarries its **own** three vertices — a triangle soup, which is how most\nhand-built geometry comes out — each vertex belongs to one face and the result\nis that face's normal, so it shades flat. Want faceted edges: duplicate the\nvertices. Want smooth: share them.\n\nAn explicit `settings.normals` always wins, and an unlit mesh gets none — there\nwould be nothing to read them.\n\n**A lit mesh with no normals used to render fullbright**, which looks like the\nlighting is broken rather than absent. If an older scene suddenly picks up\nshading, that is why.\n\n## Colouring a mesh\n\nThere are four levels, and picking the wrong one is the usual reason a colour\n\"does not apply\". They all multiply together.\n\n| level | how | use for |\n|---|---|---|\n| whole object | `mesh.setTint(\"#ff8800\")`, or `mesh.tint.setColor(r, g, b)` | flash on hit, team colour, fading one object |\n| per vertex | `settings.vertexColors`, or `mesh.setVertexColor(i, color)` | a gradient *within* one mesh — distance haze, a darker crease |\n| per material | `textureGroups`, from a multi-material OBJ + MTL | a model whose parts differ, in one draw call |\n| per instance | `new InstancedMesh(…, { instanceColors: true })` then `setInstanceColor(i, color)` | a thousand copies that differ |\n\n**`tint` is per object.** That is the trap: build a terrain as one big mesh and\nyou can tint the whole valley or none of it. Anything that varies *across* a\nsingle mesh is per-vertex.\n\n```js\n// fade a procedural terrain toward the sky the further out it goes\nconst ground = new Color(217, 230, 244);\nconst sky = new Color(207, 230, 247);\nconst haze = new Color();\nfor (let i = 0; i < mesh.vertexCount; i++) {\n    const t = Math.min(1, mesh.originalVertices[i * 3 + 2] / 6000);\n    mesh.setVertexColor(i, haze.copy(ground).lerp(sky, t));\n}\n```\n\nSupply the whole array at construction when you already have it —\n`vertexColors` takes a packed `Uint32Array` (the form the batchers read, so no\nconversion) or one `Color` per vertex. A length that does not match\n`vertexCount` **throws**; it is not padded, because a short array would leave\nthe tail of the mesh mis-coloured and that reads as a lighting bug.\n\nMutating the array directly is fine, but say so afterwards:\n\n```js\nmesh.vertexColors[i] = color.toUint32(color.alpha);\nmesh.needsUpdate = true;   // the retained Camera3d path uploads once\n```\n\n`setVertexColor` does that for you. Skip it and the colour applies under a 2D\ncamera and silently does not under `Camera3d`.\n\nOn a lit mesh the colour multiplies the **lit** result, so it behaves as albedo\nrather than as an emissive override — a vertex colour will not make an unlit\nface bright.\n\n`alpha` hides a mesh as it hides anything else: at 0 the draw is skipped. There\nis no *partial* mesh transparency though — the mesh path renders opaque, so a\nmesh at `alpha = 0.5` draws fully opaque rather than half see-through. Fade a\nmesh out and it will stay solid until it vanishes.\n\n## Moving a mesh in 3D\n\n`Mesh` and `Sprite3d` inherit the ordinary `Renderable` transform helpers, and\nboth are already 3D:\n\n```js\nmesh.rotate(Math.PI / 2, new Vector3d(1, 0, 0));   // axis overload\nmesh.scale(2, 1, 0.5);                             // z is a real argument\n```\n\n`rotate(angle)` alone is the 2D case (about Z); pass a `Vector3d` and it\nrotates about that axis. Both write to `currentTransform`, a **`Matrix3d`**,\nwhich `Mesh` folds into its model matrix — so orienting a mesh costs a matrix\nupdate, not new geometry.\n\nThis matters most for the case it is least obvious for: a horizontal plane.\n`Sprite3d` with `billboard: false` is a quad in the sprite plane — upright —\nand the way to lay one flat (water, a road, a shadow decal) is to rotate it a\nquarter turn about X, **not** to hand-build vertices. The one reason to build\ngeometry instead is tiling: a `Sprite3d` bakes its UVs 0..1 from its atlas\nframe, so a surface that needs `textureRepeat` across many world units still\nwants an explicit `vertices`/`uvs` mesh.\n\n## The mesh pass is opaque\n\nWorth stating plainly, because the symptom does not look like a blending\nproblem: **the mesh pass disables blending**. A soft-edged texture — a glow, a\nsun, a halo, anything with a gradient alpha — composites as a hard-edged disc\nwith a grey rim, which reads as a broken texture rather than as a missing\nfeature.\n\n`transparent: true` routes it through the transparent pass, and `blendMode:\n\"additive\"` on top of that makes a light source add to the sky instead of\nsitting on it. This is the same feature a translucent water surface needs.\n\n## Textures under a perspective camera\n\nA tiling texture on a large ground plane is seen at a grazing angle, and that\nchanges which detail survives:\n\n- **Nothing straight and parallel to the direction of travel.** Every such run\n  converges on the vanishing point, so a few current bands on a river become a\n  fan of rays radiating out of the horizon. Narrowing them and tiling more\n  often only multiplies the rays. Short, scattered, direction-free detail\n  minifies into an even shimmer instead.\n- **`textureFilter: \"linear\"`** for a floor — `\"nearest\"` opts out of mipmaps\n  and anisotropy, and detail aliases into a wash exactly where the camera looks\n  most.\n\n## Ground must outlast the props standing on it\n\nScenery scattered up a slope is placed by its own rule, and terrain is built by\nanother. When the planting reaches further than the mesh does, a low camera\nsees trees standing over open sky at the corners of the frame. Build the ground\nwider than the widest thing planted on it — past the profile's clamp it is a\nflat plateau, so the extra columns cost two triangles and no silhouette.\n\nRecycling has the same trap in Z: an endless runner that recycles tiles a fixed\ndistance behind the *player* forgets that the camera trails further back still,\nand the bottom of the frame falls off the world.\n\n## Sprite3d and billboards\n\n`Sprite3d` is the 2.5D workhorse: a flat sprite living at a real depth, with\n`billboard` controlling how it faces the camera.\n\n| `billboard` | behaviour |\n|---|---|\n| `false` (default) / `\"none\"` | no rotation — a flat plane in the world |\n| `true` / `\"cylindrical\"` | yaws to face the camera, stays upright (characters, trees) |\n| `\"spherical\"` | always fully faces the camera (particles, impostors) |\n\n`\"cylindrical\"` is what you want for paper-thin characters in a 2.5D game.\nBillboarding needs a `Camera3d` drawing the frame; under a 2D camera the quad\nrenders fixed-orientation. **Any other string falls through to the spherical\nbranch**, so a guessed value like `\"upright\"` silently gives you spherical.\n\nA billboard takes its **orientation** from the camera, so `rotate()` has no\neffect on one: a card already turned to face the camera has no free rotation\nleft to give. Everything that is not an orientation still applies, `scale()`\nand `meshScale` included, so animating the size of a shockwave or a pickup is\nthe ordinary call and the cull bounds grow with it:\n\n```js\nconst wave = new Sprite3d(x, y, { image: \"ring\", width: 64, height: 64,\n                                  billboard: \"spherical\", transparent: true });\nwave.scale(1 + 3 * t);          // grows; `scale` accumulates, so drive it\n                                 // from the authored size, not per frame\n```\n\nFor an in-plane rotation, a bolt streak or an exhaust, use `billboard: false`\nand orient the quad yourself.\n\n## Lighting\n\n`Light3d` takes **one argument — the options object** (it has no `x, y, z`\npair; a light carries its own `position`). Types are `\"directional\"` (the\ndefault), `\"ambient\"`, `\"point\"` and `\"spot\"`.\n\n```js\nworld.addChild(new Light3d({ type: \"directional\", direction: [0.3, 1, 0.2] }));\nworld.addChild(new Light3d({ type: \"ambient\", intensity: 0.3 }));\n// a Vector3d works too, wherever the game already has one\nworld.addChild(new Light3d({ type: \"spot\", position: torch.pos, range: 400 }));\n```\n\n`direction` and `position` take an `[x, y, z]` array **or** a `Vector3d`, and\n`color` takes a `Color`, a CSS string or an `[r, g, b]` array — so a value the\ngame already holds can go straight in. Each is read, not retained: move your\nvector afterwards and the light stays where it was.\n\nTwo things still bite, both silently:\n\n- `new Light3d(0, 0, {…})` — the constructor takes **options alone**. JavaScript\n  drops the extra arguments, so `options` becomes the number `0` and every\n  setting in your literal is discarded. The light still appears, on pure\n  defaults: a `type: \"ambient\"` written this way is a second DIRECTIONAL light,\n  and the scene looks plausible enough that nobody checks. TypeScript stops\n  checking a literal once the argument count is wrong, so nothing inside it is\n  verified either.\n- **`direction` is where the light GOES, and this is a Y-down space** — so a sun\n  overhead travels *downward* and its Y is **positive**. Get the sign wrong and\n  the scene is lit from underneath: faces that should be in shade are bright,\n  the ground glows and the sky-facing surfaces go dark.\n\n  ```js\n  direction: [-0.35, 0.8, 0.45]   // sun overhead, late afternoon\n  direction: [-0.35, -0.8, 0.45]  // lit from below — almost never what you want\n  ```\n\n  `position` follows the same convention: a lamp above the floor has a\n  **smaller** y than the floor.\n\nUse **both halves**: with a key light but no ambient, the shadow side of a mesh\ngoes black. With no `Light3d` in the world at all, a `lit: true` mesh falls back\nto a white ambient and renders fullbright — indistinguishable from unlit, which\nis why \"I added `lit: true` and nothing changed\" is the usual first report.\n\nOther defaults worth knowing: `intensity` 1, `color` white, `range` 1000 (point\nand spot, a stylised quadratic falloff, not inverse-square),\n`innerConeAngle` 0 and `outerConeAngle` π/4 (spot).\n\n**`Light2d` is 2D-only** and produces visible artifacts under perspective\nprojection. Do not combine it with `Camera3d`.\n\n**`lit: true` only does anything under a `Camera3d`.** Lighting a mesh needs\nworld-space normals and a world-space fragment position, and the 2D-camera path\nhas neither: its vertices are already the projected output, so there is no world\nspace left to light in. Such a mesh degrades to unlit and warns once, naming the\nfix. If you want lit 3D props in an otherwise 2D game, set\n`cameraClass: Camera3d` on the application. (For 2D **sprites**, lighting is a\nseparate system that works under a 2D camera as normal, see the lighting skill.)\n\nA `lit` mesh takes a **tangent-space normal map**, so relief comes from the\nmaterial rather than from geometry — `settings.normalMap` (a loader key or any\nimage-like source), or MTL `map_bump` / `bump` / `norm`, which the MTL loader\nfetches and applies per material. The tangent frame is derived per fragment, so\nthe model needs no tangent attribute, and an unmapped mesh is unaffected. This\nis the 3D counterpart of the `Sprite` normal maps in the lighting skill; they\nare separate systems and a `Mesh` uses this one.\n\n### What the ground-shadow tier can and cannot do\n\nCheck this table before claiming a setting is unsupported. Every one of these\nis read **every draw**, so all of them can be animated.\n\n| setting | `Mesh` / `Sprite3d` / `GLTFModel` part | `InstancedMesh` |\n|---|---|---|\n| `castGroundShadow` | yes | yes |\n| `shadowGroundY` | yes, the caster's own floor | yes, **one plane** through the SET's origin |\n| `shadowGroundPoint` | yes on a `Mesh` or `Sprite3d`; on a `GLTFModel` part only by setting it after the load, since it is neither a model nor a `level.load` option | **no**, warns and ignores |\n| `shadowGroundNormal` | yes | yes, one plane for the set |\n| `shadowOpacity` | yes | yes |\n| `shadowScale` | yes | yes |\n| `shadowOffset` | yes | yes, one throw for the set |\n| `shadowStretch` | yes | yes, one stretch for the set |\n| `shadowDirectionX` / `Z` | yes | yes |\n| `shadowLight` | yes | yes |\n| height fade as the caster rises | yes | **no** |\n| a different value per instance | n/a | **no**, and there is no plan for one |\n\nThe last two \"no\"s are the same limitation seen twice: a set gets ONE shadow\nplane and ONE shared quad, so the instance's own height is dropped and nothing\ncan vary between instances. Everything else is one value for the whole set,\nwhich is what a set means. A scatter over curved ground is therefore one set\nper flat facet, and a flying scatter wanting a height fade has to be\nper-object meshes.\n\n`shadowGroundPoint` (20.9) is the one genuinely per-object setting, and the\nreason is structural rather than missing work: the instanced tier builds its\nplane THROUGH the set's own origin so the translation's x and z drop out of\nthe intercept, which is then `groundY + lift / nY`. An arbitrary point does\nnot drop out, and that same `nY` divisor is why the 75 degree tilt ceiling\ncannot be lifted there either. `InstancedMesh` warns and ignores rather than\nstoring it, so a game cannot think it took.\n\nApart from that one, if a claim that a setting \"does not work on an\n`InstancedMesh`\" is in your head, it is out of date: `shadowOffset` and\n`shadowStretch` both reached the instanced tier in 20.8.\n\nGround shadows are **on by default** (the `castGroundShadow` application\nsetting), and need a GPU backend and a `Camera3d`. As a blanket default they\nskip geometry with no vertical extent — a ground plane. Per object,\n`castGroundShadow: true`/`false` overrides the app setting and is obeyed as\ngiven, safeguard included; `shadowGroundY` names the floor the blob lands on,\n`shadowOpacity` how dark it is and `shadowScale` how big it is.\n\nOn ground that is not flat, name the floor with `shadowGroundPoint` instead:\na point and `shadowGroundNormal` are a plane, and the blob leaves the caster's\nvertical column to land on it. `shadowGroundY` cannot express that, because\nthe blob is always directly below the caster in world Y, which is only \"below\"\non level ground. It is also what lifts the tilt ceiling, since that ceiling\nmeasures tilt from world up and so assumes a camera looking down the Y axis.\n\nThe height fade follows whichever floor you named. With a point **and** a\nnormal it is measured along that normal; with a point alone it is the\nstraight-line distance to the point. So on a sphere the blob fades with\nALTITUDE rather than with the vertical drop, which is the whole reason the\npoint exists. Two consequences worth knowing, because they fail in opposite\ndirections: a `shadowGroundNormal` facing inward makes the height negative, so\nthe fade clamps to full strength and the blob never fades at all; and a point\non the wrong side of the caster reads as a large height, so the blob\ndisappears outright rather than thinning to a ring the way a backwards\n`shadowGroundY` does. It is also the one shadow setting that THROWS rather\nthan clamping or hiding: a non-finite component is rejected on the spot.\n\nThe blob is an ellipse sized to the caster's own footprint and placed at the\ncaster's x/z, or at `shadowGroundPoint` when one is set. So a tall or narrow object (a character, a tree, a pickup) shows\nits shadow clearly, while a wide, flat-bottomed one resting on the floor covers\nits own completely from a camera looking down at it. That is the shadow\nbehaving correctly, not a bug.\n\n**Do not chase it by raising `shadowGroundY`.** Lifting the plane does not slide\nthe blob out from under the object, it floats the blob *up* — and past a few\nunits it projects over the top of the caster as a dark halo ringing it.\n\nThe simplest fix is a bigger blob, so its edge shows past the object:\n\n```js\nmesh.shadowScale = 1.5;        // 1 is the default; 0 hides the shadow\n```\n\n`shadowScale` works everywhere, `InstancedMesh` included. Changing it on a\nset rewrites the shared quad's four corners in place rather than rebuilding\nit, as does a `shadowLight` that moves, so an animated sun costs no\nallocation.\n\nOr, for the look of a sun that is not overhead, use the shape controls:\n\n```js\nmesh.shadowOffset = 1.5;       // slide it out, in blob radii\nmesh.shadowDirectionX = 1;     // the ground direction it is cast along\nmesh.shadowDirectionZ = 0;\nmesh.shadowStretch = 2;        // and lengthen it along the same line\n```\n\n`shadowOffset` is measured in multiples of the blob's **own radius**, not in\nworld units, so one value serves a boulder and a pebble and every part of a\nglTF model. About `stretch - 1` is what keeps the shadow attached to its\ncaster. It is honoured **only when the floor has been named**, by\n`shadowGroundY` or by `shadowGroundPoint`, because the\nblob is a flat quad on one named plane: sliding it across a plane the game has\nnot named puts it where there may be no floor. `shadowStretch` is clamped to 3\nand the blob fades as it pulls, so an extreme value degrades to nothing rather\nthan to a smear.\n\nAn `InstancedMesh` honours both, as **one** offset and **one** stretch for the\nwhole set, which is all a shared set can carry. Both ride the quad the blobs\nshare rather than the matrix that also places them: a stretch in the matrix\nwould scale the instance positions too and smear the scatter instead of\nlengthening its blobs. So a scatter can sit next to loose props under the same\nsun without looking lit from somewhere else.\n\n`shadowLight` takes the direction from a `Light3d` you name, re-read every\ndraw, so a moving sun carries the shadows with it. Nothing is inferred: the\nengine never picks a dominant light, because a scene with several has no\nnon-arbitrary answer and one with none has no answer at all.\n\n### A floor that is not level\n\n`shadowGroundY` says where the floor is; `shadowGroundNormal` says which way\nit faces:\n\n```js\nprop.shadowGroundY = floorY;\nprop.shadowGroundNormal = [Math.sin(0.35), -Math.cos(0.35), 0];  // a 20 deg slope\n```\n\nWorld up is `(0, -1, 0)`, because render space is Y-down, and that is the\ndefault. The blob is **rotated** onto the plane rather than projected, so it\nkeeps its size; it sits directly under the caster, since the normal turns it\nrather than moving it, unless `shadowGroundPoint` says where instead. It needs\nthe floor named, by `shadowGroundY` or `shadowGroundPoint`, for the same\nreason the offset does: tilting the bounds fallback has no anchor to turn\nabout.\n\nThe tilt is clamped at 75 degrees alongside `shadowGroundY`, past which an\nedge-on blob has nothing left to show. That ceiling measures tilt from WORLD\nup, which stands in for \"tilted away from the viewer\" only while the camera\nlooks down the Y axis, so it is lifted alongside `shadowGroundPoint`, where a\npoint and a normal state the plane outright.\n\nOn an `InstancedMesh` one plane serves the **whole set**: it passes through\nthe set's origin at `shadowGroundY`, faces the normal, and every blob is\nprojected onto it whatever height its own instance sits at, exactly as the\nlevel case puts every blob at one height. A scatter over curved ground is\ntherefore one set per flat facet, each with the facet's own plane, not one\nset with an averaged normal.\n\nAll three compose on a set: the stretch turns the shared quad, the offset\nslides the whole set along the light, and both are carried onto the tilted\nplane rather than applied flat, so a bank's planting lies along its slope\ninstead of hovering across it.\n\nGive blobs to the plants that stand apart. Dense low ground cover is better\nleft without: a thousand blobs each as wide as its own fern sum to a darkened\nhillside, not to a thousand shadows, and ground cover is the ground.\n\nThese are **art direction, not a projection**. A stretched ellipse is not a\nsilhouette and has no contact with terrain, so on ground that is not the plane\nyou named it will not lie on it. Direction-correct shadows want a shadow map,\nwhich this tier does not do.\n\n### Get the sign right: the floor is a GREATER y\n\nRender space is **Y-down**, so the floor an object stands on is a *larger* y\nthan the object itself. Nudging a shadow plane below a surface is therefore a\n**plus**:\n\n```js\nmesh.shadowGroundY = WATER_LEVEL + 8;   // just under the surface — correct\nmesh.shadowGroundY = mesh.pos.y - 8;    // eight units ABOVE its own base — wrong\n```\n\nGet it backwards and the blob becomes a horizontal quad slicing through the\ncaster's own body. It is still drawn at full strength — the height fade sees a\n*negative* height and clamps to 1, so nothing warns you — but the depth test\nhides every part of it that falls inside the caster's silhouette. What survives\nis a thin ring, only as wide as the blob overhangs the object.\n\nThe symptom is distinctive and easy to misread: **shadows appear to switch on as\ncasters approach the camera.** That ring is a few pixels on the nearest object\nand sub-pixel further out, so a scene reads as \"only nearby things have\nshadows\" — which looks like culling, an LOD stage or a fog problem, none of\nwhich it is. Before chasing any of those, dump `shadowGroundY` against the\ncaster's own y and check which one is larger.\n\n## Collision in 3D\n\nUse a body with a depth shape, not a hand-rolled distance check. Two shapes\ncarry depth — `Box3d` and `Sphere` — and any pair of them is resolved in three\ndimensions; the response carries `overlapNZ` / `overlapZ` for the depth axis.\n\nPick by whether the thing turns. `Box3d` is world-axis-aligned and cannot be\noriented, so it fits walls, floors and crates and misfits anything that\ntumbles. `Sphere` has no orientation at all, which makes it the shape for\nspinning debris, projectiles, and anything spread over a curved surface: on a\nglobe each object's own \"up\" points a different way, so one axis-aligned box\ncannot fit any of them.\n\nA hand-rolled check is the usual mistake here and it costs twice — it throws\naway the penetration vector the response already carries, and it is invisible\nto the debug plugin's hitbox overlay, which draws from `body.shapes`. If the\nred boxes never appear, the reason is usually that there is no body to draw.\n\nAgainst a planar shape, either 3D shape degrades to its XY footprint with the\nplanar side read as unbounded along Z, so a sphere or box body still collides\nwith an ordinary collision layer, just without a depth result. And mind the\nnormal: a box pair separates along one world axis, a sphere along the line\nbetween two centres, so a sphere contact fills the 2D and depth halves of the\nresponse at the same time. See the physics skill for the full contract.\n\n## glTF / GLB scenes\n\nLoaded through the same level director as everything else:\n\n```js\nawait loader.preload([{ name: \"diorama\", type: \"glb\", src: \"data/diorama.glb\" }]);\nlevel.load(\"diorama\", { scale, castGroundShadow, shadowGroundY, onLoaded });\n\n// every ground-shadow setting travels except shadowGroundPoint, which is\n// per object and dropped by a scene load: a loaded scene can\n// be told where its sun is, and `shadowLight` is re-read every draw\nlevel.load(\"diorama\", {\n    scale, castGroundShadow: true, shadowGroundY: 0,\n    shadowLight: sun, shadowOffset: 0.6, shadowStretch: 1.8, async: true,\n});\n```\n\n`.obj` / `.mtl` are also supported loader types.\n\nAn **animated** asset instantiates as a `GLTFModel`, which is a game object as\nmuch as a scene: construct it straight from the parsed descriptor, and place it\nwith `pos` / `depth` / `rotate` / `scale` like any other renderable — the\nplacement drives the whole rig and composes with the playing clip.\n\n```js\nconst model = new GLTFModel(loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nmodel.setCurrentAnimation(\"paddle\", { loop: true });\nmodel.pos.set(x, y);\nmodel.depth = z;\n```\n\nTo scatter an authored mesh yourself, read the geometry off the descriptor and\nhand it to an `InstancedMesh` — `loader.getGLTF(name).nodes[0]` carries\n`vertices` / `uvs` / `normals` / `indices`. That wants a single merged\nprimitive exported at the origin; see `melonjs-3d-assets`.\n\n`melonjs-3d-assets` covers the rest: every `level.load` option, which material\nfeatures are imported, imported lights and their i","packages/melonjs/skills/melonjs-getting-started/SKILL.md":"---\nname: melonjs-getting-started\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Getting started with melonJS\n\n> Bootstrapping a melonJS 20.x game: the `Application` lifecycle, the mandatory\n> `await app.init()`, the world container, and what the renderer fallback means\n> for feature availability.\n\n**Applies to:** melonJS 20.x. Older major versions differ — see the\n`melonjs-20-migration` skill.\n\n## The bootstrap\n\n`Application` construction is synchronous but **initialisation is not**. `init()`\nreturns a promise and must be awaited before building any renderable, loading\nassets, or entering a scene.\n\n```js\nimport { Application, Text } from \"melonjs\";\n\nconst app = new Application(1218, 562, {\n    parent: \"screen\",      // id of the DOM element to mount into\n    scale: \"auto\",\n    backgroundColor: \"#202020\",\n});\nawait app.init();          // ← REQUIRED. Nothing below works without it.\n\napp.world.addChild(\n    new Text(609, 281, {\n        font: \"Arial\",\n        size: 160,\n        fillStyle: \"#FFFFFF\",\n        textAlign: \"center\",\n        textBaseline: \"middle\",\n        text: \"Hello World !\",\n    }),\n);\n```\n\n**This is the single most common thing to get wrong.** On 19.x the constructor\ncalled `init(width, height, options)` for you; on 20.x `init()` takes no\narguments and calling — and awaiting — it is your job. Nothing that follows\nnames the missing call. The constructor does build `app.world`, so adding a\nplain renderable succeeds and simply never draws — no canvas is appended and the\napp never subscribes to the frame tick — while constructing a `Sprite` or a\n`Text` throws `Cannot read properties of undefined (reading 'renderer')`,\nbecause the global `game` those two fall back on is only set once the first\n`init()` resolves. Either way `app.renderer` and `app.viewport` stay `undefined`\nand the page stays blank.\n\n`init()` is asynchronous because acquiring a WebGPU device is asynchronous. There\nis no synchronous alternative.\n\n## Top-level await\n\nThe bootstrap needs `await` at module scope. In an ES module that is fine. If the\nsurrounding tooling does not allow it, wrap it:\n\n```js\nasync function main() {\n    const app = new Application(800, 600, { parent: \"screen\" });\n    await app.init();\n    // …\n}\nvoid main();\n```\n\n## Choosing a renderer\n\n`video.AUTO` (the default) tries **WebGPU → WebGL 2 → Canvas** and uses the first\nthat works.\n\n```js\nimport { video } from \"melonjs\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    renderer: video.AUTO,      // default; or video.WEBGPU / video.WEBGL / video.CANVAS\n});\n```\n\nTwo consequences worth knowing before writing rendering code:\n\n- **Which backend you get is a runtime fact, not a build-time one.** The same\n  code runs on a different renderer depending on the machine. Do not assume\n  WebGL.\n- **Not every feature exists on every backend.** Shader effects and post effects\n  need a programmable pipeline and are inert on Canvas. Where a feature degrades,\n  the engine warns once rather than throwing — so a missing effect is a console\n  warning, not a crash.\n\nPin the renderer explicitly (`video.WEBGL`) only when you have a reason to. It\ncosts you the WebGPU path and the Canvas fallback.\n\n## The scene graph\n\n`app.world` is the root `Container`. Everything drawn is a descendant of it.\n\n```js\napp.world.addChild(sprite, 10);   // 10 is the z / draw order\n```\n\n**Pass the z-order as the second argument to `addChild`** — it is the only form\nthat works. `renderable.z` does not exist as a property, setting `depth` before\n`addChild` is overwritten by `autoDepth`, and setting it afterwards does not\nresort. See the `melonjs-renderables` skill for the full explanation.\n\n## Scenes\n\nFor anything beyond a single screen, use `Stage` subclasses and the `state`\nmanager rather than mutating `app.world` directly:\n\n```js\nimport { Stage, state } from \"melonjs\";\n\nclass PlayScreen extends Stage {\n    onResetEvent() { /* build the scene */ }\n    onDestroyEvent() { /* tear it down */ }\n}\n\nstate.set(state.PLAY, new PlayScreen());\nstate.change(state.PLAY);\n```\n\n`onResetEvent` runs every time the state is entered, not once — build scene\ncontent there, not in the constructor.\n\n## Loading assets\n\nAssets are declared and preloaded through `loader`; audio needs `audio.init()`\nwith the formats you ship before any audio asset is preloaded.\n\n```js\nimport { audio, loader } from \"melonjs\";\n\nloader.setOptions({ crossOrigin: \"anonymous\" });   // needed for remote assets\naudio.init(\"mp3,ogg\");                             // BEFORE preloading audio\nawait loader.preload([\n    { name: \"tileset\", type: \"image\", src: \"data/img/tileset.png\" },\n    { name: \"jump\",    type: \"audio\", src: \"data/sfx/\" },   // directory, not file\n]);\n```\n\nTwo ordering rules that produce confusing failures when missed:\n\n- **`audio.init()` must run before any audio asset is preloaded**, and names the\n  formats you actually ship. Preloading first throws *\"target audio extension(s)\n  should be set through me.audio.init() before calling the preloader\"*.\n- The audio `src` is a **directory**, not a file. The loader appends the asset\n  `name` plus each configured extension, so a full filename gives you a 404.\n\n## `anchorPoint` defaults to the centre\n\n`Renderable.anchorPoint` is `(0.5, 0.5)`, so `pos` is an object's **centre**, not\nits top-left. `Container` forces `(0, 0)` instead.\n\nWriting top-left math against a centred anchor puts everything half a sprite off\nwith no error — it is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Common mistakes\n\n| symptom | cause |\n|---|---|\n| blank page, nothing drawn, or a `TypeError` reading `renderer` from `new Sprite` / `new Text` | missing `await app.init()` |\n| objects draw in the wrong order | `.z`/`depth` set instead of `addChild(child, z)` — `.z` is not even a real property |\n| shader/post effect does nothing, warning in console | running on the Canvas renderer |\n| audio 404s | audio asset `src` given as a file rather than a directory |\n| scene content appears once and never resets | scene built in the constructor instead of `onResetEvent` |\n| everything offset by half a sprite | top-left math against the centred `anchorPoint` default |\n| a `Tween` or `ParticleEmitter` does nothing | `.start()` / `streamParticles()` never called |\n| cross-origin textures fail to load | missing `loader.setOptions({ crossOrigin: \"anonymous\" })` |\n\n## Related skills\n\n- `melonjs-20-migration` — what changed in 20.x that older code and older\n  training data get wrong\n- `melonjs-renderables` — custom `Renderable` subclasses, pointer events, post\n  effects and the traps in each\n","packages/melonjs/skills/melonjs-20-migration/SKILL.md":"---\nname: melonjs-20-migration\ndescription: \"Use this skill when writing melonJS code that may be based on pre-20.x knowledge, when migrating a game from melonJS 19.x or earlier, or when an API you expected does not exist. Covers the mandatory async Application.init(), the WebGL 2 baseline and WebGPU backend, renderable post effects replacing the shader property, and the deprecated exports still present. Triggers on: migration, upgrade, melonJS 19, me.game, me.video.init, renderable.shader, Compositor, CanvasTexture, me.Math, deprecated.\"\nlicense: MIT\n---\n\n# Migrating to melonJS 20.x\n\n> What changed in melonJS 20.x, and which older APIs are gone, deprecated, or\n> renamed. Read this before writing melonJS code from memory — much of what is\n> published about melonJS predates 20.0.\n\n**Why this skill exists:** most melonJS material in circulation describes 19.x or\nearlier. Code written from that knowledge looks correct and fails at runtime.\nThe items below are ordered by how likely they are to be reproduced from memory.\n\n## 1. `Application.init()` is mandatory and asynchronous\n\nSince 20.0, constructing an `Application` no longer initialises it.\n\n```js\n// ✗ pre-20.x — the constructor called init(width, height, options) for you\nconst app = new Application(800, 600, { parent: \"screen\" });\napp.world.addChild(sprite);          // added, but nothing ever renders\n\n// ✓ 20.x\nconst app = new Application(800, 600, { parent: \"screen\" });\nawait app.init();                    // REQUIRED\napp.world.addChild(sprite);\n```\n\n`init()` is async because acquiring a WebGPU device is async. There is no\nsynchronous alternative, and no automatic fallback that papers over the missing\ncall. Nothing that then breaks names it: the constructor still builds\n`app.world`, so children can be added and simply never draw — no canvas is\nappended, the app never subscribes to the frame tick, and `app.renderer` /\n`app.viewport` stay `undefined` — while constructing a `Sprite` or `Text` first\nthrows a `TypeError` on the unset global `game` instead.\n\nThe signature changed too: 19.x took `init(width, height, options)`, 20.x takes\nnone, and anything passed is ignored (settings come from the constructor).\n\n**`video.init(...)` no longer exists.** The `video` module now exports only the\nfour renderer constants (`AUTO`, `CANVAS`, `WEBGL`, `WEBGPU`) — there is no\n`init` on it to call. Any code shaped like `me.video.init(800, 600, {...})` is\nfrom a removed API. Build an `Application` and await `init()`.\n\nNote that `import * as me from \"melonjs\"` still works, so `me.Sprite` style is\nfine. It is the *globals bootstrap* that is gone, not the namespace import.\n\n## 2. Post effects replaced the `shader` property\n\n`renderable.shader = …` is **deprecated since 19.2.0**. Use the post-effect API:\n\n```js\n// ✗ deprecated\nmySprite.shader = new ShaderEffect(app.renderer, glslBody);\n\n// ✓\nmySprite.addPostEffect(new ShaderEffect(app.renderer, glslBody));\n```\n\nRelated trap: **`removePostEffect()` destroys the effect** — and so does\nassigning `renderable.shader`, which destroys whatever it replaces. Only an\neffect carrying `shared = true` is spared. To turn one off temporarily, toggle\n`effect.enabled` instead: remove-and-re-add hands you back an object whose GPU\nresources have already been freed.\n\n## 3. WebGL 2 is the baseline; WebGPU is the default attempt\n\n`video.AUTO` now tries **WebGPU → WebGL 2 → Canvas**. WebGL 1 is gone.\n\nConsequences:\n\n- Which backend runs is a runtime fact. Do not write code that assumes WebGL.\n- Custom shaders can carry both GLSL and WGSL in one asset, so one effect runs\n  on either GPU backend.\n- Features needing a programmable pipeline are inert on Canvas and warn once\n  rather than throwing.\n\n## 4. Deprecated exports still present\n\nThese resolve but should not be used in new code:\n\n| deprecated | since | use instead |\n|---|---|---|\n| `CanvasTexture` | 17.1.0 | `CanvasRenderTarget` |\n| `Compositor` | 18.1.0 | `WebGLBatcher` |\n| `PrimitiveCompositor` | 18.1.0 | `PrimitiveBatcher` |\n| `QuadCompositor` | 18.1.0 | `QuadBatcher` |\n| `Math` (capitalised) | 18.0.0 | `math` (lowercase) |\n| `device.requestFullscreen` / `exitFullscreen` | 19.7.0 | `app.requestFullscreen()` / `app.exitFullscreen()` |\n| `renderable.shader = …` | 19.2.0 | `addPostEffect()` |\n| `Entity` | 18.1.0 | `Sprite` / `Renderable` + a `Body` |\n| `loader.onload` / `onProgress` / `onError` | 18.2.0, **removed 20.3** | the `LOADER_*` events, or `preload(assets, onloadcb)` |\n| `response.overlap` / `overlapN` / `overlapV` | — | `response.depth` / `response.normal` |\n| `setLineWidth()` | 17.3.0 | the `lineWidth` property |\n\nTwo of these deserve emphasis:\n\n- **`Entity`** is still exported, so code using it compiles and runs while being\n  the wrong shape for 20.x. Compose a `Sprite`/`Renderable` with a `Body`\n  instead.\n- **The capitalised `Math`** shadows the global `Math` if imported carelessly.\n  `me.Math.random()` from older docs resolves to the deprecated re-export.\n\n## 5. The global `game` is not the modern shape\n\n`game` is still exported, but since 20.0 it names the most recently\n**initialized** `Application` and is `undefined` until the first `init()`\nresolves — so anything reading it at module scope gets `undefined`. Take the\n`Application` instance as a parameter instead: `Stage#onResetEvent` and\n`onDestroyEvent` are handed it, and any renderable can reach it through\n`parentApp`.\n\n## 6. Node and SSR\n\nmelonJS imports cleanly under Node — useful for isomorphic apps and for tooling.\nIt has **no runtime dependencies** as of 20.3. Do not add polyfills for\n`globalThis`, `String.trimStart` or `String.trimEnd`: the published bundle\ntargets ES2022, so every environment that can parse it already has them.\n\n## 7. Browser support\n\nThe published bundle targets **ES2022** and uses private class members. A browser\nthat predates that cannot parse the file at all — it fails at load, not when a\nfeature is used. Transpile if you need older browsers, and note that transpiling\nhandles *syntax* only: built-in methods need separate polyfills, which a\nbundler's `target` setting will never add.\n\n## The shipped examples are not all current\n\nThe examples in this repository are the best reference for how melonJS is\nassembled, but a few contain stale comments or legacy calls. Do not learn these\nfrom them:\n\n- `webgpu/` has a comment claiming *\"`video.AUTO` never selects the WebGPU\n  backend\"*. That is pre-20 leftover — AUTO tries WebGPU **first**. The code is\n  right; the comment is wrong.\n- Several examples assign `viewport.shader = …` / `sprite.shader = …`, which is\n  deprecated. `addPostEffect()` is current.\n- Five files set `this.z = Number.POSITIVE_INFINITY` on a HUD. That property\n  does not exist; those HUDs draw on top only because they are added last.\n- `platformer/entities/player.ts` reads `response.overlapV`, the deprecated\n  field. Use `response.depth` / `response.normal`.\n- Particle speeds tuned before 20.2 look wrong now: the particle transform fix\n  changed how far a burst travels, and emitters were retuned upward to match.\n\n## Checklist when code \"should work\" but doesn't\n\n1. Is `await app.init()` there?\n2. Is the code assuming WebGL when it might be running on Canvas or WebGPU?\n3. Is it using `renderable.shader =` instead of `addPostEffect()`?\n4. Is it importing a deprecated symbol from the table above?\n5. Is it referencing a global `game`?\n\n## Related skills\n\n- `melonjs-getting-started` — the correct 20.x bootstrap in full\n- `melonjs-renderables` — post effects, pointer events and custom draw code\n","packages/melonjs/skills/melonjs/SKILL.md":"---\nname: melonjs\ndescription: \"Use this skill FIRST for any melonJS task; it routes to the right specialised skill and lists the companion packages. melonJS is an open-source 2D, 2.5D and 3D HTML5 game engine running on WebGPU, WebGL 2 or Canvas. Covers Application setup, the scene graph, renderables, sprites, tilemaps, physics, audio, particles, shaders, and the 3D tier — perspective cameras, meshes and glTF scenes. Triggers on: melonjs, melonJS, me.game, Application, app.init, app.world, Renderable, Sprite, Stage, state.change, Camera2d, Camera3d, Mesh, tilemap, Tiled, TMX, ParticleEmitter, ShaderEffect, how do I make a game, HTML5 game engine.\"\nlicense: MIT\n---\n\n# melonJS\n\nEntry point for the melonJS skill collection. melonJS is an open-source HTML5\ngame engine for indie developers, covering 2D, 2.5D and 3D — perspective and\northogonal cameras, GPU-accelerated tilemap rendering, post-processing effects,\ncustom shaders, 3D meshes, polygon-accurate physics and modern Tiled workflows.\nIt runs on WebGPU, WebGL 2 or Canvas with automatic fallback, and has no runtime\ndependencies.\n\n## How to use this skill\n\n1. Find the specialised skill below that matches the task and follow it.\n2. If the task involves anything 3D, read `melonjs-3d` **first** — melonJS's\n   coordinate conventions are the inverse of OpenGL's, so untutored 3D instincts\n   are backwards.\n3. If you are writing melonJS from prior knowledge rather than from the current\n   docs, read `melonjs-20-migration` — most published melonJS material predates\n   20.0 and its bootstrap no longer exists.\n4. If none of them covers the task, use the escape hatch below rather than\n   guessing at an API.\n\n## Skill router\n\n| Skill | Load when… |\n|---|---|\n| [melonjs-getting-started](../melonjs-getting-started/SKILL.md) | Bootstrapping a game, `new Application`, `await app.init()`, choosing a renderer, preloading assets. |\n| [melonjs-20-migration](../melonjs-20-migration/SKILL.md) | Code based on melonJS 19.x or earlier, or an API that \"should exist\" but doesn't — `video.init`, `Entity`, `renderable.shader =`. |\n| [melonjs-scenes-and-state](../melonjs-scenes-and-state/SKILL.md) | `Stage` subclasses, the state manager, transitions, pausing, the update loop, timers, tweens, saved data. |\n| [melonjs-renderables](../melonjs-renderables/SKILL.md) | Subclassing `Renderable`, custom `draw()`, draw order, anchors, `floating`, post effects on an object. |\n| [melonjs-sprites-and-animation](../melonjs-sprites-and-animation/SKILL.md) | `Sprite`, frame animation, texture atlases (TexturePacker / Aseprite / ShoeBox), tint, flip, pooling. |\n| [melonjs-input](../melonjs-input/SKILL.md) | Keyboard actions, pointer and touch handling, drag, gamepad, virtual controls. |\n| [melonjs-physics](../melonjs-physics/SKILL.md) | Collision, bodies, movement, spatial queries, and the planck / matter adapters. |\n| [melonjs-math](../melonjs-math/SKILL.md) | Vectors and matrices: what mutates, what allocates, and interpolating with lerp, slerp, damp or moveTowards. |\n| [melonjs-tilemaps](../melonjs-tilemaps/SKILL.md) | Tiled maps — TMX/TSX loading, spawning entities from objects, collision layers, isometric maps. |\n| [melonjs-audio](../melonjs-audio/SKILL.md) | Sound effects, music, audio sprites, spatial audio, procedural tone and noise. |\n| [melonjs-effects-and-shaders](../melonjs-effects-and-shaders/SKILL.md) | Post effects, custom GLSL/WGSL shaders, blend modes, colour grading, screen capture. |\n| [melonjs-3d](../melonjs-3d/SKILL.md) | Anything 3D or 2.5D — `Camera3d`, meshes, instancing, `Sprite3d` billboards, `Light3d`, distance fog, glTF scenes. |\n| [melonjs-3d-assets](../melonjs-3d-assets/SKILL.md) | Loading glTF/GLB or OBJ models — materials, imported lights, instancing, and what is not supported. |\n| [melonjs-camera-and-drawing](../melonjs-camera-and-drawing/SKILL.md) | Camera follow, bounds, shake and fade, coordinate conversion, and immediate-mode shape drawing. |\n| [melonjs-ui-and-text](../melonjs-ui-and-text/SKILL.md) | HUDs, buttons, menus, drag-and-drop, `Text` and `BitmapText`, web fonts, panels. |\n| [melonjs-particles-and-trails](../melonjs-particles-and-trails/SKILL.md) | `ParticleEmitter`, burst versus stream, `referenceSpace`, and the `Trail` renderable. |\n| [melonjs-lighting](../melonjs-lighting/SKILL.md) | 2D dynamic lighting — `Light2d`, stage ambient, normal-mapped sprites. |\n| [melonjs-deployment](../melonjs-deployment/SKILL.md) | Scaffolding a project, building, browser support and transpiling, native and mini-game platforms. |\n| [melonjs-loading-assets](../melonjs-loading-assets/SKILL.md) | Resource descriptors, the asset type list, base URLs, cross-origin, progress events, unloading. |\n| [melonjs-renderer-backends](../melonjs-renderer-backends/SKILL.md) | A feature works on one machine but not another; choosing or pinning a backend; degrading gracefully. |\n| [melonjs-display-and-scaling](../melonjs-display-and-scaling/SKILL.md) | Blurry, soft or cropped output; the design resolution, how the canvas fills its page, and pixel-perfect integer scaling. |\n| [melonjs-events](../melonjs-events/SKILL.md) | The event bus, per-frame and lifecycle events, and subscribing without leaking. |\n| [melonjs-performance](../melonjs-performance/SKILL.md) | Dropped frames, draw-call counts, pooling, batching, culling, baking, profiling. |\n| [melonjs-plugins](../melonjs-plugins/SKILL.md) | Registering the debug, Spine or Tiled-inflate plugins, physics adapters, or writing your own plugin. |\n\n\n## Escape hatch: the full API index\n\nThese skills are hand-written and deliberately partial — they cover the paths\npeople actually take and the mistakes those paths invite. When the task needs\nsomething they do not mention, do **not** infer an API from the shape of the\nengine. Fetch the generated index instead:\n\n<https://melonjs.github.io/melonJS/llms.txt>\n\nIt is regenerated on every docs build from the same TSDoc comments the reference\npages render, so it is always current for `master`. It lists every exported\nclass, function, interface, type and namespace with a one-line summary and a\nlink to its page — fetch the pages you need from there.\n\nTwo things it gives you that this collection cannot:\n\n- **Members.** The skills name classes; `llms.txt` links to the page listing\n  every method, property, default and `@since` tag on them.\n- **Deprecations.** Entries marked `**deprecated**` are exactly the APIs that a\n  model trained on older melonJS material will reach for. If something you were\n  about to write appears with that marker, follow the page for the replacement.\n\nIf a class, function, namespace or type is absent from `llms.txt`, it is absent\nfrom the engine's public API — that is the useful negative answer, and it is\nworth acting on rather than writing code around something that does not exist.\nThe index lists top-level exports only, so a *member* missing from it proves\nnothing: follow the owning class's page to check that.\n\n## The three rules that catch everyone\n\nWhatever the task, these produce code that runs and is wrong:\n\n1. **`await app.init()` is mandatory** since 20.0. The constructor builds the\n   world but not the renderer: a plain renderable can still be added and simply\n   never draws, while `new Sprite(...)` / `new Text(...)` throw a `TypeError`\n   on the global `game`, which is unset until the first `init()` resolves.\n   Neither symptom names the missing call.\n2. **`addChild(child, z)` is the only way to set draw order.** `renderable.z`\n   does not exist; setting `depth` before `addChild` is overwritten, and setting\n   it after does not resort (except under `Camera3d`).\n3. **`isKinematic` defaults to `true`**, which silently excludes a renderable\n   from pointer events and from the physics broadphase. Plain `Renderable` and\n   `Sprite` subclasses need `this.isKinematic = false`.\n\n## Companion packages\n\nmelonJS ships optional packages alongside the engine. Reach for these rather\nthan reimplementing what they cover — and note the physics adapters and the\nTiled inflate plugin are *required* for their use cases, not merely convenient.\n\n| Package | Use when |\n|---|---|\n| [`@melonjs/planck-adapter`](https://www.npmjs.com/package/@melonjs/planck-adapter) | You want full rigid-body dynamics — stacking, joints, realistic restitution. Pass it as the Application's `physic` setting; the portable `raycast` and `queryAABB` calls work unchanged across adapters (`querySphere` and `raycast3d` are built-in-adapter only). |\n| [`@melonjs/matter-adapter`](https://www.npmjs.com/package/@melonjs/matter-adapter) | Same, backed by matter-js. Choose one adapter per game. |\n| [`@melonjs/tiled-inflate-plugin`](https://www.npmjs.com/package/@melonjs/tiled-inflate-plugin) | **Required** to load gzip-, zlib- or zstd-compressed Tiled maps. Without it, parsing such a map throws `No inflate function set`. |\n| [`@melonjs/spine-plugin`](https://www.npmjs.com/package/@melonjs/spine-plugin) | Skeletal animation authored in Spine, as a melonJS renderable. |\n| [`@melonjs/debug-plugin`](https://www.npmjs.com/package/@melonjs/debug-plugin) | An in-game debug panel — FPS, draw counts, collision-shape and bounds overlays. Register it after `app.init()`. |\n| [`@melonjs/capacitor-plugin`](https://www.npmjs.com/package/@melonjs/capacitor-plugin) | Wrapping a game as a native iOS/Android app: it bridges Capacitor's lifecycle — pause/resume, the hardware back button, orientation lock, splash screen. |\n| [`create-melonjs`](https://www.npmjs.com/package/create-melonjs) | Scaffolding a new project — start here rather than assembling a build by hand. |\n\nPlugins are registered **after** `init()`, into a global registry:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { DebugPanelPlugin } from \"@melonjs/debug-plugin\";\n\nawait app.init();\nplugin.register(DebugPanelPlugin, \"debugPanel\");\n```\n\nPhysics adapters are different — they are an `Application` setting, not a\nplugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    // gravity is in pixels/s²; pixelsPerMeter converts it to the adapter's\n    // metres. Both are shown at their defaults; subSteps defaults to 1\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },\n        pixelsPerMeter: 32,\n        subSteps: 2,\n    }),\n});\nawait app.init();\n```\n\n`melonjs-plugins` covers the registry, the version gate and writing your own.\n\n## Prefer built-in features\n\nmelonJS covers a lot that is easy to reimplement badly. Before hand-rolling,\ncheck for: `ParticleEmitter` and `Trail`, `Tween` with easing, the built-in\n`ShaderEffect` presets (vignette, scanline, chromatic aberration, glow, dissolve\nand more), `Light2d` / `Light3d`, `UIBaseElement` / `UISpriteElement` /\n`UITextButton`, `NineSliceSprite`, `TextureAtlas` (TexturePacker, ShoeBox,\nAseprite), `BitmapText`, `CanvasRenderTarget` for bake-once drawing,\n`NoiseTexture2d`, `viewport.shake` / `fadeIn` / `fadeOut`, and `save` for\nlocalStorage-backed persistence.\n","packages/melonjs/skills/melonjs-renderables/SKILL.md":"---\nname: melonjs-renderables\ndescription: \"Use this skill when subclassing Renderable, writing a custom draw() method, handling pointer or click events on game objects, applying post effects and shaders, or controlling draw order. Covers the pos-relative draw contract, isKinematic and input, addPostEffect vs the deprecated shader property, and z-ordering. Triggers on: Tween, easing, animate, Renderable, extends Renderable, draw(, update(, isKinematic, pointerEvent, registerPointerEvent, onClick, addPostEffect, removePostEffect, ShaderEffect, addChild, z-order, anchorPoint, floating.\"\nlicense: MIT\n---\n\n# Renderables, input and effects\n\n> Subclassing `Renderable`, custom drawing, receiving pointer events, and\n> applying post effects — with the traps that produce silently wrong output\n> rather than an error.\n\nEverything here is a **silent failure**: the code runs, nothing throws, and the\nresult is wrong. Those are the cases worth memorising, because nothing in the\ntype signatures warns you.\n\n## 1. A custom `draw()` must position itself from `this.pos`\n\nThe renderer is **not** pre-translated to the renderable's position when `draw()`\nis called. `Container.draw` translates by the *container's* own `pos` and then\ncalls `child.preDraw` / `draw` / `postDraw`; `preDraw` never translates by the\nchild's `pos`. Read `this.pos` and draw relative to it.\n\n```js\nclass Marker extends Renderable {\n    draw(renderer) {\n        // ✗ draws at the parent container's origin, not at the marker\n        renderer.fillRect(0, 0, this.width, this.height);\n\n        // ✓\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\nThe symptom is an object that renders correctly at position (0, 0) and appears\nstuck at the top-left as soon as you move it.\n\n**Zero the anchor in custom-draw renderables.** `preDraw` does not translate to\n`pos`, but it *does* apply the anchor offset `(-width * anchorPoint.x,\n-height * anchorPoint.y)`. With the default centred anchor, drawing at\n`this.pos` therefore lands half a size off:\n\n```js\nconstructor(x, y, w, h) {\n    super(x, y, w, h);\n    this.anchorPoint.set(0, 0);   // ← or every custom draw is offset by -w/2, -h/2\n}\n```\n\n## 2. `isKinematic` must be `false` to receive input or collisions\n\n`Renderable.isKinematic` **defaults to `true`**, which means *\"physics collision\nand input events will not impact this renderable\"*. A kinematic renderable is\nskipped by the pointer-event dispatcher entirely.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y) {\n        super(x, y, 100, 40);\n        this.isKinematic = false;          // ← required, or clicks never arrive\n        input.registerPointerEvent(\"pointerdown\", this, () => { /* … */ });\n    }\n}\n```\n\nThe symptom is a handler that is registered, looks correct, and never fires.\n\nNote that `Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*`\nelements set it to `false` for you, and so does the `Body` constructor on its\nowner (which covers `Entity` and anything you give a body to) — so a plain\n`Renderable` or `Sprite` subclass with no body is the case that catches people.\n\nThe same flag has a second, less obvious effect: it is also the opt-in to the\nworld **broadphase**. The quadtree/octree insert skips any child with\n`isKinematic === true`, so body-less objects you want `adapter.queryAABB` /\n`querySphere` to find need `isKinematic = false` as well. The builtin\nnarrowphase likewise requires *both* partners to be non-kinematic.\n\n## 3. Draw order: only `addChild(child, z)` works\n\nThere are three separate traps here and they compound.\n\n```js\nworld.addChild(sprite, 10);   // ✓ the only reliable form\n```\n\n1. **`renderable.z` does not exist.** There is no `z` accessor anywhere on\n   `Renderable` (nor on `Rect`/`Polygon` above it) — only `depth`, which proxies\n   `pos.z`, and the container's default `sortOn: \"z\"` comparator reads `pos.z`.\n   Writing `sprite.z = 100` creates an inert property that nothing reads.\n   Several shipped examples still contain `this.z = Number.POSITIVE_INFINITY`;\n   those HUDs draw on top only because they happen to be added last. (A `z` key\n   in a `Sprite`/`ImageLayer` *settings* object is a different thing and is real\n   — it assigns `pos.z` in the constructor — but trap 2 then overwrites it.)\n2. **Setting `depth` *before* `addChild` is overwritten.** With `autoDepth` on\n   (the default), `addChild` without an explicit `z` assigns\n   `child.pos.z = children.length` after the push, discarding whatever you set.\n3. **Setting `depth` *after* `addChild` does not reorder.** A `\"z\"` sort runs\n   when a child is added (`autoSort`, deferred to the next tick), not when\n   `pos.z` changes. If you must change it later, call `container.sort()`\n   yourself, or use `moveToTop` / `moveUp` / `moveDown` / `moveToBottom`, which\n   reorder the children themselves. (An `\"x\"` / `\"y\"` sort is different: see\n   below.)\n\n### Sorting on `\"y\"` (or `\"x\"`), and `sortOffset`\n\nA container with `sortOn = \"y\"` draws its children from the smallest `pos.y` to\nthe largest, so whatever is lower on screen covers what is above it (top-down and\nisometric games). `\"x\"` does the same along x. A different `pos.z` still decides\nfirst, so keep the children that should interleave at one z.\n\n- **Moving a child re-sorts by itself.** With `autoSort` on (the default), the\n  container checks its order at the end of every update, after physics and the\n  children's own updates, and re-sorts when anything is out of place: a walking\n  character needs no `sort()` call. A frame where nothing moved costs one pass\n  over the children and does not cause a redraw. Turn `autoSort` off to keep\n  the children in the order you put them.\n- **Ties keep insertion order, and the child added LAST is drawn BEHIND.** A\n  torch placed at its wall's exact position disappears behind the wall.\n\n`sortOffset` changes the ORDER only, never where anything is: the child is lined\nup as if it stood that many pixels lower (positive) or higher (negative), and is\ndrawn exactly where it was. Use it when `pos` is not the point to sort by:\n\n```js\ntorch.sortOffset = 1;                        // win the tie with the wall it hangs on\ndust.sortOffset = door.pos.y - dust.pos.y;   // an emitter's pos is the top of its\n                                             // spawn area: line it up with the door\nprop.sortOffset = prop.height;               // sort by the bottom of the image\n```\n\nA container that keeps itself sorted picks a change up at its next update (with\n`autoSort` off, call its `sort()`). `\"z\"` and `\"depth\"` sorting ignore it. It must be a finite number (a string\nor `NaN` throws, rather than scrambling every sibling), and a pooled renderable\nstarts again at `0`.\n\nThe exception: under `Camera3d` the world's `sortOn` is `\"depth\"`, and the\ncamera calls `container.sortNow(true)` every frame. Note that mode does **not**\nsort by `pos.z` as a layer index — it sorts by squared distance from the camera\nin world space (nearest first in the array, painted far→near). So changing\n`depth` does reorder there, but as a position change, not as a layer number.\n\n```js\n// ✗ overwritten by autoDepth\nsprite.depth = 10;\nworld.addChild(sprite);\n\n// ✗ no resort happens (except under Camera3d)\nworld.addChild(sprite);\nsprite.depth = 10;\n\n// ✓\nworld.addChild(sprite, 10);\n```\n\n## 4. `removePostEffect()` destroys the effect\n\nTo disable an effect temporarily, toggle it. Removing it frees its GPU\nresources and leaves you holding a dead object.\n\n```js\n// ✗ effect is destroyed; re-adding it will not work\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nException worth knowing: an effect with `effect.shared === true` opts out of\nauto-destroy, because it is reused across renderables.\n\nUse `addPostEffect()` / `getPostEffect()` / `removePostEffect()`. Assigning\n`renderable.shader = …` is deprecated since 19.2.0.\n\nPost effects need a programmable pipeline. A `ShaderEffect` constructed against\nthe Canvas renderer (`renderer.shaderLanguage === null`) logs a warning, stays\nat `enabled === false` and no-ops every method rather than throwing — and\n`beginPostEffect` filters disabled effects out. Since `video.AUTO` picks the\nbackend at runtime, an effect that works on your machine may do nothing on\nanother.\n\n## 5. Opacity cascades; `setChildsProperty` assigns\n\n`alpha` composes down the tree. Fading a container fades everything inside\nit, and the values multiply — a child at `0.5` inside a parent at `0.5` draws\nat `0.25`. Each renderable keeps its own value; the composition happens at\ndraw time and is undone when the renderable is finished.\n\n```js\nmodel.alpha = 0.3;                             // fades the whole rig\ncontainer.setChildsProperty(\"alpha\", 0.3, true); // REWRITES each child's alpha\n```\n\nThe two are not interchangeable. `setChildsProperty` is a bulk assignment: it\nchanges what the children *are*, so their own `alpha` now reads `0.3` and\nstays that way after the container goes back to full. Reach for it when you\nmean to change the children, and for `alpha` when you mean to fade a subtree.\n\nDo not assign alpha down a hierarchy by hand. The renderer composes it as it\nwalks the tree, so setting it at more than one level multiplies: in a rig\nnested three deep, `0.3` applied at every level lands parts on `0.3`, `0.09`\nand `0.027` depending where they sit. The symptom does not look like an\nopacity problem — a model whose parts hold *different* opacities, and which\nappears to change opacity as it turns, because turning changes which parts\nface the camera.\n\nA useful consequence: a `GLTFModel` is a `Container`, so `model.alpha` fades\nevery part of a loaded rig — which is what you want for a ghost, a fade-in,\nor a reflection.\n\n## 6. `Vector3d.set(x, y)` silently zeroes `z`\n\n```js\nv.set(10, 20);        // z becomes 0, not \"unchanged\"\nv.set(10, 20, v.z);   // keep it\n```\n\n`pos` is an `ObservableVector3d`, whose `set(x = 0, y = 0, z = 0)` does the same\n— so `this.pos.set(x, y)` wipes the object's depth.\n\n`Renderable#setPosition(x, y, z?)` is the renderable-level form and does not:\nomit `z` and the depth is left exactly as it was. Reach for it whenever you\nare moving an object rather than editing a vector, and keep `v.set(x, y, v.z)`\nfor the vector itself.\n\n## 7. A settings key the class never reads is silently ignored\n\nA settings object is a plain literal: nothing rejects a key, so a name that\nbelongs somewhere else does nothing at all and says nothing about it.\n\n```js\n// WRONG — only the RENDERER reads a `blendMode` setting, never a renderable\nconst glow = new Sprite3d(x, y, { image: \"sun\", blendMode: \"additive\" });\n\n// right: it is a property\nglow.blendMode = \"additive\";\n```\n\nThe same shape catches `transparent` on a `GLTFModel` (its parts carry it —\n`model.setChildsProperty(\"transparent\", true, true)`) and any `Mesh` setting\nhanded to a class that forwards only a curated subset. If a visual option\nappears to do nothing, check whether the class actually reads it before\ntuning the value.\n\n## 8. `isRenderable` does not hide anything\n\nIt gates `updateBounds`, not drawing. Setting it `false` leaves the object on\nscreen and merely stops its bounds tracking, which is worse than doing nothing.\n\n```js\nsprite.isRenderable = false;   // WRONG — still drawn, bounds now stale\nsprite.alpha = 0;              // hides it\n```\n\nUse `alpha` for a blink, or remove the child for a long absence.\n\n## A mask belongs to you, not to the renderable\n\n`renderable.mask` takes a shape you created, and `destroy()` releases the\nreference without recycling it: the same instance may still be masking\nsomething else, so the engine has no claim on it. If you pool masks, release\nthem yourself.\n\nBefore 20.7.0 `destroy()` tried to recycle it and **threw** for every type\n`mask` accepts, because it used the legacy name-keyed pool and no geometry\nclass is registered there. It failed mid-teardown, so a masked renderable\ncould not be destroyed or removed without keepalive at all.\n\n## Update and draw\n\n```js\nclass Enemy extends Renderable {\n    update(dt) {\n        this.pos.x += this.speed * (dt / 1000);\n        return true;                       // ← true means \"redraw me\"\n    }\n    draw(renderer) { /* … */ }\n}\n```\n\n`update()` returns a boolean meaning *\"this object changed and needs\nredrawing\"*. The parent accumulates it into the container's `isDirty`, and the\napplication skips the whole draw pass while nothing is dirty — so returning\nnothing (falsy) can leave the scene frozen even though state is updating. The\nbase implementation returns `this.isDirty`, so `return super.update(dt)` is the\nother correct ending.\n\n`update()` is only called when `obj.inViewport === true` or\n`obj.alwaysUpdate === true`. An off-screen object with the default\n`alwaysUpdate = false` does not tick at all.\n\n## Anchors and floating\n\n- `anchorPoint` defaults to centre `(0.5, 0.5)` on `Renderable`, but `Container`\n  forces `(0, 0)`. Mixing them up shifts children by half their size.\n- `floating = true` pins a renderable to the screen rather than the world, which\n  is what you want for HUD elements. It opts the object out of camera transforms\n  and out of ancestor position accumulation.\n\n## Prefer built-in features\n\nBefore writing custom `draw()` code, check whether the engine already covers it —\n`ParticleEmitter`, `Tween`, the `ShaderEffect` presets, `UISpriteElement` and\n`UITextButton`, the physics adapters, and the Tiled tilemap renderers all exist.\nHand-rolled equivalents miss the batching and the multi-backend support.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| object draws at its parent's origin, ignores its position | `draw()` not reading `this.pos` |\n| pointer handler registered but never fires | `isKinematic` left at its `true` default |\n| wrong draw order | anything other than `addChild(child, z)` — see section 3 |\n| `me.pool: object ... cannot be recycled` while destroying | a mask on a renderable, before 20.7.0 |\n| effect cannot be re-enabled after removal | `removePostEffect()` destroyed it |\n| effect silently does nothing | running on the Canvas renderer |\n| object appears frozen while its state changes | `update()` not returning `true` |\n| off-screen object never ticks | `update()` is gated on `inViewport` — set `alwaysUpdate = true` |\n| custom draw offset by half the size | centred `anchorPoint` default not zeroed |\n| spatial query never finds an object | `isKinematic` left `true` — not in the broadphase |\n| `z` unexpectedly 0 after a `set` | `Vector3d.set(x, y)` defaults `z` to 0 — `setPosition(x, y)` on the renderable leaves the depth alone |\n| a settings option appears to do nothing | the class never reads that key — see section 7 |\n| `isRenderable = false` did not hide the object | it gates bounds, not drawing — use `alpha` |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-getting-started` — Application lifecycle and the scene graph\n- `melonjs-20-migration` — the deprecated `shader` property and other pre-20 APIs\n","packages/melonjs/skills/melonjs-3d-assets/SKILL.md":"---\nname: melonjs-3d-assets\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Loading 3D assets\n\n> glTF/GLB is the primary path for 3D content, and it goes through the **level\n> director** rather than the world directly: `level.load(\"scene\")`, the same call\n> a Tiled map uses. OBJ/MTL is the simpler path for a single static model.\n\n## Loading a glTF/GLB scene\n\n```js\nawait loader.preload([\n    { name: \"diorama\", type: \"glb\", src: \"data/scenes/diorama.glb\" },\n]);\n\nlevel.load(\"diorama\", { scale: 50 });\n```\n\n`type: \"gltf\"` (JSON plus external buffers) and `type: \"glb\"` (single binary)\nare both loader types. A self-contained GLB is the better shipping format — one\nrequest, nothing to resolve relatively.\n\nThe scene loads under whatever camera the stage has; view it under a `Camera3d`\nor it renders flat. See `melonjs-3d` for the camera.\n\n### `level.load` options for glTF/GLB\n\n| option | default | what it does |\n|---|---|---|\n| `scale` | `1` | pixels per glTF unit, applied to the whole scene. Blender's metre-scale export usually needs 20–100. |\n| `container` | `game.world` | where the nodes are added |\n| `onLoaded` | `game.onLevelLoaded` | called with the **level id**, not the scene |\n| `async` | `false` | return a promise that settles once the scene is in the world, instead of a boolean |\n| `rightHanded` | `true` | see below |\n| `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |\n| `lightIntensityScale` | — | keep authored intensity ratios instead of normalising every light to 1 |\n\nThose `game.` defaults are the engine's, not a recommendation. `game` is\nwhichever `Application` finished `init()` last, so **pass `container`\nexplicitly** from a `Stage`, where the right one is in your hand:\n\n```js\nlevel.load(\"sceneId\", { container: this.app.world, scale: 40 });\n```\n| `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |\n| `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |\n| `shadowOpacity` | `0.45` | how dark the blobs are |\n| `shadowScale` | `1` | blob footprint multiplier; `0` or less hides them |\n| `shadowOffset` | `0` | slide them along the light, in blob radii; needs `shadowGroundY`, which is the only way a scene load can name its floor |\n| `shadowStretch` | `1` | lengthen them along it, clamped to 3 |\n| `shadowDirectionX` / `shadowDirectionZ` | `0` | the ground direction they are cast along |\n| `shadowGroundNormal` | world up | up normal of the floor, for a scene that is not level |\n| `shadowLight` | — | a `Light3d` to take the direction from, re-read every draw |\n\n`shadowGroundPoint` is **not** on that list, and a scene load drops it without\na warning. One floor POINT shared by every part of a scene would stack the\nwhole thing's blobs on a single spot, which is nonsense where one floor\nHEIGHT is perfectly sensible. Ground that is not flat is a per-object answer:\nset it on the parts you want once the load has finished.\n\n`onLoaded` receives the level id — it is a \"done\" signal, not a handle on the\nscene. You need it, or `async`: with the game loop running, `level.load` stops\nthe loop and defers the actual load to a timer, so by default it returns\n*before* anything is in the world.\n\n```js\nawait level.load(\"diorama\", { scale: 50, async: true });\n// the scene is in the world here\n```\n\nNote `await level.load(\"diorama\")` without the flag does not await the load — the\ncall returns a boolean, and `await true` resolves immediately. To get at what was\nloaded, load into a container you own, or look the nodes up by their authored\nnames:\n\n```js\nlevel.load(\"diorama\", { scale: 50, onLoaded: () => {\n    const sun = app.world.getChildByName(\"Sun\")[0];   // note: returns an array\n    sun.intensity = 2;\n}});\n```\n\n### The same options from a Tiled `Trigger`\n\nA `Trigger` authored in Tiled forwards these to `level.load()`, so a trigger that\nwalks the player into a glTF level carries the scene's settings as Tiled\nproperties:\n\n```js\nworld.addChild(new Trigger(x, y, {\n    shapes: [new Rect(0, 0, 100, 100)],\n    to: \"diorama\",              // the level id — `to`, not `level`\n    scale: 50,\n    rightHanded: false,\n    castGroundShadow: false,\n}));\n```\n\nEvery `level.load` option above travels, including the whole ground-shadow set,\nand so do `container`, `onLoaded`, `flatten` and `setViewportBounds`. The list is\n**derived** from `level.loadOptions` rather than restated, so an option added to\na level format arrives here for free. It used to be hand-written and had fallen\nthree releases behind the glTF path (#1649).\n\n**`async` is deliberately excluded.** A trigger with a `color` + `duration`\nsets it itself, because it needs the promise to sequence its fade and reveal; a\ntrigger without one loads fire-and-forget and never sets it. Either way an\nauthored `async` property is ignored rather than honoured.\n\n## `rightHanded` — and the default that differs between the two APIs\n\nglTF is authored **Y-up, right-handed**; melonJS is **Y-down, +Z forward**.\nThere are two ways to bridge that, and they are not equivalent:\n\n- a **rotation** — preserves chirality, matches the authoring tool\n- a **mirror** (negate Y only) — flips the scene left/right\n\n`level.load` defaults `rightHanded: true`, so a scene loaded through the level\ndirector comes in correctly. **The `Mesh` constructor defaults it to `false`**,\nso glTF or OBJ geometry you feed to a `Mesh` yourself comes in mirrored unless\nyou pass `rightHanded: true`. A model that is subtly wrong — text backwards, a\ncharacter's gear on the wrong side — is this and nothing else.\n\nDo not add a flip of your own on top; two corrections cancel back to wrong.\n\n## Materials — what actually arrives\n\nThe importer covers a deliberate subset:\n\n- `pbrMetallicRoughness` **base colour texture and factor**\n- metallic/roughness **factors** converted to the engine's specular model\n- `emissiveFactor`, including `KHR_materials_emissive_strength`\n- `KHR_materials_unlit` — honoured, for baked/stylised workflows\n- `alphaMode: \"MASK\"` — the cutout threshold is applied\n- `doubleSided`\n\nNot imported: **normal maps**, metallic/roughness *textures*, occlusion maps,\nmorph targets, `KHR_texture_transform`. Nothing errors — the material simply\narrives without them, which reads as \"flatter than the preview in my authoring\ntool\". Bake what you need into the base colour.\n\n**Draco compression is the exception that does error.** Its geometry sits in\naccessors with no buffer view, which the parser rejects outright, so the whole\nasset fails to load. Export uncompressed.\n\n`textureFilter` on the Application settings (`\"nearest\"` / `\"linear\"` /\n`\"auto\"`) drives sampling; glTF materials that declare their own sampler carry\nit through. Pixel-art models want `\"nearest\"`.\n\n### `baseColorFactor` is LINEAR; a `tint` is sRGB\n**Pass the factor as a setting and the encoding is done for you.** `Mesh`\ntakes `baseColorFactor` — `[r, g, b]` in 0..1, linear, which is the form the\nloader hands back — so a mesh built from a node passes the node's own field\nstraight through:\n\n```js\nconst node = loader.getGLTF(\"palm\").nodes[0];\nconst mesh = new Mesh(x, y, {\n    vertices: node.vertices, normals: node.normals,\n    uvs: node.uvs, indices: node.indices, normalize: false,\n    baseColorFactor: node.baseColorFactor,   // encoded, not scaled\n});\n```\n\nThe setting's type admits `undefined`, because the loader declares the field\noptional and that line has to compile under `exactOptionalPropertyTypes`.\n`Sprite3d` forwards it too, and `mesh.setBaseColorFactor(f)` applies one after\nconstruction. For an ordinary sRGB colour use `setTint` instead — the rest of\nthis section is why the two are different.\n\n\nThe loader handles this for you. It only bites when you build meshes yourself\nfrom `loader.getGLTF(name).nodes` — which is what you do for per-node control,\nsince there is no public per-node `Mesh` factory — and reach for the obvious\n`* 255`:\n\n```js\n// WRONG — every untextured material renders far too dark\nmesh.tint.setColor(f[0] * 255, f[1] * 255, f[2] * 255, 1);\n```\n\nglTF defines `pbrMetallicRoughness.baseColorFactor` in **linear** space (spec\n3.9.2), while a melonJS `tint` is 8-bit **sRGB** — the same space as a CSS\ncolour or a texel out of a PNG. They are not the same number, and the gap is\nwidest exactly where art tends to live:\n\n| linear | `* 255` | correct | \n|---|---|---|\n| 0.05 | 13 | 63 |\n| 0.20 | 51 | 124 |\n| 0.42 | 107 | 173 |\n| 0.72 | 184 | 221 |\n| 1.00 | 255 | 255 |\n\n`Color.setLinear` does the encoding for you — it is the linear-space sibling of\n`setFloat`, which takes the same `0..1` range but treats it as already-sRGB:\n\n```js\nmesh.tint.setLinear(...node.baseColorFactor);   // encodes; what you want\nmesh.tint.setFloat(...node.baseColorFactor);    // does NOT encode; too dark\n```\n\nIt clamps, so the slightly out-of-range factors some exporters emit cannot turn\ninto `NaN`, and it keeps the encoded value as a float rather than rounding\nthrough 8 bits.\n\nMeasured on a scene whose every material went through `* 255`: mean frame luma\n85.5 against 147.8 once encoded, with each channel sitting 60 to 73 counts low.\n\n**Fix it early.** It is nearly invisible as a bug, because the scene still\nlooks coherent — just moody — so lighting and bloom thresholds get tuned\nagainst the wrong values. Correcting the encoding afterwards blows the frame\nout and forces a lighting retune, which is a much bigger job than the encode.\n\nThis applies only to factors coming out of glTF. Colours **you** author for the\nscreen, a HUD tint or a glow, are already sRGB: `* 255` is right for those.\n\n## Lights\n\nAuthored `KHR_lights_punctual` lights — sun, point, spot — become `Light3d`\nworld children, plus a soft ambient fill, and each keeps its authored name.\nA scene carrying any of them renders through the **lit** path automatically.\n\nglTF stores physical units — lux for suns, candela for lamps — and a Blender\ndaylight sun is 1000+, which blows out the engine's stylised shading. So every\nlight is normalised to intensity 1 by default. To keep the *ratios* the artist\nset, scale instead of normalising:\n\n```js\n// a 1000-lux sun lands at 1, a 500-lux fill lands at 0.5\nlevel.load(\"diorama\", { scale: 50, lightIntensityScale: 0.001 });\n```\n\nPass `lights: false` to manage lighting yourself.\n\n## Animation\n\nNode-TRS animation **is** supported: translation, rotation and scale channels,\nthe rigid hierarchical animation used by blocky/low-poly character packs. An\nanimated asset instantiates as a single `GLTFModel` keeping the node hierarchy\nintact, with an API deliberately mirroring `Sprite`. The model carries the asset\nname, which is how you get hold of it after loading:\n\n```js\nconst model = app.world.getChildByName(\"hero\")[0];   // the GLTFModel\n\nmodel.getAnimationNames();                           // [\"idle\", \"walk\", ...]\nmodel.setCurrentAnimation(\"walk\", { loop: true, speed: 1, onComplete });\nmodel.setCurrentAnimation(\"jump\", { next: \"idle\" }); // chain\nmodel.isCurrentAnimation(\"walk\");\nmodel.play();\nmodel.pause();\n```\n\n`speed` is a playback multiplier (1 = authored speed), not a frame delay. Only\nan explicit `loop: false` stops looping. `setCurrentAnimation` **throws** on a\nclip name the asset does not define — check `getAnimationNames()` first.\n\n### Placing and moving a model\n\nA `GLTFModel` is placed like any other renderable — `pos`, `depth`, `rotate`\nand `scale` move the **whole rig**, and compose with whatever the active clip\nis doing, so a walk cycle plays wherever the character stands:\n\n```js\nconst boat = new me.GLTFModel(me.loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nboat.setCurrentAnimation(\"paddle\", { loop: true });\napp.world.addChild(boat);\n\nboat.pos.set(steerX, waterLevel);\nboat.depth = travelled;\nboat.rotate(lean - lastLean, AXIS_Z);   // `rotate` is RELATIVE — feed it the delta\nboat.animationspeed = 0.85 + 0.9 * pace; // one authored tempo, many run speeds\n```\n\nConstructing one directly from `loader.getGLTF(name)` is fine and is the way to\ngo when the model is a game object rather than a scene — `level.load` is for\nloading a whole scene into the world.\n\n**Vertex skinning is out of scope** — `JOINTS_0` / `WEIGHTS_0` are not read, and\nneither are morph targets. A smoothly-deforming character will not deform. The\noptions are: rig it hierarchically instead (separate meshes parented into a\nnode tree), use `Sprite3d` billboards, or use the Spine plugin for 2D skeletal\nwork.\n\n## Instancing\n\nA node using `EXT_mesh_gpu_instancing` loads as an `InstancedMesh` — one draw\ncall for every copy, which is the difference between a hundred trees and a\nhundred thousand. Export with instancing enabled where your tool supports it.\nSparse accessors on the instancing attributes are rejected with a clear error\nrather than silently misplacing every instance.\n\n## Ground shadows\n\n```js\nlevel.load(\"diorama\", { scale: 50, castGroundShadow: true, shadowGroundY: 0 });\n```\n\n`castGroundShadow` is tri-state: set it and it overrides the application setting\nfor this scene in both directions; omit it and the scene inherits — and that\nsetting is `true` by default, so shadows are already on. As a scene-wide opt-in\nit skips nodes with no vertical extent — a ground plane is exactly that, and\nshadowing it with itself smears a blob across the whole floor. Blob shadows need\na GPU backend and a `Camera3d`; nothing is drawn on the 2D-camera or Canvas\npaths.\n\nOmit `shadowGroundY` and each blob sits at its own object's base at full\nstrength, which is right for props already resting on the ground. Set it when\nthings jump or fly, so the shadow stays on the floor and shrinks with height.\n\nA blob sits on its caster's x/z by default, so a wide flat-bottomed prop hides\nits own shadow under itself. The knob for that is `shadowScale`, which widens\nthe blob until its edge shows past the prop, or `shadowOffset` plus a direction,\nwhich slides it clear. **Not** `shadowGroundY`: raising that floats the blob up\nover the object as a dark ring instead of sliding it out, because it is for\nthings that leave the ground, not a visibility control.\n\nFor a light that is not overhead, name one: `shadowLight` takes the direction\nfrom a `Light3d` and is re-read every draw, so a day or night cycle carries\nevery shadow in the scene with it. Nothing is inferred — a scene with no named\nlight and no `shadowDirectionX`/`Z` gets no direction, because a scene with\nseveral lights has no non-arbitrary answer. `shadowStretch` lengthens the blob\nalong that direction and fades it as it pulls; `shadowGroundNormal` lays it on\na floor that is not level. All of these apply to an `InstancedMesh` too, as one\nvalue for the whole set.\n\n## OBJ/MTL\n\nA different shape entirely: OBJ produces raw geometry you hand to a `Mesh`,\nnot a scene. Preload the two files and name them in the `Mesh` settings — the\n`model` and `material` names are independent, the same name for both is just a\nconvention:\n\n```js\nawait loader.preload([\n    { name: \"fox\", type: \"obj\", src: \"models/fox.obj\" },\n    { name: \"fox\", type: \"mtl\", src: \"models/fox.mtl\" },\n]);\n\nconst mesh = new Mesh(400, 300, {\n    model: \"fox\",\n    material: \"fox\",        // texture resolved from the material's map_Kd\n    width: 200,\n    height: 200,\n    rightHanded: true,      // the Mesh default is false — see above\n});\nworld.addChild(mesh);\n```\n\n**The MTL loader fetches the textures it references itself** — `map_Kd`,\n`map_d` and `map_bump` are resolved relative to the `.mtl` and preloaded for\nyou, so there is no image entry in the manifest above. Passing `texture:`\nexplicitly overrides that and pins *one* binding over the whole model, which on\na multi-material model suppresses the per-material texture split; leave it out\nunless you mean that.\n\nMTL contributes `Kd` (diffuse tint), `d` / `Tr` (opacity), `Ke` (emissive),\n`Ks` + `Ns` (specular highlight), `map_Kd` (diffuse texture), `map_d`\n(per-texel opacity) and `map_bump` / `bump` / `norm` (tangent-space normal\nmap — needs `lit: true`, and the tangent frame is derived per fragment, so the\nmodel needs no tangent attribute). All three normal-map spellings are read as\nnormal maps even though the format specifies two of them as height maps, which\nis what every exporter actually writes. Specular *maps* (`map_Ks`) and the PBR\nextension's `map_Pr` / `map_Pm` are not supported and warn — the `Pr` / `Pm`\n*scalars* are read and approximated onto the specular terms, the maps would\nneed a real PBR model.\n\n`Ka` (ambient) and `illum` (illumination model) are read onto the material and\nreadable from `loader.getMTL(name)`, but take no part in shading. That is\ndeliberate rather than pending: `Ka` predates scene-wide ambient light and would\nfight `Stage.ambientLightingColor`, and `illum` enumerates fixed-function\nbehaviours this renderer has no equivalent for. In particular an `illum` of 0 or\n1 nominally means \"no specular\", and the engine does **not** honour that — `Ns`\nstays the only gate, so a model that renders correctly today is not restyled by\na field its exporter wrote inconsistently.\n\n`loader.getOBJ(name)` and `loader.getMTL(name)` return the parsed data if you\nwant it directly. OBJ has no scene graph, no lights, no animation — use glTF for\nanything beyond a single static model.\n\n## Authoring assets for the engine\n\nNotes from modelling props and a rigged character against this loader. They are\nBlender-specific in the API details, general in the traps.\n\n### One clip means one action\n\nglTF names an animation after the **action** it came from, so three objects\neach carrying their own action export as three separate clips — and a game can\nonly play one clip at a time, so the other two never move. Put every animated\nobject in **one action**, using slots (Blender 4.4+):\n\n```python\nshared = bpy.data.actions.new(\"paddle\")\nfor ob in (pivot, shoulder_l, shoulder_r, body):\n    ad = ob.animation_data_create()\n    ad.action = shared\n    ad.action_slot = shared.slots.new('OBJECT', ob.name)\n```\n\nExporting with the scene/whole-timeline mode does **not** merge per-object\nactions into one clip — it still emits one per action. Slots are the fix.\n\nIn Blender 5.x, `action.fcurves` no longer exists; curves live under\n`action.layers[].strips[].channelbags[].fcurves`, and a channelbag identifies\nits slot as `cb.slot.identifier` (there is no `slot_identifier`).\n\n### Never re-parent or join a rig that is mid-pose\n\nBoth parenting and joining bake the objects' **current world transforms**.\nParenting a child while the parent sits in an animated pose writes the inverse\nof that pose into `matrix_parent_inverse`, which silently cancels the animation\n— the part renders rigid while the curves still exist. Detach the action, zero\nthe rig, do the surgery, then reattach:\n\n```python\nsaved = [(o, o.animation_data.action_slot) for o in animated]\nfor ob, _ in saved:\n    ob.animation_data.action = None\n    ob.rotation_euler = (0, 0, 0)\n# ...re-parent / join here...\nfor ob, slot in saved:\n    ob.animation_data.action = act\n    ob.animation_data.action_slot = slot\n```\n\nA joined mesh also inherits the **active** object's scale, so joining into a\nbox that was scaled to `(0.8, 2.0, 0.1)` leaves a node with that non-uniform\nscale and geometry divided back through it. It renders in the right place but\nskews flat-shaded normals — apply transforms (`rotation=True, scale=True`)\nafter joining. Watch for a related trap: setting `ob.scale.x` on a mesh whose\nsize was already applied *multiplies* rather than replaces it.\n\n### A model only needs to stay split where something animates it\n\nEvery glTF primitive is a draw call. Merge everything static into one mesh and\nkeep separate nodes only for the parts a clip actually drives — a boat with a\nrabbit and a paddle went from 28 primitives to 5 that way, with no visible\nchange.\n\n### One palette strip beats one material per colour\n\nAn `InstancedMesh` is **one geometry and one material**, so a prop that wants\nfive colours cannot use five materials. Give the whole model a single material\nwhose image is an *N*×1 strip, one pixel per shade, and point each face's UVs\nat a cell centre (`u = (cell + 0.5) / N`, `v = 0.5`) with the sampler set to\nnearest. A whole scene's palette then costs one tiny texture, a merged mesh can\nbe twenty colours, and there is nothing to bleed between cells.\n\nColour-space trap when generating that strip programmatically: Blender's\n`image.pixels` are scene-linear floats, but this export path writes them out\n**verbatim**, so pre-converting sRGB→linear darkens every colour. White is the\ntell — it is the one value identical in both spaces, so if white survives and\neverything else came out dark, that is the bug. Verify by decoding the PNG back\nout of the GLB and comparing bytes to the source palette.\n\n### Feeding an authored mesh into an `InstancedMesh`\n\n`EXT_mesh_gpu_instancing` covers instances authored *in the file*. When the\ngame places them itself, take the geometry off the parsed descriptor instead:\n\n```js\nconst node = me.loader.getGLTF(\"palm\").nodes[0];\nconst trees = new me.InstancedMesh(0, groundY, {\n    vertices: node.vertices, uvs: node.uvs,\n    normals: node.normals, indices: node.indices,\n    texture: palette, textureFilter: \"nearest\", scale: 40,\n    instanceCount: count,\n});\n```\n\n`nodes` is one entry **per primitive**, each in its own local space with a\nseparate `world` matrix — so this only works cleanly when the asset is a single\nmerged primitive exported with an identity node transform. Model it at the\norigin, base on the floor, and apply transforms before exporting.\n\nOne geometry stamped out hundreds of times reads as one object *copied*\nhundreds of times, so vary each instance in its transform. Rotate **after**\ntranslating, or the instance swings around the group origin instead of turning\nwhere it stands:\n\n```js\nplacement.identity().translate(x / s, -y / s, z / s)\n         .rotate(math.randomFloat(0, Math.PI * 2), AXIS_Y);\nconst j = math.randomFloat(0.82, 1.18);\nplacement.scale(j, j, j);\n```\n\nSizing every prop against **one** scale constant, rather than giving each its\nown, keeps \"this looks wrong\" a modelling question instead of a scaling one.\n\n## Packaging\n\nA self-contained GLB is one file with nothing to resolve — the safest thing to\nship. External references still work in both formats: `.bin` buffers and image\n`uri`s are resolved **relative to the asset's own URL** (a GLB with an external\ntexture loads as-shipped). But an external file that 404s rejects the parse, so\nthe whole asset fails to load rather than arriving partially — no untextured\nfallback. Note that `loader.setBaseURL(\"*\", …)` deliberately skips `gltf`,\n`glb`, `obj` and `mtl` for this reason; set those types individually if you\nneed a prefix.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| model mirrored — text backwards, gear on the wrong side | `Mesh` defaults `rightHanded` to `false`; pass `true` |\n| model upside down | a manual axis flip on top of the loader's conversion |\n| scene is enormous or invisibly small | `scale` left at 1 against a metre-scale export |\n| the asset fails to load entirely | an external buffer/image `uri` that 404s, or Draco-compressed geometry |\n| everything is blown out white | authored lux/candela intensities; use `lightIntensityScale` |\n| materials flatter than the authoring-tool preview | no normal or metallic/roughness *textures* are imported |\n| a character does not deform | vertex skinning is out of scope; rig hierarchically or billboard |\n| animation names come back empty | the asset has no node-TRS channels (skin-only rig) |\n| a hundred copies tank the frame rate | exported without `EXT_mesh_gpu_instancing` |\n| a whole scatter looks like one object repeated | every instance shares the group transform — vary yaw/scale per instance, rotating *after* the translate |\n| three clips where you authored one | one action per object; use a single action with a slot each |\n| one part of a rig renders rigid while its curves exist | it was re-parented mid-pose, baking the inverse into `matrix_parent_inverse` |\n| a merged mesh lights wrong along one axis | it inherited the active object's non-uniform scale on join — apply transforms |\n| an authored palette comes out uniformly dark, but white is correct | linear values written to a strip that is saved verbatim |\n| every untextured glTF material renders too dark, white unaffected | `baseColorFactor` is linear and a `tint` is sRGB — `tint.setLinear(...)`, not `* 255` or `setFloat` |\n| `getGLTF(name).nodes[0]` geometry lands in the wrong place | `nodes` is per primitive, each with its own `world`; merge to one primitive and export at the origin |\n| a prop casts no visible shadow | wide and flat-bottomed — the blob is under it; `shadowGroundY` haloes it rather than revealing it |\n| shadows only show on casters near the camera | `shadowGroundY` is on the wrong side — Y-down means the floor is a **greater** y, so a `pos.y - lift` puts the blob inside the caster and the depth test leaves only a hairline ring |\n| a shadow smeared across the whole floor | a ground plane cast its own blob — use the scene-wide opt-in |\n| `onLoaded` gets a string, not the scene | it is called with the level id; load into your own container instead |\n| scene renders flat and unlit | no `Camera3d` — the 2D-camera path is CPU-projected and unlit (with a `Camera3d` on Canvas you get a black canvas instead) |\n\n## Related skills\n\n- `melonjs-3d` — conventions, `Camera3d`, meshes, `Light3d`, instancing, and\n  `camera.setFog` (an outdoor scene almost always wants it; it is set on the\n  camera, so a loaded scene needs no per-node work)\n- `melonjs-lighting` — how `Light3d` behaves once imported\n- `melonjs-loading-assets` — the loader, asset types and base URLs\n- `melonjs-plugins` — the Spine plugin, for skeletal characters\n","packages/melonjs/skills/melonjs-physics/SKILL.md":"---\nname: melonjs-physics\ndescription: \"Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT.\"\nlicense: MIT\n---\n\n# Physics and collision\n\n> melonJS has a built-in SAT collision world and two optional rigid-body\n> adapters. Movement, collision callbacks and spatial queries all behave\n> differently depending on which is active — and several of the differences fail\n> silently.\n\n## Choosing a world\n\n| | when |\n|---|---|\n| **built-in** (default) | Arcade-style: platformers, top-down games. Fast, position-based, not Newtonian. |\n| **`@melonjs/planck-adapter`** | Full rigid-body dynamics — stacking, joints, realistic restitution. |\n| **`@melonjs/matter-adapter`** | Same class of thing, backed by matter-js. |\n\n### Picking one\n\nStart with the built-in. It is the default for a reason: no dependency, the\nsmallest bundle, and its position-based model is what most 2D games actually\nwant — a platformer's jump arc is a designed curve, not a simulated one, and\ntuning gravity against a solver you are fighting is a bad afternoon.\n\n**Move to an adapter when the game needs something the builtin cannot express**,\nand these are the usual triggers:\n\n- **Rotation that collides.** The clearest signal. The builtin's SAT never reads\n  `body.angle`, so a spinning shape collides as though it never turned. See\n  the note in *Built-in world quirks* below.\n- **Torque, angular velocity, or spin from an impact.** Anything that should\n  start turning *because* it was hit.\n- **Joints and constraints** — ragdolls, chains, ropes, vehicles, hinged doors.\n  The builtin has no concept of them.\n- **Stacking and resting contacts.** Boxes that pile up and stay put need an\n  iterative solver. The builtin resolves each contact once per step with no\n  iteration pass, which splits three ways. A dynamic body resting on STATIC\n  geometry is exact, to the pixel, which is the platformer case it exists for.\n  A body squeezed between a dynamic body and static geometry is exact too: the\n  immovable side gets the casting vote and the other body absorbs the whole\n  correction, so a crate shoved against a wall stops at the wall. But a body\n  squeezed between two DYNAMIC bodies has no immovable side to defer to, so\n  those per-pair corrections still compete and it comes to rest slightly\n  overlapping. Three stacked 44px boxes rest flush on the floor with about 2px\n  of overlap inside the pile, and stay there: a stable equilibrium, not jitter\n  you can wait out or tune away.\n- **Believable restitution and friction**, where a ball's bounce height and roll\n  should follow from its material rather than from code you wrote per case.\n\n**Stay on the built-in when** the game is grid or tile based, movement is\nauthored rather than simulated, collision is \"did these two boxes touch\" plus a\npush-out, or you are shipping to a tight bundle budget. Tiled maps, `Trigger`\nand `Collectable` all work on every backend, so this is not a fork in the road\nfor level content.\n\n**planck or matter?** Both are real rigid-body engines and either will do; the\nadapters expose the same portable API, so a game can switch with an import\nchange and a gravity re-tune.\n\n- **planck** (a Box2D port) is the more accurate and the more predictable under\n  stress: better stacking, better joint behaviour, a continuous-collision\n  `bullet` mode for fast movers. It works in **metres**, so it has a\n  `pixelsPerMeter` scale to think about, and its tuning vocabulary is Box2D's.\n- **matter-js** works directly in **pixels**, which makes it the gentler\n  introduction, and its compound bodies and constraints are pleasant to author.\n  It is the softer solver: stacks settle less crisply and fast bodies need\n  `subSteps`.\n\nIf you have no preference and the game leans on stacking, joints or fast\nprojectiles, take planck. If you want to be up and running with the least\nconversion to think about, take matter.\n\nAn adapter is an `Application` setting, not a plugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },   // pixels/s², default {x: 0, y: 320}\n        pixelsPerMeter: 32,          // default 32\n        subSteps: 2,                 // default 1\n    }),\n});\nawait app.init();\n```\n\n`PlanckAdapter` also takes `velocityIterations` (default `8`) and\n`positionIterations` (default `3`). `MatterAdapter` takes a different set —\n`gravity` (default `{x: 0, y: 1}`, matter's own convention), `subSteps` and\n`matterEngineOptions`; it has no `pixelsPerMeter` because it already works in\npixels. `subSteps` is the anti-tunnelling knob when per-frame motion exceeds a\nbody radius. Pick **one** adapter per game; `physic: \"none\"` disables physics\nentirely — `world.step` skips the simulation and the world behaves as a pure\nscene graph.\n\n**Gravity is a different unit in every backend, and the defaults are NOT the\nsame speed.** Each is \"Earth-like\" in its own convention, but measured against\neach other by timing a 200px free fall in one scene:\n\n| backend | default | unit | measured fall |\n|---|---|---|---|\n| built-in | `0.98` | per FRAME, integrated against `timer.tick` | **~4450 px/s²** |\n| matter | `{x: 0, y: 1}` | matter's own, after its internal `gravity.scale` | **~900 px/s²** |\n| planck | `320` | pixels per second² | **~330 px/s²** |\n\nThat is a **thirteenfold spread**, so swapping the built-in world for an adapter\nand keeping everything else the same makes the whole game feel different. It is\nnot a bug in either one: they simply do not share a unit.\n\nTo match a backend you are porting FROM, pick a target in px/s² and convert:\nplanck takes it directly; matter takes `target / 900`; the built-in takes\n`target / 4450 * 0.98`, because it applies gravity per frame rather than per\nsecond. Passing one number to two of them compares nothing: hand planck\nmatter's `{x: 0, y: 1}` and it runs at a three-hundredth of Earth gravity.\n\nThat mistake is quiet and it does not look like a gravity problem. Bodies fall\nslowly enough that a spawner keeping a fixed interval starts constructing them\ninside each other, and **a body born overlapping another is outside what a\nsolver promises to fix**: the contact normals of an interlocked pair can cancel\nout, leaving the position solver with nowhere to push. The symptom is two\nshapes permanently fused at rest, which reads as a broken collision shape\nrather than as a bad gravity constant. Spawn clear of anything already there.\n\n`world.physic` carries the active adapter's label (`\"builtin\"`, `\"planck\"`,\n`\"matter\"`, `\"none\"`), so game code can branch without importing the class.\n\n## Two ways to declare a body\n\n**`bodyDef`** — declarative, portable, works on every adapter. Prefer it.\n\n```js\nclass Player extends Sprite {\n    constructor(x, y, settings) {\n        super(x, y, settings);\n        this.bodyDef = {\n            type: \"dynamic\",\n            shapes: [ /* … */ ],\n            collisionType: collision.types.PLAYER_OBJECT,\n            collisionMask: collision.types.WORLD_SHAPE | collision.types.ENEMY_OBJECT,\n            restitution: 0.2,\n        };\n    }\n}\n```\n\nThe engine forwards it to the active adapter when the renderable is added with\n`Container.addChild`, and only if that container is already attached to the root\nworld; the broadphase picks the renderable up on the next `world.update()`.\nCalling `adapter.addBody()` yourself registers the body but leaves the\nrenderable out of the scene graph, so it integrates but never collides.\n\n**`fixedRotation` follows the engine, so bodies rotate.** Both matter and\nplanck default it to `false`, and so do the adapters from matter-adapter 1.4.0\nand planck-adapter 1.5.0. Pass `fixedRotation: true` for anything that must\nstay upright, such as a platformer actor. Earlier adapter versions locked\nrotation unless told otherwise, which inverted the engine they wrap: bodies\nlanded holding whatever angle they were given and no collision could turn them,\nwhile `setAngle()` still worked, so a body could look rotated while being\nunable to rotate.\n\n`frictionAir` damps **rotation as well as translation** on both adapters, so\nit is also what settles a spin. Left at `0` a body that starts turning never\nstops, which only became visible once rotation stopped being locked.\n\nOther portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),\n`friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,\n`isSensor`, `userData`. Not every adapter honours every one — the built-in\nadapter ignores `friction`, `fixedRotation` and `userData`, and maps\n`frictionAir` onto its per-axis `body.friction` damping vector.\n\n**`new Body(this, shape)`** — the built-in-native API. Fine, but ties the entity\nto the built-in world.\n\n### Per-shape filtering and triggers\n\nA shape in `shapes` may carry its own `collisionType`, `collisionMask`,\n`isTrigger` (default `false`) and `isActive` (default `true`). A shape can only\n**narrow** what its body allows — the body's type/mask are checked first.\n\n```js\nconst feet = new Rect(0, 24, 32, 8);\nfeet.collisionMask = collision.types.WORLD_SHAPE;\nconst torso = new Rect(0, 0, 32, 24);\ntorso.collisionMask = collision.types.ENEMY_OBJECT;\ntorso.isTrigger = true;      // detects hits, never gets pushed by them\n```\n\n`isActive: false` removes the shape from the narrow phase *and* from raycasts\nwithout shrinking the body's bounds. Honoured by the built-in and planck\nadapters; the matter adapter filters per body and ignores them. Under planck the\nbody-wide setters (`setCollisionType` / `setCollisionMask` / `setSensor`) write\nevery fixture and so overwrite per-shape values.\n\nFor contacts reported per *shape pair* rather than per body pair, define\n`onShapeCollisionStart` / `onShapeCollisionActive` / `onShapeCollisionEnd`.\nDeclaring at least one of them opts into shape-pair enumeration; declaring none\ncosts nothing.\n\n### Shapes drawn in a physics-shape editor\n\nA visual collision-shape editor exports a JSON file holding a map of body name\nto the fixtures authored for it. Preload it like any other JSON, then name it\nfrom `bodyDef.shapes` with `id` naming the body to read:\n\n```js\n// in the preloader\n{ name: \"shapes\", type: \"json\", src: \"data/physics/shapes.json\" }\n\n// then, on the renderable\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\" };\n```\n\nThis works identically on the built-in, matter and planck adapters — the file is\nresolved into real shapes before any adapter is handed the definition. Circles\nand polygons both load, and a concave outline (exported already decomposed into\nconvex pieces) becomes one `Polygon` per piece.\n\nThe density, friction and bounce the shapes were authored with are carried onto\nthe body definition too, each honoured by whichever backends support it — the\nbuilt-in adapter ignores contact `friction`, as it does for a hand-written\ndefinition. The format carries these per fixture, but only planck could honour\nthat, so they reduce onto the body and a file whose fixtures disagree warns. An\nexplicit field on the definition always wins:\n\n```js\n// bounces at 0.9 whatever the file says\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\", restitution: 0.9 };\n```\n\nThe exporting tool's own filter block is **not** read, in any spelling. Its\ncategory and mask bits are numbers in that tool's namespace rather than\n{@link collision.types}, and a template writes its defaults for every fixture\nwhether or not you set anything — importing them would silently tag a shape as\nsomething it is not and stop it colliding. Set `collisionType` / `collisionMask`\non the body definition instead. A file written by hand may carry those two names\nper fixture, and `isSensor`, which mean the same thing here as there.\n\nAuthored shapes and imported ones can sit in the same list.\n\nTwo bodies must never share one `shapes` array: a body **owns** its shapes and\nrotating it mutates them in place. Naming the file sidesteps this — every body\nresolved from it mints its own shapes.\n\n`Body#fromJSON()` and passing an exported list to `addShape()` are the old,\nbuilt-in-only spelling of this and are deprecated since 20.7.0.\n\n### Which shapes each backend can actually simulate\n\n| shape | builtin | matter | planck |\n|---|---|---|---|\n| `Rect`, `Polygon` | exact | exact | exact, up to 12 vertices |\n| `RoundRect` | exact | exact (decomposed) | sampled down to 12 vertices |\n| `Ellipse` | exact | circle of the average radius | circle of the average radius |\n| `Line` | exact | thin quad along the segment | thin quad along the segment |\n| `Box3d`, `Sphere` | exact, and in 3D | **throws** | **throws** |\n| `Point` | supported | **throws** | **throws** |\n\nTwo of those are worth knowing before you author geometry. Box2D caps a\npolygon's vertex count and says nothing when you go over: measured, a\n16-point outline comes back with 12 vertices and a 36-point one collapses\nfrom 70x36 to 47x18. A `RoundRect` carries 36 points, four corners of nine\narc segments each, so it is the usual way to meet that cap; the adapter now\nsamples it down and keeps the extent. Anything the 2D backends cannot express\nis refused loudly on both, rather than leaving a body with no collision\ngeometry that silently never collides.\n\nWhatever a backend really simulates is what `getBodyShapes()` reports, so the\ndebug panel is the fastest way to see an approximation for what it is.\n\n### `Line` shapes, and why slopes need a rigid-body adapter\n\nA `Line` is two points and no area, and it is what Tiled emits for every\npolyline, so it is the natural way to author a slope or a strip of ground.\nNeither rigid-body engine has a segment primitive, so both simulate one as a\nthin quad following the segment (2px), which is what `getBodyShapes()` reports\nback. Before 20.7 each substituted something of its own instead: matter the\nline's bounding box, planck a one-metre square wherever the body happened to\nbe. Neither logged anything.\n\nThe builtin solver resolves a sloped segment too, and correctly: a body lands\non it and SLIDES down it, holding a constant distance from the surface while\nits x advances. What it has no notion of is SURFACE friction. `def.friction`\nis ignored entirely and `body.friction` is a per-step velocity damping vector\nfed from `def.frictionAir`, so nothing removes the tangential component of\ngravity. A body on an incline therefore accelerates down it forever and leaves\nat the end, and in a valley of segments it swings from arm to arm for a while\nbefore coming to rest on the flat. (It used to escape through an arm instead;\nthat was a separate defect in how a body was separated from a shape list, fixed\nin 20.7.)\n\nmatter and planck both have real surface friction, so the same valley holds a\nbody still.\n\n**Several shapes on one body.** A Tiled polyline arrives as ONE body carrying\none shape per segment, and until 20.7 a body resting on such a ground could\nsink through one of the segments and drop out. The narrowphase reports the\nFIRST overlapping shape pair it finds, and the compound resolution pass\nre-ran that same first-hit-wins scan, so it kept re-resolving the pair it had\nalready resolved while a neighbour was penetrated unmeasured. Every pair is\nenumerated now. If you are ever diagnosing something similar, the experiment\nthat localises it is to re-run the same geometry as SEVERAL single-shape\nbodies: if that holds and one multi-shape body does not, the fault is in the\ncompound path rather than in the shape type.\n\nDo not diagnose this by dropping a body and reading its final `y`. A body that\nslid off the end of a ramp and a body that fell straight through it both end up\nfar below, and they look identical in that one number. Track the body against\nthe surface height at its CURRENT x instead: riding the slope shows a constant\noffset while x advances.\n\nSo segments collide on every backend. If you need a body to come to REST on a\nsloped one, use matter or planck, or damp it yourself: the builtin will slide\nit down the incline indefinitely. The `Line Collision` example draws the\nauthored segment against the geometry each backend reports, which is the\nquickest way to see what a backend is really simulating.\n\n### `anchorPoint` moves the collision shapes with the drawing\n\n`anchorPoint` says where in its own bounds a renderable sits on its `pos`:\n`updateBounds()` translates its bounds by `-width * anchorPoint.x, -height *\nanchorPoint.y`, and `preDraw` shifts its rendering by the same amount. A\nbody's collision shapes are measured from that same frame, so a shape of\n`new Rect(0, 0, width, height)` covers the sprite whatever the anchor is, and\na shape's own `pos` still offsets it inside that frame (a small hitbox at the\nfeet, a hurtbox at the head).\n\n**That frame's origin is its TOP-LEFT corner, which is the trap for a small\nshape.** `new Rect(0, 0, w, h)` reads naturally because it spans the frame.\nA circle or a `Sphere` does not span anything, so `new Sphere(0, 0, 0, r)` on\na centre-anchored sprite sits on the sprite's corner, not on the sprite. Put\nit at half the frame:\n\n```js\n// centred on a 24x128 sprite whose anchorPoint is (0.5, 0.5)\nbolt.bodyDef = { type: \"dynamic\", shapes: [new Sphere(24 / 2, 128 / 2, 0, r)] };\n```\n\nMeasured on exactly that sprite: with the shape at `(0, 0, 0)`,\n`adapter.getBodyShapes()` reported it at `(-12, -64)` — **64 units up the\nscreen, four times its own radius** — so the projectile collided well away\nfrom where the player saw it, in a way that still looked like a working game\nbecause the targets were large.\n\nThe offset comes from the renderable's own `width` / `height`, NOT from what\nit draws at. Scale a sprite through `currentTransform` and those stay the\ntexture's size, so the half-frame term is half the texture, not half the\ndrawn quad. The debug overlay is the quickest confirmation: the red shape\nshould sit on the art.\n\nThis was broken until 20.7: shapes were measured from `pos` regardless of the\nanchor, so anything not anchored at its top-left corner collided where it was\nnot drawn, by half a body at the default centred anchor and by a full body\nheight at the bottom anchor a platformer actor uses. If you compensated by\noffsetting your shapes by hand, remove those offsets. `Entity` and Tiled\nobjects set their own anchor to `(0, 0)`, so nothing built either of those\nways is affected.\n\nThe debug panel is the quickest check: green is the renderable's bounds, red\nis what actually collides, and the two now agree.\n\nOne renderable does not take that offset at all: one that has cleared\n`applyAnchorTransform`, the flag `preDraw` reads before shifting anything.\n`GLTFModel` sets it `false` outright and `Mesh` clears it under a `Camera3d`,\nbecause both emit world coordinates and pivot about their own model origin\nrather than a bounds box, so they draw at `pos` whatever their `anchorPoint`\nstill holds. Their bodies are built there too, on every backend, and\n`raycast3d` reports hits in that same frame. If you clear the flag on a\nrenderable of your own, the same applies: you place it, and your shapes are\nmeasured from `pos`.\n\n### `autoTransform: false` opts out of rotated bounds\n\n`updateBounds()` only applies `currentTransform` when `autoTransform` is\n`true`. Turn it off and the renderable's bounds stay the UNROTATED frame\nwhile the sprite turns inside them, which quietly feeds a wrong box to\nfrustum culling and to the broadphase. Measured on a 60x20 box at 90\ndegrees: `20x60` with `autoTransform` on, `60x20` with it off.\n\nThat flag means \"I place myself\", so it is a fair contract, but reach for it\nonly when you really are drawing in your own frame, and remember the bounds\nare yours to keep honest then.\n\n## Move with forces, not by assigning position\n\n```js\n// ✗ the adapters overwrite this from the engine body every step\nthis.pos.x += this.speed;\n\n// ✓ portable — works on every adapter\nthis.body.applyForce(thrustX, 0);\nthis.body.setVelocity(vx, vy);\n\n// ✓ built-in only — `force` and `maxVel` are BuiltinAdapter Body fields\nthis.body.force.x = this.body.maxVel.x;\n```\n\n`maxVel` is what turns a force into a speed, so the two are set together. Use\nthe setters rather than writing the vectors — both take an optional THIRD\nargument for the depth axis, left unchanged when omitted, so a 2D call keeps\nits behaviour:\n\n```js\nthis.body.setMaxVelocity(3, 15);   // walk speed, jump speed\nthis.body.setFriction(0.4, 0);     // ground drag, none vertically\n```\n\n`friction` here is per-axis per-step velocity damping, not a surface\ncoefficient — see the built-in quirks below.\n\nUnder matter and planck, `syncFromPhysics()` copies the engine body's position\nback onto `renderable.pos` after every step, so a direct write is simply erased\n— no error. The built-in world integrates `pos` in place instead, so a write\nsurvives but fights collision resolution and jitters. To teleport, go through\n`adapter.setPosition(renderable, p)` so the engine body moves with it.\n\n## Collision callbacks — which one to use\n\nThe lifecycle is `onCollisionStart` (contact begins), `onCollisionActive` (every\nstep while it persists) and `onCollisionEnd` (contact breaks). None of them are\ndefined on `Renderable` — you just declare the ones you want and the dispatcher\nchecks `typeof`. That is also why they cannot be declared on the base class: a\nfield there would SHADOW the prototype method every subclass that implements\none defines.\n\n**In TypeScript the response is `CollisionResponse`**, exported from `melonjs`:\n\n```ts\nimport type { CollisionResponse, Renderable } from \"melonjs\";\n\nclass Coin extends Sprite {\n    onCollisionStart(response: CollisionResponse, other: Renderable) { … }\n}\n```\n\nOn a renderable you do not subclass, declare the hooks you want as OPTIONAL\nmembers of a local interface and assign through one plain cast — that is the\nopt-in, and it needs no `as unknown as`:\n\n```ts\ninterface Contacts {\n    onCollisionStart?: (response: CollisionResponse, other: Renderable) => void;\n}\n(rock as Contacts).onCollisionStart = (response, other) => { … };\n``` The legacy `onCollision` still exists and has a trap:\n\n> On the built-in world, `onCollision` fires **twice per frame** for\n> dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT\n> response with a fixed `a`/`b` per pair. On the adapters it is routed to their\n> \"still touching\" phase and fires once per side.\n\nThe legacy dedupe idiom is `if (this !== response.a) return;`. Defining\n`onCollisionActive` suppresses the legacy callback on that renderable (the\n\"supersedes\" rule, applied per side so `a` and `b` can migrate independently),\nand is deduped to once per pair per side per frame on every adapter.\n\n**Return values only work on the legacy handler.** `return false` from\n`onCollision` skips the built-in SAT push-out; what `onCollisionStart` /\n`onCollisionActive` return is discarded. To opt a body out of the physical\nresponse, set `bodyDef.isSensor` / `body.setSensor(true)`, or make the shape a\ntrigger. With no `onCollision` defined at all, push-out happens by default for\ndynamic non-sensor bodies.\n\nThe two handler families do **not** get the same response object:\n\n| | `onCollision` (legacy) | `onCollisionStart` / `Active` / `End` |\n|---|---|---|\n| `a` / `b` | fixed per pair | receiver-symmetric: `a === this`, `b === other` |\n| overlap magnitude | `response.overlap` | `response.depth` |\n| contact axis | `response.overlapN` / `overlapV` | `response.normal` |\n| also carries | `aInB`, `bInA`, `indexShapeA/B`, `isTriggerContact` | `pair` (adapter-native, absent on built-in) |\n\nOn the modern response `overlap` / `overlapN` / `overlapV` are still present but\n`@deprecated` (and `undefined` under the matter adapter) — use `depth` and\n`normal`. `normal` is the direction the receiver must move to separate, in\ncanvas coordinates: `normal.y < -0.7` means \"push me up\", i.e. I landed on top\nof `other`.\n\nOne portability wrinkle: the built-in adapter dispatches `onCollisionEnd` with\n`undefined` as the response; the matter and planck adapters pass a real one.\nGuard it.\n\n## Never mutate the world during contact dispatch\n\nCollision callbacks are dispatched from inside the physics step on every\nadapter, and each engine has its own tolerance for being mutated there. Flag the\nwork and drain it from your `Stage.update`:\n\n```js\nonCollisionStart(response, other) {\n    this.pendingRemoval = true;    // don't addChild / removeChildNow here\n}\n```\n\nWhat actually happens per adapter:\n\n- **built-in** — callbacks fire inline during `step()`. `Container.removeChild`\n  is deferred, so it is safe; `removeChildNow()`, and the internal `destroy()`\n  it calls, are immediate,\n  and the detector only survives them because it re-checks `body === undefined`\n  after every user callback.\n- **planck** — `onCollisionStart` / `onCollisionEnd` are dispatched from\n  planck's `begin-contact` / `end-contact` events, which fire *while the world\n  is locked*. Adding a renderable with a `bodyDef` there makes planck's\n  `createBody` return `null` and the adapter then throws on the null handle.\n  `onCollisionActive` is dispatched after the step and is the safe one.\n- **matter** — callbacks come from matter's `collisionStart` / `collisionActive`\n  / `collisionEnd` events during `Engine.update`.\n\n## Spatial queries\n\n`raycast` and `queryAABB` are implemented by all three adapters:\n\n```js\nimport { Rect, Vector2d } from \"melonjs\";\n\n// nearest hit only — returns null when nothing is hit\nconst hit = app.world.adapter.raycast(new Vector2d(x0, y0), new Vector2d(x1, y1));\n// hit → { renderable, point, normal, fraction }\n\n// takes a Rect, not a Bounds\nconst inArea = app.world.adapter.queryAABB(new Rect(x, y, w, h));\n```\n\n`querySphere(centre, radius)` (or `querySphere(sphere)`) is **built-in only**,\nand only active under a 3D broadphase — i.e. `world.sortOn === \"depth\"`, which\n`Camera3d` sets. So is `raycast3d`. Check\n`world.adapter.capabilities.raycasts3d` before calling either. `raycast` itself\nis capability-gated by `capabilities.raycasts`.\n\nThe legacy `collision.rayCast(line, result)` is a different API: it takes a\n`Line`, returns an **array** of every intersecting renderable, and only exists\non the built-in path.\n\nTwo prerequisites that produce empty results rather than errors:\n\n- **`isKinematic` must be `false`** for a renderable to be in the broadphase at\n  all (a `Body` sets this for you).\n- On the **built-in** adapter, the broadphase is cleared and rebuilt inside\n  `world.update()`, so querying before the first update returns nothing.\n\n## 3D collision: `Box3d`, `Sphere` and the Z pushback\n\nThe narrowphase is not 2D-only. Two shapes carry a depth extent — `Box3d` and\n`Sphere` — and any pair of them is resolved in three dimensions, pushing back\nalong **Z** as well as X and Y.\n\n```js\nimport { Box3d, Sphere } from \"melonjs\";\n\n// a box: centre offset from the body, then half-extents on each axis\ncrate.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));\n// a sphere: centre offset, then one radius\nrock.body.addShape(new Sphere(0, 0, 0, 30));\n```\n\nReach for these for anything moving in the XZ plane under a `Camera3d` — a\nrunner dodging obstacles, a 2.5D platformer, pickups on a course. Hand-rolling\na distance check there is the usual mistake, and it throws away the\npenetration vector the response already carries.\n\n**Which of the two.** A `Box3d` is world-axis-aligned and cannot be oriented:\nit is right for walls, floors, crates and anything else that stays square to\nthe world, and wrong for anything that turns. A `Sphere` has no orientation to\nget wrong, which makes it the shape for things that tumble, and for anything\nlaid out over a curved surface — on a globe every object's own \"up\" points a\ndifferent way, so no single axis-aligned box fits any of them. It is also the\ncheaper test: one distance against a sum of radii.\n\n**Z arrives beside `overlapN`, not inside it.** `overlapN` stays the 2D unit\nvector it always was and `overlapNZ` carries the depth component, because\n`Vector3d` does not extend `Vector2d` and widening the field would have broken\nevery existing 2D handler. The two together are a 3D unit vector, and\n`overlapZ = overlapNZ * overlap` exactly as `overlapV = overlapN * overlap`.\n\n**How much of the normal is in each half depends on the shapes.** A `Box3d`\npair separates along ONE world axis, so a depth contact leaves the 2D fields\nat zero and a legacy 2D handler correctly applies no push. A `Sphere`\nseparates along the line between the two centres, which is generally diagonal:\nall three components are non-zero at once. So subtract both halves rather than\nbranching on one:\n\n```js\nonCollision(response, other) {\n    // works for either shape: the 2D half is zero when there is no 2D push\n    this.pos.x -= response.overlapV.x;\n    this.pos.y -= response.overlapV.y;\n    this.pos.z -= response.overlapZ;\n    return false;\n}\n```\n\nBranching on `if (response.overlapNZ !== 0)` and then moving only `pos.z` is\nthe mistake to avoid: correct for a box pair, and for a sphere it throws away\nthe planar half of the contact.\n\n**Mixed pairs degrade to 2D.** A `Box3d` or a `Sphere` against a `Polygon`,\n`Rectangle`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and\ntreats the planar shape as unbounded along Z, which is what keeps a sphere or\nbox body colliding with an ordinary Tiled collision layer. `overlapZ` is zero\nfor those, since only one side has a depth to resolve. Give BOTH sides a 3D\nshape when you want a depth result.\n\nThis is the narrowphase and is independent of the 3D broadphase queries above\n— `querySphere` / `raycast3d` need `world.sortOn === \"depth\"`; a 3D body shape\ndoes not.\n\n`raycast3d` measures a `Box3d` body as its box and a body that is one `Sphere`\nas that sphere, both exactly. Anything else, including a body mixing the two,\nis measured as the AABB containing its 3D shapes, and a body with none falls\nback to a bounding sphere derived from the renderable's 2D bounds.\n\n**Bounds are what the broadphase sorts by, and they come from the SHAPE.**\nEvery item is filed by its renderable's 2D `getBounds()`, so an object with no\nextent cannot be placed and silently collides with nothing. A `Mesh` takes its\nextent from its geometry and a `GLTFModel` from its part meshes, so both report\nwhere they are without being told. A body's shapes do **not** contribute, so a\ncustom collidable built on a bare `Container` has no extent until you give it\none:\n\n```js\n// a container reports an EMPTY bounds until it has a size of its own\ngroup.resize(width, height);\n```\n\n`enableChildBoundsUpdate` also gives it one, but it re-measures the whole child\nlist on every update — reach for it only when the extent genuinely IS the\nmembers' union and moves with them (a flock, a squad), not to give a fixed\ngroup a size.\n\n## Built-in world quirks\n\nThese are specific to the default world and surprise people arriving from a\nreal rigid-body engine:\n\n- **Dynamic-dynamic collision is position-based, not Newtonian.** Separation is\n  mass-proportional but the velocity response is per-body cancellation — two\n  equal-mass bodies do not exchange momentum the way you would expect.\n- **An immovable contact wins the step.** Each step records, per body, which\n  directions it has already been pushed out of, and a later dynamic-dynamic\n  contact will not push it back in one of those directions: the other body\n  takes the whole correction instead. A light body shoving a heavy one against\n  a wall therefore stops dead at the wall rather than driving it through, and\n  the shover absorbs all of the separation. The record lasts one step and is\n  not contact state — it says nothing about what a body is touching.\n- **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate\n  `app.world.adapter.gravity` at runtime, or pass\n  `physic: new BuiltinAdapter({ gravity })` to override it. `body.gravityScale`\n  (default `1`) scales it per body; `0` disables gravity for that body and is\n  the portable replacement for the deprecated `body.ignoreGravity`.\n- **`applyForce(x, y)` is linear only** unless you pass the optional\n  `(pointX, pointY)` application point, which generates a torque\n  `τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.\n  `applyTorque(τ)` is the direct form.\n- **Force accumulators reset at end-of-step** — for every body, including\n  static, paused and off-screen ones — so forces must be applied every frame to\n  persist.\n- **`isGrounded` is flag-based, not contact-based** — the adapter returns\n  `!body.falling && !body.jumping`, flags updated by the last resolved\n  collision, not a live contact test.\n- **`def.density` maps 1:1 to `body.mass`** (default `1`), and `def.friction` is\n  **ignored** entirely. `body.friction` is a per-axis `Vector2d` of per-step\n  velocity damping fed from `def.frictionAir`, not a surface coefficient.\n- **`body.maxVel` defaults to `(490, 490)`** and hard-clamps velocity after\n  forces and friction. The canonical movement idiom is\n  `this.body.force.x = this.body.maxVel.x`.\n- **Default `collisionType` is `ENEMY_OBJECT`**, and default `collisionMask` is\n  `ALL_OBJECT` — set both explicitly.\n- **Integration is gated on `inViewport || alwaysUpdate`** — an off-screen body\n  stops simulating unless you set `alwaysUpdate = true`. It is also gated on\n  `updateWhenPaused` while the state manager is paused, and static bodies never\n  integrate.\n- **`addBody` throws on double-registration** for a renderable that is already\n  adapter-managed.\n\n### Rotation collides on planck and matter, but not on the built-in solver\n\nThis is the sharpest behaviour difference between the adapters, and it is\nsilent — nothing warns you.\n\n| adapter | a rotating body |\n| --- | --- |\n| **builtin** | rotates **visually only**. `body.angle`, `setAngle()` and angular integration write `renderable.currentTransform`; the collision shapes are never rotated, so a spinning sprite collides as if it were upright |\n| **planck** / **matter** | rotates for real — the solver owns the body's transform and collision follows it |\n\nSo a turret, a swinging platform or a tumbling crate that must *collide*\nrotated needs planck or matter. Under the builtin solver, approximate it with a\nshape that is close enough at every angle (a circle, or a square for\n90° steps), or rotate the shapes yourself with `body.rotate(angle)` — which\ndoes rotate them, unlike `setAngle()`.\n\n`body.rotate()` applies a **delta** and does not update `body.angle`; the two\nare unlinked, so a body driven by `rotate()` still reports an angle of `0`.\nTrack it yourself if you need to read it back. Before 20.7 it also threw on a\n`Box3d` or `Point` shape, and did not tell its owner that the body's bounds had\ngrown.\n\n`adapter.getBodyShapes(renderable)` reports whichever is true: the geometry\nthe engine is actually simulating on planck and matter (matter-adapter 1.3.0 /\nplanck-adapter 1.4.0 and later), unrotated shapes on the builtin solver because\nthere they genuinely are. That is what the debug overlay draws, so the hitbox\nyou see is the hitbox that collides — a sprite spinning inside a stationary red\nbox on the builtin is the solver telling you the truth, not a drawing bug.\n\nOn planck and matter that report comes from the engine rather than from your\ndefinitions, so it shows the approximations they make: an `Ellipse` appears as\nthe circle of average radius it is simulated as, a concave polygon as the\nconvex pieces it was decomposed into, and a `Rect` as a `Polygon`. Your\nauthored shapes are untouched and still readable on `renderable.bodyDef.shapes`.\n\n**If you need rotation to affect collision, use planck or matter.** That is the\ndividing line: the builtin is a position-based solver whose SAT never reads\n`body.angle`, so rotation there is a visual property. `body.rotate(angle)` is\nthe workaround, not a rotating body — it permanently moves the shapes, does not\ntrack an angle, and gives you no angular velocity, torque or rotational\nresponse from a collision. A game whose collision genuinely turns (a spinning\nhazard, a swinging bridge, a car, anything with torque) wants a real rigid-body\nadapter rather than a workaround.\n\n## Porting between adapters\n\nSame call, different magnitude — these are the ones that waste an afternoon:\n\n- **The portable API converts for you, the native handle does not.**\n  `adapter.setVelocity` / `body.setVelocity(x, y)` are in pixels on every\n  adapter; reaching for the raw handle (`body.setLinearVelocity` on planck) puts\n  you in metres per second, scaled by `pixelsPerMeter`.\n- **Time base differs.** Built-in `body.vel` is pixels *per frame*; planck and\n  matter integrate in seconds. The same number is ~60× off.\n- **Force magnitudes are not comparable** between adapters; re-tune rather than\n  reuse. `applyForce` is a per-step accumulator everywhere (cleared each step,\n  so call it every frame) — `applyImpulse` is the one-shot. Matter has no native\n  impulse, so the adapter emulates it as `Δv = J / mass` and ignores the\n  application point.\n- **`body.position` is the centroid in matter**, while `renderable.pos` is\n  top-left in melonJS. The adapter stores a per-body offset so `renderable.pos`\n  stays correct — but read `body.position` directly and you get the centroid.\n- **Degenerate (zero-area / collinear) polygons — a Tiled polyline, a `Line` —\n  cannot be built by matter and fall back to their axis-aligned bounding box.**\n  Any other shape type throws `unsupported shape type`.\n- **Ellipses become circles on matter**, using the average of the two radii.\n- **`isGrounded` is capability-gated** (`capabilities.isGrounded`) and computed\n  differently per adapter: a flag read on built-in, a live scan of active\n  contacts on matter.\n\nAdapter-specific escapes exist (`(this.body as MatterAdapter.Body).frictionAir`,\n`adapter.matter.Constraint.create(...)`) but are not portable — flag them if you\nuse them. `adapter.capabilities` (`constraints`,\n`continuousCollisionDetection`, `sleepingBodies`, `raycasts`, `raycasts3d`,\n`velocityLimit`, `isGrounded`) is the portable way to branch.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| entity jitters or does not move | assigning `pos` instead of applying force |\n| collision handler runs twice per frame | legacy `onCollision` on a dynamic pair (built-in) — use `onCollisionActive` |\n| `return false` from a handler does nothing | return values are only honoured by legacy `onCollision` — use `isSensor` / `isTrigger` |\n| crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |\n| `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |\n| `querySphere` / `raycast3d` is not a function | 2D adapter, or built-in without a 3D broadphase (`sortOn !== \"depth\"`) |\n| a 3D contact reports no overlap to push against | read `overlapNZ` / `overlapZ` — a Z-axis MTV leaves `overlapN` / `overlapV` at zero |\n| two 3D objects never separate in depth | one of them carries a planar shape; a mixed pair is tested as the box's XY footprint, unbounded along Z |\n| `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |\n| `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |\n| off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |\n| a body sinks through one shape of a multi-shape body | fixed in 20.7; before that the compound pass only ever re-resolved the first overlapping pair |\n| a body will not stay put on a slope on the builtin | it is riding the slope and sliding: the builtin has no surface friction, only `frictionAir` damping. Damp it yourself, or use matter/planck |\n| the sprite is drawn offset from its hitbox | pre-20.7 `anchorPoint` moved the drawing but not the collision shapes; upgrade, and drop any offsets you added to compensate |\n| a small shape sits at the sprite's corner | a shape's offset is measured from the drawn frame's TOP-LEFT; `new Sphere(0, 0, 0, r)` is the corner, `(width / 2, height / 2)` is the centre |\n| a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |\n| a body in a pile sinks into the floor (built-in) | fixed in 20.7 for anything with an immovable side; bodies pinned only by other DYNAMIC bodies still overlap by a pixel or two, which is what planck/matter are for |\n| forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |\n| `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, melonJS the top-left — the adapter offsets between them |\n| `bodyDef.shapes: \"…\"` throws \"is not loaded\" | the name must match the `name` the JSON was preloaded under, not its filename |\n| everything falls far too fast or too slow after swapping backends | gravity is a different unit per backend; see the table above |\n| debris lands holding its spawn angle and never tumbles | `fixedRotation: true`, or matter-adapter < 1.4.0 / planck-adapter < 1.5.0 |\n| a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use planck/matter, or `body.rotate(angle)` |\n| the debug hitbox does not follow a spinning body | pre-1.2.1 matter-adapter / pre-1.3.1 planck-adapter returned the authored shapes rather than the rotated ones |\n| a Tiled polyline becomes a solid box on matter | matter cannot build a zero-area polygon; the adapter falls back to its AABB |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic`, which also gates the broadphase\n- `melonjs-tilemaps` — collision shapes authored in Tiled\n- `melonjs-loading-assets` — preloading a shape editor's export as JSON\n","packages/melonjs/skills/melonjs-sprites-and-animation/SKILL.md":"---\nname: melonjs-sprites-and-animation\ndescription: \"Use this skill when creating sprites, playing frame animations, or working with texture atlases in melonJS. Covers Sprite construction from a plain image or an atlas region, addAnimation/setCurrentAnimation, TextureAtlas for TexturePacker/ShoeBox/Aseprite, tint, flip, NineSliceSprite, and the centred anchorPoint that offsets naive positioning. Triggers on: Sprite, new Sprite, addAnimation, setCurrentAnimation, isCurrentAnimation, TextureAtlas, TexturePacker, Aseprite, ShoeBox, atlas, region, framewidth, frameheight, tint, flipX, flipY, NineSliceSprite, animation.\"\nlicense: MIT\n---\n\n# Sprites, animation and atlases\n\n> Creating sprites from images or atlases, driving frame animation, and the\n> positioning trap that catches almost everyone.\n\n## `pos` is the centre, not the top-left\n\n`Renderable.anchorPoint` defaults to **`(0.5, 0.5)`**, so a sprite's `pos` is its\n**centre**. `Container` forces `(0, 0)` instead, which is why containers and\nsprites appear to disagree.\n\n```js\n// a 64×64 sprite at (100, 100) covers 68..132, not 100..164\nconst s = new Sprite(100, 100, { image: \"hero\", framewidth: 64, frameheight: 64 });\n\ns.anchorPoint.set(0, 0);   // switch to top-left positioning if you prefer\n```\n\nWriting top-left maths against the centred default puts everything half a sprite\noff, with no error. It is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Constructing a sprite\n\nFrom a plain image:\n\n```js\nconst hero = new Sprite(x, y, {\n    image: \"hero\",          // preloaded asset name\n    framewidth: 32,\n    frameheight: 32,\n});\n```\n\nFrom a texture atlas region:\n\n```js\nconst atlas = new TextureAtlas(loader.getJSON(\"sprites\"), loader.getImage(\"sprites\"));\nconst coin = new Sprite(x, y, { image: atlas, region: \"coin_01.png\" });\n```\n\n`framewidth` / `frameheight` define the animation grid on a sheet. Omit them and\nthe whole image is one frame.\n\n## Animation\n\n```js\nhero.addAnimation(\"idle\", [0, 1, 2, 3], 100);   // frames, ms per frame\nhero.addAnimation(\"walk\", [4, 5, 6, 7]);\nhero.addAnimation(\"die\",  [8, 9], 150);\n\nhero.setCurrentAnimation(\"walk\");\nhero.setCurrentAnimation(\"die\", \"idle\");                          // chain to idle when it ends\nhero.setCurrentAnimation(\"die\", { loop: false });                 // play once, hold the last frame\nhero.setCurrentAnimation(\"die\", { loop: false, onComplete: fn }); // …and call fn when it does\nhero.setCurrentAnimation(\"use\", { next: \"idle\", onComplete: fn }); // call fn, then chain to idle\nhero.setCurrentAnimation(\"run\", { onFrame: (frame) => {            // each frame playback reaches\n    if (frame === 3 || frame === 8) footstep();\n} });\n\nif (hero.isCurrentAnimation(\"walk\")) { /* … */ }\n```\n\nFrame indices are into the `framewidth`/`frameheight` grid. With an atlas, use\n`atlas.getAnimationSettings([regionNames])` and spread it into the Sprite\nsettings, or `atlas.createAnimationFromName([regionNames])`, which returns a\nready-made Sprite. On a fixed-cell spritesheet only numeric indices are\naccepted — passing region names throws.\n\nThe second argument to `setCurrentAnimation` is polymorphic. A **string** chains\nto that animation when this one ends. An **options object** takes\n`{ loop, next, speed, onComplete, onFrame }`, and only an explicit `loop: false`\nstops the looping. A **bare function** is the legacy form: it fires at *every* cycle end,\nand only holds the last frame if it returns `false`. Omit the argument entirely\nand the animation loops forever.\n\n`onFrame(frame)` is called with frame 0 when the animation starts, then with each\nframe playback reaches, so a footstep or a hit lands on its frame without polling\n`getCurrentAnimationFrame()`. A long tick reports every frame it goes past. It is\nnot called for a frame set with `setAnimationFrame`, nor for the frame 0 a\nplay-once animation would wrap to. Keep the callback in a field when you call\n`setCurrentAnimation` every frame: re-selecting keeps the first one anyway.\n\nWith both `next` and `onComplete`, the callback runs first and the chained\nanimation starts with none of its own, so a one-off action (a pickup, an\nattack) needs no manual switch back to idle.\n\nRe-selecting the animation that is already current is a no-op, so calling\n`setCurrentAnimation` every frame is safe, and an `isCurrentAnimation` check\naround it adds nothing. An unknown id throws.\n\n## Texture atlases\n\n`TextureAtlas` reads several formats:\n\n| source | how |\n|---|---|\n| TexturePacker / Free Texture Packer JSON | `new TextureAtlas(loader.getJSON(name), loader.getImage(name))` |\n| Aseprite **JSON export** | same as TexturePacker — the format is detected from `meta.app` |\n| Aseprite **`.aseprite` / `.ase` binary** | loader type `\"aseprite\"`, which stores the composited image *and* a JSON sidecar under the one asset name |\n| ShoeBox | JSON export using the melonJS exporter settings file; same call as TexturePacker |\n| fixed-cell spritesheet | `new TextureAtlas({ framewidth, frameheight, anchorPoint }, loader.getImage(name))` |\n| multi-page | pass an array of atlas JSON objects as the first argument |\n\nHelpers: `createSpriteFromName(name)`, `createAnimationFromName([names])`,\n`getAnimationSettings([names])`.\n\nAtlases matter for performance: the GPU batchers bind several textures at once\n(up to `renderer.maxTextures`), so a handful of separate images still batch —\nbut once a frame crosses that many distinct textures the batch has to flush.\nPacking into one atlas keeps everything on a single binding.\n\n## Visual properties\n\n```js\nsprite.setTint(\"#ff8000\");   // multiply tint\nsprite.alpha = 0.5;\nsprite.flipX(true);                   // mirror horizontally\nsprite.flipY(true);\nsprite.blendMode = \"additive\";        // see the effects skill for the full list\nsprite.scale(2, 2);                   // ← multiplicative, see below\n```\n\n**`scale()` is multiplicative, not absolute.** Calling it every frame compounds.\nFor absolute scaling reset first:\n\n```js\nsprite.currentTransform.identity();\nsprite.currentTransform.scale(s, s, 1);\n```\n\nThis bites hardest with pooled sprites, which arrive carrying their previous\nlife's transform.\n\n## `NineSliceSprite`\n\nFor panels and dialogue boxes that stretch without distorting their corners:\n\n```js\nnew NineSliceSprite(x, y, {\n    image: \"panel\", width: 300, height: 120,\n    insetx: 12, insety: 12,     // lowercase x/y — `insetX` is silently ignored\n});\n```\n\n`width` and `height` are mandatory and the constructor throws without them.\nThe insets default to a quarter of the frame.\n\n## Pooling\n\nFrequently spawned sprites should be pooled:\n\n```js\nimport { createPool } from \"melonjs\";\n\nconst bulletPool = createPool((x, y) => {    // me.pool is deprecated\n    const instance = new Bullet(x, y);\n    return { instance, reset: (x, y) => instance.onResetEvent(x, y) };\n});\nconst b = bulletPool.get(x, y);             // new Bullet(x, y), or reset(x, y) on a reused one\nworld.removeChild(b);                       // returns it to bulletPool automatically\n```\n\nTwo consequences:\n\n- A recyclable class's `onResetEvent` must reset **everything** its lifetime\n  mutates — alpha, tint, scale, animation state. Anything you forget carries\n  over to the next use.\n- Pool-recycled objects do **not** fire `onDestroyEvent` on removal. Pair event\n  subscriptions with `onActivateEvent` / `onDeactivateEvent` instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| everything offset by half a sprite | top-left maths against the centred `anchorPoint` |\n| sprite grows every frame | `scale()` is multiplicative — reset the transform |\n| a recycled sprite keeps its old look | `onResetEvent` does not restore every mutated property |\n| event handlers leak on pooled objects | bound in the constructor rather than `onActivateEvent` |\n| animation never advances | no `framewidth`/`frameheight`, so the sheet is one frame |\n| animation plays once and stops | `{ loop: false }`, or a callback that returned `false` |\n| sprites do not batch | more distinct textures in one frame than `renderer.maxTextures` |\n| nine-slice corners stretch anyway | `insetX`/`insetY` instead of `insetx`/`insety` |\n\n## Related skills\n\n- `melonjs-renderables` — anchors, draw order, custom draw\n- `melonjs-getting-started` — preloading images and atlases\n","packages/melonjs/skills/melonjs-input/SKILL.md":"---\nname: melonjs-input\ndescription: \"Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click.\"\nlicense: MIT\n---\n\n# Input: keyboard, pointer and gamepad\n\n## Keyboard goes through named actions\n\nDo not poll key codes. Bind a key to an **action string**, then poll the action —\nthis is what lets gamepad and virtual controls map onto the same handling.\n\n```js\nimport { input } from \"melonjs\";\n\ninput.bindKey(input.KEY.LEFT,  \"left\");\ninput.bindKey(input.KEY.RIGHT, \"right\");\ninput.bindKey(input.KEY.X,     \"jump\", true);   // true = locked: one press per keydown\n\n// in update()\nif (input.isKeyPressed(\"left\"))  { /* … */ }\nif (input.isKeyPressed(\"jump\"))  { /* fires once per press, not per frame */ }\n```\n\nThe third argument to `bindKey` is `lock` (default `false`). Without it,\n`isKeyPressed` stays true for as long as the key is held — correct for movement,\nwrong for jump or fire. A locked action stays consumed until the key is released\nor you call `input.unlockKey(\"jump\")`. Use `input.keyStatus(action)` when you\nwant the raw held/not-held state regardless of locking.\n\n### A poll can miss a press entirely\n\n`isKeyPressed` is sampled once per frame, and `keyup` decrements the key's\ncounter the moment it arrives — locked or not. A press that goes down and up\nbetween two frames therefore leaves the counter back at zero and is never\nseen. At 60fps that is anything under ~16ms: rare from a human finger, routine\nfrom a synthetic event, and more likely the slower the frame rate.\n\nFor a screen where a missed press is a dead end — a splash, a \"press any key\",\na notice to acknowledge — listen for the event instead, which cannot be missed:\n\n```js\nimport { event } from \"melonjs\";\n\n// action is the bound action name, or undefined for an unbound key\nconst onKey = (action) => { if (action === \"menuSelect\") continue(); };\nevent.on(event.KEYDOWN, onKey);      // in onResetEvent\nevent.off(event.KEYDOWN, onKey);     // in onDestroyEvent — it does not self-remove\n```\n\n`KEYDOWN` fires for *every* key, bound or not, so matching on `action` is what\nnarrows it; it also repeats while the key is held, so guard anything that must\nhappen once. Keep polling for movement and for a menu cursor, where one step\nper press and a key held across a state change are the things that matter.\n\nThe full signature is `bindKey(keyCode, action, lock?, preventDefault?)`;\n`preventDefault` defaults to `input.preventDefault` (itself `true`). Pass the\nfourth argument per binding, or change the global default with\n`input.setPreventDefault(false)` — **assigning `input.preventDefault = false`\ndoes not work**, it is a read-only module binding.\n\n## Pointer events need `isKinematic = false`\n\n**This is the trap.** `Renderable.isKinematic` defaults to `true`, and both the\nbroadphase and the pointer dispatcher skip any candidate whose `isKinematic` is\nstill `true`. The handler registers successfully and simply never fires.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.isKinematic = false;               // ← required\n    }\n    onActivateEvent() {\n        input.registerPointerEvent(\"pointerdown\", this, this.onDown.bind(this));\n    }\n    onDeactivateEvent() {\n        input.releasePointerEvent(\"pointerdown\", this);\n    }\n}\n```\n\n`Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*` elements set it\nto `false` for you, and so does attaching a physics body (`Body`'s constructor\nsets `ancestor.isKinematic = false`) — so plain `Renderable` and `Sprite`\nsubclasses with no body are the ones that catch people.\n\nRegister in `onActivateEvent` and release in `onDeactivateEvent`, not in the\nconstructor — pooled objects are reused and would otherwise accumulate handlers.\n\n`app.viewport` works as a whole-screen region when you want global clicks. Note\nit is asked **before** everything else, not as a fallback after nothing matched:\nthe dispatcher appends it to the candidate list and walks that list from the\nend. So a viewport handler must not return `false` unless it really means to\nswallow every pointer event in the game.\n\nThe accepted event names are exactly `\"pointerdown\"`, `\"pointerup\"`,\n`\"pointermove\"`, `\"pointercancel\"`, `\"pointerenter\"`, `\"pointerover\"`,\n`\"pointerleave\"` and `\"wheel\"`. Anything else — `\"click\"`, `\"mousedown\"`,\n`\"touchstart\"` — makes both `registerPointerEvent` and `releasePointerEvent`\nthrow `invalid event type`. melonJS maps the canonical name onto whichever\nmouse/touch events the device actually supports, so you always register the\npointer name.\n\n## Which object gets the event\n\nThe dispatcher collects the regions near the pointer from the broadphase,\nbounds-checks each one, orders them **the way they are drawn** and asks the\ntopmost first, walking down until something consumes.\n\nA handler consumes by returning `false`. This is the DOM sense and it reads\nbackwards: `false` means handled, and anything else, `undefined` included, lets\nthe walk carry on to whatever is underneath.\n\n```js\ninput.registerPointerEvent(\"pointerdown\", this, () => {\n    this.fire();\n    return false;        // nothing under this gets the click\n});\n```\n\nThree things decide the order, and only the first is obvious:\n\n- **`pos.z`, among siblings of one container.** Higher is on top.\n- **Across containers, the common ancestor decides.** `pos.z` is\n  container-local, since `autoDepth` numbers each container's children from 1,\n  so a button at `z = 8` inside one panel does not outrank a second panel at\n  `z = 2`. The pair is resolved by walking both up to their shared parent and\n  comparing the two siblings there, which is exactly how drawing resolves it.\n  Raise a whole panel with `ancestor.moveToTop(panel)`, not by inflating a\n  child's `z`.\n- **A child is asked before the container holding it**, since it draws over it.\n\nEqual `z` between siblings falls back to child order, lower index on top. Two\ncallbacks registered on the **same** region for the same event run\nlast-registered-first.\n\nConsuming a **move** also takes the pointer away from the regions below: each\none still holding it gets its `pointerleave`, because it is covered now and\nbeing inside its own bounds no longer decides. Consuming a press, a release or a\nwheel does not, since none of those says where the pointer is.\n\n## World coordinates versus screen coordinates\n\nThe pointer carries several, and picking the wrong one produces a feedback loop:\n\n| property | meaning | use for |\n|---|---|---|\n| `pointer.gameScreenX` / `gameScreenY` | canvas-relative position | anything that **moves the camera** |\n| `pointer.gameWorldX` / `gameWorldY` | projected into world space | picking and spawning objects |\n| `pointer.gameLocalX` / `gameLocalY` | relative to the region's ancestor container | hit-testing inside a container |\n| `pointer.gameX` / `gameY` | **context-dependent** — see below | region handlers on non-floating regions |\n\n`gameX`/`gameY` is not a fixed coordinate space. The dispatcher rewrites it per\ncandidate just before calling your handler: world coordinates for a normal\nregion, screen coordinates for a `isFloating === true` region. On the global\n`event.POINTERMOVE` broadcast it is set to the **screen** coordinates. When you\nneed one specific space, read `gameWorldX`/`gameWorldY` or\n`gameScreenX`/`gameScreenY` by name rather than relying on `gameX`.\n\nUsing world coordinates for a camera-orbit drag means the same pixel maps to a\ndifferent world point every frame as the camera moves — the drag jumps wildly.\n\n## Pointer move and release outside a region\n\nRegion handlers only fire while the pointer is over the region. For drag\ntracking, listen globally:\n\n```js\nimport { event } from \"melonjs\";\nevent.on(event.POINTERMOVE, (pointer) => { /* … */ });\n```\n\nThere is **no global pointer-up system event** — `event.ts` publishes only\n`POINTERMOVE`, `POINTERLOCKCHANGE`, `DRAGSTART` and `DRAGEND`. For \"released\nanywhere\" you need a raw listener:\n\n```js\nglobalThis.addEventListener(\"pointerup\", onRelease);\n```\n\nFor plain drag-and-drop, `Draggable` / `DropTarget` already wire all of this up\n(`pointerdown` / `pointerup` / `pointercancel` regions plus a global\n`POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`. `DRAGEND`\nstill originates from a region handler, so it only fires when the release lands\non the draggable itself.\n\n## Gamepad\n\nGamepad input maps onto the same action strings, so game code does not branch:\n\n```js\ninput.bindGamepad(0,\n    { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 },\n    input.KEY.X);        // → the \"jump\" action bound above\n```\n\n**Order matters:** `bindGamepad` throws `no action defined for keycode N` if the\nkeycode has not already been passed to `bindKey`.\n\nAxes use the same call with a threshold, and bind **one half of the axis each**\n— the sign selects which half:\n\n```js\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold: -0.15 }, input.KEY.LEFT);\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold:  0.15 }, input.KEY.RIGHT);\n```\n\n**`threshold` is added to the deadzone, it does not replace it.** The test is\n`abs(value) >= deadzone + abs(threshold)`, so with the default deadzone of 0.1\na threshold of `0.5` does not fire until 0.6 of full travel, and `0.15` fires at\n0.25. Pick the number you want the stick to break at and subtract the deadzone.\nRaising `setGamepadDeadzone` moves every axis binding with it.\n\n`unbindGamepad(index, button, type?)`, `setGamepadDeadzone(value)` and\n`setGamepadMapping(id, mapping)` complete the surface.\n\n### Which index to bind\n\nBindings are per pad **index**, and the index is plug order, not identity —\nnothing reports which pad is \"the\" controller. Slot 0 is a guess: a wheel, a\nspare pad or a keyboard-shaped device left plugged in can hold it for a whole\nsession while the player presses buttons on slot 1.\n\nSo a one-player game binds **every slot it will accept** to the same actions,\nand lets `activeGamepad()` say which one is in use when something has to go\nback the other way:\n\n```js\nfor (let pad = 0; pad < 4; pad++) {\n    input.bindGamepad(pad, { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 }, input.KEY.X);\n}\ninput.activeGamepad();   // the last pad to produce a bound input, else the\n                         // first connected one, else -1\n```\n\nBinding spare slots costs nothing: the poll skips a bound index with no pad in\nit. `activeGamepad()` follows **bound inputs only**, which is what makes it an\nanswer about the player rather than about electrical noise — a worn stick at\nrest reports movement for ever and would otherwise claim to be the active pad.\n\n### Rumble\n\n```js\ninput.vibrateGamepad(input.activeGamepad(), {\n    duration: 120, strongMagnitude: 0.6, weakMagnitude: 0.9,\n});\ninput.stopGamepadVibration(index);   // cut a long effect short\ninput.hasGamepadVibration(index);    // for an options-screen toggle\n```\n\nSafe to call blind: no pad, no motors, or no Gamepad API at all resolves\n`false` and does nothing, so game code never branches on support. Magnitudes\ndefault to **full** — the underlying web API defaults every parameter to zero,\nwhich makes a bare `playEffect` a silent no-op. A second call preempts the\nfirst on the same pad, so a rapid series of hits reads as a rapid series of\nhits rather than as a queue. For a SUSTAINED rumble issue one effect with a\nlong `duration` instead of calling every frame.\n\nHot-plugging needs no polling of your own — the engine listens for the browser's\nown events and republishes them:\n\n```js\nimport { event } from \"melonjs\";\n\nevent.on(event.GAMEPAD_CONNECTED,    (gamepad) => { /* gamepad.id, .index */ });\nevent.on(event.GAMEPAD_DISCONNECTED, (gamepad) => { /* pause, show a prompt */ });\n```\n\nBindings survive a disconnect: they are attached to the index, not to the\ndevice, so a pad unplugged and plugged back into the same slot keeps working.\n\n## Virtual controls\n\nOn-screen buttons synthesise key events, again reaching the same actions:\n\n```js\ninput.triggerKeyEvent(input.KEY.LEFT, true);    // press\ninput.triggerKeyEvent(input.KEY.LEFT, false);   // release\n```\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| pointer handler never fires | `isKinematic` left at its `true` default |\n| handlers fire twice, or on dead objects | registered in the constructor instead of `onActivateEvent` |\n| camera drag jumps around | using world coordinates instead of `gameScreenX`/`gameScreenY` |\n| `gameX` holds screen coords where world coords were expected | read on `event.POINTERMOVE`, or on a floating region — use `gameWorldX`/`gameWorldY` |\n| drag stops when leaving the object | region events end at the region — use `event.POINTERMOVE` |\n| release never detected outside the region | no global pointer-up event — add a raw listener |\n| jump fires every frame while held | `bindKey` without its third `lock` argument |\n| `invalid event type` thrown at registration | non-pointer name (`\"click\"`, `\"mousedown\"`, …) passed to `registerPointerEvent` |\n| `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |\n| stick has to be pushed well past its `threshold` before it registers | `threshold` is ADDED to the deadzone, not used instead of it |\n| one half of a stick does nothing | each half is a separate `bindGamepad`, chosen by the sign of `threshold` |\n| a widget under an overlapping panel still reacts | the panel consumes nothing — return `false` from its `onClick` and `onOver` |\n| a widget stays highlighted after the pointer moves onto a panel over it | the panel consumes no `pointermove`, so nothing takes the pointer off the widget |\n| a child's big `z` does not put it in front of another container | `pos.z` is container-local — use `ancestor.moveToTop()` on the container |\n| every click in the game stops working | a handler on `app.viewport` returning `false`, which is asked first |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic` also gates the physics broadphase\n- `melonjs-ui-and-text` — `UISpriteElement` and `UITextButton` handle this for you\n","packages/melonjs/skills/melonjs-tilemaps/SKILL.md":"---\nname: melonjs-tilemaps\ndescription: \"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 registerTiledObjectClass and 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, registerTiledObjectClass, registerTiledObjectFactory, TMXObject, resolvedProperty, resolvedProperties, tile class, tile properties.\"\nlicense: MIT\n---\n\n# Tiled maps\n\n> melonJS's flagship content pipeline. Maps are authored in\n> [Tiled](https://www.mapeditor.org/) and loaded through the `level` director —\n> you rarely touch the `TMX*` classes directly.\n\n## Loading a level\n\nPreload the map **and** everything it references — the `.tmx`, any external\n`.tsx` tilesets, and the tileset images:\n\n```js\nawait loader.preload([\n    { name: \"map1\",    type: \"tmx\", src: \"data/map/map1.tmx\" },\n    { name: \"tileset\", type: \"tsx\", src: \"data/map/tileset.tsx\" },\n    { name: \"tiles\",   type: \"image\", src: \"data/img/tiles.png\" },\n]);\n\nlevel.load(\"map1\");           // inside Stage.onResetEvent\n```\n\nAsset `type` is `\"tmx\"` for maps and `\"tsx\"` for external tilesets — those are\nthe only two type strings the loader knows. The *file* can be `.tmx` / `.tsx`\n(XML), or `.json` / `.tmj` / `.tsj`; the parser branches on the extension, not\non the type. `type: \"tmj\"` throws `unknown or invalid resource type`. You can\nalso skip `src` and pass the map inline via `data` (with `format: \"json\"` or\n`\"xml\"`).\n\n`level.load(levelId, options)` accepts `container` (default `game.world`),\n`onLoaded` (default `game.onLevelLoaded`), `flatten` (default `game.mergeGroup`),\n`setViewportBounds` (default **`true`**) and `async` (default `false`). It throws\n`level <id> not found` for an unknown id — synchronously, in both forms, because\nthat is a typo rather than a load failure.\n\nThose `game.` defaults are the engine's own, and `game` is whichever\n`Application` finished `init()` last. **Pass `container` explicitly** from a\n`Stage`, which has the right one: `level.load(\"map\", { container: this.app.world })`.\n\n**`level.load` is deferred while the game loop is running.** It calls\n`state.stop()` and finishes the load on a timer, after the current frame, so by\ndefault it returns `true` before anything is in the world. Two ways to sequence\nwork after it:\n\n```js\n// await it\nawait level.load(\"map1\", { async: true });\n// the world is populated here\n\n// ...or use the callback / event, which fire in both forms\nlevel.load(\"map1\", { onLoaded: () => this.spawnPlayer() });\n```\n\n`async: true` is the only thing that changes the return value — everything else\nbehaves identically, `onLoaded` included. Without it the call returns a boolean,\nso `await level.load(\"map1\")` is not an error and does not await the load:\n`await true` resolves immediately, while the load is still sitting on a timer.\nPass the flag when you mean to await.\n\n`level.reload()`, `level.next()` and `level.previous()` take the same `async`\noption and resolve the same value they return — so `if (level.next())` becomes\n`if (await level.next({ async: true }))`. Running out of levels reports `false`\neither way rather than throwing. `level.getCurrentLevelId()` and\n`level.levelCount()` round out the namespace. `flatten: false` wraps each Tiled\nobject group in its own `Container` named after the group.\n\n## Spawning entities from Tiled objects\n\nObjects are matched against a factory registry, in this order: the object's\nTiled **class**, then its **name**, then, for a tile object with no class of\nits own, its **tile's class**, then the structural fallbacks `\"text\"`,\n`\"tile\"` and `\"shape\"`.\n\n```js\nimport { registerTiledObjectClass, registerTiledObjectFactory } from \"melonjs\";\n\nregisterTiledObjectClass(\"Enemy\", Enemy);   // ← matches Tiled class OR name\nregisterTiledObjectFactory(\"Spine\", (settings, map) => { /* → Renderable */ });\nlevel.load(\"map1\");\n```\n\nThese are the entry points to use. The registry is Tiled's own, held by the\nobject factory, and has nothing to do with object pooling.\n\n`pool.register(className, classObj)` **also** works and registers a Tiled\nfactory as a side effect, under the name and again under an `me.`-prefixed\nalias, unless you set `pool.autoRegisterTiled = false`. That is the older\nspelling, kept working for maps and games written against it, and it now\nprints a one-off deprecation notice: `me.pool` is deprecated since 18.0.0.\n\nIts third argument no longer buys anything here. It used to make Tiled\nobjects recyclable; they are constructed directly now, and the container\nreturns children to typed pools rather than to the name-keyed one. To pool a\nclass you spawn constantly, give it a `createPool` and register it for Tiled\nseparately:\n\n```js\nregisterTiledObjectClass(\"mainPlayer\", PlayerEntity);\n```\n\nRegister **before** loading. With no match the object falls through to the\nstructural factory and becomes a plain `Renderable` with a static body — it does\nnot error, it just has none of your behaviour.\n\n`registerTiledObjectClass` **throws** if you register a *different* constructor\nunder a name already taken (re-registering the same one is a no-op).\n`registerTiledObjectFactory` overwrites and only `console.warn`s, so it is the\none to use for replacing something already registered.\n\n**Overriding a built-in name always works.** The engine's own classes are\nDEFAULTS: they fill a name nothing else has claimed, and yours takes it over\nwhenever you register, before or after a map has loaded.\n\n```js\nregisterTiledObjectClass(\"Trigger\", MyTrigger);   // whenever you like\n```\n\nThe throw is for a clash between two of *your* classes under one name. For\nthat case `registerTiledObjectFactory` overwrites and only warns.\n\n### Classes and properties set on tiles\n\nIn Tiled, a tile can carry a class and custom properties (tileset editor), and\nevery object placed from that tile shows them as its own. melonJS follows:\n\n- **The tile's class.** A tile object with no class is built by the class\n  registered under its tile's class, so a designer sets \"Chest\" once on the\n  chest tile and every chest dropped in the map is a `Chest`. The object's\n  own class or registered name still comes first, and a tile classed like a\n  built-in (`Sprite`, `Trigger`, `shape`...) stays a plain tile.\n- **The tile's properties.** Read them with `settings.resolvedProperty(name)`\n  or `settings.resolvedProperties()`, which merge the tile's properties under\n  the object's own, as Tiled's scripting API of the same names does. They are\n  **not** set on `settings` by name: only the object's own properties are,\n  as before, so a tile property named like a sprite setting never reaches a\n  plain tile sprite.\n\n```ts\nimport { registerTiledObjectClass, Sprite, type TMXObject } from \"melonjs\";\n\n// the custom properties set on the object itself, by name\ninterface DoorSettings extends TMXObject {\n    locked?: boolean;\n}\n\nclass Door extends Sprite {\n    locked: boolean;\n    facing: string;\n    constructor(x: number, y: number, settings: DoorSettings) {\n        super(x, y, { image: \"door\" });\n        this.locked = settings.locked ?? false;\n        // on the object, or else on the door tile in the tileset\n        this.facing = settings.resolvedProperty(\"facing\") ?? \"N\";\n    }\n}\nregisterTiledObjectClass(\"Door\", Door);   // and give the door tile the class \"Door\"\n```\n\n`TMXObject` is the type of the `settings` a registered class (or factory)\nreceives: the object as read from the map, with `tile` (a `Tile`) on a tile\nobject. Extend it with the properties your class reads.\n\nFor a class that is not a `Sprite` itself (a `Container` holding the tile and\nsomething else, say), `settings.tile.getRenderable()` returns the tile's sprite\nas a plain tile object would get it: from a tileset image, an image collection\nor an animated tile alike, with no region names to build by hand.\n\nmelonJS pre-registers `Renderable`, `Sprite`, `NineSliceSprite`, `Text`,\n`BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable`, `Trigger`\nand `Entity` as Tiled classes, so `Collectable` and `Trigger` work out of the\nbox. Each is registered under its `me.`-prefixed alias as well, so a map\nauthored against melonJS 1.x finds them where it looks.\n\nA `Trigger` forwards a **fixed list** of settings to `level.load()` — not\nwhatever you pass it. Today that list is `container`, `onLoaded`, `flatten`,\n`setViewportBounds` and the glTF scene options (`scale`, `rightHanded`,\n`lights`, `lightIntensityScale`, `castGroundShadow`, `shadowGroundY`); see the\n3D assets skill. Anything outside it is **dropped silently**, which is worth\nknowing before you spend an afternoon on a map that loads at the wrong scale.\n`async` is excluded deliberately — the trigger needs to sequence its own\ntransition.\n\n## Camera bounds\n\n`level.load` sets the viewport bounds to the map size for you —\n`setViewportBounds` defaults to `true`, and the handler is re-run on every\nviewport resize, centring the map when it is smaller than the screen. You only\nneed to do it by hand when you opted out:\n\n```js\nlevel.load(\"map1\", { setViewportBounds: false });\napp.viewport.setBounds(0, 0, mapWidth, mapHeight);   // now it's on you\n```\n\nThe same is true if you drive `TMXTileMap.addTo(container, flatten, setViewportBounds)`\ndirectly — note that method's `setViewportBounds` defaults to **`false`**, the\nopposite of `level.load`'s.\n\n## Collision shapes\n\nDraw collision geometry in Tiled on an object layer and melonJS turns it into\nworld shapes automatically — rectangles, polygons, polylines, ellipses and\npoints all work.\n\nThe contract is the **layer name**: melonJS lowercases it and checks whether it\n*contains* `\"collision\"` (so `collision`, `Collision`, `world collision` all\nmatch). Inside such a layer, objects **with no name** get\n`collisionType = collision.types.WORLD_SHAPE` and `type = \"static\"`. A *named*\nobject in a collision layer keeps whatever its factory gave it — that is how you\nmix triggers into the collision layer.\n\nAn object that matches no factory and carries no text or tile data falls to the\n`\"shape\"` factory: a plain `Renderable` with a static `bodyDef` built from its\ngeometry, which the world auto-registers with the active adapter on `addChild`.\n\nFor per-object physics, set the object's custom properties in Tiled; they arrive\non the settings object passed to your entity's constructor. Properties set on\nthe object's tile are read with `settings.resolvedProperty(name)`.\n\n## Map orientations\n\nOrthogonal, **isometric**, hexagonal, staggered and oblique are all supported;\nanything else throws `<orientation> type TMX Tile Map not supported!` at load.\nIsometric maps need y-sorting so objects overlap correctly:\n\n```js\napp.world.sortOn = \"y\";       // after level.load\n```\n\nThe world then keeps its objects in order as they move (`autoSort`, on by\ndefault): a character walking around passes in front of and behind the scenery\nwith no `sort()` call.\n\n`sortOn` only accepts `\"x\"`, `\"y\"`, `\"z\"` (the default) or `\"depth\"` and throws\non anything else. `Vector2d`, `Vector3d`, their observable variants and `Polygon`\nall carry `.toIso()` / `.to2d()` for converting between screen and map space.\n\n### What the y-sort can and cannot sort\n\n`sortOn = \"y\"` sorts **objects**, never tiles. A tile layer is a single\nrenderable drawn in one piece, so a character ends up in front of all of it or\nbehind all of it. Anything a character walks both in front of and behind\n(walls, trees, furniture) has to be on an **object layer, as tile objects**,\nand in the **same** object layer as the characters: each Tiled layer gets its\nown `z`, and the y-sort compares `z` before `y`. Keep the floor on a tile layer\nbelow them. This holds for any y-sorted map, isometric or a top-down\northogonal one.\n\n```\nfloor      tile layer     the ground, drawn first\ndungeon    object layer   walls, props, chests, doors, the hero\ncollision  object layer   the shapes (not drawn)\n```\n\nTile objects with no class are built for you: each one is a `Sprite` with its\ndefault body, static, over its whole image box and typed `ENEMY_OBJECT`. A\ncharacter keeping the default `collisionMask` (every type) collides with all of\nthem, so when the scenery's real collision is drawn in the collision layer,\nmask the character to it:\n\n```js\nthis.body.setCollisionMask(collision.types.WORLD_SHAPE);\n```\n\nGive the objects with behaviour a Tiled class and register it, as above.\n\nThe `y` it sorts on is `pos.y`, and Tiled objects default to a `(0, 0)` anchor,\nso that is the **top** of each image. Sorting by the top only matches sorting by\nthe feet when every image is the same height and stands on the bottom of its\nbox, the usual layout of an isometric pack. A shorter image sorts in front of\nthings it stands behind. Either pad the art to one height, or line every object\nup by the bottom of its image with `sortOffset`, which changes the order and\nnever the position:\n\n```js\nlevel.load(\"map1\");\napp.world.sortOn = \"y\";\nfor (const child of app.world.getChildren()) {\n    child.sortOffset = child.height;   // sort by the bottom of each image\n}\n```\n\nA character you create in code follows the same convention as the scenery: by\nthe top of a frame as tall as the scenery's (`anchorPoint` `(0, 0)`), or by its\nfeet when everything sorts by the bottom.\n\nTwo equal positions keep insertion order, and the one added **last** is drawn\n**behind**: a decoration placed at its wall's exact position disappears. Give\nit `sortOffset = 1`. A `Container` or a particle emitter is sorted by its top\nleft corner (an emitter's by the top of its spawn area); give it the offset to\nthe point it belongs with, such as `door.pos.y - dust.pos.y`.\n\n## Image layers\n\n`ImageLayer` gives parallax backgrounds with a ratio per axis, set in Tiled or in\ncode. Repeat behaviour matches Tiled's.\n\n## Compressed maps need a plugin\n\nA map saved with **gzip, zlib or zstd** compressed layer data throws\n`No inflate function set — GZIP/ZLIB decompression not supported!` at parse time\nwithout:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { TiledInflatePlugin } from \"@melonjs/tiled-inflate-plugin\";\n\nplugin.register(TiledInflatePlugin);   // before preloading any compressed map\n```\n\nUncompressed base64 and CSV layer data need no plugin. If a map loads in Tiled\nbut not in melonJS, check the layer compression setting first.\n\n## Rendering\n\nEach tile layer resolves to one of three `renderMode`s — `\"shader\"`,\n`\"prerender\"` or `\"perTile\"` — auto-selected in that order of preference, or\nforced per layer. The `\"shader\"` path is a single quad per tileset with a\nper-pixel GID lookup in the fragment shader. It requires **all** of:\n\n- a renderer that reports GPU tile-layer support (WebGL 2 / WebGPU — not Canvas),\n- `world.gpuTilemap !== false` (the `gpuTilemap` application setting, default `true`),\n- an **orthogonal** layer — isometric, hexagonal, staggered and oblique always\n  fall back,\n- no collection-of-image tileset, no non-zero `tileoffset`, and tile overflow\n  within the shader's 4-cell limit.\n\nWhen a layer falls back, melonJS `console.warn`s once with the reason. Practical\nconsequences: fewer tilesets means fewer batches (one packed tileset beats\nseveral small ones), and on the Canvas fallback large maps get noticeably\nslower. `world.preRender` (or a `preRender` layer property in Tiled) opts an\nunanimated layer into the offscreen-bake path instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| 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` |\n| the world is still empty right after `level.load` | the load is deferred to a timer while the loop runs — `await level.load(id, { async: true })`, or use `onLoaded` / `LEVEL_LOADED` |\n| `await level.load(id)` returned `true` rather than a promise | without `async: true` the call returns a boolean; `await true` resolves immediately and the load has not run yet — pass the flag when you mean to await |\n| `level <id> not found` | the map was never preloaded, or the asset `name` differs from the id passed to `load` |\n| `unknown or invalid resource type` | asset `type` set to `\"tmj\"` / `\"tsj\"` — use `\"tmx\"` / `\"tsx\"` with the `.tmj` / `.tsj` file |\n| camera will not scroll | `setViewportBounds: false`, or the map was added with `addTo()` (which defaults to `false`) |\n| `No inflate function set` at parse time | compressed layers without the tiled-inflate plugin |\n| collision shapes are not solid | the object layer's name does not contain `\"collision\"`, or the objects are named |\n| isometric objects overlap wrongly | `world.sortOn` not set to `\"y\"` |\n| a character is always in front of (or behind) every wall | the walls are on a tile layer, or on another object layer than the character: each layer has its own `z`, compared before `y` |\n| a small prop draws over a character standing in front of it | images of different heights: the y-sort uses `pos.y`, the top of a Tiled object's image. Pad the art, or set `sortOffset = height` on every object to sort by the bottom |\n| a decoration placed at the same position as a wall disappears behind it | a tie keeps insertion order and draws the later one behind: `sortOffset = 1` |\n| particles (or a container) at the foot of something draw over a character standing in front of it | they sort by the top of their spawn area (or the container's top left): set `sortOffset` to line them up with what they belong to |\n| a character is stopped short of walls and props placed as tile objects | it collides with their default bodies (static, `ENEMY_OBJECT`, the whole image box); mask it to `collision.types.WORLD_SHAPE` when the collision layer holds the real shapes |\n| a property set on a tile is undefined on the object's `settings` | tile properties are not copied onto `settings`: read them with `settings.resolvedProperty(name)` |\n| objects placed from a classed tile come out as plain sprites | that class is not registered, it is a built-in name, or the object has a class or a registered name of its own, which come first |\n| missing tiles or a blank map | the `.tsx` or tileset image was not preloaded |\n| tilemap slow on some machines | layer fell back off the shader path — check the console warning for the reason |\n\n## Related skills\n\n- `melonjs-physics` — what the collision shapes become once loaded\n- `melonjs-getting-started` — preloading and the asset type list\n","packages/melonjs/skills/melonjs-audio/SKILL.md":"---\nname: melonjs-audio\ndescription: \"Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay, no sound, sound stopped working, context suspended, silent after idle, clips silent but tones play.\"\nlicense: MIT\n---\n\n# Audio\n\n## Two setup rules\n\n```js\nimport { audio, loader } from \"melonjs\";\n\naudio.init(\"mp3,ogg\");                      // 1. BEFORE preloading any audio\n\nawait loader.preload([\n    { name: \"cling\", type: \"audio\", src: \"data/sfx/\" },   // 2. a PREFIX, not a file\n]);\n```\n\n1. **`audio.init(format)` must run before any audio asset is preloaded**, and\n   lists the formats you actually ship. Preloading first throws\n   `target audio extension(s) should be set through me.audio.init() before\n   calling the preloader.` It returns `false` when no audio backend is\n   available, so it is worth checking. The format list defaults to `\"mp3\"`.\n2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus\n   each configured extension and uses the first that loads — so\n   `data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename\n   gives you a 404. A `data:audio/...` URL is the one exception: it is used\n   verbatim, prefix and extension skipped.\n\n`audio.hasAudio()` says whether the browser can play audio at all, and\n`audio.hasFormat(\"ogg\")` whether it can play one codec — useful for choosing\nthe `init` list or hiding a sound toggle on a device with no audio.\n\nOrder matters — melonJS tries the listed formats left to right, so put the\npreferred one first. Two formats (`\"webm,mp3\"` or `\"mp3,ogg\"`) is the usual\nbelt-and-braces for codec coverage. Accepted tokens: `mp3`, `mpeg`, `opus`,\n`ogg`, `oga`, `wav`, `aac`, `caf`, `m4a`, `m4b`, `mp4`, `weba`, `webm`, `dolby`,\n`flac`.\n\n## Effects versus music\n\n```js\naudio.play(\"cling\");                    // fire and forget\naudio.play(\"cling\", false, null, 0.5);  // loop, onend, volume\nconst id = audio.play(\"engine\", true);  // looping; keep the id to stop it\n\naudio.playTrack(\"theme\");               // background music\naudio.stopTrack();\n```\n\nUse **`playTrack`** for music, not `play`. `playTrack(name, volume?)` always\nloops, and it records the name as the current track — which is what\n`app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via\n`state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,\n`resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there\nis only ever one current *track name*.\n\n**`playTrack` stops the track it replaces** (20.9.0 and later; before that it\nonly assigned the slot, and the previous bed kept looping underneath where\n`stopTrack()` could no longer reach it). So a scene just states its own music\nand nothing cancels anything:\n\n```js\nclass PlayScreen extends Stage {\n    onResetEvent() { audio.playTrack(\"battle\", 0.6); }\n}\n```\n\nCalling it with the track already playing **restarts** it, the same as `play()`\non any other clip. A scene that wants the bed to carry on across a screen\nchange should not call it again, rather than rely on it doing nothing.\n\n`stopTrack()` is for where nothing follows: a scene whose successor has no\nmusic of its own, or a game going quiet. `state.change` destroys the outgoing\nstage **before** it resets the incoming one, so a `stopTrack()` in\n`onDestroyEvent` and a `playTrack()` in the next `onResetEvent` are in that\norder and never cancel each other.\n\nTo end a track on an event rather than on a screen change, say a run ending\nwhile the player stays on the same screen, `audio.fade(name, from, 0, ms)`\ntakes it down gently. A fade to zero is silence, not a stop, so the voice is\nstill live until something stops it or replaces it.\n\nEvery per-clip function throws `audio clip <name> does not exist` if the clip was\nnever loaded, and `play` additionally throws when a named sprite region is\nmissing.\n\n## Audio sprites\n\nOne file carrying many effects, with named regions in milliseconds:\n\n```js\nawait loader.preload([{\n    name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { jump: [0, 450], hit: [2000, 250], music: [4000, 12000, true] },\n}]);\n\naudio.play(\"sfx\", { sprite: \"jump\" });\n```\n\n### A loop with no gap\n\nAn MP3 encoder pads the end of its file. Chrome and Firefox trim the padding when\nthey decode it, Safari keeps it, so a looping MP3 has a short silence at every\nloop there (27 ms on a 12 s clip at 22 kHz, measured). Declare a region of the\nloop's exact length and loop on that: the voice never reaches the padding.\n\n```js\nawait loader.preload([{\n    name: \"ambience\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { loop: [0, 12000] },\n}]);\naudio.playTrack(\"ambience\", { sprite: \"loop\", volume: 0.6 });\n// or, as a plain clip: audio.play(\"ambience\", { sprite: \"loop\", loop: true })\n```\n\nOgg Vorbis carries no such padding.\n\nThe optional third element marks a region as looping: it loops unless `play()` is\ntold `loop: false` (before 20.11 it only looped when `loop: true` was passed as\nwell). `play()`'s second argument\ntakes either the original `loop` boolean or a `PlayOptions` object\n(`sprite`, `loop`, `onend`, `volume`, plus the spatial `follow`, `at` and\n`stopWithTarget`) — both forms work. Object fields win over the positional\n`onend` / `volume` arguments when both are supplied.\n\n## Clip state\n\n```js\naudio.duration(\"theme\");        // seconds — 0 while still loading\naudio.duration(\"sfx\", id);      // duration of the region THAT instance plays\naudio.playing(\"theme\");         // is anything playing?\naudio.playing(\"theme\", id);     // is that instance playing?\naudio.state(\"theme\");           // \"unloaded\" | \"loading\" | \"loaded\"\n```\n\n`stop()`, `pause()` and `resume()` take an optional instance `id` too; omit it\nand the whole group is affected. `audio.stop()` with no arguments at all stops\nevery sound. `audio.seek(name)` reads the position, `audio.seek(name, s, id?)`\nwrites it; `audio.rate(name, r, id?)` changes speed and pitch together\n(`0.5`..`4.0`, `1` is normal) and `audio.fade(name, from, to, ms, id?)` ramps\nthe volume (`0`..`1`, over milliseconds).\n\n## Volume and muting\n\n```js\naudio.setVolume(0.5);           // global, 0..1\naudio.getVolume();              // read it back\naudio.mute(\"cling\");            // one clip  — mute(name, id?, shouldMute = true)\naudio.unmute(\"cling\");\naudio.muteAll();                // everything (audio.disable() is an alias)\naudio.unmuteAll();              // (audio.enable() is an alias)\naudio.muted();                  // global mute state\n```\n\n`audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`\nalso clears the current-track pointer if it named that clip.\n\n### Losing focus mutes everything\n\nWhen the window loses focus the engine mutes all audio, and unmutes it on\nfocus. That covers sound effects, procedural `tone` / `noise` and the mixer\neffects, not just the music track that `state.pause()` pauses. Set\n`pauseOnBlur: false` in the application settings if a clip has to keep playing\nin the background.\n\n## Spatial audio\n\n```js\naudio.position(\"engine\", x, y, z);\naudio.orientation(\"engine\", x, y, z);\naudio.stereo(\"engine\", -1);     // -1 left … 1 right\naudio.panner(\"engine\", { coneInnerAngle: 90, /* … */ });\n```\n\n`position(name, x, y = 0, z = -0.5, id?)` and\n`orientation(name, x, y, z, id?)` both default `id` to \"the whole group\".\nCalled with just the clip name, each of the four returns the current value —\n`stereo` gives `0` and `position` gives `[0, 0, 0]` before they have ever been\nset.\n\n### Let the engine place the sound: `follow` and `at`\n\nThe shortest path to positional audio is a play option. Both take **world\npixels**, the same numbers as `pos.x` / `pos.y`, and the engine converts to\naudio space for you:\n\n```js\n// tracks the renderable every frame, and stops when it is destroyed\naudio.play(\"ufo\", { follow: this, stopWithTarget: true });\n\n// pinned to a fixed world point\naudio.play(\"portal\", { at: { x: 1200, y: 480 } });\n```\n\n`follow` and `at` are mutually exclusive: passing both throws, rather than\nquietly picking one. `follow` re-reads the target's **absolute** position each\nframe, so a renderable nested in a moving container is placed correctly. `stopWithTarget` is what you\nwant for a sound that belongs to an entity: without it a looping clip on a\ndestroyed renderable keeps playing from wherever it last was. `audio.unfollow(id)`\ndetaches a sound and leaves it playing where it is.\n\n### Move the listener, don't do the subtraction yourself\n\n```js\naudio.setListener(player);                  // tracked every frame\naudio.listener(x, y, z);                    // or place it by hand\naudio.listener();                           // → [x, y, z] in world pixels\naudio.listenerOrientation(fx, fy, fz, ux, uy, uz);\naudio.setListener(null);                    // stop tracking\n```\n\nWith a listener in the world, source positions are **absolute world\ncoordinates**, not offsets. The older idiom — passing `source.pos.x -\nplayer.pos.x` every frame with the ear stuck at the origin — still works, but\nonly if you leave the listener at `(0, 0, 0)`; mixing the two double-counts the\nplayer's position.\n\n`setListener` places the ear at the target's absolute position after the world\nupdates. A plain renderable contributes position only, so a side-scroller keeps\nthe default \"facing into the screen\" and gets left-right panning plus distance\nfalloff from the one call.\n\nA `Camera3d` target also contributes its **orientation**, taken from its own\nbasis, so turning the camera swings the stereo image with it. That is the whole\nof what a posed 3D scene needs:\n\n```js\naudio.setListener(app.viewport);                 // in a 3D scene this is a Camera3d\naudio.play(\"drone\", { follow: saucer });         // scene coordinates, directly\n```\n\n**Y is down.** Every one of these calls takes and returns melonJS world\ncoordinates, where y grows downward; the Y-up flip Web Audio wants happens\ninside the engine. Pass `pos.y` as it is.\n\n### The defaults are tuned for pixels\n\n`audio.setSpatialDefaults()` / `getSpatialDefaults()` set the panner attributes\na sound placed through `follow` or `at` starts from, and the shipped values\nassume a 2D game measured in pixels:\n\n| attribute | default |\n|---|---|\n| `refDistance` | `240` |\n| `maxDistance` | `10000` |\n| `panningModel` | `\"equalpower\"` |\n| `distanceModel` | `\"inverse\"` |\n\nWith those, a placed sound is at full volume out to 240 px and then halves\neach time the distance doubles: about half at 480, a quarter at 960, roughly\nan eighth at 2000. `maxDistance` is where it stops getting quieter.\n\n**They apply to placed sounds only.** A plain `audio.play()` followed by a\nbare `audio.position()` keeps WebAudio's own defaults, where `refDistance` is\n`1` METRE and a source 100 px out is already near silent. That path is\nunchanged, which is what keeps old code working.\n\nWiden or tighten the field once, globally:\n\n```js\naudio.setSpatialDefaults({ refDistance: 400, maxDistance: 20000 });\n```\n\n`audio.panner(name, attrs, id?)` still overrides per clip or per voice, and it\nmerges over the current values rather than replacing them, so writing one field\nno longer resets the rest. Called without an `id` it writes the **group**\ndefaults, which is what a clip-wide tuning call looks like.\n\nFor a 3D scene where front-to-back matters, `panningModel: \"HRTF\"` is worth the\ncost; for a 2D one it mostly muddies the stereo image.\n\n### `stereo` and `position`: last writer wins\n\nBoth drive the same per-instance panner node, and the engine swaps the node to\nmatch whichever you called **last**: `stereo()` after `position()` gives you\nplain left-right panning, and `position()` after `stereo()` gives you 3D\nfalloff. Neither silently does nothing any more, and the order you call them in\nno longer matters.\n\nStill pick one per voice and stay with it. A 2D game usually wants `stereo()`\nalone; reach for `position()` (or `follow` / `at`) when distance falloff\nmatters.\n\n**Spatial audio is WebAudio-only.** On a clip loaded with `stream: true` (or\n`html5: true`) `stereo` / `position` / `orientation` return early and do\nnothing — which is exactly the case for the long music tracks people reach for\nfirst. `follow` and `at` are equally inert on a streamed clip.\n\n## Procedural sound\n\nFor beeps, blips and noise without shipping files. Both take **one options\nobject**, and `duration` (in seconds) is required:\n\n```js\naudio.tone({ freq: 440, duration: 0.2, gain: 0.3, pan: -0.5 });\naudio.tone({ freq: [440, 660, 880], duration: 0.4 });   // layered partials\naudio.noise({ duration: 0.5, filter: { type: \"lowpass\", frequency: 800 } });\n```\n\n`tone` also takes `wave` (default `\"sine\"`), `attack` (`0.005`) and `pitchSlide`\n(`1`). `noise` takes `type` (`\"white\"` | `\"pink\"` | `\"brown\"`, default\n`\"white\"`), `attack`, `pan`, `filter` and `filterSweep`. `gain` defaults to `0.1`\non both.\n\n### Sequencing: `delay`, not `setTimeout`\n\nBoth take `delay` — seconds before the sound starts, scheduled on the **audio\nclock**. That is what builds a multi-part sound: a stinger's second note, an\nexplosion's double-tap.\n\n```js\naudio.tone({ freq: 392, duration: 0.12 });\naudio.tone({ freq: 587, duration: 0.18, delay: 0.11 });\naudio.tone({ freq: [784, 1176], duration: 0.75, delay: 0.23 });\n```\n\nDo not reach for `setTimeout` here. A timer fires on the main thread, so a busy\nframe slips the note late and the sequence audibly loses time — the fault is\neasy to blame on the sound design rather than the scheduling. `delay` is\nsample-accurate because the whole envelope is scheduled up front.\n\nBoth are **silent no-ops** when there is no WebAudio context — they bail on\n`getAudioContext() === null` rather than throwing. Neither needs the clip to be\nloaded; neither goes through the file-playback path at all.\n\n`audio.getAudioContext()` and `audio.getMasterGain()` are the escape hatches for\ncustom Web Audio work; both return `null` when audio is unavailable, so guard.\nConnect a custom graph to the master gain rather than `ctx.destination` if you\nwant it to respect `setVolume` / `muteAll`.\n\n## Effects on all game audio: reverb, echo, compressor\n\nThere is no built-in reverb, delay, distortion or compressor, and no per-clip\nfilter on file playback. There does not need to be one for the common case:\nevery buffered clip and every `tone` / `noise` passes through the master gain\non its way to the speakers, so an effect inserted **after** it applies to the\nwhole mix.\n\n```js\nconst ctx = audio.getAudioContext();   // creates the context if needed\nconst master = audio.getMasterGain();\nif (ctx && master) {                   // null when audio is unavailable\n    const comp = ctx.createDynamicsCompressor();\n    master.disconnect();               // master -> speakers becomes\n    master.connect(comp);              // master -> compressor -> speakers\n    comp.connect(ctx.destination);\n}\n```\n\nAny Web Audio node or chain works the same way: a `ConvolverNode` with an\nimpulse response for reverb, a `DelayNode` with a feedback gain for echo, a\n`BiquadFilterNode` for an underwater low-pass, a `WaveShaperNode` for\ndistortion. Do it once, at startup; the master gain lives as long as the\ncontext.\n\n- **`setVolume` / `muteAll` still work**, because they act on the master gain,\n  which is now upstream of the effect.\n- **Streamed clips are not affected.** A `stream: true` (`html5: true`) clip\n  plays through an `<audio>` element and never reaches the master gain — which\n  is usually the music. Preload the music buffered if it must go through the\n  effect.\n- **It is all or nothing.** The per-instance nodes are not public, so an effect\n  on one clip only (muffling one sound behind a wall) is not possible through\n  this route; `noise` has its own `filter` for generated sounds.\n- To remove it, reverse the wiring: `master.disconnect(); master.connect(ctx.destination);`.\n\n## Autoplay: sound is silent until the first gesture\n\nBrowsers create the audio context in the `suspended` state and keep it there\nuntil a user interaction. Nothing errors — audio simply does not play, which\nreads as a bug. melonJS installs one-shot `touchstart` / `touchend` / `click` /\n`keydown` unlock listeners on `document`, so the first real gesture anywhere\nunlocks it and fires the clip's `on.unlock` callback. Design for it: start music\non the first click or key press, not on load. The usual way to guarantee one is\na gate screen — a splash, a \"press any key\", or a health/photosensitivity notice\n— whose dismissal *is* the gesture, so the menu behind it can start its music\nunconditionally. Note the unlock listens for `keydown` too, so a keyboard-only\nplayer is covered.\n\n### The context can stop again, long after it was unlocked\n\nUnlocking is only the first half. A context that has been running can be\nstopped later: melonJS suspends it itself after thirty idle seconds to save\npower, and browsers stop it for their own reasons, a backgrounded tab among\nthem. Recovery is automatic and you do not have to write it: a user gesture\nor the tab coming back to the front revives it, and a deliberate idle\nsuspension is left alone so the power saving still works.\n\n**The symptom worth memorising**, because it points straight at the cause:\nif `tone` and `noise` are audible while loaded clips are silent, the context\nis healthy and the engine's playback state is wrong. The generators only need\na live context, where a clip goes through the engine's state machine and can\nbe parked waiting for a resume. It is never \"the context died\".\n\nBoth routes ask the same owner to resume, so hearing a tone tells you the\nhardware is back; it tells you nothing about whether the clips waiting on the\nengine's state have been let go.\n\nThat asymmetry was a real Safari-only bug: thirty idle seconds before the\nfirst click left the engine waiting on a suspend the browser never confirmed,\nso every clip queued for the life of the page while generated sound played\nnormally. Fixed in 20.9, and the lesson generalises: a clip that will not play\nis a state question, not a hardware one.\n\nPlayback also starts **asynchronously**: `play()` returns an id while the\ninstance is still starting, so `playing()` can legitimately be `false` on the\nvery next line.\n\n## Load-time options\n\n```js\n{ name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n  sprite: { jump: [0, 450] },\n  pool: 5,            // finished instances kept for reuse — NOT a concurrency cap\n  rate: 1.0,          // initial playback rate, 0.5..4.0\n  mute: false,\n  loop: false,\n  autoplay: false,\n  preload: true,      // or \"metadata\"\n  format: \"mp3\",      // explicit hint when the URL has no usable extension\n  stream: true,       // long music: stream rather than decode into memory\n  on: { play() {}, end() {}, stop() {}, unlock() {} },\n}\n```\n\n`pool` (default `5`) is a **reuse cap on finished voices**, not a limit on\nsimultaneous playback: when the number of ended voices exceeds it, the surplus\nare disconnected and dropped. Raise it for a clip triggered in rapid bursts, not\nto allow more overlap.\n\nUse `stream: true` for long music tracks — it plays through an HTML5 `<audio>`\nelement instead of decoding the whole file into memory. (`html5: true` is the\nsame switch under its backend name.) `on` accepts the full lifecycle set:\n`play`, `pause`, `stop`, `end`, `fade`, `seek`, `rate`, `volume`, `mute`,\n`unlock`.\n\nOutside a preload, `audio.load(asset, onload?, onerror?)` takes the same\ndescriptor and loads one clip at runtime — for a level's sounds fetched when\nthe level starts. The loader calls the same function, so the same rules apply.\n\nRe-preloading a manifest that already contains a loaded clip is a no-op —\n`audio.unload(name)` first if you genuinely want to reload it.\n\n**Credentials use the same global switch as every other asset type** —\n`loader.setOptions({ withCredentials: true })`, not an audio-specific option.\nBuffered audio fetches through the shared loader path, so it honours it exactly\nas an image or a JSON file does. A `stream: true` clip does not: it plays\nthrough an `<audio>` element, which needs a `crossorigin` attribute rather than\nfetch credentials. Preload a clip buffered if it sits behind a login.\n\n**Failing clips and `setStopOnAudioError(false)`.** The default is to fail the\npreload when a clip cannot be loaded after its retries: the error reaches the\nloader, so `await loader.preload(...)` rejects and your `catch` runs, and the\nreason is logged. Turn the flag off and the engine mutes audio, warns\n(`failed loading <name>, disabling audio`) and lets the load *continue* — the\npreload finishes and the game reaches the next scene without sound.\n\nBefore 20.4 both halves were broken: with the flag off it disabled audio and\nfailed the preload anyway, and a clip whose bytes arrived but would not decode\n(a missing file served as HTML by an SPA rewrite, say) reported nothing at all\neither way — blank screen, empty console.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| audio assets 404 | `src` given as a full filename instead of a directory prefix |\n| `target audio extension(s) should be set...` thrown at preload | `audio.init()` not called before preloading |\n| `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |\n| nothing plays until the user clicks | browser autoplay policy — expected, design around it |\n| `playing()` is false right after `play()` | playback starts asynchronously |\n| music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |\n| two music tracks playing at once, and `stopTrack()` only silences one | melonJS before 20.9.0: `playTrack` did not stop the previous track, so the scene that started it had to `stopTrack()` on the way out |\n| the music restarts when returning to a screen | `playTrack` with the track already playing restarts it; don't call it again if the bed should carry on |\n| `stereo` / `position` do nothing | the clip was loaded with `stream: true` — spatial is WebAudio-only |\n| one format works, another does not | that codec is not in the `audio.init()` list |\n| audio behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses |\n| a streamed clip ignores `withCredentials` | `stream: true` plays through an `<audio>` element; preload it buffered instead |\n| `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |\n| `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |\n| a positioned sound is silent, or only audible right next to the player | placed by hand with `audio.position()`, which keeps WebAudio's metre defaults (`refDistance: 1`) — use `follow` / `at`, or set `refDistance` in pixels with `audio.panner()` |\n| a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |\n| a followed sound pans to the wrong side as the player moves | the listener was moved **and** positions are still relative to the player — with a listener in the world, pass absolute world coordinates |\n| a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |\n| a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |\n| a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |\n| a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see \"A loop with no gap\") |\n| sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |\n\n## Related skills\n\n- `melonjs-getting-started` — preloading and asset ordering\n","packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md":"---\nname: melonjs-effects-and-shaders\ndescription: \"Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: Tween, easing, animate an effect, ShaderEffect, addPostEffect, removePostEffect, getPostEffect, postEffectNeedsCapture, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter.\"\nlicense: MIT\n---\n\n# Post effects and custom shaders\n\n> melonJS ships a catalogue of post effects and a dual-language shader system.\n> The recurring trap is that every shader path needs a GPU backend and\n> **degrades to nothing** on Canvas rather than erroring. Blend modes are the\n> exception — those work on all three renderers.\n\n## Applying an effect\n\nEffects attach to a renderable **or** to a camera (for full-screen grading):\n\n```js\nimport { VignetteEffect, GlowEffect } from \"melonjs\";\n\nsprite.addPostEffect(new GlowEffect(app.renderer));\napp.viewport.addPostEffect(new VignetteEffect(app.renderer));\n```\n\n`renderable.shader = …` is **deprecated since 19.2.0**, and its setter destroys\nwhatever it replaces. Use `addPostEffect` / `getPostEffect` / `removePostEffect`.\n\n### A camera effect covers the HUD too\n\nA camera's post-effect brackets the **entire** world draw — floating children\nincluded. So the obvious way to write a full-screen pass also washes over every\nHUD label in that world, which is rarely what you want.\n\nTo land a pass *after* the world but *before* the HUD, host the effect on a\nscreen-filling floating renderable ordered below the HUD's z instead:\n\n```js\nconst quad = new Sprite(0, 0, { image: anyImage });\nquad.anchorPoint.set(0, 0);\nquad.scale(viewW / anyImage.width, viewH / anyImage.height);\nquad.floating = true;                 // its own uv is now screen space\nquad.blendMode = \"additive\";\nquad.addPostEffect(myEffect);\nworld.addChild(quad, HUD_Z - 10);     // 2D: below the labels. See the note.\n```\n\nTwo things follow from the quad filling the frame: its own `uv` is screen space\n(so a pass like this needs none of the `screen_uv` / `screen_texture` builtins),\nand the incoming `color` is the quad's own texture, which a body that paints\nfrom scratch can ignore entirely.\n\n**Depth here is easy to get backwards, and the rule FLIPS between the two sort\nmodes.** Under the default `sortOn = \"z\"` a higher z draws later, i.e. on top,\nso `HUD_Z - 10` puts the pass under the labels as written above. Under\n`sortOn = \"depth\"`, which every `Camera3d` scene sets, a floating child's sort\nkey is `pos.z * pos.z` and the walk paints far to near, so the SMALLER\nmagnitude is nearer and draws on top — `HUD_Z - 10` there puts the pass OVER\nthe labels, the opposite of the comment.\n\nSo in a 3D scene give the overlay a larger magnitude than the HUD, and give\nthe HUD a small one (`melonjs-3d` and `melonjs-ui-and-text` both say a HUD\nwants a small depth, not the huge z that would put it on top in 2D).\n\nEither way, verify it rather than reasoning about it: return a flat colour from\nthe body for one frame and see what it tints.\n\n## A renderable that draws with PRIMITIVES has to ask to be captured\n\nOne effect is applied the cheap way: the renderable is drawn with the effect's\nown program instead of being captured offscreen and post-processed, which costs\nno render target. That is equivalent only when everything the renderable draws\nis a textured quad. `fillRect`, `strokeRect` and `renderer.fill(shape)` go\nthrough a batcher that never reads that shader, so **one** effect on such a\nrenderable would silently do nothing, while **two** would work, because a chain\nalways captures.\n\nSo a renderable of your own that draws with primitives says so:\n\n```js\nclass Bar extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.postEffectNeedsCapture = true;   // ← or one effect is a no-op\n    }\n    draw(renderer) {\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\n`ProgressBar` and `Trail` set it for you, and a sprite needs nothing: the flag\ndefaults to `false` and only ever changes the single-effect case.\n\n## Toggle with `enabled`, do not remove\n\n**`removePostEffect()` destroys the effect** — it calls `effect.destroy()` and\nfrees GPU resources, so the instance cannot be re-added. Same for\n`clearPostEffects()` and for reassigning the deprecated `.shader`.\n\n```js\n// ✗ destroys it; re-adding later fails or forces a recompile\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nAn effect with `effect.shared === true` opts out of auto-destroy because it is\nreused across renderables.\n\n## Built-in effects\n\nTwenty-three presets, every one constructed as `new XEffect(renderer, options)`\nand every one shipping both a GLSL and a WGSL body, so they all run on either\nGPU backend: `VignetteEffect`, `ScanlineEffect` (optional CRT curvature),\n`BloomEffect`, `GlowEffect`, `BlurEffect`, `PixelateEffect`,\n`ChromaticAberrationEffect`, `DropShadowEffect`, `FlashEffect`,\n`OutlineEffect`, `ColorMatrixEffect`, `DesaturateEffect`, `DissolveEffect`,\n`TintPulseEffect`, `WaveEffect`, `SwayEffect`, `InvertEffect`, `SepiaEffect`,\n`HologramEffect`, `ShineEffect`, `RadialGradientEffect`, `SquishEffect`,\n`ToneMappingEffect`.\n\n**Colors take an alpha.** Every color option and `setColor` takes `[r, g, b]`\nor `[r, g, b, a]`, each 0.0 to 1.0, and the alpha is the color's own opacity: a\nhalf-transparent outline, a glow at half strength, a flash that only half\ncovers. `[r, g, b]` is opaque, as it always was. Any other length throws.\n`RadialGradientEffect` takes a `Color` instead, and its alpha scales the spot.\n\n```js\nsprite.addPostEffect(new OutlineEffect(app.renderer, { color: [1, 1, 1, 0.5] }));\n```\n\n**On a sprite cut from an atlas or a tileset, the effects that read around a\npixel keep to the sprite's own frame.** `GlowEffect`, `OutlineEffect`,\n`BlurEffect`, `DropShadowEffect`, `ChromaticAberrationEffect`, `WaveEffect`,\n`SwayEffect`, `PixelateEffect` and `BloomEffect` read the neighbouring frames\nas transparent, as if the sprite were drawn on its own, so they never show\nalong its edges and the atlas needs no padding for them. Past the texture's own\nedge (a standalone image, or the outside of an atlas) nothing changes. Their `textureSize` is still the size of the whole atlas image: it is\nwhat turns a width in texels into a distance in the texture.\n\n`SwayEffect` leans a sprite or a layer from side to side, as if in a breeze: the\ntop sways most and everything from its `anchor` row down stays put, so a tree's\ntrunk or a reed's root stays planted. It suits anything planted at its bottom;\nsomething hanging from its top, like a banner, needs its own shader. It is a\n`WaveEffect`, which takes the same `anchor`, so it adds no shader. Drive it with\n`setTime(seconds)` every frame, like any time-based effect; it wraps the phase\nitself, so a raw clock never costs precision on a phone's medium-precision\nfloats.\n\n```js\nconst sway = new SwayEffect(app.renderer, { strength: 0.02, anchor: 0.9 });\ntree.addPostEffect(sway);\n// each frame\nsway.setTime(timer.getTime() / 1000);\n```\n\nRows are measured across the texture the effect samples, so on a sprite cut from\nan atlas the anchor is a row of the atlas: it suits a standalone image or a\nlayer best.\n\n`RadialGradientEffect` is a procedural soft spot: solid at the centre of its\nhost quad, fading linearly to transparent at the edge, and elliptical for free\non a non-square quad. It is the one to reach for instead of baking a disc into\na canvas, for a pickup highlight, a trigger marker or a damage flash. Its\nfalloff assumes the quad samples a FULL texture rect, so pair it with a\nstandalone image or `Renderer.getWhitePixel()` rather than a sprite that draws\na sub-region of an atlas, where the centre would land off-quad.\n\n`SquishEffect` bulges a sprite's middle out (`setBulge(0.3)`) or pinches it in\n(`setBulge(-0.3)`), its top and bottom rows staying put. Bulging out widens the\nsprite past its own frame, and the effect draws on a wider quad to make room,\nso the art needs no transparent margin. On its own it is a jelly wobble; paired\nwith a change of height it is **squash and stretch that keeps the volume**:\nscale the sprite to `scaleY` of its height about its feet and set the bulge to\n`1 - scaleY`, so a stretched sprite pinches in and a squashed one bulges out.\nEase `scaleY` back to 1 with a short `Tween.Easing.Back.Out`; an elastic ease\nkeeps it trembling long after the landing.\n\n```js\nconst squish = new SquishEffect(app.renderer);\nplayer.addPostEffect(squish);\n// every frame, from the player's update()\nconst feet = player.height * (1 - player.anchorPoint.y);\nplayer.currentTransform\n    .identity()\n    .translate(0, feet)\n    .scale(1, scaleY)\n    .translate(0, -feet);\nsquish.setBulge(1 - scaleY);\n```\n\nApply that from the sprite's own `update()`, not from a tween's `onUpdate`. A\nphysics adapter that mirrors a body's angle (Matter does) rewrites\n`currentTransform` on every step, before the renderables update, so a scale set\nanywhere else is gone by the time the sprite is drawn. Let the tween animate a\nplain `{ scaleY }` object and read it in `update()`.\n\nIt is made for a sprite carrying it as its only effect: chained with another,\nor on the camera, the frame it measures against is the whole capture, so it\nsquishes the whole frame. The bulge is outside the sprite's bounds, so culling\nand pointer events ignore it.\n\n**`BloomEffect` and `GlowEffect` are not the same thing**, and reaching for the\nwrong one is the usual disappointment here. `GlowEffect` draws a halo OUTSIDE a\nsprite's silhouette: its body returns the colour untouched wherever alpha is\nalready non-zero, so on an opaque frame it returns early on every fragment and\na camera-wide glow does nothing at all. `BloomEffect` is the screen bloom —\nbright pixels bleed into their neighbours — and it is the one to put on the\ncamera:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\n// only the brightest highlights, with a wider halo\napp.viewport.addPostEffect(\n    new BloomEffect(app.renderer, { threshold: 0.85, radius: 4 }),\n);\n```\n\nIts tap offsets are in texels, so it takes its size from the renderer at\nconstruction; call `setTextureSize(w, h)` from an `event.ONRESIZE` handler, or\nwhen applying it to something smaller than the frame.\n\n**`ToneMappingEffect` goes after it, and goes last.** Bloom spreads the bright\nparts; the tone map is what stops the sum of the two clipping flat at white,\nwhich is the difference between an effect that reads as light and one that\nreads as a white sticker. `exposure` lifts the image into the curve, and the\ncurve (`aces`, `reinhard` or `exponential`) is baked at construction:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\napp.viewport.addPostEffect(\n    new ToneMappingEffect(app.renderer, { exposure: 1.6 }),\n);\n```\n\nIt is a grade, not a colour pipeline. By default melonJS renders into `RGBA8`\neverywhere, so the frame reaching it is already clamped and `exposure` past 1\nis what gives the curve range to compress.\n\n**`hdr: true` in the application settings changes that**: render targets\nbecome half-float, so the chain carries values above 1 instead of clamping at\nevery write, and the bloom threshold starts discriminating genuinely\nover-bright pixels rather than merely bright ones. Off by default: it costs\nGPU memory on the camera's two render targets and depends on the driver.\n\nOn its own it is **not HDR output**: the frame is still presented in SDR, so\nanything above 1 clamps on the final blit. What it buys is that nothing clamped\nBEFORE then. Removing that last clamp is a second, separate opt-in —\n`hdrOutput`, below — and it is WebGPU-only.\n\n```js\nconst app = new Application(1024, 576, { hdr: true, … });\n// …and check what you actually got, since a driver can refuse it\nif (app.renderer.supportsHDR) { /* the chain now carries values above 1 */ }\n```\n\nBranch the GRADE on that flag, not just the feature. An exposure under 1 and a\nbloom threshold at 1 make sense only where values above 1 exist; applied to a\nchain that clamps, the same numbers tone map an already display-referred frame\nand switch the bloom off, which reads as a dark, flat picture rather than as a\nmissing feature. Where there is no headroom, squeeze each emitter into range in\nits own shader instead and put the bloom threshold back under 1.\n\n**Nothing is added to your chain.** `hdr` is additive: the same frame, plus\nadditive content that no longer saturates per write and a threshold that means\nsomething. Anything still above 1 at the end clamps on the blit, as it always\ndid.\n\nA tone map is therefore a decision, not a consequence, and it is a bigger one\nthan it looks. Every curve maps 1 to less than 1, so a 2D look built ON the\nclamp — clipped white specular, saturated neon — goes matte the moment you add\none, at any exposure. Add one when your art is authored in linear light and you\nwant the highlights rolled off; skip it when the clipping IS the look.\n\nIt applies to the CAMERA chain only. A sprite's chain keeps 8-bit targets on\npurpose: its final blit composites with `ONE_MINUS_SRC_ALPHA`, and additive\nblending accumulates alpha past 1 in a float target, so that factor would go\nnegative and subtract the backdrop.\n\n### Once it is on: putting values above 1 into the frame\n\n`hdr: true` on its own changes nothing visible. It removes a ceiling; something\nstill has to reach for it. No single draw exceeds 1 on its own, so a game that\nturns the setting on and changes nothing else renders very nearly the frame it\ndid before and looks like the feature is broken. The exception is worth knowing\nbecause it is free: additive content that already overlapped was being clipped\nat every write, and stops being.\n\nWhat produces over-bright pixels:\n\n```js\n// a built-in colour matrix, scaled past 1 — no custom shader needed\nsprite.addPostEffect(new ColorMatrixEffect(app.renderer).brightness(4));\n\n// in 3D, both of these were always unbounded; the 8-bit target was the only\n// thing destroying the range. `emissive` is a 3-component Float32Array, set\n// from the mesh settings or replaced wholesale — it is not a Color\nconst mesh = new Mesh(x, y, { emissive: [2.5, 1.8, 0.4], /* … */ });\nlight.intensity = 3;\n\n// and additive blending, which accumulates past 1 once the target can hold it\nsprite.blendMode = \"additive\";\n```\n\n**One effect, not two.** A renderable carrying exactly ONE post effect takes\nthe renderer's `customShader` fast path: no offscreen target, and it draws\nstraight into the camera's half-float target with its range intact. A chain of\ntwo or more is routed through a pooled intermediate that stays 8 bits per\nchannel on purpose, so the energy is clamped there, before the camera ever sees\nit. Adding a second effect to an emitter is enough to silently undo its\nheadroom.\n\n### Everything inside the camera is graded, including your UI\n\nThe tone curve runs on the camera's whole frame. A `floating` HUD is still\ninside it, so a label authored at display white arrives at the curve as 1 and\nleaves at `1 - exp(-exposure)` — at exposure 0.45 that is 0.36, and the HUD\ncomes out at roughly a third of its brightness for no reason the author can\nsee.\n\nUI in an HDR chain has to be authored for that chain, exactly as the scene is:\n\n```js\n// drive the readout so the curve lands it back at display white. `shared`\n// keeps one instance across every element: it opts the effect out of the\n// per-renderable auto-destroy, so the first element torn down does not take\n// the program the others are still using\nconst energy = new ColorMatrixEffect(app.renderer).brightness(2);\nenergy.shared = true;\nfor (const element of hudElements) element.addPostEffect(energy);\n```\n\nPut it on each ELEMENT, not on their container: `beginPostEffect` clears\n`customShader` for any renderable with no effects of its own, so a container's\nshader is thrown away by its first child. The alternative is to keep the UI out\nof the camera pass entirely, by overriding `Stage.draw` and drawing it after\n`super.draw(...)` — at the cost of the bloom that makes additive UI glow.\n\nThe quad trick under *A camera effect covers the HUD too* does not help here.\nIt works by hosting a pass BELOW the HUD's z, and a tone map cannot live there:\nit has to be the last thing before the blit to the 8-bit canvas, which is the\ncamera. Anything the camera draws goes through it.\n\nPick the multiplier by measuring against the no-headroom build, not by\ninverting the curve. Inverting it over-estimates, because additive UI is inside\nthe BLOOM too and driving it harder also makes it spill wider into the same\npixels.\n\n### `hdrOutput`: actually presenting in HDR, and what it costs you\n\n`hdr` gives the chain headroom and then throws it away at the very last step,\nbecause the surface the browser presents into is 8 bits per channel. Values\nabove 1 clamp there, exactly as they did before the setting existed.\n`hdrOutput` changes that surface, so they reach the compositor instead:\n\n```js\nconst app = new Application(1024, 576, { hdr: true, hdrOutput: true, … });\nif (app.renderer.supportsHDROutput) { /* the display is getting values above 1 */ }\n\napp.renderer.setHDROutput(false);   // and at runtime, for a settings screen\n```\n\n**It is WebGPU-only, and that is not a melonJS limitation.** The WebGPU\nspecification defines a tone mapping mode on the canvas configuration, and two\nengines ship it. The WebGL2 equivalent needs a drawing-buffer tone mapping mode\nthat is still an unapproved specification change with no implementation in any\nbrowser; the colour-space half of it exists in Chrome behind\n`--enable-experimental-web-platform-features`, which is not something you can\nask a player to enable. So the WebGL2 backend reports `false`, warns once if\nyou asked, and presents in SDR as it always has.\n\nThat makes this the first setting where **the same machine answers differently\ndepending on which backend `video.AUTO` picked**. Branch on\n`renderer.supportsHDROutput`, never on the setting.\n\n**It requires `hdr`** — the last pass blits from the camera's target, so if\nthat target is 8 bits there is nothing above 1 left to present and the extra\ncost would buy an identical picture. Turning `hdr` off turns this off with it.\n\n**It changes how your game looks, on an HDR display.** Everything else here is\ncareful not to: `hdr` is additive, the tone curve defaults to none. This one\nremoves the final clamp, so anything you drove above 1 — an additive stack, an\nemitter, a `brightness()` above 1 — gets genuinely brighter rather than\nsaturating at white. That is the point of it, and it is why it is a separate\nopt-in rather than part of `hdr`. Two consequences worth planning for:\n\n- a look built on clipped whites will not survive it unchanged, because the\n  clipping is what you were art-directing against\n- a tone curve and `hdrOutput` pull in opposite directions. A curve exists to\n  fit values into `[0, 1]`; if you have removed the clamp, either drop the\n  curve or give it a white point high enough to leave the top of the range\n  alone\n- **`\"aces\"` cannot produce HDR output.** Its expression clamps to `[0, 1]`\n  internally, so everything past the shoulder collapses to one value before\n  the white point scales it — different bright things come out equally bright.\n  Under `hdrOutput` use `\"reinhard\"` or `\"exponential\"`, which approach 1\n  asymptotically without clamping and so keep their ordering when the white\n  point lifts them past it, or use no curve at all\n\n**What it is not.** It does not make an SDR display brighter, and there is no\nway to ask melonJS how bright the panel is — `matchMedia(\"(dynamic-range:\nhigh)\")` is the closest the platform gets. Calibrating in nits, the way a\nconsole settings screen does, is not reachable from the web platform today.\n\n### Exposure, and the runtime toggle\n\nUnder `hdr` the useful exposure is often **below** 1, which surprises people who\nhave only used the effect as an LDR grade. The curve has to work where it\nseparates: at exposure 1 the ACES shoulder maps 2, 4 and 8 to 0.86, 0.96 and\n0.99, so genuinely different energies all arrive at white and the headroom buys\nnothing you can see. Lower exposure spreads them out. The right value is a\nproperty of how bright your scene is authored, so measure it; there is no\ndefault that is right for two different games.\n\nA tone curve is a setting rather than an effect you wire up:\n\n```js\nrenderer.setToneMapping(\"aces\", { exposure: 0.4, white: 6 });\nrenderer.setToneMapping(\"none\");                       // and off again\n```\n\n`\"none\"` by default. The renderer keeps the curve LAST on the camera, so\nwhatever you put in front of it is mapped along with everything else. Changing\nthe mode rebuilds one shader (the curve is baked); `exposure` and `white` are\nuniform writes, so a settings screen can drive them live. `white` is the value\nthat comes out as display white — the knob a \"brightness\" or \"peak\" slider\nwants, since exposure alone drags the midtones up with the highlights.\n\n`renderer.setHDR(enable)` moves the whole pipeline at runtime, the way\n`setAntiAlias` does, for a settings toggle. It empties the render-target pool\nwhen the format actually changes, so it is not something to drive per frame.\nThe boot header reports what happened either way:\n\n```\nresolution: requested 1024x576 (HDR), got 1024x576 (SDR)   ← the driver refused\n```\n\nFor grading without writing a shader yourself, the camera has a colour matrix:\n\n```js\napp.viewport.colorMatrix.contrast(1.1).saturate(1.1);\n```\n\nIt is still *implemented* as one: a non-identity matrix makes the camera append\nan internal `ColorMatrixEffect` to its post-effect chain for that frame, so it\nneeds a GPU backend like everything else here.\n\n## Custom effects are dual-language\n\nA custom `ShaderEffect` supplies a fragment **body**, not a whole program. To run\non both GPU backends it needs GLSL *and* WGSL — a GLSL-only effect silently does\nnothing when `video.AUTO` lands on WebGPU.\n\n```js\nconst effect = new ShaderEffect(app.renderer, { glsl, wgsl });\neffect.setUniform(\"uStrength\", 0.5);\neffect.setTexture(\"uScene\", tex);\neffect.setTime(timer.getTime() / 1000);   // setTime takes SECONDS; getTime is ms\n```\n\nUniform names are shared across the two bodies — in WGSL they are the members of\none `@group(3) @binding(0) var<uniform>` struct — so a single `setUniform` call\nfeeds whichever backend is live. `setTime` is a convenience for a `uTime`\nuniform and silently does nothing if the shader does not declare one.\n\n**Never put a backtick in a shader body, including in its comments.** An inline\nbody is a JavaScript template literal, so a backtick ENDS it — and what follows\nis parsed as code, which fails somewhere further down the file with an error\nthat says nothing about shaders:\n\n```js\nconst glsl = `\n// x * x, not `pow(x, 2.0)`: the base goes negative here   <-- ends the literal\nvec4 apply(vec4 color, vec2 uv) { ... }\n`;\n```\n\nMarkdown-quoting an identifier is a reflex when writing a comment, and this is\nthe one place it breaks the file. Write `pow(x, 2.0)` bare. A body loaded from\na `.glsl` / `.wgsl` file has no such problem, which is one more reason to move\na shader out once it stops being a sketch.\n\n### One effect is one program — prefer preloading, and share\n\nA `ShaderEffect` compiles and links in its **constructor**, and the link is\nchecked with `getProgramParameter(LINK_STATUS)` straight after `linkProgram` —\na blocking call. So the cost is paid on whatever frame the effect is\nconstructed, and *n* identical effects cost it *n* times. `clone()` does not\navoid it: it constructs a new effect and links a new program.\n\nPreload it instead. Loader type `\"shader\"` takes a `src` **or inline source via\n`data`**, compiles at load time (inside the loading screen), and hands back a\n**shared** instance:\n\n```js\nloader.preload([{ name: \"ramp\", type: \"shader\", data: myFragmentBody }]);\n// …then, for every label that wants it:\nlabel.addPostEffect(loader.getShader(\"ramp\"));\n```\n\n### Declare a level's effects with that level's assets\n\nAn effect the loader knows about can be prepared before anything draws it; an\neffect built inline cannot, because it does not exist until the code that\nconstructs it runs — which is the stage or level you were hoping to speed up.\nThat is the practical reason to prefer the asset form even when the source is a\nstring in your own module:\n\n```js\n// with the level's other assets, not in the stage that uses them.\n// `preload()` rather than `load()` — preload is what warms the shaders it\n// brought in, and it can be called again per level, not just at boot\nawait loader.preload([\n    { name: \"ripples\", type: \"shader\", src: {\n        glsl: \"assets/level2/ripples.glsl\",\n        wgsl: \"assets/level2/ripples.wgsl\",\n    }},\n    // …that level's textures, maps and audio alongside it\n], undefined, false);   // `false`: stay on this stage, no loading screen\n```\n\n`src` takes the dual-language pair, so one asset carries both realizations and\nthe two backends cannot drift apart. `data` takes inline source the same way.\n\nWith `prewarm` on (the default since 20.6), `loader.preload()` warms on its way\nthrough — automatically, on every call — and compiles the GPU-side objects for\nevery shader it holds — on WebGPU that is the\nWGSL module, which is otherwise built inside the draw path the first time the\neffect is used.\n\nSince 20.10 the rest is warmed when the effect is **attached**, inline effects\nincluded: `addPostEffect` (or assigning `shader`) compiles the pipelines that\neffect will draw with on that renderable — on WebGPU the costly part, keyed by\nthe renderable's blend mode and target as well as the shader — and on WebGL\nlinks the spliced program when the host is a mesh. It finishes off the frame,\nso attach when you build (a pool, a stage starting), not when you show: an\neffect attached and drawn in the same frame still compiles on that frame.\n\nOne caveat if you are moving an inline effect to a file: a template literal can\ninterpolate constants into the source (`vec2(${ASPECT}, 1.0)`), and a static\nfile cannot. Promote those to uniforms rather than duplicating the numbers\nacross the GLSL and WGSL copies — duplicated constants are exactly how the two\nbackends drift.\n\nSharing one instance across renderables is safe *because* the loader sets\n`effect.shared`. Without that flag a renderable's teardown destroys its post\neffects, which would free the program out from under everything else still\nusing it. Uniforms live on the effect, so a shared instance means one set of\nuniform values — fine when the look is uniform, and the reason to clone (and\npay a link) when it is not.\n\nNote this covers **your** effects only. The engine's own mesh/quad shader\nvariants are covered by the loading-screen warm-up and by the per-renderable one\n— see `melonjs-loading-assets`.\n\n### Screen-reading builtins\n\nFor refraction, distortion and anything that samples what is already drawn:\n\n- **`screen_texture`** — a sampler kept filled with everything drawn so far\n  (GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`)\n- **`screen_uv`** — this fragment's 0..1 position in that capture\n- **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame\n\nIn WGSL the capture is sampled through `screen_sampler` (clamped) or\n`screen_sampler_repeat` (wrapping) instead of GLSL's `: screen_texture`\nannotation. `screen_uv` is y-**up** in GLSL and y-**down** in WGSL, which exactly\nmatches each backend's own capture orientation — so a body that samples\n`screen_texture` at `screen_uv` needs no flip in *either* language.\n\nTwo places where a straight port really does differ:\n\n- **A capture you bind yourself.** `renderer.toFrameTexture()` grabs the frame\n  into a `Texture2d` you hand to `setTexture` — the supported replacement for\n  `readPixels` tricks. The GL capture is bottom-up (GLSL bodies sample it with\n  `1.0 - uv.y`) and opaque RGB; the WebGPU capture is top-down (the WGSL twin\n  must *not* flip) and preserves alpha.\n- **Vertical UV offsets** — a drop shadow, a directional smear. The WebGL pooled\n  multi-effect path composites through bottom-up FBOs, so declare a `uUVYDir`\n  uniform (initialise it to `1.0`) and multiply vertical offsets by it; the\n  renderer feeds `-1` on that path and `+1` everywhere else.\n\n## A ShaderEffect on a mesh is a colour hook, and drops some builtins\n\n`mesh.addPostEffect(fx)` shades a `Mesh` on **both** backends since 20.5. The\nbody is spliced into the engine's own mesh shader as a colour hook, so the mesh\nkeeps its placement, alpha cutout, lighting and fog, and `setUniform` /\n`setTime` drive it exactly as they do over a sprite. Uniform-only bodies are\nthe supported case and they are most of them.\n\nFive things the quad tier gives an effect that the mesh tier does not, each\nrefused rather than half-working:\n\n| refused | why |\n|---|---|\n| its own `setTexture` samplers | resolved by the quad tier's per-effect device state; refused on both backends |\n| `screen_texture`, `screen_uv` | need the screen capture only the quad tier takes |\n| `noise_uv` | derived from a quad's image and object size |\n| `vColor`, on WGSL only | the WGSL mesh module has no module-scope equivalent; the GLSL one declares it, so GLSL is fine |\n| `InstancedMesh` | the instanced WGSL builder drops everything between the two stages |\n\nHitting one warns **once** and falls back to the built-in shading. Nothing\nthrows, so the symptom is a mesh drawing with its plain texture, which reads as\na broken shader, a bad binding or a texture that failed to load. Check the\nconsole for the warning before you start eliminating alpha, bindings and\ntransparency: it names which of the five it was.\n\n2D renderables never hit this. `Sprite`, `Text` and the rest go through the\nquad path, which has the full feature set, so a gradient fill for text or a\nrefraction over a sprite is fine.\n\nIf you need one of the five on a mesh, supply a full dual-language `GLShader`\nrather than a fragment body, and declare the fog uniforms yourself (see\n`melonjs-3d`).\n\n## Extra textures bind in setTexture order\n\n`setTexture(\"uFoo\", …)` assigns bindings **1 and 2** to the first extra\ntexture, **3 and 4** to the second, and so on. The WGSL must match:\n\n```wgsl\n@group(3) @binding(1) var uFoo : texture_2d<f32>;\n@group(3) @binding(2) var uFooSampler : sampler;\n```\n\nCopying a two-texture effect and deleting one leaves the survivor declared at\n3/4 while it is bound at 1/2 — the program fails to build and the effect is\nsilently dropped.\n\n## Blend modes\n\nFourteen modes; thirteen of them are honoured by all three renderers:\n\n```js\nsprite.blendMode = \"additive\";   // or \"multiply\", \"screen\", \"overlay\", …\n```\n\nSix ride fixed-function blend state and cost nothing extra: `normal`,\n`additive` (spelled `add` or `lighter` too), `multiply`, `screen`, `exclusion`,\nand `none` (blending off — the source replaces the destination, alpha included).\n`none` is the one mode the Canvas backend does not implement; it reports\n`\"normal\"` instead.\n\nThe other eight cannot be expressed as a multiply-add on the destination, so the\nGPU backends capture the destination and composite through `BlendEffect` — one\ncapture plus one composite **per draw**: `difference`, `overlay`, `hard-light`,\n`color-dodge`, `color-burn`, `soft-light`, `darken`, `lighten`. Right for\naccents; expensive for hundreds of objects. The Canvas renderer reaches all\neight through `globalCompositeOperation` instead, at no extra cost.\n\nAnything else collapses to `\"normal\"`, and so do 3D meshes and `Gradient` fills\nwhatever you ask for — the latter with a one-time console warning.\n`renderer.setBlendMode(mode)` returns the mode it actually applied, so comparing\nthat against your request detects the fallback.\n\n## The Canvas fallback\n\n`ShaderEffect` on a Canvas renderer logs a warning, leaves `enabled = false`, and\nturns every method into a no-op. Nothing throws. The same happens on a GPU\nbackend when no body matches its language — a GLSL-only effect under WebGPU.\n\nThat is fine for decoration. It is **not** fine when the shader draws your\ncontent — the carrier renderable then shows through raw. A 1×1 white texture\nstretched to 280×156 renders as a white box where the effect should be.\n\nGuard when the effect is load-bearing:\n\n```js\nif (app.renderer.shaderLanguage === null) {\n    // no programmable pipeline — skip the carrier draw entirely\n}\n```\n\n`shaderLanguage` (`\"glsl\"`, `\"wgsl\"`, or `null`) is the flag to test here rather\nthan a backend name or `supportsDepthBuffer`: it answers \"can this renderer\ncompile what I am about to hand it\". Per-effect, `effect.enabled` answers the\nsame question after construction.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| effect does nothing, warning in console | Canvas fallback — no programmable pipeline |\n| effect does nothing on some machines only | GLSL-only shader, `video.AUTO` chose WebGPU |\n| a white or solid box where the effect should be | carrier renderable drawn while the effect is disabled |\n| ONE effect does nothing but two of them work | the renderable draws with primitives — set `postEffectNeedsCapture` |\n| effect cannot be re-enabled | `removePostEffect()` destroyed it — use `enabled` |\n| ported shader renders upside down | a hand-bound `toFrameTexture()` capture — GL is bottom-up, WebGPU top-down |\n| shadow/smear offset flips on some draws | vertical UV offset not multiplied by `uUVYDir` |\n| frame rate collapses with many blended sprites | an *advanced* mode (overlay, darken, …) — each draw is a capture plus a composite |\n| animated shader never moves | `setTime` fed milliseconds, or the shader declares no `uTime` |\n| squash and stretch only bulges, the height never changes, under Matter | the adapter rewrites `currentTransform` every step; set the scale from the sprite's `update()` |\n| `SquishEffect` squishes the whole screen instead of the sprite | it is chained with another effect, or on the camera; give the sprite it alone |\n| syntax error in a file whose shader you just edited, pointing at a line of GLSL | a backtick in the shader body ended the template literal — most often one wrapped around an identifier in a comment |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-renderables` — where post effects attach, and the destroy trap\n- `melonjs-3d` — the GPU-backend requirement, and why a custom mesh shader is\n  not affected by the camera's distance fog\n","packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md":"# melonjs-scenes-and-state\n\nPath: `packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md)","packages/melonjs/skills/melonjs-ui-and-text/SKILL.md":"# melonjs-ui-and-text\n\nPath: `packages/melonjs/skills/melonjs-ui-and-text/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md)","packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md":"# melonjs-particles-and-trails\n\nPath: `packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md)","packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md":"# melonjs-camera-and-drawing\n\nPath: `packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md)","packages/melonjs/skills/melonjs-lighting/SKILL.md":"# melonjs-lighting\n\nPath: `packages/melonjs/skills/melonjs-lighting/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/SKILL.md)","packages/melonjs/skills/melonjs-math/SKILL.md":"# melonjs-math\n\nPath: `packages/melonjs/skills/melonjs-math/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/SKILL.md)","packages/melonjs/skills/melonjs-deployment/SKILL.md":"# melonjs-deployment\n\nPath: `packages/melonjs/skills/melonjs-deployment/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/SKILL.md)","packages/melonjs/skills/melonjs-loading-assets/SKILL.md":"# melonjs-loading-assets\n\nPath: `packages/melonjs/skills/melonjs-loading-assets/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/SKILL.md)","packages/melonjs/skills/melonjs-renderer-backends/SKILL.md":"# melonjs-renderer-backends\n\nPath: `packages/melonjs/skills/melonjs-renderer-backends/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/SKILL.md)","packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md":"# melonjs-display-and-scaling\n\nPath: `packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md)","packages/melonjs/skills/melonjs-performance/SKILL.md":"# melonjs-performance\n\nPath: `packages/melonjs/skills/melonjs-performance/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/SKILL.md)","packages/melonjs/skills/melonjs-events/SKILL.md":"# melonjs-events\n\nPath: `packages/melonjs/skills/melonjs-events/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/SKILL.md)","packages/melonjs/skills/melonjs-plugins/SKILL.md":"# melonjs-plugins\n\nPath: `packages/melonjs/skills/melonjs-plugins/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/CLAUDE.md)","GEMINI.md":"# Gemini Agent & Antigravity Instructions\n\nPath: `GEMINI.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/GEMINI.md)","COPILOT.md":"# GitHub Copilot Context & Rules\n\nPath: `COPILOT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/COPILOT.md)",".cursorrules":"# Cursor IDE Native Rules\n\nPath: `.cursorrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursorrules)",".windsurfrules":"# Windsurf Cascade Agent Rules\n\nPath: `.windsurfrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.windsurfrules)",".clinerules":"# Cline Extension Native Directives\n\nPath: `.clinerules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.clinerules)","llms.txt":"# LLM Index & Context Digest\n\nPath: `llms.txt`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/llms.txt)","mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/mcp.json)","marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/marketplace.json)","plugin.json":"# Plugin Plugin Manifest\n\nPath: `plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/plugin.json)",".github/copilot-instructions.md":"# GitHub Copilot Instructions\n\nPath: `.github/copilot-instructions.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.github/copilot-instructions.md)",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/plugin.json)","packages/melonjs/skills/melonjs/README.md":"# melonjs Documentation\n\nPath: `packages/melonjs/skills/melonjs/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs/README.md)","packages/melonjs/skills/melonjs-getting-started/README.md":"# melonjs-getting-started Documentation\n\nPath: `packages/melonjs/skills/melonjs-getting-started/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-getting-started/README.md)","packages/melonjs/skills/melonjs-20-migration/README.md":"# melonjs-20-migration Documentation\n\nPath: `packages/melonjs/skills/melonjs-20-migration/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-20-migration/README.md)","packages/melonjs/skills/melonjs-renderables/README.md":"# melonjs-renderables Documentation\n\nPath: `packages/melonjs/skills/melonjs-renderables/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderables/README.md)","packages/melonjs/skills/melonjs-3d/README.md":"# melonjs-3d Documentation\n\nPath: `packages/melonjs/skills/melonjs-3d/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d/README.md)","packages/melonjs/skills/melonjs-3d-assets/README.md":"# melonjs-3d-assets Documentation\n\nPath: `packages/melonjs/skills/melonjs-3d-assets/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d-assets/README.md)","packages/melonjs/skills/melonjs-physics/README.md":"# melonjs-physics Documentation\n\nPath: `packages/melonjs/skills/melonjs-physics/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-physics/README.md)","packages/melonjs/skills/melonjs-sprites-and-animation/README.md":"# melonjs-sprites-and-animation Documentation\n\nPath: `packages/melonjs/skills/melonjs-sprites-and-animation/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-sprites-and-animation/README.md)","packages/melonjs/skills/melonjs-input/README.md":"# melonjs-input Documentation\n\nPath: `packages/melonjs/skills/melonjs-input/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-input/README.md)","packages/melonjs/skills/melonjs-effects-and-shaders/README.md":"# melonjs-effects-and-shaders Documentation\n\nPath: `packages/melonjs/skills/melonjs-effects-and-shaders/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-effects-and-shaders/README.md)","packages/melonjs/skills/melonjs-tilemaps/README.md":"# melonjs-tilemaps Documentation\n\nPath: `packages/melonjs/skills/melonjs-tilemaps/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-tilemaps/README.md)","packages/melonjs/skills/melonjs-audio/README.md":"# melonjs-audio Documentation\n\nPath: `packages/melonjs/skills/melonjs-audio/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-audio/README.md)","packages/melonjs/skills/melonjs-scenes-and-state/README.md":"# melonjs-scenes-and-state Documentation\n\nPath: `packages/melonjs/skills/melonjs-scenes-and-state/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/README.md)","packages/melonjs/skills/melonjs-ui-and-text/README.md":"# melonjs-ui-and-text Documentation\n\nPath: `packages/melonjs/skills/melonjs-ui-and-text/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/README.md)","packages/melonjs/skills/melonjs-particles-and-trails/README.md":"# melonjs-particles-and-trails Documentation\n\nPath: `packages/melonjs/skills/melonjs-particles-and-trails/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/README.md)","packages/melonjs/skills/melonjs-camera-and-drawing/README.md":"# melonjs-camera-and-drawing Documentation\n\nPath: `packages/melonjs/skills/melonjs-camera-and-drawing/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/README.md)","packages/melonjs/skills/melonjs-lighting/README.md":"# melonjs-lighting Documentation\n\nPath: `packages/melonjs/skills/melonjs-lighting/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/README.md)","packages/melonjs/skills/melonjs-math/README.md":"# melonjs-math Documentation\n\nPath: `packages/melonjs/skills/melonjs-math/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/README.md)","packages/melonjs/skills/melonjs-deployment/README.md":"# melonjs-deployment Documentation\n\nPath: `packages/melonjs/skills/melonjs-deployment/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/README.md)","packages/melonjs/skills/melonjs-loading-assets/README.md":"# melonjs-loading-assets Documentation\n\nPath: `packages/melonjs/skills/melonjs-loading-assets/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/README.md)","packages/melonjs/skills/melonjs-renderer-backends/README.md":"# melonjs-renderer-backends Documentation\n\nPath: `packages/melonjs/skills/melonjs-renderer-backends/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/README.md)","packages/melonjs/skills/melonjs-display-and-scaling/README.md":"# melonjs-display-and-scaling Documentation\n\nPath: `packages/melonjs/skills/melonjs-display-and-scaling/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/README.md)","packages/melonjs/skills/melonjs-performance/README.md":"# melonjs-performance Documentation\n\nPath: `packages/melonjs/skills/melonjs-performance/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/README.md)","packages/melonjs/skills/melonjs-events/README.md":"# melonjs-events Documentation\n\nPath: `packages/melonjs/skills/melonjs-events/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/README.md)","packages/melonjs/skills/melonjs-plugins/README.md":"# melonjs-plugins Documentation\n\nPath: `packages/melonjs/skills/melonjs-plugins/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/README.md)"},"files":{"packages/melonjs/skills/melonjs-3d/SKILL.md":"---\nname: melonjs-3d\ndescription: \"Use this skill for anything 3D or 2.5D in melonJS — Camera3d, Mesh, InstancedMesh, Sprite3d billboards, Light3d, ground shadows, glTF/GLB scenes, and depth sorting. Covers the Y-down/+Z-forward convention that is the inverse of OpenGL, the cameraClass opt-in, clip planes, and what does not work on the Canvas fallback. Triggers on: Camera3d, Mesh, InstancedMesh, Sprite3d, Trail3d, ribbon, exhaust trail, Light3d, billboard, glTF, glb, 3D, 2.5D, depth, cameraClass, fov, setClipPlanes, setFog, fog, distance fog, height fog, heightFalloff, transparent, transparency, alpha, blendMode, fade, castGroundShadow, lit, setBasis, getBasis, lookAt, up vector, gimbal, slerp, great circle, turn a direction.\"\nlicense: MIT\n---\n\n# 3D and 2.5D in melonJS\n\n> melonJS is a 2D engine that grew a real 3D tier: perspective cameras, meshes,\n> instancing, glTF scenes and 3D lights. Most of what a model \"knows\" about 3D\n> graphics comes from the OpenGL convention, and **melonJS's is the inverse**.\n> Read the conventions section before writing any 3D code.\n\n## Coordinate conventions — the inverse of OpenGL\n\nThis is the single most important thing on this page.\n\n| | melonJS | OpenGL convention |\n|---|---|---|\n| vertical | **Y-down** — higher `y` is *lower* on screen | Y-up |\n| depth | **+Z forward** — higher `z` is *farther* away | −Z forward |\n\n`Camera3d` exposes three rotation angles: `camera.pitch`\n(X, look up/down), `camera.yaw` (Y, look left/right) and `camera.roll` (Z, bank\nthe horizon). The view is `R(yaw) ∘ R(pitch) ∘ R(roll)` inverted, and the frustum\nplanes come off that same matrix — so culling follows a banked view.\n\nThe basis those angles produce is **readable and writable**: `getBasis(right,\nup, forward)` reads it, `setBasis(right, up, forward)` poses the camera from\none.\n\n`camera.rotate()` is NOT the way to bank a 3D camera. It writes the inherited\n`currentTransform`, which a 3D view never reads, so the call is silently inert.\nSet `roll`. (On a `Camera2d` it is the other way round: roll IS that transform's\nrotation, which is why `worldToLocal` / `localToWorld` compensate for it.)\n\nThe payoff is that 2D code translates directly — anywhere you used `pos.x` /\n`pos.y`, add `pos.z` and the maths still holds. The cost is that every\nOpenGL-shaped instinct is backwards.\n\n**Consequence that bites immediately:** \"behind everything\" is the *largest*\ndepth when the camera looks along +Z. Put a backdrop on the wrong side and it\neither paints over the whole scene (it is nearer than everything else) or\ndisappears without trace (it falls in front of the near plane and the GPU clips\nit). Which sign means \"far\" depends on which side the camera sits, so check the\ncamera before picking a backdrop depth.\n\nglTF assets are authored Y-up right-handed. `level.load` converts them by\nrotation (`rightHanded: true` is its default), so you do not apply a flip\nyourself. The **`Mesh` constructor defaults `rightHanded` to `false`**, which\nbridges by mirroring instead — geometry you hand to a `Mesh` directly comes in\nflipped left/right unless you pass `rightHanded: true`. See\n`melonjs-3d-assets`.\n\n## Opting in\n\n3D starts with the camera class, and nothing else has to change. Set it once on\nthe `Application`:\n\n```js\nimport { Application, Camera3d } from \"melonjs\";\n\nconst app = new Application(1024, 768, {\n    parent: \"screen\",\n    cameraClass: Camera3d,      // ← the opt-in\n});\nawait app.init();\n```\n\nThat flips the world's sort to `\"depth\"`, switches the broadphase to a 3D index,\nand renders sprites and meshes with perspective projection. `Camera3d` extends\n`Camera2d`, so follow, fade, shake and post effects all still work.\n\n### Keep the camera, do not fish it back out\n\n`app.viewport` is the **active stage's** default camera, so it is typed\n`Camera2d`: a stage overrides the application's `cameraClass`, and the\nbuilt-in loading screen does exactly that. Reading your `Camera3d` back out of\nit therefore needs a cast, and a cast that is wrong while the loader is up:\n\n```js\nconst camera = app.viewport as Camera3d;   // works, until it does not\n```\n\nOwn it instead. Pass the camera in and keep the typed reference, and nothing\ndownstream ever narrows:\n\n```js\nclass GameStage extends Stage {\n    readonly camera: Camera3d;\n\n    constructor(app) {\n        const camera = new Camera3d(0, 0, app.renderer.width, app.renderer.height);\n        super({ cameras: [camera] });\n        this.camera = camera;\n    }\n}\n```\n\nA freshly built camera is named `\"default\"`, and a stage registers its\n`cameras` by name, so this IS the stage's default camera: `app.viewport`\nreturns the same instance.\n\n### The two patterns differ in LIFETIME, and that decides which you want\n\nThis is the part that bites. `cameraClass` builds a **fresh camera for each\nstage**, which the engine does deliberately so \"Camera3d state shouldn't\nbleed across stages\". An owned camera passed through `super({ cameras })` is\nbuilt once and lives as long as the stage object, which for a stage you\nregister with `state.set()` means as long as the application.\n\nSo anything you attach to the camera now outlives the visit:\n\n```js\n// in onResetEvent — runs EVERY time the stage is entered\napp.viewport.addPostEffect(bloom);\n```\n\nWith `cameraClass` that lands on a fresh camera each time and is harmless.\nWith an owned camera it stacks: a second bloom on the second visit, a third\non the third. The symptom is text and edges appearing to double or smear,\nbecause the scene is being post-processed once per accumulated effect.\n\nNote that `removePostEffect` DESTROYS the effect rather than detaching it, so\n\"just remove it on the way out\" is not a free undo.\n\n**Choose on lifetime, not on typing:**\n\n| your effects / camera state are | use |\n|---|---|\n| set up per stage visit, in `onResetEvent` | `cameraClass` — fresh camera per stage |\n| set up once, in the stage constructor | an owned camera in `super({ cameras })` |\n\nIf you want the typed reference but your setup is per-visit, keep\n`cameraClass` and narrow once on reset:\n\n```js\noverride onResetEvent(app) {\n    const viewport = app.viewport;\n    if (!(viewport instanceof Camera3d)) {\n        throw new Error(\"this stage needs a Camera3d\");\n    }\n    this.camera = viewport;   // typed, checked, and still fresh per visit\n}\n```\n\nThat is what the Earth Defender example does, and why: it attaches post\neffects on every reset and never removes them.\n\nNarrowing beats asserting wherever you read the shared slot: the language\nverifies `instanceof`, and fails loudly if the application was never given a\n`cameraClass`, where a cast fails silently at the first 3D call.\n\nPer-stage opt-in also works, which is useful when the loading screen is 2D:\n\n```js\nclass GameStage extends Stage {\n    constructor() {\n        super({ cameras: [new Camera3d(0, 0, 1024, 768, { fov: Math.PI / 3 })] });\n    }\n}\n```\n\n## Camera traps\n\n- **There is no sky.** The frame clears to `renderer.backgroundColor` (opaque\n  black unless the renderer was created transparent), and `world.backgroundColor`\n  clears over it when you set one — both are flat colours. Anything more is a\n  backdrop renderable, screen-fixed or placed at a far depth.\n- **Set the clip planes for your scene scale.** Defaults are near 0.1 / far 1000.\n  Objects beyond the far plane clip or project with bad w-divides; a `near` that\n  is too small wastes depth precision and distant props z-fight. Use\n  `camera.setClipPlanes(near, far)`.\n- **`camera.pos.set(x, y)` is 2-argument and zeroes z.** Use\n  `camera.setPosition(x, y, z)`, which leaves the depth untouched when `z` is\n  omitted; `camera.depth` is the z accessor if you only want that.\n- **`lookAt(target)` solves pitch and yaw only**, so on its own it cannot\n  express \"up is this direction\" and it leaves `roll` alone. Pass an up to\n  bank the horizon too: `camera.lookAt(target, up)`, where `up` is what should\n  appear UP ON SCREEN. This engine renders Y-down, so a level horizon is\n  `(0, -1, 0)`. There is deliberately no default, because giving one would\n  silently level every camera that had banked its own horizon.\n- **When the game already has all three axes, hand them over**:\n  `camera.setBasis(right, up, forward)`, the writable counterpart of\n  `getBasis`. The case that needs it is a view over a curved surface, where up\n  is the surface normal and differs at every point, so no pitch and yaw pair\n  expresses it and integrating towards one drifts and gimbals at the poles.\n  `setBasis` absorbs a triple that has drifted off orthonormal rather than\n  leaving the camera holding something that is not a rotation, and it does not\n  care what the axes are scaled to: a direction is a direction, so anything\n  from a denormal to 1e300 resolves the same way. Note its `up` is the camera's\n  local +Y column, which under Y-down points DOWN the screen: the opposite\n  sense to `lookAt`'s, and one negation apart.\n- **`worldToLocal` does not project.** It is a 2D camera's offset subtraction.\n  To pin a label or marker to a point in the scene use\n  `camera.worldToScreen(x, y, z)`, which applies the projection and returns\n  `null` behind the camera. See `melonjs-camera-and-drawing`.\n\n## Where an up vector earns its keep\n\nThree cases. In the first two, `lookAt(target)` on its own is already right and\nan up would be noise. The third is the one it exists for.\n\n```js\n// 1. ordinary third-person: world up never changes, so say nothing\ncamera.setPosition(x, y, z).lookAt(player);\n\n// 2. a deliberate dutch angle: this is a ROLL, not an up vector\ncamera.lookAt(player);\ncamera.roll = Math.PI / 32;\n\n// 3. a view over a curved surface: up is the surface normal, and it is\n//    different at every point on the globe. No pitch and yaw pair expresses\n//    it, and integrating towards it drifts and gimbals over the poles.\nconst up = player.pos.clone().sub(planetCentre).normalize();  // surface normal\ncamera.setPosition(eye.x, eye.y, eye.z).lookAt(player, up.negate());\n```\n\nNote the `negate()` in case 3. The surface normal points away from the planet,\nwhich is the direction the player's head points, and `up` wants what should\nappear UP ON SCREEN. This engine renders Y-down, so `(0, -1, 0)` is a level\nhorizon and a world-space \"up\" direction has to be handed over in that sense.\nGet it backwards and the view is upside down, which is the single most likely\nmistake with this argument.\n\nIf the game already computes all three axes, skip the solve entirely:\n\n```js\n// a surface frame the game is already maintaining\ncamera.setBasis(right, up, forward);\n```\n\n`setBasis` takes the camera's OWN columns, where `up` is local +Y and so points\ndown the screen: the opposite sense to `lookAt`'s, one negation apart. It also\nabsorbs a frame that has drifted off orthonormal, which one integrated over\nthousands of steps will have.\n\n**A camera posed this way and a world rotated into a fixed camera's frame are\nthe same picture.** If a game already transforms its world into view space by\nhand, there is nothing to gain by switching; the value of `setBasis` is not\nhaving to write that transform in the first place.\n\n## Turning a direction: `slerp`, not `lerp`\n\n`lerp` walks the straight line between the two tips, which passes INSIDE the\nsphere. The result points the right way at both ends, lingers near them, rushes\nthe middle, and comes out shorter than either input as it crosses the midpoint.\nOn a camera swing that reads as a lurch; on a body walking over a globe it dips\ninto the surface.\n\n`Vector3d#slerp` turns along the great circle instead, at a constant angular\nrate, and carries the length across linearly, so two unit vectors give back a\nunit vector and nothing needs renormalizing.\n\n```js\n// a turret, a camera, a ship: a facing chasing another facing\nfacing.slerp(toTarget, 0.25);\n\n// frame-rate independent, the same shape as math.damp\nfacing.slerp(toTarget, 1 - Math.exp((-6 * dt) / 1000));\n```\n\n`ObservableVector3d` has it too and fires its callback ONCE, so\n`renderable.pos.slerp(...)` is one notification rather than three.\n\n**`Vector2d` has the same method**, and it is not a cut-down copy: in 2D the\nwhole turn is one signed angle, so there is no sine to divide by and no\ndegenerate near-parallel case at all. What it buys a 2D game is the wrap: a\nheading damped as a NUMBER from 170 degrees to -170 degrees sweeps the long 340\ninstead of the short 20, and an arc between directions cannot do that.\n\nTwo degenerate pairs are handled inside rather than left to the caller, and\nboth turn up in ordinary game code:\n\n- **the same direction**, where every path between them is the same path.\n- **opposite directions**, where infinitely many great circles pass through\n  both and none is the shorter way round. The textbook formula divides by a\n  vanishing sine here and hands back `NaN`, which propagates into a position or\n  a matrix and the object disappears. `slerp` picks a stable perpendicular and\n  turns through half a circle instead.\n\nThat second case is why a hand-rolled version is a liability rather than a\nshortcut: anything orbiting a planet reaches the antipode of where it started,\nevery run.\n\n**`math.damp` for a scalar, `slerp` for a direction.** Same job, different\nspace; what `damp` cannot do is keep a vector on the sphere.\n\n## Depth sorting\n\nUnder `Camera3d` the world sorts on `\"depth\"` **every frame** (recursively), so\nunlike 2D you *can* assign `depth` after `addChild` and it will reorder. That is\nthe one place where the usual z-ordering rule is relaxed — see\n`melonjs-renderables` for the 2D rules, which still apply everywhere else.\n\nYou will often *have* to. A `Container` has `autoDepth: true` by default, so\n`addChild(child)` with no explicit z **overwrites `pos.z` with the child's\nindex** — a real world depth in a 3D scene, and never the one you wanted. Pass\nit (`world.addChild(mesh, z)`), or set `mesh.depth` afterwards. The glTF\nimporter turns `autoDepth` off on the container it loads into for this reason.\n\n**`floating` does not opt out of this sort.** It skips the camera *transform*,\nnot the depth *order*. A floating child is ordered by `|pos.z|` alone — its\n`pos.x/y` are screen pixels, and the camera does not move relative to it — so\nthe magnitude is the distance and the sign is ignored:\n\n```js\nworld.addChild(hud, -150);       // small -> nearer than anything -> on top\nworld.addChild(skybox, -10000);  // large -> farther -> behind everything\nworld.addChild(skybox, 100000);  // equally far: sign does not matter\n```\n\nBoth hold at any camera position. A HUD given the huge z that would put it on\ntop in 2D lands at the far end of the level instead, with the scenery drawing\nover it.\n\n## Transparency\n\nA mesh fades by setting its opacity — there is nothing else to switch on:\n\n```js\nghost.setOpacity(0.4);\n```\n\nMeshes render in two phases. The **opaque pass** writes depth in sort order; the\n**transparent pass** replays afterwards, back-to-front, blending and writing no\ndepth. A draw lands in the second whenever its alpha is fractional.\n\nThat default matters because the opaque path writes premultiplied colour with\nblending off, so before this a faded mesh came out **darkened toward black**\nrather than see-through — the background contributed nothing.\n\n**`transparent: true`** when the transparency is in the TEXTURE rather than the\nopacity — a soft-edged glow, smoke, a glTF material with `alphaMode: \"BLEND\"`.\nThe automatic check reads the draw's alpha and cannot see into a texture. Watch\n`alphaCutoff` here: it discards texels *before* blending sees them, so a soft\nedge needs a low cutoff (`Sprite3d` drops its own default to `1/255` when you\nset `transparent: true`). The cutoff thresholds the MATERIAL's alpha, not the\ndrawn alpha, so a fading cutout mesh keeps its shape instead of disappearing at\nits own threshold.\n\nThe glTF loader does **not** set this for you: one loaded mesh can merge several\nmaterials and the flag routes the whole mesh, so a `\"BLEND\"` material sharing\ngeometry with an opaque one would drag the opaque half into the transparent pass.\n\n**`transparent: false`** pins a mesh to the opaque pass however it is faded.\n\n### `depthTest: false` for something with no surface to be occluded on\n\n`transparent` decides the PASS, and the transparent pass turns depth *writing*\noff on its own so overlapping blended draws blend instead of fighting. Whether\ngeometry in front of a mesh **hides** it is a separate question, and\n`depthTest` is where you answer it. The default, `true`, is what you want for\nanything genuinely in the scene — a muzzle flash behind a wall should still be\nbehind the wall, and this is not a synonym for \"additive\".\n\nSet it `false` for a mesh standing in for a screen-space effect: an additive\nglow carrying a world position only so it can sort and move with the thing it\nbelongs to. A world-space caption billboarded over a prop wants it for the\nsame reason, since it has no surface for the prop's own foliage to be in front\nof. Left depth tested, a flat billboard is sliced along a hard straight\nline the moment any geometry is nearer at some pixel. That shows up worst\naround something round — a glow on a rock near a planet's limb gets cut,\nand no offset escapes it, because the near surface bulges further toward the\ncamera than any offset you would dare apply.\n\n```js\nconst halo = new Sprite3d(0, 0, {\n    image: glowTexture,\n    width: rock.radius * 3, height: rock.radius * 3,\n    billboard: \"spherical\",\n    blendMode: \"additive\",\n    transparent: true,     // the blended pass\n    alphaCutoff: 0,        // do not cut the soft falloff\n    depthTest: false,      // and do not let the world slice it\n});\n```\n\nHonoured in the transparent pass on both GPU backends; the Canvas renderer has\nno depth buffer and ignores it.\n\nBlending uses the renderable's existing `blendMode`, so a glow is one property.\nThe advanced modes (`\"overlay\"`, `\"difference\"`, and the rest that need a\ncompositing pass) fall back to `\"normal\"` here, on both backends:\n\n```js\nconst glow = new Mesh(0, 0, {\n    ...quad, texture: glowTexture,\n    transparent: true, blendMode: \"additive\", alphaCutoff: 0,\n});\n```\n\n| | |\n| --- | --- |\n| sorting | **per object**, by distance from the camera |\n| intersecting transparent meshes | may pop as the camera moves — split them, or accept it |\n| `InstancedMesh` | sorts as **one** object; instances draw in buffer order |\n| needs | a GPU backend and a `Camera3d` |\n\nGround shadows ride the same pass — a blob is a decal, and decals are its first\nclient rather than a feature of their own.\n\n**`Trail3d`** is the transparent pass's other obvious client: a camera-facing\nribbon for an exhaust streak, a speed trail or a tyre mark. It is a `Mesh`, so\nit sorts and fogs with everything else here, and like the rest of this tier it\nneeds a `Camera3d`. `Trail` is the 2D one and stays flat under a perspective\ncamera. See `melonjs-particles-and-trails`.\n\n## Distance fog\n\nOff until you ask for it, and one call on the camera:\n\n```js\ncamera.setFog({ near: 2000, far: 7000 });   // linear: name the two distances\ncamera.setFog({ mode: \"exp2\", density: 4e-4 }); // or one density\ncamera.setFog(null);                         // off\n```\n\nIt is the cheapest thing that stops a 3D scene reading as flat cut-outs, and it\nlets props arrive at the far plane without a visible edge.\n\n**Every parameter is optional, and the omitted ones track live.** Distances\ndefault to the camera's own clip planes, so fog cannot silently disagree with\nthem after a later `setClipPlanes`. The colour defaults to\n`renderer.backgroundColor` and follows it, so geometry dissolves into whatever\nsky you already set — including through a day/night fade. Pass `color` only\nwhen the fog should deliberately differ from the backdrop:\n\n```js\ncamera.setFog({ far: 5000, color: \"#8899aa\" });\n```\n\nA `Color` is held by reference, so mutating it animates the fog.\n\n**Height falloff** makes mist pool in low ground instead of hanging at every\naltitude equally — the difference between fog reading as weather and as a global\ndesaturation:\n\n```js\ncamera.setFog({ near: 1200, far: 7000, fogHeight: 0, heightFalloff: 0.0015 });\n```\n\n`heightFalloff` defaults to **0**, which is uniform fog — not a special case,\nthe same integral with the dial at zero, so leaving it out changes nothing.\nRender space is **Y-down**, so `fogHeight` is the floor and density rises\n*below* it; every published form of this formula assumes Y-up and has the\nopposite sign.\n\nFog is measured **radially** from the camera and applied **per fragment**, so\nit does not slide as the camera turns and does not band across large triangles.\nIt lives on the camera, so a split-screen or minimap view fogs independently —\nand a `Camera2d` never fogs at all.\n\n**Per object:** `fog: false` exempts a mesh however far away it is — for a\nwaypoint or objective marker that has to stay readable. It exempts the mesh and\nnot the ground shadow it casts: a blob is a mark on the floor and fogs with the\nfloor. Emissive surfaces fog\nlike everything else (light travelling through fog is attenuated too), so a\nneon sign that should punch through wants `fog: false`, not a brighter\nemissive.\n\nOnly meshes fog. 2D content, HUDs and `floating` renderables never reach the\nmesh shaders, so a screen-space overlay stays clean with no work.\n\n**A custom mesh shader is not fogged unless it asks to be.** Fog is compiled\ninto the engine's own mesh programs — `#define FOG` on WebGL, an `enable_fog`\noverridable constant on WebGPU — and a shader you supply is yours: the engine\nbinds it as written and never substitutes a fogged variant. So a mesh given its\nown `GLShader` keeps full contrast while the scene around it recedes. It is\nsafe — nothing throws, and the camera's fog is simply not applied — but it is\nusually surprising.\n\nA hosted `ShaderEffect` is the other case and needs nothing: its body is\nspliced into the engine's own mesh program as a colour hook, and fog is applied\nto the hook's result, so such a mesh fogs like any other.\n\nTo opt in, declare the same uniforms and the engine will feed them, because the\nfog values are pushed to any mesh program that declares them rather than only to\nthe built-in ones:\n\n```glsl\nuniform vec3 uFogColor;   // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams;  // x = mode (0 off / 1 linear / 2 exp2),\n                          // y = near, z = 1/(far - near), w = density\n```\n\nYour vertex stage computes the distance itself — `length((uViewMatrix *\nuModelMatrix * vec4(aVertex, 1.0)).xyz)`, radially so it does not swim as the\ncamera turns — and the blend must scale the fog colour by the fragment's own\nalpha, `mix(uFogColor * a, rgb, f)`, because `vColor` arrives premultiplied.\nMixing toward the unscaled colour haloes every alpha-cutout edge.\n\nThe flip side is the reason fog costs nothing when unused: with no camera fog,\nthe mesh programs are compiled without any of it, on both backends. It is not a\nbranch that is skipped at runtime — the code is not there.\n\n## Meshes\n\n```js\nconst cube = new Mesh(x, y, {\n    model: \"cube\",          // a preloaded OBJ name — no built-in primitives\n    texture: atlas,         // TextureAtlas, image, or a preloaded image name\n    width: 64, height: 64,\n    cullBackFaces: true,    // the default\n    lit: true,\n});\n```\n\n**A mesh pivots about its model origin `(0, 0, 0)`, not an anchor point.** Under\n`Camera3d` it opts out of the anchor offset entirely, so `anchorPoint` is inert\nthere — place the origin where you want the pivot at authoring time, or nest the\nmesh under a transformed parent. The anchor is only honoured on the legacy\n2D-camera path.\n\n### InstancedMesh, and when it is the wrong tool\n\n**`InstancedMesh`** draws one mesh many times in a single draw call — the\ndifference between a hundred trees and a hundred thousand. glTF scenes using\n`EXT_mesh_gpu_instancing` load as one automatically; by hand it is a `Mesh`\nwith a count:\n\n```js\nconst trees = new InstancedMesh(0, 0, { ...treeGeometry, instanceCount: 400 });\nconst at = new Matrix3d();               // one scratch, reused\nfor (let i = 0; i < trees.instanceCount; i++) {\n    at.identity().translate(x, y, z);\n    trees.setInstance(i, at);\n}\nworld.addChild(trees, 0);\n\ntrees.visibleInstanceCount = 120;        // draw fewer, without re-uploading\n```\n\nIt is not a free upgrade. One `InstancedMesh` is **one geometry and one\nmaterial**, and four things move from per-object to per-group:\n\n| | with `Mesh` | with `InstancedMesh` |\n| --- | --- | --- |\n| depth sort | each object sorts on its own `pos` | the whole set has **one** sort key |\n| ground shadow | one blob per object | one instanced draw for the set |\n| removal | `removeChild`, indices unaffected | `removeInstance(i)` swaps the **last** instance into the hole, so any index you were holding is now wrong |\n| colour | `tint` per object | needs `instanceColors: true` and `setInstanceColor(i, …)` |\n\nSo the question is not \"how many are there\" but **\"does the game address them\nindividually\"**:\n\n- **Scenery — instance it.** Trees, rocks, grass, debris: the game never asks\n  about one of them.\n- **Collision-tested props — still fine.** You test against positions you\n  already own; instancing only changes how they are *drawn*.\n- **Collectibles and enemies — usually not.** Anything removed one at a time\n  makes `removeInstance`'s swap your problem: you have to keep an index↔object\n  map and repair it on every removal. At small counts a pooled `Mesh` each is\n  less code and no slower.\n\nUnder a few hundred objects the draw-call saving is not what limits you\nanyway — reach for it when the count is in the thousands, or when the objects\nare pure scenery and it costs nothing to.\n\n## Normals are generated for you\n\nA `lit` mesh needs per-vertex normals for the shader to light with. Supply them\nif you have them; **omit them and the engine computes them from the geometry**:\n\n```js\nconst mesh = new Mesh(x, y, { vertices, uvs, indices, lit: true });\n// normals derived from the triangles — nothing else to do\n```\n\nYou do not choose flat or smooth, because the geometry already decides. Face\nnormals accumulate into their vertices weighted by area, so where faces **share**\na vertex they average and the surface shades smoothly, and where every triangle\ncarries its **own** three vertices — a triangle soup, which is how most\nhand-built geometry comes out — each vertex belongs to one face and the result\nis that face's normal, so it shades flat. Want faceted edges: duplicate the\nvertices. Want smooth: share them.\n\nAn explicit `settings.normals` always wins, and an unlit mesh gets none — there\nwould be nothing to read them.\n\n**A lit mesh with no normals used to render fullbright**, which looks like the\nlighting is broken rather than absent. If an older scene suddenly picks up\nshading, that is why.\n\n## Colouring a mesh\n\nThere are four levels, and picking the wrong one is the usual reason a colour\n\"does not apply\". They all multiply together.\n\n| level | how | use for |\n|---|---|---|\n| whole object | `mesh.setTint(\"#ff8800\")`, or `mesh.tint.setColor(r, g, b)` | flash on hit, team colour, fading one object |\n| per vertex | `settings.vertexColors`, or `mesh.setVertexColor(i, color)` | a gradient *within* one mesh — distance haze, a darker crease |\n| per material | `textureGroups`, from a multi-material OBJ + MTL | a model whose parts differ, in one draw call |\n| per instance | `new InstancedMesh(…, { instanceColors: true })` then `setInstanceColor(i, color)` | a thousand copies that differ |\n\n**`tint` is per object.** That is the trap: build a terrain as one big mesh and\nyou can tint the whole valley or none of it. Anything that varies *across* a\nsingle mesh is per-vertex.\n\n```js\n// fade a procedural terrain toward the sky the further out it goes\nconst ground = new Color(217, 230, 244);\nconst sky = new Color(207, 230, 247);\nconst haze = new Color();\nfor (let i = 0; i < mesh.vertexCount; i++) {\n    const t = Math.min(1, mesh.originalVertices[i * 3 + 2] / 6000);\n    mesh.setVertexColor(i, haze.copy(ground).lerp(sky, t));\n}\n```\n\nSupply the whole array at construction when you already have it —\n`vertexColors` takes a packed `Uint32Array` (the form the batchers read, so no\nconversion) or one `Color` per vertex. A length that does not match\n`vertexCount` **throws**; it is not padded, because a short array would leave\nthe tail of the mesh mis-coloured and that reads as a lighting bug.\n\nMutating the array directly is fine, but say so afterwards:\n\n```js\nmesh.vertexColors[i] = color.toUint32(color.alpha);\nmesh.needsUpdate = true;   // the retained Camera3d path uploads once\n```\n\n`setVertexColor` does that for you. Skip it and the colour applies under a 2D\ncamera and silently does not under `Camera3d`.\n\nOn a lit mesh the colour multiplies the **lit** result, so it behaves as albedo\nrather than as an emissive override — a vertex colour will not make an unlit\nface bright.\n\n`alpha` hides a mesh as it hides anything else: at 0 the draw is skipped. There\nis no *partial* mesh transparency though — the mesh path renders opaque, so a\nmesh at `alpha = 0.5` draws fully opaque rather than half see-through. Fade a\nmesh out and it will stay solid until it vanishes.\n\n## Moving a mesh in 3D\n\n`Mesh` and `Sprite3d` inherit the ordinary `Renderable` transform helpers, and\nboth are already 3D:\n\n```js\nmesh.rotate(Math.PI / 2, new Vector3d(1, 0, 0));   // axis overload\nmesh.scale(2, 1, 0.5);                             // z is a real argument\n```\n\n`rotate(angle)` alone is the 2D case (about Z); pass a `Vector3d` and it\nrotates about that axis. Both write to `currentTransform`, a **`Matrix3d`**,\nwhich `Mesh` folds into its model matrix — so orienting a mesh costs a matrix\nupdate, not new geometry.\n\nThis matters most for the case it is least obvious for: a horizontal plane.\n`Sprite3d` with `billboard: false` is a quad in the sprite plane — upright —\nand the way to lay one flat (water, a road, a shadow decal) is to rotate it a\nquarter turn about X, **not** to hand-build vertices. The one reason to build\ngeometry instead is tiling: a `Sprite3d` bakes its UVs 0..1 from its atlas\nframe, so a surface that needs `textureRepeat` across many world units still\nwants an explicit `vertices`/`uvs` mesh.\n\n## The mesh pass is opaque\n\nWorth stating plainly, because the symptom does not look like a blending\nproblem: **the mesh pass disables blending**. A soft-edged texture — a glow, a\nsun, a halo, anything with a gradient alpha — composites as a hard-edged disc\nwith a grey rim, which reads as a broken texture rather than as a missing\nfeature.\n\n`transparent: true` routes it through the transparent pass, and `blendMode:\n\"additive\"` on top of that makes a light source add to the sky instead of\nsitting on it. This is the same feature a translucent water surface needs.\n\n## Textures under a perspective camera\n\nA tiling texture on a large ground plane is seen at a grazing angle, and that\nchanges which detail survives:\n\n- **Nothing straight and parallel to the direction of travel.** Every such run\n  converges on the vanishing point, so a few current bands on a river become a\n  fan of rays radiating out of the horizon. Narrowing them and tiling more\n  often only multiplies the rays. Short, scattered, direction-free detail\n  minifies into an even shimmer instead.\n- **`textureFilter: \"linear\"`** for a floor — `\"nearest\"` opts out of mipmaps\n  and anisotropy, and detail aliases into a wash exactly where the camera looks\n  most.\n\n## Ground must outlast the props standing on it\n\nScenery scattered up a slope is placed by its own rule, and terrain is built by\nanother. When the planting reaches further than the mesh does, a low camera\nsees trees standing over open sky at the corners of the frame. Build the ground\nwider than the widest thing planted on it — past the profile's clamp it is a\nflat plateau, so the extra columns cost two triangles and no silhouette.\n\nRecycling has the same trap in Z: an endless runner that recycles tiles a fixed\ndistance behind the *player* forgets that the camera trails further back still,\nand the bottom of the frame falls off the world.\n\n## Sprite3d and billboards\n\n`Sprite3d` is the 2.5D workhorse: a flat sprite living at a real depth, with\n`billboard` controlling how it faces the camera.\n\n| `billboard` | behaviour |\n|---|---|\n| `false` (default) / `\"none\"` | no rotation — a flat plane in the world |\n| `true` / `\"cylindrical\"` | yaws to face the camera, stays upright (characters, trees) |\n| `\"spherical\"` | always fully faces the camera (particles, impostors) |\n\n`\"cylindrical\"` is what you want for paper-thin characters in a 2.5D game.\nBillboarding needs a `Camera3d` drawing the frame; under a 2D camera the quad\nrenders fixed-orientation. **Any other string falls through to the spherical\nbranch**, so a guessed value like `\"upright\"` silently gives you spherical.\n\nA billboard takes its **orientation** from the camera, so `rotate()` has no\neffect on one: a card already turned to face the camera has no free rotation\nleft to give. Everything that is not an orientation still applies, `scale()`\nand `meshScale` included, so animating the size of a shockwave or a pickup is\nthe ordinary call and the cull bounds grow with it:\n\n```js\nconst wave = new Sprite3d(x, y, { image: \"ring\", width: 64, height: 64,\n                                  billboard: \"spherical\", transparent: true });\nwave.scale(1 + 3 * t);          // grows; `scale` accumulates, so drive it\n                                 // from the authored size, not per frame\n```\n\nFor an in-plane rotation, a bolt streak or an exhaust, use `billboard: false`\nand orient the quad yourself.\n\n## Lighting\n\n`Light3d` takes **one argument — the options object** (it has no `x, y, z`\npair; a light carries its own `position`). Types are `\"directional\"` (the\ndefault), `\"ambient\"`, `\"point\"` and `\"spot\"`.\n\n```js\nworld.addChild(new Light3d({ type: \"directional\", direction: [0.3, 1, 0.2] }));\nworld.addChild(new Light3d({ type: \"ambient\", intensity: 0.3 }));\n// a Vector3d works too, wherever the game already has one\nworld.addChild(new Light3d({ type: \"spot\", position: torch.pos, range: 400 }));\n```\n\n`direction` and `position` take an `[x, y, z]` array **or** a `Vector3d`, and\n`color` takes a `Color`, a CSS string or an `[r, g, b]` array — so a value the\ngame already holds can go straight in. Each is read, not retained: move your\nvector afterwards and the light stays where it was.\n\nTwo things still bite, both silently:\n\n- `new Light3d(0, 0, {…})` — the constructor takes **options alone**. JavaScript\n  drops the extra arguments, so `options` becomes the number `0` and every\n  setting in your literal is discarded. The light still appears, on pure\n  defaults: a `type: \"ambient\"` written this way is a second DIRECTIONAL light,\n  and the scene looks plausible enough that nobody checks. TypeScript stops\n  checking a literal once the argument count is wrong, so nothing inside it is\n  verified either.\n- **`direction` is where the light GOES, and this is a Y-down space** — so a sun\n  overhead travels *downward* and its Y is **positive**. Get the sign wrong and\n  the scene is lit from underneath: faces that should be in shade are bright,\n  the ground glows and the sky-facing surfaces go dark.\n\n  ```js\n  direction: [-0.35, 0.8, 0.45]   // sun overhead, late afternoon\n  direction: [-0.35, -0.8, 0.45]  // lit from below — almost never what you want\n  ```\n\n  `position` follows the same convention: a lamp above the floor has a\n  **smaller** y than the floor.\n\nUse **both halves**: with a key light but no ambient, the shadow side of a mesh\ngoes black. With no `Light3d` in the world at all, a `lit: true` mesh falls back\nto a white ambient and renders fullbright — indistinguishable from unlit, which\nis why \"I added `lit: true` and nothing changed\" is the usual first report.\n\nOther defaults worth knowing: `intensity` 1, `color` white, `range` 1000 (point\nand spot, a stylised quadratic falloff, not inverse-square),\n`innerConeAngle` 0 and `outerConeAngle` π/4 (spot).\n\n**`Light2d` is 2D-only** and produces visible artifacts under perspective\nprojection. Do not combine it with `Camera3d`.\n\n**`lit: true` only does anything under a `Camera3d`.** Lighting a mesh needs\nworld-space normals and a world-space fragment position, and the 2D-camera path\nhas neither: its vertices are already the projected output, so there is no world\nspace left to light in. Such a mesh degrades to unlit and warns once, naming the\nfix. If you want lit 3D props in an otherwise 2D game, set\n`cameraClass: Camera3d` on the application. (For 2D **sprites**, lighting is a\nseparate system that works under a 2D camera as normal, see the lighting skill.)\n\nA `lit` mesh takes a **tangent-space normal map**, so relief comes from the\nmaterial rather than from geometry — `settings.normalMap` (a loader key or any\nimage-like source), or MTL `map_bump` / `bump` / `norm`, which the MTL loader\nfetches and applies per material. The tangent frame is derived per fragment, so\nthe model needs no tangent attribute, and an unmapped mesh is unaffected. This\nis the 3D counterpart of the `Sprite` normal maps in the lighting skill; they\nare separate systems and a `Mesh` uses this one.\n\n### What the ground-shadow tier can and cannot do\n\nCheck this table before claiming a setting is unsupported. Every one of these\nis read **every draw**, so all of them can be animated.\n\n| setting | `Mesh` / `Sprite3d` / `GLTFModel` part | `InstancedMesh` |\n|---|---|---|\n| `castGroundShadow` | yes | yes |\n| `shadowGroundY` | yes, the caster's own floor | yes, **one plane** through the SET's origin |\n| `shadowGroundPoint` | yes on a `Mesh` or `Sprite3d`; on a `GLTFModel` part only by setting it after the load, since it is neither a model nor a `level.load` option | **no**, warns and ignores |\n| `shadowGroundNormal` | yes | yes, one plane for the set |\n| `shadowOpacity` | yes | yes |\n| `shadowScale` | yes | yes |\n| `shadowOffset` | yes | yes, one throw for the set |\n| `shadowStretch` | yes | yes, one stretch for the set |\n| `shadowDirectionX` / `Z` | yes | yes |\n| `shadowLight` | yes | yes |\n| height fade as the caster rises | yes | **no** |\n| a different value per instance | n/a | **no**, and there is no plan for one |\n\nThe last two \"no\"s are the same limitation seen twice: a set gets ONE shadow\nplane and ONE shared quad, so the instance's own height is dropped and nothing\ncan vary between instances. Everything else is one value for the whole set,\nwhich is what a set means. A scatter over curved ground is therefore one set\nper flat facet, and a flying scatter wanting a height fade has to be\nper-object meshes.\n\n`shadowGroundPoint` (20.9) is the one genuinely per-object setting, and the\nreason is structural rather than missing work: the instanced tier builds its\nplane THROUGH the set's own origin so the translation's x and z drop out of\nthe intercept, which is then `groundY + lift / nY`. An arbitrary point does\nnot drop out, and that same `nY` divisor is why the 75 degree tilt ceiling\ncannot be lifted there either. `InstancedMesh` warns and ignores rather than\nstoring it, so a game cannot think it took.\n\nApart from that one, if a claim that a setting \"does not work on an\n`InstancedMesh`\" is in your head, it is out of date: `shadowOffset` and\n`shadowStretch` both reached the instanced tier in 20.8.\n\nGround shadows are **on by default** (the `castGroundShadow` application\nsetting), and need a GPU backend and a `Camera3d`. As a blanket default they\nskip geometry with no vertical extent — a ground plane. Per object,\n`castGroundShadow: true`/`false` overrides the app setting and is obeyed as\ngiven, safeguard included; `shadowGroundY` names the floor the blob lands on,\n`shadowOpacity` how dark it is and `shadowScale` how big it is.\n\nOn ground that is not flat, name the floor with `shadowGroundPoint` instead:\na point and `shadowGroundNormal` are a plane, and the blob leaves the caster's\nvertical column to land on it. `shadowGroundY` cannot express that, because\nthe blob is always directly below the caster in world Y, which is only \"below\"\non level ground. It is also what lifts the tilt ceiling, since that ceiling\nmeasures tilt from world up and so assumes a camera looking down the Y axis.\n\nThe height fade follows whichever floor you named. With a point **and** a\nnormal it is measured along that normal; with a point alone it is the\nstraight-line distance to the point. So on a sphere the blob fades with\nALTITUDE rather than with the vertical drop, which is the whole reason the\npoint exists. Two consequences worth knowing, because they fail in opposite\ndirections: a `shadowGroundNormal` facing inward makes the height negative, so\nthe fade clamps to full strength and the blob never fades at all; and a point\non the wrong side of the caster reads as a large height, so the blob\ndisappears outright rather than thinning to a ring the way a backwards\n`shadowGroundY` does. It is also the one shadow setting that THROWS rather\nthan clamping or hiding: a non-finite component is rejected on the spot.\n\nThe blob is an ellipse sized to the caster's own footprint and placed at the\ncaster's x/z, or at `shadowGroundPoint` when one is set. So a tall or narrow object (a character, a tree, a pickup) shows\nits shadow clearly, while a wide, flat-bottomed one resting on the floor covers\nits own completely from a camera looking down at it. That is the shadow\nbehaving correctly, not a bug.\n\n**Do not chase it by raising `shadowGroundY`.** Lifting the plane does not slide\nthe blob out from under the object, it floats the blob *up* — and past a few\nunits it projects over the top of the caster as a dark halo ringing it.\n\nThe simplest fix is a bigger blob, so its edge shows past the object:\n\n```js\nmesh.shadowScale = 1.5;        // 1 is the default; 0 hides the shadow\n```\n\n`shadowScale` works everywhere, `InstancedMesh` included. Changing it on a\nset rewrites the shared quad's four corners in place rather than rebuilding\nit, as does a `shadowLight` that moves, so an animated sun costs no\nallocation.\n\nOr, for the look of a sun that is not overhead, use the shape controls:\n\n```js\nmesh.shadowOffset = 1.5;       // slide it out, in blob radii\nmesh.shadowDirectionX = 1;     // the ground direction it is cast along\nmesh.shadowDirectionZ = 0;\nmesh.shadowStretch = 2;        // and lengthen it along the same line\n```\n\n`shadowOffset` is measured in multiples of the blob's **own radius**, not in\nworld units, so one value serves a boulder and a pebble and every part of a\nglTF model. About `stretch - 1` is what keeps the shadow attached to its\ncaster. It is honoured **only when the floor has been named**, by\n`shadowGroundY` or by `shadowGroundPoint`, because the\nblob is a flat quad on one named plane: sliding it across a plane the game has\nnot named puts it where there may be no floor. `shadowStretch` is clamped to 3\nand the blob fades as it pulls, so an extreme value degrades to nothing rather\nthan to a smear.\n\nAn `InstancedMesh` honours both, as **one** offset and **one** stretch for the\nwhole set, which is all a shared set can carry. Both ride the quad the blobs\nshare rather than the matrix that also places them: a stretch in the matrix\nwould scale the instance positions too and smear the scatter instead of\nlengthening its blobs. So a scatter can sit next to loose props under the same\nsun without looking lit from somewhere else.\n\n`shadowLight` takes the direction from a `Light3d` you name, re-read every\ndraw, so a moving sun carries the shadows with it. Nothing is inferred: the\nengine never picks a dominant light, because a scene with several has no\nnon-arbitrary answer and one with none has no answer at all.\n\n### A floor that is not level\n\n`shadowGroundY` says where the floor is; `shadowGroundNormal` says which way\nit faces:\n\n```js\nprop.shadowGroundY = floorY;\nprop.shadowGroundNormal = [Math.sin(0.35), -Math.cos(0.35), 0];  // a 20 deg slope\n```\n\nWorld up is `(0, -1, 0)`, because render space is Y-down, and that is the\ndefault. The blob is **rotated** onto the plane rather than projected, so it\nkeeps its size; it sits directly under the caster, since the normal turns it\nrather than moving it, unless `shadowGroundPoint` says where instead. It needs\nthe floor named, by `shadowGroundY` or `shadowGroundPoint`, for the same\nreason the offset does: tilting the bounds fallback has no anchor to turn\nabout.\n\nThe tilt is clamped at 75 degrees alongside `shadowGroundY`, past which an\nedge-on blob has nothing left to show. That ceiling measures tilt from WORLD\nup, which stands in for \"tilted away from the viewer\" only while the camera\nlooks down the Y axis, so it is lifted alongside `shadowGroundPoint`, where a\npoint and a normal state the plane outright.\n\nOn an `InstancedMesh` one plane serves the **whole set**: it passes through\nthe set's origin at `shadowGroundY`, faces the normal, and every blob is\nprojected onto it whatever height its own instance sits at, exactly as the\nlevel case puts every blob at one height. A scatter over curved ground is\ntherefore one set per flat facet, each with the facet's own plane, not one\nset with an averaged normal.\n\nAll three compose on a set: the stretch turns the shared quad, the offset\nslides the whole set along the light, and both are carried onto the tilted\nplane rather than applied flat, so a bank's planting lies along its slope\ninstead of hovering across it.\n\nGive blobs to the plants that stand apart. Dense low ground cover is better\nleft without: a thousand blobs each as wide as its own fern sum to a darkened\nhillside, not to a thousand shadows, and ground cover is the ground.\n\nThese are **art direction, not a projection**. A stretched ellipse is not a\nsilhouette and has no contact with terrain, so on ground that is not the plane\nyou named it will not lie on it. Direction-correct shadows want a shadow map,\nwhich this tier does not do.\n\n### Get the sign right: the floor is a GREATER y\n\nRender space is **Y-down**, so the floor an object stands on is a *larger* y\nthan the object itself. Nudging a shadow plane below a surface is therefore a\n**plus**:\n\n```js\nmesh.shadowGroundY = WATER_LEVEL + 8;   // just under the surface — correct\nmesh.shadowGroundY = mesh.pos.y - 8;    // eight units ABOVE its own base — wrong\n```\n\nGet it backwards and the blob becomes a horizontal quad slicing through the\ncaster's own body. It is still drawn at full strength — the height fade sees a\n*negative* height and clamps to 1, so nothing warns you — but the depth test\nhides every part of it that falls inside the caster's silhouette. What survives\nis a thin ring, only as wide as the blob overhangs the object.\n\nThe symptom is distinctive and easy to misread: **shadows appear to switch on as\ncasters approach the camera.** That ring is a few pixels on the nearest object\nand sub-pixel further out, so a scene reads as \"only nearby things have\nshadows\" — which looks like culling, an LOD stage or a fog problem, none of\nwhich it is. Before chasing any of those, dump `shadowGroundY` against the\ncaster's own y and check which one is larger.\n\n## Collision in 3D\n\nUse a body with a depth shape, not a hand-rolled distance check. Two shapes\ncarry depth — `Box3d` and `Sphere` — and any pair of them is resolved in three\ndimensions; the response carries `overlapNZ` / `overlapZ` for the depth axis.\n\nPick by whether the thing turns. `Box3d` is world-axis-aligned and cannot be\noriented, so it fits walls, floors and crates and misfits anything that\ntumbles. `Sphere` has no orientation at all, which makes it the shape for\nspinning debris, projectiles, and anything spread over a curved surface: on a\nglobe each object's own \"up\" points a different way, so one axis-aligned box\ncannot fit any of them.\n\nA hand-rolled check is the usual mistake here and it costs twice — it throws\naway the penetration vector the response already carries, and it is invisible\nto the debug plugin's hitbox overlay, which draws from `body.shapes`. If the\nred boxes never appear, the reason is usually that there is no body to draw.\n\nAgainst a planar shape, either 3D shape degrades to its XY footprint with the\nplanar side read as unbounded along Z, so a sphere or box body still collides\nwith an ordinary collision layer, just without a depth result. And mind the\nnormal: a box pair separates along one world axis, a sphere along the line\nbetween two centres, so a sphere contact fills the 2D and depth halves of the\nresponse at the same time. See the physics skill for the full contract.\n\n## glTF / GLB scenes\n\nLoaded through the same level director as everything else:\n\n```js\nawait loader.preload([{ name: \"diorama\", type: \"glb\", src: \"data/diorama.glb\" }]);\nlevel.load(\"diorama\", { scale, castGroundShadow, shadowGroundY, onLoaded });\n\n// every ground-shadow setting travels except shadowGroundPoint, which is\n// per object and dropped by a scene load: a loaded scene can\n// be told where its sun is, and `shadowLight` is re-read every draw\nlevel.load(\"diorama\", {\n    scale, castGroundShadow: true, shadowGroundY: 0,\n    shadowLight: sun, shadowOffset: 0.6, shadowStretch: 1.8, async: true,\n});\n```\n\n`.obj` / `.mtl` are also supported loader types.\n\nAn **animated** asset instantiates as a `GLTFModel`, which is a game object as\nmuch as a scene: construct it straight from the parsed descriptor, and place it\nwith `pos` / `depth` / `rotate` / `scale` like any other renderable — the\nplacement drives the whole rig and composes with the playing clip.\n\n```js\nconst model = new GLTFModel(loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nmodel.setCurrentAnimation(\"paddle\", { loop: true });\nmodel.pos.set(x, y);\nmodel.depth = z;\n```\n\nTo scatter an authored mesh yourself, read the geometry off the descriptor and\nhand it to an `InstancedMesh` — `loader.getGLTF(name).nodes[0]` carries\n`vertices` / `uvs` / `normals` / `indices`. That wants a single merged\nprimitive exported at the origin; see `melonjs-3d-assets`.\n\n`melonjs-3d-assets` covers the rest: every `level.load` option, which material\nfeatures are imported, imported lights and their i","packages/melonjs/skills/melonjs-getting-started/SKILL.md":"---\nname: melonjs-getting-started\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Getting started with melonJS\n\n> Bootstrapping a melonJS 20.x game: the `Application` lifecycle, the mandatory\n> `await app.init()`, the world container, and what the renderer fallback means\n> for feature availability.\n\n**Applies to:** melonJS 20.x. Older major versions differ — see the\n`melonjs-20-migration` skill.\n\n## The bootstrap\n\n`Application` construction is synchronous but **initialisation is not**. `init()`\nreturns a promise and must be awaited before building any renderable, loading\nassets, or entering a scene.\n\n```js\nimport { Application, Text } from \"melonjs\";\n\nconst app = new Application(1218, 562, {\n    parent: \"screen\",      // id of the DOM element to mount into\n    scale: \"auto\",\n    backgroundColor: \"#202020\",\n});\nawait app.init();          // ← REQUIRED. Nothing below works without it.\n\napp.world.addChild(\n    new Text(609, 281, {\n        font: \"Arial\",\n        size: 160,\n        fillStyle: \"#FFFFFF\",\n        textAlign: \"center\",\n        textBaseline: \"middle\",\n        text: \"Hello World !\",\n    }),\n);\n```\n\n**This is the single most common thing to get wrong.** On 19.x the constructor\ncalled `init(width, height, options)` for you; on 20.x `init()` takes no\narguments and calling — and awaiting — it is your job. Nothing that follows\nnames the missing call. The constructor does build `app.world`, so adding a\nplain renderable succeeds and simply never draws — no canvas is appended and the\napp never subscribes to the frame tick — while constructing a `Sprite` or a\n`Text` throws `Cannot read properties of undefined (reading 'renderer')`,\nbecause the global `game` those two fall back on is only set once the first\n`init()` resolves. Either way `app.renderer` and `app.viewport` stay `undefined`\nand the page stays blank.\n\n`init()` is asynchronous because acquiring a WebGPU device is asynchronous. There\nis no synchronous alternative.\n\n## Top-level await\n\nThe bootstrap needs `await` at module scope. In an ES module that is fine. If the\nsurrounding tooling does not allow it, wrap it:\n\n```js\nasync function main() {\n    const app = new Application(800, 600, { parent: \"screen\" });\n    await app.init();\n    // …\n}\nvoid main();\n```\n\n## Choosing a renderer\n\n`video.AUTO` (the default) tries **WebGPU → WebGL 2 → Canvas** and uses the first\nthat works.\n\n```js\nimport { video } from \"melonjs\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    renderer: video.AUTO,      // default; or video.WEBGPU / video.WEBGL / video.CANVAS\n});\n```\n\nTwo consequences worth knowing before writing rendering code:\n\n- **Which backend you get is a runtime fact, not a build-time one.** The same\n  code runs on a different renderer depending on the machine. Do not assume\n  WebGL.\n- **Not every feature exists on every backend.** Shader effects and post effects\n  need a programmable pipeline and are inert on Canvas. Where a feature degrades,\n  the engine warns once rather than throwing — so a missing effect is a console\n  warning, not a crash.\n\nPin the renderer explicitly (`video.WEBGL`) only when you have a reason to. It\ncosts you the WebGPU path and the Canvas fallback.\n\n## The scene graph\n\n`app.world` is the root `Container`. Everything drawn is a descendant of it.\n\n```js\napp.world.addChild(sprite, 10);   // 10 is the z / draw order\n```\n\n**Pass the z-order as the second argument to `addChild`** — it is the only form\nthat works. `renderable.z` does not exist as a property, setting `depth` before\n`addChild` is overwritten by `autoDepth`, and setting it afterwards does not\nresort. See the `melonjs-renderables` skill for the full explanation.\n\n## Scenes\n\nFor anything beyond a single screen, use `Stage` subclasses and the `state`\nmanager rather than mutating `app.world` directly:\n\n```js\nimport { Stage, state } from \"melonjs\";\n\nclass PlayScreen extends Stage {\n    onResetEvent() { /* build the scene */ }\n    onDestroyEvent() { /* tear it down */ }\n}\n\nstate.set(state.PLAY, new PlayScreen());\nstate.change(state.PLAY);\n```\n\n`onResetEvent` runs every time the state is entered, not once — build scene\ncontent there, not in the constructor.\n\n## Loading assets\n\nAssets are declared and preloaded through `loader`; audio needs `audio.init()`\nwith the formats you ship before any audio asset is preloaded.\n\n```js\nimport { audio, loader } from \"melonjs\";\n\nloader.setOptions({ crossOrigin: \"anonymous\" });   // needed for remote assets\naudio.init(\"mp3,ogg\");                             // BEFORE preloading audio\nawait loader.preload([\n    { name: \"tileset\", type: \"image\", src: \"data/img/tileset.png\" },\n    { name: \"jump\",    type: \"audio\", src: \"data/sfx/\" },   // directory, not file\n]);\n```\n\nTwo ordering rules that produce confusing failures when missed:\n\n- **`audio.init()` must run before any audio asset is preloaded**, and names the\n  formats you actually ship. Preloading first throws *\"target audio extension(s)\n  should be set through me.audio.init() before calling the preloader\"*.\n- The audio `src` is a **directory**, not a file. The loader appends the asset\n  `name` plus each configured extension, so a full filename gives you a 404.\n\n## `anchorPoint` defaults to the centre\n\n`Renderable.anchorPoint` is `(0.5, 0.5)`, so `pos` is an object's **centre**, not\nits top-left. `Container` forces `(0, 0)` instead.\n\nWriting top-left math against a centred anchor puts everything half a sprite off\nwith no error — it is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Common mistakes\n\n| symptom | cause |\n|---|---|\n| blank page, nothing drawn, or a `TypeError` reading `renderer` from `new Sprite` / `new Text` | missing `await app.init()` |\n| objects draw in the wrong order | `.z`/`depth` set instead of `addChild(child, z)` — `.z` is not even a real property |\n| shader/post effect does nothing, warning in console | running on the Canvas renderer |\n| audio 404s | audio asset `src` given as a file rather than a directory |\n| scene content appears once and never resets | scene built in the constructor instead of `onResetEvent` |\n| everything offset by half a sprite | top-left math against the centred `anchorPoint` default |\n| a `Tween` or `ParticleEmitter` does nothing | `.start()` / `streamParticles()` never called |\n| cross-origin textures fail to load | missing `loader.setOptions({ crossOrigin: \"anonymous\" })` |\n\n## Related skills\n\n- `melonjs-20-migration` — what changed in 20.x that older code and older\n  training data get wrong\n- `melonjs-renderables` — custom `Renderable` subclasses, pointer events, post\n  effects and the traps in each\n","packages/melonjs/skills/melonjs-20-migration/SKILL.md":"---\nname: melonjs-20-migration\ndescription: \"Use this skill when writing melonJS code that may be based on pre-20.x knowledge, when migrating a game from melonJS 19.x or earlier, or when an API you expected does not exist. Covers the mandatory async Application.init(), the WebGL 2 baseline and WebGPU backend, renderable post effects replacing the shader property, and the deprecated exports still present. Triggers on: migration, upgrade, melonJS 19, me.game, me.video.init, renderable.shader, Compositor, CanvasTexture, me.Math, deprecated.\"\nlicense: MIT\n---\n\n# Migrating to melonJS 20.x\n\n> What changed in melonJS 20.x, and which older APIs are gone, deprecated, or\n> renamed. Read this before writing melonJS code from memory — much of what is\n> published about melonJS predates 20.0.\n\n**Why this skill exists:** most melonJS material in circulation describes 19.x or\nearlier. Code written from that knowledge looks correct and fails at runtime.\nThe items below are ordered by how likely they are to be reproduced from memory.\n\n## 1. `Application.init()` is mandatory and asynchronous\n\nSince 20.0, constructing an `Application` no longer initialises it.\n\n```js\n// ✗ pre-20.x — the constructor called init(width, height, options) for you\nconst app = new Application(800, 600, { parent: \"screen\" });\napp.world.addChild(sprite);          // added, but nothing ever renders\n\n// ✓ 20.x\nconst app = new Application(800, 600, { parent: \"screen\" });\nawait app.init();                    // REQUIRED\napp.world.addChild(sprite);\n```\n\n`init()` is async because acquiring a WebGPU device is async. There is no\nsynchronous alternative, and no automatic fallback that papers over the missing\ncall. Nothing that then breaks names it: the constructor still builds\n`app.world`, so children can be added and simply never draw — no canvas is\nappended, the app never subscribes to the frame tick, and `app.renderer` /\n`app.viewport` stay `undefined` — while constructing a `Sprite` or `Text` first\nthrows a `TypeError` on the unset global `game` instead.\n\nThe signature changed too: 19.x took `init(width, height, options)`, 20.x takes\nnone, and anything passed is ignored (settings come from the constructor).\n\n**`video.init(...)` no longer exists.** The `video` module now exports only the\nfour renderer constants (`AUTO`, `CANVAS`, `WEBGL`, `WEBGPU`) — there is no\n`init` on it to call. Any code shaped like `me.video.init(800, 600, {...})` is\nfrom a removed API. Build an `Application` and await `init()`.\n\nNote that `import * as me from \"melonjs\"` still works, so `me.Sprite` style is\nfine. It is the *globals bootstrap* that is gone, not the namespace import.\n\n## 2. Post effects replaced the `shader` property\n\n`renderable.shader = …` is **deprecated since 19.2.0**. Use the post-effect API:\n\n```js\n// ✗ deprecated\nmySprite.shader = new ShaderEffect(app.renderer, glslBody);\n\n// ✓\nmySprite.addPostEffect(new ShaderEffect(app.renderer, glslBody));\n```\n\nRelated trap: **`removePostEffect()` destroys the effect** — and so does\nassigning `renderable.shader`, which destroys whatever it replaces. Only an\neffect carrying `shared = true` is spared. To turn one off temporarily, toggle\n`effect.enabled` instead: remove-and-re-add hands you back an object whose GPU\nresources have already been freed.\n\n## 3. WebGL 2 is the baseline; WebGPU is the default attempt\n\n`video.AUTO` now tries **WebGPU → WebGL 2 → Canvas**. WebGL 1 is gone.\n\nConsequences:\n\n- Which backend runs is a runtime fact. Do not write code that assumes WebGL.\n- Custom shaders can carry both GLSL and WGSL in one asset, so one effect runs\n  on either GPU backend.\n- Features needing a programmable pipeline are inert on Canvas and warn once\n  rather than throwing.\n\n## 4. Deprecated exports still present\n\nThese resolve but should not be used in new code:\n\n| deprecated | since | use instead |\n|---|---|---|\n| `CanvasTexture` | 17.1.0 | `CanvasRenderTarget` |\n| `Compositor` | 18.1.0 | `WebGLBatcher` |\n| `PrimitiveCompositor` | 18.1.0 | `PrimitiveBatcher` |\n| `QuadCompositor` | 18.1.0 | `QuadBatcher` |\n| `Math` (capitalised) | 18.0.0 | `math` (lowercase) |\n| `device.requestFullscreen` / `exitFullscreen` | 19.7.0 | `app.requestFullscreen()` / `app.exitFullscreen()` |\n| `renderable.shader = …` | 19.2.0 | `addPostEffect()` |\n| `Entity` | 18.1.0 | `Sprite` / `Renderable` + a `Body` |\n| `loader.onload` / `onProgress` / `onError` | 18.2.0, **removed 20.3** | the `LOADER_*` events, or `preload(assets, onloadcb)` |\n| `response.overlap` / `overlapN` / `overlapV` | — | `response.depth` / `response.normal` |\n| `setLineWidth()` | 17.3.0 | the `lineWidth` property |\n\nTwo of these deserve emphasis:\n\n- **`Entity`** is still exported, so code using it compiles and runs while being\n  the wrong shape for 20.x. Compose a `Sprite`/`Renderable` with a `Body`\n  instead.\n- **The capitalised `Math`** shadows the global `Math` if imported carelessly.\n  `me.Math.random()` from older docs resolves to the deprecated re-export.\n\n## 5. The global `game` is not the modern shape\n\n`game` is still exported, but since 20.0 it names the most recently\n**initialized** `Application` and is `undefined` until the first `init()`\nresolves — so anything reading it at module scope gets `undefined`. Take the\n`Application` instance as a parameter instead: `Stage#onResetEvent` and\n`onDestroyEvent` are handed it, and any renderable can reach it through\n`parentApp`.\n\n## 6. Node and SSR\n\nmelonJS imports cleanly under Node — useful for isomorphic apps and for tooling.\nIt has **no runtime dependencies** as of 20.3. Do not add polyfills for\n`globalThis`, `String.trimStart` or `String.trimEnd`: the published bundle\ntargets ES2022, so every environment that can parse it already has them.\n\n## 7. Browser support\n\nThe published bundle targets **ES2022** and uses private class members. A browser\nthat predates that cannot parse the file at all — it fails at load, not when a\nfeature is used. Transpile if you need older browsers, and note that transpiling\nhandles *syntax* only: built-in methods need separate polyfills, which a\nbundler's `target` setting will never add.\n\n## The shipped examples are not all current\n\nThe examples in this repository are the best reference for how melonJS is\nassembled, but a few contain stale comments or legacy calls. Do not learn these\nfrom them:\n\n- `webgpu/` has a comment claiming *\"`video.AUTO` never selects the WebGPU\n  backend\"*. That is pre-20 leftover — AUTO tries WebGPU **first**. The code is\n  right; the comment is wrong.\n- Several examples assign `viewport.shader = …` / `sprite.shader = …`, which is\n  deprecated. `addPostEffect()` is current.\n- Five files set `this.z = Number.POSITIVE_INFINITY` on a HUD. That property\n  does not exist; those HUDs draw on top only because they are added last.\n- `platformer/entities/player.ts` reads `response.overlapV`, the deprecated\n  field. Use `response.depth` / `response.normal`.\n- Particle speeds tuned before 20.2 look wrong now: the particle transform fix\n  changed how far a burst travels, and emitters were retuned upward to match.\n\n## Checklist when code \"should work\" but doesn't\n\n1. Is `await app.init()` there?\n2. Is the code assuming WebGL when it might be running on Canvas or WebGPU?\n3. Is it using `renderable.shader =` instead of `addPostEffect()`?\n4. Is it importing a deprecated symbol from the table above?\n5. Is it referencing a global `game`?\n\n## Related skills\n\n- `melonjs-getting-started` — the correct 20.x bootstrap in full\n- `melonjs-renderables` — post effects, pointer events and custom draw code\n","packages/melonjs/skills/melonjs/SKILL.md":"---\nname: melonjs\ndescription: \"Use this skill FIRST for any melonJS task; it routes to the right specialised skill and lists the companion packages. melonJS is an open-source 2D, 2.5D and 3D HTML5 game engine running on WebGPU, WebGL 2 or Canvas. Covers Application setup, the scene graph, renderables, sprites, tilemaps, physics, audio, particles, shaders, and the 3D tier — perspective cameras, meshes and glTF scenes. Triggers on: melonjs, melonJS, me.game, Application, app.init, app.world, Renderable, Sprite, Stage, state.change, Camera2d, Camera3d, Mesh, tilemap, Tiled, TMX, ParticleEmitter, ShaderEffect, how do I make a game, HTML5 game engine.\"\nlicense: MIT\n---\n\n# melonJS\n\nEntry point for the melonJS skill collection. melonJS is an open-source HTML5\ngame engine for indie developers, covering 2D, 2.5D and 3D — perspective and\northogonal cameras, GPU-accelerated tilemap rendering, post-processing effects,\ncustom shaders, 3D meshes, polygon-accurate physics and modern Tiled workflows.\nIt runs on WebGPU, WebGL 2 or Canvas with automatic fallback, and has no runtime\ndependencies.\n\n## How to use this skill\n\n1. Find the specialised skill below that matches the task and follow it.\n2. If the task involves anything 3D, read `melonjs-3d` **first** — melonJS's\n   coordinate conventions are the inverse of OpenGL's, so untutored 3D instincts\n   are backwards.\n3. If you are writing melonJS from prior knowledge rather than from the current\n   docs, read `melonjs-20-migration` — most published melonJS material predates\n   20.0 and its bootstrap no longer exists.\n4. If none of them covers the task, use the escape hatch below rather than\n   guessing at an API.\n\n## Skill router\n\n| Skill | Load when… |\n|---|---|\n| [melonjs-getting-started](../melonjs-getting-started/SKILL.md) | Bootstrapping a game, `new Application`, `await app.init()`, choosing a renderer, preloading assets. |\n| [melonjs-20-migration](../melonjs-20-migration/SKILL.md) | Code based on melonJS 19.x or earlier, or an API that \"should exist\" but doesn't — `video.init`, `Entity`, `renderable.shader =`. |\n| [melonjs-scenes-and-state](../melonjs-scenes-and-state/SKILL.md) | `Stage` subclasses, the state manager, transitions, pausing, the update loop, timers, tweens, saved data. |\n| [melonjs-renderables](../melonjs-renderables/SKILL.md) | Subclassing `Renderable`, custom `draw()`, draw order, anchors, `floating`, post effects on an object. |\n| [melonjs-sprites-and-animation](../melonjs-sprites-and-animation/SKILL.md) | `Sprite`, frame animation, texture atlases (TexturePacker / Aseprite / ShoeBox), tint, flip, pooling. |\n| [melonjs-input](../melonjs-input/SKILL.md) | Keyboard actions, pointer and touch handling, drag, gamepad, virtual controls. |\n| [melonjs-physics](../melonjs-physics/SKILL.md) | Collision, bodies, movement, spatial queries, and the planck / matter adapters. |\n| [melonjs-math](../melonjs-math/SKILL.md) | Vectors and matrices: what mutates, what allocates, and interpolating with lerp, slerp, damp or moveTowards. |\n| [melonjs-tilemaps](../melonjs-tilemaps/SKILL.md) | Tiled maps — TMX/TSX loading, spawning entities from objects, collision layers, isometric maps. |\n| [melonjs-audio](../melonjs-audio/SKILL.md) | Sound effects, music, audio sprites, spatial audio, procedural tone and noise. |\n| [melonjs-effects-and-shaders](../melonjs-effects-and-shaders/SKILL.md) | Post effects, custom GLSL/WGSL shaders, blend modes, colour grading, screen capture. |\n| [melonjs-3d](../melonjs-3d/SKILL.md) | Anything 3D or 2.5D — `Camera3d`, meshes, instancing, `Sprite3d` billboards, `Light3d`, distance fog, glTF scenes. |\n| [melonjs-3d-assets](../melonjs-3d-assets/SKILL.md) | Loading glTF/GLB or OBJ models — materials, imported lights, instancing, and what is not supported. |\n| [melonjs-camera-and-drawing](../melonjs-camera-and-drawing/SKILL.md) | Camera follow, bounds, shake and fade, coordinate conversion, and immediate-mode shape drawing. |\n| [melonjs-ui-and-text](../melonjs-ui-and-text/SKILL.md) | HUDs, buttons, menus, drag-and-drop, `Text` and `BitmapText`, web fonts, panels. |\n| [melonjs-particles-and-trails](../melonjs-particles-and-trails/SKILL.md) | `ParticleEmitter`, burst versus stream, `referenceSpace`, and the `Trail` renderable. |\n| [melonjs-lighting](../melonjs-lighting/SKILL.md) | 2D dynamic lighting — `Light2d`, stage ambient, normal-mapped sprites. |\n| [melonjs-deployment](../melonjs-deployment/SKILL.md) | Scaffolding a project, building, browser support and transpiling, native and mini-game platforms. |\n| [melonjs-loading-assets](../melonjs-loading-assets/SKILL.md) | Resource descriptors, the asset type list, base URLs, cross-origin, progress events, unloading. |\n| [melonjs-renderer-backends](../melonjs-renderer-backends/SKILL.md) | A feature works on one machine but not another; choosing or pinning a backend; degrading gracefully. |\n| [melonjs-display-and-scaling](../melonjs-display-and-scaling/SKILL.md) | Blurry, soft or cropped output; the design resolution, how the canvas fills its page, and pixel-perfect integer scaling. |\n| [melonjs-events](../melonjs-events/SKILL.md) | The event bus, per-frame and lifecycle events, and subscribing without leaking. |\n| [melonjs-performance](../melonjs-performance/SKILL.md) | Dropped frames, draw-call counts, pooling, batching, culling, baking, profiling. |\n| [melonjs-plugins](../melonjs-plugins/SKILL.md) | Registering the debug, Spine or Tiled-inflate plugins, physics adapters, or writing your own plugin. |\n\n\n## Escape hatch: the full API index\n\nThese skills are hand-written and deliberately partial — they cover the paths\npeople actually take and the mistakes those paths invite. When the task needs\nsomething they do not mention, do **not** infer an API from the shape of the\nengine. Fetch the generated index instead:\n\n<https://melonjs.github.io/melonJS/llms.txt>\n\nIt is regenerated on every docs build from the same TSDoc comments the reference\npages render, so it is always current for `master`. It lists every exported\nclass, function, interface, type and namespace with a one-line summary and a\nlink to its page — fetch the pages you need from there.\n\nTwo things it gives you that this collection cannot:\n\n- **Members.** The skills name classes; `llms.txt` links to the page listing\n  every method, property, default and `@since` tag on them.\n- **Deprecations.** Entries marked `**deprecated**` are exactly the APIs that a\n  model trained on older melonJS material will reach for. If something you were\n  about to write appears with that marker, follow the page for the replacement.\n\nIf a class, function, namespace or type is absent from `llms.txt`, it is absent\nfrom the engine's public API — that is the useful negative answer, and it is\nworth acting on rather than writing code around something that does not exist.\nThe index lists top-level exports only, so a *member* missing from it proves\nnothing: follow the owning class's page to check that.\n\n## The three rules that catch everyone\n\nWhatever the task, these produce code that runs and is wrong:\n\n1. **`await app.init()` is mandatory** since 20.0. The constructor builds the\n   world but not the renderer: a plain renderable can still be added and simply\n   never draws, while `new Sprite(...)` / `new Text(...)` throw a `TypeError`\n   on the global `game`, which is unset until the first `init()` resolves.\n   Neither symptom names the missing call.\n2. **`addChild(child, z)` is the only way to set draw order.** `renderable.z`\n   does not exist; setting `depth` before `addChild` is overwritten, and setting\n   it after does not resort (except under `Camera3d`).\n3. **`isKinematic` defaults to `true`**, which silently excludes a renderable\n   from pointer events and from the physics broadphase. Plain `Renderable` and\n   `Sprite` subclasses need `this.isKinematic = false`.\n\n## Companion packages\n\nmelonJS ships optional packages alongside the engine. Reach for these rather\nthan reimplementing what they cover — and note the physics adapters and the\nTiled inflate plugin are *required* for their use cases, not merely convenient.\n\n| Package | Use when |\n|---|---|\n| [`@melonjs/planck-adapter`](https://www.npmjs.com/package/@melonjs/planck-adapter) | You want full rigid-body dynamics — stacking, joints, realistic restitution. Pass it as the Application's `physic` setting; the portable `raycast` and `queryAABB` calls work unchanged across adapters (`querySphere` and `raycast3d` are built-in-adapter only). |\n| [`@melonjs/matter-adapter`](https://www.npmjs.com/package/@melonjs/matter-adapter) | Same, backed by matter-js. Choose one adapter per game. |\n| [`@melonjs/tiled-inflate-plugin`](https://www.npmjs.com/package/@melonjs/tiled-inflate-plugin) | **Required** to load gzip-, zlib- or zstd-compressed Tiled maps. Without it, parsing such a map throws `No inflate function set`. |\n| [`@melonjs/spine-plugin`](https://www.npmjs.com/package/@melonjs/spine-plugin) | Skeletal animation authored in Spine, as a melonJS renderable. |\n| [`@melonjs/debug-plugin`](https://www.npmjs.com/package/@melonjs/debug-plugin) | An in-game debug panel — FPS, draw counts, collision-shape and bounds overlays. Register it after `app.init()`. |\n| [`@melonjs/capacitor-plugin`](https://www.npmjs.com/package/@melonjs/capacitor-plugin) | Wrapping a game as a native iOS/Android app: it bridges Capacitor's lifecycle — pause/resume, the hardware back button, orientation lock, splash screen. |\n| [`create-melonjs`](https://www.npmjs.com/package/create-melonjs) | Scaffolding a new project — start here rather than assembling a build by hand. |\n\nPlugins are registered **after** `init()`, into a global registry:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { DebugPanelPlugin } from \"@melonjs/debug-plugin\";\n\nawait app.init();\nplugin.register(DebugPanelPlugin, \"debugPanel\");\n```\n\nPhysics adapters are different — they are an `Application` setting, not a\nplugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    // gravity is in pixels/s²; pixelsPerMeter converts it to the adapter's\n    // metres. Both are shown at their defaults; subSteps defaults to 1\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },\n        pixelsPerMeter: 32,\n        subSteps: 2,\n    }),\n});\nawait app.init();\n```\n\n`melonjs-plugins` covers the registry, the version gate and writing your own.\n\n## Prefer built-in features\n\nmelonJS covers a lot that is easy to reimplement badly. Before hand-rolling,\ncheck for: `ParticleEmitter` and `Trail`, `Tween` with easing, the built-in\n`ShaderEffect` presets (vignette, scanline, chromatic aberration, glow, dissolve\nand more), `Light2d` / `Light3d`, `UIBaseElement` / `UISpriteElement` /\n`UITextButton`, `NineSliceSprite`, `TextureAtlas` (TexturePacker, ShoeBox,\nAseprite), `BitmapText`, `CanvasRenderTarget` for bake-once drawing,\n`NoiseTexture2d`, `viewport.shake` / `fadeIn` / `fadeOut`, and `save` for\nlocalStorage-backed persistence.\n","packages/melonjs/skills/melonjs-renderables/SKILL.md":"---\nname: melonjs-renderables\ndescription: \"Use this skill when subclassing Renderable, writing a custom draw() method, handling pointer or click events on game objects, applying post effects and shaders, or controlling draw order. Covers the pos-relative draw contract, isKinematic and input, addPostEffect vs the deprecated shader property, and z-ordering. Triggers on: Tween, easing, animate, Renderable, extends Renderable, draw(, update(, isKinematic, pointerEvent, registerPointerEvent, onClick, addPostEffect, removePostEffect, ShaderEffect, addChild, z-order, anchorPoint, floating.\"\nlicense: MIT\n---\n\n# Renderables, input and effects\n\n> Subclassing `Renderable`, custom drawing, receiving pointer events, and\n> applying post effects — with the traps that produce silently wrong output\n> rather than an error.\n\nEverything here is a **silent failure**: the code runs, nothing throws, and the\nresult is wrong. Those are the cases worth memorising, because nothing in the\ntype signatures warns you.\n\n## 1. A custom `draw()` must position itself from `this.pos`\n\nThe renderer is **not** pre-translated to the renderable's position when `draw()`\nis called. `Container.draw` translates by the *container's* own `pos` and then\ncalls `child.preDraw` / `draw` / `postDraw`; `preDraw` never translates by the\nchild's `pos`. Read `this.pos` and draw relative to it.\n\n```js\nclass Marker extends Renderable {\n    draw(renderer) {\n        // ✗ draws at the parent container's origin, not at the marker\n        renderer.fillRect(0, 0, this.width, this.height);\n\n        // ✓\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\nThe symptom is an object that renders correctly at position (0, 0) and appears\nstuck at the top-left as soon as you move it.\n\n**Zero the anchor in custom-draw renderables.** `preDraw` does not translate to\n`pos`, but it *does* apply the anchor offset `(-width * anchorPoint.x,\n-height * anchorPoint.y)`. With the default centred anchor, drawing at\n`this.pos` therefore lands half a size off:\n\n```js\nconstructor(x, y, w, h) {\n    super(x, y, w, h);\n    this.anchorPoint.set(0, 0);   // ← or every custom draw is offset by -w/2, -h/2\n}\n```\n\n## 2. `isKinematic` must be `false` to receive input or collisions\n\n`Renderable.isKinematic` **defaults to `true`**, which means *\"physics collision\nand input events will not impact this renderable\"*. A kinematic renderable is\nskipped by the pointer-event dispatcher entirely.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y) {\n        super(x, y, 100, 40);\n        this.isKinematic = false;          // ← required, or clicks never arrive\n        input.registerPointerEvent(\"pointerdown\", this, () => { /* … */ });\n    }\n}\n```\n\nThe symptom is a handler that is registered, looks correct, and never fires.\n\nNote that `Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*`\nelements set it to `false` for you, and so does the `Body` constructor on its\nowner (which covers `Entity` and anything you give a body to) — so a plain\n`Renderable` or `Sprite` subclass with no body is the case that catches people.\n\nThe same flag has a second, less obvious effect: it is also the opt-in to the\nworld **broadphase**. The quadtree/octree insert skips any child with\n`isKinematic === true`, so body-less objects you want `adapter.queryAABB` /\n`querySphere` to find need `isKinematic = false` as well. The builtin\nnarrowphase likewise requires *both* partners to be non-kinematic.\n\n## 3. Draw order: only `addChild(child, z)` works\n\nThere are three separate traps here and they compound.\n\n```js\nworld.addChild(sprite, 10);   // ✓ the only reliable form\n```\n\n1. **`renderable.z` does not exist.** There is no `z` accessor anywhere on\n   `Renderable` (nor on `Rect`/`Polygon` above it) — only `depth`, which proxies\n   `pos.z`, and the container's default `sortOn: \"z\"` comparator reads `pos.z`.\n   Writing `sprite.z = 100` creates an inert property that nothing reads.\n   Several shipped examples still contain `this.z = Number.POSITIVE_INFINITY`;\n   those HUDs draw on top only because they happen to be added last. (A `z` key\n   in a `Sprite`/`ImageLayer` *settings* object is a different thing and is real\n   — it assigns `pos.z` in the constructor — but trap 2 then overwrites it.)\n2. **Setting `depth` *before* `addChild` is overwritten.** With `autoDepth` on\n   (the default), `addChild` without an explicit `z` assigns\n   `child.pos.z = children.length` after the push, discarding whatever you set.\n3. **Setting `depth` *after* `addChild` does not reorder.** A `\"z\"` sort runs\n   when a child is added (`autoSort`, deferred to the next tick), not when\n   `pos.z` changes. If you must change it later, call `container.sort()`\n   yourself, or use `moveToTop` / `moveUp` / `moveDown` / `moveToBottom`, which\n   reorder the children themselves. (An `\"x\"` / `\"y\"` sort is different: see\n   below.)\n\n### Sorting on `\"y\"` (or `\"x\"`), and `sortOffset`\n\nA container with `sortOn = \"y\"` draws its children from the smallest `pos.y` to\nthe largest, so whatever is lower on screen covers what is above it (top-down and\nisometric games). `\"x\"` does the same along x. A different `pos.z` still decides\nfirst, so keep the children that should interleave at one z.\n\n- **Moving a child re-sorts by itself.** With `autoSort` on (the default), the\n  container checks its order at the end of every update, after physics and the\n  children's own updates, and re-sorts when anything is out of place: a walking\n  character needs no `sort()` call. A frame where nothing moved costs one pass\n  over the children and does not cause a redraw. Turn `autoSort` off to keep\n  the children in the order you put them.\n- **Ties keep insertion order, and the child added LAST is drawn BEHIND.** A\n  torch placed at its wall's exact position disappears behind the wall.\n\n`sortOffset` changes the ORDER only, never where anything is: the child is lined\nup as if it stood that many pixels lower (positive) or higher (negative), and is\ndrawn exactly where it was. Use it when `pos` is not the point to sort by:\n\n```js\ntorch.sortOffset = 1;                        // win the tie with the wall it hangs on\ndust.sortOffset = door.pos.y - dust.pos.y;   // an emitter's pos is the top of its\n                                             // spawn area: line it up with the door\nprop.sortOffset = prop.height;               // sort by the bottom of the image\n```\n\nA container that keeps itself sorted picks a change up at its next update (with\n`autoSort` off, call its `sort()`). `\"z\"` and `\"depth\"` sorting ignore it. It must be a finite number (a string\nor `NaN` throws, rather than scrambling every sibling), and a pooled renderable\nstarts again at `0`.\n\nThe exception: under `Camera3d` the world's `sortOn` is `\"depth\"`, and the\ncamera calls `container.sortNow(true)` every frame. Note that mode does **not**\nsort by `pos.z` as a layer index — it sorts by squared distance from the camera\nin world space (nearest first in the array, painted far→near). So changing\n`depth` does reorder there, but as a position change, not as a layer number.\n\n```js\n// ✗ overwritten by autoDepth\nsprite.depth = 10;\nworld.addChild(sprite);\n\n// ✗ no resort happens (except under Camera3d)\nworld.addChild(sprite);\nsprite.depth = 10;\n\n// ✓\nworld.addChild(sprite, 10);\n```\n\n## 4. `removePostEffect()` destroys the effect\n\nTo disable an effect temporarily, toggle it. Removing it frees its GPU\nresources and leaves you holding a dead object.\n\n```js\n// ✗ effect is destroyed; re-adding it will not work\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nException worth knowing: an effect with `effect.shared === true` opts out of\nauto-destroy, because it is reused across renderables.\n\nUse `addPostEffect()` / `getPostEffect()` / `removePostEffect()`. Assigning\n`renderable.shader = …` is deprecated since 19.2.0.\n\nPost effects need a programmable pipeline. A `ShaderEffect` constructed against\nthe Canvas renderer (`renderer.shaderLanguage === null`) logs a warning, stays\nat `enabled === false` and no-ops every method rather than throwing — and\n`beginPostEffect` filters disabled effects out. Since `video.AUTO` picks the\nbackend at runtime, an effect that works on your machine may do nothing on\nanother.\n\n## 5. Opacity cascades; `setChildsProperty` assigns\n\n`alpha` composes down the tree. Fading a container fades everything inside\nit, and the values multiply — a child at `0.5` inside a parent at `0.5` draws\nat `0.25`. Each renderable keeps its own value; the composition happens at\ndraw time and is undone when the renderable is finished.\n\n```js\nmodel.alpha = 0.3;                             // fades the whole rig\ncontainer.setChildsProperty(\"alpha\", 0.3, true); // REWRITES each child's alpha\n```\n\nThe two are not interchangeable. `setChildsProperty` is a bulk assignment: it\nchanges what the children *are*, so their own `alpha` now reads `0.3` and\nstays that way after the container goes back to full. Reach for it when you\nmean to change the children, and for `alpha` when you mean to fade a subtree.\n\nDo not assign alpha down a hierarchy by hand. The renderer composes it as it\nwalks the tree, so setting it at more than one level multiplies: in a rig\nnested three deep, `0.3` applied at every level lands parts on `0.3`, `0.09`\nand `0.027` depending where they sit. The symptom does not look like an\nopacity problem — a model whose parts hold *different* opacities, and which\nappears to change opacity as it turns, because turning changes which parts\nface the camera.\n\nA useful consequence: a `GLTFModel` is a `Container`, so `model.alpha` fades\nevery part of a loaded rig — which is what you want for a ghost, a fade-in,\nor a reflection.\n\n## 6. `Vector3d.set(x, y)` silently zeroes `z`\n\n```js\nv.set(10, 20);        // z becomes 0, not \"unchanged\"\nv.set(10, 20, v.z);   // keep it\n```\n\n`pos` is an `ObservableVector3d`, whose `set(x = 0, y = 0, z = 0)` does the same\n— so `this.pos.set(x, y)` wipes the object's depth.\n\n`Renderable#setPosition(x, y, z?)` is the renderable-level form and does not:\nomit `z` and the depth is left exactly as it was. Reach for it whenever you\nare moving an object rather than editing a vector, and keep `v.set(x, y, v.z)`\nfor the vector itself.\n\n## 7. A settings key the class never reads is silently ignored\n\nA settings object is a plain literal: nothing rejects a key, so a name that\nbelongs somewhere else does nothing at all and says nothing about it.\n\n```js\n// WRONG — only the RENDERER reads a `blendMode` setting, never a renderable\nconst glow = new Sprite3d(x, y, { image: \"sun\", blendMode: \"additive\" });\n\n// right: it is a property\nglow.blendMode = \"additive\";\n```\n\nThe same shape catches `transparent` on a `GLTFModel` (its parts carry it —\n`model.setChildsProperty(\"transparent\", true, true)`) and any `Mesh` setting\nhanded to a class that forwards only a curated subset. If a visual option\nappears to do nothing, check whether the class actually reads it before\ntuning the value.\n\n## 8. `isRenderable` does not hide anything\n\nIt gates `updateBounds`, not drawing. Setting it `false` leaves the object on\nscreen and merely stops its bounds tracking, which is worse than doing nothing.\n\n```js\nsprite.isRenderable = false;   // WRONG — still drawn, bounds now stale\nsprite.alpha = 0;              // hides it\n```\n\nUse `alpha` for a blink, or remove the child for a long absence.\n\n## A mask belongs to you, not to the renderable\n\n`renderable.mask` takes a shape you created, and `destroy()` releases the\nreference without recycling it: the same instance may still be masking\nsomething else, so the engine has no claim on it. If you pool masks, release\nthem yourself.\n\nBefore 20.7.0 `destroy()` tried to recycle it and **threw** for every type\n`mask` accepts, because it used the legacy name-keyed pool and no geometry\nclass is registered there. It failed mid-teardown, so a masked renderable\ncould not be destroyed or removed without keepalive at all.\n\n## Update and draw\n\n```js\nclass Enemy extends Renderable {\n    update(dt) {\n        this.pos.x += this.speed * (dt / 1000);\n        return true;                       // ← true means \"redraw me\"\n    }\n    draw(renderer) { /* … */ }\n}\n```\n\n`update()` returns a boolean meaning *\"this object changed and needs\nredrawing\"*. The parent accumulates it into the container's `isDirty`, and the\napplication skips the whole draw pass while nothing is dirty — so returning\nnothing (falsy) can leave the scene frozen even though state is updating. The\nbase implementation returns `this.isDirty`, so `return super.update(dt)` is the\nother correct ending.\n\n`update()` is only called when `obj.inViewport === true` or\n`obj.alwaysUpdate === true`. An off-screen object with the default\n`alwaysUpdate = false` does not tick at all.\n\n## Anchors and floating\n\n- `anchorPoint` defaults to centre `(0.5, 0.5)` on `Renderable`, but `Container`\n  forces `(0, 0)`. Mixing them up shifts children by half their size.\n- `floating = true` pins a renderable to the screen rather than the world, which\n  is what you want for HUD elements. It opts the object out of camera transforms\n  and out of ancestor position accumulation.\n\n## Prefer built-in features\n\nBefore writing custom `draw()` code, check whether the engine already covers it —\n`ParticleEmitter`, `Tween`, the `ShaderEffect` presets, `UISpriteElement` and\n`UITextButton`, the physics adapters, and the Tiled tilemap renderers all exist.\nHand-rolled equivalents miss the batching and the multi-backend support.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| object draws at its parent's origin, ignores its position | `draw()` not reading `this.pos` |\n| pointer handler registered but never fires | `isKinematic` left at its `true` default |\n| wrong draw order | anything other than `addChild(child, z)` — see section 3 |\n| `me.pool: object ... cannot be recycled` while destroying | a mask on a renderable, before 20.7.0 |\n| effect cannot be re-enabled after removal | `removePostEffect()` destroyed it |\n| effect silently does nothing | running on the Canvas renderer |\n| object appears frozen while its state changes | `update()` not returning `true` |\n| off-screen object never ticks | `update()` is gated on `inViewport` — set `alwaysUpdate = true` |\n| custom draw offset by half the size | centred `anchorPoint` default not zeroed |\n| spatial query never finds an object | `isKinematic` left `true` — not in the broadphase |\n| `z` unexpectedly 0 after a `set` | `Vector3d.set(x, y)` defaults `z` to 0 — `setPosition(x, y)` on the renderable leaves the depth alone |\n| a settings option appears to do nothing | the class never reads that key — see section 7 |\n| `isRenderable = false` did not hide the object | it gates bounds, not drawing — use `alpha` |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-getting-started` — Application lifecycle and the scene graph\n- `melonjs-20-migration` — the deprecated `shader` property and other pre-20 APIs\n","packages/melonjs/skills/melonjs-3d-assets/SKILL.md":"---\nname: melonjs-3d-assets\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Loading 3D assets\n\n> glTF/GLB is the primary path for 3D content, and it goes through the **level\n> director** rather than the world directly: `level.load(\"scene\")`, the same call\n> a Tiled map uses. OBJ/MTL is the simpler path for a single static model.\n\n## Loading a glTF/GLB scene\n\n```js\nawait loader.preload([\n    { name: \"diorama\", type: \"glb\", src: \"data/scenes/diorama.glb\" },\n]);\n\nlevel.load(\"diorama\", { scale: 50 });\n```\n\n`type: \"gltf\"` (JSON plus external buffers) and `type: \"glb\"` (single binary)\nare both loader types. A self-contained GLB is the better shipping format — one\nrequest, nothing to resolve relatively.\n\nThe scene loads under whatever camera the stage has; view it under a `Camera3d`\nor it renders flat. See `melonjs-3d` for the camera.\n\n### `level.load` options for glTF/GLB\n\n| option | default | what it does |\n|---|---|---|\n| `scale` | `1` | pixels per glTF unit, applied to the whole scene. Blender's metre-scale export usually needs 20–100. |\n| `container` | `game.world` | where the nodes are added |\n| `onLoaded` | `game.onLevelLoaded` | called with the **level id**, not the scene |\n| `async` | `false` | return a promise that settles once the scene is in the world, instead of a boolean |\n| `rightHanded` | `true` | see below |\n| `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |\n| `lightIntensityScale` | — | keep authored intensity ratios instead of normalising every light to 1 |\n\nThose `game.` defaults are the engine's, not a recommendation. `game` is\nwhichever `Application` finished `init()` last, so **pass `container`\nexplicitly** from a `Stage`, where the right one is in your hand:\n\n```js\nlevel.load(\"sceneId\", { container: this.app.world, scale: 40 });\n```\n| `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |\n| `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |\n| `shadowOpacity` | `0.45` | how dark the blobs are |\n| `shadowScale` | `1` | blob footprint multiplier; `0` or less hides them |\n| `shadowOffset` | `0` | slide them along the light, in blob radii; needs `shadowGroundY`, which is the only way a scene load can name its floor |\n| `shadowStretch` | `1` | lengthen them along it, clamped to 3 |\n| `shadowDirectionX` / `shadowDirectionZ` | `0` | the ground direction they are cast along |\n| `shadowGroundNormal` | world up | up normal of the floor, for a scene that is not level |\n| `shadowLight` | — | a `Light3d` to take the direction from, re-read every draw |\n\n`shadowGroundPoint` is **not** on that list, and a scene load drops it without\na warning. One floor POINT shared by every part of a scene would stack the\nwhole thing's blobs on a single spot, which is nonsense where one floor\nHEIGHT is perfectly sensible. Ground that is not flat is a per-object answer:\nset it on the parts you want once the load has finished.\n\n`onLoaded` receives the level id — it is a \"done\" signal, not a handle on the\nscene. You need it, or `async`: with the game loop running, `level.load` stops\nthe loop and defers the actual load to a timer, so by default it returns\n*before* anything is in the world.\n\n```js\nawait level.load(\"diorama\", { scale: 50, async: true });\n// the scene is in the world here\n```\n\nNote `await level.load(\"diorama\")` without the flag does not await the load — the\ncall returns a boolean, and `await true` resolves immediately. To get at what was\nloaded, load into a container you own, or look the nodes up by their authored\nnames:\n\n```js\nlevel.load(\"diorama\", { scale: 50, onLoaded: () => {\n    const sun = app.world.getChildByName(\"Sun\")[0];   // note: returns an array\n    sun.intensity = 2;\n}});\n```\n\n### The same options from a Tiled `Trigger`\n\nA `Trigger` authored in Tiled forwards these to `level.load()`, so a trigger that\nwalks the player into a glTF level carries the scene's settings as Tiled\nproperties:\n\n```js\nworld.addChild(new Trigger(x, y, {\n    shapes: [new Rect(0, 0, 100, 100)],\n    to: \"diorama\",              // the level id — `to`, not `level`\n    scale: 50,\n    rightHanded: false,\n    castGroundShadow: false,\n}));\n```\n\nEvery `level.load` option above travels, including the whole ground-shadow set,\nand so do `container`, `onLoaded`, `flatten` and `setViewportBounds`. The list is\n**derived** from `level.loadOptions` rather than restated, so an option added to\na level format arrives here for free. It used to be hand-written and had fallen\nthree releases behind the glTF path (#1649).\n\n**`async` is deliberately excluded.** A trigger with a `color` + `duration`\nsets it itself, because it needs the promise to sequence its fade and reveal; a\ntrigger without one loads fire-and-forget and never sets it. Either way an\nauthored `async` property is ignored rather than honoured.\n\n## `rightHanded` — and the default that differs between the two APIs\n\nglTF is authored **Y-up, right-handed**; melonJS is **Y-down, +Z forward**.\nThere are two ways to bridge that, and they are not equivalent:\n\n- a **rotation** — preserves chirality, matches the authoring tool\n- a **mirror** (negate Y only) — flips the scene left/right\n\n`level.load` defaults `rightHanded: true`, so a scene loaded through the level\ndirector comes in correctly. **The `Mesh` constructor defaults it to `false`**,\nso glTF or OBJ geometry you feed to a `Mesh` yourself comes in mirrored unless\nyou pass `rightHanded: true`. A model that is subtly wrong — text backwards, a\ncharacter's gear on the wrong side — is this and nothing else.\n\nDo not add a flip of your own on top; two corrections cancel back to wrong.\n\n## Materials — what actually arrives\n\nThe importer covers a deliberate subset:\n\n- `pbrMetallicRoughness` **base colour texture and factor**\n- metallic/roughness **factors** converted to the engine's specular model\n- `emissiveFactor`, including `KHR_materials_emissive_strength`\n- `KHR_materials_unlit` — honoured, for baked/stylised workflows\n- `alphaMode: \"MASK\"` — the cutout threshold is applied\n- `doubleSided`\n\nNot imported: **normal maps**, metallic/roughness *textures*, occlusion maps,\nmorph targets, `KHR_texture_transform`. Nothing errors — the material simply\narrives without them, which reads as \"flatter than the preview in my authoring\ntool\". Bake what you need into the base colour.\n\n**Draco compression is the exception that does error.** Its geometry sits in\naccessors with no buffer view, which the parser rejects outright, so the whole\nasset fails to load. Export uncompressed.\n\n`textureFilter` on the Application settings (`\"nearest\"` / `\"linear\"` /\n`\"auto\"`) drives sampling; glTF materials that declare their own sampler carry\nit through. Pixel-art models want `\"nearest\"`.\n\n### `baseColorFactor` is LINEAR; a `tint` is sRGB\n**Pass the factor as a setting and the encoding is done for you.** `Mesh`\ntakes `baseColorFactor` — `[r, g, b]` in 0..1, linear, which is the form the\nloader hands back — so a mesh built from a node passes the node's own field\nstraight through:\n\n```js\nconst node = loader.getGLTF(\"palm\").nodes[0];\nconst mesh = new Mesh(x, y, {\n    vertices: node.vertices, normals: node.normals,\n    uvs: node.uvs, indices: node.indices, normalize: false,\n    baseColorFactor: node.baseColorFactor,   // encoded, not scaled\n});\n```\n\nThe setting's type admits `undefined`, because the loader declares the field\noptional and that line has to compile under `exactOptionalPropertyTypes`.\n`Sprite3d` forwards it too, and `mesh.setBaseColorFactor(f)` applies one after\nconstruction. For an ordinary sRGB colour use `setTint` instead — the rest of\nthis section is why the two are different.\n\n\nThe loader handles this for you. It only bites when you build meshes yourself\nfrom `loader.getGLTF(name).nodes` — which is what you do for per-node control,\nsince there is no public per-node `Mesh` factory — and reach for the obvious\n`* 255`:\n\n```js\n// WRONG — every untextured material renders far too dark\nmesh.tint.setColor(f[0] * 255, f[1] * 255, f[2] * 255, 1);\n```\n\nglTF defines `pbrMetallicRoughness.baseColorFactor` in **linear** space (spec\n3.9.2), while a melonJS `tint` is 8-bit **sRGB** — the same space as a CSS\ncolour or a texel out of a PNG. They are not the same number, and the gap is\nwidest exactly where art tends to live:\n\n| linear | `* 255` | correct | \n|---|---|---|\n| 0.05 | 13 | 63 |\n| 0.20 | 51 | 124 |\n| 0.42 | 107 | 173 |\n| 0.72 | 184 | 221 |\n| 1.00 | 255 | 255 |\n\n`Color.setLinear` does the encoding for you — it is the linear-space sibling of\n`setFloat`, which takes the same `0..1` range but treats it as already-sRGB:\n\n```js\nmesh.tint.setLinear(...node.baseColorFactor);   // encodes; what you want\nmesh.tint.setFloat(...node.baseColorFactor);    // does NOT encode; too dark\n```\n\nIt clamps, so the slightly out-of-range factors some exporters emit cannot turn\ninto `NaN`, and it keeps the encoded value as a float rather than rounding\nthrough 8 bits.\n\nMeasured on a scene whose every material went through `* 255`: mean frame luma\n85.5 against 147.8 once encoded, with each channel sitting 60 to 73 counts low.\n\n**Fix it early.** It is nearly invisible as a bug, because the scene still\nlooks coherent — just moody — so lighting and bloom thresholds get tuned\nagainst the wrong values. Correcting the encoding afterwards blows the frame\nout and forces a lighting retune, which is a much bigger job than the encode.\n\nThis applies only to factors coming out of glTF. Colours **you** author for the\nscreen, a HUD tint or a glow, are already sRGB: `* 255` is right for those.\n\n## Lights\n\nAuthored `KHR_lights_punctual` lights — sun, point, spot — become `Light3d`\nworld children, plus a soft ambient fill, and each keeps its authored name.\nA scene carrying any of them renders through the **lit** path automatically.\n\nglTF stores physical units — lux for suns, candela for lamps — and a Blender\ndaylight sun is 1000+, which blows out the engine's stylised shading. So every\nlight is normalised to intensity 1 by default. To keep the *ratios* the artist\nset, scale instead of normalising:\n\n```js\n// a 1000-lux sun lands at 1, a 500-lux fill lands at 0.5\nlevel.load(\"diorama\", { scale: 50, lightIntensityScale: 0.001 });\n```\n\nPass `lights: false` to manage lighting yourself.\n\n## Animation\n\nNode-TRS animation **is** supported: translation, rotation and scale channels,\nthe rigid hierarchical animation used by blocky/low-poly character packs. An\nanimated asset instantiates as a single `GLTFModel` keeping the node hierarchy\nintact, with an API deliberately mirroring `Sprite`. The model carries the asset\nname, which is how you get hold of it after loading:\n\n```js\nconst model = app.world.getChildByName(\"hero\")[0];   // the GLTFModel\n\nmodel.getAnimationNames();                           // [\"idle\", \"walk\", ...]\nmodel.setCurrentAnimation(\"walk\", { loop: true, speed: 1, onComplete });\nmodel.setCurrentAnimation(\"jump\", { next: \"idle\" }); // chain\nmodel.isCurrentAnimation(\"walk\");\nmodel.play();\nmodel.pause();\n```\n\n`speed` is a playback multiplier (1 = authored speed), not a frame delay. Only\nan explicit `loop: false` stops looping. `setCurrentAnimation` **throws** on a\nclip name the asset does not define — check `getAnimationNames()` first.\n\n### Placing and moving a model\n\nA `GLTFModel` is placed like any other renderable — `pos`, `depth`, `rotate`\nand `scale` move the **whole rig**, and compose with whatever the active clip\nis doing, so a walk cycle plays wherever the character stands:\n\n```js\nconst boat = new me.GLTFModel(me.loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nboat.setCurrentAnimation(\"paddle\", { loop: true });\napp.world.addChild(boat);\n\nboat.pos.set(steerX, waterLevel);\nboat.depth = travelled;\nboat.rotate(lean - lastLean, AXIS_Z);   // `rotate` is RELATIVE — feed it the delta\nboat.animationspeed = 0.85 + 0.9 * pace; // one authored tempo, many run speeds\n```\n\nConstructing one directly from `loader.getGLTF(name)` is fine and is the way to\ngo when the model is a game object rather than a scene — `level.load` is for\nloading a whole scene into the world.\n\n**Vertex skinning is out of scope** — `JOINTS_0` / `WEIGHTS_0` are not read, and\nneither are morph targets. A smoothly-deforming character will not deform. The\noptions are: rig it hierarchically instead (separate meshes parented into a\nnode tree), use `Sprite3d` billboards, or use the Spine plugin for 2D skeletal\nwork.\n\n## Instancing\n\nA node using `EXT_mesh_gpu_instancing` loads as an `InstancedMesh` — one draw\ncall for every copy, which is the difference between a hundred trees and a\nhundred thousand. Export with instancing enabled where your tool supports it.\nSparse accessors on the instancing attributes are rejected with a clear error\nrather than silently misplacing every instance.\n\n## Ground shadows\n\n```js\nlevel.load(\"diorama\", { scale: 50, castGroundShadow: true, shadowGroundY: 0 });\n```\n\n`castGroundShadow` is tri-state: set it and it overrides the application setting\nfor this scene in both directions; omit it and the scene inherits — and that\nsetting is `true` by default, so shadows are already on. As a scene-wide opt-in\nit skips nodes with no vertical extent — a ground plane is exactly that, and\nshadowing it with itself smears a blob across the whole floor. Blob shadows need\na GPU backend and a `Camera3d`; nothing is drawn on the 2D-camera or Canvas\npaths.\n\nOmit `shadowGroundY` and each blob sits at its own object's base at full\nstrength, which is right for props already resting on the ground. Set it when\nthings jump or fly, so the shadow stays on the floor and shrinks with height.\n\nA blob sits on its caster's x/z by default, so a wide flat-bottomed prop hides\nits own shadow under itself. The knob for that is `shadowScale`, which widens\nthe blob until its edge shows past the prop, or `shadowOffset` plus a direction,\nwhich slides it clear. **Not** `shadowGroundY`: raising that floats the blob up\nover the object as a dark ring instead of sliding it out, because it is for\nthings that leave the ground, not a visibility control.\n\nFor a light that is not overhead, name one: `shadowLight` takes the direction\nfrom a `Light3d` and is re-read every draw, so a day or night cycle carries\nevery shadow in the scene with it. Nothing is inferred — a scene with no named\nlight and no `shadowDirectionX`/`Z` gets no direction, because a scene with\nseveral lights has no non-arbitrary answer. `shadowStretch` lengthens the blob\nalong that direction and fades it as it pulls; `shadowGroundNormal` lays it on\na floor that is not level. All of these apply to an `InstancedMesh` too, as one\nvalue for the whole set.\n\n## OBJ/MTL\n\nA different shape entirely: OBJ produces raw geometry you hand to a `Mesh`,\nnot a scene. Preload the two files and name them in the `Mesh` settings — the\n`model` and `material` names are independent, the same name for both is just a\nconvention:\n\n```js\nawait loader.preload([\n    { name: \"fox\", type: \"obj\", src: \"models/fox.obj\" },\n    { name: \"fox\", type: \"mtl\", src: \"models/fox.mtl\" },\n]);\n\nconst mesh = new Mesh(400, 300, {\n    model: \"fox\",\n    material: \"fox\",        // texture resolved from the material's map_Kd\n    width: 200,\n    height: 200,\n    rightHanded: true,      // the Mesh default is false — see above\n});\nworld.addChild(mesh);\n```\n\n**The MTL loader fetches the textures it references itself** — `map_Kd`,\n`map_d` and `map_bump` are resolved relative to the `.mtl` and preloaded for\nyou, so there is no image entry in the manifest above. Passing `texture:`\nexplicitly overrides that and pins *one* binding over the whole model, which on\na multi-material model suppresses the per-material texture split; leave it out\nunless you mean that.\n\nMTL contributes `Kd` (diffuse tint), `d` / `Tr` (opacity), `Ke` (emissive),\n`Ks` + `Ns` (specular highlight), `map_Kd` (diffuse texture), `map_d`\n(per-texel opacity) and `map_bump` / `bump` / `norm` (tangent-space normal\nmap — needs `lit: true`, and the tangent frame is derived per fragment, so the\nmodel needs no tangent attribute). All three normal-map spellings are read as\nnormal maps even though the format specifies two of them as height maps, which\nis what every exporter actually writes. Specular *maps* (`map_Ks`) and the PBR\nextension's `map_Pr` / `map_Pm` are not supported and warn — the `Pr` / `Pm`\n*scalars* are read and approximated onto the specular terms, the maps would\nneed a real PBR model.\n\n`Ka` (ambient) and `illum` (illumination model) are read onto the material and\nreadable from `loader.getMTL(name)`, but take no part in shading. That is\ndeliberate rather than pending: `Ka` predates scene-wide ambient light and would\nfight `Stage.ambientLightingColor`, and `illum` enumerates fixed-function\nbehaviours this renderer has no equivalent for. In particular an `illum` of 0 or\n1 nominally means \"no specular\", and the engine does **not** honour that — `Ns`\nstays the only gate, so a model that renders correctly today is not restyled by\na field its exporter wrote inconsistently.\n\n`loader.getOBJ(name)` and `loader.getMTL(name)` return the parsed data if you\nwant it directly. OBJ has no scene graph, no lights, no animation — use glTF for\nanything beyond a single static model.\n\n## Authoring assets for the engine\n\nNotes from modelling props and a rigged character against this loader. They are\nBlender-specific in the API details, general in the traps.\n\n### One clip means one action\n\nglTF names an animation after the **action** it came from, so three objects\neach carrying their own action export as three separate clips — and a game can\nonly play one clip at a time, so the other two never move. Put every animated\nobject in **one action**, using slots (Blender 4.4+):\n\n```python\nshared = bpy.data.actions.new(\"paddle\")\nfor ob in (pivot, shoulder_l, shoulder_r, body):\n    ad = ob.animation_data_create()\n    ad.action = shared\n    ad.action_slot = shared.slots.new('OBJECT', ob.name)\n```\n\nExporting with the scene/whole-timeline mode does **not** merge per-object\nactions into one clip — it still emits one per action. Slots are the fix.\n\nIn Blender 5.x, `action.fcurves` no longer exists; curves live under\n`action.layers[].strips[].channelbags[].fcurves`, and a channelbag identifies\nits slot as `cb.slot.identifier` (there is no `slot_identifier`).\n\n### Never re-parent or join a rig that is mid-pose\n\nBoth parenting and joining bake the objects' **current world transforms**.\nParenting a child while the parent sits in an animated pose writes the inverse\nof that pose into `matrix_parent_inverse`, which silently cancels the animation\n— the part renders rigid while the curves still exist. Detach the action, zero\nthe rig, do the surgery, then reattach:\n\n```python\nsaved = [(o, o.animation_data.action_slot) for o in animated]\nfor ob, _ in saved:\n    ob.animation_data.action = None\n    ob.rotation_euler = (0, 0, 0)\n# ...re-parent / join here...\nfor ob, slot in saved:\n    ob.animation_data.action = act\n    ob.animation_data.action_slot = slot\n```\n\nA joined mesh also inherits the **active** object's scale, so joining into a\nbox that was scaled to `(0.8, 2.0, 0.1)` leaves a node with that non-uniform\nscale and geometry divided back through it. It renders in the right place but\nskews flat-shaded normals — apply transforms (`rotation=True, scale=True`)\nafter joining. Watch for a related trap: setting `ob.scale.x` on a mesh whose\nsize was already applied *multiplies* rather than replaces it.\n\n### A model only needs to stay split where something animates it\n\nEvery glTF primitive is a draw call. Merge everything static into one mesh and\nkeep separate nodes only for the parts a clip actually drives — a boat with a\nrabbit and a paddle went from 28 primitives to 5 that way, with no visible\nchange.\n\n### One palette strip beats one material per colour\n\nAn `InstancedMesh` is **one geometry and one material**, so a prop that wants\nfive colours cannot use five materials. Give the whole model a single material\nwhose image is an *N*×1 strip, one pixel per shade, and point each face's UVs\nat a cell centre (`u = (cell + 0.5) / N`, `v = 0.5`) with the sampler set to\nnearest. A whole scene's palette then costs one tiny texture, a merged mesh can\nbe twenty colours, and there is nothing to bleed between cells.\n\nColour-space trap when generating that strip programmatically: Blender's\n`image.pixels` are scene-linear floats, but this export path writes them out\n**verbatim**, so pre-converting sRGB→linear darkens every colour. White is the\ntell — it is the one value identical in both spaces, so if white survives and\neverything else came out dark, that is the bug. Verify by decoding the PNG back\nout of the GLB and comparing bytes to the source palette.\n\n### Feeding an authored mesh into an `InstancedMesh`\n\n`EXT_mesh_gpu_instancing` covers instances authored *in the file*. When the\ngame places them itself, take the geometry off the parsed descriptor instead:\n\n```js\nconst node = me.loader.getGLTF(\"palm\").nodes[0];\nconst trees = new me.InstancedMesh(0, groundY, {\n    vertices: node.vertices, uvs: node.uvs,\n    normals: node.normals, indices: node.indices,\n    texture: palette, textureFilter: \"nearest\", scale: 40,\n    instanceCount: count,\n});\n```\n\n`nodes` is one entry **per primitive**, each in its own local space with a\nseparate `world` matrix — so this only works cleanly when the asset is a single\nmerged primitive exported with an identity node transform. Model it at the\norigin, base on the floor, and apply transforms before exporting.\n\nOne geometry stamped out hundreds of times reads as one object *copied*\nhundreds of times, so vary each instance in its transform. Rotate **after**\ntranslating, or the instance swings around the group origin instead of turning\nwhere it stands:\n\n```js\nplacement.identity().translate(x / s, -y / s, z / s)\n         .rotate(math.randomFloat(0, Math.PI * 2), AXIS_Y);\nconst j = math.randomFloat(0.82, 1.18);\nplacement.scale(j, j, j);\n```\n\nSizing every prop against **one** scale constant, rather than giving each its\nown, keeps \"this looks wrong\" a modelling question instead of a scaling one.\n\n## Packaging\n\nA self-contained GLB is one file with nothing to resolve — the safest thing to\nship. External references still work in both formats: `.bin` buffers and image\n`uri`s are resolved **relative to the asset's own URL** (a GLB with an external\ntexture loads as-shipped). But an external file that 404s rejects the parse, so\nthe whole asset fails to load rather than arriving partially — no untextured\nfallback. Note that `loader.setBaseURL(\"*\", …)` deliberately skips `gltf`,\n`glb`, `obj` and `mtl` for this reason; set those types individually if you\nneed a prefix.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| model mirrored — text backwards, gear on the wrong side | `Mesh` defaults `rightHanded` to `false`; pass `true` |\n| model upside down | a manual axis flip on top of the loader's conversion |\n| scene is enormous or invisibly small | `scale` left at 1 against a metre-scale export |\n| the asset fails to load entirely | an external buffer/image `uri` that 404s, or Draco-compressed geometry |\n| everything is blown out white | authored lux/candela intensities; use `lightIntensityScale` |\n| materials flatter than the authoring-tool preview | no normal or metallic/roughness *textures* are imported |\n| a character does not deform | vertex skinning is out of scope; rig hierarchically or billboard |\n| animation names come back empty | the asset has no node-TRS channels (skin-only rig) |\n| a hundred copies tank the frame rate | exported without `EXT_mesh_gpu_instancing` |\n| a whole scatter looks like one object repeated | every instance shares the group transform — vary yaw/scale per instance, rotating *after* the translate |\n| three clips where you authored one | one action per object; use a single action with a slot each |\n| one part of a rig renders rigid while its curves exist | it was re-parented mid-pose, baking the inverse into `matrix_parent_inverse` |\n| a merged mesh lights wrong along one axis | it inherited the active object's non-uniform scale on join — apply transforms |\n| an authored palette comes out uniformly dark, but white is correct | linear values written to a strip that is saved verbatim |\n| every untextured glTF material renders too dark, white unaffected | `baseColorFactor` is linear and a `tint` is sRGB — `tint.setLinear(...)`, not `* 255` or `setFloat` |\n| `getGLTF(name).nodes[0]` geometry lands in the wrong place | `nodes` is per primitive, each with its own `world`; merge to one primitive and export at the origin |\n| a prop casts no visible shadow | wide and flat-bottomed — the blob is under it; `shadowGroundY` haloes it rather than revealing it |\n| shadows only show on casters near the camera | `shadowGroundY` is on the wrong side — Y-down means the floor is a **greater** y, so a `pos.y - lift` puts the blob inside the caster and the depth test leaves only a hairline ring |\n| a shadow smeared across the whole floor | a ground plane cast its own blob — use the scene-wide opt-in |\n| `onLoaded` gets a string, not the scene | it is called with the level id; load into your own container instead |\n| scene renders flat and unlit | no `Camera3d` — the 2D-camera path is CPU-projected and unlit (with a `Camera3d` on Canvas you get a black canvas instead) |\n\n## Related skills\n\n- `melonjs-3d` — conventions, `Camera3d`, meshes, `Light3d`, instancing, and\n  `camera.setFog` (an outdoor scene almost always wants it; it is set on the\n  camera, so a loaded scene needs no per-node work)\n- `melonjs-lighting` — how `Light3d` behaves once imported\n- `melonjs-loading-assets` — the loader, asset types and base URLs\n- `melonjs-plugins` — the Spine plugin, for skeletal characters\n","packages/melonjs/skills/melonjs-physics/SKILL.md":"---\nname: melonjs-physics\ndescription: \"Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT.\"\nlicense: MIT\n---\n\n# Physics and collision\n\n> melonJS has a built-in SAT collision world and two optional rigid-body\n> adapters. Movement, collision callbacks and spatial queries all behave\n> differently depending on which is active — and several of the differences fail\n> silently.\n\n## Choosing a world\n\n| | when |\n|---|---|\n| **built-in** (default) | Arcade-style: platformers, top-down games. Fast, position-based, not Newtonian. |\n| **`@melonjs/planck-adapter`** | Full rigid-body dynamics — stacking, joints, realistic restitution. |\n| **`@melonjs/matter-adapter`** | Same class of thing, backed by matter-js. |\n\n### Picking one\n\nStart with the built-in. It is the default for a reason: no dependency, the\nsmallest bundle, and its position-based model is what most 2D games actually\nwant — a platformer's jump arc is a designed curve, not a simulated one, and\ntuning gravity against a solver you are fighting is a bad afternoon.\n\n**Move to an adapter when the game needs something the builtin cannot express**,\nand these are the usual triggers:\n\n- **Rotation that collides.** The clearest signal. The builtin's SAT never reads\n  `body.angle`, so a spinning shape collides as though it never turned. See\n  the note in *Built-in world quirks* below.\n- **Torque, angular velocity, or spin from an impact.** Anything that should\n  start turning *because* it was hit.\n- **Joints and constraints** — ragdolls, chains, ropes, vehicles, hinged doors.\n  The builtin has no concept of them.\n- **Stacking and resting contacts.** Boxes that pile up and stay put need an\n  iterative solver. The builtin resolves each contact once per step with no\n  iteration pass, which splits three ways. A dynamic body resting on STATIC\n  geometry is exact, to the pixel, which is the platformer case it exists for.\n  A body squeezed between a dynamic body and static geometry is exact too: the\n  immovable side gets the casting vote and the other body absorbs the whole\n  correction, so a crate shoved against a wall stops at the wall. But a body\n  squeezed between two DYNAMIC bodies has no immovable side to defer to, so\n  those per-pair corrections still compete and it comes to rest slightly\n  overlapping. Three stacked 44px boxes rest flush on the floor with about 2px\n  of overlap inside the pile, and stay there: a stable equilibrium, not jitter\n  you can wait out or tune away.\n- **Believable restitution and friction**, where a ball's bounce height and roll\n  should follow from its material rather than from code you wrote per case.\n\n**Stay on the built-in when** the game is grid or tile based, movement is\nauthored rather than simulated, collision is \"did these two boxes touch\" plus a\npush-out, or you are shipping to a tight bundle budget. Tiled maps, `Trigger`\nand `Collectable` all work on every backend, so this is not a fork in the road\nfor level content.\n\n**planck or matter?** Both are real rigid-body engines and either will do; the\nadapters expose the same portable API, so a game can switch with an import\nchange and a gravity re-tune.\n\n- **planck** (a Box2D port) is the more accurate and the more predictable under\n  stress: better stacking, better joint behaviour, a continuous-collision\n  `bullet` mode for fast movers. It works in **metres**, so it has a\n  `pixelsPerMeter` scale to think about, and its tuning vocabulary is Box2D's.\n- **matter-js** works directly in **pixels**, which makes it the gentler\n  introduction, and its compound bodies and constraints are pleasant to author.\n  It is the softer solver: stacks settle less crisply and fast bodies need\n  `subSteps`.\n\nIf you have no preference and the game leans on stacking, joints or fast\nprojectiles, take planck. If you want to be up and running with the least\nconversion to think about, take matter.\n\nAn adapter is an `Application` setting, not a plugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },   // pixels/s², default {x: 0, y: 320}\n        pixelsPerMeter: 32,          // default 32\n        subSteps: 2,                 // default 1\n    }),\n});\nawait app.init();\n```\n\n`PlanckAdapter` also takes `velocityIterations` (default `8`) and\n`positionIterations` (default `3`). `MatterAdapter` takes a different set —\n`gravity` (default `{x: 0, y: 1}`, matter's own convention), `subSteps` and\n`matterEngineOptions`; it has no `pixelsPerMeter` because it already works in\npixels. `subSteps` is the anti-tunnelling knob when per-frame motion exceeds a\nbody radius. Pick **one** adapter per game; `physic: \"none\"` disables physics\nentirely — `world.step` skips the simulation and the world behaves as a pure\nscene graph.\n\n**Gravity is a different unit in every backend, and the defaults are NOT the\nsame speed.** Each is \"Earth-like\" in its own convention, but measured against\neach other by timing a 200px free fall in one scene:\n\n| backend | default | unit | measured fall |\n|---|---|---|---|\n| built-in | `0.98` | per FRAME, integrated against `timer.tick` | **~4450 px/s²** |\n| matter | `{x: 0, y: 1}` | matter's own, after its internal `gravity.scale` | **~900 px/s²** |\n| planck | `320` | pixels per second² | **~330 px/s²** |\n\nThat is a **thirteenfold spread**, so swapping the built-in world for an adapter\nand keeping everything else the same makes the whole game feel different. It is\nnot a bug in either one: they simply do not share a unit.\n\nTo match a backend you are porting FROM, pick a target in px/s² and convert:\nplanck takes it directly; matter takes `target / 900`; the built-in takes\n`target / 4450 * 0.98`, because it applies gravity per frame rather than per\nsecond. Passing one number to two of them compares nothing: hand planck\nmatter's `{x: 0, y: 1}` and it runs at a three-hundredth of Earth gravity.\n\nThat mistake is quiet and it does not look like a gravity problem. Bodies fall\nslowly enough that a spawner keeping a fixed interval starts constructing them\ninside each other, and **a body born overlapping another is outside what a\nsolver promises to fix**: the contact normals of an interlocked pair can cancel\nout, leaving the position solver with nowhere to push. The symptom is two\nshapes permanently fused at rest, which reads as a broken collision shape\nrather than as a bad gravity constant. Spawn clear of anything already there.\n\n`world.physic` carries the active adapter's label (`\"builtin\"`, `\"planck\"`,\n`\"matter\"`, `\"none\"`), so game code can branch without importing the class.\n\n## Two ways to declare a body\n\n**`bodyDef`** — declarative, portable, works on every adapter. Prefer it.\n\n```js\nclass Player extends Sprite {\n    constructor(x, y, settings) {\n        super(x, y, settings);\n        this.bodyDef = {\n            type: \"dynamic\",\n            shapes: [ /* … */ ],\n            collisionType: collision.types.PLAYER_OBJECT,\n            collisionMask: collision.types.WORLD_SHAPE | collision.types.ENEMY_OBJECT,\n            restitution: 0.2,\n        };\n    }\n}\n```\n\nThe engine forwards it to the active adapter when the renderable is added with\n`Container.addChild`, and only if that container is already attached to the root\nworld; the broadphase picks the renderable up on the next `world.update()`.\nCalling `adapter.addBody()` yourself registers the body but leaves the\nrenderable out of the scene graph, so it integrates but never collides.\n\n**`fixedRotation` follows the engine, so bodies rotate.** Both matter and\nplanck default it to `false`, and so do the adapters from matter-adapter 1.4.0\nand planck-adapter 1.5.0. Pass `fixedRotation: true` for anything that must\nstay upright, such as a platformer actor. Earlier adapter versions locked\nrotation unless told otherwise, which inverted the engine they wrap: bodies\nlanded holding whatever angle they were given and no collision could turn them,\nwhile `setAngle()` still worked, so a body could look rotated while being\nunable to rotate.\n\n`frictionAir` damps **rotation as well as translation** on both adapters, so\nit is also what settles a spin. Left at `0` a body that starts turning never\nstops, which only became visible once rotation stopped being locked.\n\nOther portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),\n`friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,\n`isSensor`, `userData`. Not every adapter honours every one — the built-in\nadapter ignores `friction`, `fixedRotation` and `userData`, and maps\n`frictionAir` onto its per-axis `body.friction` damping vector.\n\n**`new Body(this, shape)`** — the built-in-native API. Fine, but ties the entity\nto the built-in world.\n\n### Per-shape filtering and triggers\n\nA shape in `shapes` may carry its own `collisionType`, `collisionMask`,\n`isTrigger` (default `false`) and `isActive` (default `true`). A shape can only\n**narrow** what its body allows — the body's type/mask are checked first.\n\n```js\nconst feet = new Rect(0, 24, 32, 8);\nfeet.collisionMask = collision.types.WORLD_SHAPE;\nconst torso = new Rect(0, 0, 32, 24);\ntorso.collisionMask = collision.types.ENEMY_OBJECT;\ntorso.isTrigger = true;      // detects hits, never gets pushed by them\n```\n\n`isActive: false` removes the shape from the narrow phase *and* from raycasts\nwithout shrinking the body's bounds. Honoured by the built-in and planck\nadapters; the matter adapter filters per body and ignores them. Under planck the\nbody-wide setters (`setCollisionType` / `setCollisionMask` / `setSensor`) write\nevery fixture and so overwrite per-shape values.\n\nFor contacts reported per *shape pair* rather than per body pair, define\n`onShapeCollisionStart` / `onShapeCollisionActive` / `onShapeCollisionEnd`.\nDeclaring at least one of them opts into shape-pair enumeration; declaring none\ncosts nothing.\n\n### Shapes drawn in a physics-shape editor\n\nA visual collision-shape editor exports a JSON file holding a map of body name\nto the fixtures authored for it. Preload it like any other JSON, then name it\nfrom `bodyDef.shapes` with `id` naming the body to read:\n\n```js\n// in the preloader\n{ name: \"shapes\", type: \"json\", src: \"data/physics/shapes.json\" }\n\n// then, on the renderable\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\" };\n```\n\nThis works identically on the built-in, matter and planck adapters — the file is\nresolved into real shapes before any adapter is handed the definition. Circles\nand polygons both load, and a concave outline (exported already decomposed into\nconvex pieces) becomes one `Polygon` per piece.\n\nThe density, friction and bounce the shapes were authored with are carried onto\nthe body definition too, each honoured by whichever backends support it — the\nbuilt-in adapter ignores contact `friction`, as it does for a hand-written\ndefinition. The format carries these per fixture, but only planck could honour\nthat, so they reduce onto the body and a file whose fixtures disagree warns. An\nexplicit field on the definition always wins:\n\n```js\n// bounces at 0.9 whatever the file says\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\", restitution: 0.9 };\n```\n\nThe exporting tool's own filter block is **not** read, in any spelling. Its\ncategory and mask bits are numbers in that tool's namespace rather than\n{@link collision.types}, and a template writes its defaults for every fixture\nwhether or not you set anything — importing them would silently tag a shape as\nsomething it is not and stop it colliding. Set `collisionType` / `collisionMask`\non the body definition instead. A file written by hand may carry those two names\nper fixture, and `isSensor`, which mean the same thing here as there.\n\nAuthored shapes and imported ones can sit in the same list.\n\nTwo bodies must never share one `shapes` array: a body **owns** its shapes and\nrotating it mutates them in place. Naming the file sidesteps this — every body\nresolved from it mints its own shapes.\n\n`Body#fromJSON()` and passing an exported list to `addShape()` are the old,\nbuilt-in-only spelling of this and are deprecated since 20.7.0.\n\n### Which shapes each backend can actually simulate\n\n| shape | builtin | matter | planck |\n|---|---|---|---|\n| `Rect`, `Polygon` | exact | exact | exact, up to 12 vertices |\n| `RoundRect` | exact | exact (decomposed) | sampled down to 12 vertices |\n| `Ellipse` | exact | circle of the average radius | circle of the average radius |\n| `Line` | exact | thin quad along the segment | thin quad along the segment |\n| `Box3d`, `Sphere` | exact, and in 3D | **throws** | **throws** |\n| `Point` | supported | **throws** | **throws** |\n\nTwo of those are worth knowing before you author geometry. Box2D caps a\npolygon's vertex count and says nothing when you go over: measured, a\n16-point outline comes back with 12 vertices and a 36-point one collapses\nfrom 70x36 to 47x18. A `RoundRect` carries 36 points, four corners of nine\narc segments each, so it is the usual way to meet that cap; the adapter now\nsamples it down and keeps the extent. Anything the 2D backends cannot express\nis refused loudly on both, rather than leaving a body with no collision\ngeometry that silently never collides.\n\nWhatever a backend really simulates is what `getBodyShapes()` reports, so the\ndebug panel is the fastest way to see an approximation for what it is.\n\n### `Line` shapes, and why slopes need a rigid-body adapter\n\nA `Line` is two points and no area, and it is what Tiled emits for every\npolyline, so it is the natural way to author a slope or a strip of ground.\nNeither rigid-body engine has a segment primitive, so both simulate one as a\nthin quad following the segment (2px), which is what `getBodyShapes()` reports\nback. Before 20.7 each substituted something of its own instead: matter the\nline's bounding box, planck a one-metre square wherever the body happened to\nbe. Neither logged anything.\n\nThe builtin solver resolves a sloped segment too, and correctly: a body lands\non it and SLIDES down it, holding a constant distance from the surface while\nits x advances. What it has no notion of is SURFACE friction. `def.friction`\nis ignored entirely and `body.friction` is a per-step velocity damping vector\nfed from `def.frictionAir`, so nothing removes the tangential component of\ngravity. A body on an incline therefore accelerates down it forever and leaves\nat the end, and in a valley of segments it swings from arm to arm for a while\nbefore coming to rest on the flat. (It used to escape through an arm instead;\nthat was a separate defect in how a body was separated from a shape list, fixed\nin 20.7.)\n\nmatter and planck both have real surface friction, so the same valley holds a\nbody still.\n\n**Several shapes on one body.** A Tiled polyline arrives as ONE body carrying\none shape per segment, and until 20.7 a body resting on such a ground could\nsink through one of the segments and drop out. The narrowphase reports the\nFIRST overlapping shape pair it finds, and the compound resolution pass\nre-ran that same first-hit-wins scan, so it kept re-resolving the pair it had\nalready resolved while a neighbour was penetrated unmeasured. Every pair is\nenumerated now. If you are ever diagnosing something similar, the experiment\nthat localises it is to re-run the same geometry as SEVERAL single-shape\nbodies: if that holds and one multi-shape body does not, the fault is in the\ncompound path rather than in the shape type.\n\nDo not diagnose this by dropping a body and reading its final `y`. A body that\nslid off the end of a ramp and a body that fell straight through it both end up\nfar below, and they look identical in that one number. Track the body against\nthe surface height at its CURRENT x instead: riding the slope shows a constant\noffset while x advances.\n\nSo segments collide on every backend. If you need a body to come to REST on a\nsloped one, use matter or planck, or damp it yourself: the builtin will slide\nit down the incline indefinitely. The `Line Collision` example draws the\nauthored segment against the geometry each backend reports, which is the\nquickest way to see what a backend is really simulating.\n\n### `anchorPoint` moves the collision shapes with the drawing\n\n`anchorPoint` says where in its own bounds a renderable sits on its `pos`:\n`updateBounds()` translates its bounds by `-width * anchorPoint.x, -height *\nanchorPoint.y`, and `preDraw` shifts its rendering by the same amount. A\nbody's collision shapes are measured from that same frame, so a shape of\n`new Rect(0, 0, width, height)` covers the sprite whatever the anchor is, and\na shape's own `pos` still offsets it inside that frame (a small hitbox at the\nfeet, a hurtbox at the head).\n\n**That frame's origin is its TOP-LEFT corner, which is the trap for a small\nshape.** `new Rect(0, 0, w, h)` reads naturally because it spans the frame.\nA circle or a `Sphere` does not span anything, so `new Sphere(0, 0, 0, r)` on\na centre-anchored sprite sits on the sprite's corner, not on the sprite. Put\nit at half the frame:\n\n```js\n// centred on a 24x128 sprite whose anchorPoint is (0.5, 0.5)\nbolt.bodyDef = { type: \"dynamic\", shapes: [new Sphere(24 / 2, 128 / 2, 0, r)] };\n```\n\nMeasured on exactly that sprite: with the shape at `(0, 0, 0)`,\n`adapter.getBodyShapes()` reported it at `(-12, -64)` — **64 units up the\nscreen, four times its own radius** — so the projectile collided well away\nfrom where the player saw it, in a way that still looked like a working game\nbecause the targets were large.\n\nThe offset comes from the renderable's own `width` / `height`, NOT from what\nit draws at. Scale a sprite through `currentTransform` and those stay the\ntexture's size, so the half-frame term is half the texture, not half the\ndrawn quad. The debug overlay is the quickest confirmation: the red shape\nshould sit on the art.\n\nThis was broken until 20.7: shapes were measured from `pos` regardless of the\nanchor, so anything not anchored at its top-left corner collided where it was\nnot drawn, by half a body at the default centred anchor and by a full body\nheight at the bottom anchor a platformer actor uses. If you compensated by\noffsetting your shapes by hand, remove those offsets. `Entity` and Tiled\nobjects set their own anchor to `(0, 0)`, so nothing built either of those\nways is affected.\n\nThe debug panel is the quickest check: green is the renderable's bounds, red\nis what actually collides, and the two now agree.\n\nOne renderable does not take that offset at all: one that has cleared\n`applyAnchorTransform`, the flag `preDraw` reads before shifting anything.\n`GLTFModel` sets it `false` outright and `Mesh` clears it under a `Camera3d`,\nbecause both emit world coordinates and pivot about their own model origin\nrather than a bounds box, so they draw at `pos` whatever their `anchorPoint`\nstill holds. Their bodies are built there too, on every backend, and\n`raycast3d` reports hits in that same frame. If you clear the flag on a\nrenderable of your own, the same applies: you place it, and your shapes are\nmeasured from `pos`.\n\n### `autoTransform: false` opts out of rotated bounds\n\n`updateBounds()` only applies `currentTransform` when `autoTransform` is\n`true`. Turn it off and the renderable's bounds stay the UNROTATED frame\nwhile the sprite turns inside them, which quietly feeds a wrong box to\nfrustum culling and to the broadphase. Measured on a 60x20 box at 90\ndegrees: `20x60` with `autoTransform` on, `60x20` with it off.\n\nThat flag means \"I place myself\", so it is a fair contract, but reach for it\nonly when you really are drawing in your own frame, and remember the bounds\nare yours to keep honest then.\n\n## Move with forces, not by assigning position\n\n```js\n// ✗ the adapters overwrite this from the engine body every step\nthis.pos.x += this.speed;\n\n// ✓ portable — works on every adapter\nthis.body.applyForce(thrustX, 0);\nthis.body.setVelocity(vx, vy);\n\n// ✓ built-in only — `force` and `maxVel` are BuiltinAdapter Body fields\nthis.body.force.x = this.body.maxVel.x;\n```\n\n`maxVel` is what turns a force into a speed, so the two are set together. Use\nthe setters rather than writing the vectors — both take an optional THIRD\nargument for the depth axis, left unchanged when omitted, so a 2D call keeps\nits behaviour:\n\n```js\nthis.body.setMaxVelocity(3, 15);   // walk speed, jump speed\nthis.body.setFriction(0.4, 0);     // ground drag, none vertically\n```\n\n`friction` here is per-axis per-step velocity damping, not a surface\ncoefficient — see the built-in quirks below.\n\nUnder matter and planck, `syncFromPhysics()` copies the engine body's position\nback onto `renderable.pos` after every step, so a direct write is simply erased\n— no error. The built-in world integrates `pos` in place instead, so a write\nsurvives but fights collision resolution and jitters. To teleport, go through\n`adapter.setPosition(renderable, p)` so the engine body moves with it.\n\n## Collision callbacks — which one to use\n\nThe lifecycle is `onCollisionStart` (contact begins), `onCollisionActive` (every\nstep while it persists) and `onCollisionEnd` (contact breaks). None of them are\ndefined on `Renderable` — you just declare the ones you want and the dispatcher\nchecks `typeof`. That is also why they cannot be declared on the base class: a\nfield there would SHADOW the prototype method every subclass that implements\none defines.\n\n**In TypeScript the response is `CollisionResponse`**, exported from `melonjs`:\n\n```ts\nimport type { CollisionResponse, Renderable } from \"melonjs\";\n\nclass Coin extends Sprite {\n    onCollisionStart(response: CollisionResponse, other: Renderable) { … }\n}\n```\n\nOn a renderable you do not subclass, declare the hooks you want as OPTIONAL\nmembers of a local interface and assign through one plain cast — that is the\nopt-in, and it needs no `as unknown as`:\n\n```ts\ninterface Contacts {\n    onCollisionStart?: (response: CollisionResponse, other: Renderable) => void;\n}\n(rock as Contacts).onCollisionStart = (response, other) => { … };\n``` The legacy `onCollision` still exists and has a trap:\n\n> On the built-in world, `onCollision` fires **twice per frame** for\n> dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT\n> response with a fixed `a`/`b` per pair. On the adapters it is routed to their\n> \"still touching\" phase and fires once per side.\n\nThe legacy dedupe idiom is `if (this !== response.a) return;`. Defining\n`onCollisionActive` suppresses the legacy callback on that renderable (the\n\"supersedes\" rule, applied per side so `a` and `b` can migrate independently),\nand is deduped to once per pair per side per frame on every adapter.\n\n**Return values only work on the legacy handler.** `return false` from\n`onCollision` skips the built-in SAT push-out; what `onCollisionStart` /\n`onCollisionActive` return is discarded. To opt a body out of the physical\nresponse, set `bodyDef.isSensor` / `body.setSensor(true)`, or make the shape a\ntrigger. With no `onCollision` defined at all, push-out happens by default for\ndynamic non-sensor bodies.\n\nThe two handler families do **not** get the same response object:\n\n| | `onCollision` (legacy) | `onCollisionStart` / `Active` / `End` |\n|---|---|---|\n| `a` / `b` | fixed per pair | receiver-symmetric: `a === this`, `b === other` |\n| overlap magnitude | `response.overlap` | `response.depth` |\n| contact axis | `response.overlapN` / `overlapV` | `response.normal` |\n| also carries | `aInB`, `bInA`, `indexShapeA/B`, `isTriggerContact` | `pair` (adapter-native, absent on built-in) |\n\nOn the modern response `overlap` / `overlapN` / `overlapV` are still present but\n`@deprecated` (and `undefined` under the matter adapter) — use `depth` and\n`normal`. `normal` is the direction the receiver must move to separate, in\ncanvas coordinates: `normal.y < -0.7` means \"push me up\", i.e. I landed on top\nof `other`.\n\nOne portability wrinkle: the built-in adapter dispatches `onCollisionEnd` with\n`undefined` as the response; the matter and planck adapters pass a real one.\nGuard it.\n\n## Never mutate the world during contact dispatch\n\nCollision callbacks are dispatched from inside the physics step on every\nadapter, and each engine has its own tolerance for being mutated there. Flag the\nwork and drain it from your `Stage.update`:\n\n```js\nonCollisionStart(response, other) {\n    this.pendingRemoval = true;    // don't addChild / removeChildNow here\n}\n```\n\nWhat actually happens per adapter:\n\n- **built-in** — callbacks fire inline during `step()`. `Container.removeChild`\n  is deferred, so it is safe; `removeChildNow()`, and the internal `destroy()`\n  it calls, are immediate,\n  and the detector only survives them because it re-checks `body === undefined`\n  after every user callback.\n- **planck** — `onCollisionStart` / `onCollisionEnd` are dispatched from\n  planck's `begin-contact` / `end-contact` events, which fire *while the world\n  is locked*. Adding a renderable with a `bodyDef` there makes planck's\n  `createBody` return `null` and the adapter then throws on the null handle.\n  `onCollisionActive` is dispatched after the step and is the safe one.\n- **matter** — callbacks come from matter's `collisionStart` / `collisionActive`\n  / `collisionEnd` events during `Engine.update`.\n\n## Spatial queries\n\n`raycast` and `queryAABB` are implemented by all three adapters:\n\n```js\nimport { Rect, Vector2d } from \"melonjs\";\n\n// nearest hit only — returns null when nothing is hit\nconst hit = app.world.adapter.raycast(new Vector2d(x0, y0), new Vector2d(x1, y1));\n// hit → { renderable, point, normal, fraction }\n\n// takes a Rect, not a Bounds\nconst inArea = app.world.adapter.queryAABB(new Rect(x, y, w, h));\n```\n\n`querySphere(centre, radius)` (or `querySphere(sphere)`) is **built-in only**,\nand only active under a 3D broadphase — i.e. `world.sortOn === \"depth\"`, which\n`Camera3d` sets. So is `raycast3d`. Check\n`world.adapter.capabilities.raycasts3d` before calling either. `raycast` itself\nis capability-gated by `capabilities.raycasts`.\n\nThe legacy `collision.rayCast(line, result)` is a different API: it takes a\n`Line`, returns an **array** of every intersecting renderable, and only exists\non the built-in path.\n\nTwo prerequisites that produce empty results rather than errors:\n\n- **`isKinematic` must be `false`** for a renderable to be in the broadphase at\n  all (a `Body` sets this for you).\n- On the **built-in** adapter, the broadphase is cleared and rebuilt inside\n  `world.update()`, so querying before the first update returns nothing.\n\n## 3D collision: `Box3d`, `Sphere` and the Z pushback\n\nThe narrowphase is not 2D-only. Two shapes carry a depth extent — `Box3d` and\n`Sphere` — and any pair of them is resolved in three dimensions, pushing back\nalong **Z** as well as X and Y.\n\n```js\nimport { Box3d, Sphere } from \"melonjs\";\n\n// a box: centre offset from the body, then half-extents on each axis\ncrate.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));\n// a sphere: centre offset, then one radius\nrock.body.addShape(new Sphere(0, 0, 0, 30));\n```\n\nReach for these for anything moving in the XZ plane under a `Camera3d` — a\nrunner dodging obstacles, a 2.5D platformer, pickups on a course. Hand-rolling\na distance check there is the usual mistake, and it throws away the\npenetration vector the response already carries.\n\n**Which of the two.** A `Box3d` is world-axis-aligned and cannot be oriented:\nit is right for walls, floors, crates and anything else that stays square to\nthe world, and wrong for anything that turns. A `Sphere` has no orientation to\nget wrong, which makes it the shape for things that tumble, and for anything\nlaid out over a curved surface — on a globe every object's own \"up\" points a\ndifferent way, so no single axis-aligned box fits any of them. It is also the\ncheaper test: one distance against a sum of radii.\n\n**Z arrives beside `overlapN`, not inside it.** `overlapN` stays the 2D unit\nvector it always was and `overlapNZ` carries the depth component, because\n`Vector3d` does not extend `Vector2d` and widening the field would have broken\nevery existing 2D handler. The two together are a 3D unit vector, and\n`overlapZ = overlapNZ * overlap` exactly as `overlapV = overlapN * overlap`.\n\n**How much of the normal is in each half depends on the shapes.** A `Box3d`\npair separates along ONE world axis, so a depth contact leaves the 2D fields\nat zero and a legacy 2D handler correctly applies no push. A `Sphere`\nseparates along the line between the two centres, which is generally diagonal:\nall three components are non-zero at once. So subtract both halves rather than\nbranching on one:\n\n```js\nonCollision(response, other) {\n    // works for either shape: the 2D half is zero when there is no 2D push\n    this.pos.x -= response.overlapV.x;\n    this.pos.y -= response.overlapV.y;\n    this.pos.z -= response.overlapZ;\n    return false;\n}\n```\n\nBranching on `if (response.overlapNZ !== 0)` and then moving only `pos.z` is\nthe mistake to avoid: correct for a box pair, and for a sphere it throws away\nthe planar half of the contact.\n\n**Mixed pairs degrade to 2D.** A `Box3d` or a `Sphere` against a `Polygon`,\n`Rectangle`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and\ntreats the planar shape as unbounded along Z, which is what keeps a sphere or\nbox body colliding with an ordinary Tiled collision layer. `overlapZ` is zero\nfor those, since only one side has a depth to resolve. Give BOTH sides a 3D\nshape when you want a depth result.\n\nThis is the narrowphase and is independent of the 3D broadphase queries above\n— `querySphere` / `raycast3d` need `world.sortOn === \"depth\"`; a 3D body shape\ndoes not.\n\n`raycast3d` measures a `Box3d` body as its box and a body that is one `Sphere`\nas that sphere, both exactly. Anything else, including a body mixing the two,\nis measured as the AABB containing its 3D shapes, and a body with none falls\nback to a bounding sphere derived from the renderable's 2D bounds.\n\n**Bounds are what the broadphase sorts by, and they come from the SHAPE.**\nEvery item is filed by its renderable's 2D `getBounds()`, so an object with no\nextent cannot be placed and silently collides with nothing. A `Mesh` takes its\nextent from its geometry and a `GLTFModel` from its part meshes, so both report\nwhere they are without being told. A body's shapes do **not** contribute, so a\ncustom collidable built on a bare `Container` has no extent until you give it\none:\n\n```js\n// a container reports an EMPTY bounds until it has a size of its own\ngroup.resize(width, height);\n```\n\n`enableChildBoundsUpdate` also gives it one, but it re-measures the whole child\nlist on every update — reach for it only when the extent genuinely IS the\nmembers' union and moves with them (a flock, a squad), not to give a fixed\ngroup a size.\n\n## Built-in world quirks\n\nThese are specific to the default world and surprise people arriving from a\nreal rigid-body engine:\n\n- **Dynamic-dynamic collision is position-based, not Newtonian.** Separation is\n  mass-proportional but the velocity response is per-body cancellation — two\n  equal-mass bodies do not exchange momentum the way you would expect.\n- **An immovable contact wins the step.** Each step records, per body, which\n  directions it has already been pushed out of, and a later dynamic-dynamic\n  contact will not push it back in one of those directions: the other body\n  takes the whole correction instead. A light body shoving a heavy one against\n  a wall therefore stops dead at the wall rather than driving it through, and\n  the shover absorbs all of the separation. The record lasts one step and is\n  not contact state — it says nothing about what a body is touching.\n- **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate\n  `app.world.adapter.gravity` at runtime, or pass\n  `physic: new BuiltinAdapter({ gravity })` to override it. `body.gravityScale`\n  (default `1`) scales it per body; `0` disables gravity for that body and is\n  the portable replacement for the deprecated `body.ignoreGravity`.\n- **`applyForce(x, y)` is linear only** unless you pass the optional\n  `(pointX, pointY)` application point, which generates a torque\n  `τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.\n  `applyTorque(τ)` is the direct form.\n- **Force accumulators reset at end-of-step** — for every body, including\n  static, paused and off-screen ones — so forces must be applied every frame to\n  persist.\n- **`isGrounded` is flag-based, not contact-based** — the adapter returns\n  `!body.falling && !body.jumping`, flags updated by the last resolved\n  collision, not a live contact test.\n- **`def.density` maps 1:1 to `body.mass`** (default `1`), and `def.friction` is\n  **ignored** entirely. `body.friction` is a per-axis `Vector2d` of per-step\n  velocity damping fed from `def.frictionAir`, not a surface coefficient.\n- **`body.maxVel` defaults to `(490, 490)`** and hard-clamps velocity after\n  forces and friction. The canonical movement idiom is\n  `this.body.force.x = this.body.maxVel.x`.\n- **Default `collisionType` is `ENEMY_OBJECT`**, and default `collisionMask` is\n  `ALL_OBJECT` — set both explicitly.\n- **Integration is gated on `inViewport || alwaysUpdate`** — an off-screen body\n  stops simulating unless you set `alwaysUpdate = true`. It is also gated on\n  `updateWhenPaused` while the state manager is paused, and static bodies never\n  integrate.\n- **`addBody` throws on double-registration** for a renderable that is already\n  adapter-managed.\n\n### Rotation collides on planck and matter, but not on the built-in solver\n\nThis is the sharpest behaviour difference between the adapters, and it is\nsilent — nothing warns you.\n\n| adapter | a rotating body |\n| --- | --- |\n| **builtin** | rotates **visually only**. `body.angle`, `setAngle()` and angular integration write `renderable.currentTransform`; the collision shapes are never rotated, so a spinning sprite collides as if it were upright |\n| **planck** / **matter** | rotates for real — the solver owns the body's transform and collision follows it |\n\nSo a turret, a swinging platform or a tumbling crate that must *collide*\nrotated needs planck or matter. Under the builtin solver, approximate it with a\nshape that is close enough at every angle (a circle, or a square for\n90° steps), or rotate the shapes yourself with `body.rotate(angle)` — which\ndoes rotate them, unlike `setAngle()`.\n\n`body.rotate()` applies a **delta** and does not update `body.angle`; the two\nare unlinked, so a body driven by `rotate()` still reports an angle of `0`.\nTrack it yourself if you need to read it back. Before 20.7 it also threw on a\n`Box3d` or `Point` shape, and did not tell its owner that the body's bounds had\ngrown.\n\n`adapter.getBodyShapes(renderable)` reports whichever is true: the geometry\nthe engine is actually simulating on planck and matter (matter-adapter 1.3.0 /\nplanck-adapter 1.4.0 and later), unrotated shapes on the builtin solver because\nthere they genuinely are. That is what the debug overlay draws, so the hitbox\nyou see is the hitbox that collides — a sprite spinning inside a stationary red\nbox on the builtin is the solver telling you the truth, not a drawing bug.\n\nOn planck and matter that report comes from the engine rather than from your\ndefinitions, so it shows the approximations they make: an `Ellipse` appears as\nthe circle of average radius it is simulated as, a concave polygon as the\nconvex pieces it was decomposed into, and a `Rect` as a `Polygon`. Your\nauthored shapes are untouched and still readable on `renderable.bodyDef.shapes`.\n\n**If you need rotation to affect collision, use planck or matter.** That is the\ndividing line: the builtin is a position-based solver whose SAT never reads\n`body.angle`, so rotation there is a visual property. `body.rotate(angle)` is\nthe workaround, not a rotating body — it permanently moves the shapes, does not\ntrack an angle, and gives you no angular velocity, torque or rotational\nresponse from a collision. A game whose collision genuinely turns (a spinning\nhazard, a swinging bridge, a car, anything with torque) wants a real rigid-body\nadapter rather than a workaround.\n\n## Porting between adapters\n\nSame call, different magnitude — these are the ones that waste an afternoon:\n\n- **The portable API converts for you, the native handle does not.**\n  `adapter.setVelocity` / `body.setVelocity(x, y)` are in pixels on every\n  adapter; reaching for the raw handle (`body.setLinearVelocity` on planck) puts\n  you in metres per second, scaled by `pixelsPerMeter`.\n- **Time base differs.** Built-in `body.vel` is pixels *per frame*; planck and\n  matter integrate in seconds. The same number is ~60× off.\n- **Force magnitudes are not comparable** between adapters; re-tune rather than\n  reuse. `applyForce` is a per-step accumulator everywhere (cleared each step,\n  so call it every frame) — `applyImpulse` is the one-shot. Matter has no native\n  impulse, so the adapter emulates it as `Δv = J / mass` and ignores the\n  application point.\n- **`body.position` is the centroid in matter**, while `renderable.pos` is\n  top-left in melonJS. The adapter stores a per-body offset so `renderable.pos`\n  stays correct — but read `body.position` directly and you get the centroid.\n- **Degenerate (zero-area / collinear) polygons — a Tiled polyline, a `Line` —\n  cannot be built by matter and fall back to their axis-aligned bounding box.**\n  Any other shape type throws `unsupported shape type`.\n- **Ellipses become circles on matter**, using the average of the two radii.\n- **`isGrounded` is capability-gated** (`capabilities.isGrounded`) and computed\n  differently per adapter: a flag read on built-in, a live scan of active\n  contacts on matter.\n\nAdapter-specific escapes exist (`(this.body as MatterAdapter.Body).frictionAir`,\n`adapter.matter.Constraint.create(...)`) but are not portable — flag them if you\nuse them. `adapter.capabilities` (`constraints`,\n`continuousCollisionDetection`, `sleepingBodies`, `raycasts`, `raycasts3d`,\n`velocityLimit`, `isGrounded`) is the portable way to branch.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| entity jitters or does not move | assigning `pos` instead of applying force |\n| collision handler runs twice per frame | legacy `onCollision` on a dynamic pair (built-in) — use `onCollisionActive` |\n| `return false` from a handler does nothing | return values are only honoured by legacy `onCollision` — use `isSensor` / `isTrigger` |\n| crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |\n| `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |\n| `querySphere` / `raycast3d` is not a function | 2D adapter, or built-in without a 3D broadphase (`sortOn !== \"depth\"`) |\n| a 3D contact reports no overlap to push against | read `overlapNZ` / `overlapZ` — a Z-axis MTV leaves `overlapN` / `overlapV` at zero |\n| two 3D objects never separate in depth | one of them carries a planar shape; a mixed pair is tested as the box's XY footprint, unbounded along Z |\n| `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |\n| `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |\n| off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |\n| a body sinks through one shape of a multi-shape body | fixed in 20.7; before that the compound pass only ever re-resolved the first overlapping pair |\n| a body will not stay put on a slope on the builtin | it is riding the slope and sliding: the builtin has no surface friction, only `frictionAir` damping. Damp it yourself, or use matter/planck |\n| the sprite is drawn offset from its hitbox | pre-20.7 `anchorPoint` moved the drawing but not the collision shapes; upgrade, and drop any offsets you added to compensate |\n| a small shape sits at the sprite's corner | a shape's offset is measured from the drawn frame's TOP-LEFT; `new Sphere(0, 0, 0, r)` is the corner, `(width / 2, height / 2)` is the centre |\n| a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |\n| a body in a pile sinks into the floor (built-in) | fixed in 20.7 for anything with an immovable side; bodies pinned only by other DYNAMIC bodies still overlap by a pixel or two, which is what planck/matter are for |\n| forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |\n| `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, melonJS the top-left — the adapter offsets between them |\n| `bodyDef.shapes: \"…\"` throws \"is not loaded\" | the name must match the `name` the JSON was preloaded under, not its filename |\n| everything falls far too fast or too slow after swapping backends | gravity is a different unit per backend; see the table above |\n| debris lands holding its spawn angle and never tumbles | `fixedRotation: true`, or matter-adapter < 1.4.0 / planck-adapter < 1.5.0 |\n| a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use planck/matter, or `body.rotate(angle)` |\n| the debug hitbox does not follow a spinning body | pre-1.2.1 matter-adapter / pre-1.3.1 planck-adapter returned the authored shapes rather than the rotated ones |\n| a Tiled polyline becomes a solid box on matter | matter cannot build a zero-area polygon; the adapter falls back to its AABB |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic`, which also gates the broadphase\n- `melonjs-tilemaps` — collision shapes authored in Tiled\n- `melonjs-loading-assets` — preloading a shape editor's export as JSON\n","packages/melonjs/skills/melonjs-sprites-and-animation/SKILL.md":"---\nname: melonjs-sprites-and-animation\ndescription: \"Use this skill when creating sprites, playing frame animations, or working with texture atlases in melonJS. Covers Sprite construction from a plain image or an atlas region, addAnimation/setCurrentAnimation, TextureAtlas for TexturePacker/ShoeBox/Aseprite, tint, flip, NineSliceSprite, and the centred anchorPoint that offsets naive positioning. Triggers on: Sprite, new Sprite, addAnimation, setCurrentAnimation, isCurrentAnimation, TextureAtlas, TexturePacker, Aseprite, ShoeBox, atlas, region, framewidth, frameheight, tint, flipX, flipY, NineSliceSprite, animation.\"\nlicense: MIT\n---\n\n# Sprites, animation and atlases\n\n> Creating sprites from images or atlases, driving frame animation, and the\n> positioning trap that catches almost everyone.\n\n## `pos` is the centre, not the top-left\n\n`Renderable.anchorPoint` defaults to **`(0.5, 0.5)`**, so a sprite's `pos` is its\n**centre**. `Container` forces `(0, 0)` instead, which is why containers and\nsprites appear to disagree.\n\n```js\n// a 64×64 sprite at (100, 100) covers 68..132, not 100..164\nconst s = new Sprite(100, 100, { image: \"hero\", framewidth: 64, frameheight: 64 });\n\ns.anchorPoint.set(0, 0);   // switch to top-left positioning if you prefer\n```\n\nWriting top-left maths against the centred default puts everything half a sprite\noff, with no error. It is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Constructing a sprite\n\nFrom a plain image:\n\n```js\nconst hero = new Sprite(x, y, {\n    image: \"hero\",          // preloaded asset name\n    framewidth: 32,\n    frameheight: 32,\n});\n```\n\nFrom a texture atlas region:\n\n```js\nconst atlas = new TextureAtlas(loader.getJSON(\"sprites\"), loader.getImage(\"sprites\"));\nconst coin = new Sprite(x, y, { image: atlas, region: \"coin_01.png\" });\n```\n\n`framewidth` / `frameheight` define the animation grid on a sheet. Omit them and\nthe whole image is one frame.\n\n## Animation\n\n```js\nhero.addAnimation(\"idle\", [0, 1, 2, 3], 100);   // frames, ms per frame\nhero.addAnimation(\"walk\", [4, 5, 6, 7]);\nhero.addAnimation(\"die\",  [8, 9], 150);\n\nhero.setCurrentAnimation(\"walk\");\nhero.setCurrentAnimation(\"die\", \"idle\");                          // chain to idle when it ends\nhero.setCurrentAnimation(\"die\", { loop: false });                 // play once, hold the last frame\nhero.setCurrentAnimation(\"die\", { loop: false, onComplete: fn }); // …and call fn when it does\nhero.setCurrentAnimation(\"use\", { next: \"idle\", onComplete: fn }); // call fn, then chain to idle\nhero.setCurrentAnimation(\"run\", { onFrame: (frame) => {            // each frame playback reaches\n    if (frame === 3 || frame === 8) footstep();\n} });\n\nif (hero.isCurrentAnimation(\"walk\")) { /* … */ }\n```\n\nFrame indices are into the `framewidth`/`frameheight` grid. With an atlas, use\n`atlas.getAnimationSettings([regionNames])` and spread it into the Sprite\nsettings, or `atlas.createAnimationFromName([regionNames])`, which returns a\nready-made Sprite. On a fixed-cell spritesheet only numeric indices are\naccepted — passing region names throws.\n\nThe second argument to `setCurrentAnimation` is polymorphic. A **string** chains\nto that animation when this one ends. An **options object** takes\n`{ loop, next, speed, onComplete, onFrame }`, and only an explicit `loop: false`\nstops the looping. A **bare function** is the legacy form: it fires at *every* cycle end,\nand only holds the last frame if it returns `false`. Omit the argument entirely\nand the animation loops forever.\n\n`onFrame(frame)` is called with frame 0 when the animation starts, then with each\nframe playback reaches, so a footstep or a hit lands on its frame without polling\n`getCurrentAnimationFrame()`. A long tick reports every frame it goes past. It is\nnot called for a frame set with `setAnimationFrame`, nor for the frame 0 a\nplay-once animation would wrap to. Keep the callback in a field when you call\n`setCurrentAnimation` every frame: re-selecting keeps the first one anyway.\n\nWith both `next` and `onComplete`, the callback runs first and the chained\nanimation starts with none of its own, so a one-off action (a pickup, an\nattack) needs no manual switch back to idle.\n\nRe-selecting the animation that is already current is a no-op, so calling\n`setCurrentAnimation` every frame is safe, and an `isCurrentAnimation` check\naround it adds nothing. An unknown id throws.\n\n## Texture atlases\n\n`TextureAtlas` reads several formats:\n\n| source | how |\n|---|---|\n| TexturePacker / Free Texture Packer JSON | `new TextureAtlas(loader.getJSON(name), loader.getImage(name))` |\n| Aseprite **JSON export** | same as TexturePacker — the format is detected from `meta.app` |\n| Aseprite **`.aseprite` / `.ase` binary** | loader type `\"aseprite\"`, which stores the composited image *and* a JSON sidecar under the one asset name |\n| ShoeBox | JSON export using the melonJS exporter settings file; same call as TexturePacker |\n| fixed-cell spritesheet | `new TextureAtlas({ framewidth, frameheight, anchorPoint }, loader.getImage(name))` |\n| multi-page | pass an array of atlas JSON objects as the first argument |\n\nHelpers: `createSpriteFromName(name)`, `createAnimationFromName([names])`,\n`getAnimationSettings([names])`.\n\nAtlases matter for performance: the GPU batchers bind several textures at once\n(up to `renderer.maxTextures`), so a handful of separate images still batch —\nbut once a frame crosses that many distinct textures the batch has to flush.\nPacking into one atlas keeps everything on a single binding.\n\n## Visual properties\n\n```js\nsprite.setTint(\"#ff8000\");   // multiply tint\nsprite.alpha = 0.5;\nsprite.flipX(true);                   // mirror horizontally\nsprite.flipY(true);\nsprite.blendMode = \"additive\";        // see the effects skill for the full list\nsprite.scale(2, 2);                   // ← multiplicative, see below\n```\n\n**`scale()` is multiplicative, not absolute.** Calling it every frame compounds.\nFor absolute scaling reset first:\n\n```js\nsprite.currentTransform.identity();\nsprite.currentTransform.scale(s, s, 1);\n```\n\nThis bites hardest with pooled sprites, which arrive carrying their previous\nlife's transform.\n\n## `NineSliceSprite`\n\nFor panels and dialogue boxes that stretch without distorting their corners:\n\n```js\nnew NineSliceSprite(x, y, {\n    image: \"panel\", width: 300, height: 120,\n    insetx: 12, insety: 12,     // lowercase x/y — `insetX` is silently ignored\n});\n```\n\n`width` and `height` are mandatory and the constructor throws without them.\nThe insets default to a quarter of the frame.\n\n## Pooling\n\nFrequently spawned sprites should be pooled:\n\n```js\nimport { createPool } from \"melonjs\";\n\nconst bulletPool = createPool((x, y) => {    // me.pool is deprecated\n    const instance = new Bullet(x, y);\n    return { instance, reset: (x, y) => instance.onResetEvent(x, y) };\n});\nconst b = bulletPool.get(x, y);             // new Bullet(x, y), or reset(x, y) on a reused one\nworld.removeChild(b);                       // returns it to bulletPool automatically\n```\n\nTwo consequences:\n\n- A recyclable class's `onResetEvent` must reset **everything** its lifetime\n  mutates — alpha, tint, scale, animation state. Anything you forget carries\n  over to the next use.\n- Pool-recycled objects do **not** fire `onDestroyEvent` on removal. Pair event\n  subscriptions with `onActivateEvent` / `onDeactivateEvent` instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| everything offset by half a sprite | top-left maths against the centred `anchorPoint` |\n| sprite grows every frame | `scale()` is multiplicative — reset the transform |\n| a recycled sprite keeps its old look | `onResetEvent` does not restore every mutated property |\n| event handlers leak on pooled objects | bound in the constructor rather than `onActivateEvent` |\n| animation never advances | no `framewidth`/`frameheight`, so the sheet is one frame |\n| animation plays once and stops | `{ loop: false }`, or a callback that returned `false` |\n| sprites do not batch | more distinct textures in one frame than `renderer.maxTextures` |\n| nine-slice corners stretch anyway | `insetX`/`insetY` instead of `insetx`/`insety` |\n\n## Related skills\n\n- `melonjs-renderables` — anchors, draw order, custom draw\n- `melonjs-getting-started` — preloading images and atlases\n","packages/melonjs/skills/melonjs-input/SKILL.md":"---\nname: melonjs-input\ndescription: \"Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click.\"\nlicense: MIT\n---\n\n# Input: keyboard, pointer and gamepad\n\n## Keyboard goes through named actions\n\nDo not poll key codes. Bind a key to an **action string**, then poll the action —\nthis is what lets gamepad and virtual controls map onto the same handling.\n\n```js\nimport { input } from \"melonjs\";\n\ninput.bindKey(input.KEY.LEFT,  \"left\");\ninput.bindKey(input.KEY.RIGHT, \"right\");\ninput.bindKey(input.KEY.X,     \"jump\", true);   // true = locked: one press per keydown\n\n// in update()\nif (input.isKeyPressed(\"left\"))  { /* … */ }\nif (input.isKeyPressed(\"jump\"))  { /* fires once per press, not per frame */ }\n```\n\nThe third argument to `bindKey` is `lock` (default `false`). Without it,\n`isKeyPressed` stays true for as long as the key is held — correct for movement,\nwrong for jump or fire. A locked action stays consumed until the key is released\nor you call `input.unlockKey(\"jump\")`. Use `input.keyStatus(action)` when you\nwant the raw held/not-held state regardless of locking.\n\n### A poll can miss a press entirely\n\n`isKeyPressed` is sampled once per frame, and `keyup` decrements the key's\ncounter the moment it arrives — locked or not. A press that goes down and up\nbetween two frames therefore leaves the counter back at zero and is never\nseen. At 60fps that is anything under ~16ms: rare from a human finger, routine\nfrom a synthetic event, and more likely the slower the frame rate.\n\nFor a screen where a missed press is a dead end — a splash, a \"press any key\",\na notice to acknowledge — listen for the event instead, which cannot be missed:\n\n```js\nimport { event } from \"melonjs\";\n\n// action is the bound action name, or undefined for an unbound key\nconst onKey = (action) => { if (action === \"menuSelect\") continue(); };\nevent.on(event.KEYDOWN, onKey);      // in onResetEvent\nevent.off(event.KEYDOWN, onKey);     // in onDestroyEvent — it does not self-remove\n```\n\n`KEYDOWN` fires for *every* key, bound or not, so matching on `action` is what\nnarrows it; it also repeats while the key is held, so guard anything that must\nhappen once. Keep polling for movement and for a menu cursor, where one step\nper press and a key held across a state change are the things that matter.\n\nThe full signature is `bindKey(keyCode, action, lock?, preventDefault?)`;\n`preventDefault` defaults to `input.preventDefault` (itself `true`). Pass the\nfourth argument per binding, or change the global default with\n`input.setPreventDefault(false)` — **assigning `input.preventDefault = false`\ndoes not work**, it is a read-only module binding.\n\n## Pointer events need `isKinematic = false`\n\n**This is the trap.** `Renderable.isKinematic` defaults to `true`, and both the\nbroadphase and the pointer dispatcher skip any candidate whose `isKinematic` is\nstill `true`. The handler registers successfully and simply never fires.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.isKinematic = false;               // ← required\n    }\n    onActivateEvent() {\n        input.registerPointerEvent(\"pointerdown\", this, this.onDown.bind(this));\n    }\n    onDeactivateEvent() {\n        input.releasePointerEvent(\"pointerdown\", this);\n    }\n}\n```\n\n`Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*` elements set it\nto `false` for you, and so does attaching a physics body (`Body`'s constructor\nsets `ancestor.isKinematic = false`) — so plain `Renderable` and `Sprite`\nsubclasses with no body are the ones that catch people.\n\nRegister in `onActivateEvent` and release in `onDeactivateEvent`, not in the\nconstructor — pooled objects are reused and would otherwise accumulate handlers.\n\n`app.viewport` works as a whole-screen region when you want global clicks. Note\nit is asked **before** everything else, not as a fallback after nothing matched:\nthe dispatcher appends it to the candidate list and walks that list from the\nend. So a viewport handler must not return `false` unless it really means to\nswallow every pointer event in the game.\n\nThe accepted event names are exactly `\"pointerdown\"`, `\"pointerup\"`,\n`\"pointermove\"`, `\"pointercancel\"`, `\"pointerenter\"`, `\"pointerover\"`,\n`\"pointerleave\"` and `\"wheel\"`. Anything else — `\"click\"`, `\"mousedown\"`,\n`\"touchstart\"` — makes both `registerPointerEvent` and `releasePointerEvent`\nthrow `invalid event type`. melonJS maps the canonical name onto whichever\nmouse/touch events the device actually supports, so you always register the\npointer name.\n\n## Which object gets the event\n\nThe dispatcher collects the regions near the pointer from the broadphase,\nbounds-checks each one, orders them **the way they are drawn** and asks the\ntopmost first, walking down until something consumes.\n\nA handler consumes by returning `false`. This is the DOM sense and it reads\nbackwards: `false` means handled, and anything else, `undefined` included, lets\nthe walk carry on to whatever is underneath.\n\n```js\ninput.registerPointerEvent(\"pointerdown\", this, () => {\n    this.fire();\n    return false;        // nothing under this gets the click\n});\n```\n\nThree things decide the order, and only the first is obvious:\n\n- **`pos.z`, among siblings of one container.** Higher is on top.\n- **Across containers, the common ancestor decides.** `pos.z` is\n  container-local, since `autoDepth` numbers each container's children from 1,\n  so a button at `z = 8` inside one panel does not outrank a second panel at\n  `z = 2`. The pair is resolved by walking both up to their shared parent and\n  comparing the two siblings there, which is exactly how drawing resolves it.\n  Raise a whole panel with `ancestor.moveToTop(panel)`, not by inflating a\n  child's `z`.\n- **A child is asked before the container holding it**, since it draws over it.\n\nEqual `z` between siblings falls back to child order, lower index on top. Two\ncallbacks registered on the **same** region for the same event run\nlast-registered-first.\n\nConsuming a **move** also takes the pointer away from the regions below: each\none still holding it gets its `pointerleave`, because it is covered now and\nbeing inside its own bounds no longer decides. Consuming a press, a release or a\nwheel does not, since none of those says where the pointer is.\n\n## World coordinates versus screen coordinates\n\nThe pointer carries several, and picking the wrong one produces a feedback loop:\n\n| property | meaning | use for |\n|---|---|---|\n| `pointer.gameScreenX` / `gameScreenY` | canvas-relative position | anything that **moves the camera** |\n| `pointer.gameWorldX` / `gameWorldY` | projected into world space | picking and spawning objects |\n| `pointer.gameLocalX` / `gameLocalY` | relative to the region's ancestor container | hit-testing inside a container |\n| `pointer.gameX` / `gameY` | **context-dependent** — see below | region handlers on non-floating regions |\n\n`gameX`/`gameY` is not a fixed coordinate space. The dispatcher rewrites it per\ncandidate just before calling your handler: world coordinates for a normal\nregion, screen coordinates for a `isFloating === true` region. On the global\n`event.POINTERMOVE` broadcast it is set to the **screen** coordinates. When you\nneed one specific space, read `gameWorldX`/`gameWorldY` or\n`gameScreenX`/`gameScreenY` by name rather than relying on `gameX`.\n\nUsing world coordinates for a camera-orbit drag means the same pixel maps to a\ndifferent world point every frame as the camera moves — the drag jumps wildly.\n\n## Pointer move and release outside a region\n\nRegion handlers only fire while the pointer is over the region. For drag\ntracking, listen globally:\n\n```js\nimport { event } from \"melonjs\";\nevent.on(event.POINTERMOVE, (pointer) => { /* … */ });\n```\n\nThere is **no global pointer-up system event** — `event.ts` publishes only\n`POINTERMOVE`, `POINTERLOCKCHANGE`, `DRAGSTART` and `DRAGEND`. For \"released\nanywhere\" you need a raw listener:\n\n```js\nglobalThis.addEventListener(\"pointerup\", onRelease);\n```\n\nFor plain drag-and-drop, `Draggable` / `DropTarget` already wire all of this up\n(`pointerdown` / `pointerup` / `pointercancel` regions plus a global\n`POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`. `DRAGEND`\nstill originates from a region handler, so it only fires when the release lands\non the draggable itself.\n\n## Gamepad\n\nGamepad input maps onto the same action strings, so game code does not branch:\n\n```js\ninput.bindGamepad(0,\n    { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 },\n    input.KEY.X);        // → the \"jump\" action bound above\n```\n\n**Order matters:** `bindGamepad` throws `no action defined for keycode N` if the\nkeycode has not already been passed to `bindKey`.\n\nAxes use the same call with a threshold, and bind **one half of the axis each**\n— the sign selects which half:\n\n```js\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold: -0.15 }, input.KEY.LEFT);\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold:  0.15 }, input.KEY.RIGHT);\n```\n\n**`threshold` is added to the deadzone, it does not replace it.** The test is\n`abs(value) >= deadzone + abs(threshold)`, so with the default deadzone of 0.1\na threshold of `0.5` does not fire until 0.6 of full travel, and `0.15` fires at\n0.25. Pick the number you want the stick to break at and subtract the deadzone.\nRaising `setGamepadDeadzone` moves every axis binding with it.\n\n`unbindGamepad(index, button, type?)`, `setGamepadDeadzone(value)` and\n`setGamepadMapping(id, mapping)` complete the surface.\n\n### Which index to bind\n\nBindings are per pad **index**, and the index is plug order, not identity —\nnothing reports which pad is \"the\" controller. Slot 0 is a guess: a wheel, a\nspare pad or a keyboard-shaped device left plugged in can hold it for a whole\nsession while the player presses buttons on slot 1.\n\nSo a one-player game binds **every slot it will accept** to the same actions,\nand lets `activeGamepad()` say which one is in use when something has to go\nback the other way:\n\n```js\nfor (let pad = 0; pad < 4; pad++) {\n    input.bindGamepad(pad, { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 }, input.KEY.X);\n}\ninput.activeGamepad();   // the last pad to produce a bound input, else the\n                         // first connected one, else -1\n```\n\nBinding spare slots costs nothing: the poll skips a bound index with no pad in\nit. `activeGamepad()` follows **bound inputs only**, which is what makes it an\nanswer about the player rather than about electrical noise — a worn stick at\nrest reports movement for ever and would otherwise claim to be the active pad.\n\n### Rumble\n\n```js\ninput.vibrateGamepad(input.activeGamepad(), {\n    duration: 120, strongMagnitude: 0.6, weakMagnitude: 0.9,\n});\ninput.stopGamepadVibration(index);   // cut a long effect short\ninput.hasGamepadVibration(index);    // for an options-screen toggle\n```\n\nSafe to call blind: no pad, no motors, or no Gamepad API at all resolves\n`false` and does nothing, so game code never branches on support. Magnitudes\ndefault to **full** — the underlying web API defaults every parameter to zero,\nwhich makes a bare `playEffect` a silent no-op. A second call preempts the\nfirst on the same pad, so a rapid series of hits reads as a rapid series of\nhits rather than as a queue. For a SUSTAINED rumble issue one effect with a\nlong `duration` instead of calling every frame.\n\nHot-plugging needs no polling of your own — the engine listens for the browser's\nown events and republishes them:\n\n```js\nimport { event } from \"melonjs\";\n\nevent.on(event.GAMEPAD_CONNECTED,    (gamepad) => { /* gamepad.id, .index */ });\nevent.on(event.GAMEPAD_DISCONNECTED, (gamepad) => { /* pause, show a prompt */ });\n```\n\nBindings survive a disconnect: they are attached to the index, not to the\ndevice, so a pad unplugged and plugged back into the same slot keeps working.\n\n## Virtual controls\n\nOn-screen buttons synthesise key events, again reaching the same actions:\n\n```js\ninput.triggerKeyEvent(input.KEY.LEFT, true);    // press\ninput.triggerKeyEvent(input.KEY.LEFT, false);   // release\n```\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| pointer handler never fires | `isKinematic` left at its `true` default |\n| handlers fire twice, or on dead objects | registered in the constructor instead of `onActivateEvent` |\n| camera drag jumps around | using world coordinates instead of `gameScreenX`/`gameScreenY` |\n| `gameX` holds screen coords where world coords were expected | read on `event.POINTERMOVE`, or on a floating region — use `gameWorldX`/`gameWorldY` |\n| drag stops when leaving the object | region events end at the region — use `event.POINTERMOVE` |\n| release never detected outside the region | no global pointer-up event — add a raw listener |\n| jump fires every frame while held | `bindKey` without its third `lock` argument |\n| `invalid event type` thrown at registration | non-pointer name (`\"click\"`, `\"mousedown\"`, …) passed to `registerPointerEvent` |\n| `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |\n| stick has to be pushed well past its `threshold` before it registers | `threshold` is ADDED to the deadzone, not used instead of it |\n| one half of a stick does nothing | each half is a separate `bindGamepad`, chosen by the sign of `threshold` |\n| a widget under an overlapping panel still reacts | the panel consumes nothing — return `false` from its `onClick` and `onOver` |\n| a widget stays highlighted after the pointer moves onto a panel over it | the panel consumes no `pointermove`, so nothing takes the pointer off the widget |\n| a child's big `z` does not put it in front of another container | `pos.z` is container-local — use `ancestor.moveToTop()` on the container |\n| every click in the game stops working | a handler on `app.viewport` returning `false`, which is asked first |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic` also gates the physics broadphase\n- `melonjs-ui-and-text` — `UISpriteElement` and `UITextButton` handle this for you\n","packages/melonjs/skills/melonjs-tilemaps/SKILL.md":"---\nname: melonjs-tilemaps\ndescription: \"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 registerTiledObjectClass and 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, registerTiledObjectClass, registerTiledObjectFactory, TMXObject, resolvedProperty, resolvedProperties, tile class, tile properties.\"\nlicense: MIT\n---\n\n# Tiled maps\n\n> melonJS's flagship content pipeline. Maps are authored in\n> [Tiled](https://www.mapeditor.org/) and loaded through the `level` director —\n> you rarely touch the `TMX*` classes directly.\n\n## Loading a level\n\nPreload the map **and** everything it references — the `.tmx`, any external\n`.tsx` tilesets, and the tileset images:\n\n```js\nawait loader.preload([\n    { name: \"map1\",    type: \"tmx\", src: \"data/map/map1.tmx\" },\n    { name: \"tileset\", type: \"tsx\", src: \"data/map/tileset.tsx\" },\n    { name: \"tiles\",   type: \"image\", src: \"data/img/tiles.png\" },\n]);\n\nlevel.load(\"map1\");           // inside Stage.onResetEvent\n```\n\nAsset `type` is `\"tmx\"` for maps and `\"tsx\"` for external tilesets — those are\nthe only two type strings the loader knows. The *file* can be `.tmx` / `.tsx`\n(XML), or `.json` / `.tmj` / `.tsj`; the parser branches on the extension, not\non the type. `type: \"tmj\"` throws `unknown or invalid resource type`. You can\nalso skip `src` and pass the map inline via `data` (with `format: \"json\"` or\n`\"xml\"`).\n\n`level.load(levelId, options)` accepts `container` (default `game.world`),\n`onLoaded` (default `game.onLevelLoaded`), `flatten` (default `game.mergeGroup`),\n`setViewportBounds` (default **`true`**) and `async` (default `false`). It throws\n`level <id> not found` for an unknown id — synchronously, in both forms, because\nthat is a typo rather than a load failure.\n\nThose `game.` defaults are the engine's own, and `game` is whichever\n`Application` finished `init()` last. **Pass `container` explicitly** from a\n`Stage`, which has the right one: `level.load(\"map\", { container: this.app.world })`.\n\n**`level.load` is deferred while the game loop is running.** It calls\n`state.stop()` and finishes the load on a timer, after the current frame, so by\ndefault it returns `true` before anything is in the world. Two ways to sequence\nwork after it:\n\n```js\n// await it\nawait level.load(\"map1\", { async: true });\n// the world is populated here\n\n// ...or use the callback / event, which fire in both forms\nlevel.load(\"map1\", { onLoaded: () => this.spawnPlayer() });\n```\n\n`async: true` is the only thing that changes the return value — everything else\nbehaves identically, `onLoaded` included. Without it the call returns a boolean,\nso `await level.load(\"map1\")` is not an error and does not await the load:\n`await true` resolves immediately, while the load is still sitting on a timer.\nPass the flag when you mean to await.\n\n`level.reload()`, `level.next()` and `level.previous()` take the same `async`\noption and resolve the same value they return — so `if (level.next())` becomes\n`if (await level.next({ async: true }))`. Running out of levels reports `false`\neither way rather than throwing. `level.getCurrentLevelId()` and\n`level.levelCount()` round out the namespace. `flatten: false` wraps each Tiled\nobject group in its own `Container` named after the group.\n\n## Spawning entities from Tiled objects\n\nObjects are matched against a factory registry, in this order: the object's\nTiled **class**, then its **name**, then, for a tile object with no class of\nits own, its **tile's class**, then the structural fallbacks `\"text\"`,\n`\"tile\"` and `\"shape\"`.\n\n```js\nimport { registerTiledObjectClass, registerTiledObjectFactory } from \"melonjs\";\n\nregisterTiledObjectClass(\"Enemy\", Enemy);   // ← matches Tiled class OR name\nregisterTiledObjectFactory(\"Spine\", (settings, map) => { /* → Renderable */ });\nlevel.load(\"map1\");\n```\n\nThese are the entry points to use. The registry is Tiled's own, held by the\nobject factory, and has nothing to do with object pooling.\n\n`pool.register(className, classObj)` **also** works and registers a Tiled\nfactory as a side effect, under the name and again under an `me.`-prefixed\nalias, unless you set `pool.autoRegisterTiled = false`. That is the older\nspelling, kept working for maps and games written against it, and it now\nprints a one-off deprecation notice: `me.pool` is deprecated since 18.0.0.\n\nIts third argument no longer buys anything here. It used to make Tiled\nobjects recyclable; they are constructed directly now, and the container\nreturns children to typed pools rather than to the name-keyed one. To pool a\nclass you spawn constantly, give it a `createPool` and register it for Tiled\nseparately:\n\n```js\nregisterTiledObjectClass(\"mainPlayer\", PlayerEntity);\n```\n\nRegister **before** loading. With no match the object falls through to the\nstructural factory and becomes a plain `Renderable` with a static body — it does\nnot error, it just has none of your behaviour.\n\n`registerTiledObjectClass` **throws** if you register a *different* constructor\nunder a name already taken (re-registering the same one is a no-op).\n`registerTiledObjectFactory` overwrites and only `console.warn`s, so it is the\none to use for replacing something already registered.\n\n**Overriding a built-in name always works.** The engine's own classes are\nDEFAULTS: they fill a name nothing else has claimed, and yours takes it over\nwhenever you register, before or after a map has loaded.\n\n```js\nregisterTiledObjectClass(\"Trigger\", MyTrigger);   // whenever you like\n```\n\nThe throw is for a clash between two of *your* classes under one name. For\nthat case `registerTiledObjectFactory` overwrites and only warns.\n\n### Classes and properties set on tiles\n\nIn Tiled, a tile can carry a class and custom properties (tileset editor), and\nevery object placed from that tile shows them as its own. melonJS follows:\n\n- **The tile's class.** A tile object with no class is built by the class\n  registered under its tile's class, so a designer sets \"Chest\" once on the\n  chest tile and every chest dropped in the map is a `Chest`. The object's\n  own class or registered name still comes first, and a tile classed like a\n  built-in (`Sprite`, `Trigger`, `shape`...) stays a plain tile.\n- **The tile's properties.** Read them with `settings.resolvedProperty(name)`\n  or `settings.resolvedProperties()`, which merge the tile's properties under\n  the object's own, as Tiled's scripting API of the same names does. They are\n  **not** set on `settings` by name: only the object's own properties are,\n  as before, so a tile property named like a sprite setting never reaches a\n  plain tile sprite.\n\n```ts\nimport { registerTiledObjectClass, Sprite, type TMXObject } from \"melonjs\";\n\n// the custom properties set on the object itself, by name\ninterface DoorSettings extends TMXObject {\n    locked?: boolean;\n}\n\nclass Door extends Sprite {\n    locked: boolean;\n    facing: string;\n    constructor(x: number, y: number, settings: DoorSettings) {\n        super(x, y, { image: \"door\" });\n        this.locked = settings.locked ?? false;\n        // on the object, or else on the door tile in the tileset\n        this.facing = settings.resolvedProperty(\"facing\") ?? \"N\";\n    }\n}\nregisterTiledObjectClass(\"Door\", Door);   // and give the door tile the class \"Door\"\n```\n\n`TMXObject` is the type of the `settings` a registered class (or factory)\nreceives: the object as read from the map, with `tile` (a `Tile`) on a tile\nobject. Extend it with the properties your class reads.\n\nFor a class that is not a `Sprite` itself (a `Container` holding the tile and\nsomething else, say), `settings.tile.getRenderable()` returns the tile's sprite\nas a plain tile object would get it: from a tileset image, an image collection\nor an animated tile alike, with no region names to build by hand.\n\nmelonJS pre-registers `Renderable`, `Sprite`, `NineSliceSprite`, `Text`,\n`BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable`, `Trigger`\nand `Entity` as Tiled classes, so `Collectable` and `Trigger` work out of the\nbox. Each is registered under its `me.`-prefixed alias as well, so a map\nauthored against melonJS 1.x finds them where it looks.\n\nA `Trigger` forwards a **fixed list** of settings to `level.load()` — not\nwhatever you pass it. Today that list is `container`, `onLoaded`, `flatten`,\n`setViewportBounds` and the glTF scene options (`scale`, `rightHanded`,\n`lights`, `lightIntensityScale`, `castGroundShadow`, `shadowGroundY`); see the\n3D assets skill. Anything outside it is **dropped silently**, which is worth\nknowing before you spend an afternoon on a map that loads at the wrong scale.\n`async` is excluded deliberately — the trigger needs to sequence its own\ntransition.\n\n## Camera bounds\n\n`level.load` sets the viewport bounds to the map size for you —\n`setViewportBounds` defaults to `true`, and the handler is re-run on every\nviewport resize, centring the map when it is smaller than the screen. You only\nneed to do it by hand when you opted out:\n\n```js\nlevel.load(\"map1\", { setViewportBounds: false });\napp.viewport.setBounds(0, 0, mapWidth, mapHeight);   // now it's on you\n```\n\nThe same is true if you drive `TMXTileMap.addTo(container, flatten, setViewportBounds)`\ndirectly — note that method's `setViewportBounds` defaults to **`false`**, the\nopposite of `level.load`'s.\n\n## Collision shapes\n\nDraw collision geometry in Tiled on an object layer and melonJS turns it into\nworld shapes automatically — rectangles, polygons, polylines, ellipses and\npoints all work.\n\nThe contract is the **layer name**: melonJS lowercases it and checks whether it\n*contains* `\"collision\"` (so `collision`, `Collision`, `world collision` all\nmatch). Inside such a layer, objects **with no name** get\n`collisionType = collision.types.WORLD_SHAPE` and `type = \"static\"`. A *named*\nobject in a collision layer keeps whatever its factory gave it — that is how you\nmix triggers into the collision layer.\n\nAn object that matches no factory and carries no text or tile data falls to the\n`\"shape\"` factory: a plain `Renderable` with a static `bodyDef` built from its\ngeometry, which the world auto-registers with the active adapter on `addChild`.\n\nFor per-object physics, set the object's custom properties in Tiled; they arrive\non the settings object passed to your entity's constructor. Properties set on\nthe object's tile are read with `settings.resolvedProperty(name)`.\n\n## Map orientations\n\nOrthogonal, **isometric**, hexagonal, staggered and oblique are all supported;\nanything else throws `<orientation> type TMX Tile Map not supported!` at load.\nIsometric maps need y-sorting so objects overlap correctly:\n\n```js\napp.world.sortOn = \"y\";       // after level.load\n```\n\nThe world then keeps its objects in order as they move (`autoSort`, on by\ndefault): a character walking around passes in front of and behind the scenery\nwith no `sort()` call.\n\n`sortOn` only accepts `\"x\"`, `\"y\"`, `\"z\"` (the default) or `\"depth\"` and throws\non anything else. `Vector2d`, `Vector3d`, their observable variants and `Polygon`\nall carry `.toIso()` / `.to2d()` for converting between screen and map space.\n\n### What the y-sort can and cannot sort\n\n`sortOn = \"y\"` sorts **objects**, never tiles. A tile layer is a single\nrenderable drawn in one piece, so a character ends up in front of all of it or\nbehind all of it. Anything a character walks both in front of and behind\n(walls, trees, furniture) has to be on an **object layer, as tile objects**,\nand in the **same** object layer as the characters: each Tiled layer gets its\nown `z`, and the y-sort compares `z` before `y`. Keep the floor on a tile layer\nbelow them. This holds for any y-sorted map, isometric or a top-down\northogonal one.\n\n```\nfloor      tile layer     the ground, drawn first\ndungeon    object layer   walls, props, chests, doors, the hero\ncollision  object layer   the shapes (not drawn)\n```\n\nTile objects with no class are built for you: each one is a `Sprite` with its\ndefault body, static, over its whole image box and typed `ENEMY_OBJECT`. A\ncharacter keeping the default `collisionMask` (every type) collides with all of\nthem, so when the scenery's real collision is drawn in the collision layer,\nmask the character to it:\n\n```js\nthis.body.setCollisionMask(collision.types.WORLD_SHAPE);\n```\n\nGive the objects with behaviour a Tiled class and register it, as above.\n\nThe `y` it sorts on is `pos.y`, and Tiled objects default to a `(0, 0)` anchor,\nso that is the **top** of each image. Sorting by the top only matches sorting by\nthe feet when every image is the same height and stands on the bottom of its\nbox, the usual layout of an isometric pack. A shorter image sorts in front of\nthings it stands behind. Either pad the art to one height, or line every object\nup by the bottom of its image with `sortOffset`, which changes the order and\nnever the position:\n\n```js\nlevel.load(\"map1\");\napp.world.sortOn = \"y\";\nfor (const child of app.world.getChildren()) {\n    child.sortOffset = child.height;   // sort by the bottom of each image\n}\n```\n\nA character you create in code follows the same convention as the scenery: by\nthe top of a frame as tall as the scenery's (`anchorPoint` `(0, 0)`), or by its\nfeet when everything sorts by the bottom.\n\nTwo equal positions keep insertion order, and the one added **last** is drawn\n**behind**: a decoration placed at its wall's exact position disappears. Give\nit `sortOffset = 1`. A `Container` or a particle emitter is sorted by its top\nleft corner (an emitter's by the top of its spawn area); give it the offset to\nthe point it belongs with, such as `door.pos.y - dust.pos.y`.\n\n## Image layers\n\n`ImageLayer` gives parallax backgrounds with a ratio per axis, set in Tiled or in\ncode. Repeat behaviour matches Tiled's.\n\n## Compressed maps need a plugin\n\nA map saved with **gzip, zlib or zstd** compressed layer data throws\n`No inflate function set — GZIP/ZLIB decompression not supported!` at parse time\nwithout:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { TiledInflatePlugin } from \"@melonjs/tiled-inflate-plugin\";\n\nplugin.register(TiledInflatePlugin);   // before preloading any compressed map\n```\n\nUncompressed base64 and CSV layer data need no plugin. If a map loads in Tiled\nbut not in melonJS, check the layer compression setting first.\n\n## Rendering\n\nEach tile layer resolves to one of three `renderMode`s — `\"shader\"`,\n`\"prerender\"` or `\"perTile\"` — auto-selected in that order of preference, or\nforced per layer. The `\"shader\"` path is a single quad per tileset with a\nper-pixel GID lookup in the fragment shader. It requires **all** of:\n\n- a renderer that reports GPU tile-layer support (WebGL 2 / WebGPU — not Canvas),\n- `world.gpuTilemap !== false` (the `gpuTilemap` application setting, default `true`),\n- an **orthogonal** layer — isometric, hexagonal, staggered and oblique always\n  fall back,\n- no collection-of-image tileset, no non-zero `tileoffset`, and tile overflow\n  within the shader's 4-cell limit.\n\nWhen a layer falls back, melonJS `console.warn`s once with the reason. Practical\nconsequences: fewer tilesets means fewer batches (one packed tileset beats\nseveral small ones), and on the Canvas fallback large maps get noticeably\nslower. `world.preRender` (or a `preRender` layer property in Tiled) opts an\nunanimated layer into the offscreen-bake path instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| 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` |\n| the world is still empty right after `level.load` | the load is deferred to a timer while the loop runs — `await level.load(id, { async: true })`, or use `onLoaded` / `LEVEL_LOADED` |\n| `await level.load(id)` returned `true` rather than a promise | without `async: true` the call returns a boolean; `await true` resolves immediately and the load has not run yet — pass the flag when you mean to await |\n| `level <id> not found` | the map was never preloaded, or the asset `name` differs from the id passed to `load` |\n| `unknown or invalid resource type` | asset `type` set to `\"tmj\"` / `\"tsj\"` — use `\"tmx\"` / `\"tsx\"` with the `.tmj` / `.tsj` file |\n| camera will not scroll | `setViewportBounds: false`, or the map was added with `addTo()` (which defaults to `false`) |\n| `No inflate function set` at parse time | compressed layers without the tiled-inflate plugin |\n| collision shapes are not solid | the object layer's name does not contain `\"collision\"`, or the objects are named |\n| isometric objects overlap wrongly | `world.sortOn` not set to `\"y\"` |\n| a character is always in front of (or behind) every wall | the walls are on a tile layer, or on another object layer than the character: each layer has its own `z`, compared before `y` |\n| a small prop draws over a character standing in front of it | images of different heights: the y-sort uses `pos.y`, the top of a Tiled object's image. Pad the art, or set `sortOffset = height` on every object to sort by the bottom |\n| a decoration placed at the same position as a wall disappears behind it | a tie keeps insertion order and draws the later one behind: `sortOffset = 1` |\n| particles (or a container) at the foot of something draw over a character standing in front of it | they sort by the top of their spawn area (or the container's top left): set `sortOffset` to line them up with what they belong to |\n| a character is stopped short of walls and props placed as tile objects | it collides with their default bodies (static, `ENEMY_OBJECT`, the whole image box); mask it to `collision.types.WORLD_SHAPE` when the collision layer holds the real shapes |\n| a property set on a tile is undefined on the object's `settings` | tile properties are not copied onto `settings`: read them with `settings.resolvedProperty(name)` |\n| objects placed from a classed tile come out as plain sprites | that class is not registered, it is a built-in name, or the object has a class or a registered name of its own, which come first |\n| missing tiles or a blank map | the `.tsx` or tileset image was not preloaded |\n| tilemap slow on some machines | layer fell back off the shader path — check the console warning for the reason |\n\n## Related skills\n\n- `melonjs-physics` — what the collision shapes become once loaded\n- `melonjs-getting-started` — preloading and the asset type list\n","packages/melonjs/skills/melonjs-audio/SKILL.md":"---\nname: melonjs-audio\ndescription: \"Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay, no sound, sound stopped working, context suspended, silent after idle, clips silent but tones play.\"\nlicense: MIT\n---\n\n# Audio\n\n## Two setup rules\n\n```js\nimport { audio, loader } from \"melonjs\";\n\naudio.init(\"mp3,ogg\");                      // 1. BEFORE preloading any audio\n\nawait loader.preload([\n    { name: \"cling\", type: \"audio\", src: \"data/sfx/\" },   // 2. a PREFIX, not a file\n]);\n```\n\n1. **`audio.init(format)` must run before any audio asset is preloaded**, and\n   lists the formats you actually ship. Preloading first throws\n   `target audio extension(s) should be set through me.audio.init() before\n   calling the preloader.` It returns `false` when no audio backend is\n   available, so it is worth checking. The format list defaults to `\"mp3\"`.\n2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus\n   each configured extension and uses the first that loads — so\n   `data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename\n   gives you a 404. A `data:audio/...` URL is the one exception: it is used\n   verbatim, prefix and extension skipped.\n\n`audio.hasAudio()` says whether the browser can play audio at all, and\n`audio.hasFormat(\"ogg\")` whether it can play one codec — useful for choosing\nthe `init` list or hiding a sound toggle on a device with no audio.\n\nOrder matters — melonJS tries the listed formats left to right, so put the\npreferred one first. Two formats (`\"webm,mp3\"` or `\"mp3,ogg\"`) is the usual\nbelt-and-braces for codec coverage. Accepted tokens: `mp3`, `mpeg`, `opus`,\n`ogg`, `oga`, `wav`, `aac`, `caf`, `m4a`, `m4b`, `mp4`, `weba`, `webm`, `dolby`,\n`flac`.\n\n## Effects versus music\n\n```js\naudio.play(\"cling\");                    // fire and forget\naudio.play(\"cling\", false, null, 0.5);  // loop, onend, volume\nconst id = audio.play(\"engine\", true);  // looping; keep the id to stop it\n\naudio.playTrack(\"theme\");               // background music\naudio.stopTrack();\n```\n\nUse **`playTrack`** for music, not `play`. `playTrack(name, volume?)` always\nloops, and it records the name as the current track — which is what\n`app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via\n`state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,\n`resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there\nis only ever one current *track name*.\n\n**`playTrack` stops the track it replaces** (20.9.0 and later; before that it\nonly assigned the slot, and the previous bed kept looping underneath where\n`stopTrack()` could no longer reach it). So a scene just states its own music\nand nothing cancels anything:\n\n```js\nclass PlayScreen extends Stage {\n    onResetEvent() { audio.playTrack(\"battle\", 0.6); }\n}\n```\n\nCalling it with the track already playing **restarts** it, the same as `play()`\non any other clip. A scene that wants the bed to carry on across a screen\nchange should not call it again, rather than rely on it doing nothing.\n\n`stopTrack()` is for where nothing follows: a scene whose successor has no\nmusic of its own, or a game going quiet. `state.change` destroys the outgoing\nstage **before** it resets the incoming one, so a `stopTrack()` in\n`onDestroyEvent` and a `playTrack()` in the next `onResetEvent` are in that\norder and never cancel each other.\n\nTo end a track on an event rather than on a screen change, say a run ending\nwhile the player stays on the same screen, `audio.fade(name, from, 0, ms)`\ntakes it down gently. A fade to zero is silence, not a stop, so the voice is\nstill live until something stops it or replaces it.\n\nEvery per-clip function throws `audio clip <name> does not exist` if the clip was\nnever loaded, and `play` additionally throws when a named sprite region is\nmissing.\n\n## Audio sprites\n\nOne file carrying many effects, with named regions in milliseconds:\n\n```js\nawait loader.preload([{\n    name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { jump: [0, 450], hit: [2000, 250], music: [4000, 12000, true] },\n}]);\n\naudio.play(\"sfx\", { sprite: \"jump\" });\n```\n\n### A loop with no gap\n\nAn MP3 encoder pads the end of its file. Chrome and Firefox trim the padding when\nthey decode it, Safari keeps it, so a looping MP3 has a short silence at every\nloop there (27 ms on a 12 s clip at 22 kHz, measured). Declare a region of the\nloop's exact length and loop on that: the voice never reaches the padding.\n\n```js\nawait loader.preload([{\n    name: \"ambience\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { loop: [0, 12000] },\n}]);\naudio.playTrack(\"ambience\", { sprite: \"loop\", volume: 0.6 });\n// or, as a plain clip: audio.play(\"ambience\", { sprite: \"loop\", loop: true })\n```\n\nOgg Vorbis carries no such padding.\n\nThe optional third element marks a region as looping: it loops unless `play()` is\ntold `loop: false` (before 20.11 it only looped when `loop: true` was passed as\nwell). `play()`'s second argument\ntakes either the original `loop` boolean or a `PlayOptions` object\n(`sprite`, `loop`, `onend`, `volume`, plus the spatial `follow`, `at` and\n`stopWithTarget`) — both forms work. Object fields win over the positional\n`onend` / `volume` arguments when both are supplied.\n\n## Clip state\n\n```js\naudio.duration(\"theme\");        // seconds — 0 while still loading\naudio.duration(\"sfx\", id);      // duration of the region THAT instance plays\naudio.playing(\"theme\");         // is anything playing?\naudio.playing(\"theme\", id);     // is that instance playing?\naudio.state(\"theme\");           // \"unloaded\" | \"loading\" | \"loaded\"\n```\n\n`stop()`, `pause()` and `resume()` take an optional instance `id` too; omit it\nand the whole group is affected. `audio.stop()` with no arguments at all stops\nevery sound. `audio.seek(name)` reads the position, `audio.seek(name, s, id?)`\nwrites it; `audio.rate(name, r, id?)` changes speed and pitch together\n(`0.5`..`4.0`, `1` is normal) and `audio.fade(name, from, to, ms, id?)` ramps\nthe volume (`0`..`1`, over milliseconds).\n\n## Volume and muting\n\n```js\naudio.setVolume(0.5);           // global, 0..1\naudio.getVolume();              // read it back\naudio.mute(\"cling\");            // one clip  — mute(name, id?, shouldMute = true)\naudio.unmute(\"cling\");\naudio.muteAll();                // everything (audio.disable() is an alias)\naudio.unmuteAll();              // (audio.enable() is an alias)\naudio.muted();                  // global mute state\n```\n\n`audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`\nalso clears the current-track pointer if it named that clip.\n\n### Losing focus mutes everything\n\nWhen the window loses focus the engine mutes all audio, and unmutes it on\nfocus. That covers sound effects, procedural `tone` / `noise` and the mixer\neffects, not just the music track that `state.pause()` pauses. Set\n`pauseOnBlur: false` in the application settings if a clip has to keep playing\nin the background.\n\n## Spatial audio\n\n```js\naudio.position(\"engine\", x, y, z);\naudio.orientation(\"engine\", x, y, z);\naudio.stereo(\"engine\", -1);     // -1 left … 1 right\naudio.panner(\"engine\", { coneInnerAngle: 90, /* … */ });\n```\n\n`position(name, x, y = 0, z = -0.5, id?)` and\n`orientation(name, x, y, z, id?)` both default `id` to \"the whole group\".\nCalled with just the clip name, each of the four returns the current value —\n`stereo` gives `0` and `position` gives `[0, 0, 0]` before they have ever been\nset.\n\n### Let the engine place the sound: `follow` and `at`\n\nThe shortest path to positional audio is a play option. Both take **world\npixels**, the same numbers as `pos.x` / `pos.y`, and the engine converts to\naudio space for you:\n\n```js\n// tracks the renderable every frame, and stops when it is destroyed\naudio.play(\"ufo\", { follow: this, stopWithTarget: true });\n\n// pinned to a fixed world point\naudio.play(\"portal\", { at: { x: 1200, y: 480 } });\n```\n\n`follow` and `at` are mutually exclusive: passing both throws, rather than\nquietly picking one. `follow` re-reads the target's **absolute** position each\nframe, so a renderable nested in a moving container is placed correctly. `stopWithTarget` is what you\nwant for a sound that belongs to an entity: without it a looping clip on a\ndestroyed renderable keeps playing from wherever it last was. `audio.unfollow(id)`\ndetaches a sound and leaves it playing where it is.\n\n### Move the listener, don't do the subtraction yourself\n\n```js\naudio.setListener(player);                  // tracked every frame\naudio.listener(x, y, z);                    // or place it by hand\naudio.listener();                           // → [x, y, z] in world pixels\naudio.listenerOrientation(fx, fy, fz, ux, uy, uz);\naudio.setListener(null);                    // stop tracking\n```\n\nWith a listener in the world, source positions are **absolute world\ncoordinates**, not offsets. The older idiom — passing `source.pos.x -\nplayer.pos.x` every frame with the ear stuck at the origin — still works, but\nonly if you leave the listener at `(0, 0, 0)`; mixing the two double-counts the\nplayer's position.\n\n`setListener` places the ear at the target's absolute position after the world\nupdates. A plain renderable contributes position only, so a side-scroller keeps\nthe default \"facing into the screen\" and gets left-right panning plus distance\nfalloff from the one call.\n\nA `Camera3d` target also contributes its **orientation**, taken from its own\nbasis, so turning the camera swings the stereo image with it. That is the whole\nof what a posed 3D scene needs:\n\n```js\naudio.setListener(app.viewport);                 // in a 3D scene this is a Camera3d\naudio.play(\"drone\", { follow: saucer });         // scene coordinates, directly\n```\n\n**Y is down.** Every one of these calls takes and returns melonJS world\ncoordinates, where y grows downward; the Y-up flip Web Audio wants happens\ninside the engine. Pass `pos.y` as it is.\n\n### The defaults are tuned for pixels\n\n`audio.setSpatialDefaults()` / `getSpatialDefaults()` set the panner attributes\na sound placed through `follow` or `at` starts from, and the shipped values\nassume a 2D game measured in pixels:\n\n| attribute | default |\n|---|---|\n| `refDistance` | `240` |\n| `maxDistance` | `10000` |\n| `panningModel` | `\"equalpower\"` |\n| `distanceModel` | `\"inverse\"` |\n\nWith those, a placed sound is at full volume out to 240 px and then halves\neach time the distance doubles: about half at 480, a quarter at 960, roughly\nan eighth at 2000. `maxDistance` is where it stops getting quieter.\n\n**They apply to placed sounds only.** A plain `audio.play()` followed by a\nbare `audio.position()` keeps WebAudio's own defaults, where `refDistance` is\n`1` METRE and a source 100 px out is already near silent. That path is\nunchanged, which is what keeps old code working.\n\nWiden or tighten the field once, globally:\n\n```js\naudio.setSpatialDefaults({ refDistance: 400, maxDistance: 20000 });\n```\n\n`audio.panner(name, attrs, id?)` still overrides per clip or per voice, and it\nmerges over the current values rather than replacing them, so writing one field\nno longer resets the rest. Called without an `id` it writes the **group**\ndefaults, which is what a clip-wide tuning call looks like.\n\nFor a 3D scene where front-to-back matters, `panningModel: \"HRTF\"` is worth the\ncost; for a 2D one it mostly muddies the stereo image.\n\n### `stereo` and `position`: last writer wins\n\nBoth drive the same per-instance panner node, and the engine swaps the node to\nmatch whichever you called **last**: `stereo()` after `position()` gives you\nplain left-right panning, and `position()` after `stereo()` gives you 3D\nfalloff. Neither silently does nothing any more, and the order you call them in\nno longer matters.\n\nStill pick one per voice and stay with it. A 2D game usually wants `stereo()`\nalone; reach for `position()` (or `follow` / `at`) when distance falloff\nmatters.\n\n**Spatial audio is WebAudio-only.** On a clip loaded with `stream: true` (or\n`html5: true`) `stereo` / `position` / `orientation` return early and do\nnothing — which is exactly the case for the long music tracks people reach for\nfirst. `follow` and `at` are equally inert on a streamed clip.\n\n## Procedural sound\n\nFor beeps, blips and noise without shipping files. Both take **one options\nobject**, and `duration` (in seconds) is required:\n\n```js\naudio.tone({ freq: 440, duration: 0.2, gain: 0.3, pan: -0.5 });\naudio.tone({ freq: [440, 660, 880], duration: 0.4 });   // layered partials\naudio.noise({ duration: 0.5, filter: { type: \"lowpass\", frequency: 800 } });\n```\n\n`tone` also takes `wave` (default `\"sine\"`), `attack` (`0.005`) and `pitchSlide`\n(`1`). `noise` takes `type` (`\"white\"` | `\"pink\"` | `\"brown\"`, default\n`\"white\"`), `attack`, `pan`, `filter` and `filterSweep`. `gain` defaults to `0.1`\non both.\n\n### Sequencing: `delay`, not `setTimeout`\n\nBoth take `delay` — seconds before the sound starts, scheduled on the **audio\nclock**. That is what builds a multi-part sound: a stinger's second note, an\nexplosion's double-tap.\n\n```js\naudio.tone({ freq: 392, duration: 0.12 });\naudio.tone({ freq: 587, duration: 0.18, delay: 0.11 });\naudio.tone({ freq: [784, 1176], duration: 0.75, delay: 0.23 });\n```\n\nDo not reach for `setTimeout` here. A timer fires on the main thread, so a busy\nframe slips the note late and the sequence audibly loses time — the fault is\neasy to blame on the sound design rather than the scheduling. `delay` is\nsample-accurate because the whole envelope is scheduled up front.\n\nBoth are **silent no-ops** when there is no WebAudio context — they bail on\n`getAudioContext() === null` rather than throwing. Neither needs the clip to be\nloaded; neither goes through the file-playback path at all.\n\n`audio.getAudioContext()` and `audio.getMasterGain()` are the escape hatches for\ncustom Web Audio work; both return `null` when audio is unavailable, so guard.\nConnect a custom graph to the master gain rather than `ctx.destination` if you\nwant it to respect `setVolume` / `muteAll`.\n\n## Effects on all game audio: reverb, echo, compressor\n\nThere is no built-in reverb, delay, distortion or compressor, and no per-clip\nfilter on file playback. There does not need to be one for the common case:\nevery buffered clip and every `tone` / `noise` passes through the master gain\non its way to the speakers, so an effect inserted **after** it applies to the\nwhole mix.\n\n```js\nconst ctx = audio.getAudioContext();   // creates the context if needed\nconst master = audio.getMasterGain();\nif (ctx && master) {                   // null when audio is unavailable\n    const comp = ctx.createDynamicsCompressor();\n    master.disconnect();               // master -> speakers becomes\n    master.connect(comp);              // master -> compressor -> speakers\n    comp.connect(ctx.destination);\n}\n```\n\nAny Web Audio node or chain works the same way: a `ConvolverNode` with an\nimpulse response for reverb, a `DelayNode` with a feedback gain for echo, a\n`BiquadFilterNode` for an underwater low-pass, a `WaveShaperNode` for\ndistortion. Do it once, at startup; the master gain lives as long as the\ncontext.\n\n- **`setVolume` / `muteAll` still work**, because they act on the master gain,\n  which is now upstream of the effect.\n- **Streamed clips are not affected.** A `stream: true` (`html5: true`) clip\n  plays through an `<audio>` element and never reaches the master gain — which\n  is usually the music. Preload the music buffered if it must go through the\n  effect.\n- **It is all or nothing.** The per-instance nodes are not public, so an effect\n  on one clip only (muffling one sound behind a wall) is not possible through\n  this route; `noise` has its own `filter` for generated sounds.\n- To remove it, reverse the wiring: `master.disconnect(); master.connect(ctx.destination);`.\n\n## Autoplay: sound is silent until the first gesture\n\nBrowsers create the audio context in the `suspended` state and keep it there\nuntil a user interaction. Nothing errors — audio simply does not play, which\nreads as a bug. melonJS installs one-shot `touchstart` / `touchend` / `click` /\n`keydown` unlock listeners on `document`, so the first real gesture anywhere\nunlocks it and fires the clip's `on.unlock` callback. Design for it: start music\non the first click or key press, not on load. The usual way to guarantee one is\na gate screen — a splash, a \"press any key\", or a health/photosensitivity notice\n— whose dismissal *is* the gesture, so the menu behind it can start its music\nunconditionally. Note the unlock listens for `keydown` too, so a keyboard-only\nplayer is covered.\n\n### The context can stop again, long after it was unlocked\n\nUnlocking is only the first half. A context that has been running can be\nstopped later: melonJS suspends it itself after thirty idle seconds to save\npower, and browsers stop it for their own reasons, a backgrounded tab among\nthem. Recovery is automatic and you do not have to write it: a user gesture\nor the tab coming back to the front revives it, and a deliberate idle\nsuspension is left alone so the power saving still works.\n\n**The symptom worth memorising**, because it points straight at the cause:\nif `tone` and `noise` are audible while loaded clips are silent, the context\nis healthy and the engine's playback state is wrong. The generators only need\na live context, where a clip goes through the engine's state machine and can\nbe parked waiting for a resume. It is never \"the context died\".\n\nBoth routes ask the same owner to resume, so hearing a tone tells you the\nhardware is back; it tells you nothing about whether the clips waiting on the\nengine's state have been let go.\n\nThat asymmetry was a real Safari-only bug: thirty idle seconds before the\nfirst click left the engine waiting on a suspend the browser never confirmed,\nso every clip queued for the life of the page while generated sound played\nnormally. Fixed in 20.9, and the lesson generalises: a clip that will not play\nis a state question, not a hardware one.\n\nPlayback also starts **asynchronously**: `play()` returns an id while the\ninstance is still starting, so `playing()` can legitimately be `false` on the\nvery next line.\n\n## Load-time options\n\n```js\n{ name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n  sprite: { jump: [0, 450] },\n  pool: 5,            // finished instances kept for reuse — NOT a concurrency cap\n  rate: 1.0,          // initial playback rate, 0.5..4.0\n  mute: false,\n  loop: false,\n  autoplay: false,\n  preload: true,      // or \"metadata\"\n  format: \"mp3\",      // explicit hint when the URL has no usable extension\n  stream: true,       // long music: stream rather than decode into memory\n  on: { play() {}, end() {}, stop() {}, unlock() {} },\n}\n```\n\n`pool` (default `5`) is a **reuse cap on finished voices**, not a limit on\nsimultaneous playback: when the number of ended voices exceeds it, the surplus\nare disconnected and dropped. Raise it for a clip triggered in rapid bursts, not\nto allow more overlap.\n\nUse `stream: true` for long music tracks — it plays through an HTML5 `<audio>`\nelement instead of decoding the whole file into memory. (`html5: true` is the\nsame switch under its backend name.) `on` accepts the full lifecycle set:\n`play`, `pause`, `stop`, `end`, `fade`, `seek`, `rate`, `volume`, `mute`,\n`unlock`.\n\nOutside a preload, `audio.load(asset, onload?, onerror?)` takes the same\ndescriptor and loads one clip at runtime — for a level's sounds fetched when\nthe level starts. The loader calls the same function, so the same rules apply.\n\nRe-preloading a manifest that already contains a loaded clip is a no-op —\n`audio.unload(name)` first if you genuinely want to reload it.\n\n**Credentials use the same global switch as every other asset type** —\n`loader.setOptions({ withCredentials: true })`, not an audio-specific option.\nBuffered audio fetches through the shared loader path, so it honours it exactly\nas an image or a JSON file does. A `stream: true` clip does not: it plays\nthrough an `<audio>` element, which needs a `crossorigin` attribute rather than\nfetch credentials. Preload a clip buffered if it sits behind a login.\n\n**Failing clips and `setStopOnAudioError(false)`.** The default is to fail the\npreload when a clip cannot be loaded after its retries: the error reaches the\nloader, so `await loader.preload(...)` rejects and your `catch` runs, and the\nreason is logged. Turn the flag off and the engine mutes audio, warns\n(`failed loading <name>, disabling audio`) and lets the load *continue* — the\npreload finishes and the game reaches the next scene without sound.\n\nBefore 20.4 both halves were broken: with the flag off it disabled audio and\nfailed the preload anyway, and a clip whose bytes arrived but would not decode\n(a missing file served as HTML by an SPA rewrite, say) reported nothing at all\neither way — blank screen, empty console.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| audio assets 404 | `src` given as a full filename instead of a directory prefix |\n| `target audio extension(s) should be set...` thrown at preload | `audio.init()` not called before preloading |\n| `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |\n| nothing plays until the user clicks | browser autoplay policy — expected, design around it |\n| `playing()` is false right after `play()` | playback starts asynchronously |\n| music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |\n| two music tracks playing at once, and `stopTrack()` only silences one | melonJS before 20.9.0: `playTrack` did not stop the previous track, so the scene that started it had to `stopTrack()` on the way out |\n| the music restarts when returning to a screen | `playTrack` with the track already playing restarts it; don't call it again if the bed should carry on |\n| `stereo` / `position` do nothing | the clip was loaded with `stream: true` — spatial is WebAudio-only |\n| one format works, another does not | that codec is not in the `audio.init()` list |\n| audio behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses |\n| a streamed clip ignores `withCredentials` | `stream: true` plays through an `<audio>` element; preload it buffered instead |\n| `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |\n| `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |\n| a positioned sound is silent, or only audible right next to the player | placed by hand with `audio.position()`, which keeps WebAudio's metre defaults (`refDistance: 1`) — use `follow` / `at`, or set `refDistance` in pixels with `audio.panner()` |\n| a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |\n| a followed sound pans to the wrong side as the player moves | the listener was moved **and** positions are still relative to the player — with a listener in the world, pass absolute world coordinates |\n| a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |\n| a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |\n| a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |\n| a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see \"A loop with no gap\") |\n| sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |\n\n## Related skills\n\n- `melonjs-getting-started` — preloading and asset ordering\n","packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md":"---\nname: melonjs-effects-and-shaders\ndescription: \"Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: Tween, easing, animate an effect, ShaderEffect, addPostEffect, removePostEffect, getPostEffect, postEffectNeedsCapture, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter.\"\nlicense: MIT\n---\n\n# Post effects and custom shaders\n\n> melonJS ships a catalogue of post effects and a dual-language shader system.\n> The recurring trap is that every shader path needs a GPU backend and\n> **degrades to nothing** on Canvas rather than erroring. Blend modes are the\n> exception — those work on all three renderers.\n\n## Applying an effect\n\nEffects attach to a renderable **or** to a camera (for full-screen grading):\n\n```js\nimport { VignetteEffect, GlowEffect } from \"melonjs\";\n\nsprite.addPostEffect(new GlowEffect(app.renderer));\napp.viewport.addPostEffect(new VignetteEffect(app.renderer));\n```\n\n`renderable.shader = …` is **deprecated since 19.2.0**, and its setter destroys\nwhatever it replaces. Use `addPostEffect` / `getPostEffect` / `removePostEffect`.\n\n### A camera effect covers the HUD too\n\nA camera's post-effect brackets the **entire** world draw — floating children\nincluded. So the obvious way to write a full-screen pass also washes over every\nHUD label in that world, which is rarely what you want.\n\nTo land a pass *after* the world but *before* the HUD, host the effect on a\nscreen-filling floating renderable ordered below the HUD's z instead:\n\n```js\nconst quad = new Sprite(0, 0, { image: anyImage });\nquad.anchorPoint.set(0, 0);\nquad.scale(viewW / anyImage.width, viewH / anyImage.height);\nquad.floating = true;                 // its own uv is now screen space\nquad.blendMode = \"additive\";\nquad.addPostEffect(myEffect);\nworld.addChild(quad, HUD_Z - 10);     // 2D: below the labels. See the note.\n```\n\nTwo things follow from the quad filling the frame: its own `uv` is screen space\n(so a pass like this needs none of the `screen_uv` / `screen_texture` builtins),\nand the incoming `color` is the quad's own texture, which a body that paints\nfrom scratch can ignore entirely.\n\n**Depth here is easy to get backwards, and the rule FLIPS between the two sort\nmodes.** Under the default `sortOn = \"z\"` a higher z draws later, i.e. on top,\nso `HUD_Z - 10` puts the pass under the labels as written above. Under\n`sortOn = \"depth\"`, which every `Camera3d` scene sets, a floating child's sort\nkey is `pos.z * pos.z` and the walk paints far to near, so the SMALLER\nmagnitude is nearer and draws on top — `HUD_Z - 10` there puts the pass OVER\nthe labels, the opposite of the comment.\n\nSo in a 3D scene give the overlay a larger magnitude than the HUD, and give\nthe HUD a small one (`melonjs-3d` and `melonjs-ui-and-text` both say a HUD\nwants a small depth, not the huge z that would put it on top in 2D).\n\nEither way, verify it rather than reasoning about it: return a flat colour from\nthe body for one frame and see what it tints.\n\n## A renderable that draws with PRIMITIVES has to ask to be captured\n\nOne effect is applied the cheap way: the renderable is drawn with the effect's\nown program instead of being captured offscreen and post-processed, which costs\nno render target. That is equivalent only when everything the renderable draws\nis a textured quad. `fillRect`, `strokeRect` and `renderer.fill(shape)` go\nthrough a batcher that never reads that shader, so **one** effect on such a\nrenderable would silently do nothing, while **two** would work, because a chain\nalways captures.\n\nSo a renderable of your own that draws with primitives says so:\n\n```js\nclass Bar extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.postEffectNeedsCapture = true;   // ← or one effect is a no-op\n    }\n    draw(renderer) {\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\n`ProgressBar` and `Trail` set it for you, and a sprite needs nothing: the flag\ndefaults to `false` and only ever changes the single-effect case.\n\n## Toggle with `enabled`, do not remove\n\n**`removePostEffect()` destroys the effect** — it calls `effect.destroy()` and\nfrees GPU resources, so the instance cannot be re-added. Same for\n`clearPostEffects()` and for reassigning the deprecated `.shader`.\n\n```js\n// ✗ destroys it; re-adding later fails or forces a recompile\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nAn effect with `effect.shared === true` opts out of auto-destroy because it is\nreused across renderables.\n\n## Built-in effects\n\nTwenty-three presets, every one constructed as `new XEffect(renderer, options)`\nand every one shipping both a GLSL and a WGSL body, so they all run on either\nGPU backend: `VignetteEffect`, `ScanlineEffect` (optional CRT curvature),\n`BloomEffect`, `GlowEffect`, `BlurEffect`, `PixelateEffect`,\n`ChromaticAberrationEffect`, `DropShadowEffect`, `FlashEffect`,\n`OutlineEffect`, `ColorMatrixEffect`, `DesaturateEffect`, `DissolveEffect`,\n`TintPulseEffect`, `WaveEffect`, `SwayEffect`, `InvertEffect`, `SepiaEffect`,\n`HologramEffect`, `ShineEffect`, `RadialGradientEffect`, `SquishEffect`,\n`ToneMappingEffect`.\n\n**Colors take an alpha.** Every color option and `setColor` takes `[r, g, b]`\nor `[r, g, b, a]`, each 0.0 to 1.0, and the alpha is the color's own opacity: a\nhalf-transparent outline, a glow at half strength, a flash that only half\ncovers. `[r, g, b]` is opaque, as it always was. Any other length throws.\n`RadialGradientEffect` takes a `Color` instead, and its alpha scales the spot.\n\n```js\nsprite.addPostEffect(new OutlineEffect(app.renderer, { color: [1, 1, 1, 0.5] }));\n```\n\n**On a sprite cut from an atlas or a tileset, the effects that read around a\npixel keep to the sprite's own frame.** `GlowEffect`, `OutlineEffect`,\n`BlurEffect`, `DropShadowEffect`, `ChromaticAberrationEffect`, `WaveEffect`,\n`SwayEffect`, `PixelateEffect` and `BloomEffect` read the neighbouring frames\nas transparent, as if the sprite were drawn on its own, so they never show\nalong its edges and the atlas needs no padding for them. Past the texture's own\nedge (a standalone image, or the outside of an atlas) nothing changes. Their `textureSize` is still the size of the whole atlas image: it is\nwhat turns a width in texels into a distance in the texture.\n\n`SwayEffect` leans a sprite or a layer from side to side, as if in a breeze: the\ntop sways most and everything from its `anchor` row down stays put, so a tree's\ntrunk or a reed's root stays planted. It suits anything planted at its bottom;\nsomething hanging from its top, like a banner, needs its own shader. It is a\n`WaveEffect`, which takes the same `anchor`, so it adds no shader. Drive it with\n`setTime(seconds)` every frame, like any time-based effect; it wraps the phase\nitself, so a raw clock never costs precision on a phone's medium-precision\nfloats.\n\n```js\nconst sway = new SwayEffect(app.renderer, { strength: 0.02, anchor: 0.9 });\ntree.addPostEffect(sway);\n// each frame\nsway.setTime(timer.getTime() / 1000);\n```\n\nRows are measured across the texture the effect samples, so on a sprite cut from\nan atlas the anchor is a row of the atlas: it suits a standalone image or a\nlayer best.\n\n`RadialGradientEffect` is a procedural soft spot: solid at the centre of its\nhost quad, fading linearly to transparent at the edge, and elliptical for free\non a non-square quad. It is the one to reach for instead of baking a disc into\na canvas, for a pickup highlight, a trigger marker or a damage flash. Its\nfalloff assumes the quad samples a FULL texture rect, so pair it with a\nstandalone image or `Renderer.getWhitePixel()` rather than a sprite that draws\na sub-region of an atlas, where the centre would land off-quad.\n\n`SquishEffect` bulges a sprite's middle out (`setBulge(0.3)`) or pinches it in\n(`setBulge(-0.3)`), its top and bottom rows staying put. Bulging out widens the\nsprite past its own frame, and the effect draws on a wider quad to make room,\nso the art needs no transparent margin. On its own it is a jelly wobble; paired\nwith a change of height it is **squash and stretch that keeps the volume**:\nscale the sprite to `scaleY` of its height about its feet and set the bulge to\n`1 - scaleY`, so a stretched sprite pinches in and a squashed one bulges out.\nEase `scaleY` back to 1 with a short `Tween.Easing.Back.Out`; an elastic ease\nkeeps it trembling long after the landing.\n\n```js\nconst squish = new SquishEffect(app.renderer);\nplayer.addPostEffect(squish);\n// every frame, from the player's update()\nconst feet = player.height * (1 - player.anchorPoint.y);\nplayer.currentTransform\n    .identity()\n    .translate(0, feet)\n    .scale(1, scaleY)\n    .translate(0, -feet);\nsquish.setBulge(1 - scaleY);\n```\n\nApply that from the sprite's own `update()`, not from a tween's `onUpdate`. A\nphysics adapter that mirrors a body's angle (Matter does) rewrites\n`currentTransform` on every step, before the renderables update, so a scale set\nanywhere else is gone by the time the sprite is drawn. Let the tween animate a\nplain `{ scaleY }` object and read it in `update()`.\n\nIt is made for a sprite carrying it as its only effect: chained with another,\nor on the camera, the frame it measures against is the whole capture, so it\nsquishes the whole frame. The bulge is outside the sprite's bounds, so culling\nand pointer events ignore it.\n\n**`BloomEffect` and `GlowEffect` are not the same thing**, and reaching for the\nwrong one is the usual disappointment here. `GlowEffect` draws a halo OUTSIDE a\nsprite's silhouette: its body returns the colour untouched wherever alpha is\nalready non-zero, so on an opaque frame it returns early on every fragment and\na camera-wide glow does nothing at all. `BloomEffect` is the screen bloom —\nbright pixels bleed into their neighbours — and it is the one to put on the\ncamera:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\n// only the brightest highlights, with a wider halo\napp.viewport.addPostEffect(\n    new BloomEffect(app.renderer, { threshold: 0.85, radius: 4 }),\n);\n```\n\nIts tap offsets are in texels, so it takes its size from the renderer at\nconstruction; call `setTextureSize(w, h)` from an `event.ONRESIZE` handler, or\nwhen applying it to something smaller than the frame.\n\n**`ToneMappingEffect` goes after it, and goes last.** Bloom spreads the bright\nparts; the tone map is what stops the sum of the two clipping flat at white,\nwhich is the difference between an effect that reads as light and one that\nreads as a white sticker. `exposure` lifts the image into the curve, and the\ncurve (`aces`, `reinhard` or `exponential`) is baked at construction:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\napp.viewport.addPostEffect(\n    new ToneMappingEffect(app.renderer, { exposure: 1.6 }),\n);\n```\n\nIt is a grade, not a colour pipeline. By default melonJS renders into `RGBA8`\neverywhere, so the frame reaching it is already clamped and `exposure` past 1\nis what gives the curve range to compress.\n\n**`hdr: true` in the application settings changes that**: render targets\nbecome half-float, so the chain carries values above 1 instead of clamping at\nevery write, and the bloom threshold starts discriminating genuinely\nover-bright pixels rather than merely bright ones. Off by default: it costs\nGPU memory on the camera's two render targets and depends on the driver.\n\nOn its own it is **not HDR output**: the frame is still presented in SDR, so\nanything above 1 clamps on the final blit. What it buys is that nothing clamped\nBEFORE then. Removing that last clamp is a second, separate opt-in —\n`hdrOutput`, below — and it is WebGPU-only.\n\n```js\nconst app = new Application(1024, 576, { hdr: true, … });\n// …and check what you actually got, since a driver can refuse it\nif (app.renderer.supportsHDR) { /* the chain now carries values above 1 */ }\n```\n\nBranch the GRADE on that flag, not just the feature. An exposure under 1 and a\nbloom threshold at 1 make sense only where values above 1 exist; applied to a\nchain that clamps, the same numbers tone map an already display-referred frame\nand switch the bloom off, which reads as a dark, flat picture rather than as a\nmissing feature. Where there is no headroom, squeeze each emitter into range in\nits own shader instead and put the bloom threshold back under 1.\n\n**Nothing is added to your chain.** `hdr` is additive: the same frame, plus\nadditive content that no longer saturates per write and a threshold that means\nsomething. Anything still above 1 at the end clamps on the blit, as it always\ndid.\n\nA tone map is therefore a decision, not a consequence, and it is a bigger one\nthan it looks. Every curve maps 1 to less than 1, so a 2D look built ON the\nclamp — clipped white specular, saturated neon — goes matte the moment you add\none, at any exposure. Add one when your art is authored in linear light and you\nwant the highlights rolled off; skip it when the clipping IS the look.\n\nIt applies to the CAMERA chain only. A sprite's chain keeps 8-bit targets on\npurpose: its final blit composites with `ONE_MINUS_SRC_ALPHA`, and additive\nblending accumulates alpha past 1 in a float target, so that factor would go\nnegative and subtract the backdrop.\n\n### Once it is on: putting values above 1 into the frame\n\n`hdr: true` on its own changes nothing visible. It removes a ceiling; something\nstill has to reach for it. No single draw exceeds 1 on its own, so a game that\nturns the setting on and changes nothing else renders very nearly the frame it\ndid before and looks like the feature is broken. The exception is worth knowing\nbecause it is free: additive content that already overlapped was being clipped\nat every write, and stops being.\n\nWhat produces over-bright pixels:\n\n```js\n// a built-in colour matrix, scaled past 1 — no custom shader needed\nsprite.addPostEffect(new ColorMatrixEffect(app.renderer).brightness(4));\n\n// in 3D, both of these were always unbounded; the 8-bit target was the only\n// thing destroying the range. `emissive` is a 3-component Float32Array, set\n// from the mesh settings or replaced wholesale — it is not a Color\nconst mesh = new Mesh(x, y, { emissive: [2.5, 1.8, 0.4], /* … */ });\nlight.intensity = 3;\n\n// and additive blending, which accumulates past 1 once the target can hold it\nsprite.blendMode = \"additive\";\n```\n\n**One effect, not two.** A renderable carrying exactly ONE post effect takes\nthe renderer's `customShader` fast path: no offscreen target, and it draws\nstraight into the camera's half-float target with its range intact. A chain of\ntwo or more is routed through a pooled intermediate that stays 8 bits per\nchannel on purpose, so the energy is clamped there, before the camera ever sees\nit. Adding a second effect to an emitter is enough to silently undo its\nheadroom.\n\n### Everything inside the camera is graded, including your UI\n\nThe tone curve runs on the camera's whole frame. A `floating` HUD is still\ninside it, so a label authored at display white arrives at the curve as 1 and\nleaves at `1 - exp(-exposure)` — at exposure 0.45 that is 0.36, and the HUD\ncomes out at roughly a third of its brightness for no reason the author can\nsee.\n\nUI in an HDR chain has to be authored for that chain, exactly as the scene is:\n\n```js\n// drive the readout so the curve lands it back at display white. `shared`\n// keeps one instance across every element: it opts the effect out of the\n// per-renderable auto-destroy, so the first element torn down does not take\n// the program the others are still using\nconst energy = new ColorMatrixEffect(app.renderer).brightness(2);\nenergy.shared = true;\nfor (const element of hudElements) element.addPostEffect(energy);\n```\n\nPut it on each ELEMENT, not on their container: `beginPostEffect` clears\n`customShader` for any renderable with no effects of its own, so a container's\nshader is thrown away by its first child. The alternative is to keep the UI out\nof the camera pass entirely, by overriding `Stage.draw` and drawing it after\n`super.draw(...)` — at the cost of the bloom that makes additive UI glow.\n\nThe quad trick under *A camera effect covers the HUD too* does not help here.\nIt works by hosting a pass BELOW the HUD's z, and a tone map cannot live there:\nit has to be the last thing before the blit to the 8-bit canvas, which is the\ncamera. Anything the camera draws goes through it.\n\nPick the multiplier by measuring against the no-headroom build, not by\ninverting the curve. Inverting it over-estimates, because additive UI is inside\nthe BLOOM too and driving it harder also makes it spill wider into the same\npixels.\n\n### `hdrOutput`: actually presenting in HDR, and what it costs you\n\n`hdr` gives the chain headroom and then throws it away at the very last step,\nbecause the surface the browser presents into is 8 bits per channel. Values\nabove 1 clamp there, exactly as they did before the setting existed.\n`hdrOutput` changes that surface, so they reach the compositor instead:\n\n```js\nconst app = new Application(1024, 576, { hdr: true, hdrOutput: true, … });\nif (app.renderer.supportsHDROutput) { /* the display is getting values above 1 */ }\n\napp.renderer.setHDROutput(false);   // and at runtime, for a settings screen\n```\n\n**It is WebGPU-only, and that is not a melonJS limitation.** The WebGPU\nspecification defines a tone mapping mode on the canvas configuration, and two\nengines ship it. The WebGL2 equivalent needs a drawing-buffer tone mapping mode\nthat is still an unapproved specification change with no implementation in any\nbrowser; the colour-space half of it exists in Chrome behind\n`--enable-experimental-web-platform-features`, which is not something you can\nask a player to enable. So the WebGL2 backend reports `false`, warns once if\nyou asked, and presents in SDR as it always has.\n\nThat makes this the first setting where **the same machine answers differently\ndepending on which backend `video.AUTO` picked**. Branch on\n`renderer.supportsHDROutput`, never on the setting.\n\n**It requires `hdr`** — the last pass blits from the camera's target, so if\nthat target is 8 bits there is nothing above 1 left to present and the extra\ncost would buy an identical picture. Turning `hdr` off turns this off with it.\n\n**It changes how your game looks, on an HDR display.** Everything else here is\ncareful not to: `hdr` is additive, the tone curve defaults to none. This one\nremoves the final clamp, so anything you drove above 1 — an additive stack, an\nemitter, a `brightness()` above 1 — gets genuinely brighter rather than\nsaturating at white. That is the point of it, and it is why it is a separate\nopt-in rather than part of `hdr`. Two consequences worth planning for:\n\n- a look built on clipped whites will not survive it unchanged, because the\n  clipping is what you were art-directing against\n- a tone curve and `hdrOutput` pull in opposite directions. A curve exists to\n  fit values into `[0, 1]`; if you have removed the clamp, either drop the\n  curve or give it a white point high enough to leave the top of the range\n  alone\n- **`\"aces\"` cannot produce HDR output.** Its expression clamps to `[0, 1]`\n  internally, so everything past the shoulder collapses to one value before\n  the white point scales it — different bright things come out equally bright.\n  Under `hdrOutput` use `\"reinhard\"` or `\"exponential\"`, which approach 1\n  asymptotically without clamping and so keep their ordering when the white\n  point lifts them past it, or use no curve at all\n\n**What it is not.** It does not make an SDR display brighter, and there is no\nway to ask melonJS how bright the panel is — `matchMedia(\"(dynamic-range:\nhigh)\")` is the closest the platform gets. Calibrating in nits, the way a\nconsole settings screen does, is not reachable from the web platform today.\n\n### Exposure, and the runtime toggle\n\nUnder `hdr` the useful exposure is often **below** 1, which surprises people who\nhave only used the effect as an LDR grade. The curve has to work where it\nseparates: at exposure 1 the ACES shoulder maps 2, 4 and 8 to 0.86, 0.96 and\n0.99, so genuinely different energies all arrive at white and the headroom buys\nnothing you can see. Lower exposure spreads them out. The right value is a\nproperty of how bright your scene is authored, so measure it; there is no\ndefault that is right for two different games.\n\nA tone curve is a setting rather than an effect you wire up:\n\n```js\nrenderer.setToneMapping(\"aces\", { exposure: 0.4, white: 6 });\nrenderer.setToneMapping(\"none\");                       // and off again\n```\n\n`\"none\"` by default. The renderer keeps the curve LAST on the camera, so\nwhatever you put in front of it is mapped along with everything else. Changing\nthe mode rebuilds one shader (the curve is baked); `exposure` and `white` are\nuniform writes, so a settings screen can drive them live. `white` is the value\nthat comes out as display white — the knob a \"brightness\" or \"peak\" slider\nwants, since exposure alone drags the midtones up with the highlights.\n\n`renderer.setHDR(enable)` moves the whole pipeline at runtime, the way\n`setAntiAlias` does, for a settings toggle. It empties the render-target pool\nwhen the format actually changes, so it is not something to drive per frame.\nThe boot header reports what happened either way:\n\n```\nresolution: requested 1024x576 (HDR), got 1024x576 (SDR)   ← the driver refused\n```\n\nFor grading without writing a shader yourself, the camera has a colour matrix:\n\n```js\napp.viewport.colorMatrix.contrast(1.1).saturate(1.1);\n```\n\nIt is still *implemented* as one: a non-identity matrix makes the camera append\nan internal `ColorMatrixEffect` to its post-effect chain for that frame, so it\nneeds a GPU backend like everything else here.\n\n## Custom effects are dual-language\n\nA custom `ShaderEffect` supplies a fragment **body**, not a whole program. To run\non both GPU backends it needs GLSL *and* WGSL — a GLSL-only effect silently does\nnothing when `video.AUTO` lands on WebGPU.\n\n```js\nconst effect = new ShaderEffect(app.renderer, { glsl, wgsl });\neffect.setUniform(\"uStrength\", 0.5);\neffect.setTexture(\"uScene\", tex);\neffect.setTime(timer.getTime() / 1000);   // setTime takes SECONDS; getTime is ms\n```\n\nUniform names are shared across the two bodies — in WGSL they are the members of\none `@group(3) @binding(0) var<uniform>` struct — so a single `setUniform` call\nfeeds whichever backend is live. `setTime` is a convenience for a `uTime`\nuniform and silently does nothing if the shader does not declare one.\n\n**Never put a backtick in a shader body, including in its comments.** An inline\nbody is a JavaScript template literal, so a backtick ENDS it — and what follows\nis parsed as code, which fails somewhere further down the file with an error\nthat says nothing about shaders:\n\n```js\nconst glsl = `\n// x * x, not `pow(x, 2.0)`: the base goes negative here   <-- ends the literal\nvec4 apply(vec4 color, vec2 uv) { ... }\n`;\n```\n\nMarkdown-quoting an identifier is a reflex when writing a comment, and this is\nthe one place it breaks the file. Write `pow(x, 2.0)` bare. A body loaded from\na `.glsl` / `.wgsl` file has no such problem, which is one more reason to move\na shader out once it stops being a sketch.\n\n### One effect is one program — prefer preloading, and share\n\nA `ShaderEffect` compiles and links in its **constructor**, and the link is\nchecked with `getProgramParameter(LINK_STATUS)` straight after `linkProgram` —\na blocking call. So the cost is paid on whatever frame the effect is\nconstructed, and *n* identical effects cost it *n* times. `clone()` does not\navoid it: it constructs a new effect and links a new program.\n\nPreload it instead. Loader type `\"shader\"` takes a `src` **or inline source via\n`data`**, compiles at load time (inside the loading screen), and hands back a\n**shared** instance:\n\n```js\nloader.preload([{ name: \"ramp\", type: \"shader\", data: myFragmentBody }]);\n// …then, for every label that wants it:\nlabel.addPostEffect(loader.getShader(\"ramp\"));\n```\n\n### Declare a level's effects with that level's assets\n\nAn effect the loader knows about can be prepared before anything draws it; an\neffect built inline cannot, because it does not exist until the code that\nconstructs it runs — which is the stage or level you were hoping to speed up.\nThat is the practical reason to prefer the asset form even when the source is a\nstring in your own module:\n\n```js\n// with the level's other assets, not in the stage that uses them.\n// `preload()` rather than `load()` — preload is what warms the shaders it\n// brought in, and it can be called again per level, not just at boot\nawait loader.preload([\n    { name: \"ripples\", type: \"shader\", src: {\n        glsl: \"assets/level2/ripples.glsl\",\n        wgsl: \"assets/level2/ripples.wgsl\",\n    }},\n    // …that level's textures, maps and audio alongside it\n], undefined, false);   // `false`: stay on this stage, no loading screen\n```\n\n`src` takes the dual-language pair, so one asset carries both realizations and\nthe two backends cannot drift apart. `data` takes inline source the same way.\n\nWith `prewarm` on (the default since 20.6), `loader.preload()` warms on its way\nthrough — automatically, on every call — and compiles the GPU-side objects for\nevery shader it holds — on WebGPU that is the\nWGSL module, which is otherwise built inside the draw path the first time the\neffect is used.\n\nSince 20.10 the rest is warmed when the effect is **attached**, inline effects\nincluded: `addPostEffect` (or assigning `shader`) compiles the pipelines that\neffect will draw with on that renderable — on WebGPU the costly part, keyed by\nthe renderable's blend mode and target as well as the shader — and on WebGL\nlinks the spliced program when the host is a mesh. It finishes off the frame,\nso attach when you build (a pool, a stage starting), not when you show: an\neffect attached and drawn in the same frame still compiles on that frame.\n\nOne caveat if you are moving an inline effect to a file: a template literal can\ninterpolate constants into the source (`vec2(${ASPECT}, 1.0)`), and a static\nfile cannot. Promote those to uniforms rather than duplicating the numbers\nacross the GLSL and WGSL copies — duplicated constants are exactly how the two\nbackends drift.\n\nSharing one instance across renderables is safe *because* the loader sets\n`effect.shared`. Without that flag a renderable's teardown destroys its post\neffects, which would free the program out from under everything else still\nusing it. Uniforms live on the effect, so a shared instance means one set of\nuniform values — fine when the look is uniform, and the reason to clone (and\npay a link) when it is not.\n\nNote this covers **your** effects only. The engine's own mesh/quad shader\nvariants are covered by the loading-screen warm-up and by the per-renderable one\n— see `melonjs-loading-assets`.\n\n### Screen-reading builtins\n\nFor refraction, distortion and anything that samples what is already drawn:\n\n- **`screen_texture`** — a sampler kept filled with everything drawn so far\n  (GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`)\n- **`screen_uv`** — this fragment's 0..1 position in that capture\n- **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame\n\nIn WGSL the capture is sampled through `screen_sampler` (clamped) or\n`screen_sampler_repeat` (wrapping) instead of GLSL's `: screen_texture`\nannotation. `screen_uv` is y-**up** in GLSL and y-**down** in WGSL, which exactly\nmatches each backend's own capture orientation — so a body that samples\n`screen_texture` at `screen_uv` needs no flip in *either* language.\n\nTwo places where a straight port really does differ:\n\n- **A capture you bind yourself.** `renderer.toFrameTexture()` grabs the frame\n  into a `Texture2d` you hand to `setTexture` — the supported replacement for\n  `readPixels` tricks. The GL capture is bottom-up (GLSL bodies sample it with\n  `1.0 - uv.y`) and opaque RGB; the WebGPU capture is top-down (the WGSL twin\n  must *not* flip) and preserves alpha.\n- **Vertical UV offsets** — a drop shadow, a directional smear. The WebGL pooled\n  multi-effect path composites through bottom-up FBOs, so declare a `uUVYDir`\n  uniform (initialise it to `1.0`) and multiply vertical offsets by it; the\n  renderer feeds `-1` on that path and `+1` everywhere else.\n\n## A ShaderEffect on a mesh is a colour hook, and drops some builtins\n\n`mesh.addPostEffect(fx)` shades a `Mesh` on **both** backends since 20.5. The\nbody is spliced into the engine's own mesh shader as a colour hook, so the mesh\nkeeps its placement, alpha cutout, lighting and fog, and `setUniform` /\n`setTime` drive it exactly as they do over a sprite. Uniform-only bodies are\nthe supported case and they are most of them.\n\nFive things the quad tier gives an effect that the mesh tier does not, each\nrefused rather than half-working:\n\n| refused | why |\n|---|---|\n| its own `setTexture` samplers | resolved by the quad tier's per-effect device state; refused on both backends |\n| `screen_texture`, `screen_uv` | need the screen capture only the quad tier takes |\n| `noise_uv` | derived from a quad's image and object size |\n| `vColor`, on WGSL only | the WGSL mesh module has no module-scope equivalent; the GLSL one declares it, so GLSL is fine |\n| `InstancedMesh` | the instanced WGSL builder drops everything between the two stages |\n\nHitting one warns **once** and falls back to the built-in shading. Nothing\nthrows, so the symptom is a mesh drawing with its plain texture, which reads as\na broken shader, a bad binding or a texture that failed to load. Check the\nconsole for the warning before you start eliminating alpha, bindings and\ntransparency: it names which of the five it was.\n\n2D renderables never hit this. `Sprite`, `Text` and the rest go through the\nquad path, which has the full feature set, so a gradient fill for text or a\nrefraction over a sprite is fine.\n\nIf you need one of the five on a mesh, supply a full dual-language `GLShader`\nrather than a fragment body, and declare the fog uniforms yourself (see\n`melonjs-3d`).\n\n## Extra textures bind in setTexture order\n\n`setTexture(\"uFoo\", …)` assigns bindings **1 and 2** to the first extra\ntexture, **3 and 4** to the second, and so on. The WGSL must match:\n\n```wgsl\n@group(3) @binding(1) var uFoo : texture_2d<f32>;\n@group(3) @binding(2) var uFooSampler : sampler;\n```\n\nCopying a two-texture effect and deleting one leaves the survivor declared at\n3/4 while it is bound at 1/2 — the program fails to build and the effect is\nsilently dropped.\n\n## Blend modes\n\nFourteen modes; thirteen of them are honoured by all three renderers:\n\n```js\nsprite.blendMode = \"additive\";   // or \"multiply\", \"screen\", \"overlay\", …\n```\n\nSix ride fixed-function blend state and cost nothing extra: `normal`,\n`additive` (spelled `add` or `lighter` too), `multiply`, `screen`, `exclusion`,\nand `none` (blending off — the source replaces the destination, alpha included).\n`none` is the one mode the Canvas backend does not implement; it reports\n`\"normal\"` instead.\n\nThe other eight cannot be expressed as a multiply-add on the destination, so the\nGPU backends capture the destination and composite through `BlendEffect` — one\ncapture plus one composite **per draw**: `difference`, `overlay`, `hard-light`,\n`color-dodge`, `color-burn`, `soft-light`, `darken`, `lighten`. Right for\naccents; expensive for hundreds of objects. The Canvas renderer reaches all\neight through `globalCompositeOperation` instead, at no extra cost.\n\nAnything else collapses to `\"normal\"`, and so do 3D meshes and `Gradient` fills\nwhatever you ask for — the latter with a one-time console warning.\n`renderer.setBlendMode(mode)` returns the mode it actually applied, so comparing\nthat against your request detects the fallback.\n\n## The Canvas fallback\n\n`ShaderEffect` on a Canvas renderer logs a warning, leaves `enabled = false`, and\nturns every method into a no-op. Nothing throws. The same happens on a GPU\nbackend when no body matches its language — a GLSL-only effect under WebGPU.\n\nThat is fine for decoration. It is **not** fine when the shader draws your\ncontent — the carrier renderable then shows through raw. A 1×1 white texture\nstretched to 280×156 renders as a white box where the effect should be.\n\nGuard when the effect is load-bearing:\n\n```js\nif (app.renderer.shaderLanguage === null) {\n    // no programmable pipeline — skip the carrier draw entirely\n}\n```\n\n`shaderLanguage` (`\"glsl\"`, `\"wgsl\"`, or `null`) is the flag to test here rather\nthan a backend name or `supportsDepthBuffer`: it answers \"can this renderer\ncompile what I am about to hand it\". Per-effect, `effect.enabled` answers the\nsame question after construction.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| effect does nothing, warning in console | Canvas fallback — no programmable pipeline |\n| effect does nothing on some machines only | GLSL-only shader, `video.AUTO` chose WebGPU |\n| a white or solid box where the effect should be | carrier renderable drawn while the effect is disabled |\n| ONE effect does nothing but two of them work | the renderable draws with primitives — set `postEffectNeedsCapture` |\n| effect cannot be re-enabled | `removePostEffect()` destroyed it — use `enabled` |\n| ported shader renders upside down | a hand-bound `toFrameTexture()` capture — GL is bottom-up, WebGPU top-down |\n| shadow/smear offset flips on some draws | vertical UV offset not multiplied by `uUVYDir` |\n| frame rate collapses with many blended sprites | an *advanced* mode (overlay, darken, …) — each draw is a capture plus a composite |\n| animated shader never moves | `setTime` fed milliseconds, or the shader declares no `uTime` |\n| squash and stretch only bulges, the height never changes, under Matter | the adapter rewrites `currentTransform` every step; set the scale from the sprite's `update()` |\n| `SquishEffect` squishes the whole screen instead of the sprite | it is chained with another effect, or on the camera; give the sprite it alone |\n| syntax error in a file whose shader you just edited, pointing at a line of GLSL | a backtick in the shader body ended the template literal — most often one wrapped around an identifier in a comment |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-renderables` — where post effects attach, and the destroy trap\n- `melonjs-3d` — the GPU-backend requirement, and why a custom mesh shader is\n  not affected by the camera's distance fog\n","packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md":"# melonjs-scenes-and-state\n\nPath: `packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md)","packages/melonjs/skills/melonjs-ui-and-text/SKILL.md":"# melonjs-ui-and-text\n\nPath: `packages/melonjs/skills/melonjs-ui-and-text/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md)","packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md":"# melonjs-particles-and-trails\n\nPath: `packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md)","packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md":"# melonjs-camera-and-drawing\n\nPath: `packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md)","packages/melonjs/skills/melonjs-lighting/SKILL.md":"# melonjs-lighting\n\nPath: `packages/melonjs/skills/melonjs-lighting/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/SKILL.md)","packages/melonjs/skills/melonjs-math/SKILL.md":"# melonjs-math\n\nPath: `packages/melonjs/skills/melonjs-math/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/SKILL.md)","packages/melonjs/skills/melonjs-deployment/SKILL.md":"# melonjs-deployment\n\nPath: `packages/melonjs/skills/melonjs-deployment/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/SKILL.md)","packages/melonjs/skills/melonjs-loading-assets/SKILL.md":"# melonjs-loading-assets\n\nPath: `packages/melonjs/skills/melonjs-loading-assets/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/SKILL.md)","packages/melonjs/skills/melonjs-renderer-backends/SKILL.md":"# melonjs-renderer-backends\n\nPath: `packages/melonjs/skills/melonjs-renderer-backends/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/SKILL.md)","packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md":"# melonjs-display-and-scaling\n\nPath: `packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md)","packages/melonjs/skills/melonjs-performance/SKILL.md":"# melonjs-performance\n\nPath: `packages/melonjs/skills/melonjs-performance/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/SKILL.md)","packages/melonjs/skills/melonjs-events/SKILL.md":"# melonjs-events\n\nPath: `packages/melonjs/skills/melonjs-events/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/SKILL.md)","packages/melonjs/skills/melonjs-plugins/SKILL.md":"# melonjs-plugins\n\nPath: `packages/melonjs/skills/melonjs-plugins/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/SKILL.md)","AGENTS.md":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/AGENTS.md)","CLAUDE.md":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/CLAUDE.md)","GEMINI.md":"# Gemini Agent & Antigravity Instructions\n\nPath: `GEMINI.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/GEMINI.md)","COPILOT.md":"# GitHub Copilot Context & Rules\n\nPath: `COPILOT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/COPILOT.md)",".cursorrules":"# Cursor IDE Native Rules\n\nPath: `.cursorrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursorrules)",".windsurfrules":"# Windsurf Cascade Agent Rules\n\nPath: `.windsurfrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.windsurfrules)",".clinerules":"# Cline Extension Native Directives\n\nPath: `.clinerules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.clinerules)","llms.txt":"# LLM Index & Context Digest\n\nPath: `llms.txt`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/llms.txt)","mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/mcp.json)","marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/marketplace.json)","plugin.json":"# Plugin Plugin Manifest\n\nPath: `plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/plugin.json)",".github/copilot-instructions.md":"# GitHub Copilot Instructions\n\nPath: `.github/copilot-instructions.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.github/copilot-instructions.md)",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/plugin.json)","packages/melonjs/skills/melonjs/README.md":"# melonjs Documentation\n\nPath: `packages/melonjs/skills/melonjs/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs/README.md)","packages/melonjs/skills/melonjs-getting-started/README.md":"# melonjs-getting-started Documentation\n\nPath: `packages/melonjs/skills/melonjs-getting-started/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-getting-started/README.md)","packages/melonjs/skills/melonjs-20-migration/README.md":"# melonjs-20-migration Documentation\n\nPath: `packages/melonjs/skills/melonjs-20-migration/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-20-migration/README.md)","packages/melonjs/skills/melonjs-renderables/README.md":"# melonjs-renderables Documentation\n\nPath: `packages/melonjs/skills/melonjs-renderables/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderables/README.md)","packages/melonjs/skills/melonjs-3d/README.md":"# melonjs-3d Documentation\n\nPath: `packages/melonjs/skills/melonjs-3d/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d/README.md)","packages/melonjs/skills/melonjs-3d-assets/README.md":"# melonjs-3d-assets Documentation\n\nPath: `packages/melonjs/skills/melonjs-3d-assets/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d-assets/README.md)","packages/melonjs/skills/melonjs-physics/README.md":"# melonjs-physics Documentation\n\nPath: `packages/melonjs/skills/melonjs-physics/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-physics/README.md)","packages/melonjs/skills/melonjs-sprites-and-animation/README.md":"# melonjs-sprites-and-animation Documentation\n\nPath: `packages/melonjs/skills/melonjs-sprites-and-animation/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-sprites-and-animation/README.md)","packages/melonjs/skills/melonjs-input/README.md":"# melonjs-input Documentation\n\nPath: `packages/melonjs/skills/melonjs-input/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-input/README.md)","packages/melonjs/skills/melonjs-effects-and-shaders/README.md":"# melonjs-effects-and-shaders Documentation\n\nPath: `packages/melonjs/skills/melonjs-effects-and-shaders/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-effects-and-shaders/README.md)","packages/melonjs/skills/melonjs-tilemaps/README.md":"# melonjs-tilemaps Documentation\n\nPath: `packages/melonjs/skills/melonjs-tilemaps/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-tilemaps/README.md)","packages/melonjs/skills/melonjs-audio/README.md":"# melonjs-audio Documentation\n\nPath: `packages/melonjs/skills/melonjs-audio/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-audio/README.md)","packages/melonjs/skills/melonjs-scenes-and-state/README.md":"# melonjs-scenes-and-state Documentation\n\nPath: `packages/melonjs/skills/melonjs-scenes-and-state/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/README.md)","packages/melonjs/skills/melonjs-ui-and-text/README.md":"# melonjs-ui-and-text Documentation\n\nPath: `packages/melonjs/skills/melonjs-ui-and-text/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/README.md)","packages/melonjs/skills/melonjs-particles-and-trails/README.md":"# melonjs-particles-and-trails Documentation\n\nPath: `packages/melonjs/skills/melonjs-particles-and-trails/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/README.md)","packages/melonjs/skills/melonjs-camera-and-drawing/README.md":"# melonjs-camera-and-drawing Documentation\n\nPath: `packages/melonjs/skills/melonjs-camera-and-drawing/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/README.md)","packages/melonjs/skills/melonjs-lighting/README.md":"# melonjs-lighting Documentation\n\nPath: `packages/melonjs/skills/melonjs-lighting/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/README.md)","packages/melonjs/skills/melonjs-math/README.md":"# melonjs-math Documentation\n\nPath: `packages/melonjs/skills/melonjs-math/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/README.md)","packages/melonjs/skills/melonjs-deployment/README.md":"# melonjs-deployment Documentation\n\nPath: `packages/melonjs/skills/melonjs-deployment/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/README.md)","packages/melonjs/skills/melonjs-loading-assets/README.md":"# melonjs-loading-assets Documentation\n\nPath: `packages/melonjs/skills/melonjs-loading-assets/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/README.md)","packages/melonjs/skills/melonjs-renderer-backends/README.md":"# melonjs-renderer-backends Documentation\n\nPath: `packages/melonjs/skills/melonjs-renderer-backends/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/README.md)","packages/melonjs/skills/melonjs-display-and-scaling/README.md":"# melonjs-display-and-scaling Documentation\n\nPath: `packages/melonjs/skills/melonjs-display-and-scaling/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/README.md)","packages/melonjs/skills/melonjs-performance/README.md":"# melonjs-performance Documentation\n\nPath: `packages/melonjs/skills/melonjs-performance/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/README.md)","packages/melonjs/skills/melonjs-events/README.md":"# melonjs-events Documentation\n\nPath: `packages/melonjs/skills/melonjs-events/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/README.md)","packages/melonjs/skills/melonjs-plugins/README.md":"# melonjs-plugins Documentation\n\nPath: `packages/melonjs/skills/melonjs-plugins/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/README.md)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/AGENTS.md)","tokens":300,"sizeBytes":149},{"name":"llms.txt","path":"llms.txt","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/llms.txt","title":"LLM Index & Context Digest","category":"root-instruction","format":"text","content":"# LLM Index & Context Digest\n\nPath: `llms.txt`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/llms.txt)","tokens":300,"sizeBytes":141},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-20-migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-20-migration/SKILL.md","title":"melonjs-20-migration","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-20-migration\ndescription: \"Use this skill when writing melonJS code that may be based on pre-20.x knowledge, when migrating a game from melonJS 19.x or earlier, or when an API you expected does not exist. Covers the mandatory async Application.init(), the WebGL 2 baseline and WebGPU backend, renderable post effects replacing the shader property, and the deprecated exports still present. Triggers on: migration, upgrade, melonJS 19, me.game, me.video.init, renderable.shader, Compositor, CanvasTexture, me.Math, deprecated.\"\nlicense: MIT\n---\n\n# Migrating to melonJS 20.x\n\n> What changed in melonJS 20.x, and which older APIs are gone, deprecated, or\n> renamed. Read this before writing melonJS code from memory — much of what is\n> published about melonJS predates 20.0.\n\n**Why this skill exists:** most melonJS material in circulation describes 19.x or\nearlier. Code written from that knowledge looks correct and fails at runtime.\nThe items below are ordered by how likely they are to be reproduced from memory.\n\n## 1. `Application.init()` is mandatory and asynchronous\n\nSince 20.0, constructing an `Application` no longer initialises it.\n\n```js\n// ✗ pre-20.x — the constructor called init(width, height, options) for you\nconst app = new Application(800, 600, { parent: \"screen\" });\napp.world.addChild(sprite);          // added, but nothing ever renders\n\n// ✓ 20.x\nconst app = new Application(800, 600, { parent: \"screen\" });\nawait app.init();                    // REQUIRED\napp.world.addChild(sprite);\n```\n\n`init()` is async because acquiring a WebGPU device is async. There is no\nsynchronous alternative, and no automatic fallback that papers over the missing\ncall. Nothing that then breaks names it: the constructor still builds\n`app.world`, so children can be added and simply never draw — no canvas is\nappended, the app never subscribes to the frame tick, and `app.renderer` /\n`app.viewport` stay `undefined` — while constructing a `Sprite` or `Text` first\nthrows a `TypeError` on the unset global `game` instead.\n\nThe signature changed too: 19.x took `init(width, height, options)`, 20.x takes\nnone, and anything passed is ignored (settings come from the constructor).\n\n**`video.init(...)` no longer exists.** The `video` module now exports only the\nfour renderer constants (`AUTO`, `CANVAS`, `WEBGL`, `WEBGPU`) — there is no\n`init` on it to call. Any code shaped like `me.video.init(800, 600, {...})` is\nfrom a removed API. Build an `Application` and await `init()`.\n\nNote that `import * as me from \"melonjs\"` still works, so `me.Sprite` style is\nfine. It is the *globals bootstrap* that is gone, not the namespace import.\n\n## 2. Post effects replaced the `shader` property\n\n`renderable.shader = …` is **deprecated since 19.2.0**. Use the post-effect API:\n\n```js\n// ✗ deprecated\nmySprite.shader = new ShaderEffect(app.renderer, glslBody);\n\n// ✓\nmySprite.addPostEffect(new ShaderEffect(app.renderer, glslBody));\n```\n\nRelated trap: **`removePostEffect()` destroys the effect** — and so does\nassigning `renderable.shader`, which destroys whatever it replaces. Only an\neffect carrying `shared = true` is spared. To turn one off temporarily, toggle\n`effect.enabled` instead: remove-and-re-add hands you back an object whose GPU\nresources have already been freed.\n\n## 3. WebGL 2 is the baseline; WebGPU is the default attempt\n\n`video.AUTO` now tries **WebGPU → WebGL 2 → Canvas**. WebGL 1 is gone.\n\nConsequences:\n\n- Which backend runs is a runtime fact. Do not write code that assumes WebGL.\n- Custom shaders can carry both GLSL and WGSL in one asset, so one effect runs\n  on either GPU backend.\n- Features needing a programmable pipeline are inert on Canvas and warn once\n  rather than throwing.\n\n## 4. Deprecated exports still present\n\nThese resolve but should not be used in new code:\n\n| deprecated | since | use instead |\n|---|---|---|\n| `CanvasTexture` | 17.1.0 | `CanvasRenderTarget` |\n| `Compositor` | 18.1.0 | `WebGLBatcher` |\n| `PrimitiveCompositor` | 18.1.0 | `PrimitiveBatcher` |\n| `QuadCompositor` | 18.1.0 | `QuadBatcher` |\n| `Math` (capitalised) | 18.0.0 | `math` (lowercase) |\n| `device.requestFullscreen` / `exitFullscreen` | 19.7.0 | `app.requestFullscreen()` / `app.exitFullscreen()` |\n| `renderable.shader = …` | 19.2.0 | `addPostEffect()` |\n| `Entity` | 18.1.0 | `Sprite` / `Renderable` + a `Body` |\n| `loader.onload` / `onProgress` / `onError` | 18.2.0, **removed 20.3** | the `LOADER_*` events, or `preload(assets, onloadcb)` |\n| `response.overlap` / `overlapN` / `overlapV` | — | `response.depth` / `response.normal` |\n| `setLineWidth()` | 17.3.0 | the `lineWidth` property |\n\nTwo of these deserve emphasis:\n\n- **`Entity`** is still exported, so code using it compiles and runs while being\n  the wrong shape for 20.x. Compose a `Sprite`/`Renderable` with a `Body`\n  instead.\n- **The capitalised `Math`** shadows the global `Math` if imported carelessly.\n  `me.Math.random()` from older docs resolves to the deprecated re-export.\n\n## 5. The global `game` is not the modern shape\n\n`game` is still exported, but since 20.0 it names the most recently\n**initialized** `Application` and is `undefined` until the first `init()`\nresolves — so anything reading it at module scope gets `undefined`. Take the\n`Application` instance as a parameter instead: `Stage#onResetEvent` and\n`onDestroyEvent` are handed it, and any renderable can reach it through\n`parentApp`.\n\n## 6. Node and SSR\n\nmelonJS imports cleanly under Node — useful for isomorphic apps and for tooling.\nIt has **no runtime dependencies** as of 20.3. Do not add polyfills for\n`globalThis`, `String.trimStart` or `String.trimEnd`: the published bundle\ntargets ES2022, so every environment that can parse it already has them.\n\n## 7. Browser support\n\nThe published bundle targets **ES2022** and uses private class members. A browser\nthat predates that cannot parse the file at all — it fails at load, not when a\nfeature is used. Transpile if you need older browsers, and note that transpiling\nhandles *syntax* only: built-in methods need separate polyfills, which a\nbundler's `target` setting will never add.\n\n## The shipped examples are not all current\n\nThe examples in this repository are the best reference for how melonJS is\nassembled, but a few contain stale comments or legacy calls. Do not learn these\nfrom them:\n\n- `webgpu/` has a comment claiming *\"`video.AUTO` never selects the WebGPU\n  backend\"*. That is pre-20 leftover — AUTO tries WebGPU **first**. The code is\n  right; the comment is wrong.\n- Several examples assign `viewport.shader = …` / `sprite.shader = …`, which is\n  deprecated. `addPostEffect()` is current.\n- Five files set `this.z = Number.POSITIVE_INFINITY` on a HUD. That property\n  does not exist; those HUDs draw on top only because they are added last.\n- `platformer/entities/player.ts` reads `response.overlapV`, the deprecated\n  field. Use `response.depth` / `response.normal`.\n- Particle speeds tuned before 20.2 look wrong now: the particle transform fix\n  changed how far a burst travels, and emitters were retuned upward to match.\n\n## Checklist when code \"should work\" but doesn't\n\n1. Is `await app.init()` there?\n2. Is the code assuming WebGL when it might be running on Canvas or WebGPU?\n3. Is it using `renderable.shader =` instead of `addPostEffect()`?\n4. Is it importing a deprecated symbol from the table above?\n5. Is it referencing a global `game`?\n\n## Related skills\n\n- `melonjs-getting-started` — the correct 20.x bootstrap in full\n- `melonjs-renderables` — post effects, pointer events and custom draw code\n","frontmatter":{"name":"melonjs-20-migration","description":"Use this skill when writing melonJS code that may be based on pre-20.x knowledge, when migrating a game from melonJS 19.x or earlier, or when an API you expected does not exist. Covers the mandatory async Application.init(), the WebGL 2 baseline and WebGPU backend, renderable post effects replacing the shader property, and the deprecated exports still present. Triggers on: migration, upgrade, melonJS 19, me.game, me.video.init, renderable.shader, Compositor, CanvasTexture, me.Math, deprecated.","license":"MIT"},"isInternal":false,"tokens":1964,"sizeBytes":7594},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-3d-assets/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d-assets/SKILL.md","title":"melonjs-3d-assets","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-3d-assets\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Loading 3D assets\n\n> glTF/GLB is the primary path for 3D content, and it goes through the **level\n> director** rather than the world directly: `level.load(\"scene\")`, the same call\n> a Tiled map uses. OBJ/MTL is the simpler path for a single static model.\n\n## Loading a glTF/GLB scene\n\n```js\nawait loader.preload([\n    { name: \"diorama\", type: \"glb\", src: \"data/scenes/diorama.glb\" },\n]);\n\nlevel.load(\"diorama\", { scale: 50 });\n```\n\n`type: \"gltf\"` (JSON plus external buffers) and `type: \"glb\"` (single binary)\nare both loader types. A self-contained GLB is the better shipping format — one\nrequest, nothing to resolve relatively.\n\nThe scene loads under whatever camera the stage has; view it under a `Camera3d`\nor it renders flat. See `melonjs-3d` for the camera.\n\n### `level.load` options for glTF/GLB\n\n| option | default | what it does |\n|---|---|---|\n| `scale` | `1` | pixels per glTF unit, applied to the whole scene. Blender's metre-scale export usually needs 20–100. |\n| `container` | `game.world` | where the nodes are added |\n| `onLoaded` | `game.onLevelLoaded` | called with the **level id**, not the scene |\n| `async` | `false` | return a promise that settles once the scene is in the world, instead of a boolean |\n| `rightHanded` | `true` | see below |\n| `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |\n| `lightIntensityScale` | — | keep authored intensity ratios instead of normalising every light to 1 |\n\nThose `game.` defaults are the engine's, not a recommendation. `game` is\nwhichever `Application` finished `init()` last, so **pass `container`\nexplicitly** from a `Stage`, where the right one is in your hand:\n\n```js\nlevel.load(\"sceneId\", { container: this.app.world, scale: 40 });\n```\n| `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |\n| `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |\n| `shadowOpacity` | `0.45` | how dark the blobs are |\n| `shadowScale` | `1` | blob footprint multiplier; `0` or less hides them |\n| `shadowOffset` | `0` | slide them along the light, in blob radii; needs `shadowGroundY`, which is the only way a scene load can name its floor |\n| `shadowStretch` | `1` | lengthen them along it, clamped to 3 |\n| `shadowDirectionX` / `shadowDirectionZ` | `0` | the ground direction they are cast along |\n| `shadowGroundNormal` | world up | up normal of the floor, for a scene that is not level |\n| `shadowLight` | — | a `Light3d` to take the direction from, re-read every draw |\n\n`shadowGroundPoint` is **not** on that list, and a scene load drops it without\na warning. One floor POINT shared by every part of a scene would stack the\nwhole thing's blobs on a single spot, which is nonsense where one floor\nHEIGHT is perfectly sensible. Ground that is not flat is a per-object answer:\nset it on the parts you want once the load has finished.\n\n`onLoaded` receives the level id — it is a \"done\" signal, not a handle on the\nscene. You need it, or `async`: with the game loop running, `level.load` stops\nthe loop and defers the actual load to a timer, so by default it returns\n*before* anything is in the world.\n\n```js\nawait level.load(\"diorama\", { scale: 50, async: true });\n// the scene is in the world here\n```\n\nNote `await level.load(\"diorama\")` without the flag does not await the load — the\ncall returns a boolean, and `await true` resolves immediately. To get at what was\nloaded, load into a container you own, or look the nodes up by their authored\nnames:\n\n```js\nlevel.load(\"diorama\", { scale: 50, onLoaded: () => {\n    const sun = app.world.getChildByName(\"Sun\")[0];   // note: returns an array\n    sun.intensity = 2;\n}});\n```\n\n### The same options from a Tiled `Trigger`\n\nA `Trigger` authored in Tiled forwards these to `level.load()`, so a trigger that\nwalks the player into a glTF level carries the scene's settings as Tiled\nproperties:\n\n```js\nworld.addChild(new Trigger(x, y, {\n    shapes: [new Rect(0, 0, 100, 100)],\n    to: \"diorama\",              // the level id — `to`, not `level`\n    scale: 50,\n    rightHanded: false,\n    castGroundShadow: false,\n}));\n```\n\nEvery `level.load` option above travels, including the whole ground-shadow set,\nand so do `container`, `onLoaded`, `flatten` and `setViewportBounds`. The list is\n**derived** from `level.loadOptions` rather than restated, so an option added to\na level format arrives here for free. It used to be hand-written and had fallen\nthree releases behind the glTF path (#1649).\n\n**`async` is deliberately excluded.** A trigger with a `color` + `duration`\nsets it itself, because it needs the promise to sequence its fade and reveal; a\ntrigger without one loads fire-and-forget and never sets it. Either way an\nauthored `async` property is ignored rather than honoured.\n\n## `rightHanded` — and the default that differs between the two APIs\n\nglTF is authored **Y-up, right-handed**; melonJS is **Y-down, +Z forward**.\nThere are two ways to bridge that, and they are not equivalent:\n\n- a **rotation** — preserves chirality, matches the authoring tool\n- a **mirror** (negate Y only) — flips the scene left/right\n\n`level.load` defaults `rightHanded: true`, so a scene loaded through the level\ndirector comes in correctly. **The `Mesh` constructor defaults it to `false`**,\nso glTF or OBJ geometry you feed to a `Mesh` yourself comes in mirrored unless\nyou pass `rightHanded: true`. A model that is subtly wrong — text backwards, a\ncharacter's gear on the wrong side — is this and nothing else.\n\nDo not add a flip of your own on top; two corrections cancel back to wrong.\n\n## Materials — what actually arrives\n\nThe importer covers a deliberate subset:\n\n- `pbrMetallicRoughness` **base colour texture and factor**\n- metallic/roughness **factors** converted to the engine's specular model\n- `emissiveFactor`, including `KHR_materials_emissive_strength`\n- `KHR_materials_unlit` — honoured, for baked/stylised workflows\n- `alphaMode: \"MASK\"` — the cutout threshold is applied\n- `doubleSided`\n\nNot imported: **normal maps**, metallic/roughness *textures*, occlusion maps,\nmorph targets, `KHR_texture_transform`. Nothing errors — the material simply\narrives without them, which reads as \"flatter than the preview in my authoring\ntool\". Bake what you need into the base colour.\n\n**Draco compression is the exception that does error.** Its geometry sits in\naccessors with no buffer view, which the parser rejects outright, so the whole\nasset fails to load. Export uncompressed.\n\n`textureFilter` on the Application settings (`\"nearest\"` / `\"linear\"` /\n`\"auto\"`) drives sampling; glTF materials that declare their own sampler carry\nit through. Pixel-art models want `\"nearest\"`.\n\n### `baseColorFactor` is LINEAR; a `tint` is sRGB\n**Pass the factor as a setting and the encoding is done for you.** `Mesh`\ntakes `baseColorFactor` — `[r, g, b]` in 0..1, linear, which is the form the\nloader hands back — so a mesh built from a node passes the node's own field\nstraight through:\n\n```js\nconst node = loader.getGLTF(\"palm\").nodes[0];\nconst mesh = new Mesh(x, y, {\n    vertices: node.vertices, normals: node.normals,\n    uvs: node.uvs, indices: node.indices, normalize: false,\n    baseColorFactor: node.baseColorFactor,   // encoded, not scaled\n});\n```\n\nThe setting's type admits `undefined`, because the loader declares the field\noptional and that line has to compile under `exactOptionalPropertyTypes`.\n`Sprite3d` forwards it too, and `mesh.setBaseColorFactor(f)` applies one after\nconstruction. For an ordinary sRGB colour use `setTint` instead — the rest of\nthis section is why the two are different.\n\n\nThe loader handles this for you. It only bites when you build meshes yourself\nfrom `loader.getGLTF(name).nodes` — which is what you do for per-node control,\nsince there is no public per-node `Mesh` factory — and reach for the obvious\n`* 255`:\n\n```js\n// WRONG — every untextured material renders far too dark\nmesh.tint.setColor(f[0] * 255, f[1] * 255, f[2] * 255, 1);\n```\n\nglTF defines `pbrMetallicRoughness.baseColorFactor` in **linear** space (spec\n3.9.2), while a melonJS `tint` is 8-bit **sRGB** — the same space as a CSS\ncolour or a texel out of a PNG. They are not the same number, and the gap is\nwidest exactly where art tends to live:\n\n| linear | `* 255` | correct | \n|---|---|---|\n| 0.05 | 13 | 63 |\n| 0.20 | 51 | 124 |\n| 0.42 | 107 | 173 |\n| 0.72 | 184 | 221 |\n| 1.00 | 255 | 255 |\n\n`Color.setLinear` does the encoding for you — it is the linear-space sibling of\n`setFloat`, which takes the same `0..1` range but treats it as already-sRGB:\n\n```js\nmesh.tint.setLinear(...node.baseColorFactor);   // encodes; what you want\nmesh.tint.setFloat(...node.baseColorFactor);    // does NOT encode; too dark\n```\n\nIt clamps, so the slightly out-of-range factors some exporters emit cannot turn\ninto `NaN`, and it keeps the encoded value as a float rather than rounding\nthrough 8 bits.\n\nMeasured on a scene whose every material went through `* 255`: mean frame luma\n85.5 against 147.8 once encoded, with each channel sitting 60 to 73 counts low.\n\n**Fix it early.** It is nearly invisible as a bug, because the scene still\nlooks coherent — just moody — so lighting and bloom thresholds get tuned\nagainst the wrong values. Correcting the encoding afterwards blows the frame\nout and forces a lighting retune, which is a much bigger job than the encode.\n\nThis applies only to factors coming out of glTF. Colours **you** author for the\nscreen, a HUD tint or a glow, are already sRGB: `* 255` is right for those.\n\n## Lights\n\nAuthored `KHR_lights_punctual` lights — sun, point, spot — become `Light3d`\nworld children, plus a soft ambient fill, and each keeps its authored name.\nA scene carrying any of them renders through the **lit** path automatically.\n\nglTF stores physical units — lux for suns, candela for lamps — and a Blender\ndaylight sun is 1000+, which blows out the engine's stylised shading. So every\nlight is normalised to intensity 1 by default. To keep the *ratios* the artist\nset, scale instead of normalising:\n\n```js\n// a 1000-lux sun lands at 1, a 500-lux fill lands at 0.5\nlevel.load(\"diorama\", { scale: 50, lightIntensityScale: 0.001 });\n```\n\nPass `lights: false` to manage lighting yourself.\n\n## Animation\n\nNode-TRS animation **is** supported: translation, rotation and scale channels,\nthe rigid hierarchical animation used by blocky/low-poly character packs. An\nanimated asset instantiates as a single `GLTFModel` keeping the node hierarchy\nintact, with an API deliberately mirroring `Sprite`. The model carries the asset\nname, which is how you get hold of it after loading:\n\n```js\nconst model = app.world.getChildByName(\"hero\")[0];   // the GLTFModel\n\nmodel.getAnimationNames();                           // [\"idle\", \"walk\", ...]\nmodel.setCurrentAnimation(\"walk\", { loop: true, speed: 1, onComplete });\nmodel.setCurrentAnimation(\"jump\", { next: \"idle\" }); // chain\nmodel.isCurrentAnimation(\"walk\");\nmodel.play();\nmodel.pause();\n```\n\n`speed` is a playback multiplier (1 = authored speed), not a frame delay. Only\nan explicit `loop: false` stops looping. `setCurrentAnimation` **throws** on a\nclip name the asset does not define — check `getAnimationNames()` first.\n\n### Placing and moving a model\n\nA `GLTFModel` is placed like any other renderable — `pos`, `depth`, `rotate`\nand `scale` move the **whole rig**, and compose with whatever the active clip\nis doing, so a walk cycle plays wherever the character stands:\n\n```js\nconst boat = new me.GLTFModel(me.loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nboat.setCurrentAnimation(\"paddle\", { loop: true });\napp.world.addChild(boat);\n\nboat.pos.set(steerX, waterLevel);\nboat.depth = travelled;\nboat.rotate(lean - lastLean, AXIS_Z);   // `rotate` is RELATIVE — feed it the delta\nboat.animationspeed = 0.85 + 0.9 * pace; // one authored tempo, many run speeds\n```\n\nConstructing one directly from `loader.getGLTF(name)` is fine and is the way to\ngo when the model is a game object rather than a scene — `level.load` is for\nloading a whole scene into the world.\n\n**Vertex skinning is out of scope** — `JOINTS_0` / `WEIGHTS_0` are not read, and\nneither are morph targets. A smoothly-deforming character will not deform. The\noptions are: rig it hierarchically instead (separate meshes parented into a\nnode tree), use `Sprite3d` billboards, or use the Spine plugin for 2D skeletal\nwork.\n\n## Instancing\n\nA node using `EXT_mesh_gpu_instancing` loads as an `InstancedMesh` — one draw\ncall for every copy, which is the difference between a hundred trees and a\nhundred thousand. Export with instancing enabled where your tool supports it.\nSparse accessors on the instancing attributes are rejected with a clear error\nrather than silently misplacing every instance.\n\n## Ground shadows\n\n```js\nlevel.load(\"diorama\", { scale: 50, castGroundShadow: true, shadowGroundY: 0 });\n```\n\n`castGroundShadow` is tri-state: set it and it overrides the application setting\nfor this scene in both directions; omit it and the scene inherits — and that\nsetting is `true` by default, so shadows are already on. As a scene-wide opt-in\nit skips nodes with no vertical extent — a ground plane is exactly that, and\nshadowing it with itself smears a blob across the whole floor. Blob shadows need\na GPU backend and a `Camera3d`; nothing is drawn on the 2D-camera or Canvas\npaths.\n\nOmit `shadowGroundY` and each blob sits at its own object's base at full\nstrength, which is right for props already resting on the ground. Set it when\nthings jump or fly, so the shadow stays on the floor and shrinks with height.\n\nA blob sits on its caster's x/z by default, so a wide flat-bottomed prop hides\nits own shadow under itself. The knob for that is `shadowScale`, which widens\nthe blob until its edge shows past the prop, or `shadowOffset` plus a direction,\nwhich slides it clear. **Not** `shadowGroundY`: raising that floats the blob up\nover the object as a dark ring instead of sliding it out, because it is for\nthings that leave the ground, not a visibility control.\n\nFor a light that is not overhead, name one: `shadowLight` takes the direction\nfrom a `Light3d` and is re-read every draw, so a day or night cycle carries\nevery shadow in the scene with it. Nothing is inferred — a scene with no named\nlight and no `shadowDirectionX`/`Z` gets no direction, because a scene with\nseveral lights has no non-arbitrary answer. `shadowStretch` lengthens the blob\nalong that direction and fades it as it pulls; `shadowGroundNormal` lays it on\na floor that is not level. All of these apply to an `InstancedMesh` too, as one\nvalue for the whole set.\n\n## OBJ/MTL\n\nA different shape entirely: OBJ produces raw geometry you hand to a `Mesh`,\nnot a scene. Preload the two files and name them in the `Mesh` settings — the\n`model` and `material` names are independent, the same name for both is just a\nconvention:\n\n```js\nawait loader.preload([\n    { name: \"fox\", type: \"obj\", src: \"models/fox.obj\" },\n    { name: \"fox\", type: \"mtl\", src: \"models/fox.mtl\" },\n]);\n\nconst mesh = new Mesh(400, 300, {\n    model: \"fox\",\n    material: \"fox\",        // texture resolved from the material's map_Kd\n    width: 200,\n    height: 200,\n    rightHanded: true,      // the Mesh default is false — see above\n});\nworld.addChild(mesh);\n```\n\n**The MTL loader fetches the textures it references itself** — `map_Kd`,\n`map_d` and `map_bump` are resolved relative to the `.mtl` and preloaded for\nyou, so there is no image entry in the manifest above. Passing `texture:`\nexplicitly overrides that and pins *one* binding over the whole model, which on\na multi-material model suppresses the per-material texture split; leave it out\nunless you mean that.\n\nMTL contributes `Kd` (diffuse tint), `d` / `Tr` (opacity), `Ke` (emissive),\n`Ks` + `Ns` (specular highlight), `map_Kd` (diffuse texture), `map_d`\n(per-texel opacity) and `map_bump` / `bump` / `norm` (tangent-space normal\nmap — needs `lit: true`, and the tangent frame is derived per fragment, so the\nmodel needs no tangent attribute). All three normal-map spellings are read as\nnormal maps even though the format specifies two of them as height maps, which\nis what every exporter actually writes. Specular *maps* (`map_Ks`) and the PBR\nextension's `map_Pr` / `map_Pm` are not supported and warn — the `Pr` / `Pm`\n*scalars* are read and approximated onto the specular terms, the maps would\nneed a real PBR model.\n\n`Ka` (ambient) and `illum` (illumination model) are read onto the material and\nreadable from `loader.getMTL(name)`, but take no part in shading. That is\ndeliberate rather than pending: `Ka` predates scene-wide ambient light and would\nfight `Stage.ambientLightingColor`, and `illum` enumerates fixed-function\nbehaviours this renderer has no equivalent for. In particular an `illum` of 0 or\n1 nominally means \"no specular\", and the engine does **not** honour that — `Ns`\nstays the only gate, so a model that renders correctly today is not restyled by\na field its exporter wrote inconsistently.\n\n`loader.getOBJ(name)` and `loader.getMTL(name)` return the parsed data if you\nwant it directly. OBJ has no scene graph, no lights, no animation — use glTF for\nanything beyond a single static model.\n\n## Authoring assets for the engine\n\nNotes from modelling props and a rigged character against this loader. They are\nBlender-specific in the API details, general in the traps.\n\n### One clip means one action\n\nglTF names an animation after the **action** it came from, so three objects\neach carrying their own action export as three separate clips — and a game can\nonly play one clip at a time, so the other two never move. Put every animated\nobject in **one action**, using slots (Blender 4.4+):\n\n```python\nshared = bpy.data.actions.new(\"paddle\")\nfor ob in (pivot, shoulder_l, shoulder_r, body):\n    ad = ob.animation_data_create()\n    ad.action = shared\n    ad.action_slot = shared.slots.new('OBJECT', ob.name)\n```\n\nExporting with the scene/whole-timeline mode does **not** merge per-object\nactions into one clip — it still emits one per action. Slots are the fix.\n\nIn Blender 5.x, `action.fcurves` no longer exists; curves live under\n`action.layers[].strips[].channelbags[].fcurves`, and a channelbag identifies\nits slot as `cb.slot.identifier` (there is no `slot_identifier`).\n\n### Never re-parent or join a rig that is mid-pose\n\nBoth parenting and joining bake the objects' **current world transforms**.\nParenting a child while the parent sits in an animated pose writes the inverse\nof that pose into `matrix_parent_inverse`, which silently cancels the animation\n— the part renders rigid while the curves still exist. Detach the action, zero\nthe rig, do the surgery, then reattach:\n\n```python\nsaved = [(o, o.animation_data.action_slot) for o in animated]\nfor ob, _ in saved:\n    ob.animation_data.action = None\n    ob.rotation_euler = (0, 0, 0)\n# ...re-parent / join here...\nfor ob, slot in saved:\n    ob.animation_data.action = act\n    ob.animation_data.action_slot = slot\n```\n\nA joined mesh also inherits the **active** object's scale, so joining into a\nbox that was scaled to `(0.8, 2.0, 0.1)` leaves a node with that non-uniform\nscale and geometry divided back through it. It renders in the right place but\nskews flat-shaded normals — apply transforms (`rotation=True, scale=True`)\nafter joining. Watch for a related trap: setting `ob.scale.x` on a mesh whose\nsize was already applied *multiplies* rather than replaces it.\n\n### A model only needs to stay split where something animates it\n\nEvery glTF primitive is a draw call. Merge everything static into one mesh and\nkeep separate nodes only for the parts a clip actually drives — a boat with a\nrabbit and a paddle went from 28 primitives to 5 that way, with no visible\nchange.\n\n### One palette strip beats one material per colour\n\nAn `InstancedMesh` is **one geometry and one material**, so a prop that wants\nfive colours cannot use five materials. Give the whole model a single material\nwhose image is an *N*×1 strip, one pixel per shade, and point each face's UVs\nat a cell centre (`u = (cell + 0.5) / N`, `v = 0.5`) with the sampler set to\nnearest. A whole scene's palette then costs one tiny texture, a merged mesh can\nbe twenty colours, and there is nothing to bleed between cells.\n\nColour-space trap when generating that strip programmatically: Blender's\n`image.pixels` are scene-linear floats, but this export path writes them out\n**verbatim**, so pre-converting sRGB→linear darkens every colour. White is the\ntell — it is the one value identical in both spaces, so if white survives and\neverything else came out dark, that is the bug. Verify by decoding the PNG back\nout of the GLB and comparing bytes to the source palette.\n\n### Feeding an authored mesh into an `InstancedMesh`\n\n`EXT_mesh_gpu_instancing` covers instances authored *in the file*. When the\ngame places them itself, take the geometry off the parsed descriptor instead:\n\n```js\nconst node = me.loader.getGLTF(\"palm\").nodes[0];\nconst trees = new me.InstancedMesh(0, groundY, {\n    vertices: node.vertices, uvs: node.uvs,\n    normals: node.normals, indices: node.indices,\n    texture: palette, textureFilter: \"nearest\", scale: 40,\n    instanceCount: count,\n});\n```\n\n`nodes` is one entry **per primitive**, each in its own local space with a\nseparate `world` matrix — so this only works cleanly when the asset is a single\nmerged primitive exported with an identity node transform. Model it at the\norigin, base on the floor, and apply transforms before exporting.\n\nOne geometry stamped out hundreds of times reads as one object *copied*\nhundreds of times, so vary each instance in its transform. Rotate **after**\ntranslating, or the instance swings around the group origin instead of turning\nwhere it stands:\n\n```js\nplacement.identity().translate(x / s, -y / s, z / s)\n         .rotate(math.randomFloat(0, Math.PI * 2), AXIS_Y);\nconst j = math.randomFloat(0.82, 1.18);\nplacement.scale(j, j, j);\n```\n\nSizing every prop against **one** scale constant, rather than giving each its\nown, keeps \"this looks wrong\" a modelling question instead of a scaling one.\n\n## Packaging\n\nA self-contained GLB is one file with nothing to resolve — the safest thing to\nship. External references still work in both formats: `.bin` buffers and image\n`uri`s are resolved **relative to the asset's own URL** (a GLB with an external\ntexture loads as-shipped). But an external file that 404s rejects the parse, so\nthe whole asset fails to load rather than arriving partially — no untextured\nfallback. Note that `loader.setBaseURL(\"*\", …)` deliberately skips `gltf`,\n`glb`, `obj` and `mtl` for this reason; set those types individually if you\nneed a prefix.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| model mirrored — text backwards, gear on the wrong side | `Mesh` defaults `rightHanded` to `false`; pass `true` |\n| model upside down | a manual axis flip on top of the loader's conversion |\n| scene is enormous or invisibly small | `scale` left at 1 against a metre-scale export |\n| the asset fails to load entirely | an external buffer/image `uri` that 404s, or Draco-compressed geometry |\n| everything is blown out white | authored lux/candela intensities; use `lightIntensityScale` |\n| materials flatter than the authoring-tool preview | no normal or metallic/roughness *textures* are imported |\n| a character does not deform | vertex skinning is out of scope; rig hierarchically or billboard |\n| animation names come back empty | the asset has no node-TRS channels (skin-only rig) |\n| a hundred copies tank the frame rate | exported without `EXT_mesh_gpu_instancing` |\n| a whole scatter looks like one object repeated | every instance shares the group transform — vary yaw/scale per instance, rotating *after* the translate |\n| three clips where you authored one | one action per object; use a single action with a slot each |\n| one part of a rig renders rigid while its curves exist | it was re-parented mid-pose, baking the inverse into `matrix_parent_inverse` |\n| a merged mesh lights wrong along one axis | it inherited the active object's non-uniform scale on join — apply transforms |\n| an authored palette comes out uniformly dark, but white is correct | linear values written to a strip that is saved verbatim |\n| every untextured glTF material renders too dark, white unaffected | `baseColorFactor` is linear and a `tint` is sRGB — `tint.setLinear(...)`, not `* 255` or `setFloat` |\n| `getGLTF(name).nodes[0]` geometry lands in the wrong place | `nodes` is per primitive, each with its own `world`; merge to one primitive and export at the origin |\n| a prop casts no visible shadow | wide and flat-bottomed — the blob is under it; `shadowGroundY` haloes it rather than revealing it |\n| shadows only show on casters near the camera | `shadowGroundY` is on the wrong side — Y-down means the floor is a **greater** y, so a `pos.y - lift` puts the blob inside the caster and the depth test leaves only a hairline ring |\n| a shadow smeared across the whole floor | a ground plane cast its own blob — use the scene-wide opt-in |\n| `onLoaded` gets a string, not the scene | it is called with the level id; load into your own container instead |\n| scene renders flat and unlit | no `Camera3d` — the 2D-camera path is CPU-projected and unlit (with a `Camera3d` on Canvas you get a black canvas instead) |\n\n## Related skills\n\n- `melonjs-3d` — conventions, `Camera3d`, meshes, `Light3d`, instancing, and\n  `camera.setFog` (an outdoor scene almost always wants it; it is set on the\n  camera, so a loaded scene needs no per-node work)\n- `melonjs-lighting` — how `Light3d` behaves once imported\n- `melonjs-loading-assets` — the loader, asset types and base URLs\n- `melonjs-plugins` — the Spine plugin, for skeletal characters\n","frontmatter":{"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 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"},"isInternal":false,"tokens":6792,"sizeBytes":26460},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-3d/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d/SKILL.md","title":"melonjs-3d","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-3d\ndescription: \"Use this skill for anything 3D or 2.5D in melonJS — Camera3d, Mesh, InstancedMesh, Sprite3d billboards, Light3d, ground shadows, glTF/GLB scenes, and depth sorting. Covers the Y-down/+Z-forward convention that is the inverse of OpenGL, the cameraClass opt-in, clip planes, and what does not work on the Canvas fallback. Triggers on: Camera3d, Mesh, InstancedMesh, Sprite3d, Trail3d, ribbon, exhaust trail, Light3d, billboard, glTF, glb, 3D, 2.5D, depth, cameraClass, fov, setClipPlanes, setFog, fog, distance fog, height fog, heightFalloff, transparent, transparency, alpha, blendMode, fade, castGroundShadow, lit, setBasis, getBasis, lookAt, up vector, gimbal, slerp, great circle, turn a direction.\"\nlicense: MIT\n---\n\n# 3D and 2.5D in melonJS\n\n> melonJS is a 2D engine that grew a real 3D tier: perspective cameras, meshes,\n> instancing, glTF scenes and 3D lights. Most of what a model \"knows\" about 3D\n> graphics comes from the OpenGL convention, and **melonJS's is the inverse**.\n> Read the conventions section before writing any 3D code.\n\n## Coordinate conventions — the inverse of OpenGL\n\nThis is the single most important thing on this page.\n\n| | melonJS | OpenGL convention |\n|---|---|---|\n| vertical | **Y-down** — higher `y` is *lower* on screen | Y-up |\n| depth | **+Z forward** — higher `z` is *farther* away | −Z forward |\n\n`Camera3d` exposes three rotation angles: `camera.pitch`\n(X, look up/down), `camera.yaw` (Y, look left/right) and `camera.roll` (Z, bank\nthe horizon). The view is `R(yaw) ∘ R(pitch) ∘ R(roll)` inverted, and the frustum\nplanes come off that same matrix — so culling follows a banked view.\n\nThe basis those angles produce is **readable and writable**: `getBasis(right,\nup, forward)` reads it, `setBasis(right, up, forward)` poses the camera from\none.\n\n`camera.rotate()` is NOT the way to bank a 3D camera. It writes the inherited\n`currentTransform`, which a 3D view never reads, so the call is silently inert.\nSet `roll`. (On a `Camera2d` it is the other way round: roll IS that transform's\nrotation, which is why `worldToLocal` / `localToWorld` compensate for it.)\n\nThe payoff is that 2D code translates directly — anywhere you used `pos.x` /\n`pos.y`, add `pos.z` and the maths still holds. The cost is that every\nOpenGL-shaped instinct is backwards.\n\n**Consequence that bites immediately:** \"behind everything\" is the *largest*\ndepth when the camera looks along +Z. Put a backdrop on the wrong side and it\neither paints over the whole scene (it is nearer than everything else) or\ndisappears without trace (it falls in front of the near plane and the GPU clips\nit). Which sign means \"far\" depends on which side the camera sits, so check the\ncamera before picking a backdrop depth.\n\nglTF assets are authored Y-up right-handed. `level.load` converts them by\nrotation (`rightHanded: true` is its default), so you do not apply a flip\nyourself. The **`Mesh` constructor defaults `rightHanded` to `false`**, which\nbridges by mirroring instead — geometry you hand to a `Mesh` directly comes in\nflipped left/right unless you pass `rightHanded: true`. See\n`melonjs-3d-assets`.\n\n## Opting in\n\n3D starts with the camera class, and nothing else has to change. Set it once on\nthe `Application`:\n\n```js\nimport { Application, Camera3d } from \"melonjs\";\n\nconst app = new Application(1024, 768, {\n    parent: \"screen\",\n    cameraClass: Camera3d,      // ← the opt-in\n});\nawait app.init();\n```\n\nThat flips the world's sort to `\"depth\"`, switches the broadphase to a 3D index,\nand renders sprites and meshes with perspective projection. `Camera3d` extends\n`Camera2d`, so follow, fade, shake and post effects all still work.\n\n### Keep the camera, do not fish it back out\n\n`app.viewport` is the **active stage's** default camera, so it is typed\n`Camera2d`: a stage overrides the application's `cameraClass`, and the\nbuilt-in loading screen does exactly that. Reading your `Camera3d` back out of\nit therefore needs a cast, and a cast that is wrong while the loader is up:\n\n```js\nconst camera = app.viewport as Camera3d;   // works, until it does not\n```\n\nOwn it instead. Pass the camera in and keep the typed reference, and nothing\ndownstream ever narrows:\n\n```js\nclass GameStage extends Stage {\n    readonly camera: Camera3d;\n\n    constructor(app) {\n        const camera = new Camera3d(0, 0, app.renderer.width, app.renderer.height);\n        super({ cameras: [camera] });\n        this.camera = camera;\n    }\n}\n```\n\nA freshly built camera is named `\"default\"`, and a stage registers its\n`cameras` by name, so this IS the stage's default camera: `app.viewport`\nreturns the same instance.\n\n### The two patterns differ in LIFETIME, and that decides which you want\n\nThis is the part that bites. `cameraClass` builds a **fresh camera for each\nstage**, which the engine does deliberately so \"Camera3d state shouldn't\nbleed across stages\". An owned camera passed through `super({ cameras })` is\nbuilt once and lives as long as the stage object, which for a stage you\nregister with `state.set()` means as long as the application.\n\nSo anything you attach to the camera now outlives the visit:\n\n```js\n// in onResetEvent — runs EVERY time the stage is entered\napp.viewport.addPostEffect(bloom);\n```\n\nWith `cameraClass` that lands on a fresh camera each time and is harmless.\nWith an owned camera it stacks: a second bloom on the second visit, a third\non the third. The symptom is text and edges appearing to double or smear,\nbecause the scene is being post-processed once per accumulated effect.\n\nNote that `removePostEffect` DESTROYS the effect rather than detaching it, so\n\"just remove it on the way out\" is not a free undo.\n\n**Choose on lifetime, not on typing:**\n\n| your effects / camera state are | use |\n|---|---|\n| set up per stage visit, in `onResetEvent` | `cameraClass` — fresh camera per stage |\n| set up once, in the stage constructor | an owned camera in `super({ cameras })` |\n\nIf you want the typed reference but your setup is per-visit, keep\n`cameraClass` and narrow once on reset:\n\n```js\noverride onResetEvent(app) {\n    const viewport = app.viewport;\n    if (!(viewport instanceof Camera3d)) {\n        throw new Error(\"this stage needs a Camera3d\");\n    }\n    this.camera = viewport;   // typed, checked, and still fresh per visit\n}\n```\n\nThat is what the Earth Defender example does, and why: it attaches post\neffects on every reset and never removes them.\n\nNarrowing beats asserting wherever you read the shared slot: the language\nverifies `instanceof`, and fails loudly if the application was never given a\n`cameraClass`, where a cast fails silently at the first 3D call.\n\nPer-stage opt-in also works, which is useful when the loading screen is 2D:\n\n```js\nclass GameStage extends Stage {\n    constructor() {\n        super({ cameras: [new Camera3d(0, 0, 1024, 768, { fov: Math.PI / 3 })] });\n    }\n}\n```\n\n## Camera traps\n\n- **There is no sky.** The frame clears to `renderer.backgroundColor` (opaque\n  black unless the renderer was created transparent), and `world.backgroundColor`\n  clears over it when you set one — both are flat colours. Anything more is a\n  backdrop renderable, screen-fixed or placed at a far depth.\n- **Set the clip planes for your scene scale.** Defaults are near 0.1 / far 1000.\n  Objects beyond the far plane clip or project with bad w-divides; a `near` that\n  is too small wastes depth precision and distant props z-fight. Use\n  `camera.setClipPlanes(near, far)`.\n- **`camera.pos.set(x, y)` is 2-argument and zeroes z.** Use\n  `camera.setPosition(x, y, z)`, which leaves the depth untouched when `z` is\n  omitted; `camera.depth` is the z accessor if you only want that.\n- **`lookAt(target)` solves pitch and yaw only**, so on its own it cannot\n  express \"up is this direction\" and it leaves `roll` alone. Pass an up to\n  bank the horizon too: `camera.lookAt(target, up)`, where `up` is what should\n  appear UP ON SCREEN. This engine renders Y-down, so a level horizon is\n  `(0, -1, 0)`. There is deliberately no default, because giving one would\n  silently level every camera that had banked its own horizon.\n- **When the game already has all three axes, hand them over**:\n  `camera.setBasis(right, up, forward)`, the writable counterpart of\n  `getBasis`. The case that needs it is a view over a curved surface, where up\n  is the surface normal and differs at every point, so no pitch and yaw pair\n  expresses it and integrating towards one drifts and gimbals at the poles.\n  `setBasis` absorbs a triple that has drifted off orthonormal rather than\n  leaving the camera holding something that is not a rotation, and it does not\n  care what the axes are scaled to: a direction is a direction, so anything\n  from a denormal to 1e300 resolves the same way. Note its `up` is the camera's\n  local +Y column, which under Y-down points DOWN the screen: the opposite\n  sense to `lookAt`'s, and one negation apart.\n- **`worldToLocal` does not project.** It is a 2D camera's offset subtraction.\n  To pin a label or marker to a point in the scene use\n  `camera.worldToScreen(x, y, z)`, which applies the projection and returns\n  `null` behind the camera. See `melonjs-camera-and-drawing`.\n\n## Where an up vector earns its keep\n\nThree cases. In the first two, `lookAt(target)` on its own is already right and\nan up would be noise. The third is the one it exists for.\n\n```js\n// 1. ordinary third-person: world up never changes, so say nothing\ncamera.setPosition(x, y, z).lookAt(player);\n\n// 2. a deliberate dutch angle: this is a ROLL, not an up vector\ncamera.lookAt(player);\ncamera.roll = Math.PI / 32;\n\n// 3. a view over a curved surface: up is the surface normal, and it is\n//    different at every point on the globe. No pitch and yaw pair expresses\n//    it, and integrating towards it drifts and gimbals over the poles.\nconst up = player.pos.clone().sub(planetCentre).normalize();  // surface normal\ncamera.setPosition(eye.x, eye.y, eye.z).lookAt(player, up.negate());\n```\n\nNote the `negate()` in case 3. The surface normal points away from the planet,\nwhich is the direction the player's head points, and `up` wants what should\nappear UP ON SCREEN. This engine renders Y-down, so `(0, -1, 0)` is a level\nhorizon and a world-space \"up\" direction has to be handed over in that sense.\nGet it backwards and the view is upside down, which is the single most likely\nmistake with this argument.\n\nIf the game already computes all three axes, skip the solve entirely:\n\n```js\n// a surface frame the game is already maintaining\ncamera.setBasis(right, up, forward);\n```\n\n`setBasis` takes the camera's OWN columns, where `up` is local +Y and so points\ndown the screen: the opposite sense to `lookAt`'s, one negation apart. It also\nabsorbs a frame that has drifted off orthonormal, which one integrated over\nthousands of steps will have.\n\n**A camera posed this way and a world rotated into a fixed camera's frame are\nthe same picture.** If a game already transforms its world into view space by\nhand, there is nothing to gain by switching; the value of `setBasis` is not\nhaving to write that transform in the first place.\n\n## Turning a direction: `slerp`, not `lerp`\n\n`lerp` walks the straight line between the two tips, which passes INSIDE the\nsphere. The result points the right way at both ends, lingers near them, rushes\nthe middle, and comes out shorter than either input as it crosses the midpoint.\nOn a camera swing that reads as a lurch; on a body walking over a globe it dips\ninto the surface.\n\n`Vector3d#slerp` turns along the great circle instead, at a constant angular\nrate, and carries the length across linearly, so two unit vectors give back a\nunit vector and nothing needs renormalizing.\n\n```js\n// a turret, a camera, a ship: a facing chasing another facing\nfacing.slerp(toTarget, 0.25);\n\n// frame-rate independent, the same shape as math.damp\nfacing.slerp(toTarget, 1 - Math.exp((-6 * dt) / 1000));\n```\n\n`ObservableVector3d` has it too and fires its callback ONCE, so\n`renderable.pos.slerp(...)` is one notification rather than three.\n\n**`Vector2d` has the same method**, and it is not a cut-down copy: in 2D the\nwhole turn is one signed angle, so there is no sine to divide by and no\ndegenerate near-parallel case at all. What it buys a 2D game is the wrap: a\nheading damped as a NUMBER from 170 degrees to -170 degrees sweeps the long 340\ninstead of the short 20, and an arc between directions cannot do that.\n\nTwo degenerate pairs are handled inside rather than left to the caller, and\nboth turn up in ordinary game code:\n\n- **the same direction**, where every path between them is the same path.\n- **opposite directions**, where infinitely many great circles pass through\n  both and none is the shorter way round. The textbook formula divides by a\n  vanishing sine here and hands back `NaN`, which propagates into a position or\n  a matrix and the object disappears. `slerp` picks a stable perpendicular and\n  turns through half a circle instead.\n\nThat second case is why a hand-rolled version is a liability rather than a\nshortcut: anything orbiting a planet reaches the antipode of where it started,\nevery run.\n\n**`math.damp` for a scalar, `slerp` for a direction.** Same job, different\nspace; what `damp` cannot do is keep a vector on the sphere.\n\n## Depth sorting\n\nUnder `Camera3d` the world sorts on `\"depth\"` **every frame** (recursively), so\nunlike 2D you *can* assign `depth` after `addChild` and it will reorder. That is\nthe one place where the usual z-ordering rule is relaxed — see\n`melonjs-renderables` for the 2D rules, which still apply everywhere else.\n\nYou will often *have* to. A `Container` has `autoDepth: true` by default, so\n`addChild(child)` with no explicit z **overwrites `pos.z` with the child's\nindex** — a real world depth in a 3D scene, and never the one you wanted. Pass\nit (`world.addChild(mesh, z)`), or set `mesh.depth` afterwards. The glTF\nimporter turns `autoDepth` off on the container it loads into for this reason.\n\n**`floating` does not opt out of this sort.** It skips the camera *transform*,\nnot the depth *order*. A floating child is ordered by `|pos.z|` alone — its\n`pos.x/y` are screen pixels, and the camera does not move relative to it — so\nthe magnitude is the distance and the sign is ignored:\n\n```js\nworld.addChild(hud, -150);       // small -> nearer than anything -> on top\nworld.addChild(skybox, -10000);  // large -> farther -> behind everything\nworld.addChild(skybox, 100000);  // equally far: sign does not matter\n```\n\nBoth hold at any camera position. A HUD given the huge z that would put it on\ntop in 2D lands at the far end of the level instead, with the scenery drawing\nover it.\n\n## Transparency\n\nA mesh fades by setting its opacity — there is nothing else to switch on:\n\n```js\nghost.setOpacity(0.4);\n```\n\nMeshes render in two phases. The **opaque pass** writes depth in sort order; the\n**transparent pass** replays afterwards, back-to-front, blending and writing no\ndepth. A draw lands in the second whenever its alpha is fractional.\n\nThat default matters because the opaque path writes premultiplied colour with\nblending off, so before this a faded mesh came out **darkened toward black**\nrather than see-through — the background contributed nothing.\n\n**`transparent: true`** when the transparency is in the TEXTURE rather than the\nopacity — a soft-edged glow, smoke, a glTF material with `alphaMode: \"BLEND\"`.\nThe automatic check reads the draw's alpha and cannot see into a texture. Watch\n`alphaCutoff` here: it discards texels *before* blending sees them, so a soft\nedge needs a low cutoff (`Sprite3d` drops its own default to `1/255` when you\nset `transparent: true`). The cutoff thresholds the MATERIAL's alpha, not the\ndrawn alpha, so a fading cutout mesh keeps its shape instead of disappearing at\nits own threshold.\n\nThe glTF loader does **not** set this for you: one loaded mesh can merge several\nmaterials and the flag routes the whole mesh, so a `\"BLEND\"` material sharing\ngeometry with an opaque one would drag the opaque half into the transparent pass.\n\n**`transparent: false`** pins a mesh to the opaque pass however it is faded.\n\n### `depthTest: false` for something with no surface to be occluded on\n\n`transparent` decides the PASS, and the transparent pass turns depth *writing*\noff on its own so overlapping blended draws blend instead of fighting. Whether\ngeometry in front of a mesh **hides** it is a separate question, and\n`depthTest` is where you answer it. The default, `true`, is what you want for\nanything genuinely in the scene — a muzzle flash behind a wall should still be\nbehind the wall, and this is not a synonym for \"additive\".\n\nSet it `false` for a mesh standing in for a screen-space effect: an additive\nglow carrying a world position only so it can sort and move with the thing it\nbelongs to. A world-space caption billboarded over a prop wants it for the\nsame reason, since it has no surface for the prop's own foliage to be in front\nof. Left depth tested, a flat billboard is sliced along a hard straight\nline the moment any geometry is nearer at some pixel. That shows up worst\naround something round — a glow on a rock near a planet's limb gets cut,\nand no offset escapes it, because the near surface bulges further toward the\ncamera than any offset you would dare apply.\n\n```js\nconst halo = new Sprite3d(0, 0, {\n    image: glowTexture,\n    width: rock.radius * 3, height: rock.radius * 3,\n    billboard: \"spherical\",\n    blendMode: \"additive\",\n    transparent: true,     // the blended pass\n    alphaCutoff: 0,        // do not cut the soft falloff\n    depthTest: false,      // and do not let the world slice it\n});\n```\n\nHonoured in the transparent pass on both GPU backends; the Canvas renderer has\nno depth buffer and ignores it.\n\nBlending uses the renderable's existing `blendMode`, so a glow is one property.\nThe advanced modes (`\"overlay\"`, `\"difference\"`, and the rest that need a\ncompositing pass) fall back to `\"normal\"` here, on both backends:\n\n```js\nconst glow = new Mesh(0, 0, {\n    ...quad, texture: glowTexture,\n    transparent: true, blendMode: \"additive\", alphaCutoff: 0,\n});\n```\n\n| | |\n| --- | --- |\n| sorting | **per object**, by distance from the camera |\n| intersecting transparent meshes | may pop as the camera moves — split them, or accept it |\n| `InstancedMesh` | sorts as **one** object; instances draw in buffer order |\n| needs | a GPU backend and a `Camera3d` |\n\nGround shadows ride the same pass — a blob is a decal, and decals are its first\nclient rather than a feature of their own.\n\n**`Trail3d`** is the transparent pass's other obvious client: a camera-facing\nribbon for an exhaust streak, a speed trail or a tyre mark. It is a `Mesh`, so\nit sorts and fogs with everything else here, and like the rest of this tier it\nneeds a `Camera3d`. `Trail` is the 2D one and stays flat under a perspective\ncamera. See `melonjs-particles-and-trails`.\n\n## Distance fog\n\nOff until you ask for it, and one call on the camera:\n\n```js\ncamera.setFog({ near: 2000, far: 7000 });   // linear: name the two distances\ncamera.setFog({ mode: \"exp2\", density: 4e-4 }); // or one density\ncamera.setFog(null);                         // off\n```\n\nIt is the cheapest thing that stops a 3D scene reading as flat cut-outs, and it\nlets props arrive at the far plane without a visible edge.\n\n**Every parameter is optional, and the omitted ones track live.** Distances\ndefault to the camera's own clip planes, so fog cannot silently disagree with\nthem after a later `setClipPlanes`. The colour defaults to\n`renderer.backgroundColor` and follows it, so geometry dissolves into whatever\nsky you already set — including through a day/night fade. Pass `color` only\nwhen the fog should deliberately differ from the backdrop:\n\n```js\ncamera.setFog({ far: 5000, color: \"#8899aa\" });\n```\n\nA `Color` is held by reference, so mutating it animates the fog.\n\n**Height falloff** makes mist pool in low ground instead of hanging at every\naltitude equally — the difference between fog reading as weather and as a global\ndesaturation:\n\n```js\ncamera.setFog({ near: 1200, far: 7000, fogHeight: 0, heightFalloff: 0.0015 });\n```\n\n`heightFalloff` defaults to **0**, which is uniform fog — not a special case,\nthe same integral with the dial at zero, so leaving it out changes nothing.\nRender space is **Y-down**, so `fogHeight` is the floor and density rises\n*below* it; every published form of this formula assumes Y-up and has the\nopposite sign.\n\nFog is measured **radially** from the camera and applied **per fragment**, so\nit does not slide as the camera turns and does not band across large triangles.\nIt lives on the camera, so a split-screen or minimap view fogs independently —\nand a `Camera2d` never fogs at all.\n\n**Per object:** `fog: false` exempts a mesh however far away it is — for a\nwaypoint or objective marker that has to stay readable. It exempts the mesh and\nnot the ground shadow it casts: a blob is a mark on the floor and fogs with the\nfloor. Emissive surfaces fog\nlike everything else (light travelling through fog is attenuated too), so a\nneon sign that should punch through wants `fog: false`, not a brighter\nemissive.\n\nOnly meshes fog. 2D content, HUDs and `floating` renderables never reach the\nmesh shaders, so a screen-space overlay stays clean with no work.\n\n**A custom mesh shader is not fogged unless it asks to be.** Fog is compiled\ninto the engine's own mesh programs — `#define FOG` on WebGL, an `enable_fog`\noverridable constant on WebGPU — and a shader you supply is yours: the engine\nbinds it as written and never substitutes a fogged variant. So a mesh given its\nown `GLShader` keeps full contrast while the scene around it recedes. It is\nsafe — nothing throws, and the camera's fog is simply not applied — but it is\nusually surprising.\n\nA hosted `ShaderEffect` is the other case and needs nothing: its body is\nspliced into the engine's own mesh program as a colour hook, and fog is applied\nto the hook's result, so such a mesh fogs like any other.\n\nTo opt in, declare the same uniforms and the engine will feed them, because the\nfog values are pushed to any mesh program that declares them rather than only to\nthe built-in ones:\n\n```glsl\nuniform vec3 uFogColor;   // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams;  // x = mode (0 off / 1 linear / 2 exp2),\n                          // y = near, z = 1/(far - near), w = density\n```\n\nYour vertex stage computes the distance itself — `length((uViewMatrix *\nuModelMatrix * vec4(aVertex, 1.0)).xyz)`, radially so it does not swim as the\ncamera turns — and the blend must scale the fog colour by the fragment's own\nalpha, `mix(uFogColor * a, rgb, f)`, because `vColor` arrives premultiplied.\nMixing toward the unscaled colour haloes every alpha-cutout edge.\n\nThe flip side is the reason fog costs nothing when unused: with no camera fog,\nthe mesh programs are compiled without any of it, on both backends. It is not a\nbranch that is skipped at runtime — the code is not there.\n\n## Meshes\n\n```js\nconst cube = new Mesh(x, y, {\n    model: \"cube\",          // a preloaded OBJ name — no built-in primitives\n    texture: atlas,         // TextureAtlas, image, or a preloaded image name\n    width: 64, height: 64,\n    cullBackFaces: true,    // the default\n    lit: true,\n});\n```\n\n**A mesh pivots about its model origin `(0, 0, 0)`, not an anchor point.** Under\n`Camera3d` it opts out of the anchor offset entirely, so `anchorPoint` is inert\nthere — place the origin where you want the pivot at authoring time, or nest the\nmesh under a transformed parent. The anchor is only honoured on the legacy\n2D-camera path.\n\n### InstancedMesh, and when it is the wrong tool\n\n**`InstancedMesh`** draws one mesh many times in a single draw call — the\ndifference between a hundred trees and a hundred thousand. glTF scenes using\n`EXT_mesh_gpu_instancing` load as one automatically; by hand it is a `Mesh`\nwith a count:\n\n```js\nconst trees = new InstancedMesh(0, 0, { ...treeGeometry, instanceCount: 400 });\nconst at = new Matrix3d();               // one scratch, reused\nfor (let i = 0; i < trees.instanceCount; i++) {\n    at.identity().translate(x, y, z);\n    trees.setInstance(i, at);\n}\nworld.addChild(trees, 0);\n\ntrees.visibleInstanceCount = 120;        // draw fewer, without re-uploading\n```\n\nIt is not a free upgrade. One `InstancedMesh` is **one geometry and one\nmaterial**, and four things move from per-object to per-group:\n\n| | with `Mesh` | with `InstancedMesh` |\n| --- | --- | --- |\n| depth sort | each object sorts on its own `pos` | the whole set has **one** sort key |\n| ground shadow | one blob per object | one instanced draw for the set |\n| removal | `removeChild`, indices unaffected | `removeInstance(i)` swaps the **last** instance into the hole, so any index you were holding is now wrong |\n| colour | `tint` per object | needs `instanceColors: true` and `setInstanceColor(i, …)` |\n\nSo the question is not \"how many are there\" but **\"does the game address them\nindividually\"**:\n\n- **Scenery — instance it.** Trees, rocks, grass, debris: the game never asks\n  about one of them.\n- **Collision-tested props — still fine.** You test against positions you\n  already own; instancing only changes how they are *drawn*.\n- **Collectibles and enemies — usually not.** Anything removed one at a time\n  makes `removeInstance`'s swap your problem: you have to keep an index↔object\n  map and repair it on every removal. At small counts a pooled `Mesh` each is\n  less code and no slower.\n\nUnder a few hundred objects the draw-call saving is not what limits you\nanyway — reach for it when the count is in the thousands, or when the objects\nare pure scenery and it costs nothing to.\n\n## Normals are generated for you\n\nA `lit` mesh needs per-vertex normals for the shader to light with. Supply them\nif you have them; **omit them and the engine computes them from the geometry**:\n\n```js\nconst mesh = new Mesh(x, y, { vertices, uvs, indices, lit: true });\n// normals derived from the triangles — nothing else to do\n```\n\nYou do not choose flat or smooth, because the geometry already decides. Face\nnormals accumulate into their vertices weighted by area, so where faces **share**\na vertex they average and the surface shades smoothly, and where every triangle\ncarries its **own** three vertices — a triangle soup, which is how most\nhand-built geometry comes out — each vertex belongs to one face and the result\nis that face's normal, so it shades flat. Want faceted edges: duplicate the\nvertices. Want smooth: share them.\n\nAn explicit `settings.normals` always wins, and an unlit mesh gets none — there\nwould be nothing to read them.\n\n**A lit mesh with no normals used to render fullbright**, which looks like the\nlighting is broken rather than absent. If an older scene suddenly picks up\nshading, that is why.\n\n## Colouring a mesh\n\nThere are four levels, and picking the wrong one is the usual reason a colour\n\"does not apply\". They all multiply together.\n\n| level | how | use for |\n|---|---|---|\n| whole object | `mesh.setTint(\"#ff8800\")`, or `mesh.tint.setColor(r, g, b)` | flash on hit, team colour, fading one object |\n| per vertex | `settings.vertexColors`, or `mesh.setVertexColor(i, color)` | a gradient *within* one mesh — distance haze, a darker crease |\n| per material | `textureGroups`, from a multi-material OBJ + MTL | a model whose parts differ, in one draw call |\n| per instance | `new InstancedMesh(…, { instanceColors: true })` then `setInstanceColor(i, color)` | a thousand copies that differ |\n\n**`tint` is per object.** That is the trap: build a terrain as one big mesh and\nyou can tint the whole valley or none of it. Anything that varies *across* a\nsingle mesh is per-vertex.\n\n```js\n// fade a procedural terrain toward the sky the further out it goes\nconst ground = new Color(217, 230, 244);\nconst sky = new Color(207, 230, 247);\nconst haze = new Color();\nfor (let i = 0; i < mesh.vertexCount; i++) {\n    const t = Math.min(1, mesh.originalVertices[i * 3 + 2] / 6000);\n    mesh.setVertexColor(i, haze.copy(ground).lerp(sky, t));\n}\n```\n\nSupply the whole array at construction when you already have it —\n`vertexColors` takes a packed `Uint32Array` (the form the batchers read, so no\nconversion) or one `Color` per vertex. A length that does not match\n`vertexCount` **throws**; it is not padded, because a short array would leave\nthe tail of the mesh mis-coloured and that reads as a lighting bug.\n\nMutating the array directly is fine, but say so afterwards:\n\n```js\nmesh.vertexColors[i] = color.toUint32(color.alpha);\nmesh.needsUpdate = true;   // the retained Camera3d path uploads once\n```\n\n`setVertexColor` does that for you. Skip it and the colour applies under a 2D\ncamera and silently does not under `Camera3d`.\n\nOn a lit mesh the colour multiplies the **lit** result, so it behaves as albedo\nrather than as an emissive override — a vertex colour will not make an unlit\nface bright.\n\n`alpha` hides a mesh as it hides anything else: at 0 the draw is skipped. There\nis no *partial* mesh transparency though — the mesh path renders opaque, so a\nmesh at `alpha = 0.5` draws fully opaque rather than half see-through. Fade a\nmesh out and it will stay solid until it vanishes.\n\n## Moving a mesh in 3D\n\n`Mesh` and `Sprite3d` inherit the ordinary `Renderable` transform helpers, and\nboth are already 3D:\n\n```js\nmesh.rotate(Math.PI / 2, new Vector3d(1, 0, 0));   // axis overload\nmesh.scale(2, 1, 0.5);                             // z is a real argument\n```\n\n`rotate(angle)` alone is the 2D case (about Z); pass a `Vector3d` and it\nrotates about that axis. Both write to `currentTransform`, a **`Matrix3d`**,\nwhich `Mesh` folds into its model matrix — so orienting a mesh costs a matrix\nupdate, not new geometry.\n\nThis matters most for the case it is least obvious for: a horizontal plane.\n`Sprite3d` with `billboard: false` is a quad in the sprite plane — upright —\nand the way to lay one flat (water, a road, a shadow decal) is to rotate it a\nquarter turn about X, **not** to hand-build vertices. The one reason to build\ngeometry instead is tiling: a `Sprite3d` bakes its UVs 0..1 from its atlas\nframe, so a surface that needs `textureRepeat` across many world units still\nwants an explicit `vertices`/`uvs` mesh.\n\n## The mesh pass is opaque\n\nWorth stating plainly, because the symptom does not look like a blending\nproblem: **the mesh pass disables blending**. A soft-edged texture — a glow, a\nsun, a halo, anything with a gradient alpha — composites as a hard-edged disc\nwith a grey rim, which reads as a broken texture rather than as a missing\nfeature.\n\n`transparent: true` routes it through the transparent pass, and `blendMode:\n\"additive\"` on top of that makes a light source add to the sky instead of\nsitting on it. This is the same feature a translucent water surface needs.\n\n## Textures under a perspective camera\n\nA tiling texture on a large ground plane is seen at a grazing angle, and that\nchanges which detail survives:\n\n- **Nothing straight and parallel to the direction of travel.** Every such run\n  converges on the vanishing point, so a few current bands on a river become a\n  fan of rays radiating out of the horizon. Narrowing them and tiling more\n  often only multiplies the rays. Short, scattered, direction-free detail\n  minifies into an even shimmer instead.\n- **`textureFilter: \"linear\"`** for a floor — `\"nearest\"` opts out of mipmaps\n  and anisotropy, and detail aliases into a wash exactly where the camera looks\n  most.\n\n## Ground must outlast the props standing on it\n\nScenery scattered up a slope is placed by its own rule, and terrain is built by\nanother. When the planting reaches further than the mesh does, a low camera\nsees trees standing over open sky at the corners of the frame. Build the ground\nwider than the widest thing planted on it — past the profile's clamp it is a\nflat plateau, so the extra columns cost two triangles and no silhouette.\n\nRecycling has the same trap in Z: an endless runner that recycles tiles a fixed\ndistance behind the *player* forgets that the camera trails further back still,\nand the bottom of the frame falls off the world.\n\n## Sprite3d and billboards\n\n`Sprite3d` is the 2.5D workhorse: a flat sprite living at a real depth, with\n`billboard` controlling how it faces the camera.\n\n| `billboard` | behaviour |\n|---|---|\n| `false` (default) / `\"none\"` | no rotation — a flat plane in the world |\n| `true` / `\"cylindrical\"` | yaws to face the camera, stays upright (characters, trees) |\n| `\"spherical\"` | always fully faces the camera (particles, impostors) |\n\n`\"cylindrical\"` is what you want for paper-thin characters in a 2.5D game.\nBillboarding needs a `Camera3d` drawing the frame; under a 2D camera the quad\nrenders fixed-orientation. **Any other string falls through to the spherical\nbranch**, so a guessed value like `\"upright\"` silently gives you spherical.\n\nA billboard takes its **orientation** from the camera, so `rotate()` has no\neffect on one: a card already turned to face the camera has no free rotation\nleft to give. Everything that is not an orientation still applies, `scale()`\nand `meshScale` included, so animating the size of a shockwave or a pickup is\nthe ordinary call and the cull bounds grow with it:\n\n```js\nconst wave = new Sprite3d(x, y, { image: \"ring\", width: 64, height: 64,\n                                  billboard: \"spherical\", transparent: true });\nwave.scale(1 + 3 * t);          // grows; `scale` accumulates, so drive it\n                                 // from the authored size, not per frame\n```\n\nFor an in-plane rotation, a bolt streak or an exhaust, use `billboard: false`\nand orient the quad yourself.\n\n## Lighting\n\n`Light3d` takes **one argument — the options object** (it has no `x, y, z`\npair; a light carries its own `position`). Types are `\"directional\"` (the\ndefault), `\"ambient\"`, `\"point\"` and `\"spot\"`.\n\n```js\nworld.addChild(new Light3d({ type: \"directional\", direction: [0.3, 1, 0.2] }));\nworld.addChild(new Light3d({ type: \"ambient\", intensity: 0.3 }));\n// a Vector3d works too, wherever the game already has one\nworld.addChild(new Light3d({ type: \"spot\", position: torch.pos, range: 400 }));\n```\n\n`direction` and `position` take an `[x, y, z]` array **or** a `Vector3d`, and\n`color` takes a `Color`, a CSS string or an `[r, g, b]` array — so a value the\ngame already holds can go straight in. Each is read, not retained: move your\nvector afterwards and the light stays where it was.\n\nTwo things still bite, both silently:\n\n- `new Light3d(0, 0, {…})` — the constructor takes **options alone**. JavaScript\n  drops the extra arguments, so `options` becomes the number `0` and every\n  setting in your literal is discarded. The light still appears, on pure\n  defaults: a `type: \"ambient\"` written this way is a second DIRECTIONAL light,\n  and the scene looks plausible enough that nobody checks. TypeScript stops\n  checking a literal once the argument count is wrong, so nothing inside it is\n  verified either.\n- **`direction` is where the light GOES, and this is a Y-down space** — so a sun\n  overhead travels *downward* and its Y is **positive**. Get the sign wrong and\n  the scene is lit from underneath: faces that should be in shade are bright,\n  the ground glows and the sky-facing surfaces go dark.\n\n  ```js\n  direction: [-0.35, 0.8, 0.45]   // sun overhead, late afternoon\n  direction: [-0.35, -0.8, 0.45]  // lit from below — almost never what you want\n  ```\n\n  `position` follows the same convention: a lamp above the floor has a\n  **smaller** y than the floor.\n\nUse **both halves**: with a key light but no ambient, the shadow side of a mesh\ngoes black. With no `Light3d` in the world at all, a `lit: true` mesh falls back\nto a white ambient and renders fullbright — indistinguishable from unlit, which\nis why \"I added `lit: true` and nothing changed\" is the usual first report.\n\nOther defaults worth knowing: `intensity` 1, `color` white, `range` 1000 (point\nand spot, a stylised quadratic falloff, not inverse-square),\n`innerConeAngle` 0 and `outerConeAngle` π/4 (spot).\n\n**`Light2d` is 2D-only** and produces visible artifacts under perspective\nprojection. Do not combine it with `Camera3d`.\n\n**`lit: true` only does anything under a `Camera3d`.** Lighting a mesh needs\nworld-space normals and a world-space fragment position, and the 2D-camera path\nhas neither: its vertices are already the projected output, so there is no world\nspace left to light in. Such a mesh degrades to unlit and warns once, naming the\nfix. If you want lit 3D props in an otherwise 2D game, set\n`cameraClass: Camera3d` on the application. (For 2D **sprites**, lighting is a\nseparate system that works under a 2D camera as normal, see the lighting skill.)\n\nA `lit` mesh takes a **tangent-space normal map**, so relief comes from the\nmaterial rather than from geometry — `settings.normalMap` (a loader key or any\nimage-like source), or MTL `map_bump` / `bump` / `norm`, which the MTL loader\nfetches and applies per material. The tangent frame is derived per fragment, so\nthe model needs no tangent attribute, and an unmapped mesh is unaffected. This\nis the 3D counterpart of the `Sprite` normal maps in the lighting skill; they\nare separate systems and a `Mesh` uses this one.\n\n### What the ground-shadow tier can and cannot do\n\nCheck this table before claiming a setting is unsupported. Every one of these\nis read **every draw**, so all of them can be animated.\n\n| setting | `Mesh` / `Sprite3d` / `GLTFModel` part | `InstancedMesh` |\n|---|---|---|\n| `castGroundShadow` | yes | yes |\n| `shadowGroundY` | yes, the caster's own floor | yes, **one plane** through the SET's origin |\n| `shadowGroundPoint` | yes on a `Mesh` or `Sprite3d`; on a `GLTFModel` part only by setting it after the load, since it is neither a model nor a `level.load` option | **no**, warns and ignores |\n| `shadowGroundNormal` | yes | yes, one plane for the set |\n| `shadowOpacity` | yes | yes |\n| `shadowScale` | yes | yes |\n| `shadowOffset` | yes | yes, one throw for the set |\n| `shadowStretch` | yes | yes, one stretch for the set |\n| `shadowDirectionX` / `Z` | yes | yes |\n| `shadowLight` | yes | yes |\n| height fade as the caster rises | yes | **no** |\n| a different value per instance | n/a | **no**, and there is no plan for one |\n\nThe last two \"no\"s are the same limitation seen twice: a set gets ONE shadow\nplane and ONE shared quad, so the instance's own height is dropped and nothing\ncan vary between instances. Everything else is one value for the whole set,\nwhich is what a set means. A scatter over curved ground is therefore one set\nper flat facet, and a flying scatter wanting a height fade has to be\nper-object meshes.\n\n`shadowGroundPoint` (20.9) is the one genuinely per-object setting, and the\nreason is structural rather than missing work: the instanced tier builds its\nplane THROUGH the set's own origin so the translation's x and z drop out of\nthe intercept, which is then `groundY + lift / nY`. An arbitrary point does\nnot drop out, and that same `nY` divisor is why the 75 degree tilt ceiling\ncannot be lifted there either. `InstancedMesh` warns and ignores rather than\nstoring it, so a game cannot think it took.\n\nApart from that one, if a claim that a setting \"does not work on an\n`InstancedMesh`\" is in your head, it is out of date: `shadowOffset` and\n`shadowStretch` both reached the instanced tier in 20.8.\n\nGround shadows are **on by default** (the `castGroundShadow` application\nsetting), and need a GPU backend and a `Camera3d`. As a blanket default they\nskip geometry with no vertical extent — a ground plane. Per object,\n`castGroundShadow: true`/`false` overrides the app setting and is obeyed as\ngiven, safeguard included; `shadowGroundY` names the floor the blob lands on,\n`shadowOpacity` how dark it is and `shadowScale` how big it is.\n\nOn ground that is not flat, name the floor with `shadowGroundPoint` instead:\na point and `shadowGroundNormal` are a plane, and the blob leaves the caster's\nvertical column to land on it. `shadowGroundY` cannot express that, because\nthe blob is always directly below the caster in world Y, which is only \"below\"\non level ground. It is also what lifts the tilt ceiling, since that ceiling\nmeasures tilt from world up and so assumes a camera looking down the Y axis.\n\nThe height fade follows whichever floor you named. With a point **and** a\nnormal it is measured along that normal; with a point alone it is the\nstraight-line distance to the point. So on a sphere the blob fades with\nALTITUDE rather than with the vertical drop, which is the whole reason the\npoint exists. Two consequences worth knowing, because they fail in opposite\ndirections: a `shadowGroundNormal` facing inward makes the height negative, so\nthe fade clamps to full strength and the blob never fades at all; and a point\non the wrong side of the caster reads as a large height, so the blob\ndisappears outright rather than thinning to a ring the way a backwards\n`shadowGroundY` does. It is also the one shadow setting that THROWS rather\nthan clamping or hiding: a non-finite component is rejected on the spot.\n\nThe blob is an ellipse sized to the caster's own footprint and placed at the\ncaster's x/z, or at `shadowGroundPoint` when one is set. So a tall or narrow object (a character, a tree, a pickup) shows\nits shadow clearly, while a wide, flat-bottomed one resting on the floor covers\nits own completely from a camera looking down at it. That is the shadow\nbehaving correctly, not a bug.\n\n**Do not chase it by raising `shadowGroundY`.** Lifting the plane does not slide\nthe blob out from under the object, it floats the blob *up* — and past a few\nunits it projects over the top of the caster as a dark halo ringing it.\n\nThe simplest fix is a bigger blob, so its edge shows past the object:\n\n```js\nmesh.shadowScale = 1.5;        // 1 is the default; 0 hides the shadow\n```\n\n`shadowScale` works everywhere, `InstancedMesh` included. Changing it on a\nset rewrites the shared quad's four corners in place rather than rebuilding\nit, as does a `shadowLight` that moves, so an animated sun costs no\nallocation.\n\nOr, for the look of a sun that is not overhead, use the shape controls:\n\n```js\nmesh.shadowOffset = 1.5;       // slide it out, in blob radii\nmesh.shadowDirectionX = 1;     // the ground direction it is cast along\nmesh.shadowDirectionZ = 0;\nmesh.shadowStretch = 2;        // and lengthen it along the same line\n```\n\n`shadowOffset` is measured in multiples of the blob's **own radius**, not in\nworld units, so one value serves a boulder and a pebble and every part of a\nglTF model. About `stretch - 1` is what keeps the shadow attached to its\ncaster. It is honoured **only when the floor has been named**, by\n`shadowGroundY` or by `shadowGroundPoint`, because the\nblob is a flat quad on one named plane: sliding it across a plane the game has\nnot named puts it where there may be no floor. `shadowStretch` is clamped to 3\nand the blob fades as it pulls, so an extreme value degrades to nothing rather\nthan to a smear.\n\nAn `InstancedMesh` honours both, as **one** offset and **one** stretch for the\nwhole set, which is all a shared set can carry. Both ride the quad the blobs\nshare rather than the matrix that also places them: a stretch in the matrix\nwould scale the instance positions too and smear the scatter instead of\nlengthening its blobs. So a scatter can sit next to loose props under the same\nsun without looking lit from somewhere else.\n\n`shadowLight` takes the direction from a `Light3d` you name, re-read every\ndraw, so a moving sun carries the shadows with it. Nothing is inferred: the\nengine never picks a dominant light, because a scene with several has no\nnon-arbitrary answer and one with none has no answer at all.\n\n### A floor that is not level\n\n`shadowGroundY` says where the floor is; `shadowGroundNormal` says which way\nit faces:\n\n```js\nprop.shadowGroundY = floorY;\nprop.shadowGroundNormal = [Math.sin(0.35), -Math.cos(0.35), 0];  // a 20 deg slope\n```\n\nWorld up is `(0, -1, 0)`, because render space is Y-down, and that is the\ndefault. The blob is **rotated** onto the plane rather than projected, so it\nkeeps its size; it sits directly under the caster, since the normal turns it\nrather than moving it, unless `shadowGroundPoint` says where instead. It needs\nthe floor named, by `shadowGroundY` or `shadowGroundPoint`, for the same\nreason the offset does: tilting the bounds fallback has no anchor to turn\nabout.\n\nThe tilt is clamped at 75 degrees alongside `shadowGroundY`, past which an\nedge-on blob has nothing left to show. That ceiling measures tilt from WORLD\nup, which stands in for \"tilted away from the viewer\" only while the camera\nlooks down the Y axis, so it is lifted alongside `shadowGroundPoint`, where a\npoint and a normal state the plane outright.\n\nOn an `InstancedMesh` one plane serves the **whole set**: it passes through\nthe set's origin at `shadowGroundY`, faces the normal, and every blob is\nprojected onto it whatever height its own instance sits at, exactly as the\nlevel case puts every blob at one height. A scatter over curved ground is\ntherefore one set per flat facet, each with the facet's own plane, not one\nset with an averaged normal.\n\nAll three compose on a set: the stretch turns the shared quad, the offset\nslides the whole set along the light, and both are carried onto the tilted\nplane rather than applied flat, so a bank's planting lies along its slope\ninstead of hovering across it.\n\nGive blobs to the plants that stand apart. Dense low ground cover is better\nleft without: a thousand blobs each as wide as its own fern sum to a darkened\nhillside, not to a thousand shadows, and ground cover is the ground.\n\nThese are **art direction, not a projection**. A stretched ellipse is not a\nsilhouette and has no contact with terrain, so on ground that is not the plane\nyou named it will not lie on it. Direction-correct shadows want a shadow map,\nwhich this tier does not do.\n\n### Get the sign right: the floor is a GREATER y\n\nRender space is **Y-down**, so the floor an object stands on is a *larger* y\nthan the object itself. Nudging a shadow plane below a surface is therefore a\n**plus**:\n\n```js\nmesh.shadowGroundY = WATER_LEVEL + 8;   // just under the surface — correct\nmesh.shadowGroundY = mesh.pos.y - 8;    // eight units ABOVE its own base — wrong\n```\n\nGet it backwards and the blob becomes a horizontal quad slicing through the\ncaster's own body. It is still drawn at full strength — the height fade sees a\n*negative* height and clamps to 1, so nothing warns you — but the depth test\nhides every part of it that falls inside the caster's silhouette. What survives\nis a thin ring, only as wide as the blob overhangs the object.\n\nThe symptom is distinctive and easy to misread: **shadows appear to switch on as\ncasters approach the camera.** That ring is a few pixels on the nearest object\nand sub-pixel further out, so a scene reads as \"only nearby things have\nshadows\" — which looks like culling, an LOD stage or a fog problem, none of\nwhich it is. Before chasing any of those, dump `shadowGroundY` against the\ncaster's own y and check which one is larger.\n\n## Collision in 3D\n\nUse a body with a depth shape, not a hand-rolled distance check. Two shapes\ncarry depth — `Box3d` and `Sphere` — and any pair of them is resolved in three\ndimensions; the response carries `overlapNZ` / `overlapZ` for the depth axis.\n\nPick by whether the thing turns. `Box3d` is world-axis-aligned and cannot be\noriented, so it fits walls, floors and crates and misfits anything that\ntumbles. `Sphere` has no orientation at all, which makes it the shape for\nspinning debris, projectiles, and anything spread over a curved surface: on a\nglobe each object's own \"up\" points a different way, so one axis-aligned box\ncannot fit any of them.\n\nA hand-rolled check is the usual mistake here and it costs twice — it throws\naway the penetration vector the response already carries, and it is invisible\nto the debug plugin's hitbox overlay, which draws from `body.shapes`. If the\nred boxes never appear, the reason is usually that there is no body to draw.\n\nAgainst a planar shape, either 3D shape degrades to its XY footprint with the\nplanar side read as unbounded along Z, so a sphere or box body still collides\nwith an ordinary collision layer, just without a depth result. And mind the\nnormal: a box pair separates along one world axis, a sphere along the line\nbetween two centres, so a sphere contact fills the 2D and depth halves of the\nresponse at the same time. See the physics skill for the full contract.\n\n## glTF / GLB scenes\n\nLoaded through the same level director as everything else:\n\n```js\nawait loader.preload([{ name: \"diorama\", type: \"glb\", src: \"data/diorama.glb\" }]);\nlevel.load(\"diorama\", { scale, castGroundShadow, shadowGroundY, onLoaded });\n\n// every ground-shadow setting travels except shadowGroundPoint, which is\n// per object and dropped by a scene load: a loaded scene can\n// be told where its sun is, and `shadowLight` is re-read every draw\nlevel.load(\"diorama\", {\n    scale, castGroundShadow: true, shadowGroundY: 0,\n    shadowLight: sun, shadowOffset: 0.6, shadowStretch: 1.8, async: true,\n});\n```\n\n`.obj` / `.mtl` are also supported loader types.\n\nAn **animated** asset instantiates as a `GLTFModel`, which is a game object as\nmuch as a scene: construct it straight from the parsed descriptor, and place it\nwith `pos` / `depth` / `rotate` / `scale` like any other renderable — the\nplacement drives the whole rig and composes with the playing clip.\n\n```js\nconst model = new GLTFModel(loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nmodel.setCurrentAnimation(\"paddle\", { loop: true });\nmodel.pos.set(x, y);\nmodel.depth = z;\n```\n\nTo scatter an authored mesh yourself, read the geometry off the descriptor and\nhand it to an `InstancedMesh` — `loader.getGLTF(name).nodes[0]` carries\n`vertices` / `uvs` / `normals` / `indices`. That wants a single merged\nprimitive exported at the origin; see `melonjs-3d-assets`.\n\n`melonjs-3d-assets` covers the rest: every `level.load` option, which material\nfeatures are imported, imported lights and their i","frontmatter":{"name":"melonjs-3d","description":"Use this skill for anything 3D or 2.5D in melonJS — Camera3d, Mesh, InstancedMesh, Sprite3d billboards, Light3d, ground shadows, glTF/GLB scenes, and depth sorting. Covers the Y-down/+Z-forward convention that is the inverse of OpenGL, the cameraClass opt-in, clip planes, and what does not work on the Canvas fallback. Triggers on: Camera3d, Mesh, InstancedMesh, Sprite3d, Trail3d, ribbon, exhaust trail, Light3d, billboard, glTF, glb, 3D, 2.5D, depth, cameraClass, fov, setClipPlanes, setFog, fog, distance fog, height fog, heightFalloff, transparent, transparency, alpha, blendMode, fade, castGroundShadow, lit, setBasis, getBasis, lookAt, up vector, gimbal, slerp, great circle, turn a direction.","license":"MIT"},"isInternal":false,"tokens":12820,"sizeBytes":50167},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-audio/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-audio/SKILL.md","title":"melonjs-audio","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-audio\ndescription: \"Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay, no sound, sound stopped working, context suspended, silent after idle, clips silent but tones play.\"\nlicense: MIT\n---\n\n# Audio\n\n## Two setup rules\n\n```js\nimport { audio, loader } from \"melonjs\";\n\naudio.init(\"mp3,ogg\");                      // 1. BEFORE preloading any audio\n\nawait loader.preload([\n    { name: \"cling\", type: \"audio\", src: \"data/sfx/\" },   // 2. a PREFIX, not a file\n]);\n```\n\n1. **`audio.init(format)` must run before any audio asset is preloaded**, and\n   lists the formats you actually ship. Preloading first throws\n   `target audio extension(s) should be set through me.audio.init() before\n   calling the preloader.` It returns `false` when no audio backend is\n   available, so it is worth checking. The format list defaults to `\"mp3\"`.\n2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus\n   each configured extension and uses the first that loads — so\n   `data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename\n   gives you a 404. A `data:audio/...` URL is the one exception: it is used\n   verbatim, prefix and extension skipped.\n\n`audio.hasAudio()` says whether the browser can play audio at all, and\n`audio.hasFormat(\"ogg\")` whether it can play one codec — useful for choosing\nthe `init` list or hiding a sound toggle on a device with no audio.\n\nOrder matters — melonJS tries the listed formats left to right, so put the\npreferred one first. Two formats (`\"webm,mp3\"` or `\"mp3,ogg\"`) is the usual\nbelt-and-braces for codec coverage. Accepted tokens: `mp3`, `mpeg`, `opus`,\n`ogg`, `oga`, `wav`, `aac`, `caf`, `m4a`, `m4b`, `mp4`, `weba`, `webm`, `dolby`,\n`flac`.\n\n## Effects versus music\n\n```js\naudio.play(\"cling\");                    // fire and forget\naudio.play(\"cling\", false, null, 0.5);  // loop, onend, volume\nconst id = audio.play(\"engine\", true);  // looping; keep the id to stop it\n\naudio.playTrack(\"theme\");               // background music\naudio.stopTrack();\n```\n\nUse **`playTrack`** for music, not `play`. `playTrack(name, volume?)` always\nloops, and it records the name as the current track — which is what\n`app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via\n`state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,\n`resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there\nis only ever one current *track name*.\n\n**`playTrack` stops the track it replaces** (20.9.0 and later; before that it\nonly assigned the slot, and the previous bed kept looping underneath where\n`stopTrack()` could no longer reach it). So a scene just states its own music\nand nothing cancels anything:\n\n```js\nclass PlayScreen extends Stage {\n    onResetEvent() { audio.playTrack(\"battle\", 0.6); }\n}\n```\n\nCalling it with the track already playing **restarts** it, the same as `play()`\non any other clip. A scene that wants the bed to carry on across a screen\nchange should not call it again, rather than rely on it doing nothing.\n\n`stopTrack()` is for where nothing follows: a scene whose successor has no\nmusic of its own, or a game going quiet. `state.change` destroys the outgoing\nstage **before** it resets the incoming one, so a `stopTrack()` in\n`onDestroyEvent` and a `playTrack()` in the next `onResetEvent` are in that\norder and never cancel each other.\n\nTo end a track on an event rather than on a screen change, say a run ending\nwhile the player stays on the same screen, `audio.fade(name, from, 0, ms)`\ntakes it down gently. A fade to zero is silence, not a stop, so the voice is\nstill live until something stops it or replaces it.\n\nEvery per-clip function throws `audio clip <name> does not exist` if the clip was\nnever loaded, and `play` additionally throws when a named sprite region is\nmissing.\n\n## Audio sprites\n\nOne file carrying many effects, with named regions in milliseconds:\n\n```js\nawait loader.preload([{\n    name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { jump: [0, 450], hit: [2000, 250], music: [4000, 12000, true] },\n}]);\n\naudio.play(\"sfx\", { sprite: \"jump\" });\n```\n\n### A loop with no gap\n\nAn MP3 encoder pads the end of its file. Chrome and Firefox trim the padding when\nthey decode it, Safari keeps it, so a looping MP3 has a short silence at every\nloop there (27 ms on a 12 s clip at 22 kHz, measured). Declare a region of the\nloop's exact length and loop on that: the voice never reaches the padding.\n\n```js\nawait loader.preload([{\n    name: \"ambience\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { loop: [0, 12000] },\n}]);\naudio.playTrack(\"ambience\", { sprite: \"loop\", volume: 0.6 });\n// or, as a plain clip: audio.play(\"ambience\", { sprite: \"loop\", loop: true })\n```\n\nOgg Vorbis carries no such padding.\n\nThe optional third element marks a region as looping: it loops unless `play()` is\ntold `loop: false` (before 20.11 it only looped when `loop: true` was passed as\nwell). `play()`'s second argument\ntakes either the original `loop` boolean or a `PlayOptions` object\n(`sprite`, `loop`, `onend`, `volume`, plus the spatial `follow`, `at` and\n`stopWithTarget`) — both forms work. Object fields win over the positional\n`onend` / `volume` arguments when both are supplied.\n\n## Clip state\n\n```js\naudio.duration(\"theme\");        // seconds — 0 while still loading\naudio.duration(\"sfx\", id);      // duration of the region THAT instance plays\naudio.playing(\"theme\");         // is anything playing?\naudio.playing(\"theme\", id);     // is that instance playing?\naudio.state(\"theme\");           // \"unloaded\" | \"loading\" | \"loaded\"\n```\n\n`stop()`, `pause()` and `resume()` take an optional instance `id` too; omit it\nand the whole group is affected. `audio.stop()` with no arguments at all stops\nevery sound. `audio.seek(name)` reads the position, `audio.seek(name, s, id?)`\nwrites it; `audio.rate(name, r, id?)` changes speed and pitch together\n(`0.5`..`4.0`, `1` is normal) and `audio.fade(name, from, to, ms, id?)` ramps\nthe volume (`0`..`1`, over milliseconds).\n\n## Volume and muting\n\n```js\naudio.setVolume(0.5);           // global, 0..1\naudio.getVolume();              // read it back\naudio.mute(\"cling\");            // one clip  — mute(name, id?, shouldMute = true)\naudio.unmute(\"cling\");\naudio.muteAll();                // everything (audio.disable() is an alias)\naudio.unmuteAll();              // (audio.enable() is an alias)\naudio.muted();                  // global mute state\n```\n\n`audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`\nalso clears the current-track pointer if it named that clip.\n\n### Losing focus mutes everything\n\nWhen the window loses focus the engine mutes all audio, and unmutes it on\nfocus. That covers sound effects, procedural `tone` / `noise` and the mixer\neffects, not just the music track that `state.pause()` pauses. Set\n`pauseOnBlur: false` in the application settings if a clip has to keep playing\nin the background.\n\n## Spatial audio\n\n```js\naudio.position(\"engine\", x, y, z);\naudio.orientation(\"engine\", x, y, z);\naudio.stereo(\"engine\", -1);     // -1 left … 1 right\naudio.panner(\"engine\", { coneInnerAngle: 90, /* … */ });\n```\n\n`position(name, x, y = 0, z = -0.5, id?)` and\n`orientation(name, x, y, z, id?)` both default `id` to \"the whole group\".\nCalled with just the clip name, each of the four returns the current value —\n`stereo` gives `0` and `position` gives `[0, 0, 0]` before they have ever been\nset.\n\n### Let the engine place the sound: `follow` and `at`\n\nThe shortest path to positional audio is a play option. Both take **world\npixels**, the same numbers as `pos.x` / `pos.y`, and the engine converts to\naudio space for you:\n\n```js\n// tracks the renderable every frame, and stops when it is destroyed\naudio.play(\"ufo\", { follow: this, stopWithTarget: true });\n\n// pinned to a fixed world point\naudio.play(\"portal\", { at: { x: 1200, y: 480 } });\n```\n\n`follow` and `at` are mutually exclusive: passing both throws, rather than\nquietly picking one. `follow` re-reads the target's **absolute** position each\nframe, so a renderable nested in a moving container is placed correctly. `stopWithTarget` is what you\nwant for a sound that belongs to an entity: without it a looping clip on a\ndestroyed renderable keeps playing from wherever it last was. `audio.unfollow(id)`\ndetaches a sound and leaves it playing where it is.\n\n### Move the listener, don't do the subtraction yourself\n\n```js\naudio.setListener(player);                  // tracked every frame\naudio.listener(x, y, z);                    // or place it by hand\naudio.listener();                           // → [x, y, z] in world pixels\naudio.listenerOrientation(fx, fy, fz, ux, uy, uz);\naudio.setListener(null);                    // stop tracking\n```\n\nWith a listener in the world, source positions are **absolute world\ncoordinates**, not offsets. The older idiom — passing `source.pos.x -\nplayer.pos.x` every frame with the ear stuck at the origin — still works, but\nonly if you leave the listener at `(0, 0, 0)`; mixing the two double-counts the\nplayer's position.\n\n`setListener` places the ear at the target's absolute position after the world\nupdates. A plain renderable contributes position only, so a side-scroller keeps\nthe default \"facing into the screen\" and gets left-right panning plus distance\nfalloff from the one call.\n\nA `Camera3d` target also contributes its **orientation**, taken from its own\nbasis, so turning the camera swings the stereo image with it. That is the whole\nof what a posed 3D scene needs:\n\n```js\naudio.setListener(app.viewport);                 // in a 3D scene this is a Camera3d\naudio.play(\"drone\", { follow: saucer });         // scene coordinates, directly\n```\n\n**Y is down.** Every one of these calls takes and returns melonJS world\ncoordinates, where y grows downward; the Y-up flip Web Audio wants happens\ninside the engine. Pass `pos.y` as it is.\n\n### The defaults are tuned for pixels\n\n`audio.setSpatialDefaults()` / `getSpatialDefaults()` set the panner attributes\na sound placed through `follow` or `at` starts from, and the shipped values\nassume a 2D game measured in pixels:\n\n| attribute | default |\n|---|---|\n| `refDistance` | `240` |\n| `maxDistance` | `10000` |\n| `panningModel` | `\"equalpower\"` |\n| `distanceModel` | `\"inverse\"` |\n\nWith those, a placed sound is at full volume out to 240 px and then halves\neach time the distance doubles: about half at 480, a quarter at 960, roughly\nan eighth at 2000. `maxDistance` is where it stops getting quieter.\n\n**They apply to placed sounds only.** A plain `audio.play()` followed by a\nbare `audio.position()` keeps WebAudio's own defaults, where `refDistance` is\n`1` METRE and a source 100 px out is already near silent. That path is\nunchanged, which is what keeps old code working.\n\nWiden or tighten the field once, globally:\n\n```js\naudio.setSpatialDefaults({ refDistance: 400, maxDistance: 20000 });\n```\n\n`audio.panner(name, attrs, id?)` still overrides per clip or per voice, and it\nmerges over the current values rather than replacing them, so writing one field\nno longer resets the rest. Called without an `id` it writes the **group**\ndefaults, which is what a clip-wide tuning call looks like.\n\nFor a 3D scene where front-to-back matters, `panningModel: \"HRTF\"` is worth the\ncost; for a 2D one it mostly muddies the stereo image.\n\n### `stereo` and `position`: last writer wins\n\nBoth drive the same per-instance panner node, and the engine swaps the node to\nmatch whichever you called **last**: `stereo()` after `position()` gives you\nplain left-right panning, and `position()` after `stereo()` gives you 3D\nfalloff. Neither silently does nothing any more, and the order you call them in\nno longer matters.\n\nStill pick one per voice and stay with it. A 2D game usually wants `stereo()`\nalone; reach for `position()` (or `follow` / `at`) when distance falloff\nmatters.\n\n**Spatial audio is WebAudio-only.** On a clip loaded with `stream: true` (or\n`html5: true`) `stereo` / `position` / `orientation` return early and do\nnothing — which is exactly the case for the long music tracks people reach for\nfirst. `follow` and `at` are equally inert on a streamed clip.\n\n## Procedural sound\n\nFor beeps, blips and noise without shipping files. Both take **one options\nobject**, and `duration` (in seconds) is required:\n\n```js\naudio.tone({ freq: 440, duration: 0.2, gain: 0.3, pan: -0.5 });\naudio.tone({ freq: [440, 660, 880], duration: 0.4 });   // layered partials\naudio.noise({ duration: 0.5, filter: { type: \"lowpass\", frequency: 800 } });\n```\n\n`tone` also takes `wave` (default `\"sine\"`), `attack` (`0.005`) and `pitchSlide`\n(`1`). `noise` takes `type` (`\"white\"` | `\"pink\"` | `\"brown\"`, default\n`\"white\"`), `attack`, `pan`, `filter` and `filterSweep`. `gain` defaults to `0.1`\non both.\n\n### Sequencing: `delay`, not `setTimeout`\n\nBoth take `delay` — seconds before the sound starts, scheduled on the **audio\nclock**. That is what builds a multi-part sound: a stinger's second note, an\nexplosion's double-tap.\n\n```js\naudio.tone({ freq: 392, duration: 0.12 });\naudio.tone({ freq: 587, duration: 0.18, delay: 0.11 });\naudio.tone({ freq: [784, 1176], duration: 0.75, delay: 0.23 });\n```\n\nDo not reach for `setTimeout` here. A timer fires on the main thread, so a busy\nframe slips the note late and the sequence audibly loses time — the fault is\neasy to blame on the sound design rather than the scheduling. `delay` is\nsample-accurate because the whole envelope is scheduled up front.\n\nBoth are **silent no-ops** when there is no WebAudio context — they bail on\n`getAudioContext() === null` rather than throwing. Neither needs the clip to be\nloaded; neither goes through the file-playback path at all.\n\n`audio.getAudioContext()` and `audio.getMasterGain()` are the escape hatches for\ncustom Web Audio work; both return `null` when audio is unavailable, so guard.\nConnect a custom graph to the master gain rather than `ctx.destination` if you\nwant it to respect `setVolume` / `muteAll`.\n\n## Effects on all game audio: reverb, echo, compressor\n\nThere is no built-in reverb, delay, distortion or compressor, and no per-clip\nfilter on file playback. There does not need to be one for the common case:\nevery buffered clip and every `tone` / `noise` passes through the master gain\non its way to the speakers, so an effect inserted **after** it applies to the\nwhole mix.\n\n```js\nconst ctx = audio.getAudioContext();   // creates the context if needed\nconst master = audio.getMasterGain();\nif (ctx && master) {                   // null when audio is unavailable\n    const comp = ctx.createDynamicsCompressor();\n    master.disconnect();               // master -> speakers becomes\n    master.connect(comp);              // master -> compressor -> speakers\n    comp.connect(ctx.destination);\n}\n```\n\nAny Web Audio node or chain works the same way: a `ConvolverNode` with an\nimpulse response for reverb, a `DelayNode` with a feedback gain for echo, a\n`BiquadFilterNode` for an underwater low-pass, a `WaveShaperNode` for\ndistortion. Do it once, at startup; the master gain lives as long as the\ncontext.\n\n- **`setVolume` / `muteAll` still work**, because they act on the master gain,\n  which is now upstream of the effect.\n- **Streamed clips are not affected.** A `stream: true` (`html5: true`) clip\n  plays through an `<audio>` element and never reaches the master gain — which\n  is usually the music. Preload the music buffered if it must go through the\n  effect.\n- **It is all or nothing.** The per-instance nodes are not public, so an effect\n  on one clip only (muffling one sound behind a wall) is not possible through\n  this route; `noise` has its own `filter` for generated sounds.\n- To remove it, reverse the wiring: `master.disconnect(); master.connect(ctx.destination);`.\n\n## Autoplay: sound is silent until the first gesture\n\nBrowsers create the audio context in the `suspended` state and keep it there\nuntil a user interaction. Nothing errors — audio simply does not play, which\nreads as a bug. melonJS installs one-shot `touchstart` / `touchend` / `click` /\n`keydown` unlock listeners on `document`, so the first real gesture anywhere\nunlocks it and fires the clip's `on.unlock` callback. Design for it: start music\non the first click or key press, not on load. The usual way to guarantee one is\na gate screen — a splash, a \"press any key\", or a health/photosensitivity notice\n— whose dismissal *is* the gesture, so the menu behind it can start its music\nunconditionally. Note the unlock listens for `keydown` too, so a keyboard-only\nplayer is covered.\n\n### The context can stop again, long after it was unlocked\n\nUnlocking is only the first half. A context that has been running can be\nstopped later: melonJS suspends it itself after thirty idle seconds to save\npower, and browsers stop it for their own reasons, a backgrounded tab among\nthem. Recovery is automatic and you do not have to write it: a user gesture\nor the tab coming back to the front revives it, and a deliberate idle\nsuspension is left alone so the power saving still works.\n\n**The symptom worth memorising**, because it points straight at the cause:\nif `tone` and `noise` are audible while loaded clips are silent, the context\nis healthy and the engine's playback state is wrong. The generators only need\na live context, where a clip goes through the engine's state machine and can\nbe parked waiting for a resume. It is never \"the context died\".\n\nBoth routes ask the same owner to resume, so hearing a tone tells you the\nhardware is back; it tells you nothing about whether the clips waiting on the\nengine's state have been let go.\n\nThat asymmetry was a real Safari-only bug: thirty idle seconds before the\nfirst click left the engine waiting on a suspend the browser never confirmed,\nso every clip queued for the life of the page while generated sound played\nnormally. Fixed in 20.9, and the lesson generalises: a clip that will not play\nis a state question, not a hardware one.\n\nPlayback also starts **asynchronously**: `play()` returns an id while the\ninstance is still starting, so `playing()` can legitimately be `false` on the\nvery next line.\n\n## Load-time options\n\n```js\n{ name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n  sprite: { jump: [0, 450] },\n  pool: 5,            // finished instances kept for reuse — NOT a concurrency cap\n  rate: 1.0,          // initial playback rate, 0.5..4.0\n  mute: false,\n  loop: false,\n  autoplay: false,\n  preload: true,      // or \"metadata\"\n  format: \"mp3\",      // explicit hint when the URL has no usable extension\n  stream: true,       // long music: stream rather than decode into memory\n  on: { play() {}, end() {}, stop() {}, unlock() {} },\n}\n```\n\n`pool` (default `5`) is a **reuse cap on finished voices**, not a limit on\nsimultaneous playback: when the number of ended voices exceeds it, the surplus\nare disconnected and dropped. Raise it for a clip triggered in rapid bursts, not\nto allow more overlap.\n\nUse `stream: true` for long music tracks — it plays through an HTML5 `<audio>`\nelement instead of decoding the whole file into memory. (`html5: true` is the\nsame switch under its backend name.) `on` accepts the full lifecycle set:\n`play`, `pause`, `stop`, `end`, `fade`, `seek`, `rate`, `volume`, `mute`,\n`unlock`.\n\nOutside a preload, `audio.load(asset, onload?, onerror?)` takes the same\ndescriptor and loads one clip at runtime — for a level's sounds fetched when\nthe level starts. The loader calls the same function, so the same rules apply.\n\nRe-preloading a manifest that already contains a loaded clip is a no-op —\n`audio.unload(name)` first if you genuinely want to reload it.\n\n**Credentials use the same global switch as every other asset type** —\n`loader.setOptions({ withCredentials: true })`, not an audio-specific option.\nBuffered audio fetches through the shared loader path, so it honours it exactly\nas an image or a JSON file does. A `stream: true` clip does not: it plays\nthrough an `<audio>` element, which needs a `crossorigin` attribute rather than\nfetch credentials. Preload a clip buffered if it sits behind a login.\n\n**Failing clips and `setStopOnAudioError(false)`.** The default is to fail the\npreload when a clip cannot be loaded after its retries: the error reaches the\nloader, so `await loader.preload(...)` rejects and your `catch` runs, and the\nreason is logged. Turn the flag off and the engine mutes audio, warns\n(`failed loading <name>, disabling audio`) and lets the load *continue* — the\npreload finishes and the game reaches the next scene without sound.\n\nBefore 20.4 both halves were broken: with the flag off it disabled audio and\nfailed the preload anyway, and a clip whose bytes arrived but would not decode\n(a missing file served as HTML by an SPA rewrite, say) reported nothing at all\neither way — blank screen, empty console.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| audio assets 404 | `src` given as a full filename instead of a directory prefix |\n| `target audio extension(s) should be set...` thrown at preload | `audio.init()` not called before preloading |\n| `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |\n| nothing plays until the user clicks | browser autoplay policy — expected, design around it |\n| `playing()` is false right after `play()` | playback starts asynchronously |\n| music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |\n| two music tracks playing at once, and `stopTrack()` only silences one | melonJS before 20.9.0: `playTrack` did not stop the previous track, so the scene that started it had to `stopTrack()` on the way out |\n| the music restarts when returning to a screen | `playTrack` with the track already playing restarts it; don't call it again if the bed should carry on |\n| `stereo` / `position` do nothing | the clip was loaded with `stream: true` — spatial is WebAudio-only |\n| one format works, another does not | that codec is not in the `audio.init()` list |\n| audio behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses |\n| a streamed clip ignores `withCredentials` | `stream: true` plays through an `<audio>` element; preload it buffered instead |\n| `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |\n| `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |\n| a positioned sound is silent, or only audible right next to the player | placed by hand with `audio.position()`, which keeps WebAudio's metre defaults (`refDistance: 1`) — use `follow` / `at`, or set `refDistance` in pixels with `audio.panner()` |\n| a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |\n| a followed sound pans to the wrong side as the player moves | the listener was moved **and** positions are still relative to the player — with a listener in the world, pass absolute world coordinates |\n| a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |\n| a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |\n| a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |\n| a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see \"A loop with no gap\") |\n| sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |\n\n## Related skills\n\n- `melonjs-getting-started` — preloading and asset ordering\n","frontmatter":{"name":"melonjs-audio","description":"Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay, no sound, sound stopped working, context suspended, silent after idle, clips silent but tones play.","license":"MIT"},"isInternal":false,"tokens":6325,"sizeBytes":24770},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md","title":"melonjs-camera-and-drawing","category":"anthropic-skill","format":"markdown","content":"# melonjs-camera-and-drawing\n\nPath: `packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md)","tokens":300,"sizeBytes":243},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-deployment/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/SKILL.md","title":"melonjs-deployment","category":"anthropic-skill","format":"markdown","content":"# melonjs-deployment\n\nPath: `packages/melonjs/skills/melonjs-deployment/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/SKILL.md)","tokens":300,"sizeBytes":219},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md","title":"melonjs-display-and-scaling","category":"anthropic-skill","format":"markdown","content":"# melonjs-display-and-scaling\n\nPath: `packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md)","tokens":300,"sizeBytes":246},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md","title":"melonjs-effects-and-shaders","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-effects-and-shaders\ndescription: \"Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: Tween, easing, animate an effect, ShaderEffect, addPostEffect, removePostEffect, getPostEffect, postEffectNeedsCapture, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter.\"\nlicense: MIT\n---\n\n# Post effects and custom shaders\n\n> melonJS ships a catalogue of post effects and a dual-language shader system.\n> The recurring trap is that every shader path needs a GPU backend and\n> **degrades to nothing** on Canvas rather than erroring. Blend modes are the\n> exception — those work on all three renderers.\n\n## Applying an effect\n\nEffects attach to a renderable **or** to a camera (for full-screen grading):\n\n```js\nimport { VignetteEffect, GlowEffect } from \"melonjs\";\n\nsprite.addPostEffect(new GlowEffect(app.renderer));\napp.viewport.addPostEffect(new VignetteEffect(app.renderer));\n```\n\n`renderable.shader = …` is **deprecated since 19.2.0**, and its setter destroys\nwhatever it replaces. Use `addPostEffect` / `getPostEffect` / `removePostEffect`.\n\n### A camera effect covers the HUD too\n\nA camera's post-effect brackets the **entire** world draw — floating children\nincluded. So the obvious way to write a full-screen pass also washes over every\nHUD label in that world, which is rarely what you want.\n\nTo land a pass *after* the world but *before* the HUD, host the effect on a\nscreen-filling floating renderable ordered below the HUD's z instead:\n\n```js\nconst quad = new Sprite(0, 0, { image: anyImage });\nquad.anchorPoint.set(0, 0);\nquad.scale(viewW / anyImage.width, viewH / anyImage.height);\nquad.floating = true;                 // its own uv is now screen space\nquad.blendMode = \"additive\";\nquad.addPostEffect(myEffect);\nworld.addChild(quad, HUD_Z - 10);     // 2D: below the labels. See the note.\n```\n\nTwo things follow from the quad filling the frame: its own `uv` is screen space\n(so a pass like this needs none of the `screen_uv` / `screen_texture` builtins),\nand the incoming `color` is the quad's own texture, which a body that paints\nfrom scratch can ignore entirely.\n\n**Depth here is easy to get backwards, and the rule FLIPS between the two sort\nmodes.** Under the default `sortOn = \"z\"` a higher z draws later, i.e. on top,\nso `HUD_Z - 10` puts the pass under the labels as written above. Under\n`sortOn = \"depth\"`, which every `Camera3d` scene sets, a floating child's sort\nkey is `pos.z * pos.z` and the walk paints far to near, so the SMALLER\nmagnitude is nearer and draws on top — `HUD_Z - 10` there puts the pass OVER\nthe labels, the opposite of the comment.\n\nSo in a 3D scene give the overlay a larger magnitude than the HUD, and give\nthe HUD a small one (`melonjs-3d` and `melonjs-ui-and-text` both say a HUD\nwants a small depth, not the huge z that would put it on top in 2D).\n\nEither way, verify it rather than reasoning about it: return a flat colour from\nthe body for one frame and see what it tints.\n\n## A renderable that draws with PRIMITIVES has to ask to be captured\n\nOne effect is applied the cheap way: the renderable is drawn with the effect's\nown program instead of being captured offscreen and post-processed, which costs\nno render target. That is equivalent only when everything the renderable draws\nis a textured quad. `fillRect`, `strokeRect` and `renderer.fill(shape)` go\nthrough a batcher that never reads that shader, so **one** effect on such a\nrenderable would silently do nothing, while **two** would work, because a chain\nalways captures.\n\nSo a renderable of your own that draws with primitives says so:\n\n```js\nclass Bar extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.postEffectNeedsCapture = true;   // ← or one effect is a no-op\n    }\n    draw(renderer) {\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\n`ProgressBar` and `Trail` set it for you, and a sprite needs nothing: the flag\ndefaults to `false` and only ever changes the single-effect case.\n\n## Toggle with `enabled`, do not remove\n\n**`removePostEffect()` destroys the effect** — it calls `effect.destroy()` and\nfrees GPU resources, so the instance cannot be re-added. Same for\n`clearPostEffects()` and for reassigning the deprecated `.shader`.\n\n```js\n// ✗ destroys it; re-adding later fails or forces a recompile\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nAn effect with `effect.shared === true` opts out of auto-destroy because it is\nreused across renderables.\n\n## Built-in effects\n\nTwenty-three presets, every one constructed as `new XEffect(renderer, options)`\nand every one shipping both a GLSL and a WGSL body, so they all run on either\nGPU backend: `VignetteEffect`, `ScanlineEffect` (optional CRT curvature),\n`BloomEffect`, `GlowEffect`, `BlurEffect`, `PixelateEffect`,\n`ChromaticAberrationEffect`, `DropShadowEffect`, `FlashEffect`,\n`OutlineEffect`, `ColorMatrixEffect`, `DesaturateEffect`, `DissolveEffect`,\n`TintPulseEffect`, `WaveEffect`, `SwayEffect`, `InvertEffect`, `SepiaEffect`,\n`HologramEffect`, `ShineEffect`, `RadialGradientEffect`, `SquishEffect`,\n`ToneMappingEffect`.\n\n**Colors take an alpha.** Every color option and `setColor` takes `[r, g, b]`\nor `[r, g, b, a]`, each 0.0 to 1.0, and the alpha is the color's own opacity: a\nhalf-transparent outline, a glow at half strength, a flash that only half\ncovers. `[r, g, b]` is opaque, as it always was. Any other length throws.\n`RadialGradientEffect` takes a `Color` instead, and its alpha scales the spot.\n\n```js\nsprite.addPostEffect(new OutlineEffect(app.renderer, { color: [1, 1, 1, 0.5] }));\n```\n\n**On a sprite cut from an atlas or a tileset, the effects that read around a\npixel keep to the sprite's own frame.** `GlowEffect`, `OutlineEffect`,\n`BlurEffect`, `DropShadowEffect`, `ChromaticAberrationEffect`, `WaveEffect`,\n`SwayEffect`, `PixelateEffect` and `BloomEffect` read the neighbouring frames\nas transparent, as if the sprite were drawn on its own, so they never show\nalong its edges and the atlas needs no padding for them. Past the texture's own\nedge (a standalone image, or the outside of an atlas) nothing changes. Their `textureSize` is still the size of the whole atlas image: it is\nwhat turns a width in texels into a distance in the texture.\n\n`SwayEffect` leans a sprite or a layer from side to side, as if in a breeze: the\ntop sways most and everything from its `anchor` row down stays put, so a tree's\ntrunk or a reed's root stays planted. It suits anything planted at its bottom;\nsomething hanging from its top, like a banner, needs its own shader. It is a\n`WaveEffect`, which takes the same `anchor`, so it adds no shader. Drive it with\n`setTime(seconds)` every frame, like any time-based effect; it wraps the phase\nitself, so a raw clock never costs precision on a phone's medium-precision\nfloats.\n\n```js\nconst sway = new SwayEffect(app.renderer, { strength: 0.02, anchor: 0.9 });\ntree.addPostEffect(sway);\n// each frame\nsway.setTime(timer.getTime() / 1000);\n```\n\nRows are measured across the texture the effect samples, so on a sprite cut from\nan atlas the anchor is a row of the atlas: it suits a standalone image or a\nlayer best.\n\n`RadialGradientEffect` is a procedural soft spot: solid at the centre of its\nhost quad, fading linearly to transparent at the edge, and elliptical for free\non a non-square quad. It is the one to reach for instead of baking a disc into\na canvas, for a pickup highlight, a trigger marker or a damage flash. Its\nfalloff assumes the quad samples a FULL texture rect, so pair it with a\nstandalone image or `Renderer.getWhitePixel()` rather than a sprite that draws\na sub-region of an atlas, where the centre would land off-quad.\n\n`SquishEffect` bulges a sprite's middle out (`setBulge(0.3)`) or pinches it in\n(`setBulge(-0.3)`), its top and bottom rows staying put. Bulging out widens the\nsprite past its own frame, and the effect draws on a wider quad to make room,\nso the art needs no transparent margin. On its own it is a jelly wobble; paired\nwith a change of height it is **squash and stretch that keeps the volume**:\nscale the sprite to `scaleY` of its height about its feet and set the bulge to\n`1 - scaleY`, so a stretched sprite pinches in and a squashed one bulges out.\nEase `scaleY` back to 1 with a short `Tween.Easing.Back.Out`; an elastic ease\nkeeps it trembling long after the landing.\n\n```js\nconst squish = new SquishEffect(app.renderer);\nplayer.addPostEffect(squish);\n// every frame, from the player's update()\nconst feet = player.height * (1 - player.anchorPoint.y);\nplayer.currentTransform\n    .identity()\n    .translate(0, feet)\n    .scale(1, scaleY)\n    .translate(0, -feet);\nsquish.setBulge(1 - scaleY);\n```\n\nApply that from the sprite's own `update()`, not from a tween's `onUpdate`. A\nphysics adapter that mirrors a body's angle (Matter does) rewrites\n`currentTransform` on every step, before the renderables update, so a scale set\nanywhere else is gone by the time the sprite is drawn. Let the tween animate a\nplain `{ scaleY }` object and read it in `update()`.\n\nIt is made for a sprite carrying it as its only effect: chained with another,\nor on the camera, the frame it measures against is the whole capture, so it\nsquishes the whole frame. The bulge is outside the sprite's bounds, so culling\nand pointer events ignore it.\n\n**`BloomEffect` and `GlowEffect` are not the same thing**, and reaching for the\nwrong one is the usual disappointment here. `GlowEffect` draws a halo OUTSIDE a\nsprite's silhouette: its body returns the colour untouched wherever alpha is\nalready non-zero, so on an opaque frame it returns early on every fragment and\na camera-wide glow does nothing at all. `BloomEffect` is the screen bloom —\nbright pixels bleed into their neighbours — and it is the one to put on the\ncamera:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\n// only the brightest highlights, with a wider halo\napp.viewport.addPostEffect(\n    new BloomEffect(app.renderer, { threshold: 0.85, radius: 4 }),\n);\n```\n\nIts tap offsets are in texels, so it takes its size from the renderer at\nconstruction; call `setTextureSize(w, h)` from an `event.ONRESIZE` handler, or\nwhen applying it to something smaller than the frame.\n\n**`ToneMappingEffect` goes after it, and goes last.** Bloom spreads the bright\nparts; the tone map is what stops the sum of the two clipping flat at white,\nwhich is the difference between an effect that reads as light and one that\nreads as a white sticker. `exposure` lifts the image into the curve, and the\ncurve (`aces`, `reinhard` or `exponential`) is baked at construction:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\napp.viewport.addPostEffect(\n    new ToneMappingEffect(app.renderer, { exposure: 1.6 }),\n);\n```\n\nIt is a grade, not a colour pipeline. By default melonJS renders into `RGBA8`\neverywhere, so the frame reaching it is already clamped and `exposure` past 1\nis what gives the curve range to compress.\n\n**`hdr: true` in the application settings changes that**: render targets\nbecome half-float, so the chain carries values above 1 instead of clamping at\nevery write, and the bloom threshold starts discriminating genuinely\nover-bright pixels rather than merely bright ones. Off by default: it costs\nGPU memory on the camera's two render targets and depends on the driver.\n\nOn its own it is **not HDR output**: the frame is still presented in SDR, so\nanything above 1 clamps on the final blit. What it buys is that nothing clamped\nBEFORE then. Removing that last clamp is a second, separate opt-in —\n`hdrOutput`, below — and it is WebGPU-only.\n\n```js\nconst app = new Application(1024, 576, { hdr: true, … });\n// …and check what you actually got, since a driver can refuse it\nif (app.renderer.supportsHDR) { /* the chain now carries values above 1 */ }\n```\n\nBranch the GRADE on that flag, not just the feature. An exposure under 1 and a\nbloom threshold at 1 make sense only where values above 1 exist; applied to a\nchain that clamps, the same numbers tone map an already display-referred frame\nand switch the bloom off, which reads as a dark, flat picture rather than as a\nmissing feature. Where there is no headroom, squeeze each emitter into range in\nits own shader instead and put the bloom threshold back under 1.\n\n**Nothing is added to your chain.** `hdr` is additive: the same frame, plus\nadditive content that no longer saturates per write and a threshold that means\nsomething. Anything still above 1 at the end clamps on the blit, as it always\ndid.\n\nA tone map is therefore a decision, not a consequence, and it is a bigger one\nthan it looks. Every curve maps 1 to less than 1, so a 2D look built ON the\nclamp — clipped white specular, saturated neon — goes matte the moment you add\none, at any exposure. Add one when your art is authored in linear light and you\nwant the highlights rolled off; skip it when the clipping IS the look.\n\nIt applies to the CAMERA chain only. A sprite's chain keeps 8-bit targets on\npurpose: its final blit composites with `ONE_MINUS_SRC_ALPHA`, and additive\nblending accumulates alpha past 1 in a float target, so that factor would go\nnegative and subtract the backdrop.\n\n### Once it is on: putting values above 1 into the frame\n\n`hdr: true` on its own changes nothing visible. It removes a ceiling; something\nstill has to reach for it. No single draw exceeds 1 on its own, so a game that\nturns the setting on and changes nothing else renders very nearly the frame it\ndid before and looks like the feature is broken. The exception is worth knowing\nbecause it is free: additive content that already overlapped was being clipped\nat every write, and stops being.\n\nWhat produces over-bright pixels:\n\n```js\n// a built-in colour matrix, scaled past 1 — no custom shader needed\nsprite.addPostEffect(new ColorMatrixEffect(app.renderer).brightness(4));\n\n// in 3D, both of these were always unbounded; the 8-bit target was the only\n// thing destroying the range. `emissive` is a 3-component Float32Array, set\n// from the mesh settings or replaced wholesale — it is not a Color\nconst mesh = new Mesh(x, y, { emissive: [2.5, 1.8, 0.4], /* … */ });\nlight.intensity = 3;\n\n// and additive blending, which accumulates past 1 once the target can hold it\nsprite.blendMode = \"additive\";\n```\n\n**One effect, not two.** A renderable carrying exactly ONE post effect takes\nthe renderer's `customShader` fast path: no offscreen target, and it draws\nstraight into the camera's half-float target with its range intact. A chain of\ntwo or more is routed through a pooled intermediate that stays 8 bits per\nchannel on purpose, so the energy is clamped there, before the camera ever sees\nit. Adding a second effect to an emitter is enough to silently undo its\nheadroom.\n\n### Everything inside the camera is graded, including your UI\n\nThe tone curve runs on the camera's whole frame. A `floating` HUD is still\ninside it, so a label authored at display white arrives at the curve as 1 and\nleaves at `1 - exp(-exposure)` — at exposure 0.45 that is 0.36, and the HUD\ncomes out at roughly a third of its brightness for no reason the author can\nsee.\n\nUI in an HDR chain has to be authored for that chain, exactly as the scene is:\n\n```js\n// drive the readout so the curve lands it back at display white. `shared`\n// keeps one instance across every element: it opts the effect out of the\n// per-renderable auto-destroy, so the first element torn down does not take\n// the program the others are still using\nconst energy = new ColorMatrixEffect(app.renderer).brightness(2);\nenergy.shared = true;\nfor (const element of hudElements) element.addPostEffect(energy);\n```\n\nPut it on each ELEMENT, not on their container: `beginPostEffect` clears\n`customShader` for any renderable with no effects of its own, so a container's\nshader is thrown away by its first child. The alternative is to keep the UI out\nof the camera pass entirely, by overriding `Stage.draw` and drawing it after\n`super.draw(...)` — at the cost of the bloom that makes additive UI glow.\n\nThe quad trick under *A camera effect covers the HUD too* does not help here.\nIt works by hosting a pass BELOW the HUD's z, and a tone map cannot live there:\nit has to be the last thing before the blit to the 8-bit canvas, which is the\ncamera. Anything the camera draws goes through it.\n\nPick the multiplier by measuring against the no-headroom build, not by\ninverting the curve. Inverting it over-estimates, because additive UI is inside\nthe BLOOM too and driving it harder also makes it spill wider into the same\npixels.\n\n### `hdrOutput`: actually presenting in HDR, and what it costs you\n\n`hdr` gives the chain headroom and then throws it away at the very last step,\nbecause the surface the browser presents into is 8 bits per channel. Values\nabove 1 clamp there, exactly as they did before the setting existed.\n`hdrOutput` changes that surface, so they reach the compositor instead:\n\n```js\nconst app = new Application(1024, 576, { hdr: true, hdrOutput: true, … });\nif (app.renderer.supportsHDROutput) { /* the display is getting values above 1 */ }\n\napp.renderer.setHDROutput(false);   // and at runtime, for a settings screen\n```\n\n**It is WebGPU-only, and that is not a melonJS limitation.** The WebGPU\nspecification defines a tone mapping mode on the canvas configuration, and two\nengines ship it. The WebGL2 equivalent needs a drawing-buffer tone mapping mode\nthat is still an unapproved specification change with no implementation in any\nbrowser; the colour-space half of it exists in Chrome behind\n`--enable-experimental-web-platform-features`, which is not something you can\nask a player to enable. So the WebGL2 backend reports `false`, warns once if\nyou asked, and presents in SDR as it always has.\n\nThat makes this the first setting where **the same machine answers differently\ndepending on which backend `video.AUTO` picked**. Branch on\n`renderer.supportsHDROutput`, never on the setting.\n\n**It requires `hdr`** — the last pass blits from the camera's target, so if\nthat target is 8 bits there is nothing above 1 left to present and the extra\ncost would buy an identical picture. Turning `hdr` off turns this off with it.\n\n**It changes how your game looks, on an HDR display.** Everything else here is\ncareful not to: `hdr` is additive, the tone curve defaults to none. This one\nremoves the final clamp, so anything you drove above 1 — an additive stack, an\nemitter, a `brightness()` above 1 — gets genuinely brighter rather than\nsaturating at white. That is the point of it, and it is why it is a separate\nopt-in rather than part of `hdr`. Two consequences worth planning for:\n\n- a look built on clipped whites will not survive it unchanged, because the\n  clipping is what you were art-directing against\n- a tone curve and `hdrOutput` pull in opposite directions. A curve exists to\n  fit values into `[0, 1]`; if you have removed the clamp, either drop the\n  curve or give it a white point high enough to leave the top of the range\n  alone\n- **`\"aces\"` cannot produce HDR output.** Its expression clamps to `[0, 1]`\n  internally, so everything past the shoulder collapses to one value before\n  the white point scales it — different bright things come out equally bright.\n  Under `hdrOutput` use `\"reinhard\"` or `\"exponential\"`, which approach 1\n  asymptotically without clamping and so keep their ordering when the white\n  point lifts them past it, or use no curve at all\n\n**What it is not.** It does not make an SDR display brighter, and there is no\nway to ask melonJS how bright the panel is — `matchMedia(\"(dynamic-range:\nhigh)\")` is the closest the platform gets. Calibrating in nits, the way a\nconsole settings screen does, is not reachable from the web platform today.\n\n### Exposure, and the runtime toggle\n\nUnder `hdr` the useful exposure is often **below** 1, which surprises people who\nhave only used the effect as an LDR grade. The curve has to work where it\nseparates: at exposure 1 the ACES shoulder maps 2, 4 and 8 to 0.86, 0.96 and\n0.99, so genuinely different energies all arrive at white and the headroom buys\nnothing you can see. Lower exposure spreads them out. The right value is a\nproperty of how bright your scene is authored, so measure it; there is no\ndefault that is right for two different games.\n\nA tone curve is a setting rather than an effect you wire up:\n\n```js\nrenderer.setToneMapping(\"aces\", { exposure: 0.4, white: 6 });\nrenderer.setToneMapping(\"none\");                       // and off again\n```\n\n`\"none\"` by default. The renderer keeps the curve LAST on the camera, so\nwhatever you put in front of it is mapped along with everything else. Changing\nthe mode rebuilds one shader (the curve is baked); `exposure` and `white` are\nuniform writes, so a settings screen can drive them live. `white` is the value\nthat comes out as display white — the knob a \"brightness\" or \"peak\" slider\nwants, since exposure alone drags the midtones up with the highlights.\n\n`renderer.setHDR(enable)` moves the whole pipeline at runtime, the way\n`setAntiAlias` does, for a settings toggle. It empties the render-target pool\nwhen the format actually changes, so it is not something to drive per frame.\nThe boot header reports what happened either way:\n\n```\nresolution: requested 1024x576 (HDR), got 1024x576 (SDR)   ← the driver refused\n```\n\nFor grading without writing a shader yourself, the camera has a colour matrix:\n\n```js\napp.viewport.colorMatrix.contrast(1.1).saturate(1.1);\n```\n\nIt is still *implemented* as one: a non-identity matrix makes the camera append\nan internal `ColorMatrixEffect` to its post-effect chain for that frame, so it\nneeds a GPU backend like everything else here.\n\n## Custom effects are dual-language\n\nA custom `ShaderEffect` supplies a fragment **body**, not a whole program. To run\non both GPU backends it needs GLSL *and* WGSL — a GLSL-only effect silently does\nnothing when `video.AUTO` lands on WebGPU.\n\n```js\nconst effect = new ShaderEffect(app.renderer, { glsl, wgsl });\neffect.setUniform(\"uStrength\", 0.5);\neffect.setTexture(\"uScene\", tex);\neffect.setTime(timer.getTime() / 1000);   // setTime takes SECONDS; getTime is ms\n```\n\nUniform names are shared across the two bodies — in WGSL they are the members of\none `@group(3) @binding(0) var<uniform>` struct — so a single `setUniform` call\nfeeds whichever backend is live. `setTime` is a convenience for a `uTime`\nuniform and silently does nothing if the shader does not declare one.\n\n**Never put a backtick in a shader body, including in its comments.** An inline\nbody is a JavaScript template literal, so a backtick ENDS it — and what follows\nis parsed as code, which fails somewhere further down the file with an error\nthat says nothing about shaders:\n\n```js\nconst glsl = `\n// x * x, not `pow(x, 2.0)`: the base goes negative here   <-- ends the literal\nvec4 apply(vec4 color, vec2 uv) { ... }\n`;\n```\n\nMarkdown-quoting an identifier is a reflex when writing a comment, and this is\nthe one place it breaks the file. Write `pow(x, 2.0)` bare. A body loaded from\na `.glsl` / `.wgsl` file has no such problem, which is one more reason to move\na shader out once it stops being a sketch.\n\n### One effect is one program — prefer preloading, and share\n\nA `ShaderEffect` compiles and links in its **constructor**, and the link is\nchecked with `getProgramParameter(LINK_STATUS)` straight after `linkProgram` —\na blocking call. So the cost is paid on whatever frame the effect is\nconstructed, and *n* identical effects cost it *n* times. `clone()` does not\navoid it: it constructs a new effect and links a new program.\n\nPreload it instead. Loader type `\"shader\"` takes a `src` **or inline source via\n`data`**, compiles at load time (inside the loading screen), and hands back a\n**shared** instance:\n\n```js\nloader.preload([{ name: \"ramp\", type: \"shader\", data: myFragmentBody }]);\n// …then, for every label that wants it:\nlabel.addPostEffect(loader.getShader(\"ramp\"));\n```\n\n### Declare a level's effects with that level's assets\n\nAn effect the loader knows about can be prepared before anything draws it; an\neffect built inline cannot, because it does not exist until the code that\nconstructs it runs — which is the stage or level you were hoping to speed up.\nThat is the practical reason to prefer the asset form even when the source is a\nstring in your own module:\n\n```js\n// with the level's other assets, not in the stage that uses them.\n// `preload()` rather than `load()` — preload is what warms the shaders it\n// brought in, and it can be called again per level, not just at boot\nawait loader.preload([\n    { name: \"ripples\", type: \"shader\", src: {\n        glsl: \"assets/level2/ripples.glsl\",\n        wgsl: \"assets/level2/ripples.wgsl\",\n    }},\n    // …that level's textures, maps and audio alongside it\n], undefined, false);   // `false`: stay on this stage, no loading screen\n```\n\n`src` takes the dual-language pair, so one asset carries both realizations and\nthe two backends cannot drift apart. `data` takes inline source the same way.\n\nWith `prewarm` on (the default since 20.6), `loader.preload()` warms on its way\nthrough — automatically, on every call — and compiles the GPU-side objects for\nevery shader it holds — on WebGPU that is the\nWGSL module, which is otherwise built inside the draw path the first time the\neffect is used.\n\nSince 20.10 the rest is warmed when the effect is **attached**, inline effects\nincluded: `addPostEffect` (or assigning `shader`) compiles the pipelines that\neffect will draw with on that renderable — on WebGPU the costly part, keyed by\nthe renderable's blend mode and target as well as the shader — and on WebGL\nlinks the spliced program when the host is a mesh. It finishes off the frame,\nso attach when you build (a pool, a stage starting), not when you show: an\neffect attached and drawn in the same frame still compiles on that frame.\n\nOne caveat if you are moving an inline effect to a file: a template literal can\ninterpolate constants into the source (`vec2(${ASPECT}, 1.0)`), and a static\nfile cannot. Promote those to uniforms rather than duplicating the numbers\nacross the GLSL and WGSL copies — duplicated constants are exactly how the two\nbackends drift.\n\nSharing one instance across renderables is safe *because* the loader sets\n`effect.shared`. Without that flag a renderable's teardown destroys its post\neffects, which would free the program out from under everything else still\nusing it. Uniforms live on the effect, so a shared instance means one set of\nuniform values — fine when the look is uniform, and the reason to clone (and\npay a link) when it is not.\n\nNote this covers **your** effects only. The engine's own mesh/quad shader\nvariants are covered by the loading-screen warm-up and by the per-renderable one\n— see `melonjs-loading-assets`.\n\n### Screen-reading builtins\n\nFor refraction, distortion and anything that samples what is already drawn:\n\n- **`screen_texture`** — a sampler kept filled with everything drawn so far\n  (GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`)\n- **`screen_uv`** — this fragment's 0..1 position in that capture\n- **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame\n\nIn WGSL the capture is sampled through `screen_sampler` (clamped) or\n`screen_sampler_repeat` (wrapping) instead of GLSL's `: screen_texture`\nannotation. `screen_uv` is y-**up** in GLSL and y-**down** in WGSL, which exactly\nmatches each backend's own capture orientation — so a body that samples\n`screen_texture` at `screen_uv` needs no flip in *either* language.\n\nTwo places where a straight port really does differ:\n\n- **A capture you bind yourself.** `renderer.toFrameTexture()` grabs the frame\n  into a `Texture2d` you hand to `setTexture` — the supported replacement for\n  `readPixels` tricks. The GL capture is bottom-up (GLSL bodies sample it with\n  `1.0 - uv.y`) and opaque RGB; the WebGPU capture is top-down (the WGSL twin\n  must *not* flip) and preserves alpha.\n- **Vertical UV offsets** — a drop shadow, a directional smear. The WebGL pooled\n  multi-effect path composites through bottom-up FBOs, so declare a `uUVYDir`\n  uniform (initialise it to `1.0`) and multiply vertical offsets by it; the\n  renderer feeds `-1` on that path and `+1` everywhere else.\n\n## A ShaderEffect on a mesh is a colour hook, and drops some builtins\n\n`mesh.addPostEffect(fx)` shades a `Mesh` on **both** backends since 20.5. The\nbody is spliced into the engine's own mesh shader as a colour hook, so the mesh\nkeeps its placement, alpha cutout, lighting and fog, and `setUniform` /\n`setTime` drive it exactly as they do over a sprite. Uniform-only bodies are\nthe supported case and they are most of them.\n\nFive things the quad tier gives an effect that the mesh tier does not, each\nrefused rather than half-working:\n\n| refused | why |\n|---|---|\n| its own `setTexture` samplers | resolved by the quad tier's per-effect device state; refused on both backends |\n| `screen_texture`, `screen_uv` | need the screen capture only the quad tier takes |\n| `noise_uv` | derived from a quad's image and object size |\n| `vColor`, on WGSL only | the WGSL mesh module has no module-scope equivalent; the GLSL one declares it, so GLSL is fine |\n| `InstancedMesh` | the instanced WGSL builder drops everything between the two stages |\n\nHitting one warns **once** and falls back to the built-in shading. Nothing\nthrows, so the symptom is a mesh drawing with its plain texture, which reads as\na broken shader, a bad binding or a texture that failed to load. Check the\nconsole for the warning before you start eliminating alpha, bindings and\ntransparency: it names which of the five it was.\n\n2D renderables never hit this. `Sprite`, `Text` and the rest go through the\nquad path, which has the full feature set, so a gradient fill for text or a\nrefraction over a sprite is fine.\n\nIf you need one of the five on a mesh, supply a full dual-language `GLShader`\nrather than a fragment body, and declare the fog uniforms yourself (see\n`melonjs-3d`).\n\n## Extra textures bind in setTexture order\n\n`setTexture(\"uFoo\", …)` assigns bindings **1 and 2** to the first extra\ntexture, **3 and 4** to the second, and so on. The WGSL must match:\n\n```wgsl\n@group(3) @binding(1) var uFoo : texture_2d<f32>;\n@group(3) @binding(2) var uFooSampler : sampler;\n```\n\nCopying a two-texture effect and deleting one leaves the survivor declared at\n3/4 while it is bound at 1/2 — the program fails to build and the effect is\nsilently dropped.\n\n## Blend modes\n\nFourteen modes; thirteen of them are honoured by all three renderers:\n\n```js\nsprite.blendMode = \"additive\";   // or \"multiply\", \"screen\", \"overlay\", …\n```\n\nSix ride fixed-function blend state and cost nothing extra: `normal`,\n`additive` (spelled `add` or `lighter` too), `multiply`, `screen`, `exclusion`,\nand `none` (blending off — the source replaces the destination, alpha included).\n`none` is the one mode the Canvas backend does not implement; it reports\n`\"normal\"` instead.\n\nThe other eight cannot be expressed as a multiply-add on the destination, so the\nGPU backends capture the destination and composite through `BlendEffect` — one\ncapture plus one composite **per draw**: `difference`, `overlay`, `hard-light`,\n`color-dodge`, `color-burn`, `soft-light`, `darken`, `lighten`. Right for\naccents; expensive for hundreds of objects. The Canvas renderer reaches all\neight through `globalCompositeOperation` instead, at no extra cost.\n\nAnything else collapses to `\"normal\"`, and so do 3D meshes and `Gradient` fills\nwhatever you ask for — the latter with a one-time console warning.\n`renderer.setBlendMode(mode)` returns the mode it actually applied, so comparing\nthat against your request detects the fallback.\n\n## The Canvas fallback\n\n`ShaderEffect` on a Canvas renderer logs a warning, leaves `enabled = false`, and\nturns every method into a no-op. Nothing throws. The same happens on a GPU\nbackend when no body matches its language — a GLSL-only effect under WebGPU.\n\nThat is fine for decoration. It is **not** fine when the shader draws your\ncontent — the carrier renderable then shows through raw. A 1×1 white texture\nstretched to 280×156 renders as a white box where the effect should be.\n\nGuard when the effect is load-bearing:\n\n```js\nif (app.renderer.shaderLanguage === null) {\n    // no programmable pipeline — skip the carrier draw entirely\n}\n```\n\n`shaderLanguage` (`\"glsl\"`, `\"wgsl\"`, or `null`) is the flag to test here rather\nthan a backend name or `supportsDepthBuffer`: it answers \"can this renderer\ncompile what I am about to hand it\". Per-effect, `effect.enabled` answers the\nsame question after construction.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| effect does nothing, warning in console | Canvas fallback — no programmable pipeline |\n| effect does nothing on some machines only | GLSL-only shader, `video.AUTO` chose WebGPU |\n| a white or solid box where the effect should be | carrier renderable drawn while the effect is disabled |\n| ONE effect does nothing but two of them work | the renderable draws with primitives — set `postEffectNeedsCapture` |\n| effect cannot be re-enabled | `removePostEffect()` destroyed it — use `enabled` |\n| ported shader renders upside down | a hand-bound `toFrameTexture()` capture — GL is bottom-up, WebGPU top-down |\n| shadow/smear offset flips on some draws | vertical UV offset not multiplied by `uUVYDir` |\n| frame rate collapses with many blended sprites | an *advanced* mode (overlay, darken, …) — each draw is a capture plus a composite |\n| animated shader never moves | `setTime` fed milliseconds, or the shader declares no `uTime` |\n| squash and stretch only bulges, the height never changes, under Matter | the adapter rewrites `currentTransform` every step; set the scale from the sprite's `update()` |\n| `SquishEffect` squishes the whole screen instead of the sprite | it is chained with another effect, or on the camera; give the sprite it alone |\n| syntax error in a file whose shader you just edited, pointing at a line of GLSL | a backtick in the shader body ended the template literal — most often one wrapped around an identifier in a comment |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-renderables` — where post effects attach, and the destroy trap\n- `melonjs-3d` — the GPU-backend requirement, and why a custom mesh shader is\n  not affected by the camera's distance fog\n","frontmatter":{"name":"melonjs-effects-and-shaders","description":"Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: Tween, easing, animate an effect, ShaderEffect, addPostEffect, removePostEffect, getPostEffect, postEffectNeedsCapture, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter.","license":"MIT"},"isInternal":false,"tokens":8676,"sizeBytes":34925},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-events/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/SKILL.md","title":"melonjs-events","category":"anthropic-skill","format":"markdown","content":"# melonjs-events\n\nPath: `packages/melonjs/skills/melonjs-events/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/SKILL.md)","tokens":300,"sizeBytes":207},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-getting-started/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-getting-started/SKILL.md","title":"melonjs-getting-started","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-getting-started\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Getting started with melonJS\n\n> Bootstrapping a melonJS 20.x game: the `Application` lifecycle, the mandatory\n> `await app.init()`, the world container, and what the renderer fallback means\n> for feature availability.\n\n**Applies to:** melonJS 20.x. Older major versions differ — see the\n`melonjs-20-migration` skill.\n\n## The bootstrap\n\n`Application` construction is synchronous but **initialisation is not**. `init()`\nreturns a promise and must be awaited before building any renderable, loading\nassets, or entering a scene.\n\n```js\nimport { Application, Text } from \"melonjs\";\n\nconst app = new Application(1218, 562, {\n    parent: \"screen\",      // id of the DOM element to mount into\n    scale: \"auto\",\n    backgroundColor: \"#202020\",\n});\nawait app.init();          // ← REQUIRED. Nothing below works without it.\n\napp.world.addChild(\n    new Text(609, 281, {\n        font: \"Arial\",\n        size: 160,\n        fillStyle: \"#FFFFFF\",\n        textAlign: \"center\",\n        textBaseline: \"middle\",\n        text: \"Hello World !\",\n    }),\n);\n```\n\n**This is the single most common thing to get wrong.** On 19.x the constructor\ncalled `init(width, height, options)` for you; on 20.x `init()` takes no\narguments and calling — and awaiting — it is your job. Nothing that follows\nnames the missing call. The constructor does build `app.world`, so adding a\nplain renderable succeeds and simply never draws — no canvas is appended and the\napp never subscribes to the frame tick — while constructing a `Sprite` or a\n`Text` throws `Cannot read properties of undefined (reading 'renderer')`,\nbecause the global `game` those two fall back on is only set once the first\n`init()` resolves. Either way `app.renderer` and `app.viewport` stay `undefined`\nand the page stays blank.\n\n`init()` is asynchronous because acquiring a WebGPU device is asynchronous. There\nis no synchronous alternative.\n\n## Top-level await\n\nThe bootstrap needs `await` at module scope. In an ES module that is fine. If the\nsurrounding tooling does not allow it, wrap it:\n\n```js\nasync function main() {\n    const app = new Application(800, 600, { parent: \"screen\" });\n    await app.init();\n    // …\n}\nvoid main();\n```\n\n## Choosing a renderer\n\n`video.AUTO` (the default) tries **WebGPU → WebGL 2 → Canvas** and uses the first\nthat works.\n\n```js\nimport { video } from \"melonjs\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    renderer: video.AUTO,      // default; or video.WEBGPU / video.WEBGL / video.CANVAS\n});\n```\n\nTwo consequences worth knowing before writing rendering code:\n\n- **Which backend you get is a runtime fact, not a build-time one.** The same\n  code runs on a different renderer depending on the machine. Do not assume\n  WebGL.\n- **Not every feature exists on every backend.** Shader effects and post effects\n  need a programmable pipeline and are inert on Canvas. Where a feature degrades,\n  the engine warns once rather than throwing — so a missing effect is a console\n  warning, not a crash.\n\nPin the renderer explicitly (`video.WEBGL`) only when you have a reason to. It\ncosts you the WebGPU path and the Canvas fallback.\n\n## The scene graph\n\n`app.world` is the root `Container`. Everything drawn is a descendant of it.\n\n```js\napp.world.addChild(sprite, 10);   // 10 is the z / draw order\n```\n\n**Pass the z-order as the second argument to `addChild`** — it is the only form\nthat works. `renderable.z` does not exist as a property, setting `depth` before\n`addChild` is overwritten by `autoDepth`, and setting it afterwards does not\nresort. See the `melonjs-renderables` skill for the full explanation.\n\n## Scenes\n\nFor anything beyond a single screen, use `Stage` subclasses and the `state`\nmanager rather than mutating `app.world` directly:\n\n```js\nimport { Stage, state } from \"melonjs\";\n\nclass PlayScreen extends Stage {\n    onResetEvent() { /* build the scene */ }\n    onDestroyEvent() { /* tear it down */ }\n}\n\nstate.set(state.PLAY, new PlayScreen());\nstate.change(state.PLAY);\n```\n\n`onResetEvent` runs every time the state is entered, not once — build scene\ncontent there, not in the constructor.\n\n## Loading assets\n\nAssets are declared and preloaded through `loader`; audio needs `audio.init()`\nwith the formats you ship before any audio asset is preloaded.\n\n```js\nimport { audio, loader } from \"melonjs\";\n\nloader.setOptions({ crossOrigin: \"anonymous\" });   // needed for remote assets\naudio.init(\"mp3,ogg\");                             // BEFORE preloading audio\nawait loader.preload([\n    { name: \"tileset\", type: \"image\", src: \"data/img/tileset.png\" },\n    { name: \"jump\",    type: \"audio\", src: \"data/sfx/\" },   // directory, not file\n]);\n```\n\nTwo ordering rules that produce confusing failures when missed:\n\n- **`audio.init()` must run before any audio asset is preloaded**, and names the\n  formats you actually ship. Preloading first throws *\"target audio extension(s)\n  should be set through me.audio.init() before calling the preloader\"*.\n- The audio `src` is a **directory**, not a file. The loader appends the asset\n  `name` plus each configured extension, so a full filename gives you a 404.\n\n## `anchorPoint` defaults to the centre\n\n`Renderable.anchorPoint` is `(0.5, 0.5)`, so `pos` is an object's **centre**, not\nits top-left. `Container` forces `(0, 0)` instead.\n\nWriting top-left math against a centred anchor puts everything half a sprite off\nwith no error — it is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Common mistakes\n\n| symptom | cause |\n|---|---|\n| blank page, nothing drawn, or a `TypeError` reading `renderer` from `new Sprite` / `new Text` | missing `await app.init()` |\n| objects draw in the wrong order | `.z`/`depth` set instead of `addChild(child, z)` — `.z` is not even a real property |\n| shader/post effect does nothing, warning in console | running on the Canvas renderer |\n| audio 404s | audio asset `src` given as a file rather than a directory |\n| scene content appears once and never resets | scene built in the constructor instead of `onResetEvent` |\n| everything offset by half a sprite | top-left math against the centred `anchorPoint` default |\n| a `Tween` or `ParticleEmitter` does nothing | `.start()` / `streamParticles()` never called |\n| cross-origin textures fail to load | missing `loader.setOptions({ crossOrigin: \"anonymous\" })` |\n\n## Related skills\n\n- `melonjs-20-migration` — what changed in 20.x that older code and older\n  training data get wrong\n- `melonjs-renderables` — custom `Renderable` subclasses, pointer events, post\n  effects and the traps in each\n","frontmatter":{"name":"melonjs-getting-started","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.","license":"MIT"},"isInternal":false,"tokens":1724,"sizeBytes":7035},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-input/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-input/SKILL.md","title":"melonjs-input","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-input\ndescription: \"Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click.\"\nlicense: MIT\n---\n\n# Input: keyboard, pointer and gamepad\n\n## Keyboard goes through named actions\n\nDo not poll key codes. Bind a key to an **action string**, then poll the action —\nthis is what lets gamepad and virtual controls map onto the same handling.\n\n```js\nimport { input } from \"melonjs\";\n\ninput.bindKey(input.KEY.LEFT,  \"left\");\ninput.bindKey(input.KEY.RIGHT, \"right\");\ninput.bindKey(input.KEY.X,     \"jump\", true);   // true = locked: one press per keydown\n\n// in update()\nif (input.isKeyPressed(\"left\"))  { /* … */ }\nif (input.isKeyPressed(\"jump\"))  { /* fires once per press, not per frame */ }\n```\n\nThe third argument to `bindKey` is `lock` (default `false`). Without it,\n`isKeyPressed` stays true for as long as the key is held — correct for movement,\nwrong for jump or fire. A locked action stays consumed until the key is released\nor you call `input.unlockKey(\"jump\")`. Use `input.keyStatus(action)` when you\nwant the raw held/not-held state regardless of locking.\n\n### A poll can miss a press entirely\n\n`isKeyPressed` is sampled once per frame, and `keyup` decrements the key's\ncounter the moment it arrives — locked or not. A press that goes down and up\nbetween two frames therefore leaves the counter back at zero and is never\nseen. At 60fps that is anything under ~16ms: rare from a human finger, routine\nfrom a synthetic event, and more likely the slower the frame rate.\n\nFor a screen where a missed press is a dead end — a splash, a \"press any key\",\na notice to acknowledge — listen for the event instead, which cannot be missed:\n\n```js\nimport { event } from \"melonjs\";\n\n// action is the bound action name, or undefined for an unbound key\nconst onKey = (action) => { if (action === \"menuSelect\") continue(); };\nevent.on(event.KEYDOWN, onKey);      // in onResetEvent\nevent.off(event.KEYDOWN, onKey);     // in onDestroyEvent — it does not self-remove\n```\n\n`KEYDOWN` fires for *every* key, bound or not, so matching on `action` is what\nnarrows it; it also repeats while the key is held, so guard anything that must\nhappen once. Keep polling for movement and for a menu cursor, where one step\nper press and a key held across a state change are the things that matter.\n\nThe full signature is `bindKey(keyCode, action, lock?, preventDefault?)`;\n`preventDefault` defaults to `input.preventDefault` (itself `true`). Pass the\nfourth argument per binding, or change the global default with\n`input.setPreventDefault(false)` — **assigning `input.preventDefault = false`\ndoes not work**, it is a read-only module binding.\n\n## Pointer events need `isKinematic = false`\n\n**This is the trap.** `Renderable.isKinematic` defaults to `true`, and both the\nbroadphase and the pointer dispatcher skip any candidate whose `isKinematic` is\nstill `true`. The handler registers successfully and simply never fires.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.isKinematic = false;               // ← required\n    }\n    onActivateEvent() {\n        input.registerPointerEvent(\"pointerdown\", this, this.onDown.bind(this));\n    }\n    onDeactivateEvent() {\n        input.releasePointerEvent(\"pointerdown\", this);\n    }\n}\n```\n\n`Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*` elements set it\nto `false` for you, and so does attaching a physics body (`Body`'s constructor\nsets `ancestor.isKinematic = false`) — so plain `Renderable` and `Sprite`\nsubclasses with no body are the ones that catch people.\n\nRegister in `onActivateEvent` and release in `onDeactivateEvent`, not in the\nconstructor — pooled objects are reused and would otherwise accumulate handlers.\n\n`app.viewport` works as a whole-screen region when you want global clicks. Note\nit is asked **before** everything else, not as a fallback after nothing matched:\nthe dispatcher appends it to the candidate list and walks that list from the\nend. So a viewport handler must not return `false` unless it really means to\nswallow every pointer event in the game.\n\nThe accepted event names are exactly `\"pointerdown\"`, `\"pointerup\"`,\n`\"pointermove\"`, `\"pointercancel\"`, `\"pointerenter\"`, `\"pointerover\"`,\n`\"pointerleave\"` and `\"wheel\"`. Anything else — `\"click\"`, `\"mousedown\"`,\n`\"touchstart\"` — makes both `registerPointerEvent` and `releasePointerEvent`\nthrow `invalid event type`. melonJS maps the canonical name onto whichever\nmouse/touch events the device actually supports, so you always register the\npointer name.\n\n## Which object gets the event\n\nThe dispatcher collects the regions near the pointer from the broadphase,\nbounds-checks each one, orders them **the way they are drawn** and asks the\ntopmost first, walking down until something consumes.\n\nA handler consumes by returning `false`. This is the DOM sense and it reads\nbackwards: `false` means handled, and anything else, `undefined` included, lets\nthe walk carry on to whatever is underneath.\n\n```js\ninput.registerPointerEvent(\"pointerdown\", this, () => {\n    this.fire();\n    return false;        // nothing under this gets the click\n});\n```\n\nThree things decide the order, and only the first is obvious:\n\n- **`pos.z`, among siblings of one container.** Higher is on top.\n- **Across containers, the common ancestor decides.** `pos.z` is\n  container-local, since `autoDepth` numbers each container's children from 1,\n  so a button at `z = 8` inside one panel does not outrank a second panel at\n  `z = 2`. The pair is resolved by walking both up to their shared parent and\n  comparing the two siblings there, which is exactly how drawing resolves it.\n  Raise a whole panel with `ancestor.moveToTop(panel)`, not by inflating a\n  child's `z`.\n- **A child is asked before the container holding it**, since it draws over it.\n\nEqual `z` between siblings falls back to child order, lower index on top. Two\ncallbacks registered on the **same** region for the same event run\nlast-registered-first.\n\nConsuming a **move** also takes the pointer away from the regions below: each\none still holding it gets its `pointerleave`, because it is covered now and\nbeing inside its own bounds no longer decides. Consuming a press, a release or a\nwheel does not, since none of those says where the pointer is.\n\n## World coordinates versus screen coordinates\n\nThe pointer carries several, and picking the wrong one produces a feedback loop:\n\n| property | meaning | use for |\n|---|---|---|\n| `pointer.gameScreenX` / `gameScreenY` | canvas-relative position | anything that **moves the camera** |\n| `pointer.gameWorldX` / `gameWorldY` | projected into world space | picking and spawning objects |\n| `pointer.gameLocalX` / `gameLocalY` | relative to the region's ancestor container | hit-testing inside a container |\n| `pointer.gameX` / `gameY` | **context-dependent** — see below | region handlers on non-floating regions |\n\n`gameX`/`gameY` is not a fixed coordinate space. The dispatcher rewrites it per\ncandidate just before calling your handler: world coordinates for a normal\nregion, screen coordinates for a `isFloating === true` region. On the global\n`event.POINTERMOVE` broadcast it is set to the **screen** coordinates. When you\nneed one specific space, read `gameWorldX`/`gameWorldY` or\n`gameScreenX`/`gameScreenY` by name rather than relying on `gameX`.\n\nUsing world coordinates for a camera-orbit drag means the same pixel maps to a\ndifferent world point every frame as the camera moves — the drag jumps wildly.\n\n## Pointer move and release outside a region\n\nRegion handlers only fire while the pointer is over the region. For drag\ntracking, listen globally:\n\n```js\nimport { event } from \"melonjs\";\nevent.on(event.POINTERMOVE, (pointer) => { /* … */ });\n```\n\nThere is **no global pointer-up system event** — `event.ts` publishes only\n`POINTERMOVE`, `POINTERLOCKCHANGE`, `DRAGSTART` and `DRAGEND`. For \"released\nanywhere\" you need a raw listener:\n\n```js\nglobalThis.addEventListener(\"pointerup\", onRelease);\n```\n\nFor plain drag-and-drop, `Draggable` / `DropTarget` already wire all of this up\n(`pointerdown` / `pointerup` / `pointercancel` regions plus a global\n`POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`. `DRAGEND`\nstill originates from a region handler, so it only fires when the release lands\non the draggable itself.\n\n## Gamepad\n\nGamepad input maps onto the same action strings, so game code does not branch:\n\n```js\ninput.bindGamepad(0,\n    { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 },\n    input.KEY.X);        // → the \"jump\" action bound above\n```\n\n**Order matters:** `bindGamepad` throws `no action defined for keycode N` if the\nkeycode has not already been passed to `bindKey`.\n\nAxes use the same call with a threshold, and bind **one half of the axis each**\n— the sign selects which half:\n\n```js\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold: -0.15 }, input.KEY.LEFT);\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold:  0.15 }, input.KEY.RIGHT);\n```\n\n**`threshold` is added to the deadzone, it does not replace it.** The test is\n`abs(value) >= deadzone + abs(threshold)`, so with the default deadzone of 0.1\na threshold of `0.5` does not fire until 0.6 of full travel, and `0.15` fires at\n0.25. Pick the number you want the stick to break at and subtract the deadzone.\nRaising `setGamepadDeadzone` moves every axis binding with it.\n\n`unbindGamepad(index, button, type?)`, `setGamepadDeadzone(value)` and\n`setGamepadMapping(id, mapping)` complete the surface.\n\n### Which index to bind\n\nBindings are per pad **index**, and the index is plug order, not identity —\nnothing reports which pad is \"the\" controller. Slot 0 is a guess: a wheel, a\nspare pad or a keyboard-shaped device left plugged in can hold it for a whole\nsession while the player presses buttons on slot 1.\n\nSo a one-player game binds **every slot it will accept** to the same actions,\nand lets `activeGamepad()` say which one is in use when something has to go\nback the other way:\n\n```js\nfor (let pad = 0; pad < 4; pad++) {\n    input.bindGamepad(pad, { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 }, input.KEY.X);\n}\ninput.activeGamepad();   // the last pad to produce a bound input, else the\n                         // first connected one, else -1\n```\n\nBinding spare slots costs nothing: the poll skips a bound index with no pad in\nit. `activeGamepad()` follows **bound inputs only**, which is what makes it an\nanswer about the player rather than about electrical noise — a worn stick at\nrest reports movement for ever and would otherwise claim to be the active pad.\n\n### Rumble\n\n```js\ninput.vibrateGamepad(input.activeGamepad(), {\n    duration: 120, strongMagnitude: 0.6, weakMagnitude: 0.9,\n});\ninput.stopGamepadVibration(index);   // cut a long effect short\ninput.hasGamepadVibration(index);    // for an options-screen toggle\n```\n\nSafe to call blind: no pad, no motors, or no Gamepad API at all resolves\n`false` and does nothing, so game code never branches on support. Magnitudes\ndefault to **full** — the underlying web API defaults every parameter to zero,\nwhich makes a bare `playEffect` a silent no-op. A second call preempts the\nfirst on the same pad, so a rapid series of hits reads as a rapid series of\nhits rather than as a queue. For a SUSTAINED rumble issue one effect with a\nlong `duration` instead of calling every frame.\n\nHot-plugging needs no polling of your own — the engine listens for the browser's\nown events and republishes them:\n\n```js\nimport { event } from \"melonjs\";\n\nevent.on(event.GAMEPAD_CONNECTED,    (gamepad) => { /* gamepad.id, .index */ });\nevent.on(event.GAMEPAD_DISCONNECTED, (gamepad) => { /* pause, show a prompt */ });\n```\n\nBindings survive a disconnect: they are attached to the index, not to the\ndevice, so a pad unplugged and plugged back into the same slot keeps working.\n\n## Virtual controls\n\nOn-screen buttons synthesise key events, again reaching the same actions:\n\n```js\ninput.triggerKeyEvent(input.KEY.LEFT, true);    // press\ninput.triggerKeyEvent(input.KEY.LEFT, false);   // release\n```\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| pointer handler never fires | `isKinematic` left at its `true` default |\n| handlers fire twice, or on dead objects | registered in the constructor instead of `onActivateEvent` |\n| camera drag jumps around | using world coordinates instead of `gameScreenX`/`gameScreenY` |\n| `gameX` holds screen coords where world coords were expected | read on `event.POINTERMOVE`, or on a floating region — use `gameWorldX`/`gameWorldY` |\n| drag stops when leaving the object | region events end at the region — use `event.POINTERMOVE` |\n| release never detected outside the region | no global pointer-up event — add a raw listener |\n| jump fires every frame while held | `bindKey` without its third `lock` argument |\n| `invalid event type` thrown at registration | non-pointer name (`\"click\"`, `\"mousedown\"`, …) passed to `registerPointerEvent` |\n| `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |\n| stick has to be pushed well past its `threshold` before it registers | `threshold` is ADDED to the deadzone, not used instead of it |\n| one half of a stick does nothing | each half is a separate `bindGamepad`, chosen by the sign of `threshold` |\n| a widget under an overlapping panel still reacts | the panel consumes nothing — return `false` from its `onClick` and `onOver` |\n| a widget stays highlighted after the pointer moves onto a panel over it | the panel consumes no `pointermove`, so nothing takes the pointer off the widget |\n| a child's big `z` does not put it in front of another container | `pos.z` is container-local — use `ancestor.moveToTop()` on the container |\n| every click in the game stops working | a handler on `app.viewport` returning `false`, which is asked first |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic` also gates the physics broadphase\n- `melonjs-ui-and-text` — `UISpriteElement` and `UITextButton` handle this for you\n","frontmatter":{"name":"melonjs-input","description":"Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click.","license":"MIT"},"isInternal":false,"tokens":3618,"sizeBytes":14755},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-lighting/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/SKILL.md","title":"melonjs-lighting","category":"anthropic-skill","format":"markdown","content":"# melonjs-lighting\n\nPath: `packages/melonjs/skills/melonjs-lighting/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/SKILL.md)","tokens":300,"sizeBytes":213},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-loading-assets/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/SKILL.md","title":"melonjs-loading-assets","category":"anthropic-skill","format":"markdown","content":"# melonjs-loading-assets\n\nPath: `packages/melonjs/skills/melonjs-loading-assets/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/SKILL.md)","tokens":300,"sizeBytes":231},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-math/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/SKILL.md","title":"melonjs-math","category":"anthropic-skill","format":"markdown","content":"# melonjs-math\n\nPath: `packages/melonjs/skills/melonjs-math/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/SKILL.md)","tokens":300,"sizeBytes":201},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md","title":"melonjs-particles-and-trails","category":"anthropic-skill","format":"markdown","content":"# melonjs-particles-and-trails\n\nPath: `packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md)","tokens":300,"sizeBytes":249},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-performance/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/SKILL.md","title":"melonjs-performance","category":"anthropic-skill","format":"markdown","content":"# melonjs-performance\n\nPath: `packages/melonjs/skills/melonjs-performance/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/SKILL.md)","tokens":300,"sizeBytes":222},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-physics/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-physics/SKILL.md","title":"melonjs-physics","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-physics\ndescription: \"Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT.\"\nlicense: MIT\n---\n\n# Physics and collision\n\n> melonJS has a built-in SAT collision world and two optional rigid-body\n> adapters. Movement, collision callbacks and spatial queries all behave\n> differently depending on which is active — and several of the differences fail\n> silently.\n\n## Choosing a world\n\n| | when |\n|---|---|\n| **built-in** (default) | Arcade-style: platformers, top-down games. Fast, position-based, not Newtonian. |\n| **`@melonjs/planck-adapter`** | Full rigid-body dynamics — stacking, joints, realistic restitution. |\n| **`@melonjs/matter-adapter`** | Same class of thing, backed by matter-js. |\n\n### Picking one\n\nStart with the built-in. It is the default for a reason: no dependency, the\nsmallest bundle, and its position-based model is what most 2D games actually\nwant — a platformer's jump arc is a designed curve, not a simulated one, and\ntuning gravity against a solver you are fighting is a bad afternoon.\n\n**Move to an adapter when the game needs something the builtin cannot express**,\nand these are the usual triggers:\n\n- **Rotation that collides.** The clearest signal. The builtin's SAT never reads\n  `body.angle`, so a spinning shape collides as though it never turned. See\n  the note in *Built-in world quirks* below.\n- **Torque, angular velocity, or spin from an impact.** Anything that should\n  start turning *because* it was hit.\n- **Joints and constraints** — ragdolls, chains, ropes, vehicles, hinged doors.\n  The builtin has no concept of them.\n- **Stacking and resting contacts.** Boxes that pile up and stay put need an\n  iterative solver. The builtin resolves each contact once per step with no\n  iteration pass, which splits three ways. A dynamic body resting on STATIC\n  geometry is exact, to the pixel, which is the platformer case it exists for.\n  A body squeezed between a dynamic body and static geometry is exact too: the\n  immovable side gets the casting vote and the other body absorbs the whole\n  correction, so a crate shoved against a wall stops at the wall. But a body\n  squeezed between two DYNAMIC bodies has no immovable side to defer to, so\n  those per-pair corrections still compete and it comes to rest slightly\n  overlapping. Three stacked 44px boxes rest flush on the floor with about 2px\n  of overlap inside the pile, and stay there: a stable equilibrium, not jitter\n  you can wait out or tune away.\n- **Believable restitution and friction**, where a ball's bounce height and roll\n  should follow from its material rather than from code you wrote per case.\n\n**Stay on the built-in when** the game is grid or tile based, movement is\nauthored rather than simulated, collision is \"did these two boxes touch\" plus a\npush-out, or you are shipping to a tight bundle budget. Tiled maps, `Trigger`\nand `Collectable` all work on every backend, so this is not a fork in the road\nfor level content.\n\n**planck or matter?** Both are real rigid-body engines and either will do; the\nadapters expose the same portable API, so a game can switch with an import\nchange and a gravity re-tune.\n\n- **planck** (a Box2D port) is the more accurate and the more predictable under\n  stress: better stacking, better joint behaviour, a continuous-collision\n  `bullet` mode for fast movers. It works in **metres**, so it has a\n  `pixelsPerMeter` scale to think about, and its tuning vocabulary is Box2D's.\n- **matter-js** works directly in **pixels**, which makes it the gentler\n  introduction, and its compound bodies and constraints are pleasant to author.\n  It is the softer solver: stacks settle less crisply and fast bodies need\n  `subSteps`.\n\nIf you have no preference and the game leans on stacking, joints or fast\nprojectiles, take planck. If you want to be up and running with the least\nconversion to think about, take matter.\n\nAn adapter is an `Application` setting, not a plugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },   // pixels/s², default {x: 0, y: 320}\n        pixelsPerMeter: 32,          // default 32\n        subSteps: 2,                 // default 1\n    }),\n});\nawait app.init();\n```\n\n`PlanckAdapter` also takes `velocityIterations` (default `8`) and\n`positionIterations` (default `3`). `MatterAdapter` takes a different set —\n`gravity` (default `{x: 0, y: 1}`, matter's own convention), `subSteps` and\n`matterEngineOptions`; it has no `pixelsPerMeter` because it already works in\npixels. `subSteps` is the anti-tunnelling knob when per-frame motion exceeds a\nbody radius. Pick **one** adapter per game; `physic: \"none\"` disables physics\nentirely — `world.step` skips the simulation and the world behaves as a pure\nscene graph.\n\n**Gravity is a different unit in every backend, and the defaults are NOT the\nsame speed.** Each is \"Earth-like\" in its own convention, but measured against\neach other by timing a 200px free fall in one scene:\n\n| backend | default | unit | measured fall |\n|---|---|---|---|\n| built-in | `0.98` | per FRAME, integrated against `timer.tick` | **~4450 px/s²** |\n| matter | `{x: 0, y: 1}` | matter's own, after its internal `gravity.scale` | **~900 px/s²** |\n| planck | `320` | pixels per second² | **~330 px/s²** |\n\nThat is a **thirteenfold spread**, so swapping the built-in world for an adapter\nand keeping everything else the same makes the whole game feel different. It is\nnot a bug in either one: they simply do not share a unit.\n\nTo match a backend you are porting FROM, pick a target in px/s² and convert:\nplanck takes it directly; matter takes `target / 900`; the built-in takes\n`target / 4450 * 0.98`, because it applies gravity per frame rather than per\nsecond. Passing one number to two of them compares nothing: hand planck\nmatter's `{x: 0, y: 1}` and it runs at a three-hundredth of Earth gravity.\n\nThat mistake is quiet and it does not look like a gravity problem. Bodies fall\nslowly enough that a spawner keeping a fixed interval starts constructing them\ninside each other, and **a body born overlapping another is outside what a\nsolver promises to fix**: the contact normals of an interlocked pair can cancel\nout, leaving the position solver with nowhere to push. The symptom is two\nshapes permanently fused at rest, which reads as a broken collision shape\nrather than as a bad gravity constant. Spawn clear of anything already there.\n\n`world.physic` carries the active adapter's label (`\"builtin\"`, `\"planck\"`,\n`\"matter\"`, `\"none\"`), so game code can branch without importing the class.\n\n## Two ways to declare a body\n\n**`bodyDef`** — declarative, portable, works on every adapter. Prefer it.\n\n```js\nclass Player extends Sprite {\n    constructor(x, y, settings) {\n        super(x, y, settings);\n        this.bodyDef = {\n            type: \"dynamic\",\n            shapes: [ /* … */ ],\n            collisionType: collision.types.PLAYER_OBJECT,\n            collisionMask: collision.types.WORLD_SHAPE | collision.types.ENEMY_OBJECT,\n            restitution: 0.2,\n        };\n    }\n}\n```\n\nThe engine forwards it to the active adapter when the renderable is added with\n`Container.addChild`, and only if that container is already attached to the root\nworld; the broadphase picks the renderable up on the next `world.update()`.\nCalling `adapter.addBody()` yourself registers the body but leaves the\nrenderable out of the scene graph, so it integrates but never collides.\n\n**`fixedRotation` follows the engine, so bodies rotate.** Both matter and\nplanck default it to `false`, and so do the adapters from matter-adapter 1.4.0\nand planck-adapter 1.5.0. Pass `fixedRotation: true` for anything that must\nstay upright, such as a platformer actor. Earlier adapter versions locked\nrotation unless told otherwise, which inverted the engine they wrap: bodies\nlanded holding whatever angle they were given and no collision could turn them,\nwhile `setAngle()` still worked, so a body could look rotated while being\nunable to rotate.\n\n`frictionAir` damps **rotation as well as translation** on both adapters, so\nit is also what settles a spin. Left at `0` a body that starts turning never\nstops, which only became visible once rotation stopped being locked.\n\nOther portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),\n`friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,\n`isSensor`, `userData`. Not every adapter honours every one — the built-in\nadapter ignores `friction`, `fixedRotation` and `userData`, and maps\n`frictionAir` onto its per-axis `body.friction` damping vector.\n\n**`new Body(this, shape)`** — the built-in-native API. Fine, but ties the entity\nto the built-in world.\n\n### Per-shape filtering and triggers\n\nA shape in `shapes` may carry its own `collisionType`, `collisionMask`,\n`isTrigger` (default `false`) and `isActive` (default `true`). A shape can only\n**narrow** what its body allows — the body's type/mask are checked first.\n\n```js\nconst feet = new Rect(0, 24, 32, 8);\nfeet.collisionMask = collision.types.WORLD_SHAPE;\nconst torso = new Rect(0, 0, 32, 24);\ntorso.collisionMask = collision.types.ENEMY_OBJECT;\ntorso.isTrigger = true;      // detects hits, never gets pushed by them\n```\n\n`isActive: false` removes the shape from the narrow phase *and* from raycasts\nwithout shrinking the body's bounds. Honoured by the built-in and planck\nadapters; the matter adapter filters per body and ignores them. Under planck the\nbody-wide setters (`setCollisionType` / `setCollisionMask` / `setSensor`) write\nevery fixture and so overwrite per-shape values.\n\nFor contacts reported per *shape pair* rather than per body pair, define\n`onShapeCollisionStart` / `onShapeCollisionActive` / `onShapeCollisionEnd`.\nDeclaring at least one of them opts into shape-pair enumeration; declaring none\ncosts nothing.\n\n### Shapes drawn in a physics-shape editor\n\nA visual collision-shape editor exports a JSON file holding a map of body name\nto the fixtures authored for it. Preload it like any other JSON, then name it\nfrom `bodyDef.shapes` with `id` naming the body to read:\n\n```js\n// in the preloader\n{ name: \"shapes\", type: \"json\", src: \"data/physics/shapes.json\" }\n\n// then, on the renderable\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\" };\n```\n\nThis works identically on the built-in, matter and planck adapters — the file is\nresolved into real shapes before any adapter is handed the definition. Circles\nand polygons both load, and a concave outline (exported already decomposed into\nconvex pieces) becomes one `Polygon` per piece.\n\nThe density, friction and bounce the shapes were authored with are carried onto\nthe body definition too, each honoured by whichever backends support it — the\nbuilt-in adapter ignores contact `friction`, as it does for a hand-written\ndefinition. The format carries these per fixture, but only planck could honour\nthat, so they reduce onto the body and a file whose fixtures disagree warns. An\nexplicit field on the definition always wins:\n\n```js\n// bounces at 0.9 whatever the file says\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\", restitution: 0.9 };\n```\n\nThe exporting tool's own filter block is **not** read, in any spelling. Its\ncategory and mask bits are numbers in that tool's namespace rather than\n{@link collision.types}, and a template writes its defaults for every fixture\nwhether or not you set anything — importing them would silently tag a shape as\nsomething it is not and stop it colliding. Set `collisionType` / `collisionMask`\non the body definition instead. A file written by hand may carry those two names\nper fixture, and `isSensor`, which mean the same thing here as there.\n\nAuthored shapes and imported ones can sit in the same list.\n\nTwo bodies must never share one `shapes` array: a body **owns** its shapes and\nrotating it mutates them in place. Naming the file sidesteps this — every body\nresolved from it mints its own shapes.\n\n`Body#fromJSON()` and passing an exported list to `addShape()` are the old,\nbuilt-in-only spelling of this and are deprecated since 20.7.0.\n\n### Which shapes each backend can actually simulate\n\n| shape | builtin | matter | planck |\n|---|---|---|---|\n| `Rect`, `Polygon` | exact | exact | exact, up to 12 vertices |\n| `RoundRect` | exact | exact (decomposed) | sampled down to 12 vertices |\n| `Ellipse` | exact | circle of the average radius | circle of the average radius |\n| `Line` | exact | thin quad along the segment | thin quad along the segment |\n| `Box3d`, `Sphere` | exact, and in 3D | **throws** | **throws** |\n| `Point` | supported | **throws** | **throws** |\n\nTwo of those are worth knowing before you author geometry. Box2D caps a\npolygon's vertex count and says nothing when you go over: measured, a\n16-point outline comes back with 12 vertices and a 36-point one collapses\nfrom 70x36 to 47x18. A `RoundRect` carries 36 points, four corners of nine\narc segments each, so it is the usual way to meet that cap; the adapter now\nsamples it down and keeps the extent. Anything the 2D backends cannot express\nis refused loudly on both, rather than leaving a body with no collision\ngeometry that silently never collides.\n\nWhatever a backend really simulates is what `getBodyShapes()` reports, so the\ndebug panel is the fastest way to see an approximation for what it is.\n\n### `Line` shapes, and why slopes need a rigid-body adapter\n\nA `Line` is two points and no area, and it is what Tiled emits for every\npolyline, so it is the natural way to author a slope or a strip of ground.\nNeither rigid-body engine has a segment primitive, so both simulate one as a\nthin quad following the segment (2px), which is what `getBodyShapes()` reports\nback. Before 20.7 each substituted something of its own instead: matter the\nline's bounding box, planck a one-metre square wherever the body happened to\nbe. Neither logged anything.\n\nThe builtin solver resolves a sloped segment too, and correctly: a body lands\non it and SLIDES down it, holding a constant distance from the surface while\nits x advances. What it has no notion of is SURFACE friction. `def.friction`\nis ignored entirely and `body.friction` is a per-step velocity damping vector\nfed from `def.frictionAir`, so nothing removes the tangential component of\ngravity. A body on an incline therefore accelerates down it forever and leaves\nat the end, and in a valley of segments it swings from arm to arm for a while\nbefore coming to rest on the flat. (It used to escape through an arm instead;\nthat was a separate defect in how a body was separated from a shape list, fixed\nin 20.7.)\n\nmatter and planck both have real surface friction, so the same valley holds a\nbody still.\n\n**Several shapes on one body.** A Tiled polyline arrives as ONE body carrying\none shape per segment, and until 20.7 a body resting on such a ground could\nsink through one of the segments and drop out. The narrowphase reports the\nFIRST overlapping shape pair it finds, and the compound resolution pass\nre-ran that same first-hit-wins scan, so it kept re-resolving the pair it had\nalready resolved while a neighbour was penetrated unmeasured. Every pair is\nenumerated now. If you are ever diagnosing something similar, the experiment\nthat localises it is to re-run the same geometry as SEVERAL single-shape\nbodies: if that holds and one multi-shape body does not, the fault is in the\ncompound path rather than in the shape type.\n\nDo not diagnose this by dropping a body and reading its final `y`. A body that\nslid off the end of a ramp and a body that fell straight through it both end up\nfar below, and they look identical in that one number. Track the body against\nthe surface height at its CURRENT x instead: riding the slope shows a constant\noffset while x advances.\n\nSo segments collide on every backend. If you need a body to come to REST on a\nsloped one, use matter or planck, or damp it yourself: the builtin will slide\nit down the incline indefinitely. The `Line Collision` example draws the\nauthored segment against the geometry each backend reports, which is the\nquickest way to see what a backend is really simulating.\n\n### `anchorPoint` moves the collision shapes with the drawing\n\n`anchorPoint` says where in its own bounds a renderable sits on its `pos`:\n`updateBounds()` translates its bounds by `-width * anchorPoint.x, -height *\nanchorPoint.y`, and `preDraw` shifts its rendering by the same amount. A\nbody's collision shapes are measured from that same frame, so a shape of\n`new Rect(0, 0, width, height)` covers the sprite whatever the anchor is, and\na shape's own `pos` still offsets it inside that frame (a small hitbox at the\nfeet, a hurtbox at the head).\n\n**That frame's origin is its TOP-LEFT corner, which is the trap for a small\nshape.** `new Rect(0, 0, w, h)` reads naturally because it spans the frame.\nA circle or a `Sphere` does not span anything, so `new Sphere(0, 0, 0, r)` on\na centre-anchored sprite sits on the sprite's corner, not on the sprite. Put\nit at half the frame:\n\n```js\n// centred on a 24x128 sprite whose anchorPoint is (0.5, 0.5)\nbolt.bodyDef = { type: \"dynamic\", shapes: [new Sphere(24 / 2, 128 / 2, 0, r)] };\n```\n\nMeasured on exactly that sprite: with the shape at `(0, 0, 0)`,\n`adapter.getBodyShapes()` reported it at `(-12, -64)` — **64 units up the\nscreen, four times its own radius** — so the projectile collided well away\nfrom where the player saw it, in a way that still looked like a working game\nbecause the targets were large.\n\nThe offset comes from the renderable's own `width` / `height`, NOT from what\nit draws at. Scale a sprite through `currentTransform` and those stay the\ntexture's size, so the half-frame term is half the texture, not half the\ndrawn quad. The debug overlay is the quickest confirmation: the red shape\nshould sit on the art.\n\nThis was broken until 20.7: shapes were measured from `pos` regardless of the\nanchor, so anything not anchored at its top-left corner collided where it was\nnot drawn, by half a body at the default centred anchor and by a full body\nheight at the bottom anchor a platformer actor uses. If you compensated by\noffsetting your shapes by hand, remove those offsets. `Entity` and Tiled\nobjects set their own anchor to `(0, 0)`, so nothing built either of those\nways is affected.\n\nThe debug panel is the quickest check: green is the renderable's bounds, red\nis what actually collides, and the two now agree.\n\nOne renderable does not take that offset at all: one that has cleared\n`applyAnchorTransform`, the flag `preDraw` reads before shifting anything.\n`GLTFModel` sets it `false` outright and `Mesh` clears it under a `Camera3d`,\nbecause both emit world coordinates and pivot about their own model origin\nrather than a bounds box, so they draw at `pos` whatever their `anchorPoint`\nstill holds. Their bodies are built there too, on every backend, and\n`raycast3d` reports hits in that same frame. If you clear the flag on a\nrenderable of your own, the same applies: you place it, and your shapes are\nmeasured from `pos`.\n\n### `autoTransform: false` opts out of rotated bounds\n\n`updateBounds()` only applies `currentTransform` when `autoTransform` is\n`true`. Turn it off and the renderable's bounds stay the UNROTATED frame\nwhile the sprite turns inside them, which quietly feeds a wrong box to\nfrustum culling and to the broadphase. Measured on a 60x20 box at 90\ndegrees: `20x60` with `autoTransform` on, `60x20` with it off.\n\nThat flag means \"I place myself\", so it is a fair contract, but reach for it\nonly when you really are drawing in your own frame, and remember the bounds\nare yours to keep honest then.\n\n## Move with forces, not by assigning position\n\n```js\n// ✗ the adapters overwrite this from the engine body every step\nthis.pos.x += this.speed;\n\n// ✓ portable — works on every adapter\nthis.body.applyForce(thrustX, 0);\nthis.body.setVelocity(vx, vy);\n\n// ✓ built-in only — `force` and `maxVel` are BuiltinAdapter Body fields\nthis.body.force.x = this.body.maxVel.x;\n```\n\n`maxVel` is what turns a force into a speed, so the two are set together. Use\nthe setters rather than writing the vectors — both take an optional THIRD\nargument for the depth axis, left unchanged when omitted, so a 2D call keeps\nits behaviour:\n\n```js\nthis.body.setMaxVelocity(3, 15);   // walk speed, jump speed\nthis.body.setFriction(0.4, 0);     // ground drag, none vertically\n```\n\n`friction` here is per-axis per-step velocity damping, not a surface\ncoefficient — see the built-in quirks below.\n\nUnder matter and planck, `syncFromPhysics()` copies the engine body's position\nback onto `renderable.pos` after every step, so a direct write is simply erased\n— no error. The built-in world integrates `pos` in place instead, so a write\nsurvives but fights collision resolution and jitters. To teleport, go through\n`adapter.setPosition(renderable, p)` so the engine body moves with it.\n\n## Collision callbacks — which one to use\n\nThe lifecycle is `onCollisionStart` (contact begins), `onCollisionActive` (every\nstep while it persists) and `onCollisionEnd` (contact breaks). None of them are\ndefined on `Renderable` — you just declare the ones you want and the dispatcher\nchecks `typeof`. That is also why they cannot be declared on the base class: a\nfield there would SHADOW the prototype method every subclass that implements\none defines.\n\n**In TypeScript the response is `CollisionResponse`**, exported from `melonjs`:\n\n```ts\nimport type { CollisionResponse, Renderable } from \"melonjs\";\n\nclass Coin extends Sprite {\n    onCollisionStart(response: CollisionResponse, other: Renderable) { … }\n}\n```\n\nOn a renderable you do not subclass, declare the hooks you want as OPTIONAL\nmembers of a local interface and assign through one plain cast — that is the\nopt-in, and it needs no `as unknown as`:\n\n```ts\ninterface Contacts {\n    onCollisionStart?: (response: CollisionResponse, other: Renderable) => void;\n}\n(rock as Contacts).onCollisionStart = (response, other) => { … };\n``` The legacy `onCollision` still exists and has a trap:\n\n> On the built-in world, `onCollision` fires **twice per frame** for\n> dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT\n> response with a fixed `a`/`b` per pair. On the adapters it is routed to their\n> \"still touching\" phase and fires once per side.\n\nThe legacy dedupe idiom is `if (this !== response.a) return;`. Defining\n`onCollisionActive` suppresses the legacy callback on that renderable (the\n\"supersedes\" rule, applied per side so `a` and `b` can migrate independently),\nand is deduped to once per pair per side per frame on every adapter.\n\n**Return values only work on the legacy handler.** `return false` from\n`onCollision` skips the built-in SAT push-out; what `onCollisionStart` /\n`onCollisionActive` return is discarded. To opt a body out of the physical\nresponse, set `bodyDef.isSensor` / `body.setSensor(true)`, or make the shape a\ntrigger. With no `onCollision` defined at all, push-out happens by default for\ndynamic non-sensor bodies.\n\nThe two handler families do **not** get the same response object:\n\n| | `onCollision` (legacy) | `onCollisionStart` / `Active` / `End` |\n|---|---|---|\n| `a` / `b` | fixed per pair | receiver-symmetric: `a === this`, `b === other` |\n| overlap magnitude | `response.overlap` | `response.depth` |\n| contact axis | `response.overlapN` / `overlapV` | `response.normal` |\n| also carries | `aInB`, `bInA`, `indexShapeA/B`, `isTriggerContact` | `pair` (adapter-native, absent on built-in) |\n\nOn the modern response `overlap` / `overlapN` / `overlapV` are still present but\n`@deprecated` (and `undefined` under the matter adapter) — use `depth` and\n`normal`. `normal` is the direction the receiver must move to separate, in\ncanvas coordinates: `normal.y < -0.7` means \"push me up\", i.e. I landed on top\nof `other`.\n\nOne portability wrinkle: the built-in adapter dispatches `onCollisionEnd` with\n`undefined` as the response; the matter and planck adapters pass a real one.\nGuard it.\n\n## Never mutate the world during contact dispatch\n\nCollision callbacks are dispatched from inside the physics step on every\nadapter, and each engine has its own tolerance for being mutated there. Flag the\nwork and drain it from your `Stage.update`:\n\n```js\nonCollisionStart(response, other) {\n    this.pendingRemoval = true;    // don't addChild / removeChildNow here\n}\n```\n\nWhat actually happens per adapter:\n\n- **built-in** — callbacks fire inline during `step()`. `Container.removeChild`\n  is deferred, so it is safe; `removeChildNow()`, and the internal `destroy()`\n  it calls, are immediate,\n  and the detector only survives them because it re-checks `body === undefined`\n  after every user callback.\n- **planck** — `onCollisionStart` / `onCollisionEnd` are dispatched from\n  planck's `begin-contact` / `end-contact` events, which fire *while the world\n  is locked*. Adding a renderable with a `bodyDef` there makes planck's\n  `createBody` return `null` and the adapter then throws on the null handle.\n  `onCollisionActive` is dispatched after the step and is the safe one.\n- **matter** — callbacks come from matter's `collisionStart` / `collisionActive`\n  / `collisionEnd` events during `Engine.update`.\n\n## Spatial queries\n\n`raycast` and `queryAABB` are implemented by all three adapters:\n\n```js\nimport { Rect, Vector2d } from \"melonjs\";\n\n// nearest hit only — returns null when nothing is hit\nconst hit = app.world.adapter.raycast(new Vector2d(x0, y0), new Vector2d(x1, y1));\n// hit → { renderable, point, normal, fraction }\n\n// takes a Rect, not a Bounds\nconst inArea = app.world.adapter.queryAABB(new Rect(x, y, w, h));\n```\n\n`querySphere(centre, radius)` (or `querySphere(sphere)`) is **built-in only**,\nand only active under a 3D broadphase — i.e. `world.sortOn === \"depth\"`, which\n`Camera3d` sets. So is `raycast3d`. Check\n`world.adapter.capabilities.raycasts3d` before calling either. `raycast` itself\nis capability-gated by `capabilities.raycasts`.\n\nThe legacy `collision.rayCast(line, result)` is a different API: it takes a\n`Line`, returns an **array** of every intersecting renderable, and only exists\non the built-in path.\n\nTwo prerequisites that produce empty results rather than errors:\n\n- **`isKinematic` must be `false`** for a renderable to be in the broadphase at\n  all (a `Body` sets this for you).\n- On the **built-in** adapter, the broadphase is cleared and rebuilt inside\n  `world.update()`, so querying before the first update returns nothing.\n\n## 3D collision: `Box3d`, `Sphere` and the Z pushback\n\nThe narrowphase is not 2D-only. Two shapes carry a depth extent — `Box3d` and\n`Sphere` — and any pair of them is resolved in three dimensions, pushing back\nalong **Z** as well as X and Y.\n\n```js\nimport { Box3d, Sphere } from \"melonjs\";\n\n// a box: centre offset from the body, then half-extents on each axis\ncrate.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));\n// a sphere: centre offset, then one radius\nrock.body.addShape(new Sphere(0, 0, 0, 30));\n```\n\nReach for these for anything moving in the XZ plane under a `Camera3d` — a\nrunner dodging obstacles, a 2.5D platformer, pickups on a course. Hand-rolling\na distance check there is the usual mistake, and it throws away the\npenetration vector the response already carries.\n\n**Which of the two.** A `Box3d` is world-axis-aligned and cannot be oriented:\nit is right for walls, floors, crates and anything else that stays square to\nthe world, and wrong for anything that turns. A `Sphere` has no orientation to\nget wrong, which makes it the shape for things that tumble, and for anything\nlaid out over a curved surface — on a globe every object's own \"up\" points a\ndifferent way, so no single axis-aligned box fits any of them. It is also the\ncheaper test: one distance against a sum of radii.\n\n**Z arrives beside `overlapN`, not inside it.** `overlapN` stays the 2D unit\nvector it always was and `overlapNZ` carries the depth component, because\n`Vector3d` does not extend `Vector2d` and widening the field would have broken\nevery existing 2D handler. The two together are a 3D unit vector, and\n`overlapZ = overlapNZ * overlap` exactly as `overlapV = overlapN * overlap`.\n\n**How much of the normal is in each half depends on the shapes.** A `Box3d`\npair separates along ONE world axis, so a depth contact leaves the 2D fields\nat zero and a legacy 2D handler correctly applies no push. A `Sphere`\nseparates along the line between the two centres, which is generally diagonal:\nall three components are non-zero at once. So subtract both halves rather than\nbranching on one:\n\n```js\nonCollision(response, other) {\n    // works for either shape: the 2D half is zero when there is no 2D push\n    this.pos.x -= response.overlapV.x;\n    this.pos.y -= response.overlapV.y;\n    this.pos.z -= response.overlapZ;\n    return false;\n}\n```\n\nBranching on `if (response.overlapNZ !== 0)` and then moving only `pos.z` is\nthe mistake to avoid: correct for a box pair, and for a sphere it throws away\nthe planar half of the contact.\n\n**Mixed pairs degrade to 2D.** A `Box3d` or a `Sphere` against a `Polygon`,\n`Rectangle`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and\ntreats the planar shape as unbounded along Z, which is what keeps a sphere or\nbox body colliding with an ordinary Tiled collision layer. `overlapZ` is zero\nfor those, since only one side has a depth to resolve. Give BOTH sides a 3D\nshape when you want a depth result.\n\nThis is the narrowphase and is independent of the 3D broadphase queries above\n— `querySphere` / `raycast3d` need `world.sortOn === \"depth\"`; a 3D body shape\ndoes not.\n\n`raycast3d` measures a `Box3d` body as its box and a body that is one `Sphere`\nas that sphere, both exactly. Anything else, including a body mixing the two,\nis measured as the AABB containing its 3D shapes, and a body with none falls\nback to a bounding sphere derived from the renderable's 2D bounds.\n\n**Bounds are what the broadphase sorts by, and they come from the SHAPE.**\nEvery item is filed by its renderable's 2D `getBounds()`, so an object with no\nextent cannot be placed and silently collides with nothing. A `Mesh` takes its\nextent from its geometry and a `GLTFModel` from its part meshes, so both report\nwhere they are without being told. A body's shapes do **not** contribute, so a\ncustom collidable built on a bare `Container` has no extent until you give it\none:\n\n```js\n// a container reports an EMPTY bounds until it has a size of its own\ngroup.resize(width, height);\n```\n\n`enableChildBoundsUpdate` also gives it one, but it re-measures the whole child\nlist on every update — reach for it only when the extent genuinely IS the\nmembers' union and moves with them (a flock, a squad), not to give a fixed\ngroup a size.\n\n## Built-in world quirks\n\nThese are specific to the default world and surprise people arriving from a\nreal rigid-body engine:\n\n- **Dynamic-dynamic collision is position-based, not Newtonian.** Separation is\n  mass-proportional but the velocity response is per-body cancellation — two\n  equal-mass bodies do not exchange momentum the way you would expect.\n- **An immovable contact wins the step.** Each step records, per body, which\n  directions it has already been pushed out of, and a later dynamic-dynamic\n  contact will not push it back in one of those directions: the other body\n  takes the whole correction instead. A light body shoving a heavy one against\n  a wall therefore stops dead at the wall rather than driving it through, and\n  the shover absorbs all of the separation. The record lasts one step and is\n  not contact state — it says nothing about what a body is touching.\n- **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate\n  `app.world.adapter.gravity` at runtime, or pass\n  `physic: new BuiltinAdapter({ gravity })` to override it. `body.gravityScale`\n  (default `1`) scales it per body; `0` disables gravity for that body and is\n  the portable replacement for the deprecated `body.ignoreGravity`.\n- **`applyForce(x, y)` is linear only** unless you pass the optional\n  `(pointX, pointY)` application point, which generates a torque\n  `τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.\n  `applyTorque(τ)` is the direct form.\n- **Force accumulators reset at end-of-step** — for every body, including\n  static, paused and off-screen ones — so forces must be applied every frame to\n  persist.\n- **`isGrounded` is flag-based, not contact-based** — the adapter returns\n  `!body.falling && !body.jumping`, flags updated by the last resolved\n  collision, not a live contact test.\n- **`def.density` maps 1:1 to `body.mass`** (default `1`), and `def.friction` is\n  **ignored** entirely. `body.friction` is a per-axis `Vector2d` of per-step\n  velocity damping fed from `def.frictionAir`, not a surface coefficient.\n- **`body.maxVel` defaults to `(490, 490)`** and hard-clamps velocity after\n  forces and friction. The canonical movement idiom is\n  `this.body.force.x = this.body.maxVel.x`.\n- **Default `collisionType` is `ENEMY_OBJECT`**, and default `collisionMask` is\n  `ALL_OBJECT` — set both explicitly.\n- **Integration is gated on `inViewport || alwaysUpdate`** — an off-screen body\n  stops simulating unless you set `alwaysUpdate = true`. It is also gated on\n  `updateWhenPaused` while the state manager is paused, and static bodies never\n  integrate.\n- **`addBody` throws on double-registration** for a renderable that is already\n  adapter-managed.\n\n### Rotation collides on planck and matter, but not on the built-in solver\n\nThis is the sharpest behaviour difference between the adapters, and it is\nsilent — nothing warns you.\n\n| adapter | a rotating body |\n| --- | --- |\n| **builtin** | rotates **visually only**. `body.angle`, `setAngle()` and angular integration write `renderable.currentTransform`; the collision shapes are never rotated, so a spinning sprite collides as if it were upright |\n| **planck** / **matter** | rotates for real — the solver owns the body's transform and collision follows it |\n\nSo a turret, a swinging platform or a tumbling crate that must *collide*\nrotated needs planck or matter. Under the builtin solver, approximate it with a\nshape that is close enough at every angle (a circle, or a square for\n90° steps), or rotate the shapes yourself with `body.rotate(angle)` — which\ndoes rotate them, unlike `setAngle()`.\n\n`body.rotate()` applies a **delta** and does not update `body.angle`; the two\nare unlinked, so a body driven by `rotate()` still reports an angle of `0`.\nTrack it yourself if you need to read it back. Before 20.7 it also threw on a\n`Box3d` or `Point` shape, and did not tell its owner that the body's bounds had\ngrown.\n\n`adapter.getBodyShapes(renderable)` reports whichever is true: the geometry\nthe engine is actually simulating on planck and matter (matter-adapter 1.3.0 /\nplanck-adapter 1.4.0 and later), unrotated shapes on the builtin solver because\nthere they genuinely are. That is what the debug overlay draws, so the hitbox\nyou see is the hitbox that collides — a sprite spinning inside a stationary red\nbox on the builtin is the solver telling you the truth, not a drawing bug.\n\nOn planck and matter that report comes from the engine rather than from your\ndefinitions, so it shows the approximations they make: an `Ellipse` appears as\nthe circle of average radius it is simulated as, a concave polygon as the\nconvex pieces it was decomposed into, and a `Rect` as a `Polygon`. Your\nauthored shapes are untouched and still readable on `renderable.bodyDef.shapes`.\n\n**If you need rotation to affect collision, use planck or matter.** That is the\ndividing line: the builtin is a position-based solver whose SAT never reads\n`body.angle`, so rotation there is a visual property. `body.rotate(angle)` is\nthe workaround, not a rotating body — it permanently moves the shapes, does not\ntrack an angle, and gives you no angular velocity, torque or rotational\nresponse from a collision. A game whose collision genuinely turns (a spinning\nhazard, a swinging bridge, a car, anything with torque) wants a real rigid-body\nadapter rather than a workaround.\n\n## Porting between adapters\n\nSame call, different magnitude — these are the ones that waste an afternoon:\n\n- **The portable API converts for you, the native handle does not.**\n  `adapter.setVelocity` / `body.setVelocity(x, y)` are in pixels on every\n  adapter; reaching for the raw handle (`body.setLinearVelocity` on planck) puts\n  you in metres per second, scaled by `pixelsPerMeter`.\n- **Time base differs.** Built-in `body.vel` is pixels *per frame*; planck and\n  matter integrate in seconds. The same number is ~60× off.\n- **Force magnitudes are not comparable** between adapters; re-tune rather than\n  reuse. `applyForce` is a per-step accumulator everywhere (cleared each step,\n  so call it every frame) — `applyImpulse` is the one-shot. Matter has no native\n  impulse, so the adapter emulates it as `Δv = J / mass` and ignores the\n  application point.\n- **`body.position` is the centroid in matter**, while `renderable.pos` is\n  top-left in melonJS. The adapter stores a per-body offset so `renderable.pos`\n  stays correct — but read `body.position` directly and you get the centroid.\n- **Degenerate (zero-area / collinear) polygons — a Tiled polyline, a `Line` —\n  cannot be built by matter and fall back to their axis-aligned bounding box.**\n  Any other shape type throws `unsupported shape type`.\n- **Ellipses become circles on matter**, using the average of the two radii.\n- **`isGrounded` is capability-gated** (`capabilities.isGrounded`) and computed\n  differently per adapter: a flag read on built-in, a live scan of active\n  contacts on matter.\n\nAdapter-specific escapes exist (`(this.body as MatterAdapter.Body).frictionAir`,\n`adapter.matter.Constraint.create(...)`) but are not portable — flag them if you\nuse them. `adapter.capabilities` (`constraints`,\n`continuousCollisionDetection`, `sleepingBodies`, `raycasts`, `raycasts3d`,\n`velocityLimit`, `isGrounded`) is the portable way to branch.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| entity jitters or does not move | assigning `pos` instead of applying force |\n| collision handler runs twice per frame | legacy `onCollision` on a dynamic pair (built-in) — use `onCollisionActive` |\n| `return false` from a handler does nothing | return values are only honoured by legacy `onCollision` — use `isSensor` / `isTrigger` |\n| crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |\n| `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |\n| `querySphere` / `raycast3d` is not a function | 2D adapter, or built-in without a 3D broadphase (`sortOn !== \"depth\"`) |\n| a 3D contact reports no overlap to push against | read `overlapNZ` / `overlapZ` — a Z-axis MTV leaves `overlapN` / `overlapV` at zero |\n| two 3D objects never separate in depth | one of them carries a planar shape; a mixed pair is tested as the box's XY footprint, unbounded along Z |\n| `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |\n| `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |\n| off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |\n| a body sinks through one shape of a multi-shape body | fixed in 20.7; before that the compound pass only ever re-resolved the first overlapping pair |\n| a body will not stay put on a slope on the builtin | it is riding the slope and sliding: the builtin has no surface friction, only `frictionAir` damping. Damp it yourself, or use matter/planck |\n| the sprite is drawn offset from its hitbox | pre-20.7 `anchorPoint` moved the drawing but not the collision shapes; upgrade, and drop any offsets you added to compensate |\n| a small shape sits at the sprite's corner | a shape's offset is measured from the drawn frame's TOP-LEFT; `new Sphere(0, 0, 0, r)` is the corner, `(width / 2, height / 2)` is the centre |\n| a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |\n| a body in a pile sinks into the floor (built-in) | fixed in 20.7 for anything with an immovable side; bodies pinned only by other DYNAMIC bodies still overlap by a pixel or two, which is what planck/matter are for |\n| forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |\n| `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, melonJS the top-left — the adapter offsets between them |\n| `bodyDef.shapes: \"…\"` throws \"is not loaded\" | the name must match the `name` the JSON was preloaded under, not its filename |\n| everything falls far too fast or too slow after swapping backends | gravity is a different unit per backend; see the table above |\n| debris lands holding its spawn angle and never tumbles | `fixedRotation: true`, or matter-adapter < 1.4.0 / planck-adapter < 1.5.0 |\n| a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use planck/matter, or `body.rotate(angle)` |\n| the debug hitbox does not follow a spinning body | pre-1.2.1 matter-adapter / pre-1.3.1 planck-adapter returned the authored shapes rather than the rotated ones |\n| a Tiled polyline becomes a solid box on matter | matter cannot build a zero-area polygon; the adapter falls back to its AABB |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic`, which also gates the broadphase\n- `melonjs-tilemaps` — collision shapes authored in Tiled\n- `melonjs-loading-assets` — preloading a shape editor's export as JSON\n","frontmatter":{"name":"melonjs-physics","description":"Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT.","license":"MIT"},"isInternal":false,"tokens":10617,"sizeBytes":42524},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-plugins/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/SKILL.md","title":"melonjs-plugins","category":"anthropic-skill","format":"markdown","content":"# melonjs-plugins\n\nPath: `packages/melonjs/skills/melonjs-plugins/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/SKILL.md)","tokens":300,"sizeBytes":210},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-renderables/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderables/SKILL.md","title":"melonjs-renderables","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-renderables\ndescription: \"Use this skill when subclassing Renderable, writing a custom draw() method, handling pointer or click events on game objects, applying post effects and shaders, or controlling draw order. Covers the pos-relative draw contract, isKinematic and input, addPostEffect vs the deprecated shader property, and z-ordering. Triggers on: Tween, easing, animate, Renderable, extends Renderable, draw(, update(, isKinematic, pointerEvent, registerPointerEvent, onClick, addPostEffect, removePostEffect, ShaderEffect, addChild, z-order, anchorPoint, floating.\"\nlicense: MIT\n---\n\n# Renderables, input and effects\n\n> Subclassing `Renderable`, custom drawing, receiving pointer events, and\n> applying post effects — with the traps that produce silently wrong output\n> rather than an error.\n\nEverything here is a **silent failure**: the code runs, nothing throws, and the\nresult is wrong. Those are the cases worth memorising, because nothing in the\ntype signatures warns you.\n\n## 1. A custom `draw()` must position itself from `this.pos`\n\nThe renderer is **not** pre-translated to the renderable's position when `draw()`\nis called. `Container.draw` translates by the *container's* own `pos` and then\ncalls `child.preDraw` / `draw` / `postDraw`; `preDraw` never translates by the\nchild's `pos`. Read `this.pos` and draw relative to it.\n\n```js\nclass Marker extends Renderable {\n    draw(renderer) {\n        // ✗ draws at the parent container's origin, not at the marker\n        renderer.fillRect(0, 0, this.width, this.height);\n\n        // ✓\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\nThe symptom is an object that renders correctly at position (0, 0) and appears\nstuck at the top-left as soon as you move it.\n\n**Zero the anchor in custom-draw renderables.** `preDraw` does not translate to\n`pos`, but it *does* apply the anchor offset `(-width * anchorPoint.x,\n-height * anchorPoint.y)`. With the default centred anchor, drawing at\n`this.pos` therefore lands half a size off:\n\n```js\nconstructor(x, y, w, h) {\n    super(x, y, w, h);\n    this.anchorPoint.set(0, 0);   // ← or every custom draw is offset by -w/2, -h/2\n}\n```\n\n## 2. `isKinematic` must be `false` to receive input or collisions\n\n`Renderable.isKinematic` **defaults to `true`**, which means *\"physics collision\nand input events will not impact this renderable\"*. A kinematic renderable is\nskipped by the pointer-event dispatcher entirely.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y) {\n        super(x, y, 100, 40);\n        this.isKinematic = false;          // ← required, or clicks never arrive\n        input.registerPointerEvent(\"pointerdown\", this, () => { /* … */ });\n    }\n}\n```\n\nThe symptom is a handler that is registered, looks correct, and never fires.\n\nNote that `Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*`\nelements set it to `false` for you, and so does the `Body` constructor on its\nowner (which covers `Entity` and anything you give a body to) — so a plain\n`Renderable` or `Sprite` subclass with no body is the case that catches people.\n\nThe same flag has a second, less obvious effect: it is also the opt-in to the\nworld **broadphase**. The quadtree/octree insert skips any child with\n`isKinematic === true`, so body-less objects you want `adapter.queryAABB` /\n`querySphere` to find need `isKinematic = false` as well. The builtin\nnarrowphase likewise requires *both* partners to be non-kinematic.\n\n## 3. Draw order: only `addChild(child, z)` works\n\nThere are three separate traps here and they compound.\n\n```js\nworld.addChild(sprite, 10);   // ✓ the only reliable form\n```\n\n1. **`renderable.z` does not exist.** There is no `z` accessor anywhere on\n   `Renderable` (nor on `Rect`/`Polygon` above it) — only `depth`, which proxies\n   `pos.z`, and the container's default `sortOn: \"z\"` comparator reads `pos.z`.\n   Writing `sprite.z = 100` creates an inert property that nothing reads.\n   Several shipped examples still contain `this.z = Number.POSITIVE_INFINITY`;\n   those HUDs draw on top only because they happen to be added last. (A `z` key\n   in a `Sprite`/`ImageLayer` *settings* object is a different thing and is real\n   — it assigns `pos.z` in the constructor — but trap 2 then overwrites it.)\n2. **Setting `depth` *before* `addChild` is overwritten.** With `autoDepth` on\n   (the default), `addChild` without an explicit `z` assigns\n   `child.pos.z = children.length` after the push, discarding whatever you set.\n3. **Setting `depth` *after* `addChild` does not reorder.** A `\"z\"` sort runs\n   when a child is added (`autoSort`, deferred to the next tick), not when\n   `pos.z` changes. If you must change it later, call `container.sort()`\n   yourself, or use `moveToTop` / `moveUp` / `moveDown` / `moveToBottom`, which\n   reorder the children themselves. (An `\"x\"` / `\"y\"` sort is different: see\n   below.)\n\n### Sorting on `\"y\"` (or `\"x\"`), and `sortOffset`\n\nA container with `sortOn = \"y\"` draws its children from the smallest `pos.y` to\nthe largest, so whatever is lower on screen covers what is above it (top-down and\nisometric games). `\"x\"` does the same along x. A different `pos.z` still decides\nfirst, so keep the children that should interleave at one z.\n\n- **Moving a child re-sorts by itself.** With `autoSort` on (the default), the\n  container checks its order at the end of every update, after physics and the\n  children's own updates, and re-sorts when anything is out of place: a walking\n  character needs no `sort()` call. A frame where nothing moved costs one pass\n  over the children and does not cause a redraw. Turn `autoSort` off to keep\n  the children in the order you put them.\n- **Ties keep insertion order, and the child added LAST is drawn BEHIND.** A\n  torch placed at its wall's exact position disappears behind the wall.\n\n`sortOffset` changes the ORDER only, never where anything is: the child is lined\nup as if it stood that many pixels lower (positive) or higher (negative), and is\ndrawn exactly where it was. Use it when `pos` is not the point to sort by:\n\n```js\ntorch.sortOffset = 1;                        // win the tie with the wall it hangs on\ndust.sortOffset = door.pos.y - dust.pos.y;   // an emitter's pos is the top of its\n                                             // spawn area: line it up with the door\nprop.sortOffset = prop.height;               // sort by the bottom of the image\n```\n\nA container that keeps itself sorted picks a change up at its next update (with\n`autoSort` off, call its `sort()`). `\"z\"` and `\"depth\"` sorting ignore it. It must be a finite number (a string\nor `NaN` throws, rather than scrambling every sibling), and a pooled renderable\nstarts again at `0`.\n\nThe exception: under `Camera3d` the world's `sortOn` is `\"depth\"`, and the\ncamera calls `container.sortNow(true)` every frame. Note that mode does **not**\nsort by `pos.z` as a layer index — it sorts by squared distance from the camera\nin world space (nearest first in the array, painted far→near). So changing\n`depth` does reorder there, but as a position change, not as a layer number.\n\n```js\n// ✗ overwritten by autoDepth\nsprite.depth = 10;\nworld.addChild(sprite);\n\n// ✗ no resort happens (except under Camera3d)\nworld.addChild(sprite);\nsprite.depth = 10;\n\n// ✓\nworld.addChild(sprite, 10);\n```\n\n## 4. `removePostEffect()` destroys the effect\n\nTo disable an effect temporarily, toggle it. Removing it frees its GPU\nresources and leaves you holding a dead object.\n\n```js\n// ✗ effect is destroyed; re-adding it will not work\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nException worth knowing: an effect with `effect.shared === true` opts out of\nauto-destroy, because it is reused across renderables.\n\nUse `addPostEffect()` / `getPostEffect()` / `removePostEffect()`. Assigning\n`renderable.shader = …` is deprecated since 19.2.0.\n\nPost effects need a programmable pipeline. A `ShaderEffect` constructed against\nthe Canvas renderer (`renderer.shaderLanguage === null`) logs a warning, stays\nat `enabled === false` and no-ops every method rather than throwing — and\n`beginPostEffect` filters disabled effects out. Since `video.AUTO` picks the\nbackend at runtime, an effect that works on your machine may do nothing on\nanother.\n\n## 5. Opacity cascades; `setChildsProperty` assigns\n\n`alpha` composes down the tree. Fading a container fades everything inside\nit, and the values multiply — a child at `0.5` inside a parent at `0.5` draws\nat `0.25`. Each renderable keeps its own value; the composition happens at\ndraw time and is undone when the renderable is finished.\n\n```js\nmodel.alpha = 0.3;                             // fades the whole rig\ncontainer.setChildsProperty(\"alpha\", 0.3, true); // REWRITES each child's alpha\n```\n\nThe two are not interchangeable. `setChildsProperty` is a bulk assignment: it\nchanges what the children *are*, so their own `alpha` now reads `0.3` and\nstays that way after the container goes back to full. Reach for it when you\nmean to change the children, and for `alpha` when you mean to fade a subtree.\n\nDo not assign alpha down a hierarchy by hand. The renderer composes it as it\nwalks the tree, so setting it at more than one level multiplies: in a rig\nnested three deep, `0.3` applied at every level lands parts on `0.3`, `0.09`\nand `0.027` depending where they sit. The symptom does not look like an\nopacity problem — a model whose parts hold *different* opacities, and which\nappears to change opacity as it turns, because turning changes which parts\nface the camera.\n\nA useful consequence: a `GLTFModel` is a `Container`, so `model.alpha` fades\nevery part of a loaded rig — which is what you want for a ghost, a fade-in,\nor a reflection.\n\n## 6. `Vector3d.set(x, y)` silently zeroes `z`\n\n```js\nv.set(10, 20);        // z becomes 0, not \"unchanged\"\nv.set(10, 20, v.z);   // keep it\n```\n\n`pos` is an `ObservableVector3d`, whose `set(x = 0, y = 0, z = 0)` does the same\n— so `this.pos.set(x, y)` wipes the object's depth.\n\n`Renderable#setPosition(x, y, z?)` is the renderable-level form and does not:\nomit `z` and the depth is left exactly as it was. Reach for it whenever you\nare moving an object rather than editing a vector, and keep `v.set(x, y, v.z)`\nfor the vector itself.\n\n## 7. A settings key the class never reads is silently ignored\n\nA settings object is a plain literal: nothing rejects a key, so a name that\nbelongs somewhere else does nothing at all and says nothing about it.\n\n```js\n// WRONG — only the RENDERER reads a `blendMode` setting, never a renderable\nconst glow = new Sprite3d(x, y, { image: \"sun\", blendMode: \"additive\" });\n\n// right: it is a property\nglow.blendMode = \"additive\";\n```\n\nThe same shape catches `transparent` on a `GLTFModel` (its parts carry it —\n`model.setChildsProperty(\"transparent\", true, true)`) and any `Mesh` setting\nhanded to a class that forwards only a curated subset. If a visual option\nappears to do nothing, check whether the class actually reads it before\ntuning the value.\n\n## 8. `isRenderable` does not hide anything\n\nIt gates `updateBounds`, not drawing. Setting it `false` leaves the object on\nscreen and merely stops its bounds tracking, which is worse than doing nothing.\n\n```js\nsprite.isRenderable = false;   // WRONG — still drawn, bounds now stale\nsprite.alpha = 0;              // hides it\n```\n\nUse `alpha` for a blink, or remove the child for a long absence.\n\n## A mask belongs to you, not to the renderable\n\n`renderable.mask` takes a shape you created, and `destroy()` releases the\nreference without recycling it: the same instance may still be masking\nsomething else, so the engine has no claim on it. If you pool masks, release\nthem yourself.\n\nBefore 20.7.0 `destroy()` tried to recycle it and **threw** for every type\n`mask` accepts, because it used the legacy name-keyed pool and no geometry\nclass is registered there. It failed mid-teardown, so a masked renderable\ncould not be destroyed or removed without keepalive at all.\n\n## Update and draw\n\n```js\nclass Enemy extends Renderable {\n    update(dt) {\n        this.pos.x += this.speed * (dt / 1000);\n        return true;                       // ← true means \"redraw me\"\n    }\n    draw(renderer) { /* … */ }\n}\n```\n\n`update()` returns a boolean meaning *\"this object changed and needs\nredrawing\"*. The parent accumulates it into the container's `isDirty`, and the\napplication skips the whole draw pass while nothing is dirty — so returning\nnothing (falsy) can leave the scene frozen even though state is updating. The\nbase implementation returns `this.isDirty`, so `return super.update(dt)` is the\nother correct ending.\n\n`update()` is only called when `obj.inViewport === true` or\n`obj.alwaysUpdate === true`. An off-screen object with the default\n`alwaysUpdate = false` does not tick at all.\n\n## Anchors and floating\n\n- `anchorPoint` defaults to centre `(0.5, 0.5)` on `Renderable`, but `Container`\n  forces `(0, 0)`. Mixing them up shifts children by half their size.\n- `floating = true` pins a renderable to the screen rather than the world, which\n  is what you want for HUD elements. It opts the object out of camera transforms\n  and out of ancestor position accumulation.\n\n## Prefer built-in features\n\nBefore writing custom `draw()` code, check whether the engine already covers it —\n`ParticleEmitter`, `Tween`, the `ShaderEffect` presets, `UISpriteElement` and\n`UITextButton`, the physics adapters, and the Tiled tilemap renderers all exist.\nHand-rolled equivalents miss the batching and the multi-backend support.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| object draws at its parent's origin, ignores its position | `draw()` not reading `this.pos` |\n| pointer handler registered but never fires | `isKinematic` left at its `true` default |\n| wrong draw order | anything other than `addChild(child, z)` — see section 3 |\n| `me.pool: object ... cannot be recycled` while destroying | a mask on a renderable, before 20.7.0 |\n| effect cannot be re-enabled after removal | `removePostEffect()` destroyed it |\n| effect silently does nothing | running on the Canvas renderer |\n| object appears frozen while its state changes | `update()` not returning `true` |\n| off-screen object never ticks | `update()` is gated on `inViewport` — set `alwaysUpdate = true` |\n| custom draw offset by half the size | centred `anchorPoint` default not zeroed |\n| spatial query never finds an object | `isKinematic` left `true` — not in the broadphase |\n| `z` unexpectedly 0 after a `set` | `Vector3d.set(x, y)` defaults `z` to 0 — `setPosition(x, y)` on the renderable leaves the depth alone |\n| a settings option appears to do nothing | the class never reads that key — see section 7 |\n| `isRenderable = false` did not hide the object | it gates bounds, not drawing — use `alpha` |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-getting-started` — Application lifecycle and the scene graph\n- `melonjs-20-migration` — the deprecated `shader` property and other pre-20 APIs\n","frontmatter":{"name":"melonjs-renderables","description":"Use this skill when subclassing Renderable, writing a custom draw() method, handling pointer or click events on game objects, applying post effects and shaders, or controlling draw order. Covers the pos-relative draw contract, isKinematic and input, addPostEffect vs the deprecated shader property, and z-ordering. Triggers on: Tween, easing, animate, Renderable, extends Renderable, draw(, update(, isKinematic, pointerEvent, registerPointerEvent, onClick, addPostEffect, removePostEffect, ShaderEffect, addChild, z-order, anchorPoint, floating.","license":"MIT"},"isInternal":false,"tokens":3841,"sizeBytes":15119},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-renderer-backends/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/SKILL.md","title":"melonjs-renderer-backends","category":"anthropic-skill","format":"markdown","content":"# melonjs-renderer-backends\n\nPath: `packages/melonjs/skills/melonjs-renderer-backends/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/SKILL.md)","tokens":300,"sizeBytes":240},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md","title":"melonjs-scenes-and-state","category":"anthropic-skill","format":"markdown","content":"# melonjs-scenes-and-state\n\nPath: `packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md)","tokens":300,"sizeBytes":237},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-sprites-and-animation/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-sprites-and-animation/SKILL.md","title":"melonjs-sprites-and-animation","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-sprites-and-animation\ndescription: \"Use this skill when creating sprites, playing frame animations, or working with texture atlases in melonJS. Covers Sprite construction from a plain image or an atlas region, addAnimation/setCurrentAnimation, TextureAtlas for TexturePacker/ShoeBox/Aseprite, tint, flip, NineSliceSprite, and the centred anchorPoint that offsets naive positioning. Triggers on: Sprite, new Sprite, addAnimation, setCurrentAnimation, isCurrentAnimation, TextureAtlas, TexturePacker, Aseprite, ShoeBox, atlas, region, framewidth, frameheight, tint, flipX, flipY, NineSliceSprite, animation.\"\nlicense: MIT\n---\n\n# Sprites, animation and atlases\n\n> Creating sprites from images or atlases, driving frame animation, and the\n> positioning trap that catches almost everyone.\n\n## `pos` is the centre, not the top-left\n\n`Renderable.anchorPoint` defaults to **`(0.5, 0.5)`**, so a sprite's `pos` is its\n**centre**. `Container` forces `(0, 0)` instead, which is why containers and\nsprites appear to disagree.\n\n```js\n// a 64×64 sprite at (100, 100) covers 68..132, not 100..164\nconst s = new Sprite(100, 100, { image: \"hero\", framewidth: 64, frameheight: 64 });\n\ns.anchorPoint.set(0, 0);   // switch to top-left positioning if you prefer\n```\n\nWriting top-left maths against the centred default puts everything half a sprite\noff, with no error. It is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Constructing a sprite\n\nFrom a plain image:\n\n```js\nconst hero = new Sprite(x, y, {\n    image: \"hero\",          // preloaded asset name\n    framewidth: 32,\n    frameheight: 32,\n});\n```\n\nFrom a texture atlas region:\n\n```js\nconst atlas = new TextureAtlas(loader.getJSON(\"sprites\"), loader.getImage(\"sprites\"));\nconst coin = new Sprite(x, y, { image: atlas, region: \"coin_01.png\" });\n```\n\n`framewidth` / `frameheight` define the animation grid on a sheet. Omit them and\nthe whole image is one frame.\n\n## Animation\n\n```js\nhero.addAnimation(\"idle\", [0, 1, 2, 3], 100);   // frames, ms per frame\nhero.addAnimation(\"walk\", [4, 5, 6, 7]);\nhero.addAnimation(\"die\",  [8, 9], 150);\n\nhero.setCurrentAnimation(\"walk\");\nhero.setCurrentAnimation(\"die\", \"idle\");                          // chain to idle when it ends\nhero.setCurrentAnimation(\"die\", { loop: false });                 // play once, hold the last frame\nhero.setCurrentAnimation(\"die\", { loop: false, onComplete: fn }); // …and call fn when it does\nhero.setCurrentAnimation(\"use\", { next: \"idle\", onComplete: fn }); // call fn, then chain to idle\nhero.setCurrentAnimation(\"run\", { onFrame: (frame) => {            // each frame playback reaches\n    if (frame === 3 || frame === 8) footstep();\n} });\n\nif (hero.isCurrentAnimation(\"walk\")) { /* … */ }\n```\n\nFrame indices are into the `framewidth`/`frameheight` grid. With an atlas, use\n`atlas.getAnimationSettings([regionNames])` and spread it into the Sprite\nsettings, or `atlas.createAnimationFromName([regionNames])`, which returns a\nready-made Sprite. On a fixed-cell spritesheet only numeric indices are\naccepted — passing region names throws.\n\nThe second argument to `setCurrentAnimation` is polymorphic. A **string** chains\nto that animation when this one ends. An **options object** takes\n`{ loop, next, speed, onComplete, onFrame }`, and only an explicit `loop: false`\nstops the looping. A **bare function** is the legacy form: it fires at *every* cycle end,\nand only holds the last frame if it returns `false`. Omit the argument entirely\nand the animation loops forever.\n\n`onFrame(frame)` is called with frame 0 when the animation starts, then with each\nframe playback reaches, so a footstep or a hit lands on its frame without polling\n`getCurrentAnimationFrame()`. A long tick reports every frame it goes past. It is\nnot called for a frame set with `setAnimationFrame`, nor for the frame 0 a\nplay-once animation would wrap to. Keep the callback in a field when you call\n`setCurrentAnimation` every frame: re-selecting keeps the first one anyway.\n\nWith both `next` and `onComplete`, the callback runs first and the chained\nanimation starts with none of its own, so a one-off action (a pickup, an\nattack) needs no manual switch back to idle.\n\nRe-selecting the animation that is already current is a no-op, so calling\n`setCurrentAnimation` every frame is safe, and an `isCurrentAnimation` check\naround it adds nothing. An unknown id throws.\n\n## Texture atlases\n\n`TextureAtlas` reads several formats:\n\n| source | how |\n|---|---|\n| TexturePacker / Free Texture Packer JSON | `new TextureAtlas(loader.getJSON(name), loader.getImage(name))` |\n| Aseprite **JSON export** | same as TexturePacker — the format is detected from `meta.app` |\n| Aseprite **`.aseprite` / `.ase` binary** | loader type `\"aseprite\"`, which stores the composited image *and* a JSON sidecar under the one asset name |\n| ShoeBox | JSON export using the melonJS exporter settings file; same call as TexturePacker |\n| fixed-cell spritesheet | `new TextureAtlas({ framewidth, frameheight, anchorPoint }, loader.getImage(name))` |\n| multi-page | pass an array of atlas JSON objects as the first argument |\n\nHelpers: `createSpriteFromName(name)`, `createAnimationFromName([names])`,\n`getAnimationSettings([names])`.\n\nAtlases matter for performance: the GPU batchers bind several textures at once\n(up to `renderer.maxTextures`), so a handful of separate images still batch —\nbut once a frame crosses that many distinct textures the batch has to flush.\nPacking into one atlas keeps everything on a single binding.\n\n## Visual properties\n\n```js\nsprite.setTint(\"#ff8000\");   // multiply tint\nsprite.alpha = 0.5;\nsprite.flipX(true);                   // mirror horizontally\nsprite.flipY(true);\nsprite.blendMode = \"additive\";        // see the effects skill for the full list\nsprite.scale(2, 2);                   // ← multiplicative, see below\n```\n\n**`scale()` is multiplicative, not absolute.** Calling it every frame compounds.\nFor absolute scaling reset first:\n\n```js\nsprite.currentTransform.identity();\nsprite.currentTransform.scale(s, s, 1);\n```\n\nThis bites hardest with pooled sprites, which arrive carrying their previous\nlife's transform.\n\n## `NineSliceSprite`\n\nFor panels and dialogue boxes that stretch without distorting their corners:\n\n```js\nnew NineSliceSprite(x, y, {\n    image: \"panel\", width: 300, height: 120,\n    insetx: 12, insety: 12,     // lowercase x/y — `insetX` is silently ignored\n});\n```\n\n`width` and `height` are mandatory and the constructor throws without them.\nThe insets default to a quarter of the frame.\n\n## Pooling\n\nFrequently spawned sprites should be pooled:\n\n```js\nimport { createPool } from \"melonjs\";\n\nconst bulletPool = createPool((x, y) => {    // me.pool is deprecated\n    const instance = new Bullet(x, y);\n    return { instance, reset: (x, y) => instance.onResetEvent(x, y) };\n});\nconst b = bulletPool.get(x, y);             // new Bullet(x, y), or reset(x, y) on a reused one\nworld.removeChild(b);                       // returns it to bulletPool automatically\n```\n\nTwo consequences:\n\n- A recyclable class's `onResetEvent` must reset **everything** its lifetime\n  mutates — alpha, tint, scale, animation state. Anything you forget carries\n  over to the next use.\n- Pool-recycled objects do **not** fire `onDestroyEvent` on removal. Pair event\n  subscriptions with `onActivateEvent` / `onDeactivateEvent` instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| everything offset by half a sprite | top-left maths against the centred `anchorPoint` |\n| sprite grows every frame | `scale()` is multiplicative — reset the transform |\n| a recycled sprite keeps its old look | `onResetEvent` does not restore every mutated property |\n| event handlers leak on pooled objects | bound in the constructor rather than `onActivateEvent` |\n| animation never advances | no `framewidth`/`frameheight`, so the sheet is one frame |\n| animation plays once and stops | `{ loop: false }`, or a callback that returned `false` |\n| sprites do not batch | more distinct textures in one frame than `renderer.maxTextures` |\n| nine-slice corners stretch anyway | `insetX`/`insetY` instead of `insetx`/`insety` |\n\n## Related skills\n\n- `melonjs-renderables` — anchors, draw order, custom draw\n- `melonjs-getting-started` — preloading images and atlases\n","frontmatter":{"name":"melonjs-sprites-and-animation","description":"Use this skill when creating sprites, playing frame animations, or working with texture atlases in melonJS. Covers Sprite construction from a plain image or an atlas region, addAnimation/setCurrentAnimation, TextureAtlas for TexturePacker/ShoeBox/Aseprite, tint, flip, NineSliceSprite, and the centred anchorPoint that offsets naive positioning. Triggers on: Sprite, new Sprite, addAnimation, setCurrentAnimation, isCurrentAnimation, TextureAtlas, TexturePacker, Aseprite, ShoeBox, atlas, region, framewidth, frameheight, tint, flipX, flipY, NineSliceSprite, animation.","license":"MIT"},"isInternal":false,"tokens":2057,"sizeBytes":8296},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-tilemaps/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-tilemaps/SKILL.md","title":"melonjs-tilemaps","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs-tilemaps\ndescription: \"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 registerTiledObjectClass and 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, registerTiledObjectClass, registerTiledObjectFactory, TMXObject, resolvedProperty, resolvedProperties, tile class, tile properties.\"\nlicense: MIT\n---\n\n# Tiled maps\n\n> melonJS's flagship content pipeline. Maps are authored in\n> [Tiled](https://www.mapeditor.org/) and loaded through the `level` director —\n> you rarely touch the `TMX*` classes directly.\n\n## Loading a level\n\nPreload the map **and** everything it references — the `.tmx`, any external\n`.tsx` tilesets, and the tileset images:\n\n```js\nawait loader.preload([\n    { name: \"map1\",    type: \"tmx\", src: \"data/map/map1.tmx\" },\n    { name: \"tileset\", type: \"tsx\", src: \"data/map/tileset.tsx\" },\n    { name: \"tiles\",   type: \"image\", src: \"data/img/tiles.png\" },\n]);\n\nlevel.load(\"map1\");           // inside Stage.onResetEvent\n```\n\nAsset `type` is `\"tmx\"` for maps and `\"tsx\"` for external tilesets — those are\nthe only two type strings the loader knows. The *file* can be `.tmx` / `.tsx`\n(XML), or `.json` / `.tmj` / `.tsj`; the parser branches on the extension, not\non the type. `type: \"tmj\"` throws `unknown or invalid resource type`. You can\nalso skip `src` and pass the map inline via `data` (with `format: \"json\"` or\n`\"xml\"`).\n\n`level.load(levelId, options)` accepts `container` (default `game.world`),\n`onLoaded` (default `game.onLevelLoaded`), `flatten` (default `game.mergeGroup`),\n`setViewportBounds` (default **`true`**) and `async` (default `false`). It throws\n`level <id> not found` for an unknown id — synchronously, in both forms, because\nthat is a typo rather than a load failure.\n\nThose `game.` defaults are the engine's own, and `game` is whichever\n`Application` finished `init()` last. **Pass `container` explicitly** from a\n`Stage`, which has the right one: `level.load(\"map\", { container: this.app.world })`.\n\n**`level.load` is deferred while the game loop is running.** It calls\n`state.stop()` and finishes the load on a timer, after the current frame, so by\ndefault it returns `true` before anything is in the world. Two ways to sequence\nwork after it:\n\n```js\n// await it\nawait level.load(\"map1\", { async: true });\n// the world is populated here\n\n// ...or use the callback / event, which fire in both forms\nlevel.load(\"map1\", { onLoaded: () => this.spawnPlayer() });\n```\n\n`async: true` is the only thing that changes the return value — everything else\nbehaves identically, `onLoaded` included. Without it the call returns a boolean,\nso `await level.load(\"map1\")` is not an error and does not await the load:\n`await true` resolves immediately, while the load is still sitting on a timer.\nPass the flag when you mean to await.\n\n`level.reload()`, `level.next()` and `level.previous()` take the same `async`\noption and resolve the same value they return — so `if (level.next())` becomes\n`if (await level.next({ async: true }))`. Running out of levels reports `false`\neither way rather than throwing. `level.getCurrentLevelId()` and\n`level.levelCount()` round out the namespace. `flatten: false` wraps each Tiled\nobject group in its own `Container` named after the group.\n\n## Spawning entities from Tiled objects\n\nObjects are matched against a factory registry, in this order: the object's\nTiled **class**, then its **name**, then, for a tile object with no class of\nits own, its **tile's class**, then the structural fallbacks `\"text\"`,\n`\"tile\"` and `\"shape\"`.\n\n```js\nimport { registerTiledObjectClass, registerTiledObjectFactory } from \"melonjs\";\n\nregisterTiledObjectClass(\"Enemy\", Enemy);   // ← matches Tiled class OR name\nregisterTiledObjectFactory(\"Spine\", (settings, map) => { /* → Renderable */ });\nlevel.load(\"map1\");\n```\n\nThese are the entry points to use. The registry is Tiled's own, held by the\nobject factory, and has nothing to do with object pooling.\n\n`pool.register(className, classObj)` **also** works and registers a Tiled\nfactory as a side effect, under the name and again under an `me.`-prefixed\nalias, unless you set `pool.autoRegisterTiled = false`. That is the older\nspelling, kept working for maps and games written against it, and it now\nprints a one-off deprecation notice: `me.pool` is deprecated since 18.0.0.\n\nIts third argument no longer buys anything here. It used to make Tiled\nobjects recyclable; they are constructed directly now, and the container\nreturns children to typed pools rather than to the name-keyed one. To pool a\nclass you spawn constantly, give it a `createPool` and register it for Tiled\nseparately:\n\n```js\nregisterTiledObjectClass(\"mainPlayer\", PlayerEntity);\n```\n\nRegister **before** loading. With no match the object falls through to the\nstructural factory and becomes a plain `Renderable` with a static body — it does\nnot error, it just has none of your behaviour.\n\n`registerTiledObjectClass` **throws** if you register a *different* constructor\nunder a name already taken (re-registering the same one is a no-op).\n`registerTiledObjectFactory` overwrites and only `console.warn`s, so it is the\none to use for replacing something already registered.\n\n**Overriding a built-in name always works.** The engine's own classes are\nDEFAULTS: they fill a name nothing else has claimed, and yours takes it over\nwhenever you register, before or after a map has loaded.\n\n```js\nregisterTiledObjectClass(\"Trigger\", MyTrigger);   // whenever you like\n```\n\nThe throw is for a clash between two of *your* classes under one name. For\nthat case `registerTiledObjectFactory` overwrites and only warns.\n\n### Classes and properties set on tiles\n\nIn Tiled, a tile can carry a class and custom properties (tileset editor), and\nevery object placed from that tile shows them as its own. melonJS follows:\n\n- **The tile's class.** A tile object with no class is built by the class\n  registered under its tile's class, so a designer sets \"Chest\" once on the\n  chest tile and every chest dropped in the map is a `Chest`. The object's\n  own class or registered name still comes first, and a tile classed like a\n  built-in (`Sprite`, `Trigger`, `shape`...) stays a plain tile.\n- **The tile's properties.** Read them with `settings.resolvedProperty(name)`\n  or `settings.resolvedProperties()`, which merge the tile's properties under\n  the object's own, as Tiled's scripting API of the same names does. They are\n  **not** set on `settings` by name: only the object's own properties are,\n  as before, so a tile property named like a sprite setting never reaches a\n  plain tile sprite.\n\n```ts\nimport { registerTiledObjectClass, Sprite, type TMXObject } from \"melonjs\";\n\n// the custom properties set on the object itself, by name\ninterface DoorSettings extends TMXObject {\n    locked?: boolean;\n}\n\nclass Door extends Sprite {\n    locked: boolean;\n    facing: string;\n    constructor(x: number, y: number, settings: DoorSettings) {\n        super(x, y, { image: \"door\" });\n        this.locked = settings.locked ?? false;\n        // on the object, or else on the door tile in the tileset\n        this.facing = settings.resolvedProperty(\"facing\") ?? \"N\";\n    }\n}\nregisterTiledObjectClass(\"Door\", Door);   // and give the door tile the class \"Door\"\n```\n\n`TMXObject` is the type of the `settings` a registered class (or factory)\nreceives: the object as read from the map, with `tile` (a `Tile`) on a tile\nobject. Extend it with the properties your class reads.\n\nFor a class that is not a `Sprite` itself (a `Container` holding the tile and\nsomething else, say), `settings.tile.getRenderable()` returns the tile's sprite\nas a plain tile object would get it: from a tileset image, an image collection\nor an animated tile alike, with no region names to build by hand.\n\nmelonJS pre-registers `Renderable`, `Sprite`, `NineSliceSprite`, `Text`,\n`BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable`, `Trigger`\nand `Entity` as Tiled classes, so `Collectable` and `Trigger` work out of the\nbox. Each is registered under its `me.`-prefixed alias as well, so a map\nauthored against melonJS 1.x finds them where it looks.\n\nA `Trigger` forwards a **fixed list** of settings to `level.load()` — not\nwhatever you pass it. Today that list is `container`, `onLoaded`, `flatten`,\n`setViewportBounds` and the glTF scene options (`scale`, `rightHanded`,\n`lights`, `lightIntensityScale`, `castGroundShadow`, `shadowGroundY`); see the\n3D assets skill. Anything outside it is **dropped silently**, which is worth\nknowing before you spend an afternoon on a map that loads at the wrong scale.\n`async` is excluded deliberately — the trigger needs to sequence its own\ntransition.\n\n## Camera bounds\n\n`level.load` sets the viewport bounds to the map size for you —\n`setViewportBounds` defaults to `true`, and the handler is re-run on every\nviewport resize, centring the map when it is smaller than the screen. You only\nneed to do it by hand when you opted out:\n\n```js\nlevel.load(\"map1\", { setViewportBounds: false });\napp.viewport.setBounds(0, 0, mapWidth, mapHeight);   // now it's on you\n```\n\nThe same is true if you drive `TMXTileMap.addTo(container, flatten, setViewportBounds)`\ndirectly — note that method's `setViewportBounds` defaults to **`false`**, the\nopposite of `level.load`'s.\n\n## Collision shapes\n\nDraw collision geometry in Tiled on an object layer and melonJS turns it into\nworld shapes automatically — rectangles, polygons, polylines, ellipses and\npoints all work.\n\nThe contract is the **layer name**: melonJS lowercases it and checks whether it\n*contains* `\"collision\"` (so `collision`, `Collision`, `world collision` all\nmatch). Inside such a layer, objects **with no name** get\n`collisionType = collision.types.WORLD_SHAPE` and `type = \"static\"`. A *named*\nobject in a collision layer keeps whatever its factory gave it — that is how you\nmix triggers into the collision layer.\n\nAn object that matches no factory and carries no text or tile data falls to the\n`\"shape\"` factory: a plain `Renderable` with a static `bodyDef` built from its\ngeometry, which the world auto-registers with the active adapter on `addChild`.\n\nFor per-object physics, set the object's custom properties in Tiled; they arrive\non the settings object passed to your entity's constructor. Properties set on\nthe object's tile are read with `settings.resolvedProperty(name)`.\n\n## Map orientations\n\nOrthogonal, **isometric**, hexagonal, staggered and oblique are all supported;\nanything else throws `<orientation> type TMX Tile Map not supported!` at load.\nIsometric maps need y-sorting so objects overlap correctly:\n\n```js\napp.world.sortOn = \"y\";       // after level.load\n```\n\nThe world then keeps its objects in order as they move (`autoSort`, on by\ndefault): a character walking around passes in front of and behind the scenery\nwith no `sort()` call.\n\n`sortOn` only accepts `\"x\"`, `\"y\"`, `\"z\"` (the default) or `\"depth\"` and throws\non anything else. `Vector2d`, `Vector3d`, their observable variants and `Polygon`\nall carry `.toIso()` / `.to2d()` for converting between screen and map space.\n\n### What the y-sort can and cannot sort\n\n`sortOn = \"y\"` sorts **objects**, never tiles. A tile layer is a single\nrenderable drawn in one piece, so a character ends up in front of all of it or\nbehind all of it. Anything a character walks both in front of and behind\n(walls, trees, furniture) has to be on an **object layer, as tile objects**,\nand in the **same** object layer as the characters: each Tiled layer gets its\nown `z`, and the y-sort compares `z` before `y`. Keep the floor on a tile layer\nbelow them. This holds for any y-sorted map, isometric or a top-down\northogonal one.\n\n```\nfloor      tile layer     the ground, drawn first\ndungeon    object layer   walls, props, chests, doors, the hero\ncollision  object layer   the shapes (not drawn)\n```\n\nTile objects with no class are built for you: each one is a `Sprite` with its\ndefault body, static, over its whole image box and typed `ENEMY_OBJECT`. A\ncharacter keeping the default `collisionMask` (every type) collides with all of\nthem, so when the scenery's real collision is drawn in the collision layer,\nmask the character to it:\n\n```js\nthis.body.setCollisionMask(collision.types.WORLD_SHAPE);\n```\n\nGive the objects with behaviour a Tiled class and register it, as above.\n\nThe `y` it sorts on is `pos.y`, and Tiled objects default to a `(0, 0)` anchor,\nso that is the **top** of each image. Sorting by the top only matches sorting by\nthe feet when every image is the same height and stands on the bottom of its\nbox, the usual layout of an isometric pack. A shorter image sorts in front of\nthings it stands behind. Either pad the art to one height, or line every object\nup by the bottom of its image with `sortOffset`, which changes the order and\nnever the position:\n\n```js\nlevel.load(\"map1\");\napp.world.sortOn = \"y\";\nfor (const child of app.world.getChildren()) {\n    child.sortOffset = child.height;   // sort by the bottom of each image\n}\n```\n\nA character you create in code follows the same convention as the scenery: by\nthe top of a frame as tall as the scenery's (`anchorPoint` `(0, 0)`), or by its\nfeet when everything sorts by the bottom.\n\nTwo equal positions keep insertion order, and the one added **last** is drawn\n**behind**: a decoration placed at its wall's exact position disappears. Give\nit `sortOffset = 1`. A `Container` or a particle emitter is sorted by its top\nleft corner (an emitter's by the top of its spawn area); give it the offset to\nthe point it belongs with, such as `door.pos.y - dust.pos.y`.\n\n## Image layers\n\n`ImageLayer` gives parallax backgrounds with a ratio per axis, set in Tiled or in\ncode. Repeat behaviour matches Tiled's.\n\n## Compressed maps need a plugin\n\nA map saved with **gzip, zlib or zstd** compressed layer data throws\n`No inflate function set — GZIP/ZLIB decompression not supported!` at parse time\nwithout:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { TiledInflatePlugin } from \"@melonjs/tiled-inflate-plugin\";\n\nplugin.register(TiledInflatePlugin);   // before preloading any compressed map\n```\n\nUncompressed base64 and CSV layer data need no plugin. If a map loads in Tiled\nbut not in melonJS, check the layer compression setting first.\n\n## Rendering\n\nEach tile layer resolves to one of three `renderMode`s — `\"shader\"`,\n`\"prerender\"` or `\"perTile\"` — auto-selected in that order of preference, or\nforced per layer. The `\"shader\"` path is a single quad per tileset with a\nper-pixel GID lookup in the fragment shader. It requires **all** of:\n\n- a renderer that reports GPU tile-layer support (WebGL 2 / WebGPU — not Canvas),\n- `world.gpuTilemap !== false` (the `gpuTilemap` application setting, default `true`),\n- an **orthogonal** layer — isometric, hexagonal, staggered and oblique always\n  fall back,\n- no collection-of-image tileset, no non-zero `tileoffset`, and tile overflow\n  within the shader's 4-cell limit.\n\nWhen a layer falls back, melonJS `console.warn`s once with the reason. Practical\nconsequences: fewer tilesets means fewer batches (one packed tileset beats\nseveral small ones), and on the Canvas fallback large maps get noticeably\nslower. `world.preRender` (or a `preRender` layer property in Tiled) opts an\nunanimated layer into the offscreen-bake path instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| 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` |\n| the world is still empty right after `level.load` | the load is deferred to a timer while the loop runs — `await level.load(id, { async: true })`, or use `onLoaded` / `LEVEL_LOADED` |\n| `await level.load(id)` returned `true` rather than a promise | without `async: true` the call returns a boolean; `await true` resolves immediately and the load has not run yet — pass the flag when you mean to await |\n| `level <id> not found` | the map was never preloaded, or the asset `name` differs from the id passed to `load` |\n| `unknown or invalid resource type` | asset `type` set to `\"tmj\"` / `\"tsj\"` — use `\"tmx\"` / `\"tsx\"` with the `.tmj` / `.tsj` file |\n| camera will not scroll | `setViewportBounds: false`, or the map was added with `addTo()` (which defaults to `false`) |\n| `No inflate function set` at parse time | compressed layers without the tiled-inflate plugin |\n| collision shapes are not solid | the object layer's name does not contain `\"collision\"`, or the objects are named |\n| isometric objects overlap wrongly | `world.sortOn` not set to `\"y\"` |\n| a character is always in front of (or behind) every wall | the walls are on a tile layer, or on another object layer than the character: each layer has its own `z`, compared before `y` |\n| a small prop draws over a character standing in front of it | images of different heights: the y-sort uses `pos.y`, the top of a Tiled object's image. Pad the art, or set `sortOffset = height` on every object to sort by the bottom |\n| a decoration placed at the same position as a wall disappears behind it | a tie keeps insertion order and draws the later one behind: `sortOffset = 1` |\n| particles (or a container) at the foot of something draw over a character standing in front of it | they sort by the top of their spawn area (or the container's top left): set `sortOffset` to line them up with what they belong to |\n| a character is stopped short of walls and props placed as tile objects | it collides with their default bodies (static, `ENEMY_OBJECT`, the whole image box); mask it to `collision.types.WORLD_SHAPE` when the collision layer holds the real shapes |\n| a property set on a tile is undefined on the object's `settings` | tile properties are not copied onto `settings`: read them with `settings.resolvedProperty(name)` |\n| objects placed from a classed tile come out as plain sprites | that class is not registered, it is a built-in name, or the object has a class or a registered name of its own, which come first |\n| missing tiles or a blank map | the `.tsx` or tileset image was not preloaded |\n| tilemap slow on some machines | layer fell back off the shader path — check the console warning for the reason |\n\n## Related skills\n\n- `melonjs-physics` — what the collision shapes become once loaded\n- `melonjs-getting-started` — preloading and the asset type list\n","frontmatter":{"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 registerTiledObjectClass and 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, registerTiledObjectClass, registerTiledObjectFactory, TMXObject, resolvedProperty, resolvedProperties, tile class, tile properties.","license":"MIT"},"isInternal":false,"tokens":4675,"sizeBytes":18756},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs-ui-and-text/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md","title":"melonjs-ui-and-text","category":"anthropic-skill","format":"markdown","content":"# melonjs-ui-and-text\n\nPath: `packages/melonjs/skills/melonjs-ui-and-text/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md)","tokens":300,"sizeBytes":222},{"name":"SKILL.md","path":"packages/melonjs/skills/melonjs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs/SKILL.md","title":"melonjs","category":"anthropic-skill","format":"markdown","content":"---\nname: melonjs\ndescription: \"Use this skill FIRST for any melonJS task; it routes to the right specialised skill and lists the companion packages. melonJS is an open-source 2D, 2.5D and 3D HTML5 game engine running on WebGPU, WebGL 2 or Canvas. Covers Application setup, the scene graph, renderables, sprites, tilemaps, physics, audio, particles, shaders, and the 3D tier — perspective cameras, meshes and glTF scenes. Triggers on: melonjs, melonJS, me.game, Application, app.init, app.world, Renderable, Sprite, Stage, state.change, Camera2d, Camera3d, Mesh, tilemap, Tiled, TMX, ParticleEmitter, ShaderEffect, how do I make a game, HTML5 game engine.\"\nlicense: MIT\n---\n\n# melonJS\n\nEntry point for the melonJS skill collection. melonJS is an open-source HTML5\ngame engine for indie developers, covering 2D, 2.5D and 3D — perspective and\northogonal cameras, GPU-accelerated tilemap rendering, post-processing effects,\ncustom shaders, 3D meshes, polygon-accurate physics and modern Tiled workflows.\nIt runs on WebGPU, WebGL 2 or Canvas with automatic fallback, and has no runtime\ndependencies.\n\n## How to use this skill\n\n1. Find the specialised skill below that matches the task and follow it.\n2. If the task involves anything 3D, read `melonjs-3d` **first** — melonJS's\n   coordinate conventions are the inverse of OpenGL's, so untutored 3D instincts\n   are backwards.\n3. If you are writing melonJS from prior knowledge rather than from the current\n   docs, read `melonjs-20-migration` — most published melonJS material predates\n   20.0 and its bootstrap no longer exists.\n4. If none of them covers the task, use the escape hatch below rather than\n   guessing at an API.\n\n## Skill router\n\n| Skill | Load when… |\n|---|---|\n| [melonjs-getting-started](../melonjs-getting-started/SKILL.md) | Bootstrapping a game, `new Application`, `await app.init()`, choosing a renderer, preloading assets. |\n| [melonjs-20-migration](../melonjs-20-migration/SKILL.md) | Code based on melonJS 19.x or earlier, or an API that \"should exist\" but doesn't — `video.init`, `Entity`, `renderable.shader =`. |\n| [melonjs-scenes-and-state](../melonjs-scenes-and-state/SKILL.md) | `Stage` subclasses, the state manager, transitions, pausing, the update loop, timers, tweens, saved data. |\n| [melonjs-renderables](../melonjs-renderables/SKILL.md) | Subclassing `Renderable`, custom `draw()`, draw order, anchors, `floating`, post effects on an object. |\n| [melonjs-sprites-and-animation](../melonjs-sprites-and-animation/SKILL.md) | `Sprite`, frame animation, texture atlases (TexturePacker / Aseprite / ShoeBox), tint, flip, pooling. |\n| [melonjs-input](../melonjs-input/SKILL.md) | Keyboard actions, pointer and touch handling, drag, gamepad, virtual controls. |\n| [melonjs-physics](../melonjs-physics/SKILL.md) | Collision, bodies, movement, spatial queries, and the planck / matter adapters. |\n| [melonjs-math](../melonjs-math/SKILL.md) | Vectors and matrices: what mutates, what allocates, and interpolating with lerp, slerp, damp or moveTowards. |\n| [melonjs-tilemaps](../melonjs-tilemaps/SKILL.md) | Tiled maps — TMX/TSX loading, spawning entities from objects, collision layers, isometric maps. |\n| [melonjs-audio](../melonjs-audio/SKILL.md) | Sound effects, music, audio sprites, spatial audio, procedural tone and noise. |\n| [melonjs-effects-and-shaders](../melonjs-effects-and-shaders/SKILL.md) | Post effects, custom GLSL/WGSL shaders, blend modes, colour grading, screen capture. |\n| [melonjs-3d](../melonjs-3d/SKILL.md) | Anything 3D or 2.5D — `Camera3d`, meshes, instancing, `Sprite3d` billboards, `Light3d`, distance fog, glTF scenes. |\n| [melonjs-3d-assets](../melonjs-3d-assets/SKILL.md) | Loading glTF/GLB or OBJ models — materials, imported lights, instancing, and what is not supported. |\n| [melonjs-camera-and-drawing](../melonjs-camera-and-drawing/SKILL.md) | Camera follow, bounds, shake and fade, coordinate conversion, and immediate-mode shape drawing. |\n| [melonjs-ui-and-text](../melonjs-ui-and-text/SKILL.md) | HUDs, buttons, menus, drag-and-drop, `Text` and `BitmapText`, web fonts, panels. |\n| [melonjs-particles-and-trails](../melonjs-particles-and-trails/SKILL.md) | `ParticleEmitter`, burst versus stream, `referenceSpace`, and the `Trail` renderable. |\n| [melonjs-lighting](../melonjs-lighting/SKILL.md) | 2D dynamic lighting — `Light2d`, stage ambient, normal-mapped sprites. |\n| [melonjs-deployment](../melonjs-deployment/SKILL.md) | Scaffolding a project, building, browser support and transpiling, native and mini-game platforms. |\n| [melonjs-loading-assets](../melonjs-loading-assets/SKILL.md) | Resource descriptors, the asset type list, base URLs, cross-origin, progress events, unloading. |\n| [melonjs-renderer-backends](../melonjs-renderer-backends/SKILL.md) | A feature works on one machine but not another; choosing or pinning a backend; degrading gracefully. |\n| [melonjs-display-and-scaling](../melonjs-display-and-scaling/SKILL.md) | Blurry, soft or cropped output; the design resolution, how the canvas fills its page, and pixel-perfect integer scaling. |\n| [melonjs-events](../melonjs-events/SKILL.md) | The event bus, per-frame and lifecycle events, and subscribing without leaking. |\n| [melonjs-performance](../melonjs-performance/SKILL.md) | Dropped frames, draw-call counts, pooling, batching, culling, baking, profiling. |\n| [melonjs-plugins](../melonjs-plugins/SKILL.md) | Registering the debug, Spine or Tiled-inflate plugins, physics adapters, or writing your own plugin. |\n\n\n## Escape hatch: the full API index\n\nThese skills are hand-written and deliberately partial — they cover the paths\npeople actually take and the mistakes those paths invite. When the task needs\nsomething they do not mention, do **not** infer an API from the shape of the\nengine. Fetch the generated index instead:\n\n<https://melonjs.github.io/melonJS/llms.txt>\n\nIt is regenerated on every docs build from the same TSDoc comments the reference\npages render, so it is always current for `master`. It lists every exported\nclass, function, interface, type and namespace with a one-line summary and a\nlink to its page — fetch the pages you need from there.\n\nTwo things it gives you that this collection cannot:\n\n- **Members.** The skills name classes; `llms.txt` links to the page listing\n  every method, property, default and `@since` tag on them.\n- **Deprecations.** Entries marked `**deprecated**` are exactly the APIs that a\n  model trained on older melonJS material will reach for. If something you were\n  about to write appears with that marker, follow the page for the replacement.\n\nIf a class, function, namespace or type is absent from `llms.txt`, it is absent\nfrom the engine's public API — that is the useful negative answer, and it is\nworth acting on rather than writing code around something that does not exist.\nThe index lists top-level exports only, so a *member* missing from it proves\nnothing: follow the owning class's page to check that.\n\n## The three rules that catch everyone\n\nWhatever the task, these produce code that runs and is wrong:\n\n1. **`await app.init()` is mandatory** since 20.0. The constructor builds the\n   world but not the renderer: a plain renderable can still be added and simply\n   never draws, while `new Sprite(...)` / `new Text(...)` throw a `TypeError`\n   on the global `game`, which is unset until the first `init()` resolves.\n   Neither symptom names the missing call.\n2. **`addChild(child, z)` is the only way to set draw order.** `renderable.z`\n   does not exist; setting `depth` before `addChild` is overwritten, and setting\n   it after does not resort (except under `Camera3d`).\n3. **`isKinematic` defaults to `true`**, which silently excludes a renderable\n   from pointer events and from the physics broadphase. Plain `Renderable` and\n   `Sprite` subclasses need `this.isKinematic = false`.\n\n## Companion packages\n\nmelonJS ships optional packages alongside the engine. Reach for these rather\nthan reimplementing what they cover — and note the physics adapters and the\nTiled inflate plugin are *required* for their use cases, not merely convenient.\n\n| Package | Use when |\n|---|---|\n| [`@melonjs/planck-adapter`](https://www.npmjs.com/package/@melonjs/planck-adapter) | You want full rigid-body dynamics — stacking, joints, realistic restitution. Pass it as the Application's `physic` setting; the portable `raycast` and `queryAABB` calls work unchanged across adapters (`querySphere` and `raycast3d` are built-in-adapter only). |\n| [`@melonjs/matter-adapter`](https://www.npmjs.com/package/@melonjs/matter-adapter) | Same, backed by matter-js. Choose one adapter per game. |\n| [`@melonjs/tiled-inflate-plugin`](https://www.npmjs.com/package/@melonjs/tiled-inflate-plugin) | **Required** to load gzip-, zlib- or zstd-compressed Tiled maps. Without it, parsing such a map throws `No inflate function set`. |\n| [`@melonjs/spine-plugin`](https://www.npmjs.com/package/@melonjs/spine-plugin) | Skeletal animation authored in Spine, as a melonJS renderable. |\n| [`@melonjs/debug-plugin`](https://www.npmjs.com/package/@melonjs/debug-plugin) | An in-game debug panel — FPS, draw counts, collision-shape and bounds overlays. Register it after `app.init()`. |\n| [`@melonjs/capacitor-plugin`](https://www.npmjs.com/package/@melonjs/capacitor-plugin) | Wrapping a game as a native iOS/Android app: it bridges Capacitor's lifecycle — pause/resume, the hardware back button, orientation lock, splash screen. |\n| [`create-melonjs`](https://www.npmjs.com/package/create-melonjs) | Scaffolding a new project — start here rather than assembling a build by hand. |\n\nPlugins are registered **after** `init()`, into a global registry:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { DebugPanelPlugin } from \"@melonjs/debug-plugin\";\n\nawait app.init();\nplugin.register(DebugPanelPlugin, \"debugPanel\");\n```\n\nPhysics adapters are different — they are an `Application` setting, not a\nplugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    // gravity is in pixels/s²; pixelsPerMeter converts it to the adapter's\n    // metres. Both are shown at their defaults; subSteps defaults to 1\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },\n        pixelsPerMeter: 32,\n        subSteps: 2,\n    }),\n});\nawait app.init();\n```\n\n`melonjs-plugins` covers the registry, the version gate and writing your own.\n\n## Prefer built-in features\n\nmelonJS covers a lot that is easy to reimplement badly. Before hand-rolling,\ncheck for: `ParticleEmitter` and `Trail`, `Tween` with easing, the built-in\n`ShaderEffect` presets (vignette, scanline, chromatic aberration, glow, dissolve\nand more), `Light2d` / `Light3d`, `UIBaseElement` / `UISpriteElement` /\n`UITextButton`, `NineSliceSprite`, `TextureAtlas` (TexturePacker, ShoeBox,\nAseprite), `BitmapText`, `CanvasRenderTarget` for bake-once drawing,\n`NoiseTexture2d`, `viewport.shake` / `fadeIn` / `fadeOut`, and `save` for\nlocalStorage-backed persistence.\n","frontmatter":{"name":"melonjs","description":"Use this skill FIRST for any melonJS task; it routes to the right specialised skill and lists the companion packages. melonJS is an open-source 2D, 2.5D and 3D HTML5 game engine running on WebGPU, WebGL 2 or Canvas. Covers Application setup, the scene graph, renderables, sprites, tilemaps, physics, audio, particles, shaders, and the 3D tier — perspective cameras, meshes and glTF scenes. Triggers on: melonjs, melonJS, me.game, Application, app.init, app.world, Renderable, Sprite, Stage, state.change, Camera2d, Camera3d, Mesh, tilemap, Tiled, TMX, ParticleEmitter, ShaderEffect, how do I make a game, HTML5 game engine.","license":"MIT"},"isInternal":false,"tokens":2874,"sizeBytes":11054},{"name":".cursorrules","path":".cursorrules","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursorrules","title":"Cursor IDE Native Rules","category":"cursor-rule","format":"markdown","content":"# Cursor IDE Native Rules\n\nPath: `.cursorrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursorrules)","tokens":300,"sizeBytes":146},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/CLAUDE.md)","tokens":300,"sizeBytes":156},{"name":"GEMINI.md","path":"GEMINI.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/GEMINI.md","title":"Gemini Agent & Antigravity Instructions","category":"gemini-rule","format":"markdown","content":"# Gemini Agent & Antigravity Instructions\n\nPath: `GEMINI.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/GEMINI.md)","tokens":300,"sizeBytes":156},{"name":".windsurfrules","path":".windsurfrules","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.windsurfrules","title":"Windsurf Cascade Agent Rules","category":"windsurf-rule","format":"markdown","content":"# Windsurf Cascade Agent Rules\n\nPath: `.windsurfrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.windsurfrules)","tokens":300,"sizeBytes":155},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# GitHub Copilot Instructions\n\nPath: `.github/copilot-instructions.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.github/copilot-instructions.md)","tokens":300,"sizeBytes":188},{"name":"COPILOT.md","path":"COPILOT.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/COPILOT.md","title":"GitHub Copilot Context & Rules","category":"copilot-instructions","format":"markdown","content":"# GitHub Copilot Context & Rules\n\nPath: `COPILOT.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/COPILOT.md)","tokens":300,"sizeBytes":149},{"name":".clinerules","path":".clinerules","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.clinerules","title":"Cline Extension Native Directives","category":"cline-rule","format":"markdown","content":"# Cline Extension Native Directives\n\nPath: `.clinerules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.clinerules)","tokens":300,"sizeBytes":154},{"name":"marketplace.json","path":".claude-plugin/marketplace.json","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/marketplace.json","title":"Claude Plugin Marketplace Catalog","category":"marketplace","format":"json","content":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/marketplace.json)","tokens":300,"sizeBytes":194},{"name":"marketplace.json","path":"marketplace.json","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/marketplace.json","title":"Claude Plugin Marketplace Catalog","category":"marketplace","format":"json","content":"# Claude Plugin Marketplace Catalog\n\nPath: `marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/marketplace.json)","tokens":300,"sizeBytes":164},{"name":"plugin.json","path":".claude-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/plugin.json","title":"claude-plugin Plugin Manifest","category":"plugin-manifest","format":"json","content":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.claude-plugin/plugin.json)","tokens":300,"sizeBytes":180},{"name":"README.md","path":"packages/melonjs/skills/melonjs-20-migration/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-20-migration/README.md","title":"melonjs-20-migration Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-20-migration Documentation\n\nPath: `packages/melonjs/skills/melonjs-20-migration/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-20-migration/README.md)","tokens":300,"sizeBytes":241},{"name":"README.md","path":"packages/melonjs/skills/melonjs-3d-assets/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d-assets/README.md","title":"melonjs-3d-assets Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-3d-assets Documentation\n\nPath: `packages/melonjs/skills/melonjs-3d-assets/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d-assets/README.md)","tokens":300,"sizeBytes":232},{"name":"README.md","path":"packages/melonjs/skills/melonjs-3d/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d/README.md","title":"melonjs-3d Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-3d Documentation\n\nPath: `packages/melonjs/skills/melonjs-3d/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-3d/README.md)","tokens":300,"sizeBytes":211},{"name":"README.md","path":"packages/melonjs/skills/melonjs-audio/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-audio/README.md","title":"melonjs-audio Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-audio Documentation\n\nPath: `packages/melonjs/skills/melonjs-audio/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-audio/README.md)","tokens":300,"sizeBytes":220},{"name":"README.md","path":"packages/melonjs/skills/melonjs-camera-and-drawing/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/README.md","title":"melonjs-camera-and-drawing Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-camera-and-drawing Documentation\n\nPath: `packages/melonjs/skills/melonjs-camera-and-drawing/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/README.md)","tokens":300,"sizeBytes":259},{"name":"README.md","path":"packages/melonjs/skills/melonjs-deployment/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/README.md","title":"melonjs-deployment Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-deployment Documentation\n\nPath: `packages/melonjs/skills/melonjs-deployment/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/README.md)","tokens":300,"sizeBytes":235},{"name":"README.md","path":"packages/melonjs/skills/melonjs-display-and-scaling/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/README.md","title":"melonjs-display-and-scaling Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-display-and-scaling Documentation\n\nPath: `packages/melonjs/skills/melonjs-display-and-scaling/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/README.md)","tokens":300,"sizeBytes":262},{"name":"README.md","path":"packages/melonjs/skills/melonjs-effects-and-shaders/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-effects-and-shaders/README.md","title":"melonjs-effects-and-shaders Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-effects-and-shaders Documentation\n\nPath: `packages/melonjs/skills/melonjs-effects-and-shaders/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-effects-and-shaders/README.md)","tokens":300,"sizeBytes":262},{"name":"README.md","path":"packages/melonjs/skills/melonjs-events/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/README.md","title":"melonjs-events Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-events Documentation\n\nPath: `packages/melonjs/skills/melonjs-events/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/README.md)","tokens":300,"sizeBytes":223},{"name":"README.md","path":"packages/melonjs/skills/melonjs-getting-started/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-getting-started/README.md","title":"melonjs-getting-started Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-getting-started Documentation\n\nPath: `packages/melonjs/skills/melonjs-getting-started/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-getting-started/README.md)","tokens":300,"sizeBytes":250},{"name":"README.md","path":"packages/melonjs/skills/melonjs-input/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-input/README.md","title":"melonjs-input Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-input Documentation\n\nPath: `packages/melonjs/skills/melonjs-input/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-input/README.md)","tokens":300,"sizeBytes":220},{"name":"README.md","path":"packages/melonjs/skills/melonjs-lighting/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/README.md","title":"melonjs-lighting Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-lighting Documentation\n\nPath: `packages/melonjs/skills/melonjs-lighting/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/README.md)","tokens":300,"sizeBytes":229},{"name":"README.md","path":"packages/melonjs/skills/melonjs-loading-assets/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/README.md","title":"melonjs-loading-assets Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-loading-assets Documentation\n\nPath: `packages/melonjs/skills/melonjs-loading-assets/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/README.md)","tokens":300,"sizeBytes":247},{"name":"README.md","path":"packages/melonjs/skills/melonjs-math/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/README.md","title":"melonjs-math Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-math Documentation\n\nPath: `packages/melonjs/skills/melonjs-math/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/README.md)","tokens":300,"sizeBytes":217},{"name":"README.md","path":"packages/melonjs/skills/melonjs-particles-and-trails/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/README.md","title":"melonjs-particles-and-trails Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-particles-and-trails Documentation\n\nPath: `packages/melonjs/skills/melonjs-particles-and-trails/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/README.md)","tokens":300,"sizeBytes":265},{"name":"README.md","path":"packages/melonjs/skills/melonjs-performance/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/README.md","title":"melonjs-performance Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-performance Documentation\n\nPath: `packages/melonjs/skills/melonjs-performance/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/README.md)","tokens":300,"sizeBytes":238},{"name":"README.md","path":"packages/melonjs/skills/melonjs-physics/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-physics/README.md","title":"melonjs-physics Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-physics Documentation\n\nPath: `packages/melonjs/skills/melonjs-physics/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-physics/README.md)","tokens":300,"sizeBytes":226},{"name":"README.md","path":"packages/melonjs/skills/melonjs-plugins/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/README.md","title":"melonjs-plugins Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-plugins Documentation\n\nPath: `packages/melonjs/skills/melonjs-plugins/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/README.md)","tokens":300,"sizeBytes":226},{"name":"README.md","path":"packages/melonjs/skills/melonjs-renderables/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderables/README.md","title":"melonjs-renderables Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-renderables Documentation\n\nPath: `packages/melonjs/skills/melonjs-renderables/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderables/README.md)","tokens":300,"sizeBytes":238},{"name":"README.md","path":"packages/melonjs/skills/melonjs-renderer-backends/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/README.md","title":"melonjs-renderer-backends Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-renderer-backends Documentation\n\nPath: `packages/melonjs/skills/melonjs-renderer-backends/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/README.md)","tokens":300,"sizeBytes":256},{"name":"README.md","path":"packages/melonjs/skills/melonjs-scenes-and-state/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/README.md","title":"melonjs-scenes-and-state Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-scenes-and-state Documentation\n\nPath: `packages/melonjs/skills/melonjs-scenes-and-state/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/README.md)","tokens":300,"sizeBytes":253},{"name":"README.md","path":"packages/melonjs/skills/melonjs-sprites-and-animation/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-sprites-and-animation/README.md","title":"melonjs-sprites-and-animation Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-sprites-and-animation Documentation\n\nPath: `packages/melonjs/skills/melonjs-sprites-and-animation/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-sprites-and-animation/README.md)","tokens":300,"sizeBytes":268},{"name":"README.md","path":"packages/melonjs/skills/melonjs-tilemaps/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-tilemaps/README.md","title":"melonjs-tilemaps Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-tilemaps Documentation\n\nPath: `packages/melonjs/skills/melonjs-tilemaps/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-tilemaps/README.md)","tokens":300,"sizeBytes":229},{"name":"README.md","path":"packages/melonjs/skills/melonjs-ui-and-text/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/README.md","title":"melonjs-ui-and-text Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs-ui-and-text Documentation\n\nPath: `packages/melonjs/skills/melonjs-ui-and-text/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/README.md)","tokens":300,"sizeBytes":238},{"name":"README.md","path":"packages/melonjs/skills/melonjs/README.md","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs/README.md","title":"melonjs Documentation","category":"plugin-manifest","format":"markdown","content":"# melonjs Documentation\n\nPath: `packages/melonjs/skills/melonjs/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs/README.md)","tokens":300,"sizeBytes":202},{"name":"plugin.json","path":"plugin.json","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/plugin.json","title":"Plugin Plugin Manifest","category":"plugin-manifest","format":"json","content":"# Plugin Plugin Manifest\n\nPath: `plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/plugin.json)","tokens":300,"sizeBytes":143},{"name":"mcp.json","path":".cursor/mcp.json","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursor/mcp.json","title":"Model Context Protocol (MCP) Configuration","category":"mcp-config","format":"json","content":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursor/mcp.json)","tokens":300,"sizeBytes":173},{"name":"mcp.json","path":"mcp.json","rawUrl":"https://raw.githubusercontent.com/melonjs/melonJS/HEAD/mcp.json","title":"Model Context Protocol (MCP) Configuration","category":"mcp-config","format":"json","content":"# Model Context Protocol (MCP) Configuration\n\nPath: `mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/mcp.json)","tokens":300,"sizeBytes":157}],"systemPromptSnippet":"<yakaai_skills repo=\"melonjs/melonJS\">\n<!-- File: AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# AI Agent Protocol & Instructions\n\nPath: `AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/AGENTS.md)\n\n<!-- File: llms.txt (Tokens: ~300 | Category: root-instruction) -->\n# LLM Index & Context Digest\n\nPath: `llms.txt`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/llms.txt)\n\n<!-- File: packages/melonjs/skills/melonjs-20-migration/SKILL.md (Tokens: ~1964 | Category: anthropic-skill) -->\n---\nname: melonjs-20-migration\ndescription: \"Use this skill when writing melonJS code that may be based on pre-20.x knowledge, when migrating a game from melonJS 19.x or earlier, or when an API you expected does not exist. Covers the mandatory async Application.init(), the WebGL 2 baseline and WebGPU backend, renderable post effects replacing the shader property, and the deprecated exports still present. Triggers on: migration, upgrade, melonJS 19, me.game, me.video.init, renderable.shader, Compositor, CanvasTexture, me.Math, deprecated.\"\nlicense: MIT\n---\n\n# Migrating to melonJS 20.x\n\n> What changed in melonJS 20.x, and which older APIs are gone, deprecated, or\n> renamed. Read this before writing melonJS code from memory — much of what is\n> published about melonJS predates 20.0.\n\n**Why this skill exists:** most melonJS material in circulation describes 19.x or\nearlier. Code written from that knowledge looks correct and fails at runtime.\nThe items below are ordered by how likely they are to be reproduced from memory.\n\n## 1. `Application.init()` is mandatory and asynchronous\n\nSince 20.0, constructing an `Application` no longer initialises it.\n\n```js\n// ✗ pre-20.x — the constructor called init(width, height, options) for you\nconst app = new Application(800, 600, { parent: \"screen\" });\napp.world.addChild(sprite);          // added, but nothing ever renders\n\n// ✓ 20.x\nconst app = new Application(800, 600, { parent: \"screen\" });\nawait app.init();                    // REQUIRED\napp.world.addChild(sprite);\n```\n\n`init()` is async because acquiring a WebGPU device is async. There is no\nsynchronous alternative, and no automatic fallback that papers over the missing\ncall. Nothing that then breaks names it: the constructor still builds\n`app.world`, so children can be added and simply never draw — no canvas is\nappended, the app never subscribes to the frame tick, and `app.renderer` /\n`app.viewport` stay `undefined` — while constructing a `Sprite` or `Text` first\nthrows a `TypeError` on the unset global `game` instead.\n\nThe signature changed too: 19.x took `init(width, height, options)`, 20.x takes\nnone, and anything passed is ignored (settings come from the constructor).\n\n**`video.init(...)` no longer exists.** The `video` module now exports only the\nfour renderer constants (`AUTO`, `CANVAS`, `WEBGL`, `WEBGPU`) — there is no\n`init` on it to call. Any code shaped like `me.video.init(800, 600, {...})` is\nfrom a removed API. Build an `Application` and await `init()`.\n\nNote that `import * as me from \"melonjs\"` still works, so `me.Sprite` style is\nfine. It is the *globals bootstrap* that is gone, not the namespace import.\n\n## 2. Post effects replaced the `shader` property\n\n`renderable.shader = …` is **deprecated since 19.2.0**. Use the post-effect API:\n\n```js\n// ✗ deprecated\nmySprite.shader = new ShaderEffect(app.renderer, glslBody);\n\n// ✓\nmySprite.addPostEffect(new ShaderEffect(app.renderer, glslBody));\n```\n\nRelated trap: **`removePostEffect()` destroys the effect** — and so does\nassigning `renderable.shader`, which destroys whatever it replaces. Only an\neffect carrying `shared = true` is spared. To turn one off temporarily, toggle\n`effect.enabled` instead: remove-and-re-add hands you back an object whose GPU\nresources have already been freed.\n\n## 3. WebGL 2 is the baseline; WebGPU is the default attempt\n\n`video.AUTO` now tries **WebGPU → WebGL 2 → Canvas**. WebGL 1 is gone.\n\nConsequences:\n\n- Which backend runs is a runtime fact. Do not write code that assumes WebGL.\n- Custom shaders can carry both GLSL and WGSL in one asset, so one effect runs\n  on either GPU backend.\n- Features needing a programmable pipeline are inert on Canvas and warn once\n  rather than throwing.\n\n## 4. Deprecated exports still present\n\nThese resolve but should not be used in new code:\n\n| deprecated | since | use instead |\n|---|---|---|\n| `CanvasTexture` | 17.1.0 | `CanvasRenderTarget` |\n| `Compositor` | 18.1.0 | `WebGLBatcher` |\n| `PrimitiveCompositor` | 18.1.0 | `PrimitiveBatcher` |\n| `QuadCompositor` | 18.1.0 | `QuadBatcher` |\n| `Math` (capitalised) | 18.0.0 | `math` (lowercase) |\n| `device.requestFullscreen` / `exitFullscreen` | 19.7.0 | `app.requestFullscreen()` / `app.exitFullscreen()` |\n| `renderable.shader = …` | 19.2.0 | `addPostEffect()` |\n| `Entity` | 18.1.0 | `Sprite` / `Renderable` + a `Body` |\n| `loader.onload` / `onProgress` / `onError` | 18.2.0, **removed 20.3** | the `LOADER_*` events, or `preload(assets, onloadcb)` |\n| `response.overlap` / `overlapN` / `overlapV` | — | `response.depth` / `response.normal` |\n| `setLineWidth()` | 17.3.0 | the `lineWidth` property |\n\nTwo of these deserve emphasis:\n\n- **`Entity`** is still exported, so code using it compiles and runs while being\n  the wrong shape for 20.x. Compose a `Sprite`/`Renderable` with a `Body`\n  instead.\n- **The capitalised `Math`** shadows the global `Math` if imported carelessly.\n  `me.Math.random()` from older docs resolves to the deprecated re-export.\n\n## 5. The global `game` is not the modern shape\n\n`game` is still exported, but since 20.0 it names the most recently\n**initialized** `Application` and is `undefined` until the first `init()`\nresolves — so anything reading it at module scope gets `undefined`. Take the\n`Application` instance as a parameter instead: `Stage#onResetEvent` and\n`onDestroyEvent` are handed it, and any renderable can reach it through\n`parentApp`.\n\n## 6. Node and SSR\n\nmelonJS imports cleanly under Node — useful for isomorphic apps and for tooling.\nIt has **no runtime dependencies** as of 20.3. Do not add polyfills for\n`globalThis`, `String.trimStart` or `String.trimEnd`: the published bundle\ntargets ES2022, so every environment that can parse it already has them.\n\n## 7. Browser support\n\nThe published bundle targets **ES2022** and uses private class members. A browser\nthat predates that cannot parse the file at all — it fails at load, not when a\nfeature is used. Transpile if you need older browsers, and note that transpiling\nhandles *syntax* only: built-in methods need separate polyfills, which a\nbundler's `target` setting will never add.\n\n## The shipped examples are not all current\n\nThe examples in this repository are the best reference for how melonJS is\nassembled, but a few contain stale comments or legacy calls. Do not learn these\nfrom them:\n\n- `webgpu/` has a comment claiming *\"`video.AUTO` never selects the WebGPU\n  backend\"*. That is pre-20 leftover — AUTO tries WebGPU **first**. The code is\n  right; the comment is wrong.\n- Several examples assign `viewport.shader = …` / `sprite.shader = …`, which is\n  deprecated. `addPostEffect()` is current.\n- Five files set `this.z = Number.POSITIVE_INFINITY` on a HUD. That property\n  does not exist; those HUDs draw on top only because they are added last.\n- `platformer/entities/player.ts` reads `response.overlapV`, the deprecated\n  field. Use `response.depth` / `response.normal`.\n- Particle speeds tuned before 20.2 look wrong now: the particle transform fix\n  changed how far a burst travels, and emitters were retuned upward to match.\n\n## Checklist when code \"should work\" but doesn't\n\n1. Is `await app.init()` there?\n2. Is the code assuming WebGL when it might be running on Canvas or WebGPU?\n3. Is it using `renderable.shader =` instead of `addPostEffect()`?\n4. Is it importing a deprecated symbol from the table above?\n5. Is it referencing a global `game`?\n\n## Related skills\n\n- `melonjs-getting-started` — the correct 20.x bootstrap in full\n- `melonjs-renderables` — post effects, pointer events and custom draw code\n\n<!-- File: packages/melonjs/skills/melonjs-3d-assets/SKILL.md (Tokens: ~6792 | Category: anthropic-skill) -->\n---\nname: melonjs-3d-assets\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Loading 3D assets\n\n> glTF/GLB is the primary path for 3D content, and it goes through the **level\n> director** rather than the world directly: `level.load(\"scene\")`, the same call\n> a Tiled map uses. OBJ/MTL is the simpler path for a single static model.\n\n## Loading a glTF/GLB scene\n\n```js\nawait loader.preload([\n    { name: \"diorama\", type: \"glb\", src: \"data/scenes/diorama.glb\" },\n]);\n\nlevel.load(\"diorama\", { scale: 50 });\n```\n\n`type: \"gltf\"` (JSON plus external buffers) and `type: \"glb\"` (single binary)\nare both loader types. A self-contained GLB is the better shipping format — one\nrequest, nothing to resolve relatively.\n\nThe scene loads under whatever camera the stage has; view it under a `Camera3d`\nor it renders flat. See `melonjs-3d` for the camera.\n\n### `level.load` options for glTF/GLB\n\n| option | default | what it does |\n|---|---|---|\n| `scale` | `1` | pixels per glTF unit, applied to the whole scene. Blender's metre-scale export usually needs 20–100. |\n| `container` | `game.world` | where the nodes are added |\n| `onLoaded` | `game.onLevelLoaded` | called with the **level id**, not the scene |\n| `async` | `false` | return a promise that settles once the scene is in the world, instead of a boolean |\n| `rightHanded` | `true` | see below |\n| `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |\n| `lightIntensityScale` | — | keep authored intensity ratios instead of normalising every light to 1 |\n\nThose `game.` defaults are the engine's, not a recommendation. `game` is\nwhichever `Application` finished `init()` last, so **pass `container`\nexplicitly** from a `Stage`, where the right one is in your hand:\n\n```js\nlevel.load(\"sceneId\", { container: this.app.world, scale: 40 });\n```\n| `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |\n| `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |\n| `shadowOpacity` | `0.45` | how dark the blobs are |\n| `shadowScale` | `1` | blob footprint multiplier; `0` or less hides them |\n| `shadowOffset` | `0` | slide them along the light, in blob radii; needs `shadowGroundY`, which is the only way a scene load can name its floor |\n| `shadowStretch` | `1` | lengthen them along it, clamped to 3 |\n| `shadowDirectionX` / `shadowDirectionZ` | `0` | the ground direction they are cast along |\n| `shadowGroundNormal` | world up | up normal of the floor, for a scene that is not level |\n| `shadowLight` | — | a `Light3d` to take the direction from, re-read every draw |\n\n`shadowGroundPoint` is **not** on that list, and a scene load drops it without\na warning. One floor POINT shared by every part of a scene would stack the\nwhole thing's blobs on a single spot, which is nonsense where one floor\nHEIGHT is perfectly sensible. Ground that is not flat is a per-object answer:\nset it on the parts you want once the load has finished.\n\n`onLoaded` receives the level id — it is a \"done\" signal, not a handle on the\nscene. You need it, or `async`: with the game loop running, `level.load` stops\nthe loop and defers the actual load to a timer, so by default it returns\n*before* anything is in the world.\n\n```js\nawait level.load(\"diorama\", { scale: 50, async: true });\n// the scene is in the world here\n```\n\nNote `await level.load(\"diorama\")` without the flag does not await the load — the\ncall returns a boolean, and `await true` resolves immediately. To get at what was\nloaded, load into a container you own, or look the nodes up by their authored\nnames:\n\n```js\nlevel.load(\"diorama\", { scale: 50, onLoaded: () => {\n    const sun = app.world.getChildByName(\"Sun\")[0];   // note: returns an array\n    sun.intensity = 2;\n}});\n```\n\n### The same options from a Tiled `Trigger`\n\nA `Trigger` authored in Tiled forwards these to `level.load()`, so a trigger that\nwalks the player into a glTF level carries the scene's settings as Tiled\nproperties:\n\n```js\nworld.addChild(new Trigger(x, y, {\n    shapes: [new Rect(0, 0, 100, 100)],\n    to: \"diorama\",              // the level id — `to`, not `level`\n    scale: 50,\n    rightHanded: false,\n    castGroundShadow: false,\n}));\n```\n\nEvery `level.load` option above travels, including the whole ground-shadow set,\nand so do `container`, `onLoaded`, `flatten` and `setViewportBounds`. The list is\n**derived** from `level.loadOptions` rather than restated, so an option added to\na level format arrives here for free. It used to be hand-written and had fallen\nthree releases behind the glTF path (#1649).\n\n**`async` is deliberately excluded.** A trigger with a `color` + `duration`\nsets it itself, because it needs the promise to sequence its fade and reveal; a\ntrigger without one loads fire-and-forget and never sets it. Either way an\nauthored `async` property is ignored rather than honoured.\n\n## `rightHanded` — and the default that differs between the two APIs\n\nglTF is authored **Y-up, right-handed**; melonJS is **Y-down, +Z forward**.\nThere are two ways to bridge that, and they are not equivalent:\n\n- a **rotation** — preserves chirality, matches the authoring tool\n- a **mirror** (negate Y only) — flips the scene left/right\n\n`level.load` defaults `rightHanded: true`, so a scene loaded through the level\ndirector comes in correctly. **The `Mesh` constructor defaults it to `false`**,\nso glTF or OBJ geometry you feed to a `Mesh` yourself comes in mirrored unless\nyou pass `rightHanded: true`. A model that is subtly wrong — text backwards, a\ncharacter's gear on the wrong side — is this and nothing else.\n\nDo not add a flip of your own on top; two corrections cancel back to wrong.\n\n## Materials — what actually arrives\n\nThe importer covers a deliberate subset:\n\n- `pbrMetallicRoughness` **base colour texture and factor**\n- metallic/roughness **factors** converted to the engine's specular model\n- `emissiveFactor`, including `KHR_materials_emissive_strength`\n- `KHR_materials_unlit` — honoured, for baked/stylised workflows\n- `alphaMode: \"MASK\"` — the cutout threshold is applied\n- `doubleSided`\n\nNot imported: **normal maps**, metallic/roughness *textures*, occlusion maps,\nmorph targets, `KHR_texture_transform`. Nothing errors — the material simply\narrives without them, which reads as \"flatter than the preview in my authoring\ntool\". Bake what you need into the base colour.\n\n**Draco compression is the exception that does error.** Its geometry sits in\naccessors with no buffer view, which the parser rejects outright, so the whole\nasset fails to load. Export uncompressed.\n\n`textureFilter` on the Application settings (`\"nearest\"` / `\"linear\"` /\n`\"auto\"`) drives sampling; glTF materials that declare their own sampler carry\nit through. Pixel-art models want `\"nearest\"`.\n\n### `baseColorFactor` is LINEAR; a `tint` is sRGB\n**Pass the factor as a setting and the encoding is done for you.** `Mesh`\ntakes `baseColorFactor` — `[r, g, b]` in 0..1, linear, which is the form the\nloader hands back — so a mesh built from a node passes the node's own field\nstraight through:\n\n```js\nconst node = loader.getGLTF(\"palm\").nodes[0];\nconst mesh = new Mesh(x, y, {\n    vertices: node.vertices, normals: node.normals,\n    uvs: node.uvs, indices: node.indices, normalize: false,\n    baseColorFactor: node.baseColorFactor,   // encoded, not scaled\n});\n```\n\nThe setting's type admits `undefined`, because the loader declares the field\noptional and that line has to compile under `exactOptionalPropertyTypes`.\n`Sprite3d` forwards it too, and `mesh.setBaseColorFactor(f)` applies one after\nconstruction. For an ordinary sRGB colour use `setTint` instead — the rest of\nthis section is why the two are different.\n\n\nThe loader handles this for you. It only bites when you build meshes yourself\nfrom `loader.getGLTF(name).nodes` — which is what you do for per-node control,\nsince there is no public per-node `Mesh` factory — and reach for the obvious\n`* 255`:\n\n```js\n// WRONG — every untextured material renders far too dark\nmesh.tint.setColor(f[0] * 255, f[1] * 255, f[2] * 255, 1);\n```\n\nglTF defines `pbrMetallicRoughness.baseColorFactor` in **linear** space (spec\n3.9.2), while a melonJS `tint` is 8-bit **sRGB** — the same space as a CSS\ncolour or a texel out of a PNG. They are not the same number, and the gap is\nwidest exactly where art tends to live:\n\n| linear | `* 255` | correct | \n|---|---|---|\n| 0.05 | 13 | 63 |\n| 0.20 | 51 | 124 |\n| 0.42 | 107 | 173 |\n| 0.72 | 184 | 221 |\n| 1.00 | 255 | 255 |\n\n`Color.setLinear` does the encoding for you — it is the linear-space sibling of\n`setFloat`, which takes the same `0..1` range but treats it as already-sRGB:\n\n```js\nmesh.tint.setLinear(...node.baseColorFactor);   // encodes; what you want\nmesh.tint.setFloat(...node.baseColorFactor);    // does NOT encode; too dark\n```\n\nIt clamps, so the slightly out-of-range factors some exporters emit cannot turn\ninto `NaN`, and it keeps the encoded value as a float rather than rounding\nthrough 8 bits.\n\nMeasured on a scene whose every material went through `* 255`: mean frame luma\n85.5 against 147.8 once encoded, with each channel sitting 60 to 73 counts low.\n\n**Fix it early.** It is nearly invisible as a bug, because the scene still\nlooks coherent — just moody — so lighting and bloom thresholds get tuned\nagainst the wrong values. Correcting the encoding afterwards blows the frame\nout and forces a lighting retune, which is a much bigger job than the encode.\n\nThis applies only to factors coming out of glTF. Colours **you** author for the\nscreen, a HUD tint or a glow, are already sRGB: `* 255` is right for those.\n\n## Lights\n\nAuthored `KHR_lights_punctual` lights — sun, point, spot — become `Light3d`\nworld children, plus a soft ambient fill, and each keeps its authored name.\nA scene carrying any of them renders through the **lit** path automatically.\n\nglTF stores physical units — lux for suns, candela for lamps — and a Blender\ndaylight sun is 1000+, which blows out the engine's stylised shading. So every\nlight is normalised to intensity 1 by default. To keep the *ratios* the artist\nset, scale instead of normalising:\n\n```js\n// a 1000-lux sun lands at 1, a 500-lux fill lands at 0.5\nlevel.load(\"diorama\", { scale: 50, lightIntensityScale: 0.001 });\n```\n\nPass `lights: false` to manage lighting yourself.\n\n## Animation\n\nNode-TRS animation **is** supported: translation, rotation and scale channels,\nthe rigid hierarchical animation used by blocky/low-poly character packs. An\nanimated asset instantiates as a single `GLTFModel` keeping the node hierarchy\nintact, with an API deliberately mirroring `Sprite`. The model carries the asset\nname, which is how you get hold of it after loading:\n\n```js\nconst model = app.world.getChildByName(\"hero\")[0];   // the GLTFModel\n\nmodel.getAnimationNames();                           // [\"idle\", \"walk\", ...]\nmodel.setCurrentAnimation(\"walk\", { loop: true, speed: 1, onComplete });\nmodel.setCurrentAnimation(\"jump\", { next: \"idle\" }); // chain\nmodel.isCurrentAnimation(\"walk\");\nmodel.play();\nmodel.pause();\n```\n\n`speed` is a playback multiplier (1 = authored speed), not a frame delay. Only\nan explicit `loop: false` stops looping. `setCurrentAnimation` **throws** on a\nclip name the asset does not define — check `getAnimationNames()` first.\n\n### Placing and moving a model\n\nA `GLTFModel` is placed like any other renderable — `pos`, `depth`, `rotate`\nand `scale` move the **whole rig**, and compose with whatever the active clip\nis doing, so a walk cycle plays wherever the character stands:\n\n```js\nconst boat = new me.GLTFModel(me.loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nboat.setCurrentAnimation(\"paddle\", { loop: true });\napp.world.addChild(boat);\n\nboat.pos.set(steerX, waterLevel);\nboat.depth = travelled;\nboat.rotate(lean - lastLean, AXIS_Z);   // `rotate` is RELATIVE — feed it the delta\nboat.animationspeed = 0.85 + 0.9 * pace; // one authored tempo, many run speeds\n```\n\nConstructing one directly from `loader.getGLTF(name)` is fine and is the way to\ngo when the model is a game object rather than a scene — `level.load` is for\nloading a whole scene into the world.\n\n**Vertex skinning is out of scope** — `JOINTS_0` / `WEIGHTS_0` are not read, and\nneither are morph targets. A smoothly-deforming character will not deform. The\noptions are: rig it hierarchically instead (separate meshes parented into a\nnode tree), use `Sprite3d` billboards, or use the Spine plugin for 2D skeletal\nwork.\n\n## Instancing\n\nA node using `EXT_mesh_gpu_instancing` loads as an `InstancedMesh` — one draw\ncall for every copy, which is the difference between a hundred trees and a\nhundred thousand. Export with instancing enabled where your tool supports it.\nSparse accessors on the instancing attributes are rejected with a clear error\nrather than silently misplacing every instance.\n\n## Ground shadows\n\n```js\nlevel.load(\"diorama\", { scale: 50, castGroundShadow: true, shadowGroundY: 0 });\n```\n\n`castGroundShadow` is tri-state: set it and it overrides the application setting\nfor this scene in both directions; omit it and the scene inherits — and that\nsetting is `true` by default, so shadows are already on. As a scene-wide opt-in\nit skips nodes with no vertical extent — a ground plane is exactly that, and\nshadowing it with itself smears a blob across the whole floor. Blob shadows need\na GPU backend and a `Camera3d`; nothing is drawn on the 2D-camera or Canvas\npaths.\n\nOmit `shadowGroundY` and each blob sits at its own object's base at full\nstrength, which is right for props already resting on the ground. Set it when\nthings jump or fly, so the shadow stays on the floor and shrinks with height.\n\nA blob sits on its caster's x/z by default, so a wide flat-bottomed prop hides\nits own shadow under itself. The knob for that is `shadowScale`, which widens\nthe blob until its edge shows past the prop, or `shadowOffset` plus a direction,\nwhich slides it clear. **Not** `shadowGroundY`: raising that floats the blob up\nover the object as a dark ring instead of sliding it out, because it is for\nthings that leave the ground, not a visibility control.\n\nFor a light that is not overhead, name one: `shadowLight` takes the direction\nfrom a `Light3d` and is re-read every draw, so a day or night cycle carries\nevery shadow in the scene with it. Nothing is inferred — a scene with no named\nlight and no `shadowDirectionX`/`Z` gets no direction, because a scene with\nseveral lights has no non-arbitrary answer. `shadowStretch` lengthens the blob\nalong that direction and fades it as it pulls; `shadowGroundNormal` lays it on\na floor that is not level. All of these apply to an `InstancedMesh` too, as one\nvalue for the whole set.\n\n## OBJ/MTL\n\nA different shape entirely: OBJ produces raw geometry you hand to a `Mesh`,\nnot a scene. Preload the two files and name them in the `Mesh` settings — the\n`model` and `material` names are independent, the same name for both is just a\nconvention:\n\n```js\nawait loader.preload([\n    { name: \"fox\", type: \"obj\", src: \"models/fox.obj\" },\n    { name: \"fox\", type: \"mtl\", src: \"models/fox.mtl\" },\n]);\n\nconst mesh = new Mesh(400, 300, {\n    model: \"fox\",\n    material: \"fox\",        // texture resolved from the material's map_Kd\n    width: 200,\n    height: 200,\n    rightHanded: true,      // the Mesh default is false — see above\n});\nworld.addChild(mesh);\n```\n\n**The MTL loader fetches the textures it references itself** — `map_Kd`,\n`map_d` and `map_bump` are resolved relative to the `.mtl` and preloaded for\nyou, so there is no image entry in the manifest above. Passing `texture:`\nexplicitly overrides that and pins *one* binding over the whole model, which on\na multi-material model suppresses the per-material texture split; leave it out\nunless you mean that.\n\nMTL contributes `Kd` (diffuse tint), `d` / `Tr` (opacity), `Ke` (emissive),\n`Ks` + `Ns` (specular highlight), `map_Kd` (diffuse texture), `map_d`\n(per-texel opacity) and `map_bump` / `bump` / `norm` (tangent-space normal\nmap — needs `lit: true`, and the tangent frame is derived per fragment, so the\nmodel needs no tangent attribute). All three normal-map spellings are read as\nnormal maps even though the format specifies two of them as height maps, which\nis what every exporter actually writes. Specular *maps* (`map_Ks`) and the PBR\nextension's `map_Pr` / `map_Pm` are not supported and warn — the `Pr` / `Pm`\n*scalars* are read and approximated onto the specular terms, the maps would\nneed a real PBR model.\n\n`Ka` (ambient) and `illum` (illumination model) are read onto the material and\nreadable from `loader.getMTL(name)`, but take no part in shading. That is\ndeliberate rather than pending: `Ka` predates scene-wide ambient light and would\nfight `Stage.ambientLightingColor`, and `illum` enumerates fixed-function\nbehaviours this renderer has no equivalent for. In particular an `illum` of 0 or\n1 nominally means \"no specular\", and the engine does **not** honour that — `Ns`\nstays the only gate, so a model that renders correctly today is not restyled by\na field its exporter wrote inconsistently.\n\n`loader.getOBJ(name)` and `loader.getMTL(name)` return the parsed data if you\nwant it directly. OBJ has no scene graph, no lights, no animation — use glTF for\nanything beyond a single static model.\n\n## Authoring assets for the engine\n\nNotes from modelling props and a rigged character against this loader. They are\nBlender-specific in the API details, general in the traps.\n\n### One clip means one action\n\nglTF names an animation after the **action** it came from, so three objects\neach carrying their own action export as three separate clips — and a game can\nonly play one clip at a time, so the other two never move. Put every animated\nobject in **one action**, using slots (Blender 4.4+):\n\n```python\nshared = bpy.data.actions.new(\"paddle\")\nfor ob in (pivot, shoulder_l, shoulder_r, body):\n    ad = ob.animation_data_create()\n    ad.action = shared\n    ad.action_slot = shared.slots.new('OBJECT', ob.name)\n```\n\nExporting with the scene/whole-timeline mode does **not** merge per-object\nactions into one clip — it still emits one per action. Slots are the fix.\n\nIn Blender 5.x, `action.fcurves` no longer exists; curves live under\n`action.layers[].strips[].channelbags[].fcurves`, and a channelbag identifies\nits slot as `cb.slot.identifier` (there is no `slot_identifier`).\n\n### Never re-parent or join a rig that is mid-pose\n\nBoth parenting and joining bake the objects' **current world transforms**.\nParenting a child while the parent sits in an animated pose writes the inverse\nof that pose into `matrix_parent_inverse`, which silently cancels the animation\n— the part renders rigid while the curves still exist. Detach the action, zero\nthe rig, do the surgery, then reattach:\n\n```python\nsaved = [(o, o.animation_data.action_slot) for o in animated]\nfor ob, _ in saved:\n    ob.animation_data.action = None\n    ob.rotation_euler = (0, 0, 0)\n# ...re-parent / join here...\nfor ob, slot in saved:\n    ob.animation_data.action = act\n    ob.animation_data.action_slot = slot\n```\n\nA joined mesh also inherits the **active** object's scale, so joining into a\nbox that was scaled to `(0.8, 2.0, 0.1)` leaves a node with that non-uniform\nscale and geometry divided back through it. It renders in the right place but\nskews flat-shaded normals — apply transforms (`rotation=True, scale=True`)\nafter joining. Watch for a related trap: setting `ob.scale.x` on a mesh whose\nsize was already applied *multiplies* rather than replaces it.\n\n### A model only needs to stay split where something animates it\n\nEvery glTF primitive is a draw call. Merge everything static into one mesh and\nkeep separate nodes only for the parts a clip actually drives — a boat with a\nrabbit and a paddle went from 28 primitives to 5 that way, with no visible\nchange.\n\n### One palette strip beats one material per colour\n\nAn `InstancedMesh` is **one geometry and one material**, so a prop that wants\nfive colours cannot use five materials. Give the whole model a single material\nwhose image is an *N*×1 strip, one pixel per shade, and point each face's UVs\nat a cell centre (`u = (cell + 0.5) / N`, `v = 0.5`) with the sampler set to\nnearest. A whole scene's palette then costs one tiny texture, a merged mesh can\nbe twenty colours, and there is nothing to bleed between cells.\n\nColour-space trap when generating that strip programmatically: Blender's\n`image.pixels` are scene-linear floats, but this export path writes them out\n**verbatim**, so pre-converting sRGB→linear darkens every colour. White is the\ntell — it is the one value identical in both spaces, so if white survives and\neverything else came out dark, that is the bug. Verify by decoding the PNG back\nout of the GLB and comparing bytes to the source palette.\n\n### Feeding an authored mesh into an `InstancedMesh`\n\n`EXT_mesh_gpu_instancing` covers instances authored *in the file*. When the\ngame places them itself, take the geometry off the parsed descriptor instead:\n\n```js\nconst node = me.loader.getGLTF(\"palm\").nodes[0];\nconst trees = new me.InstancedMesh(0, groundY, {\n    vertices: node.vertices, uvs: node.uvs,\n    normals: node.normals, indices: node.indices,\n    texture: palette, textureFilter: \"nearest\", scale: 40,\n    instanceCount: count,\n});\n```\n\n`nodes` is one entry **per primitive**, each in its own local space with a\nseparate `world` matrix — so this only works cleanly when the asset is a single\nmerged primitive exported with an identity node transform. Model it at the\norigin, base on the floor, and apply transforms before exporting.\n\nOne geometry stamped out hundreds of times reads as one object *copied*\nhundreds of times, so vary each instance in its transform. Rotate **after**\ntranslating, or the instance swings around the group origin instead of turning\nwhere it stands:\n\n```js\nplacement.identity().translate(x / s, -y / s, z / s)\n         .rotate(math.randomFloat(0, Math.PI * 2), AXIS_Y);\nconst j = math.randomFloat(0.82, 1.18);\nplacement.scale(j, j, j);\n```\n\nSizing every prop against **one** scale constant, rather than giving each its\nown, keeps \"this looks wrong\" a modelling question instead of a scaling one.\n\n## Packaging\n\nA self-contained GLB is one file with nothing to resolve — the safest thing to\nship. External references still work in both formats: `.bin` buffers and image\n`uri`s are resolved **relative to the asset's own URL** (a GLB with an external\ntexture loads as-shipped). But an external file that 404s rejects the parse, so\nthe whole asset fails to load rather than arriving partially — no untextured\nfallback. Note that `loader.setBaseURL(\"*\", …)` deliberately skips `gltf`,\n`glb`, `obj` and `mtl` for this reason; set those types individually if you\nneed a prefix.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| model mirrored — text backwards, gear on the wrong side | `Mesh` defaults `rightHanded` to `false`; pass `true` |\n| model upside down | a manual axis flip on top of the loader's conversion |\n| scene is enormous or invisibly small | `scale` left at 1 against a metre-scale export |\n| the asset fails to load entirely | an external buffer/image `uri` that 404s, or Draco-compressed geometry |\n| everything is blown out white | authored lux/candela intensities; use `lightIntensityScale` |\n| materials flatter than the authoring-tool preview | no normal or metallic/roughness *textures* are imported |\n| a character does not deform | vertex skinning is out of scope; rig hierarchically or billboard |\n| animation names come back empty | the asset has no node-TRS channels (skin-only rig) |\n| a hundred copies tank the frame rate | exported without `EXT_mesh_gpu_instancing` |\n| a whole scatter looks like one object repeated | every instance shares the group transform — vary yaw/scale per instance, rotating *after* the translate |\n| three clips where you authored one | one action per object; use a single action with a slot each |\n| one part of a rig renders rigid while its curves exist | it was re-parented mid-pose, baking the inverse into `matrix_parent_inverse` |\n| a merged mesh lights wrong along one axis | it inherited the active object's non-uniform scale on join — apply transforms |\n| an authored palette comes out uniformly dark, but white is correct | linear values written to a strip that is saved verbatim |\n| every untextured glTF material renders too dark, white unaffected | `baseColorFactor` is linear and a `tint` is sRGB — `tint.setLinear(...)`, not `* 255` or `setFloat` |\n| `getGLTF(name).nodes[0]` geometry lands in the wrong place | `nodes` is per primitive, each with its own `world`; merge to one primitive and export at the origin |\n| a prop casts no visible shadow | wide and flat-bottomed — the blob is under it; `shadowGroundY` haloes it rather than revealing it |\n| shadows only show on casters near the camera | `shadowGroundY` is on the wrong side — Y-down means the floor is a **greater** y, so a `pos.y - lift` puts the blob inside the caster and the depth test leaves only a hairline ring |\n| a shadow smeared across the whole floor | a ground plane cast its own blob — use the scene-wide opt-in |\n| `onLoaded` gets a string, not the scene | it is called with the level id; load into your own container instead |\n| scene renders flat and unlit | no `Camera3d` — the 2D-camera path is CPU-projected and unlit (with a `Camera3d` on Canvas you get a black canvas instead) |\n\n## Related skills\n\n- `melonjs-3d` — conventions, `Camera3d`, meshes, `Light3d`, instancing, and\n  `camera.setFog` (an outdoor scene almost always wants it; it is set on the\n  camera, so a loaded scene needs no per-node work)\n- `melonjs-lighting` — how `Light3d` behaves once imported\n- `melonjs-loading-assets` — the loader, asset types and base URLs\n- `melonjs-plugins` — the Spine plugin, for skeletal characters\n\n<!-- File: packages/melonjs/skills/melonjs-3d/SKILL.md (Tokens: ~12820 | Category: anthropic-skill) -->\n---\nname: melonjs-3d\ndescription: \"Use this skill for anything 3D or 2.5D in melonJS — Camera3d, Mesh, InstancedMesh, Sprite3d billboards, Light3d, ground shadows, glTF/GLB scenes, and depth sorting. Covers the Y-down/+Z-forward convention that is the inverse of OpenGL, the cameraClass opt-in, clip planes, and what does not work on the Canvas fallback. Triggers on: Camera3d, Mesh, InstancedMesh, Sprite3d, Trail3d, ribbon, exhaust trail, Light3d, billboard, glTF, glb, 3D, 2.5D, depth, cameraClass, fov, setClipPlanes, setFog, fog, distance fog, height fog, heightFalloff, transparent, transparency, alpha, blendMode, fade, castGroundShadow, lit, setBasis, getBasis, lookAt, up vector, gimbal, slerp, great circle, turn a direction.\"\nlicense: MIT\n---\n\n# 3D and 2.5D in melonJS\n\n> melonJS is a 2D engine that grew a real 3D tier: perspective cameras, meshes,\n> instancing, glTF scenes and 3D lights. Most of what a model \"knows\" about 3D\n> graphics comes from the OpenGL convention, and **melonJS's is the inverse**.\n> Read the conventions section before writing any 3D code.\n\n## Coordinate conventions — the inverse of OpenGL\n\nThis is the single most important thing on this page.\n\n| | melonJS | OpenGL convention |\n|---|---|---|\n| vertical | **Y-down** — higher `y` is *lower* on screen | Y-up |\n| depth | **+Z forward** — higher `z` is *farther* away | −Z forward |\n\n`Camera3d` exposes three rotation angles: `camera.pitch`\n(X, look up/down), `camera.yaw` (Y, look left/right) and `camera.roll` (Z, bank\nthe horizon). The view is `R(yaw) ∘ R(pitch) ∘ R(roll)` inverted, and the frustum\nplanes come off that same matrix — so culling follows a banked view.\n\nThe basis those angles produce is **readable and writable**: `getBasis(right,\nup, forward)` reads it, `setBasis(right, up, forward)` poses the camera from\none.\n\n`camera.rotate()` is NOT the way to bank a 3D camera. It writes the inherited\n`currentTransform`, which a 3D view never reads, so the call is silently inert.\nSet `roll`. (On a `Camera2d` it is the other way round: roll IS that transform's\nrotation, which is why `worldToLocal` / `localToWorld` compensate for it.)\n\nThe payoff is that 2D code translates directly — anywhere you used `pos.x` /\n`pos.y`, add `pos.z` and the maths still holds. The cost is that every\nOpenGL-shaped instinct is backwards.\n\n**Consequence that bites immediately:** \"behind everything\" is the *largest*\ndepth when the camera looks along +Z. Put a backdrop on the wrong side and it\neither paints over the whole scene (it is nearer than everything else) or\ndisappears without trace (it falls in front of the near plane and the GPU clips\nit). Which sign means \"far\" depends on which side the camera sits, so check the\ncamera before picking a backdrop depth.\n\nglTF assets are authored Y-up right-handed. `level.load` converts them by\nrotation (`rightHanded: true` is its default), so you do not apply a flip\nyourself. The **`Mesh` constructor defaults `rightHanded` to `false`**, which\nbridges by mirroring instead — geometry you hand to a `Mesh` directly comes in\nflipped left/right unless you pass `rightHanded: true`. See\n`melonjs-3d-assets`.\n\n## Opting in\n\n3D starts with the camera class, and nothing else has to change. Set it once on\nthe `Application`:\n\n```js\nimport { Application, Camera3d } from \"melonjs\";\n\nconst app = new Application(1024, 768, {\n    parent: \"screen\",\n    cameraClass: Camera3d,      // ← the opt-in\n});\nawait app.init();\n```\n\nThat flips the world's sort to `\"depth\"`, switches the broadphase to a 3D index,\nand renders sprites and meshes with perspective projection. `Camera3d` extends\n`Camera2d`, so follow, fade, shake and post effects all still work.\n\n### Keep the camera, do not fish it back out\n\n`app.viewport` is the **active stage's** default camera, so it is typed\n`Camera2d`: a stage overrides the application's `cameraClass`, and the\nbuilt-in loading screen does exactly that. Reading your `Camera3d` back out of\nit therefore needs a cast, and a cast that is wrong while the loader is up:\n\n```js\nconst camera = app.viewport as Camera3d;   // works, until it does not\n```\n\nOwn it instead. Pass the camera in and keep the typed reference, and nothing\ndownstream ever narrows:\n\n```js\nclass GameStage extends Stage {\n    readonly camera: Camera3d;\n\n    constructor(app) {\n        const camera = new Camera3d(0, 0, app.renderer.width, app.renderer.height);\n        super({ cameras: [camera] });\n        this.camera = camera;\n    }\n}\n```\n\nA freshly built camera is named `\"default\"`, and a stage registers its\n`cameras` by name, so this IS the stage's default camera: `app.viewport`\nreturns the same instance.\n\n### The two patterns differ in LIFETIME, and that decides which you want\n\nThis is the part that bites. `cameraClass` builds a **fresh camera for each\nstage**, which the engine does deliberately so \"Camera3d state shouldn't\nbleed across stages\". An owned camera passed through `super({ cameras })` is\nbuilt once and lives as long as the stage object, which for a stage you\nregister with `state.set()` means as long as the application.\n\nSo anything you attach to the camera now outlives the visit:\n\n```js\n// in onResetEvent — runs EVERY time the stage is entered\napp.viewport.addPostEffect(bloom);\n```\n\nWith `cameraClass` that lands on a fresh camera each time and is harmless.\nWith an owned camera it stacks: a second bloom on the second visit, a third\non the third. The symptom is text and edges appearing to double or smear,\nbecause the scene is being post-processed once per accumulated effect.\n\nNote that `removePostEffect` DESTROYS the effect rather than detaching it, so\n\"just remove it on the way out\" is not a free undo.\n\n**Choose on lifetime, not on typing:**\n\n| your effects / camera state are | use |\n|---|---|\n| set up per stage visit, in `onResetEvent` | `cameraClass` — fresh camera per stage |\n| set up once, in the stage constructor | an owned camera in `super({ cameras })` |\n\nIf you want the typed reference but your setup is per-visit, keep\n`cameraClass` and narrow once on reset:\n\n```js\noverride onResetEvent(app) {\n    const viewport = app.viewport;\n    if (!(viewport instanceof Camera3d)) {\n        throw new Error(\"this stage needs a Camera3d\");\n    }\n    this.camera = viewport;   // typed, checked, and still fresh per visit\n}\n```\n\nThat is what the Earth Defender example does, and why: it attaches post\neffects on every reset and never removes them.\n\nNarrowing beats asserting wherever you read the shared slot: the language\nverifies `instanceof`, and fails loudly if the application was never given a\n`cameraClass`, where a cast fails silently at the first 3D call.\n\nPer-stage opt-in also works, which is useful when the loading screen is 2D:\n\n```js\nclass GameStage extends Stage {\n    constructor() {\n        super({ cameras: [new Camera3d(0, 0, 1024, 768, { fov: Math.PI / 3 })] });\n    }\n}\n```\n\n## Camera traps\n\n- **There is no sky.** The frame clears to `renderer.backgroundColor` (opaque\n  black unless the renderer was created transparent), and `world.backgroundColor`\n  clears over it when you set one — both are flat colours. Anything more is a\n  backdrop renderable, screen-fixed or placed at a far depth.\n- **Set the clip planes for your scene scale.** Defaults are near 0.1 / far 1000.\n  Objects beyond the far plane clip or project with bad w-divides; a `near` that\n  is too small wastes depth precision and distant props z-fight. Use\n  `camera.setClipPlanes(near, far)`.\n- **`camera.pos.set(x, y)` is 2-argument and zeroes z.** Use\n  `camera.setPosition(x, y, z)`, which leaves the depth untouched when `z` is\n  omitted; `camera.depth` is the z accessor if you only want that.\n- **`lookAt(target)` solves pitch and yaw only**, so on its own it cannot\n  express \"up is this direction\" and it leaves `roll` alone. Pass an up to\n  bank the horizon too: `camera.lookAt(target, up)`, where `up` is what should\n  appear UP ON SCREEN. This engine renders Y-down, so a level horizon is\n  `(0, -1, 0)`. There is deliberately no default, because giving one would\n  silently level every camera that had banked its own horizon.\n- **When the game already has all three axes, hand them over**:\n  `camera.setBasis(right, up, forward)`, the writable counterpart of\n  `getBasis`. The case that needs it is a view over a curved surface, where up\n  is the surface normal and differs at every point, so no pitch and yaw pair\n  expresses it and integrating towards one drifts and gimbals at the poles.\n  `setBasis` absorbs a triple that has drifted off orthonormal rather than\n  leaving the camera holding something that is not a rotation, and it does not\n  care what the axes are scaled to: a direction is a direction, so anything\n  from a denormal to 1e300 resolves the same way. Note its `up` is the camera's\n  local +Y column, which under Y-down points DOWN the screen: the opposite\n  sense to `lookAt`'s, and one negation apart.\n- **`worldToLocal` does not project.** It is a 2D camera's offset subtraction.\n  To pin a label or marker to a point in the scene use\n  `camera.worldToScreen(x, y, z)`, which applies the projection and returns\n  `null` behind the camera. See `melonjs-camera-and-drawing`.\n\n## Where an up vector earns its keep\n\nThree cases. In the first two, `lookAt(target)` on its own is already right and\nan up would be noise. The third is the one it exists for.\n\n```js\n// 1. ordinary third-person: world up never changes, so say nothing\ncamera.setPosition(x, y, z).lookAt(player);\n\n// 2. a deliberate dutch angle: this is a ROLL, not an up vector\ncamera.lookAt(player);\ncamera.roll = Math.PI / 32;\n\n// 3. a view over a curved surface: up is the surface normal, and it is\n//    different at every point on the globe. No pitch and yaw pair expresses\n//    it, and integrating towards it drifts and gimbals over the poles.\nconst up = player.pos.clone().sub(planetCentre).normalize();  // surface normal\ncamera.setPosition(eye.x, eye.y, eye.z).lookAt(player, up.negate());\n```\n\nNote the `negate()` in case 3. The surface normal points away from the planet,\nwhich is the direction the player's head points, and `up` wants what should\nappear UP ON SCREEN. This engine renders Y-down, so `(0, -1, 0)` is a level\nhorizon and a world-space \"up\" direction has to be handed over in that sense.\nGet it backwards and the view is upside down, which is the single most likely\nmistake with this argument.\n\nIf the game already computes all three axes, skip the solve entirely:\n\n```js\n// a surface frame the game is already maintaining\ncamera.setBasis(right, up, forward);\n```\n\n`setBasis` takes the camera's OWN columns, where `up` is local +Y and so points\ndown the screen: the opposite sense to `lookAt`'s, one negation apart. It also\nabsorbs a frame that has drifted off orthonormal, which one integrated over\nthousands of steps will have.\n\n**A camera posed this way and a world rotated into a fixed camera's frame are\nthe same picture.** If a game already transforms its world into view space by\nhand, there is nothing to gain by switching; the value of `setBasis` is not\nhaving to write that transform in the first place.\n\n## Turning a direction: `slerp`, not `lerp`\n\n`lerp` walks the straight line between the two tips, which passes INSIDE the\nsphere. The result points the right way at both ends, lingers near them, rushes\nthe middle, and comes out shorter than either input as it crosses the midpoint.\nOn a camera swing that reads as a lurch; on a body walking over a globe it dips\ninto the surface.\n\n`Vector3d#slerp` turns along the great circle instead, at a constant angular\nrate, and carries the length across linearly, so two unit vectors give back a\nunit vector and nothing needs renormalizing.\n\n```js\n// a turret, a camera, a ship: a facing chasing another facing\nfacing.slerp(toTarget, 0.25);\n\n// frame-rate independent, the same shape as math.damp\nfacing.slerp(toTarget, 1 - Math.exp((-6 * dt) / 1000));\n```\n\n`ObservableVector3d` has it too and fires its callback ONCE, so\n`renderable.pos.slerp(...)` is one notification rather than three.\n\n**`Vector2d` has the same method**, and it is not a cut-down copy: in 2D the\nwhole turn is one signed angle, so there is no sine to divide by and no\ndegenerate near-parallel case at all. What it buys a 2D game is the wrap: a\nheading damped as a NUMBER from 170 degrees to -170 degrees sweeps the long 340\ninstead of the short 20, and an arc between directions cannot do that.\n\nTwo degenerate pairs are handled inside rather than left to the caller, and\nboth turn up in ordinary game code:\n\n- **the same direction**, where every path between them is the same path.\n- **opposite directions**, where infinitely many great circles pass through\n  both and none is the shorter way round. The textbook formula divides by a\n  vanishing sine here and hands back `NaN`, which propagates into a position or\n  a matrix and the object disappears. `slerp` picks a stable perpendicular and\n  turns through half a circle instead.\n\nThat second case is why a hand-rolled version is a liability rather than a\nshortcut: anything orbiting a planet reaches the antipode of where it started,\nevery run.\n\n**`math.damp` for a scalar, `slerp` for a direction.** Same job, different\nspace; what `damp` cannot do is keep a vector on the sphere.\n\n## Depth sorting\n\nUnder `Camera3d` the world sorts on `\"depth\"` **every frame** (recursively), so\nunlike 2D you *can* assign `depth` after `addChild` and it will reorder. That is\nthe one place where the usual z-ordering rule is relaxed — see\n`melonjs-renderables` for the 2D rules, which still apply everywhere else.\n\nYou will often *have* to. A `Container` has `autoDepth: true` by default, so\n`addChild(child)` with no explicit z **overwrites `pos.z` with the child's\nindex** — a real world depth in a 3D scene, and never the one you wanted. Pass\nit (`world.addChild(mesh, z)`), or set `mesh.depth` afterwards. The glTF\nimporter turns `autoDepth` off on the container it loads into for this reason.\n\n**`floating` does not opt out of this sort.** It skips the camera *transform*,\nnot the depth *order*. A floating child is ordered by `|pos.z|` alone — its\n`pos.x/y` are screen pixels, and the camera does not move relative to it — so\nthe magnitude is the distance and the sign is ignored:\n\n```js\nworld.addChild(hud, -150);       // small -> nearer than anything -> on top\nworld.addChild(skybox, -10000);  // large -> farther -> behind everything\nworld.addChild(skybox, 100000);  // equally far: sign does not matter\n```\n\nBoth hold at any camera position. A HUD given the huge z that would put it on\ntop in 2D lands at the far end of the level instead, with the scenery drawing\nover it.\n\n## Transparency\n\nA mesh fades by setting its opacity — there is nothing else to switch on:\n\n```js\nghost.setOpacity(0.4);\n```\n\nMeshes render in two phases. The **opaque pass** writes depth in sort order; the\n**transparent pass** replays afterwards, back-to-front, blending and writing no\ndepth. A draw lands in the second whenever its alpha is fractional.\n\nThat default matters because the opaque path writes premultiplied colour with\nblending off, so before this a faded mesh came out **darkened toward black**\nrather than see-through — the background contributed nothing.\n\n**`transparent: true`** when the transparency is in the TEXTURE rather than the\nopacity — a soft-edged glow, smoke, a glTF material with `alphaMode: \"BLEND\"`.\nThe automatic check reads the draw's alpha and cannot see into a texture. Watch\n`alphaCutoff` here: it discards texels *before* blending sees them, so a soft\nedge needs a low cutoff (`Sprite3d` drops its own default to `1/255` when you\nset `transparent: true`). The cutoff thresholds the MATERIAL's alpha, not the\ndrawn alpha, so a fading cutout mesh keeps its shape instead of disappearing at\nits own threshold.\n\nThe glTF loader does **not** set this for you: one loaded mesh can merge several\nmaterials and the flag routes the whole mesh, so a `\"BLEND\"` material sharing\ngeometry with an opaque one would drag the opaque half into the transparent pass.\n\n**`transparent: false`** pins a mesh to the opaque pass however it is faded.\n\n### `depthTest: false` for something with no surface to be occluded on\n\n`transparent` decides the PASS, and the transparent pass turns depth *writing*\noff on its own so overlapping blended draws blend instead of fighting. Whether\ngeometry in front of a mesh **hides** it is a separate question, and\n`depthTest` is where you answer it. The default, `true`, is what you want for\nanything genuinely in the scene — a muzzle flash behind a wall should still be\nbehind the wall, and this is not a synonym for \"additive\".\n\nSet it `false` for a mesh standing in for a screen-space effect: an additive\nglow carrying a world position only so it can sort and move with the thing it\nbelongs to. A world-space caption billboarded over a prop wants it for the\nsame reason, since it has no surface for the prop's own foliage to be in front\nof. Left depth tested, a flat billboard is sliced along a hard straight\nline the moment any geometry is nearer at some pixel. That shows up worst\naround something round — a glow on a rock near a planet's limb gets cut,\nand no offset escapes it, because the near surface bulges further toward the\ncamera than any offset you would dare apply.\n\n```js\nconst halo = new Sprite3d(0, 0, {\n    image: glowTexture,\n    width: rock.radius * 3, height: rock.radius * 3,\n    billboard: \"spherical\",\n    blendMode: \"additive\",\n    transparent: true,     // the blended pass\n    alphaCutoff: 0,        // do not cut the soft falloff\n    depthTest: false,      // and do not let the world slice it\n});\n```\n\nHonoured in the transparent pass on both GPU backends; the Canvas renderer has\nno depth buffer and ignores it.\n\nBlending uses the renderable's existing `blendMode`, so a glow is one property.\nThe advanced modes (`\"overlay\"`, `\"difference\"`, and the rest that need a\ncompositing pass) fall back to `\"normal\"` here, on both backends:\n\n```js\nconst glow = new Mesh(0, 0, {\n    ...quad, texture: glowTexture,\n    transparent: true, blendMode: \"additive\", alphaCutoff: 0,\n});\n```\n\n| | |\n| --- | --- |\n| sorting | **per object**, by distance from the camera |\n| intersecting transparent meshes | may pop as the camera moves — split them, or accept it |\n| `InstancedMesh` | sorts as **one** object; instances draw in buffer order |\n| needs | a GPU backend and a `Camera3d` |\n\nGround shadows ride the same pass — a blob is a decal, and decals are its first\nclient rather than a feature of their own.\n\n**`Trail3d`** is the transparent pass's other obvious client: a camera-facing\nribbon for an exhaust streak, a speed trail or a tyre mark. It is a `Mesh`, so\nit sorts and fogs with everything else here, and like the rest of this tier it\nneeds a `Camera3d`. `Trail` is the 2D one and stays flat under a perspective\ncamera. See `melonjs-particles-and-trails`.\n\n## Distance fog\n\nOff until you ask for it, and one call on the camera:\n\n```js\ncamera.setFog({ near: 2000, far: 7000 });   // linear: name the two distances\ncamera.setFog({ mode: \"exp2\", density: 4e-4 }); // or one density\ncamera.setFog(null);                         // off\n```\n\nIt is the cheapest thing that stops a 3D scene reading as flat cut-outs, and it\nlets props arrive at the far plane without a visible edge.\n\n**Every parameter is optional, and the omitted ones track live.** Distances\ndefault to the camera's own clip planes, so fog cannot silently disagree with\nthem after a later `setClipPlanes`. The colour defaults to\n`renderer.backgroundColor` and follows it, so geometry dissolves into whatever\nsky you already set — including through a day/night fade. Pass `color` only\nwhen the fog should deliberately differ from the backdrop:\n\n```js\ncamera.setFog({ far: 5000, color: \"#8899aa\" });\n```\n\nA `Color` is held by reference, so mutating it animates the fog.\n\n**Height falloff** makes mist pool in low ground instead of hanging at every\naltitude equally — the difference between fog reading as weather and as a global\ndesaturation:\n\n```js\ncamera.setFog({ near: 1200, far: 7000, fogHeight: 0, heightFalloff: 0.0015 });\n```\n\n`heightFalloff` defaults to **0**, which is uniform fog — not a special case,\nthe same integral with the dial at zero, so leaving it out changes nothing.\nRender space is **Y-down**, so `fogHeight` is the floor and density rises\n*below* it; every published form of this formula assumes Y-up and has the\nopposite sign.\n\nFog is measured **radially** from the camera and applied **per fragment**, so\nit does not slide as the camera turns and does not band across large triangles.\nIt lives on the camera, so a split-screen or minimap view fogs independently —\nand a `Camera2d` never fogs at all.\n\n**Per object:** `fog: false` exempts a mesh however far away it is — for a\nwaypoint or objective marker that has to stay readable. It exempts the mesh and\nnot the ground shadow it casts: a blob is a mark on the floor and fogs with the\nfloor. Emissive surfaces fog\nlike everything else (light travelling through fog is attenuated too), so a\nneon sign that should punch through wants `fog: false`, not a brighter\nemissive.\n\nOnly meshes fog. 2D content, HUDs and `floating` renderables never reach the\nmesh shaders, so a screen-space overlay stays clean with no work.\n\n**A custom mesh shader is not fogged unless it asks to be.** Fog is compiled\ninto the engine's own mesh programs — `#define FOG` on WebGL, an `enable_fog`\noverridable constant on WebGPU — and a shader you supply is yours: the engine\nbinds it as written and never substitutes a fogged variant. So a mesh given its\nown `GLShader` keeps full contrast while the scene around it recedes. It is\nsafe — nothing throws, and the camera's fog is simply not applied — but it is\nusually surprising.\n\nA hosted `ShaderEffect` is the other case and needs nothing: its body is\nspliced into the engine's own mesh program as a colour hook, and fog is applied\nto the hook's result, so such a mesh fogs like any other.\n\nTo opt in, declare the same uniforms and the engine will feed them, because the\nfog values are pushed to any mesh program that declares them rather than only to\nthe built-in ones:\n\n```glsl\nuniform vec3 uFogColor;   // straight (unpremultiplied) fog colour\nuniform vec4 uFogParams;  // x = mode (0 off / 1 linear / 2 exp2),\n                          // y = near, z = 1/(far - near), w = density\n```\n\nYour vertex stage computes the distance itself — `length((uViewMatrix *\nuModelMatrix * vec4(aVertex, 1.0)).xyz)`, radially so it does not swim as the\ncamera turns — and the blend must scale the fog colour by the fragment's own\nalpha, `mix(uFogColor * a, rgb, f)`, because `vColor` arrives premultiplied.\nMixing toward the unscaled colour haloes every alpha-cutout edge.\n\nThe flip side is the reason fog costs nothing when unused: with no camera fog,\nthe mesh programs are compiled without any of it, on both backends. It is not a\nbranch that is skipped at runtime — the code is not there.\n\n## Meshes\n\n```js\nconst cube = new Mesh(x, y, {\n    model: \"cube\",          // a preloaded OBJ name — no built-in primitives\n    texture: atlas,         // TextureAtlas, image, or a preloaded image name\n    width: 64, height: 64,\n    cullBackFaces: true,    // the default\n    lit: true,\n});\n```\n\n**A mesh pivots about its model origin `(0, 0, 0)`, not an anchor point.** Under\n`Camera3d` it opts out of the anchor offset entirely, so `anchorPoint` is inert\nthere — place the origin where you want the pivot at authoring time, or nest the\nmesh under a transformed parent. The anchor is only honoured on the legacy\n2D-camera path.\n\n### InstancedMesh, and when it is the wrong tool\n\n**`InstancedMesh`** draws one mesh many times in a single draw call — the\ndifference between a hundred trees and a hundred thousand. glTF scenes using\n`EXT_mesh_gpu_instancing` load as one automatically; by hand it is a `Mesh`\nwith a count:\n\n```js\nconst trees = new InstancedMesh(0, 0, { ...treeGeometry, instanceCount: 400 });\nconst at = new Matrix3d();               // one scratch, reused\nfor (let i = 0; i < trees.instanceCount; i++) {\n    at.identity().translate(x, y, z);\n    trees.setInstance(i, at);\n}\nworld.addChild(trees, 0);\n\ntrees.visibleInstanceCount = 120;        // draw fewer, without re-uploading\n```\n\nIt is not a free upgrade. One `InstancedMesh` is **one geometry and one\nmaterial**, and four things move from per-object to per-group:\n\n| | with `Mesh` | with `InstancedMesh` |\n| --- | --- | --- |\n| depth sort | each object sorts on its own `pos` | the whole set has **one** sort key |\n| ground shadow | one blob per object | one instanced draw for the set |\n| removal | `removeChild`, indices unaffected | `removeInstance(i)` swaps the **last** instance into the hole, so any index you were holding is now wrong |\n| colour | `tint` per object | needs `instanceColors: true` and `setInstanceColor(i, …)` |\n\nSo the question is not \"how many are there\" but **\"does the game address them\nindividually\"**:\n\n- **Scenery — instance it.** Trees, rocks, grass, debris: the game never asks\n  about one of them.\n- **Collision-tested props — still fine.** You test against positions you\n  already own; instancing only changes how they are *drawn*.\n- **Collectibles and enemies — usually not.** Anything removed one at a time\n  makes `removeInstance`'s swap your problem: you have to keep an index↔object\n  map and repair it on every removal. At small counts a pooled `Mesh` each is\n  less code and no slower.\n\nUnder a few hundred objects the draw-call saving is not what limits you\nanyway — reach for it when the count is in the thousands, or when the objects\nare pure scenery and it costs nothing to.\n\n## Normals are generated for you\n\nA `lit` mesh needs per-vertex normals for the shader to light with. Supply them\nif you have them; **omit them and the engine computes them from the geometry**:\n\n```js\nconst mesh = new Mesh(x, y, { vertices, uvs, indices, lit: true });\n// normals derived from the triangles — nothing else to do\n```\n\nYou do not choose flat or smooth, because the geometry already decides. Face\nnormals accumulate into their vertices weighted by area, so where faces **share**\na vertex they average and the surface shades smoothly, and where every triangle\ncarries its **own** three vertices — a triangle soup, which is how most\nhand-built geometry comes out — each vertex belongs to one face and the result\nis that face's normal, so it shades flat. Want faceted edges: duplicate the\nvertices. Want smooth: share them.\n\nAn explicit `settings.normals` always wins, and an unlit mesh gets none — there\nwould be nothing to read them.\n\n**A lit mesh with no normals used to render fullbright**, which looks like the\nlighting is broken rather than absent. If an older scene suddenly picks up\nshading, that is why.\n\n## Colouring a mesh\n\nThere are four levels, and picking the wrong one is the usual reason a colour\n\"does not apply\". They all multiply together.\n\n| level | how | use for |\n|---|---|---|\n| whole object | `mesh.setTint(\"#ff8800\")`, or `mesh.tint.setColor(r, g, b)` | flash on hit, team colour, fading one object |\n| per vertex | `settings.vertexColors`, or `mesh.setVertexColor(i, color)` | a gradient *within* one mesh — distance haze, a darker crease |\n| per material | `textureGroups`, from a multi-material OBJ + MTL | a model whose parts differ, in one draw call |\n| per instance | `new InstancedMesh(…, { instanceColors: true })` then `setInstanceColor(i, color)` | a thousand copies that differ |\n\n**`tint` is per object.** That is the trap: build a terrain as one big mesh and\nyou can tint the whole valley or none of it. Anything that varies *across* a\nsingle mesh is per-vertex.\n\n```js\n// fade a procedural terrain toward the sky the further out it goes\nconst ground = new Color(217, 230, 244);\nconst sky = new Color(207, 230, 247);\nconst haze = new Color();\nfor (let i = 0; i < mesh.vertexCount; i++) {\n    const t = Math.min(1, mesh.originalVertices[i * 3 + 2] / 6000);\n    mesh.setVertexColor(i, haze.copy(ground).lerp(sky, t));\n}\n```\n\nSupply the whole array at construction when you already have it —\n`vertexColors` takes a packed `Uint32Array` (the form the batchers read, so no\nconversion) or one `Color` per vertex. A length that does not match\n`vertexCount` **throws**; it is not padded, because a short array would leave\nthe tail of the mesh mis-coloured and that reads as a lighting bug.\n\nMutating the array directly is fine, but say so afterwards:\n\n```js\nmesh.vertexColors[i] = color.toUint32(color.alpha);\nmesh.needsUpdate = true;   // the retained Camera3d path uploads once\n```\n\n`setVertexColor` does that for you. Skip it and the colour applies under a 2D\ncamera and silently does not under `Camera3d`.\n\nOn a lit mesh the colour multiplies the **lit** result, so it behaves as albedo\nrather than as an emissive override — a vertex colour will not make an unlit\nface bright.\n\n`alpha` hides a mesh as it hides anything else: at 0 the draw is skipped. There\nis no *partial* mesh transparency though — the mesh path renders opaque, so a\nmesh at `alpha = 0.5` draws fully opaque rather than half see-through. Fade a\nmesh out and it will stay solid until it vanishes.\n\n## Moving a mesh in 3D\n\n`Mesh` and `Sprite3d` inherit the ordinary `Renderable` transform helpers, and\nboth are already 3D:\n\n```js\nmesh.rotate(Math.PI / 2, new Vector3d(1, 0, 0));   // axis overload\nmesh.scale(2, 1, 0.5);                             // z is a real argument\n```\n\n`rotate(angle)` alone is the 2D case (about Z); pass a `Vector3d` and it\nrotates about that axis. Both write to `currentTransform`, a **`Matrix3d`**,\nwhich `Mesh` folds into its model matrix — so orienting a mesh costs a matrix\nupdate, not new geometry.\n\nThis matters most for the case it is least obvious for: a horizontal plane.\n`Sprite3d` with `billboard: false` is a quad in the sprite plane — upright —\nand the way to lay one flat (water, a road, a shadow decal) is to rotate it a\nquarter turn about X, **not** to hand-build vertices. The one reason to build\ngeometry instead is tiling: a `Sprite3d` bakes its UVs 0..1 from its atlas\nframe, so a surface that needs `textureRepeat` across many world units still\nwants an explicit `vertices`/`uvs` mesh.\n\n## The mesh pass is opaque\n\nWorth stating plainly, because the symptom does not look like a blending\nproblem: **the mesh pass disables blending**. A soft-edged texture — a glow, a\nsun, a halo, anything with a gradient alpha — composites as a hard-edged disc\nwith a grey rim, which reads as a broken texture rather than as a missing\nfeature.\n\n`transparent: true` routes it through the transparent pass, and `blendMode:\n\"additive\"` on top of that makes a light source add to the sky instead of\nsitting on it. This is the same feature a translucent water surface needs.\n\n## Textures under a perspective camera\n\nA tiling texture on a large ground plane is seen at a grazing angle, and that\nchanges which detail survives:\n\n- **Nothing straight and parallel to the direction of travel.** Every such run\n  converges on the vanishing point, so a few current bands on a river become a\n  fan of rays radiating out of the horizon. Narrowing them and tiling more\n  often only multiplies the rays. Short, scattered, direction-free detail\n  minifies into an even shimmer instead.\n- **`textureFilter: \"linear\"`** for a floor — `\"nearest\"` opts out of mipmaps\n  and anisotropy, and detail aliases into a wash exactly where the camera looks\n  most.\n\n## Ground must outlast the props standing on it\n\nScenery scattered up a slope is placed by its own rule, and terrain is built by\nanother. When the planting reaches further than the mesh does, a low camera\nsees trees standing over open sky at the corners of the frame. Build the ground\nwider than the widest thing planted on it — past the profile's clamp it is a\nflat plateau, so the extra columns cost two triangles and no silhouette.\n\nRecycling has the same trap in Z: an endless runner that recycles tiles a fixed\ndistance behind the *player* forgets that the camera trails further back still,\nand the bottom of the frame falls off the world.\n\n## Sprite3d and billboards\n\n`Sprite3d` is the 2.5D workhorse: a flat sprite living at a real depth, with\n`billboard` controlling how it faces the camera.\n\n| `billboard` | behaviour |\n|---|---|\n| `false` (default) / `\"none\"` | no rotation — a flat plane in the world |\n| `true` / `\"cylindrical\"` | yaws to face the camera, stays upright (characters, trees) |\n| `\"spherical\"` | always fully faces the camera (particles, impostors) |\n\n`\"cylindrical\"` is what you want for paper-thin characters in a 2.5D game.\nBillboarding needs a `Camera3d` drawing the frame; under a 2D camera the quad\nrenders fixed-orientation. **Any other string falls through to the spherical\nbranch**, so a guessed value like `\"upright\"` silently gives you spherical.\n\nA billboard takes its **orientation** from the camera, so `rotate()` has no\neffect on one: a card already turned to face the camera has no free rotation\nleft to give. Everything that is not an orientation still applies, `scale()`\nand `meshScale` included, so animating the size of a shockwave or a pickup is\nthe ordinary call and the cull bounds grow with it:\n\n```js\nconst wave = new Sprite3d(x, y, { image: \"ring\", width: 64, height: 64,\n                                  billboard: \"spherical\", transparent: true });\nwave.scale(1 + 3 * t);          // grows; `scale` accumulates, so drive it\n                                 // from the authored size, not per frame\n```\n\nFor an in-plane rotation, a bolt streak or an exhaust, use `billboard: false`\nand orient the quad yourself.\n\n## Lighting\n\n`Light3d` takes **one argument — the options object** (it has no `x, y, z`\npair; a light carries its own `position`). Types are `\"directional\"` (the\ndefault), `\"ambient\"`, `\"point\"` and `\"spot\"`.\n\n```js\nworld.addChild(new Light3d({ type: \"directional\", direction: [0.3, 1, 0.2] }));\nworld.addChild(new Light3d({ type: \"ambient\", intensity: 0.3 }));\n// a Vector3d works too, wherever the game already has one\nworld.addChild(new Light3d({ type: \"spot\", position: torch.pos, range: 400 }));\n```\n\n`direction` and `position` take an `[x, y, z]` array **or** a `Vector3d`, and\n`color` takes a `Color`, a CSS string or an `[r, g, b]` array — so a value the\ngame already holds can go straight in. Each is read, not retained: move your\nvector afterwards and the light stays where it was.\n\nTwo things still bite, both silently:\n\n- `new Light3d(0, 0, {…})` — the constructor takes **options alone**. JavaScript\n  drops the extra arguments, so `options` becomes the number `0` and every\n  setting in your literal is discarded. The light still appears, on pure\n  defaults: a `type: \"ambient\"` written this way is a second DIRECTIONAL light,\n  and the scene looks plausible enough that nobody checks. TypeScript stops\n  checking a literal once the argument count is wrong, so nothing inside it is\n  verified either.\n- **`direction` is where the light GOES, and this is a Y-down space** — so a sun\n  overhead travels *downward* and its Y is **positive**. Get the sign wrong and\n  the scene is lit from underneath: faces that should be in shade are bright,\n  the ground glows and the sky-facing surfaces go dark.\n\n  ```js\n  direction: [-0.35, 0.8, 0.45]   // sun overhead, late afternoon\n  direction: [-0.35, -0.8, 0.45]  // lit from below — almost never what you want\n  ```\n\n  `position` follows the same convention: a lamp above the floor has a\n  **smaller** y than the floor.\n\nUse **both halves**: with a key light but no ambient, the shadow side of a mesh\ngoes black. With no `Light3d` in the world at all, a `lit: true` mesh falls back\nto a white ambient and renders fullbright — indistinguishable from unlit, which\nis why \"I added `lit: true` and nothing changed\" is the usual first report.\n\nOther defaults worth knowing: `intensity` 1, `color` white, `range` 1000 (point\nand spot, a stylised quadratic falloff, not inverse-square),\n`innerConeAngle` 0 and `outerConeAngle` π/4 (spot).\n\n**`Light2d` is 2D-only** and produces visible artifacts under perspective\nprojection. Do not combine it with `Camera3d`.\n\n**`lit: true` only does anything under a `Camera3d`.** Lighting a mesh needs\nworld-space normals and a world-space fragment position, and the 2D-camera path\nhas neither: its vertices are already the projected output, so there is no world\nspace left to light in. Such a mesh degrades to unlit and warns once, naming the\nfix. If you want lit 3D props in an otherwise 2D game, set\n`cameraClass: Camera3d` on the application. (For 2D **sprites**, lighting is a\nseparate system that works under a 2D camera as normal, see the lighting skill.)\n\nA `lit` mesh takes a **tangent-space normal map**, so relief comes from the\nmaterial rather than from geometry — `settings.normalMap` (a loader key or any\nimage-like source), or MTL `map_bump` / `bump` / `norm`, which the MTL loader\nfetches and applies per material. The tangent frame is derived per fragment, so\nthe model needs no tangent attribute, and an unmapped mesh is unaffected. This\nis the 3D counterpart of the `Sprite` normal maps in the lighting skill; they\nare separate systems and a `Mesh` uses this one.\n\n### What the ground-shadow tier can and cannot do\n\nCheck this table before claiming a setting is unsupported. Every one of these\nis read **every draw**, so all of them can be animated.\n\n| setting | `Mesh` / `Sprite3d` / `GLTFModel` part | `InstancedMesh` |\n|---|---|---|\n| `castGroundShadow` | yes | yes |\n| `shadowGroundY` | yes, the caster's own floor | yes, **one plane** through the SET's origin |\n| `shadowGroundPoint` | yes on a `Mesh` or `Sprite3d`; on a `GLTFModel` part only by setting it after the load, since it is neither a model nor a `level.load` option | **no**, warns and ignores |\n| `shadowGroundNormal` | yes | yes, one plane for the set |\n| `shadowOpacity` | yes | yes |\n| `shadowScale` | yes | yes |\n| `shadowOffset` | yes | yes, one throw for the set |\n| `shadowStretch` | yes | yes, one stretch for the set |\n| `shadowDirectionX` / `Z` | yes | yes |\n| `shadowLight` | yes | yes |\n| height fade as the caster rises | yes | **no** |\n| a different value per instance | n/a | **no**, and there is no plan for one |\n\nThe last two \"no\"s are the same limitation seen twice: a set gets ONE shadow\nplane and ONE shared quad, so the instance's own height is dropped and nothing\ncan vary between instances. Everything else is one value for the whole set,\nwhich is what a set means. A scatter over curved ground is therefore one set\nper flat facet, and a flying scatter wanting a height fade has to be\nper-object meshes.\n\n`shadowGroundPoint` (20.9) is the one genuinely per-object setting, and the\nreason is structural rather than missing work: the instanced tier builds its\nplane THROUGH the set's own origin so the translation's x and z drop out of\nthe intercept, which is then `groundY + lift / nY`. An arbitrary point does\nnot drop out, and that same `nY` divisor is why the 75 degree tilt ceiling\ncannot be lifted there either. `InstancedMesh` warns and ignores rather than\nstoring it, so a game cannot think it took.\n\nApart from that one, if a claim that a setting \"does not work on an\n`InstancedMesh`\" is in your head, it is out of date: `shadowOffset` and\n`shadowStretch` both reached the instanced tier in 20.8.\n\nGround shadows are **on by default** (the `castGroundShadow` application\nsetting), and need a GPU backend and a `Camera3d`. As a blanket default they\nskip geometry with no vertical extent — a ground plane. Per object,\n`castGroundShadow: true`/`false` overrides the app setting and is obeyed as\ngiven, safeguard included; `shadowGroundY` names the floor the blob lands on,\n`shadowOpacity` how dark it is and `shadowScale` how big it is.\n\nOn ground that is not flat, name the floor with `shadowGroundPoint` instead:\na point and `shadowGroundNormal` are a plane, and the blob leaves the caster's\nvertical column to land on it. `shadowGroundY` cannot express that, because\nthe blob is always directly below the caster in world Y, which is only \"below\"\non level ground. It is also what lifts the tilt ceiling, since that ceiling\nmeasures tilt from world up and so assumes a camera looking down the Y axis.\n\nThe height fade follows whichever floor you named. With a point **and** a\nnormal it is measured along that normal; with a point alone it is the\nstraight-line distance to the point. So on a sphere the blob fades with\nALTITUDE rather than with the vertical drop, which is the whole reason the\npoint exists. Two consequences worth knowing, because they fail in opposite\ndirections: a `shadowGroundNormal` facing inward makes the height negative, so\nthe fade clamps to full strength and the blob never fades at all; and a point\non the wrong side of the caster reads as a large height, so the blob\ndisappears outright rather than thinning to a ring the way a backwards\n`shadowGroundY` does. It is also the one shadow setting that THROWS rather\nthan clamping or hiding: a non-finite component is rejected on the spot.\n\nThe blob is an ellipse sized to the caster's own footprint and placed at the\ncaster's x/z, or at `shadowGroundPoint` when one is set. So a tall or narrow object (a character, a tree, a pickup) shows\nits shadow clearly, while a wide, flat-bottomed one resting on the floor covers\nits own completely from a camera looking down at it. That is the shadow\nbehaving correctly, not a bug.\n\n**Do not chase it by raising `shadowGroundY`.** Lifting the plane does not slide\nthe blob out from under the object, it floats the blob *up* — and past a few\nunits it projects over the top of the caster as a dark halo ringing it.\n\nThe simplest fix is a bigger blob, so its edge shows past the object:\n\n```js\nmesh.shadowScale = 1.5;        // 1 is the default; 0 hides the shadow\n```\n\n`shadowScale` works everywhere, `InstancedMesh` included. Changing it on a\nset rewrites the shared quad's four corners in place rather than rebuilding\nit, as does a `shadowLight` that moves, so an animated sun costs no\nallocation.\n\nOr, for the look of a sun that is not overhead, use the shape controls:\n\n```js\nmesh.shadowOffset = 1.5;       // slide it out, in blob radii\nmesh.shadowDirectionX = 1;     // the ground direction it is cast along\nmesh.shadowDirectionZ = 0;\nmesh.shadowStretch = 2;        // and lengthen it along the same line\n```\n\n`shadowOffset` is measured in multiples of the blob's **own radius**, not in\nworld units, so one value serves a boulder and a pebble and every part of a\nglTF model. About `stretch - 1` is what keeps the shadow attached to its\ncaster. It is honoured **only when the floor has been named**, by\n`shadowGroundY` or by `shadowGroundPoint`, because the\nblob is a flat quad on one named plane: sliding it across a plane the game has\nnot named puts it where there may be no floor. `shadowStretch` is clamped to 3\nand the blob fades as it pulls, so an extreme value degrades to nothing rather\nthan to a smear.\n\nAn `InstancedMesh` honours both, as **one** offset and **one** stretch for the\nwhole set, which is all a shared set can carry. Both ride the quad the blobs\nshare rather than the matrix that also places them: a stretch in the matrix\nwould scale the instance positions too and smear the scatter instead of\nlengthening its blobs. So a scatter can sit next to loose props under the same\nsun without looking lit from somewhere else.\n\n`shadowLight` takes the direction from a `Light3d` you name, re-read every\ndraw, so a moving sun carries the shadows with it. Nothing is inferred: the\nengine never picks a dominant light, because a scene with several has no\nnon-arbitrary answer and one with none has no answer at all.\n\n### A floor that is not level\n\n`shadowGroundY` says where the floor is; `shadowGroundNormal` says which way\nit faces:\n\n```js\nprop.shadowGroundY = floorY;\nprop.shadowGroundNormal = [Math.sin(0.35), -Math.cos(0.35), 0];  // a 20 deg slope\n```\n\nWorld up is `(0, -1, 0)`, because render space is Y-down, and that is the\ndefault. The blob is **rotated** onto the plane rather than projected, so it\nkeeps its size; it sits directly under the caster, since the normal turns it\nrather than moving it, unless `shadowGroundPoint` says where instead. It needs\nthe floor named, by `shadowGroundY` or `shadowGroundPoint`, for the same\nreason the offset does: tilting the bounds fallback has no anchor to turn\nabout.\n\nThe tilt is clamped at 75 degrees alongside `shadowGroundY`, past which an\nedge-on blob has nothing left to show. That ceiling measures tilt from WORLD\nup, which stands in for \"tilted away from the viewer\" only while the camera\nlooks down the Y axis, so it is lifted alongside `shadowGroundPoint`, where a\npoint and a normal state the plane outright.\n\nOn an `InstancedMesh` one plane serves the **whole set**: it passes through\nthe set's origin at `shadowGroundY`, faces the normal, and every blob is\nprojected onto it whatever height its own instance sits at, exactly as the\nlevel case puts every blob at one height. A scatter over curved ground is\ntherefore one set per flat facet, each with the facet's own plane, not one\nset with an averaged normal.\n\nAll three compose on a set: the stretch turns the shared quad, the offset\nslides the whole set along the light, and both are carried onto the tilted\nplane rather than applied flat, so a bank's planting lies along its slope\ninstead of hovering across it.\n\nGive blobs to the plants that stand apart. Dense low ground cover is better\nleft without: a thousand blobs each as wide as its own fern sum to a darkened\nhillside, not to a thousand shadows, and ground cover is the ground.\n\nThese are **art direction, not a projection**. A stretched ellipse is not a\nsilhouette and has no contact with terrain, so on ground that is not the plane\nyou named it will not lie on it. Direction-correct shadows want a shadow map,\nwhich this tier does not do.\n\n### Get the sign right: the floor is a GREATER y\n\nRender space is **Y-down**, so the floor an object stands on is a *larger* y\nthan the object itself. Nudging a shadow plane below a surface is therefore a\n**plus**:\n\n```js\nmesh.shadowGroundY = WATER_LEVEL + 8;   // just under the surface — correct\nmesh.shadowGroundY = mesh.pos.y - 8;    // eight units ABOVE its own base — wrong\n```\n\nGet it backwards and the blob becomes a horizontal quad slicing through the\ncaster's own body. It is still drawn at full strength — the height fade sees a\n*negative* height and clamps to 1, so nothing warns you — but the depth test\nhides every part of it that falls inside the caster's silhouette. What survives\nis a thin ring, only as wide as the blob overhangs the object.\n\nThe symptom is distinctive and easy to misread: **shadows appear to switch on as\ncasters approach the camera.** That ring is a few pixels on the nearest object\nand sub-pixel further out, so a scene reads as \"only nearby things have\nshadows\" — which looks like culling, an LOD stage or a fog problem, none of\nwhich it is. Before chasing any of those, dump `shadowGroundY` against the\ncaster's own y and check which one is larger.\n\n## Collision in 3D\n\nUse a body with a depth shape, not a hand-rolled distance check. Two shapes\ncarry depth — `Box3d` and `Sphere` — and any pair of them is resolved in three\ndimensions; the response carries `overlapNZ` / `overlapZ` for the depth axis.\n\nPick by whether the thing turns. `Box3d` is world-axis-aligned and cannot be\noriented, so it fits walls, floors and crates and misfits anything that\ntumbles. `Sphere` has no orientation at all, which makes it the shape for\nspinning debris, projectiles, and anything spread over a curved surface: on a\nglobe each object's own \"up\" points a different way, so one axis-aligned box\ncannot fit any of them.\n\nA hand-rolled check is the usual mistake here and it costs twice — it throws\naway the penetration vector the response already carries, and it is invisible\nto the debug plugin's hitbox overlay, which draws from `body.shapes`. If the\nred boxes never appear, the reason is usually that there is no body to draw.\n\nAgainst a planar shape, either 3D shape degrades to its XY footprint with the\nplanar side read as unbounded along Z, so a sphere or box body still collides\nwith an ordinary collision layer, just without a depth result. And mind the\nnormal: a box pair separates along one world axis, a sphere along the line\nbetween two centres, so a sphere contact fills the 2D and depth halves of the\nresponse at the same time. See the physics skill for the full contract.\n\n## glTF / GLB scenes\n\nLoaded through the same level director as everything else:\n\n```js\nawait loader.preload([{ name: \"diorama\", type: \"glb\", src: \"data/diorama.glb\" }]);\nlevel.load(\"diorama\", { scale, castGroundShadow, shadowGroundY, onLoaded });\n\n// every ground-shadow setting travels except shadowGroundPoint, which is\n// per object and dropped by a scene load: a loaded scene can\n// be told where its sun is, and `shadowLight` is re-read every draw\nlevel.load(\"diorama\", {\n    scale, castGroundShadow: true, shadowGroundY: 0,\n    shadowLight: sun, shadowOffset: 0.6, shadowStretch: 1.8, async: true,\n});\n```\n\n`.obj` / `.mtl` are also supported loader types.\n\nAn **animated** asset instantiates as a `GLTFModel`, which is a game object as\nmuch as a scene: construct it straight from the parsed descriptor, and place it\nwith `pos` / `depth` / `rotate` / `scale` like any other renderable — the\nplacement drives the whole rig and composes with the playing clip.\n\n```js\nconst model = new GLTFModel(loader.getGLTF(\"boat\"), { scale: 40, lit: false });\nmodel.setCurrentAnimation(\"paddle\", { loop: true });\nmodel.pos.set(x, y);\nmodel.depth = z;\n```\n\nTo scatter an authored mesh yourself, read the geometry off the descriptor and\nhand it to an `InstancedMesh` — `loader.getGLTF(name).nodes[0]` carries\n`vertices` / `uvs` / `normals` / `indices`. That wants a single merged\nprimitive exported at the origin; see `melonjs-3d-assets`.\n\n`melonjs-3d-assets` covers the rest: every `level.load` option, which material\nfeatures are imported, imported lights and their i\n\n<!-- File: packages/melonjs/skills/melonjs-audio/SKILL.md (Tokens: ~6325 | Category: anthropic-skill) -->\n---\nname: melonjs-audio\ndescription: \"Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay, no sound, sound stopped working, context suspended, silent after idle, clips silent but tones play.\"\nlicense: MIT\n---\n\n# Audio\n\n## Two setup rules\n\n```js\nimport { audio, loader } from \"melonjs\";\n\naudio.init(\"mp3,ogg\");                      // 1. BEFORE preloading any audio\n\nawait loader.preload([\n    { name: \"cling\", type: \"audio\", src: \"data/sfx/\" },   // 2. a PREFIX, not a file\n]);\n```\n\n1. **`audio.init(format)` must run before any audio asset is preloaded**, and\n   lists the formats you actually ship. Preloading first throws\n   `target audio extension(s) should be set through me.audio.init() before\n   calling the preloader.` It returns `false` when no audio backend is\n   available, so it is worth checking. The format list defaults to `\"mp3\"`.\n2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus\n   each configured extension and uses the first that loads — so\n   `data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename\n   gives you a 404. A `data:audio/...` URL is the one exception: it is used\n   verbatim, prefix and extension skipped.\n\n`audio.hasAudio()` says whether the browser can play audio at all, and\n`audio.hasFormat(\"ogg\")` whether it can play one codec — useful for choosing\nthe `init` list or hiding a sound toggle on a device with no audio.\n\nOrder matters — melonJS tries the listed formats left to right, so put the\npreferred one first. Two formats (`\"webm,mp3\"` or `\"mp3,ogg\"`) is the usual\nbelt-and-braces for codec coverage. Accepted tokens: `mp3`, `mpeg`, `opus`,\n`ogg`, `oga`, `wav`, `aac`, `caf`, `m4a`, `m4b`, `mp4`, `weba`, `webm`, `dolby`,\n`flac`.\n\n## Effects versus music\n\n```js\naudio.play(\"cling\");                    // fire and forget\naudio.play(\"cling\", false, null, 0.5);  // loop, onend, volume\nconst id = audio.play(\"engine\", true);  // looping; keep the id to stop it\n\naudio.playTrack(\"theme\");               // background music\naudio.stopTrack();\n```\n\nUse **`playTrack`** for music, not `play`. `playTrack(name, volume?)` always\nloops, and it records the name as the current track — which is what\n`app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via\n`state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,\n`resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there\nis only ever one current *track name*.\n\n**`playTrack` stops the track it replaces** (20.9.0 and later; before that it\nonly assigned the slot, and the previous bed kept looping underneath where\n`stopTrack()` could no longer reach it). So a scene just states its own music\nand nothing cancels anything:\n\n```js\nclass PlayScreen extends Stage {\n    onResetEvent() { audio.playTrack(\"battle\", 0.6); }\n}\n```\n\nCalling it with the track already playing **restarts** it, the same as `play()`\non any other clip. A scene that wants the bed to carry on across a screen\nchange should not call it again, rather than rely on it doing nothing.\n\n`stopTrack()` is for where nothing follows: a scene whose successor has no\nmusic of its own, or a game going quiet. `state.change` destroys the outgoing\nstage **before** it resets the incoming one, so a `stopTrack()` in\n`onDestroyEvent` and a `playTrack()` in the next `onResetEvent` are in that\norder and never cancel each other.\n\nTo end a track on an event rather than on a screen change, say a run ending\nwhile the player stays on the same screen, `audio.fade(name, from, 0, ms)`\ntakes it down gently. A fade to zero is silence, not a stop, so the voice is\nstill live until something stops it or replaces it.\n\nEvery per-clip function throws `audio clip <name> does not exist` if the clip was\nnever loaded, and `play` additionally throws when a named sprite region is\nmissing.\n\n## Audio sprites\n\nOne file carrying many effects, with named regions in milliseconds:\n\n```js\nawait loader.preload([{\n    name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { jump: [0, 450], hit: [2000, 250], music: [4000, 12000, true] },\n}]);\n\naudio.play(\"sfx\", { sprite: \"jump\" });\n```\n\n### A loop with no gap\n\nAn MP3 encoder pads the end of its file. Chrome and Firefox trim the padding when\nthey decode it, Safari keeps it, so a looping MP3 has a short silence at every\nloop there (27 ms on a 12 s clip at 22 kHz, measured). Declare a region of the\nloop's exact length and loop on that: the voice never reaches the padding.\n\n```js\nawait loader.preload([{\n    name: \"ambience\", type: \"audio\", src: \"data/sfx/\",\n    sprite: { loop: [0, 12000] },\n}]);\naudio.playTrack(\"ambience\", { sprite: \"loop\", volume: 0.6 });\n// or, as a plain clip: audio.play(\"ambience\", { sprite: \"loop\", loop: true })\n```\n\nOgg Vorbis carries no such padding.\n\nThe optional third element marks a region as looping: it loops unless `play()` is\ntold `loop: false` (before 20.11 it only looped when `loop: true` was passed as\nwell). `play()`'s second argument\ntakes either the original `loop` boolean or a `PlayOptions` object\n(`sprite`, `loop`, `onend`, `volume`, plus the spatial `follow`, `at` and\n`stopWithTarget`) — both forms work. Object fields win over the positional\n`onend` / `volume` arguments when both are supplied.\n\n## Clip state\n\n```js\naudio.duration(\"theme\");        // seconds — 0 while still loading\naudio.duration(\"sfx\", id);      // duration of the region THAT instance plays\naudio.playing(\"theme\");         // is anything playing?\naudio.playing(\"theme\", id);     // is that instance playing?\naudio.state(\"theme\");           // \"unloaded\" | \"loading\" | \"loaded\"\n```\n\n`stop()`, `pause()` and `resume()` take an optional instance `id` too; omit it\nand the whole group is affected. `audio.stop()` with no arguments at all stops\nevery sound. `audio.seek(name)` reads the position, `audio.seek(name, s, id?)`\nwrites it; `audio.rate(name, r, id?)` changes speed and pitch together\n(`0.5`..`4.0`, `1` is normal) and `audio.fade(name, from, to, ms, id?)` ramps\nthe volume (`0`..`1`, over milliseconds).\n\n## Volume and muting\n\n```js\naudio.setVolume(0.5);           // global, 0..1\naudio.getVolume();              // read it back\naudio.mute(\"cling\");            // one clip  — mute(name, id?, shouldMute = true)\naudio.unmute(\"cling\");\naudio.muteAll();                // everything (audio.disable() is an alias)\naudio.unmuteAll();              // (audio.enable() is an alias)\naudio.muted();                  // global mute state\n```\n\n`audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`\nalso clears the current-track pointer if it named that clip.\n\n### Losing focus mutes everything\n\nWhen the window loses focus the engine mutes all audio, and unmutes it on\nfocus. That covers sound effects, procedural `tone` / `noise` and the mixer\neffects, not just the music track that `state.pause()` pauses. Set\n`pauseOnBlur: false` in the application settings if a clip has to keep playing\nin the background.\n\n## Spatial audio\n\n```js\naudio.position(\"engine\", x, y, z);\naudio.orientation(\"engine\", x, y, z);\naudio.stereo(\"engine\", -1);     // -1 left … 1 right\naudio.panner(\"engine\", { coneInnerAngle: 90, /* … */ });\n```\n\n`position(name, x, y = 0, z = -0.5, id?)` and\n`orientation(name, x, y, z, id?)` both default `id` to \"the whole group\".\nCalled with just the clip name, each of the four returns the current value —\n`stereo` gives `0` and `position` gives `[0, 0, 0]` before they have ever been\nset.\n\n### Let the engine place the sound: `follow` and `at`\n\nThe shortest path to positional audio is a play option. Both take **world\npixels**, the same numbers as `pos.x` / `pos.y`, and the engine converts to\naudio space for you:\n\n```js\n// tracks the renderable every frame, and stops when it is destroyed\naudio.play(\"ufo\", { follow: this, stopWithTarget: true });\n\n// pinned to a fixed world point\naudio.play(\"portal\", { at: { x: 1200, y: 480 } });\n```\n\n`follow` and `at` are mutually exclusive: passing both throws, rather than\nquietly picking one. `follow` re-reads the target's **absolute** position each\nframe, so a renderable nested in a moving container is placed correctly. `stopWithTarget` is what you\nwant for a sound that belongs to an entity: without it a looping clip on a\ndestroyed renderable keeps playing from wherever it last was. `audio.unfollow(id)`\ndetaches a sound and leaves it playing where it is.\n\n### Move the listener, don't do the subtraction yourself\n\n```js\naudio.setListener(player);                  // tracked every frame\naudio.listener(x, y, z);                    // or place it by hand\naudio.listener();                           // → [x, y, z] in world pixels\naudio.listenerOrientation(fx, fy, fz, ux, uy, uz);\naudio.setListener(null);                    // stop tracking\n```\n\nWith a listener in the world, source positions are **absolute world\ncoordinates**, not offsets. The older idiom — passing `source.pos.x -\nplayer.pos.x` every frame with the ear stuck at the origin — still works, but\nonly if you leave the listener at `(0, 0, 0)`; mixing the two double-counts the\nplayer's position.\n\n`setListener` places the ear at the target's absolute position after the world\nupdates. A plain renderable contributes position only, so a side-scroller keeps\nthe default \"facing into the screen\" and gets left-right panning plus distance\nfalloff from the one call.\n\nA `Camera3d` target also contributes its **orientation**, taken from its own\nbasis, so turning the camera swings the stereo image with it. That is the whole\nof what a posed 3D scene needs:\n\n```js\naudio.setListener(app.viewport);                 // in a 3D scene this is a Camera3d\naudio.play(\"drone\", { follow: saucer });         // scene coordinates, directly\n```\n\n**Y is down.** Every one of these calls takes and returns melonJS world\ncoordinates, where y grows downward; the Y-up flip Web Audio wants happens\ninside the engine. Pass `pos.y` as it is.\n\n### The defaults are tuned for pixels\n\n`audio.setSpatialDefaults()` / `getSpatialDefaults()` set the panner attributes\na sound placed through `follow` or `at` starts from, and the shipped values\nassume a 2D game measured in pixels:\n\n| attribute | default |\n|---|---|\n| `refDistance` | `240` |\n| `maxDistance` | `10000` |\n| `panningModel` | `\"equalpower\"` |\n| `distanceModel` | `\"inverse\"` |\n\nWith those, a placed sound is at full volume out to 240 px and then halves\neach time the distance doubles: about half at 480, a quarter at 960, roughly\nan eighth at 2000. `maxDistance` is where it stops getting quieter.\n\n**They apply to placed sounds only.** A plain `audio.play()` followed by a\nbare `audio.position()` keeps WebAudio's own defaults, where `refDistance` is\n`1` METRE and a source 100 px out is already near silent. That path is\nunchanged, which is what keeps old code working.\n\nWiden or tighten the field once, globally:\n\n```js\naudio.setSpatialDefaults({ refDistance: 400, maxDistance: 20000 });\n```\n\n`audio.panner(name, attrs, id?)` still overrides per clip or per voice, and it\nmerges over the current values rather than replacing them, so writing one field\nno longer resets the rest. Called without an `id` it writes the **group**\ndefaults, which is what a clip-wide tuning call looks like.\n\nFor a 3D scene where front-to-back matters, `panningModel: \"HRTF\"` is worth the\ncost; for a 2D one it mostly muddies the stereo image.\n\n### `stereo` and `position`: last writer wins\n\nBoth drive the same per-instance panner node, and the engine swaps the node to\nmatch whichever you called **last**: `stereo()` after `position()` gives you\nplain left-right panning, and `position()` after `stereo()` gives you 3D\nfalloff. Neither silently does nothing any more, and the order you call them in\nno longer matters.\n\nStill pick one per voice and stay with it. A 2D game usually wants `stereo()`\nalone; reach for `position()` (or `follow` / `at`) when distance falloff\nmatters.\n\n**Spatial audio is WebAudio-only.** On a clip loaded with `stream: true` (or\n`html5: true`) `stereo` / `position` / `orientation` return early and do\nnothing — which is exactly the case for the long music tracks people reach for\nfirst. `follow` and `at` are equally inert on a streamed clip.\n\n## Procedural sound\n\nFor beeps, blips and noise without shipping files. Both take **one options\nobject**, and `duration` (in seconds) is required:\n\n```js\naudio.tone({ freq: 440, duration: 0.2, gain: 0.3, pan: -0.5 });\naudio.tone({ freq: [440, 660, 880], duration: 0.4 });   // layered partials\naudio.noise({ duration: 0.5, filter: { type: \"lowpass\", frequency: 800 } });\n```\n\n`tone` also takes `wave` (default `\"sine\"`), `attack` (`0.005`) and `pitchSlide`\n(`1`). `noise` takes `type` (`\"white\"` | `\"pink\"` | `\"brown\"`, default\n`\"white\"`), `attack`, `pan`, `filter` and `filterSweep`. `gain` defaults to `0.1`\non both.\n\n### Sequencing: `delay`, not `setTimeout`\n\nBoth take `delay` — seconds before the sound starts, scheduled on the **audio\nclock**. That is what builds a multi-part sound: a stinger's second note, an\nexplosion's double-tap.\n\n```js\naudio.tone({ freq: 392, duration: 0.12 });\naudio.tone({ freq: 587, duration: 0.18, delay: 0.11 });\naudio.tone({ freq: [784, 1176], duration: 0.75, delay: 0.23 });\n```\n\nDo not reach for `setTimeout` here. A timer fires on the main thread, so a busy\nframe slips the note late and the sequence audibly loses time — the fault is\neasy to blame on the sound design rather than the scheduling. `delay` is\nsample-accurate because the whole envelope is scheduled up front.\n\nBoth are **silent no-ops** when there is no WebAudio context — they bail on\n`getAudioContext() === null` rather than throwing. Neither needs the clip to be\nloaded; neither goes through the file-playback path at all.\n\n`audio.getAudioContext()` and `audio.getMasterGain()` are the escape hatches for\ncustom Web Audio work; both return `null` when audio is unavailable, so guard.\nConnect a custom graph to the master gain rather than `ctx.destination` if you\nwant it to respect `setVolume` / `muteAll`.\n\n## Effects on all game audio: reverb, echo, compressor\n\nThere is no built-in reverb, delay, distortion or compressor, and no per-clip\nfilter on file playback. There does not need to be one for the common case:\nevery buffered clip and every `tone` / `noise` passes through the master gain\non its way to the speakers, so an effect inserted **after** it applies to the\nwhole mix.\n\n```js\nconst ctx = audio.getAudioContext();   // creates the context if needed\nconst master = audio.getMasterGain();\nif (ctx && master) {                   // null when audio is unavailable\n    const comp = ctx.createDynamicsCompressor();\n    master.disconnect();               // master -> speakers becomes\n    master.connect(comp);              // master -> compressor -> speakers\n    comp.connect(ctx.destination);\n}\n```\n\nAny Web Audio node or chain works the same way: a `ConvolverNode` with an\nimpulse response for reverb, a `DelayNode` with a feedback gain for echo, a\n`BiquadFilterNode` for an underwater low-pass, a `WaveShaperNode` for\ndistortion. Do it once, at startup; the master gain lives as long as the\ncontext.\n\n- **`setVolume` / `muteAll` still work**, because they act on the master gain,\n  which is now upstream of the effect.\n- **Streamed clips are not affected.** A `stream: true` (`html5: true`) clip\n  plays through an `<audio>` element and never reaches the master gain — which\n  is usually the music. Preload the music buffered if it must go through the\n  effect.\n- **It is all or nothing.** The per-instance nodes are not public, so an effect\n  on one clip only (muffling one sound behind a wall) is not possible through\n  this route; `noise` has its own `filter` for generated sounds.\n- To remove it, reverse the wiring: `master.disconnect(); master.connect(ctx.destination);`.\n\n## Autoplay: sound is silent until the first gesture\n\nBrowsers create the audio context in the `suspended` state and keep it there\nuntil a user interaction. Nothing errors — audio simply does not play, which\nreads as a bug. melonJS installs one-shot `touchstart` / `touchend` / `click` /\n`keydown` unlock listeners on `document`, so the first real gesture anywhere\nunlocks it and fires the clip's `on.unlock` callback. Design for it: start music\non the first click or key press, not on load. The usual way to guarantee one is\na gate screen — a splash, a \"press any key\", or a health/photosensitivity notice\n— whose dismissal *is* the gesture, so the menu behind it can start its music\nunconditionally. Note the unlock listens for `keydown` too, so a keyboard-only\nplayer is covered.\n\n### The context can stop again, long after it was unlocked\n\nUnlocking is only the first half. A context that has been running can be\nstopped later: melonJS suspends it itself after thirty idle seconds to save\npower, and browsers stop it for their own reasons, a backgrounded tab among\nthem. Recovery is automatic and you do not have to write it: a user gesture\nor the tab coming back to the front revives it, and a deliberate idle\nsuspension is left alone so the power saving still works.\n\n**The symptom worth memorising**, because it points straight at the cause:\nif `tone` and `noise` are audible while loaded clips are silent, the context\nis healthy and the engine's playback state is wrong. The generators only need\na live context, where a clip goes through the engine's state machine and can\nbe parked waiting for a resume. It is never \"the context died\".\n\nBoth routes ask the same owner to resume, so hearing a tone tells you the\nhardware is back; it tells you nothing about whether the clips waiting on the\nengine's state have been let go.\n\nThat asymmetry was a real Safari-only bug: thirty idle seconds before the\nfirst click left the engine waiting on a suspend the browser never confirmed,\nso every clip queued for the life of the page while generated sound played\nnormally. Fixed in 20.9, and the lesson generalises: a clip that will not play\nis a state question, not a hardware one.\n\nPlayback also starts **asynchronously**: `play()` returns an id while the\ninstance is still starting, so `playing()` can legitimately be `false` on the\nvery next line.\n\n## Load-time options\n\n```js\n{ name: \"sfx\", type: \"audio\", src: \"data/sfx/\",\n  sprite: { jump: [0, 450] },\n  pool: 5,            // finished instances kept for reuse — NOT a concurrency cap\n  rate: 1.0,          // initial playback rate, 0.5..4.0\n  mute: false,\n  loop: false,\n  autoplay: false,\n  preload: true,      // or \"metadata\"\n  format: \"mp3\",      // explicit hint when the URL has no usable extension\n  stream: true,       // long music: stream rather than decode into memory\n  on: { play() {}, end() {}, stop() {}, unlock() {} },\n}\n```\n\n`pool` (default `5`) is a **reuse cap on finished voices**, not a limit on\nsimultaneous playback: when the number of ended voices exceeds it, the surplus\nare disconnected and dropped. Raise it for a clip triggered in rapid bursts, not\nto allow more overlap.\n\nUse `stream: true` for long music tracks — it plays through an HTML5 `<audio>`\nelement instead of decoding the whole file into memory. (`html5: true` is the\nsame switch under its backend name.) `on` accepts the full lifecycle set:\n`play`, `pause`, `stop`, `end`, `fade`, `seek`, `rate`, `volume`, `mute`,\n`unlock`.\n\nOutside a preload, `audio.load(asset, onload?, onerror?)` takes the same\ndescriptor and loads one clip at runtime — for a level's sounds fetched when\nthe level starts. The loader calls the same function, so the same rules apply.\n\nRe-preloading a manifest that already contains a loaded clip is a no-op —\n`audio.unload(name)` first if you genuinely want to reload it.\n\n**Credentials use the same global switch as every other asset type** —\n`loader.setOptions({ withCredentials: true })`, not an audio-specific option.\nBuffered audio fetches through the shared loader path, so it honours it exactly\nas an image or a JSON file does. A `stream: true` clip does not: it plays\nthrough an `<audio>` element, which needs a `crossorigin` attribute rather than\nfetch credentials. Preload a clip buffered if it sits behind a login.\n\n**Failing clips and `setStopOnAudioError(false)`.** The default is to fail the\npreload when a clip cannot be loaded after its retries: the error reaches the\nloader, so `await loader.preload(...)` rejects and your `catch` runs, and the\nreason is logged. Turn the flag off and the engine mutes audio, warns\n(`failed loading <name>, disabling audio`) and lets the load *continue* — the\npreload finishes and the game reaches the next scene without sound.\n\nBefore 20.4 both halves were broken: with the flag off it disabled audio and\nfailed the preload anyway, and a clip whose bytes arrived but would not decode\n(a missing file served as HTML by an SPA rewrite, say) reported nothing at all\neither way — blank screen, empty console.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| audio assets 404 | `src` given as a full filename instead of a directory prefix |\n| `target audio extension(s) should be set...` thrown at preload | `audio.init()` not called before preloading |\n| `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |\n| nothing plays until the user clicks | browser autoplay policy — expected, design around it |\n| `playing()` is false right after `play()` | playback starts asynchronously |\n| music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |\n| two music tracks playing at once, and `stopTrack()` only silences one | melonJS before 20.9.0: `playTrack` did not stop the previous track, so the scene that started it had to `stopTrack()` on the way out |\n| the music restarts when returning to a screen | `playTrack` with the track already playing restarts it; don't call it again if the bed should carry on |\n| `stereo` / `position` do nothing | the clip was loaded with `stream: true` — spatial is WebAudio-only |\n| one format works, another does not | that codec is not in the `audio.init()` list |\n| audio behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses |\n| a streamed clip ignores `withCredentials` | `stream: true` plays through an `<audio>` element; preload it buffered instead |\n| `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |\n| `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |\n| a positioned sound is silent, or only audible right next to the player | placed by hand with `audio.position()`, which keeps WebAudio's metre defaults (`refDistance: 1`) — use `follow` / `at`, or set `refDistance` in pixels with `audio.panner()` |\n| a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |\n| a followed sound pans to the wrong side as the player moves | the listener was moved **and** positions are still relative to the player — with a listener in the world, pass absolute world coordinates |\n| a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |\n| a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |\n| a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |\n| a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see \"A loop with no gap\") |\n| sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |\n\n## Related skills\n\n- `melonjs-getting-started` — preloading and asset ordering\n\n<!-- File: packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-camera-and-drawing\n\nPath: `packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-camera-and-drawing/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-deployment/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-deployment\n\nPath: `packages/melonjs/skills/melonjs-deployment/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-deployment/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-display-and-scaling\n\nPath: `packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-display-and-scaling/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-effects-and-shaders/SKILL.md (Tokens: ~8676 | Category: anthropic-skill) -->\n---\nname: melonjs-effects-and-shaders\ndescription: \"Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: Tween, easing, animate an effect, ShaderEffect, addPostEffect, removePostEffect, getPostEffect, postEffectNeedsCapture, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter.\"\nlicense: MIT\n---\n\n# Post effects and custom shaders\n\n> melonJS ships a catalogue of post effects and a dual-language shader system.\n> The recurring trap is that every shader path needs a GPU backend and\n> **degrades to nothing** on Canvas rather than erroring. Blend modes are the\n> exception — those work on all three renderers.\n\n## Applying an effect\n\nEffects attach to a renderable **or** to a camera (for full-screen grading):\n\n```js\nimport { VignetteEffect, GlowEffect } from \"melonjs\";\n\nsprite.addPostEffect(new GlowEffect(app.renderer));\napp.viewport.addPostEffect(new VignetteEffect(app.renderer));\n```\n\n`renderable.shader = …` is **deprecated since 19.2.0**, and its setter destroys\nwhatever it replaces. Use `addPostEffect` / `getPostEffect` / `removePostEffect`.\n\n### A camera effect covers the HUD too\n\nA camera's post-effect brackets the **entire** world draw — floating children\nincluded. So the obvious way to write a full-screen pass also washes over every\nHUD label in that world, which is rarely what you want.\n\nTo land a pass *after* the world but *before* the HUD, host the effect on a\nscreen-filling floating renderable ordered below the HUD's z instead:\n\n```js\nconst quad = new Sprite(0, 0, { image: anyImage });\nquad.anchorPoint.set(0, 0);\nquad.scale(viewW / anyImage.width, viewH / anyImage.height);\nquad.floating = true;                 // its own uv is now screen space\nquad.blendMode = \"additive\";\nquad.addPostEffect(myEffect);\nworld.addChild(quad, HUD_Z - 10);     // 2D: below the labels. See the note.\n```\n\nTwo things follow from the quad filling the frame: its own `uv` is screen space\n(so a pass like this needs none of the `screen_uv` / `screen_texture` builtins),\nand the incoming `color` is the quad's own texture, which a body that paints\nfrom scratch can ignore entirely.\n\n**Depth here is easy to get backwards, and the rule FLIPS between the two sort\nmodes.** Under the default `sortOn = \"z\"` a higher z draws later, i.e. on top,\nso `HUD_Z - 10` puts the pass under the labels as written above. Under\n`sortOn = \"depth\"`, which every `Camera3d` scene sets, a floating child's sort\nkey is `pos.z * pos.z` and the walk paints far to near, so the SMALLER\nmagnitude is nearer and draws on top — `HUD_Z - 10` there puts the pass OVER\nthe labels, the opposite of the comment.\n\nSo in a 3D scene give the overlay a larger magnitude than the HUD, and give\nthe HUD a small one (`melonjs-3d` and `melonjs-ui-and-text` both say a HUD\nwants a small depth, not the huge z that would put it on top in 2D).\n\nEither way, verify it rather than reasoning about it: return a flat colour from\nthe body for one frame and see what it tints.\n\n## A renderable that draws with PRIMITIVES has to ask to be captured\n\nOne effect is applied the cheap way: the renderable is drawn with the effect's\nown program instead of being captured offscreen and post-processed, which costs\nno render target. That is equivalent only when everything the renderable draws\nis a textured quad. `fillRect`, `strokeRect` and `renderer.fill(shape)` go\nthrough a batcher that never reads that shader, so **one** effect on such a\nrenderable would silently do nothing, while **two** would work, because a chain\nalways captures.\n\nSo a renderable of your own that draws with primitives says so:\n\n```js\nclass Bar extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.postEffectNeedsCapture = true;   // ← or one effect is a no-op\n    }\n    draw(renderer) {\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\n`ProgressBar` and `Trail` set it for you, and a sprite needs nothing: the flag\ndefaults to `false` and only ever changes the single-effect case.\n\n## Toggle with `enabled`, do not remove\n\n**`removePostEffect()` destroys the effect** — it calls `effect.destroy()` and\nfrees GPU resources, so the instance cannot be re-added. Same for\n`clearPostEffects()` and for reassigning the deprecated `.shader`.\n\n```js\n// ✗ destroys it; re-adding later fails or forces a recompile\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nAn effect with `effect.shared === true` opts out of auto-destroy because it is\nreused across renderables.\n\n## Built-in effects\n\nTwenty-three presets, every one constructed as `new XEffect(renderer, options)`\nand every one shipping both a GLSL and a WGSL body, so they all run on either\nGPU backend: `VignetteEffect`, `ScanlineEffect` (optional CRT curvature),\n`BloomEffect`, `GlowEffect`, `BlurEffect`, `PixelateEffect`,\n`ChromaticAberrationEffect`, `DropShadowEffect`, `FlashEffect`,\n`OutlineEffect`, `ColorMatrixEffect`, `DesaturateEffect`, `DissolveEffect`,\n`TintPulseEffect`, `WaveEffect`, `SwayEffect`, `InvertEffect`, `SepiaEffect`,\n`HologramEffect`, `ShineEffect`, `RadialGradientEffect`, `SquishEffect`,\n`ToneMappingEffect`.\n\n**Colors take an alpha.** Every color option and `setColor` takes `[r, g, b]`\nor `[r, g, b, a]`, each 0.0 to 1.0, and the alpha is the color's own opacity: a\nhalf-transparent outline, a glow at half strength, a flash that only half\ncovers. `[r, g, b]` is opaque, as it always was. Any other length throws.\n`RadialGradientEffect` takes a `Color` instead, and its alpha scales the spot.\n\n```js\nsprite.addPostEffect(new OutlineEffect(app.renderer, { color: [1, 1, 1, 0.5] }));\n```\n\n**On a sprite cut from an atlas or a tileset, the effects that read around a\npixel keep to the sprite's own frame.** `GlowEffect`, `OutlineEffect`,\n`BlurEffect`, `DropShadowEffect`, `ChromaticAberrationEffect`, `WaveEffect`,\n`SwayEffect`, `PixelateEffect` and `BloomEffect` read the neighbouring frames\nas transparent, as if the sprite were drawn on its own, so they never show\nalong its edges and the atlas needs no padding for them. Past the texture's own\nedge (a standalone image, or the outside of an atlas) nothing changes. Their `textureSize` is still the size of the whole atlas image: it is\nwhat turns a width in texels into a distance in the texture.\n\n`SwayEffect` leans a sprite or a layer from side to side, as if in a breeze: the\ntop sways most and everything from its `anchor` row down stays put, so a tree's\ntrunk or a reed's root stays planted. It suits anything planted at its bottom;\nsomething hanging from its top, like a banner, needs its own shader. It is a\n`WaveEffect`, which takes the same `anchor`, so it adds no shader. Drive it with\n`setTime(seconds)` every frame, like any time-based effect; it wraps the phase\nitself, so a raw clock never costs precision on a phone's medium-precision\nfloats.\n\n```js\nconst sway = new SwayEffect(app.renderer, { strength: 0.02, anchor: 0.9 });\ntree.addPostEffect(sway);\n// each frame\nsway.setTime(timer.getTime() / 1000);\n```\n\nRows are measured across the texture the effect samples, so on a sprite cut from\nan atlas the anchor is a row of the atlas: it suits a standalone image or a\nlayer best.\n\n`RadialGradientEffect` is a procedural soft spot: solid at the centre of its\nhost quad, fading linearly to transparent at the edge, and elliptical for free\non a non-square quad. It is the one to reach for instead of baking a disc into\na canvas, for a pickup highlight, a trigger marker or a damage flash. Its\nfalloff assumes the quad samples a FULL texture rect, so pair it with a\nstandalone image or `Renderer.getWhitePixel()` rather than a sprite that draws\na sub-region of an atlas, where the centre would land off-quad.\n\n`SquishEffect` bulges a sprite's middle out (`setBulge(0.3)`) or pinches it in\n(`setBulge(-0.3)`), its top and bottom rows staying put. Bulging out widens the\nsprite past its own frame, and the effect draws on a wider quad to make room,\nso the art needs no transparent margin. On its own it is a jelly wobble; paired\nwith a change of height it is **squash and stretch that keeps the volume**:\nscale the sprite to `scaleY` of its height about its feet and set the bulge to\n`1 - scaleY`, so a stretched sprite pinches in and a squashed one bulges out.\nEase `scaleY` back to 1 with a short `Tween.Easing.Back.Out`; an elastic ease\nkeeps it trembling long after the landing.\n\n```js\nconst squish = new SquishEffect(app.renderer);\nplayer.addPostEffect(squish);\n// every frame, from the player's update()\nconst feet = player.height * (1 - player.anchorPoint.y);\nplayer.currentTransform\n    .identity()\n    .translate(0, feet)\n    .scale(1, scaleY)\n    .translate(0, -feet);\nsquish.setBulge(1 - scaleY);\n```\n\nApply that from the sprite's own `update()`, not from a tween's `onUpdate`. A\nphysics adapter that mirrors a body's angle (Matter does) rewrites\n`currentTransform` on every step, before the renderables update, so a scale set\nanywhere else is gone by the time the sprite is drawn. Let the tween animate a\nplain `{ scaleY }` object and read it in `update()`.\n\nIt is made for a sprite carrying it as its only effect: chained with another,\nor on the camera, the frame it measures against is the whole capture, so it\nsquishes the whole frame. The bulge is outside the sprite's bounds, so culling\nand pointer events ignore it.\n\n**`BloomEffect` and `GlowEffect` are not the same thing**, and reaching for the\nwrong one is the usual disappointment here. `GlowEffect` draws a halo OUTSIDE a\nsprite's silhouette: its body returns the colour untouched wherever alpha is\nalready non-zero, so on an opaque frame it returns early on every fragment and\na camera-wide glow does nothing at all. `BloomEffect` is the screen bloom —\nbright pixels bleed into their neighbours — and it is the one to put on the\ncamera:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\n// only the brightest highlights, with a wider halo\napp.viewport.addPostEffect(\n    new BloomEffect(app.renderer, { threshold: 0.85, radius: 4 }),\n);\n```\n\nIts tap offsets are in texels, so it takes its size from the renderer at\nconstruction; call `setTextureSize(w, h)` from an `event.ONRESIZE` handler, or\nwhen applying it to something smaller than the frame.\n\n**`ToneMappingEffect` goes after it, and goes last.** Bloom spreads the bright\nparts; the tone map is what stops the sum of the two clipping flat at white,\nwhich is the difference between an effect that reads as light and one that\nreads as a white sticker. `exposure` lifts the image into the curve, and the\ncurve (`aces`, `reinhard` or `exponential`) is baked at construction:\n\n```js\napp.viewport.addPostEffect(new BloomEffect(app.renderer));\napp.viewport.addPostEffect(\n    new ToneMappingEffect(app.renderer, { exposure: 1.6 }),\n);\n```\n\nIt is a grade, not a colour pipeline. By default melonJS renders into `RGBA8`\neverywhere, so the frame reaching it is already clamped and `exposure` past 1\nis what gives the curve range to compress.\n\n**`hdr: true` in the application settings changes that**: render targets\nbecome half-float, so the chain carries values above 1 instead of clamping at\nevery write, and the bloom threshold starts discriminating genuinely\nover-bright pixels rather than merely bright ones. Off by default: it costs\nGPU memory on the camera's two render targets and depends on the driver.\n\nOn its own it is **not HDR output**: the frame is still presented in SDR, so\nanything above 1 clamps on the final blit. What it buys is that nothing clamped\nBEFORE then. Removing that last clamp is a second, separate opt-in —\n`hdrOutput`, below — and it is WebGPU-only.\n\n```js\nconst app = new Application(1024, 576, { hdr: true, … });\n// …and check what you actually got, since a driver can refuse it\nif (app.renderer.supportsHDR) { /* the chain now carries values above 1 */ }\n```\n\nBranch the GRADE on that flag, not just the feature. An exposure under 1 and a\nbloom threshold at 1 make sense only where values above 1 exist; applied to a\nchain that clamps, the same numbers tone map an already display-referred frame\nand switch the bloom off, which reads as a dark, flat picture rather than as a\nmissing feature. Where there is no headroom, squeeze each emitter into range in\nits own shader instead and put the bloom threshold back under 1.\n\n**Nothing is added to your chain.** `hdr` is additive: the same frame, plus\nadditive content that no longer saturates per write and a threshold that means\nsomething. Anything still above 1 at the end clamps on the blit, as it always\ndid.\n\nA tone map is therefore a decision, not a consequence, and it is a bigger one\nthan it looks. Every curve maps 1 to less than 1, so a 2D look built ON the\nclamp — clipped white specular, saturated neon — goes matte the moment you add\none, at any exposure. Add one when your art is authored in linear light and you\nwant the highlights rolled off; skip it when the clipping IS the look.\n\nIt applies to the CAMERA chain only. A sprite's chain keeps 8-bit targets on\npurpose: its final blit composites with `ONE_MINUS_SRC_ALPHA`, and additive\nblending accumulates alpha past 1 in a float target, so that factor would go\nnegative and subtract the backdrop.\n\n### Once it is on: putting values above 1 into the frame\n\n`hdr: true` on its own changes nothing visible. It removes a ceiling; something\nstill has to reach for it. No single draw exceeds 1 on its own, so a game that\nturns the setting on and changes nothing else renders very nearly the frame it\ndid before and looks like the feature is broken. The exception is worth knowing\nbecause it is free: additive content that already overlapped was being clipped\nat every write, and stops being.\n\nWhat produces over-bright pixels:\n\n```js\n// a built-in colour matrix, scaled past 1 — no custom shader needed\nsprite.addPostEffect(new ColorMatrixEffect(app.renderer).brightness(4));\n\n// in 3D, both of these were always unbounded; the 8-bit target was the only\n// thing destroying the range. `emissive` is a 3-component Float32Array, set\n// from the mesh settings or replaced wholesale — it is not a Color\nconst mesh = new Mesh(x, y, { emissive: [2.5, 1.8, 0.4], /* … */ });\nlight.intensity = 3;\n\n// and additive blending, which accumulates past 1 once the target can hold it\nsprite.blendMode = \"additive\";\n```\n\n**One effect, not two.** A renderable carrying exactly ONE post effect takes\nthe renderer's `customShader` fast path: no offscreen target, and it draws\nstraight into the camera's half-float target with its range intact. A chain of\ntwo or more is routed through a pooled intermediate that stays 8 bits per\nchannel on purpose, so the energy is clamped there, before the camera ever sees\nit. Adding a second effect to an emitter is enough to silently undo its\nheadroom.\n\n### Everything inside the camera is graded, including your UI\n\nThe tone curve runs on the camera's whole frame. A `floating` HUD is still\ninside it, so a label authored at display white arrives at the curve as 1 and\nleaves at `1 - exp(-exposure)` — at exposure 0.45 that is 0.36, and the HUD\ncomes out at roughly a third of its brightness for no reason the author can\nsee.\n\nUI in an HDR chain has to be authored for that chain, exactly as the scene is:\n\n```js\n// drive the readout so the curve lands it back at display white. `shared`\n// keeps one instance across every element: it opts the effect out of the\n// per-renderable auto-destroy, so the first element torn down does not take\n// the program the others are still using\nconst energy = new ColorMatrixEffect(app.renderer).brightness(2);\nenergy.shared = true;\nfor (const element of hudElements) element.addPostEffect(energy);\n```\n\nPut it on each ELEMENT, not on their container: `beginPostEffect` clears\n`customShader` for any renderable with no effects of its own, so a container's\nshader is thrown away by its first child. The alternative is to keep the UI out\nof the camera pass entirely, by overriding `Stage.draw` and drawing it after\n`super.draw(...)` — at the cost of the bloom that makes additive UI glow.\n\nThe quad trick under *A camera effect covers the HUD too* does not help here.\nIt works by hosting a pass BELOW the HUD's z, and a tone map cannot live there:\nit has to be the last thing before the blit to the 8-bit canvas, which is the\ncamera. Anything the camera draws goes through it.\n\nPick the multiplier by measuring against the no-headroom build, not by\ninverting the curve. Inverting it over-estimates, because additive UI is inside\nthe BLOOM too and driving it harder also makes it spill wider into the same\npixels.\n\n### `hdrOutput`: actually presenting in HDR, and what it costs you\n\n`hdr` gives the chain headroom and then throws it away at the very last step,\nbecause the surface the browser presents into is 8 bits per channel. Values\nabove 1 clamp there, exactly as they did before the setting existed.\n`hdrOutput` changes that surface, so they reach the compositor instead:\n\n```js\nconst app = new Application(1024, 576, { hdr: true, hdrOutput: true, … });\nif (app.renderer.supportsHDROutput) { /* the display is getting values above 1 */ }\n\napp.renderer.setHDROutput(false);   // and at runtime, for a settings screen\n```\n\n**It is WebGPU-only, and that is not a melonJS limitation.** The WebGPU\nspecification defines a tone mapping mode on the canvas configuration, and two\nengines ship it. The WebGL2 equivalent needs a drawing-buffer tone mapping mode\nthat is still an unapproved specification change with no implementation in any\nbrowser; the colour-space half of it exists in Chrome behind\n`--enable-experimental-web-platform-features`, which is not something you can\nask a player to enable. So the WebGL2 backend reports `false`, warns once if\nyou asked, and presents in SDR as it always has.\n\nThat makes this the first setting where **the same machine answers differently\ndepending on which backend `video.AUTO` picked**. Branch on\n`renderer.supportsHDROutput`, never on the setting.\n\n**It requires `hdr`** — the last pass blits from the camera's target, so if\nthat target is 8 bits there is nothing above 1 left to present and the extra\ncost would buy an identical picture. Turning `hdr` off turns this off with it.\n\n**It changes how your game looks, on an HDR display.** Everything else here is\ncareful not to: `hdr` is additive, the tone curve defaults to none. This one\nremoves the final clamp, so anything you drove above 1 — an additive stack, an\nemitter, a `brightness()` above 1 — gets genuinely brighter rather than\nsaturating at white. That is the point of it, and it is why it is a separate\nopt-in rather than part of `hdr`. Two consequences worth planning for:\n\n- a look built on clipped whites will not survive it unchanged, because the\n  clipping is what you were art-directing against\n- a tone curve and `hdrOutput` pull in opposite directions. A curve exists to\n  fit values into `[0, 1]`; if you have removed the clamp, either drop the\n  curve or give it a white point high enough to leave the top of the range\n  alone\n- **`\"aces\"` cannot produce HDR output.** Its expression clamps to `[0, 1]`\n  internally, so everything past the shoulder collapses to one value before\n  the white point scales it — different bright things come out equally bright.\n  Under `hdrOutput` use `\"reinhard\"` or `\"exponential\"`, which approach 1\n  asymptotically without clamping and so keep their ordering when the white\n  point lifts them past it, or use no curve at all\n\n**What it is not.** It does not make an SDR display brighter, and there is no\nway to ask melonJS how bright the panel is — `matchMedia(\"(dynamic-range:\nhigh)\")` is the closest the platform gets. Calibrating in nits, the way a\nconsole settings screen does, is not reachable from the web platform today.\n\n### Exposure, and the runtime toggle\n\nUnder `hdr` the useful exposure is often **below** 1, which surprises people who\nhave only used the effect as an LDR grade. The curve has to work where it\nseparates: at exposure 1 the ACES shoulder maps 2, 4 and 8 to 0.86, 0.96 and\n0.99, so genuinely different energies all arrive at white and the headroom buys\nnothing you can see. Lower exposure spreads them out. The right value is a\nproperty of how bright your scene is authored, so measure it; there is no\ndefault that is right for two different games.\n\nA tone curve is a setting rather than an effect you wire up:\n\n```js\nrenderer.setToneMapping(\"aces\", { exposure: 0.4, white: 6 });\nrenderer.setToneMapping(\"none\");                       // and off again\n```\n\n`\"none\"` by default. The renderer keeps the curve LAST on the camera, so\nwhatever you put in front of it is mapped along with everything else. Changing\nthe mode rebuilds one shader (the curve is baked); `exposure` and `white` are\nuniform writes, so a settings screen can drive them live. `white` is the value\nthat comes out as display white — the knob a \"brightness\" or \"peak\" slider\nwants, since exposure alone drags the midtones up with the highlights.\n\n`renderer.setHDR(enable)` moves the whole pipeline at runtime, the way\n`setAntiAlias` does, for a settings toggle. It empties the render-target pool\nwhen the format actually changes, so it is not something to drive per frame.\nThe boot header reports what happened either way:\n\n```\nresolution: requested 1024x576 (HDR), got 1024x576 (SDR)   ← the driver refused\n```\n\nFor grading without writing a shader yourself, the camera has a colour matrix:\n\n```js\napp.viewport.colorMatrix.contrast(1.1).saturate(1.1);\n```\n\nIt is still *implemented* as one: a non-identity matrix makes the camera append\nan internal `ColorMatrixEffect` to its post-effect chain for that frame, so it\nneeds a GPU backend like everything else here.\n\n## Custom effects are dual-language\n\nA custom `ShaderEffect` supplies a fragment **body**, not a whole program. To run\non both GPU backends it needs GLSL *and* WGSL — a GLSL-only effect silently does\nnothing when `video.AUTO` lands on WebGPU.\n\n```js\nconst effect = new ShaderEffect(app.renderer, { glsl, wgsl });\neffect.setUniform(\"uStrength\", 0.5);\neffect.setTexture(\"uScene\", tex);\neffect.setTime(timer.getTime() / 1000);   // setTime takes SECONDS; getTime is ms\n```\n\nUniform names are shared across the two bodies — in WGSL they are the members of\none `@group(3) @binding(0) var<uniform>` struct — so a single `setUniform` call\nfeeds whichever backend is live. `setTime` is a convenience for a `uTime`\nuniform and silently does nothing if the shader does not declare one.\n\n**Never put a backtick in a shader body, including in its comments.** An inline\nbody is a JavaScript template literal, so a backtick ENDS it — and what follows\nis parsed as code, which fails somewhere further down the file with an error\nthat says nothing about shaders:\n\n```js\nconst glsl = `\n// x * x, not `pow(x, 2.0)`: the base goes negative here   <-- ends the literal\nvec4 apply(vec4 color, vec2 uv) { ... }\n`;\n```\n\nMarkdown-quoting an identifier is a reflex when writing a comment, and this is\nthe one place it breaks the file. Write `pow(x, 2.0)` bare. A body loaded from\na `.glsl` / `.wgsl` file has no such problem, which is one more reason to move\na shader out once it stops being a sketch.\n\n### One effect is one program — prefer preloading, and share\n\nA `ShaderEffect` compiles and links in its **constructor**, and the link is\nchecked with `getProgramParameter(LINK_STATUS)` straight after `linkProgram` —\na blocking call. So the cost is paid on whatever frame the effect is\nconstructed, and *n* identical effects cost it *n* times. `clone()` does not\navoid it: it constructs a new effect and links a new program.\n\nPreload it instead. Loader type `\"shader\"` takes a `src` **or inline source via\n`data`**, compiles at load time (inside the loading screen), and hands back a\n**shared** instance:\n\n```js\nloader.preload([{ name: \"ramp\", type: \"shader\", data: myFragmentBody }]);\n// …then, for every label that wants it:\nlabel.addPostEffect(loader.getShader(\"ramp\"));\n```\n\n### Declare a level's effects with that level's assets\n\nAn effect the loader knows about can be prepared before anything draws it; an\neffect built inline cannot, because it does not exist until the code that\nconstructs it runs — which is the stage or level you were hoping to speed up.\nThat is the practical reason to prefer the asset form even when the source is a\nstring in your own module:\n\n```js\n// with the level's other assets, not in the stage that uses them.\n// `preload()` rather than `load()` — preload is what warms the shaders it\n// brought in, and it can be called again per level, not just at boot\nawait loader.preload([\n    { name: \"ripples\", type: \"shader\", src: {\n        glsl: \"assets/level2/ripples.glsl\",\n        wgsl: \"assets/level2/ripples.wgsl\",\n    }},\n    // …that level's textures, maps and audio alongside it\n], undefined, false);   // `false`: stay on this stage, no loading screen\n```\n\n`src` takes the dual-language pair, so one asset carries both realizations and\nthe two backends cannot drift apart. `data` takes inline source the same way.\n\nWith `prewarm` on (the default since 20.6), `loader.preload()` warms on its way\nthrough — automatically, on every call — and compiles the GPU-side objects for\nevery shader it holds — on WebGPU that is the\nWGSL module, which is otherwise built inside the draw path the first time the\neffect is used.\n\nSince 20.10 the rest is warmed when the effect is **attached**, inline effects\nincluded: `addPostEffect` (or assigning `shader`) compiles the pipelines that\neffect will draw with on that renderable — on WebGPU the costly part, keyed by\nthe renderable's blend mode and target as well as the shader — and on WebGL\nlinks the spliced program when the host is a mesh. It finishes off the frame,\nso attach when you build (a pool, a stage starting), not when you show: an\neffect attached and drawn in the same frame still compiles on that frame.\n\nOne caveat if you are moving an inline effect to a file: a template literal can\ninterpolate constants into the source (`vec2(${ASPECT}, 1.0)`), and a static\nfile cannot. Promote those to uniforms rather than duplicating the numbers\nacross the GLSL and WGSL copies — duplicated constants are exactly how the two\nbackends drift.\n\nSharing one instance across renderables is safe *because* the loader sets\n`effect.shared`. Without that flag a renderable's teardown destroys its post\neffects, which would free the program out from under everything else still\nusing it. Uniforms live on the effect, so a shared instance means one set of\nuniform values — fine when the look is uniform, and the reason to clone (and\npay a link) when it is not.\n\nNote this covers **your** effects only. The engine's own mesh/quad shader\nvariants are covered by the loading-screen warm-up and by the per-renderable one\n— see `melonjs-loading-assets`.\n\n### Screen-reading builtins\n\nFor refraction, distortion and anything that samples what is already drawn:\n\n- **`screen_texture`** — a sampler kept filled with everything drawn so far\n  (GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`)\n- **`screen_uv`** — this fragment's 0..1 position in that capture\n- **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame\n\nIn WGSL the capture is sampled through `screen_sampler` (clamped) or\n`screen_sampler_repeat` (wrapping) instead of GLSL's `: screen_texture`\nannotation. `screen_uv` is y-**up** in GLSL and y-**down** in WGSL, which exactly\nmatches each backend's own capture orientation — so a body that samples\n`screen_texture` at `screen_uv` needs no flip in *either* language.\n\nTwo places where a straight port really does differ:\n\n- **A capture you bind yourself.** `renderer.toFrameTexture()` grabs the frame\n  into a `Texture2d` you hand to `setTexture` — the supported replacement for\n  `readPixels` tricks. The GL capture is bottom-up (GLSL bodies sample it with\n  `1.0 - uv.y`) and opaque RGB; the WebGPU capture is top-down (the WGSL twin\n  must *not* flip) and preserves alpha.\n- **Vertical UV offsets** — a drop shadow, a directional smear. The WebGL pooled\n  multi-effect path composites through bottom-up FBOs, so declare a `uUVYDir`\n  uniform (initialise it to `1.0`) and multiply vertical offsets by it; the\n  renderer feeds `-1` on that path and `+1` everywhere else.\n\n## A ShaderEffect on a mesh is a colour hook, and drops some builtins\n\n`mesh.addPostEffect(fx)` shades a `Mesh` on **both** backends since 20.5. The\nbody is spliced into the engine's own mesh shader as a colour hook, so the mesh\nkeeps its placement, alpha cutout, lighting and fog, and `setUniform` /\n`setTime` drive it exactly as they do over a sprite. Uniform-only bodies are\nthe supported case and they are most of them.\n\nFive things the quad tier gives an effect that the mesh tier does not, each\nrefused rather than half-working:\n\n| refused | why |\n|---|---|\n| its own `setTexture` samplers | resolved by the quad tier's per-effect device state; refused on both backends |\n| `screen_texture`, `screen_uv` | need the screen capture only the quad tier takes |\n| `noise_uv` | derived from a quad's image and object size |\n| `vColor`, on WGSL only | the WGSL mesh module has no module-scope equivalent; the GLSL one declares it, so GLSL is fine |\n| `InstancedMesh` | the instanced WGSL builder drops everything between the two stages |\n\nHitting one warns **once** and falls back to the built-in shading. Nothing\nthrows, so the symptom is a mesh drawing with its plain texture, which reads as\na broken shader, a bad binding or a texture that failed to load. Check the\nconsole for the warning before you start eliminating alpha, bindings and\ntransparency: it names which of the five it was.\n\n2D renderables never hit this. `Sprite`, `Text` and the rest go through the\nquad path, which has the full feature set, so a gradient fill for text or a\nrefraction over a sprite is fine.\n\nIf you need one of the five on a mesh, supply a full dual-language `GLShader`\nrather than a fragment body, and declare the fog uniforms yourself (see\n`melonjs-3d`).\n\n## Extra textures bind in setTexture order\n\n`setTexture(\"uFoo\", …)` assigns bindings **1 and 2** to the first extra\ntexture, **3 and 4** to the second, and so on. The WGSL must match:\n\n```wgsl\n@group(3) @binding(1) var uFoo : texture_2d<f32>;\n@group(3) @binding(2) var uFooSampler : sampler;\n```\n\nCopying a two-texture effect and deleting one leaves the survivor declared at\n3/4 while it is bound at 1/2 — the program fails to build and the effect is\nsilently dropped.\n\n## Blend modes\n\nFourteen modes; thirteen of them are honoured by all three renderers:\n\n```js\nsprite.blendMode = \"additive\";   // or \"multiply\", \"screen\", \"overlay\", …\n```\n\nSix ride fixed-function blend state and cost nothing extra: `normal`,\n`additive` (spelled `add` or `lighter` too), `multiply`, `screen`, `exclusion`,\nand `none` (blending off — the source replaces the destination, alpha included).\n`none` is the one mode the Canvas backend does not implement; it reports\n`\"normal\"` instead.\n\nThe other eight cannot be expressed as a multiply-add on the destination, so the\nGPU backends capture the destination and composite through `BlendEffect` — one\ncapture plus one composite **per draw**: `difference`, `overlay`, `hard-light`,\n`color-dodge`, `color-burn`, `soft-light`, `darken`, `lighten`. Right for\naccents; expensive for hundreds of objects. The Canvas renderer reaches all\neight through `globalCompositeOperation` instead, at no extra cost.\n\nAnything else collapses to `\"normal\"`, and so do 3D meshes and `Gradient` fills\nwhatever you ask for — the latter with a one-time console warning.\n`renderer.setBlendMode(mode)` returns the mode it actually applied, so comparing\nthat against your request detects the fallback.\n\n## The Canvas fallback\n\n`ShaderEffect` on a Canvas renderer logs a warning, leaves `enabled = false`, and\nturns every method into a no-op. Nothing throws. The same happens on a GPU\nbackend when no body matches its language — a GLSL-only effect under WebGPU.\n\nThat is fine for decoration. It is **not** fine when the shader draws your\ncontent — the carrier renderable then shows through raw. A 1×1 white texture\nstretched to 280×156 renders as a white box where the effect should be.\n\nGuard when the effect is load-bearing:\n\n```js\nif (app.renderer.shaderLanguage === null) {\n    // no programmable pipeline — skip the carrier draw entirely\n}\n```\n\n`shaderLanguage` (`\"glsl\"`, `\"wgsl\"`, or `null`) is the flag to test here rather\nthan a backend name or `supportsDepthBuffer`: it answers \"can this renderer\ncompile what I am about to hand it\". Per-effect, `effect.enabled` answers the\nsame question after construction.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| effect does nothing, warning in console | Canvas fallback — no programmable pipeline |\n| effect does nothing on some machines only | GLSL-only shader, `video.AUTO` chose WebGPU |\n| a white or solid box where the effect should be | carrier renderable drawn while the effect is disabled |\n| ONE effect does nothing but two of them work | the renderable draws with primitives — set `postEffectNeedsCapture` |\n| effect cannot be re-enabled | `removePostEffect()` destroyed it — use `enabled` |\n| ported shader renders upside down | a hand-bound `toFrameTexture()` capture — GL is bottom-up, WebGPU top-down |\n| shadow/smear offset flips on some draws | vertical UV offset not multiplied by `uUVYDir` |\n| frame rate collapses with many blended sprites | an *advanced* mode (overlay, darken, …) — each draw is a capture plus a composite |\n| animated shader never moves | `setTime` fed milliseconds, or the shader declares no `uTime` |\n| squash and stretch only bulges, the height never changes, under Matter | the adapter rewrites `currentTransform` every step; set the scale from the sprite's `update()` |\n| `SquishEffect` squishes the whole screen instead of the sprite | it is chained with another effect, or on the camera; give the sprite it alone |\n| syntax error in a file whose shader you just edited, pointing at a line of GLSL | a backtick in the shader body ended the template literal — most often one wrapped around an identifier in a comment |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-renderables` — where post effects attach, and the destroy trap\n- `melonjs-3d` — the GPU-backend requirement, and why a custom mesh shader is\n  not affected by the camera's distance fog\n\n<!-- File: packages/melonjs/skills/melonjs-events/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-events\n\nPath: `packages/melonjs/skills/melonjs-events/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-events/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-getting-started/SKILL.md (Tokens: ~1724 | Category: anthropic-skill) -->\n---\nname: melonjs-getting-started\ndescription: \"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.\"\nlicense: MIT\n---\n\n# Getting started with melonJS\n\n> Bootstrapping a melonJS 20.x game: the `Application` lifecycle, the mandatory\n> `await app.init()`, the world container, and what the renderer fallback means\n> for feature availability.\n\n**Applies to:** melonJS 20.x. Older major versions differ — see the\n`melonjs-20-migration` skill.\n\n## The bootstrap\n\n`Application` construction is synchronous but **initialisation is not**. `init()`\nreturns a promise and must be awaited before building any renderable, loading\nassets, or entering a scene.\n\n```js\nimport { Application, Text } from \"melonjs\";\n\nconst app = new Application(1218, 562, {\n    parent: \"screen\",      // id of the DOM element to mount into\n    scale: \"auto\",\n    backgroundColor: \"#202020\",\n});\nawait app.init();          // ← REQUIRED. Nothing below works without it.\n\napp.world.addChild(\n    new Text(609, 281, {\n        font: \"Arial\",\n        size: 160,\n        fillStyle: \"#FFFFFF\",\n        textAlign: \"center\",\n        textBaseline: \"middle\",\n        text: \"Hello World !\",\n    }),\n);\n```\n\n**This is the single most common thing to get wrong.** On 19.x the constructor\ncalled `init(width, height, options)` for you; on 20.x `init()` takes no\narguments and calling — and awaiting — it is your job. Nothing that follows\nnames the missing call. The constructor does build `app.world`, so adding a\nplain renderable succeeds and simply never draws — no canvas is appended and the\napp never subscribes to the frame tick — while constructing a `Sprite` or a\n`Text` throws `Cannot read properties of undefined (reading 'renderer')`,\nbecause the global `game` those two fall back on is only set once the first\n`init()` resolves. Either way `app.renderer` and `app.viewport` stay `undefined`\nand the page stays blank.\n\n`init()` is asynchronous because acquiring a WebGPU device is asynchronous. There\nis no synchronous alternative.\n\n## Top-level await\n\nThe bootstrap needs `await` at module scope. In an ES module that is fine. If the\nsurrounding tooling does not allow it, wrap it:\n\n```js\nasync function main() {\n    const app = new Application(800, 600, { parent: \"screen\" });\n    await app.init();\n    // …\n}\nvoid main();\n```\n\n## Choosing a renderer\n\n`video.AUTO` (the default) tries **WebGPU → WebGL 2 → Canvas** and uses the first\nthat works.\n\n```js\nimport { video } from \"melonjs\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    renderer: video.AUTO,      // default; or video.WEBGPU / video.WEBGL / video.CANVAS\n});\n```\n\nTwo consequences worth knowing before writing rendering code:\n\n- **Which backend you get is a runtime fact, not a build-time one.** The same\n  code runs on a different renderer depending on the machine. Do not assume\n  WebGL.\n- **Not every feature exists on every backend.** Shader effects and post effects\n  need a programmable pipeline and are inert on Canvas. Where a feature degrades,\n  the engine warns once rather than throwing — so a missing effect is a console\n  warning, not a crash.\n\nPin the renderer explicitly (`video.WEBGL`) only when you have a reason to. It\ncosts you the WebGPU path and the Canvas fallback.\n\n## The scene graph\n\n`app.world` is the root `Container`. Everything drawn is a descendant of it.\n\n```js\napp.world.addChild(sprite, 10);   // 10 is the z / draw order\n```\n\n**Pass the z-order as the second argument to `addChild`** — it is the only form\nthat works. `renderable.z` does not exist as a property, setting `depth` before\n`addChild` is overwritten by `autoDepth`, and setting it afterwards does not\nresort. See the `melonjs-renderables` skill for the full explanation.\n\n## Scenes\n\nFor anything beyond a single screen, use `Stage` subclasses and the `state`\nmanager rather than mutating `app.world` directly:\n\n```js\nimport { Stage, state } from \"melonjs\";\n\nclass PlayScreen extends Stage {\n    onResetEvent() { /* build the scene */ }\n    onDestroyEvent() { /* tear it down */ }\n}\n\nstate.set(state.PLAY, new PlayScreen());\nstate.change(state.PLAY);\n```\n\n`onResetEvent` runs every time the state is entered, not once — build scene\ncontent there, not in the constructor.\n\n## Loading assets\n\nAssets are declared and preloaded through `loader`; audio needs `audio.init()`\nwith the formats you ship before any audio asset is preloaded.\n\n```js\nimport { audio, loader } from \"melonjs\";\n\nloader.setOptions({ crossOrigin: \"anonymous\" });   // needed for remote assets\naudio.init(\"mp3,ogg\");                             // BEFORE preloading audio\nawait loader.preload([\n    { name: \"tileset\", type: \"image\", src: \"data/img/tileset.png\" },\n    { name: \"jump\",    type: \"audio\", src: \"data/sfx/\" },   // directory, not file\n]);\n```\n\nTwo ordering rules that produce confusing failures when missed:\n\n- **`audio.init()` must run before any audio asset is preloaded**, and names the\n  formats you actually ship. Preloading first throws *\"target audio extension(s)\n  should be set through me.audio.init() before calling the preloader\"*.\n- The audio `src` is a **directory**, not a file. The loader appends the asset\n  `name` plus each configured extension, so a full filename gives you a 404.\n\n## `anchorPoint` defaults to the centre\n\n`Renderable.anchorPoint` is `(0.5, 0.5)`, so `pos` is an object's **centre**, not\nits top-left. `Container` forces `(0, 0)` instead.\n\nWriting top-left math against a centred anchor puts everything half a sprite off\nwith no error — it is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Common mistakes\n\n| symptom | cause |\n|---|---|\n| blank page, nothing drawn, or a `TypeError` reading `renderer` from `new Sprite` / `new Text` | missing `await app.init()` |\n| objects draw in the wrong order | `.z`/`depth` set instead of `addChild(child, z)` — `.z` is not even a real property |\n| shader/post effect does nothing, warning in console | running on the Canvas renderer |\n| audio 404s | audio asset `src` given as a file rather than a directory |\n| scene content appears once and never resets | scene built in the constructor instead of `onResetEvent` |\n| everything offset by half a sprite | top-left math against the centred `anchorPoint` default |\n| a `Tween` or `ParticleEmitter` does nothing | `.start()` / `streamParticles()` never called |\n| cross-origin textures fail to load | missing `loader.setOptions({ crossOrigin: \"anonymous\" })` |\n\n## Related skills\n\n- `melonjs-20-migration` — what changed in 20.x that older code and older\n  training data get wrong\n- `melonjs-renderables` — custom `Renderable` subclasses, pointer events, post\n  effects and the traps in each\n\n<!-- File: packages/melonjs/skills/melonjs-input/SKILL.md (Tokens: ~3618 | Category: anthropic-skill) -->\n---\nname: melonjs-input\ndescription: \"Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, KEYDOWN, KEYUP, press any key, splash screen, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click.\"\nlicense: MIT\n---\n\n# Input: keyboard, pointer and gamepad\n\n## Keyboard goes through named actions\n\nDo not poll key codes. Bind a key to an **action string**, then poll the action —\nthis is what lets gamepad and virtual controls map onto the same handling.\n\n```js\nimport { input } from \"melonjs\";\n\ninput.bindKey(input.KEY.LEFT,  \"left\");\ninput.bindKey(input.KEY.RIGHT, \"right\");\ninput.bindKey(input.KEY.X,     \"jump\", true);   // true = locked: one press per keydown\n\n// in update()\nif (input.isKeyPressed(\"left\"))  { /* … */ }\nif (input.isKeyPressed(\"jump\"))  { /* fires once per press, not per frame */ }\n```\n\nThe third argument to `bindKey` is `lock` (default `false`). Without it,\n`isKeyPressed` stays true for as long as the key is held — correct for movement,\nwrong for jump or fire. A locked action stays consumed until the key is released\nor you call `input.unlockKey(\"jump\")`. Use `input.keyStatus(action)` when you\nwant the raw held/not-held state regardless of locking.\n\n### A poll can miss a press entirely\n\n`isKeyPressed` is sampled once per frame, and `keyup` decrements the key's\ncounter the moment it arrives — locked or not. A press that goes down and up\nbetween two frames therefore leaves the counter back at zero and is never\nseen. At 60fps that is anything under ~16ms: rare from a human finger, routine\nfrom a synthetic event, and more likely the slower the frame rate.\n\nFor a screen where a missed press is a dead end — a splash, a \"press any key\",\na notice to acknowledge — listen for the event instead, which cannot be missed:\n\n```js\nimport { event } from \"melonjs\";\n\n// action is the bound action name, or undefined for an unbound key\nconst onKey = (action) => { if (action === \"menuSelect\") continue(); };\nevent.on(event.KEYDOWN, onKey);      // in onResetEvent\nevent.off(event.KEYDOWN, onKey);     // in onDestroyEvent — it does not self-remove\n```\n\n`KEYDOWN` fires for *every* key, bound or not, so matching on `action` is what\nnarrows it; it also repeats while the key is held, so guard anything that must\nhappen once. Keep polling for movement and for a menu cursor, where one step\nper press and a key held across a state change are the things that matter.\n\nThe full signature is `bindKey(keyCode, action, lock?, preventDefault?)`;\n`preventDefault` defaults to `input.preventDefault` (itself `true`). Pass the\nfourth argument per binding, or change the global default with\n`input.setPreventDefault(false)` — **assigning `input.preventDefault = false`\ndoes not work**, it is a read-only module binding.\n\n## Pointer events need `isKinematic = false`\n\n**This is the trap.** `Renderable.isKinematic` defaults to `true`, and both the\nbroadphase and the pointer dispatcher skip any candidate whose `isKinematic` is\nstill `true`. The handler registers successfully and simply never fires.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y, w, h) {\n        super(x, y, w, h);\n        this.isKinematic = false;               // ← required\n    }\n    onActivateEvent() {\n        input.registerPointerEvent(\"pointerdown\", this, this.onDown.bind(this));\n    }\n    onDeactivateEvent() {\n        input.releasePointerEvent(\"pointerdown\", this);\n    }\n}\n```\n\n`Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*` elements set it\nto `false` for you, and so does attaching a physics body (`Body`'s constructor\nsets `ancestor.isKinematic = false`) — so plain `Renderable` and `Sprite`\nsubclasses with no body are the ones that catch people.\n\nRegister in `onActivateEvent` and release in `onDeactivateEvent`, not in the\nconstructor — pooled objects are reused and would otherwise accumulate handlers.\n\n`app.viewport` works as a whole-screen region when you want global clicks. Note\nit is asked **before** everything else, not as a fallback after nothing matched:\nthe dispatcher appends it to the candidate list and walks that list from the\nend. So a viewport handler must not return `false` unless it really means to\nswallow every pointer event in the game.\n\nThe accepted event names are exactly `\"pointerdown\"`, `\"pointerup\"`,\n`\"pointermove\"`, `\"pointercancel\"`, `\"pointerenter\"`, `\"pointerover\"`,\n`\"pointerleave\"` and `\"wheel\"`. Anything else — `\"click\"`, `\"mousedown\"`,\n`\"touchstart\"` — makes both `registerPointerEvent` and `releasePointerEvent`\nthrow `invalid event type`. melonJS maps the canonical name onto whichever\nmouse/touch events the device actually supports, so you always register the\npointer name.\n\n## Which object gets the event\n\nThe dispatcher collects the regions near the pointer from the broadphase,\nbounds-checks each one, orders them **the way they are drawn** and asks the\ntopmost first, walking down until something consumes.\n\nA handler consumes by returning `false`. This is the DOM sense and it reads\nbackwards: `false` means handled, and anything else, `undefined` included, lets\nthe walk carry on to whatever is underneath.\n\n```js\ninput.registerPointerEvent(\"pointerdown\", this, () => {\n    this.fire();\n    return false;        // nothing under this gets the click\n});\n```\n\nThree things decide the order, and only the first is obvious:\n\n- **`pos.z`, among siblings of one container.** Higher is on top.\n- **Across containers, the common ancestor decides.** `pos.z` is\n  container-local, since `autoDepth` numbers each container's children from 1,\n  so a button at `z = 8` inside one panel does not outrank a second panel at\n  `z = 2`. The pair is resolved by walking both up to their shared parent and\n  comparing the two siblings there, which is exactly how drawing resolves it.\n  Raise a whole panel with `ancestor.moveToTop(panel)`, not by inflating a\n  child's `z`.\n- **A child is asked before the container holding it**, since it draws over it.\n\nEqual `z` between siblings falls back to child order, lower index on top. Two\ncallbacks registered on the **same** region for the same event run\nlast-registered-first.\n\nConsuming a **move** also takes the pointer away from the regions below: each\none still holding it gets its `pointerleave`, because it is covered now and\nbeing inside its own bounds no longer decides. Consuming a press, a release or a\nwheel does not, since none of those says where the pointer is.\n\n## World coordinates versus screen coordinates\n\nThe pointer carries several, and picking the wrong one produces a feedback loop:\n\n| property | meaning | use for |\n|---|---|---|\n| `pointer.gameScreenX` / `gameScreenY` | canvas-relative position | anything that **moves the camera** |\n| `pointer.gameWorldX` / `gameWorldY` | projected into world space | picking and spawning objects |\n| `pointer.gameLocalX` / `gameLocalY` | relative to the region's ancestor container | hit-testing inside a container |\n| `pointer.gameX` / `gameY` | **context-dependent** — see below | region handlers on non-floating regions |\n\n`gameX`/`gameY` is not a fixed coordinate space. The dispatcher rewrites it per\ncandidate just before calling your handler: world coordinates for a normal\nregion, screen coordinates for a `isFloating === true` region. On the global\n`event.POINTERMOVE` broadcast it is set to the **screen** coordinates. When you\nneed one specific space, read `gameWorldX`/`gameWorldY` or\n`gameScreenX`/`gameScreenY` by name rather than relying on `gameX`.\n\nUsing world coordinates for a camera-orbit drag means the same pixel maps to a\ndifferent world point every frame as the camera moves — the drag jumps wildly.\n\n## Pointer move and release outside a region\n\nRegion handlers only fire while the pointer is over the region. For drag\ntracking, listen globally:\n\n```js\nimport { event } from \"melonjs\";\nevent.on(event.POINTERMOVE, (pointer) => { /* … */ });\n```\n\nThere is **no global pointer-up system event** — `event.ts` publishes only\n`POINTERMOVE`, `POINTERLOCKCHANGE`, `DRAGSTART` and `DRAGEND`. For \"released\nanywhere\" you need a raw listener:\n\n```js\nglobalThis.addEventListener(\"pointerup\", onRelease);\n```\n\nFor plain drag-and-drop, `Draggable` / `DropTarget` already wire all of this up\n(`pointerdown` / `pointerup` / `pointercancel` regions plus a global\n`POINTERMOVE` listener) and emit `event.DRAGSTART` / `event.DRAGEND`. `DRAGEND`\nstill originates from a region handler, so it only fires when the release lands\non the draggable itself.\n\n## Gamepad\n\nGamepad input maps onto the same action strings, so game code does not branch:\n\n```js\ninput.bindGamepad(0,\n    { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 },\n    input.KEY.X);        // → the \"jump\" action bound above\n```\n\n**Order matters:** `bindGamepad` throws `no action defined for keycode N` if the\nkeycode has not already been passed to `bindKey`.\n\nAxes use the same call with a threshold, and bind **one half of the axis each**\n— the sign selects which half:\n\n```js\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold: -0.15 }, input.KEY.LEFT);\ninput.bindGamepad(0, { type: \"axes\", code: input.GAMEPAD.AXES.LX, threshold:  0.15 }, input.KEY.RIGHT);\n```\n\n**`threshold` is added to the deadzone, it does not replace it.** The test is\n`abs(value) >= deadzone + abs(threshold)`, so with the default deadzone of 0.1\na threshold of `0.5` does not fire until 0.6 of full travel, and `0.15` fires at\n0.25. Pick the number you want the stick to break at and subtract the deadzone.\nRaising `setGamepadDeadzone` moves every axis binding with it.\n\n`unbindGamepad(index, button, type?)`, `setGamepadDeadzone(value)` and\n`setGamepadMapping(id, mapping)` complete the surface.\n\n### Which index to bind\n\nBindings are per pad **index**, and the index is plug order, not identity —\nnothing reports which pad is \"the\" controller. Slot 0 is a guess: a wheel, a\nspare pad or a keyboard-shaped device left plugged in can hold it for a whole\nsession while the player presses buttons on slot 1.\n\nSo a one-player game binds **every slot it will accept** to the same actions,\nand lets `activeGamepad()` say which one is in use when something has to go\nback the other way:\n\n```js\nfor (let pad = 0; pad < 4; pad++) {\n    input.bindGamepad(pad, { type: \"buttons\", code: input.GAMEPAD.BUTTONS.FACE_1 }, input.KEY.X);\n}\ninput.activeGamepad();   // the last pad to produce a bound input, else the\n                         // first connected one, else -1\n```\n\nBinding spare slots costs nothing: the poll skips a bound index with no pad in\nit. `activeGamepad()` follows **bound inputs only**, which is what makes it an\nanswer about the player rather than about electrical noise — a worn stick at\nrest reports movement for ever and would otherwise claim to be the active pad.\n\n### Rumble\n\n```js\ninput.vibrateGamepad(input.activeGamepad(), {\n    duration: 120, strongMagnitude: 0.6, weakMagnitude: 0.9,\n});\ninput.stopGamepadVibration(index);   // cut a long effect short\ninput.hasGamepadVibration(index);    // for an options-screen toggle\n```\n\nSafe to call blind: no pad, no motors, or no Gamepad API at all resolves\n`false` and does nothing, so game code never branches on support. Magnitudes\ndefault to **full** — the underlying web API defaults every parameter to zero,\nwhich makes a bare `playEffect` a silent no-op. A second call preempts the\nfirst on the same pad, so a rapid series of hits reads as a rapid series of\nhits rather than as a queue. For a SUSTAINED rumble issue one effect with a\nlong `duration` instead of calling every frame.\n\nHot-plugging needs no polling of your own — the engine listens for the browser's\nown events and republishes them:\n\n```js\nimport { event } from \"melonjs\";\n\nevent.on(event.GAMEPAD_CONNECTED,    (gamepad) => { /* gamepad.id, .index */ });\nevent.on(event.GAMEPAD_DISCONNECTED, (gamepad) => { /* pause, show a prompt */ });\n```\n\nBindings survive a disconnect: they are attached to the index, not to the\ndevice, so a pad unplugged and plugged back into the same slot keeps working.\n\n## Virtual controls\n\nOn-screen buttons synthesise key events, again reaching the same actions:\n\n```js\ninput.triggerKeyEvent(input.KEY.LEFT, true);    // press\ninput.triggerKeyEvent(input.KEY.LEFT, false);   // release\n```\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| pointer handler never fires | `isKinematic` left at its `true` default |\n| handlers fire twice, or on dead objects | registered in the constructor instead of `onActivateEvent` |\n| camera drag jumps around | using world coordinates instead of `gameScreenX`/`gameScreenY` |\n| `gameX` holds screen coords where world coords were expected | read on `event.POINTERMOVE`, or on a floating region — use `gameWorldX`/`gameWorldY` |\n| drag stops when leaving the object | region events end at the region — use `event.POINTERMOVE` |\n| release never detected outside the region | no global pointer-up event — add a raw listener |\n| jump fires every frame while held | `bindKey` without its third `lock` argument |\n| `invalid event type` thrown at registration | non-pointer name (`\"click\"`, `\"mousedown\"`, …) passed to `registerPointerEvent` |\n| `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |\n| stick has to be pushed well past its `threshold` before it registers | `threshold` is ADDED to the deadzone, not used instead of it |\n| one half of a stick does nothing | each half is a separate `bindGamepad`, chosen by the sign of `threshold` |\n| a widget under an overlapping panel still reacts | the panel consumes nothing — return `false` from its `onClick` and `onOver` |\n| a widget stays highlighted after the pointer moves onto a panel over it | the panel consumes no `pointermove`, so nothing takes the pointer off the widget |\n| a child's big `z` does not put it in front of another container | `pos.z` is container-local — use `ancestor.moveToTop()` on the container |\n| every click in the game stops working | a handler on `app.viewport` returning `false`, which is asked first |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic` also gates the physics broadphase\n- `melonjs-ui-and-text` — `UISpriteElement` and `UITextButton` handle this for you\n\n<!-- File: packages/melonjs/skills/melonjs-lighting/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-lighting\n\nPath: `packages/melonjs/skills/melonjs-lighting/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-lighting/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-loading-assets/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-loading-assets\n\nPath: `packages/melonjs/skills/melonjs-loading-assets/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-loading-assets/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-math/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-math\n\nPath: `packages/melonjs/skills/melonjs-math/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-math/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-particles-and-trails\n\nPath: `packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-particles-and-trails/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-performance/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-performance\n\nPath: `packages/melonjs/skills/melonjs-performance/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-performance/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-physics/SKILL.md (Tokens: ~10617 | Category: anthropic-skill) -->\n---\nname: melonjs-physics\ndescription: \"Use this skill for collision, physics bodies, movement, and spatial queries in melonJS — the built-in SAT world and the planck/matter adapters. Covers bodyDef vs Body, collision types and masks, the collision callback family and their firing rules, raycast/queryAABB/querySphere, the behaviour differences between adapters, and which one to pick for a given game. Triggers on: which physics engine, choosing an adapter, rotation collision, rotating body, torque, joints, stacking, Body, bodyDef, collision, collisionType, collisionMask, onCollision, onCollisionStart, onCollisionActive, raycast, queryAABB, querySphere, PlanckAdapter, MatterAdapter, gravity, velocity, applyForce, isGrounded, SAT.\"\nlicense: MIT\n---\n\n# Physics and collision\n\n> melonJS has a built-in SAT collision world and two optional rigid-body\n> adapters. Movement, collision callbacks and spatial queries all behave\n> differently depending on which is active — and several of the differences fail\n> silently.\n\n## Choosing a world\n\n| | when |\n|---|---|\n| **built-in** (default) | Arcade-style: platformers, top-down games. Fast, position-based, not Newtonian. |\n| **`@melonjs/planck-adapter`** | Full rigid-body dynamics — stacking, joints, realistic restitution. |\n| **`@melonjs/matter-adapter`** | Same class of thing, backed by matter-js. |\n\n### Picking one\n\nStart with the built-in. It is the default for a reason: no dependency, the\nsmallest bundle, and its position-based model is what most 2D games actually\nwant — a platformer's jump arc is a designed curve, not a simulated one, and\ntuning gravity against a solver you are fighting is a bad afternoon.\n\n**Move to an adapter when the game needs something the builtin cannot express**,\nand these are the usual triggers:\n\n- **Rotation that collides.** The clearest signal. The builtin's SAT never reads\n  `body.angle`, so a spinning shape collides as though it never turned. See\n  the note in *Built-in world quirks* below.\n- **Torque, angular velocity, or spin from an impact.** Anything that should\n  start turning *because* it was hit.\n- **Joints and constraints** — ragdolls, chains, ropes, vehicles, hinged doors.\n  The builtin has no concept of them.\n- **Stacking and resting contacts.** Boxes that pile up and stay put need an\n  iterative solver. The builtin resolves each contact once per step with no\n  iteration pass, which splits three ways. A dynamic body resting on STATIC\n  geometry is exact, to the pixel, which is the platformer case it exists for.\n  A body squeezed between a dynamic body and static geometry is exact too: the\n  immovable side gets the casting vote and the other body absorbs the whole\n  correction, so a crate shoved against a wall stops at the wall. But a body\n  squeezed between two DYNAMIC bodies has no immovable side to defer to, so\n  those per-pair corrections still compete and it comes to rest slightly\n  overlapping. Three stacked 44px boxes rest flush on the floor with about 2px\n  of overlap inside the pile, and stay there: a stable equilibrium, not jitter\n  you can wait out or tune away.\n- **Believable restitution and friction**, where a ball's bounce height and roll\n  should follow from its material rather than from code you wrote per case.\n\n**Stay on the built-in when** the game is grid or tile based, movement is\nauthored rather than simulated, collision is \"did these two boxes touch\" plus a\npush-out, or you are shipping to a tight bundle budget. Tiled maps, `Trigger`\nand `Collectable` all work on every backend, so this is not a fork in the road\nfor level content.\n\n**planck or matter?** Both are real rigid-body engines and either will do; the\nadapters expose the same portable API, so a game can switch with an import\nchange and a gravity re-tune.\n\n- **planck** (a Box2D port) is the more accurate and the more predictable under\n  stress: better stacking, better joint behaviour, a continuous-collision\n  `bullet` mode for fast movers. It works in **metres**, so it has a\n  `pixelsPerMeter` scale to think about, and its tuning vocabulary is Box2D's.\n- **matter-js** works directly in **pixels**, which makes it the gentler\n  introduction, and its compound bodies and constraints are pleasant to author.\n  It is the softer solver: stacks settle less crisply and fast bodies need\n  `subSteps`.\n\nIf you have no preference and the game leans on stacking, joints or fast\nprojectiles, take planck. If you want to be up and running with the least\nconversion to think about, take matter.\n\nAn adapter is an `Application` setting, not a plugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },   // pixels/s², default {x: 0, y: 320}\n        pixelsPerMeter: 32,          // default 32\n        subSteps: 2,                 // default 1\n    }),\n});\nawait app.init();\n```\n\n`PlanckAdapter` also takes `velocityIterations` (default `8`) and\n`positionIterations` (default `3`). `MatterAdapter` takes a different set —\n`gravity` (default `{x: 0, y: 1}`, matter's own convention), `subSteps` and\n`matterEngineOptions`; it has no `pixelsPerMeter` because it already works in\npixels. `subSteps` is the anti-tunnelling knob when per-frame motion exceeds a\nbody radius. Pick **one** adapter per game; `physic: \"none\"` disables physics\nentirely — `world.step` skips the simulation and the world behaves as a pure\nscene graph.\n\n**Gravity is a different unit in every backend, and the defaults are NOT the\nsame speed.** Each is \"Earth-like\" in its own convention, but measured against\neach other by timing a 200px free fall in one scene:\n\n| backend | default | unit | measured fall |\n|---|---|---|---|\n| built-in | `0.98` | per FRAME, integrated against `timer.tick` | **~4450 px/s²** |\n| matter | `{x: 0, y: 1}` | matter's own, after its internal `gravity.scale` | **~900 px/s²** |\n| planck | `320` | pixels per second² | **~330 px/s²** |\n\nThat is a **thirteenfold spread**, so swapping the built-in world for an adapter\nand keeping everything else the same makes the whole game feel different. It is\nnot a bug in either one: they simply do not share a unit.\n\nTo match a backend you are porting FROM, pick a target in px/s² and convert:\nplanck takes it directly; matter takes `target / 900`; the built-in takes\n`target / 4450 * 0.98`, because it applies gravity per frame rather than per\nsecond. Passing one number to two of them compares nothing: hand planck\nmatter's `{x: 0, y: 1}` and it runs at a three-hundredth of Earth gravity.\n\nThat mistake is quiet and it does not look like a gravity problem. Bodies fall\nslowly enough that a spawner keeping a fixed interval starts constructing them\ninside each other, and **a body born overlapping another is outside what a\nsolver promises to fix**: the contact normals of an interlocked pair can cancel\nout, leaving the position solver with nowhere to push. The symptom is two\nshapes permanently fused at rest, which reads as a broken collision shape\nrather than as a bad gravity constant. Spawn clear of anything already there.\n\n`world.physic` carries the active adapter's label (`\"builtin\"`, `\"planck\"`,\n`\"matter\"`, `\"none\"`), so game code can branch without importing the class.\n\n## Two ways to declare a body\n\n**`bodyDef`** — declarative, portable, works on every adapter. Prefer it.\n\n```js\nclass Player extends Sprite {\n    constructor(x, y, settings) {\n        super(x, y, settings);\n        this.bodyDef = {\n            type: \"dynamic\",\n            shapes: [ /* … */ ],\n            collisionType: collision.types.PLAYER_OBJECT,\n            collisionMask: collision.types.WORLD_SHAPE | collision.types.ENEMY_OBJECT,\n            restitution: 0.2,\n        };\n    }\n}\n```\n\nThe engine forwards it to the active adapter when the renderable is added with\n`Container.addChild`, and only if that container is already attached to the root\nworld; the broadphase picks the renderable up on the next `world.update()`.\nCalling `adapter.addBody()` yourself registers the body but leaves the\nrenderable out of the scene graph, so it integrates but never collides.\n\n**`fixedRotation` follows the engine, so bodies rotate.** Both matter and\nplanck default it to `false`, and so do the adapters from matter-adapter 1.4.0\nand planck-adapter 1.5.0. Pass `fixedRotation: true` for anything that must\nstay upright, such as a platformer actor. Earlier adapter versions locked\nrotation unless told otherwise, which inverted the engine they wrap: bodies\nlanded holding whatever angle they were given and no collision could turn them,\nwhile `setAngle()` still worked, so a body could look rotated while being\nunable to rotate.\n\n`frictionAir` damps **rotation as well as translation** on both adapters, so\nit is also what settles a spin. Left at `0` a body that starts turning never\nstops, which only became visible once rotation stopped being locked.\n\nOther portable `bodyDef` fields: `density`, `frictionAir` (number or `{x, y}`),\n`friction`, `restitution`, `gravityScale`, `maxVelocity`, `fixedRotation`,\n`isSensor`, `userData`. Not every adapter honours every one — the built-in\nadapter ignores `friction`, `fixedRotation` and `userData`, and maps\n`frictionAir` onto its per-axis `body.friction` damping vector.\n\n**`new Body(this, shape)`** — the built-in-native API. Fine, but ties the entity\nto the built-in world.\n\n### Per-shape filtering and triggers\n\nA shape in `shapes` may carry its own `collisionType`, `collisionMask`,\n`isTrigger` (default `false`) and `isActive` (default `true`). A shape can only\n**narrow** what its body allows — the body's type/mask are checked first.\n\n```js\nconst feet = new Rect(0, 24, 32, 8);\nfeet.collisionMask = collision.types.WORLD_SHAPE;\nconst torso = new Rect(0, 0, 32, 24);\ntorso.collisionMask = collision.types.ENEMY_OBJECT;\ntorso.isTrigger = true;      // detects hits, never gets pushed by them\n```\n\n`isActive: false` removes the shape from the narrow phase *and* from raycasts\nwithout shrinking the body's bounds. Honoured by the built-in and planck\nadapters; the matter adapter filters per body and ignores them. Under planck the\nbody-wide setters (`setCollisionType` / `setCollisionMask` / `setSensor`) write\nevery fixture and so overwrite per-shape values.\n\nFor contacts reported per *shape pair* rather than per body pair, define\n`onShapeCollisionStart` / `onShapeCollisionActive` / `onShapeCollisionEnd`.\nDeclaring at least one of them opts into shape-pair enumeration; declaring none\ncosts nothing.\n\n### Shapes drawn in a physics-shape editor\n\nA visual collision-shape editor exports a JSON file holding a map of body name\nto the fixtures authored for it. Preload it like any other JSON, then name it\nfrom `bodyDef.shapes` with `id` naming the body to read:\n\n```js\n// in the preloader\n{ name: \"shapes\", type: \"json\", src: \"data/physics/shapes.json\" }\n\n// then, on the renderable\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\" };\n```\n\nThis works identically on the built-in, matter and planck adapters — the file is\nresolved into real shapes before any adapter is handed the definition. Circles\nand polygons both load, and a concave outline (exported already decomposed into\nconvex pieces) becomes one `Polygon` per piece.\n\nThe density, friction and bounce the shapes were authored with are carried onto\nthe body definition too, each honoured by whichever backends support it — the\nbuilt-in adapter ignores contact `friction`, as it does for a hand-written\ndefinition. The format carries these per fixture, but only planck could honour\nthat, so they reduce onto the body and a file whose fixtures disagree warns. An\nexplicit field on the definition always wins:\n\n```js\n// bounces at 0.9 whatever the file says\nthis.bodyDef = { type: \"dynamic\", shapes: \"shapes\", id: \"hotdog\", restitution: 0.9 };\n```\n\nThe exporting tool's own filter block is **not** read, in any spelling. Its\ncategory and mask bits are numbers in that tool's namespace rather than\n{@link collision.types}, and a template writes its defaults for every fixture\nwhether or not you set anything — importing them would silently tag a shape as\nsomething it is not and stop it colliding. Set `collisionType` / `collisionMask`\non the body definition instead. A file written by hand may carry those two names\nper fixture, and `isSensor`, which mean the same thing here as there.\n\nAuthored shapes and imported ones can sit in the same list.\n\nTwo bodies must never share one `shapes` array: a body **owns** its shapes and\nrotating it mutates them in place. Naming the file sidesteps this — every body\nresolved from it mints its own shapes.\n\n`Body#fromJSON()` and passing an exported list to `addShape()` are the old,\nbuilt-in-only spelling of this and are deprecated since 20.7.0.\n\n### Which shapes each backend can actually simulate\n\n| shape | builtin | matter | planck |\n|---|---|---|---|\n| `Rect`, `Polygon` | exact | exact | exact, up to 12 vertices |\n| `RoundRect` | exact | exact (decomposed) | sampled down to 12 vertices |\n| `Ellipse` | exact | circle of the average radius | circle of the average radius |\n| `Line` | exact | thin quad along the segment | thin quad along the segment |\n| `Box3d`, `Sphere` | exact, and in 3D | **throws** | **throws** |\n| `Point` | supported | **throws** | **throws** |\n\nTwo of those are worth knowing before you author geometry. Box2D caps a\npolygon's vertex count and says nothing when you go over: measured, a\n16-point outline comes back with 12 vertices and a 36-point one collapses\nfrom 70x36 to 47x18. A `RoundRect` carries 36 points, four corners of nine\narc segments each, so it is the usual way to meet that cap; the adapter now\nsamples it down and keeps the extent. Anything the 2D backends cannot express\nis refused loudly on both, rather than leaving a body with no collision\ngeometry that silently never collides.\n\nWhatever a backend really simulates is what `getBodyShapes()` reports, so the\ndebug panel is the fastest way to see an approximation for what it is.\n\n### `Line` shapes, and why slopes need a rigid-body adapter\n\nA `Line` is two points and no area, and it is what Tiled emits for every\npolyline, so it is the natural way to author a slope or a strip of ground.\nNeither rigid-body engine has a segment primitive, so both simulate one as a\nthin quad following the segment (2px), which is what `getBodyShapes()` reports\nback. Before 20.7 each substituted something of its own instead: matter the\nline's bounding box, planck a one-metre square wherever the body happened to\nbe. Neither logged anything.\n\nThe builtin solver resolves a sloped segment too, and correctly: a body lands\non it and SLIDES down it, holding a constant distance from the surface while\nits x advances. What it has no notion of is SURFACE friction. `def.friction`\nis ignored entirely and `body.friction` is a per-step velocity damping vector\nfed from `def.frictionAir`, so nothing removes the tangential component of\ngravity. A body on an incline therefore accelerates down it forever and leaves\nat the end, and in a valley of segments it swings from arm to arm for a while\nbefore coming to rest on the flat. (It used to escape through an arm instead;\nthat was a separate defect in how a body was separated from a shape list, fixed\nin 20.7.)\n\nmatter and planck both have real surface friction, so the same valley holds a\nbody still.\n\n**Several shapes on one body.** A Tiled polyline arrives as ONE body carrying\none shape per segment, and until 20.7 a body resting on such a ground could\nsink through one of the segments and drop out. The narrowphase reports the\nFIRST overlapping shape pair it finds, and the compound resolution pass\nre-ran that same first-hit-wins scan, so it kept re-resolving the pair it had\nalready resolved while a neighbour was penetrated unmeasured. Every pair is\nenumerated now. If you are ever diagnosing something similar, the experiment\nthat localises it is to re-run the same geometry as SEVERAL single-shape\nbodies: if that holds and one multi-shape body does not, the fault is in the\ncompound path rather than in the shape type.\n\nDo not diagnose this by dropping a body and reading its final `y`. A body that\nslid off the end of a ramp and a body that fell straight through it both end up\nfar below, and they look identical in that one number. Track the body against\nthe surface height at its CURRENT x instead: riding the slope shows a constant\noffset while x advances.\n\nSo segments collide on every backend. If you need a body to come to REST on a\nsloped one, use matter or planck, or damp it yourself: the builtin will slide\nit down the incline indefinitely. The `Line Collision` example draws the\nauthored segment against the geometry each backend reports, which is the\nquickest way to see what a backend is really simulating.\n\n### `anchorPoint` moves the collision shapes with the drawing\n\n`anchorPoint` says where in its own bounds a renderable sits on its `pos`:\n`updateBounds()` translates its bounds by `-width * anchorPoint.x, -height *\nanchorPoint.y`, and `preDraw` shifts its rendering by the same amount. A\nbody's collision shapes are measured from that same frame, so a shape of\n`new Rect(0, 0, width, height)` covers the sprite whatever the anchor is, and\na shape's own `pos` still offsets it inside that frame (a small hitbox at the\nfeet, a hurtbox at the head).\n\n**That frame's origin is its TOP-LEFT corner, which is the trap for a small\nshape.** `new Rect(0, 0, w, h)` reads naturally because it spans the frame.\nA circle or a `Sphere` does not span anything, so `new Sphere(0, 0, 0, r)` on\na centre-anchored sprite sits on the sprite's corner, not on the sprite. Put\nit at half the frame:\n\n```js\n// centred on a 24x128 sprite whose anchorPoint is (0.5, 0.5)\nbolt.bodyDef = { type: \"dynamic\", shapes: [new Sphere(24 / 2, 128 / 2, 0, r)] };\n```\n\nMeasured on exactly that sprite: with the shape at `(0, 0, 0)`,\n`adapter.getBodyShapes()` reported it at `(-12, -64)` — **64 units up the\nscreen, four times its own radius** — so the projectile collided well away\nfrom where the player saw it, in a way that still looked like a working game\nbecause the targets were large.\n\nThe offset comes from the renderable's own `width` / `height`, NOT from what\nit draws at. Scale a sprite through `currentTransform` and those stay the\ntexture's size, so the half-frame term is half the texture, not half the\ndrawn quad. The debug overlay is the quickest confirmation: the red shape\nshould sit on the art.\n\nThis was broken until 20.7: shapes were measured from `pos` regardless of the\nanchor, so anything not anchored at its top-left corner collided where it was\nnot drawn, by half a body at the default centred anchor and by a full body\nheight at the bottom anchor a platformer actor uses. If you compensated by\noffsetting your shapes by hand, remove those offsets. `Entity` and Tiled\nobjects set their own anchor to `(0, 0)`, so nothing built either of those\nways is affected.\n\nThe debug panel is the quickest check: green is the renderable's bounds, red\nis what actually collides, and the two now agree.\n\nOne renderable does not take that offset at all: one that has cleared\n`applyAnchorTransform`, the flag `preDraw` reads before shifting anything.\n`GLTFModel` sets it `false` outright and `Mesh` clears it under a `Camera3d`,\nbecause both emit world coordinates and pivot about their own model origin\nrather than a bounds box, so they draw at `pos` whatever their `anchorPoint`\nstill holds. Their bodies are built there too, on every backend, and\n`raycast3d` reports hits in that same frame. If you clear the flag on a\nrenderable of your own, the same applies: you place it, and your shapes are\nmeasured from `pos`.\n\n### `autoTransform: false` opts out of rotated bounds\n\n`updateBounds()` only applies `currentTransform` when `autoTransform` is\n`true`. Turn it off and the renderable's bounds stay the UNROTATED frame\nwhile the sprite turns inside them, which quietly feeds a wrong box to\nfrustum culling and to the broadphase. Measured on a 60x20 box at 90\ndegrees: `20x60` with `autoTransform` on, `60x20` with it off.\n\nThat flag means \"I place myself\", so it is a fair contract, but reach for it\nonly when you really are drawing in your own frame, and remember the bounds\nare yours to keep honest then.\n\n## Move with forces, not by assigning position\n\n```js\n// ✗ the adapters overwrite this from the engine body every step\nthis.pos.x += this.speed;\n\n// ✓ portable — works on every adapter\nthis.body.applyForce(thrustX, 0);\nthis.body.setVelocity(vx, vy);\n\n// ✓ built-in only — `force` and `maxVel` are BuiltinAdapter Body fields\nthis.body.force.x = this.body.maxVel.x;\n```\n\n`maxVel` is what turns a force into a speed, so the two are set together. Use\nthe setters rather than writing the vectors — both take an optional THIRD\nargument for the depth axis, left unchanged when omitted, so a 2D call keeps\nits behaviour:\n\n```js\nthis.body.setMaxVelocity(3, 15);   // walk speed, jump speed\nthis.body.setFriction(0.4, 0);     // ground drag, none vertically\n```\n\n`friction` here is per-axis per-step velocity damping, not a surface\ncoefficient — see the built-in quirks below.\n\nUnder matter and planck, `syncFromPhysics()` copies the engine body's position\nback onto `renderable.pos` after every step, so a direct write is simply erased\n— no error. The built-in world integrates `pos` in place instead, so a write\nsurvives but fights collision resolution and jitters. To teleport, go through\n`adapter.setPosition(renderable, p)` so the engine body moves with it.\n\n## Collision callbacks — which one to use\n\nThe lifecycle is `onCollisionStart` (contact begins), `onCollisionActive` (every\nstep while it persists) and `onCollisionEnd` (contact breaks). None of them are\ndefined on `Renderable` — you just declare the ones you want and the dispatcher\nchecks `typeof`. That is also why they cannot be declared on the base class: a\nfield there would SHADOW the prototype method every subclass that implements\none defines.\n\n**In TypeScript the response is `CollisionResponse`**, exported from `melonjs`:\n\n```ts\nimport type { CollisionResponse, Renderable } from \"melonjs\";\n\nclass Coin extends Sprite {\n    onCollisionStart(response: CollisionResponse, other: Renderable) { … }\n}\n```\n\nOn a renderable you do not subclass, declare the hooks you want as OPTIONAL\nmembers of a local interface and assign through one plain cast — that is the\nopt-in, and it needs no `as unknown as`:\n\n```ts\ninterface Contacts {\n    onCollisionStart?: (response: CollisionResponse, other: Renderable) => void;\n}\n(rock as Contacts).onCollisionStart = (response, other) => { … };\n``` The legacy `onCollision` still exists and has a trap:\n\n> On the built-in world, `onCollision` fires **twice per frame** for\n> dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT\n> response with a fixed `a`/`b` per pair. On the adapters it is routed to their\n> \"still touching\" phase and fires once per side.\n\nThe legacy dedupe idiom is `if (this !== response.a) return;`. Defining\n`onCollisionActive` suppresses the legacy callback on that renderable (the\n\"supersedes\" rule, applied per side so `a` and `b` can migrate independently),\nand is deduped to once per pair per side per frame on every adapter.\n\n**Return values only work on the legacy handler.** `return false` from\n`onCollision` skips the built-in SAT push-out; what `onCollisionStart` /\n`onCollisionActive` return is discarded. To opt a body out of the physical\nresponse, set `bodyDef.isSensor` / `body.setSensor(true)`, or make the shape a\ntrigger. With no `onCollision` defined at all, push-out happens by default for\ndynamic non-sensor bodies.\n\nThe two handler families do **not** get the same response object:\n\n| | `onCollision` (legacy) | `onCollisionStart` / `Active` / `End` |\n|---|---|---|\n| `a` / `b` | fixed per pair | receiver-symmetric: `a === this`, `b === other` |\n| overlap magnitude | `response.overlap` | `response.depth` |\n| contact axis | `response.overlapN` / `overlapV` | `response.normal` |\n| also carries | `aInB`, `bInA`, `indexShapeA/B`, `isTriggerContact` | `pair` (adapter-native, absent on built-in) |\n\nOn the modern response `overlap` / `overlapN` / `overlapV` are still present but\n`@deprecated` (and `undefined` under the matter adapter) — use `depth` and\n`normal`. `normal` is the direction the receiver must move to separate, in\ncanvas coordinates: `normal.y < -0.7` means \"push me up\", i.e. I landed on top\nof `other`.\n\nOne portability wrinkle: the built-in adapter dispatches `onCollisionEnd` with\n`undefined` as the response; the matter and planck adapters pass a real one.\nGuard it.\n\n## Never mutate the world during contact dispatch\n\nCollision callbacks are dispatched from inside the physics step on every\nadapter, and each engine has its own tolerance for being mutated there. Flag the\nwork and drain it from your `Stage.update`:\n\n```js\nonCollisionStart(response, other) {\n    this.pendingRemoval = true;    // don't addChild / removeChildNow here\n}\n```\n\nWhat actually happens per adapter:\n\n- **built-in** — callbacks fire inline during `step()`. `Container.removeChild`\n  is deferred, so it is safe; `removeChildNow()`, and the internal `destroy()`\n  it calls, are immediate,\n  and the detector only survives them because it re-checks `body === undefined`\n  after every user callback.\n- **planck** — `onCollisionStart` / `onCollisionEnd` are dispatched from\n  planck's `begin-contact` / `end-contact` events, which fire *while the world\n  is locked*. Adding a renderable with a `bodyDef` there makes planck's\n  `createBody` return `null` and the adapter then throws on the null handle.\n  `onCollisionActive` is dispatched after the step and is the safe one.\n- **matter** — callbacks come from matter's `collisionStart` / `collisionActive`\n  / `collisionEnd` events during `Engine.update`.\n\n## Spatial queries\n\n`raycast` and `queryAABB` are implemented by all three adapters:\n\n```js\nimport { Rect, Vector2d } from \"melonjs\";\n\n// nearest hit only — returns null when nothing is hit\nconst hit = app.world.adapter.raycast(new Vector2d(x0, y0), new Vector2d(x1, y1));\n// hit → { renderable, point, normal, fraction }\n\n// takes a Rect, not a Bounds\nconst inArea = app.world.adapter.queryAABB(new Rect(x, y, w, h));\n```\n\n`querySphere(centre, radius)` (or `querySphere(sphere)`) is **built-in only**,\nand only active under a 3D broadphase — i.e. `world.sortOn === \"depth\"`, which\n`Camera3d` sets. So is `raycast3d`. Check\n`world.adapter.capabilities.raycasts3d` before calling either. `raycast` itself\nis capability-gated by `capabilities.raycasts`.\n\nThe legacy `collision.rayCast(line, result)` is a different API: it takes a\n`Line`, returns an **array** of every intersecting renderable, and only exists\non the built-in path.\n\nTwo prerequisites that produce empty results rather than errors:\n\n- **`isKinematic` must be `false`** for a renderable to be in the broadphase at\n  all (a `Body` sets this for you).\n- On the **built-in** adapter, the broadphase is cleared and rebuilt inside\n  `world.update()`, so querying before the first update returns nothing.\n\n## 3D collision: `Box3d`, `Sphere` and the Z pushback\n\nThe narrowphase is not 2D-only. Two shapes carry a depth extent — `Box3d` and\n`Sphere` — and any pair of them is resolved in three dimensions, pushing back\nalong **Z** as well as X and Y.\n\n```js\nimport { Box3d, Sphere } from \"melonjs\";\n\n// a box: centre offset from the body, then half-extents on each axis\ncrate.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));\n// a sphere: centre offset, then one radius\nrock.body.addShape(new Sphere(0, 0, 0, 30));\n```\n\nReach for these for anything moving in the XZ plane under a `Camera3d` — a\nrunner dodging obstacles, a 2.5D platformer, pickups on a course. Hand-rolling\na distance check there is the usual mistake, and it throws away the\npenetration vector the response already carries.\n\n**Which of the two.** A `Box3d` is world-axis-aligned and cannot be oriented:\nit is right for walls, floors, crates and anything else that stays square to\nthe world, and wrong for anything that turns. A `Sphere` has no orientation to\nget wrong, which makes it the shape for things that tumble, and for anything\nlaid out over a curved surface — on a globe every object's own \"up\" points a\ndifferent way, so no single axis-aligned box fits any of them. It is also the\ncheaper test: one distance against a sum of radii.\n\n**Z arrives beside `overlapN`, not inside it.** `overlapN` stays the 2D unit\nvector it always was and `overlapNZ` carries the depth component, because\n`Vector3d` does not extend `Vector2d` and widening the field would have broken\nevery existing 2D handler. The two together are a 3D unit vector, and\n`overlapZ = overlapNZ * overlap` exactly as `overlapV = overlapN * overlap`.\n\n**How much of the normal is in each half depends on the shapes.** A `Box3d`\npair separates along ONE world axis, so a depth contact leaves the 2D fields\nat zero and a legacy 2D handler correctly applies no push. A `Sphere`\nseparates along the line between the two centres, which is generally diagonal:\nall three components are non-zero at once. So subtract both halves rather than\nbranching on one:\n\n```js\nonCollision(response, other) {\n    // works for either shape: the 2D half is zero when there is no 2D push\n    this.pos.x -= response.overlapV.x;\n    this.pos.y -= response.overlapV.y;\n    this.pos.z -= response.overlapZ;\n    return false;\n}\n```\n\nBranching on `if (response.overlapNZ !== 0)` and then moving only `pos.z` is\nthe mistake to avoid: correct for a box pair, and for a sphere it throws away\nthe planar half of the contact.\n\n**Mixed pairs degrade to 2D.** A `Box3d` or a `Sphere` against a `Polygon`,\n`Rectangle`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and\ntreats the planar shape as unbounded along Z, which is what keeps a sphere or\nbox body colliding with an ordinary Tiled collision layer. `overlapZ` is zero\nfor those, since only one side has a depth to resolve. Give BOTH sides a 3D\nshape when you want a depth result.\n\nThis is the narrowphase and is independent of the 3D broadphase queries above\n— `querySphere` / `raycast3d` need `world.sortOn === \"depth\"`; a 3D body shape\ndoes not.\n\n`raycast3d` measures a `Box3d` body as its box and a body that is one `Sphere`\nas that sphere, both exactly. Anything else, including a body mixing the two,\nis measured as the AABB containing its 3D shapes, and a body with none falls\nback to a bounding sphere derived from the renderable's 2D bounds.\n\n**Bounds are what the broadphase sorts by, and they come from the SHAPE.**\nEvery item is filed by its renderable's 2D `getBounds()`, so an object with no\nextent cannot be placed and silently collides with nothing. A `Mesh` takes its\nextent from its geometry and a `GLTFModel` from its part meshes, so both report\nwhere they are without being told. A body's shapes do **not** contribute, so a\ncustom collidable built on a bare `Container` has no extent until you give it\none:\n\n```js\n// a container reports an EMPTY bounds until it has a size of its own\ngroup.resize(width, height);\n```\n\n`enableChildBoundsUpdate` also gives it one, but it re-measures the whole child\nlist on every update — reach for it only when the extent genuinely IS the\nmembers' union and moves with them (a flock, a squad), not to give a fixed\ngroup a size.\n\n## Built-in world quirks\n\nThese are specific to the default world and surprise people arriving from a\nreal rigid-body engine:\n\n- **Dynamic-dynamic collision is position-based, not Newtonian.** Separation is\n  mass-proportional but the velocity response is per-body cancellation — two\n  equal-mass bodies do not exchange momentum the way you would expect.\n- **An immovable contact wins the step.** Each step records, per body, which\n  directions it has already been pushed out of, and a later dynamic-dynamic\n  contact will not push it back in one of those directions: the other body\n  takes the whole correction instead. A light body shoving a heavy one against\n  a wall therefore stops dead at the wall rather than driving it through, and\n  the shover absorbs all of the separation. The record lasts one step and is\n  not contact state — it says nothing about what a body is touching.\n- **Gravity defaults to `(0, 0.98)`** — pixels per frame², not m/s². Mutate\n  `app.world.adapter.gravity` at runtime, or pass\n  `physic: new BuiltinAdapter({ gravity })` to override it. `body.gravityScale`\n  (default `1`) scales it per body; `0` disables gravity for that body and is\n  the portable replacement for the deprecated `body.ignoreGravity`.\n- **`applyForce(x, y)` is linear only** unless you pass the optional\n  `(pointX, pointY)` application point, which generates a torque\n  `τ = r × F` into `body.angularVelocity` via `body.pseudoInertia`.\n  `applyTorque(τ)` is the direct form.\n- **Force accumulators reset at end-of-step** — for every body, including\n  static, paused and off-screen ones — so forces must be applied every frame to\n  persist.\n- **`isGrounded` is flag-based, not contact-based** — the adapter returns\n  `!body.falling && !body.jumping`, flags updated by the last resolved\n  collision, not a live contact test.\n- **`def.density` maps 1:1 to `body.mass`** (default `1`), and `def.friction` is\n  **ignored** entirely. `body.friction` is a per-axis `Vector2d` of per-step\n  velocity damping fed from `def.frictionAir`, not a surface coefficient.\n- **`body.maxVel` defaults to `(490, 490)`** and hard-clamps velocity after\n  forces and friction. The canonical movement idiom is\n  `this.body.force.x = this.body.maxVel.x`.\n- **Default `collisionType` is `ENEMY_OBJECT`**, and default `collisionMask` is\n  `ALL_OBJECT` — set both explicitly.\n- **Integration is gated on `inViewport || alwaysUpdate`** — an off-screen body\n  stops simulating unless you set `alwaysUpdate = true`. It is also gated on\n  `updateWhenPaused` while the state manager is paused, and static bodies never\n  integrate.\n- **`addBody` throws on double-registration** for a renderable that is already\n  adapter-managed.\n\n### Rotation collides on planck and matter, but not on the built-in solver\n\nThis is the sharpest behaviour difference between the adapters, and it is\nsilent — nothing warns you.\n\n| adapter | a rotating body |\n| --- | --- |\n| **builtin** | rotates **visually only**. `body.angle`, `setAngle()` and angular integration write `renderable.currentTransform`; the collision shapes are never rotated, so a spinning sprite collides as if it were upright |\n| **planck** / **matter** | rotates for real — the solver owns the body's transform and collision follows it |\n\nSo a turret, a swinging platform or a tumbling crate that must *collide*\nrotated needs planck or matter. Under the builtin solver, approximate it with a\nshape that is close enough at every angle (a circle, or a square for\n90° steps), or rotate the shapes yourself with `body.rotate(angle)` — which\ndoes rotate them, unlike `setAngle()`.\n\n`body.rotate()` applies a **delta** and does not update `body.angle`; the two\nare unlinked, so a body driven by `rotate()` still reports an angle of `0`.\nTrack it yourself if you need to read it back. Before 20.7 it also threw on a\n`Box3d` or `Point` shape, and did not tell its owner that the body's bounds had\ngrown.\n\n`adapter.getBodyShapes(renderable)` reports whichever is true: the geometry\nthe engine is actually simulating on planck and matter (matter-adapter 1.3.0 /\nplanck-adapter 1.4.0 and later), unrotated shapes on the builtin solver because\nthere they genuinely are. That is what the debug overlay draws, so the hitbox\nyou see is the hitbox that collides — a sprite spinning inside a stationary red\nbox on the builtin is the solver telling you the truth, not a drawing bug.\n\nOn planck and matter that report comes from the engine rather than from your\ndefinitions, so it shows the approximations they make: an `Ellipse` appears as\nthe circle of average radius it is simulated as, a concave polygon as the\nconvex pieces it was decomposed into, and a `Rect` as a `Polygon`. Your\nauthored shapes are untouched and still readable on `renderable.bodyDef.shapes`.\n\n**If you need rotation to affect collision, use planck or matter.** That is the\ndividing line: the builtin is a position-based solver whose SAT never reads\n`body.angle`, so rotation there is a visual property. `body.rotate(angle)` is\nthe workaround, not a rotating body — it permanently moves the shapes, does not\ntrack an angle, and gives you no angular velocity, torque or rotational\nresponse from a collision. A game whose collision genuinely turns (a spinning\nhazard, a swinging bridge, a car, anything with torque) wants a real rigid-body\nadapter rather than a workaround.\n\n## Porting between adapters\n\nSame call, different magnitude — these are the ones that waste an afternoon:\n\n- **The portable API converts for you, the native handle does not.**\n  `adapter.setVelocity` / `body.setVelocity(x, y)` are in pixels on every\n  adapter; reaching for the raw handle (`body.setLinearVelocity` on planck) puts\n  you in metres per second, scaled by `pixelsPerMeter`.\n- **Time base differs.** Built-in `body.vel` is pixels *per frame*; planck and\n  matter integrate in seconds. The same number is ~60× off.\n- **Force magnitudes are not comparable** between adapters; re-tune rather than\n  reuse. `applyForce` is a per-step accumulator everywhere (cleared each step,\n  so call it every frame) — `applyImpulse` is the one-shot. Matter has no native\n  impulse, so the adapter emulates it as `Δv = J / mass` and ignores the\n  application point.\n- **`body.position` is the centroid in matter**, while `renderable.pos` is\n  top-left in melonJS. The adapter stores a per-body offset so `renderable.pos`\n  stays correct — but read `body.position` directly and you get the centroid.\n- **Degenerate (zero-area / collinear) polygons — a Tiled polyline, a `Line` —\n  cannot be built by matter and fall back to their axis-aligned bounding box.**\n  Any other shape type throws `unsupported shape type`.\n- **Ellipses become circles on matter**, using the average of the two radii.\n- **`isGrounded` is capability-gated** (`capabilities.isGrounded`) and computed\n  differently per adapter: a flag read on built-in, a live scan of active\n  contacts on matter.\n\nAdapter-specific escapes exist (`(this.body as MatterAdapter.Body).frictionAir`,\n`adapter.matter.Constraint.create(...)`) but are not portable — flag them if you\nuse them. `adapter.capabilities` (`constraints`,\n`continuousCollisionDetection`, `sleepingBodies`, `raycasts`, `raycasts3d`,\n`velocityLimit`, `isGrounded`) is the portable way to branch.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| entity jitters or does not move | assigning `pos` instead of applying force |\n| collision handler runs twice per frame | legacy `onCollision` on a dynamic pair (built-in) — use `onCollisionActive` |\n| `return false` from a handler does nothing | return values are only honoured by legacy `onCollision` — use `isSensor` / `isTrigger` |\n| crash or a null body when spawning from `onCollisionStart` | world mutated during contact dispatch (locked world under planck) |\n| `raycast` / `queryAABB` finds nothing | `isKinematic` still `true`, or no `world.update` yet (built-in) |\n| `querySphere` / `raycast3d` is not a function | 2D adapter, or built-in without a 3D broadphase (`sortOn !== \"depth\"`) |\n| a 3D contact reports no overlap to push against | read `overlapNZ` / `overlapZ` — a Z-axis MTV leaves `overlapN` / `overlapV` at zero |\n| two 3D objects never separate in depth | one of them carries a planar shape; a mixed pair is tested as the box's XY footprint, unbounded along Z |\n| `response.depth` / `response.normal` are `undefined` | reading a legacy `onCollision` response — it carries `overlap` / `overlapN` |\n| `onCollisionEnd` handler throws on `response` | built-in dispatches it with `undefined` |\n| off-screen bodies stop simulating | built-in gating on `inViewport`; set `alwaysUpdate` |\n| a body sinks through one shape of a multi-shape body | fixed in 20.7; before that the compound pass only ever re-resolved the first overlapping pair |\n| a body will not stay put on a slope on the builtin | it is riding the slope and sliding: the builtin has no surface friction, only `frictionAir` damping. Damp it yourself, or use matter/planck |\n| the sprite is drawn offset from its hitbox | pre-20.7 `anchorPoint` moved the drawing but not the collision shapes; upgrade, and drop any offsets you added to compensate |\n| a small shape sits at the sprite's corner | a shape's offset is measured from the drawn frame's TOP-LEFT; `new Sphere(0, 0, 0, r)` is the corner, `(width / 2, height / 2)` is the centre |\n| a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |\n| a body in a pile sinks into the floor (built-in) | fixed in 20.7 for anything with an immovable side; bodies pinned only by other DYNAMIC bodies still overlap by a pixel or two, which is what planck/matter are for |\n| forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |\n| `body.position` disagrees with `renderable.pos` on matter | matter stores the centroid, melonJS the top-left — the adapter offsets between them |\n| `bodyDef.shapes: \"…\"` throws \"is not loaded\" | the name must match the `name` the JSON was preloaded under, not its filename |\n| everything falls far too fast or too slow after swapping backends | gravity is a different unit per backend; see the table above |\n| debris lands holding its spawn angle and never tumbles | `fixedRotation: true`, or matter-adapter < 1.4.0 / planck-adapter < 1.5.0 |\n| a rotated sprite collides as if upright | the built-in solver rotates visually only — `body.angle` never rotates the shapes. Use planck/matter, or `body.rotate(angle)` |\n| the debug hitbox does not follow a spinning body | pre-1.2.1 matter-adapter / pre-1.3.1 planck-adapter returned the authored shapes rather than the rotated ones |\n| a Tiled polyline becomes a solid box on matter | matter cannot build a zero-area polygon; the adapter falls back to its AABB |\n\n## Related skills\n\n- `melonjs-renderables` — `isKinematic`, which also gates the broadphase\n- `melonjs-tilemaps` — collision shapes authored in Tiled\n- `melonjs-loading-assets` — preloading a shape editor's export as JSON\n\n<!-- File: packages/melonjs/skills/melonjs-plugins/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-plugins\n\nPath: `packages/melonjs/skills/melonjs-plugins/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-plugins/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-renderables/SKILL.md (Tokens: ~3841 | Category: anthropic-skill) -->\n---\nname: melonjs-renderables\ndescription: \"Use this skill when subclassing Renderable, writing a custom draw() method, handling pointer or click events on game objects, applying post effects and shaders, or controlling draw order. Covers the pos-relative draw contract, isKinematic and input, addPostEffect vs the deprecated shader property, and z-ordering. Triggers on: Tween, easing, animate, Renderable, extends Renderable, draw(, update(, isKinematic, pointerEvent, registerPointerEvent, onClick, addPostEffect, removePostEffect, ShaderEffect, addChild, z-order, anchorPoint, floating.\"\nlicense: MIT\n---\n\n# Renderables, input and effects\n\n> Subclassing `Renderable`, custom drawing, receiving pointer events, and\n> applying post effects — with the traps that produce silently wrong output\n> rather than an error.\n\nEverything here is a **silent failure**: the code runs, nothing throws, and the\nresult is wrong. Those are the cases worth memorising, because nothing in the\ntype signatures warns you.\n\n## 1. A custom `draw()` must position itself from `this.pos`\n\nThe renderer is **not** pre-translated to the renderable's position when `draw()`\nis called. `Container.draw` translates by the *container's* own `pos` and then\ncalls `child.preDraw` / `draw` / `postDraw`; `preDraw` never translates by the\nchild's `pos`. Read `this.pos` and draw relative to it.\n\n```js\nclass Marker extends Renderable {\n    draw(renderer) {\n        // ✗ draws at the parent container's origin, not at the marker\n        renderer.fillRect(0, 0, this.width, this.height);\n\n        // ✓\n        renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);\n    }\n}\n```\n\nThe symptom is an object that renders correctly at position (0, 0) and appears\nstuck at the top-left as soon as you move it.\n\n**Zero the anchor in custom-draw renderables.** `preDraw` does not translate to\n`pos`, but it *does* apply the anchor offset `(-width * anchorPoint.x,\n-height * anchorPoint.y)`. With the default centred anchor, drawing at\n`this.pos` therefore lands half a size off:\n\n```js\nconstructor(x, y, w, h) {\n    super(x, y, w, h);\n    this.anchorPoint.set(0, 0);   // ← or every custom draw is offset by -w/2, -h/2\n}\n```\n\n## 2. `isKinematic` must be `false` to receive input or collisions\n\n`Renderable.isKinematic` **defaults to `true`**, which means *\"physics collision\nand input events will not impact this renderable\"*. A kinematic renderable is\nskipped by the pointer-event dispatcher entirely.\n\n```js\nclass Button extends Renderable {\n    constructor(x, y) {\n        super(x, y, 100, 40);\n        this.isKinematic = false;          // ← required, or clicks never arrive\n        input.registerPointerEvent(\"pointerdown\", this, () => { /* … */ });\n    }\n}\n```\n\nThe symptom is a handler that is registered, looks correct, and never fires.\n\nNote that `Container`, `Camera2d`, `Draggable`, `DropTarget` and the `UI*`\nelements set it to `false` for you, and so does the `Body` constructor on its\nowner (which covers `Entity` and anything you give a body to) — so a plain\n`Renderable` or `Sprite` subclass with no body is the case that catches people.\n\nThe same flag has a second, less obvious effect: it is also the opt-in to the\nworld **broadphase**. The quadtree/octree insert skips any child with\n`isKinematic === true`, so body-less objects you want `adapter.queryAABB` /\n`querySphere` to find need `isKinematic = false` as well. The builtin\nnarrowphase likewise requires *both* partners to be non-kinematic.\n\n## 3. Draw order: only `addChild(child, z)` works\n\nThere are three separate traps here and they compound.\n\n```js\nworld.addChild(sprite, 10);   // ✓ the only reliable form\n```\n\n1. **`renderable.z` does not exist.** There is no `z` accessor anywhere on\n   `Renderable` (nor on `Rect`/`Polygon` above it) — only `depth`, which proxies\n   `pos.z`, and the container's default `sortOn: \"z\"` comparator reads `pos.z`.\n   Writing `sprite.z = 100` creates an inert property that nothing reads.\n   Several shipped examples still contain `this.z = Number.POSITIVE_INFINITY`;\n   those HUDs draw on top only because they happen to be added last. (A `z` key\n   in a `Sprite`/`ImageLayer` *settings* object is a different thing and is real\n   — it assigns `pos.z` in the constructor — but trap 2 then overwrites it.)\n2. **Setting `depth` *before* `addChild` is overwritten.** With `autoDepth` on\n   (the default), `addChild` without an explicit `z` assigns\n   `child.pos.z = children.length` after the push, discarding whatever you set.\n3. **Setting `depth` *after* `addChild` does not reorder.** A `\"z\"` sort runs\n   when a child is added (`autoSort`, deferred to the next tick), not when\n   `pos.z` changes. If you must change it later, call `container.sort()`\n   yourself, or use `moveToTop` / `moveUp` / `moveDown` / `moveToBottom`, which\n   reorder the children themselves. (An `\"x\"` / `\"y\"` sort is different: see\n   below.)\n\n### Sorting on `\"y\"` (or `\"x\"`), and `sortOffset`\n\nA container with `sortOn = \"y\"` draws its children from the smallest `pos.y` to\nthe largest, so whatever is lower on screen covers what is above it (top-down and\nisometric games). `\"x\"` does the same along x. A different `pos.z` still decides\nfirst, so keep the children that should interleave at one z.\n\n- **Moving a child re-sorts by itself.** With `autoSort` on (the default), the\n  container checks its order at the end of every update, after physics and the\n  children's own updates, and re-sorts when anything is out of place: a walking\n  character needs no `sort()` call. A frame where nothing moved costs one pass\n  over the children and does not cause a redraw. Turn `autoSort` off to keep\n  the children in the order you put them.\n- **Ties keep insertion order, and the child added LAST is drawn BEHIND.** A\n  torch placed at its wall's exact position disappears behind the wall.\n\n`sortOffset` changes the ORDER only, never where anything is: the child is lined\nup as if it stood that many pixels lower (positive) or higher (negative), and is\ndrawn exactly where it was. Use it when `pos` is not the point to sort by:\n\n```js\ntorch.sortOffset = 1;                        // win the tie with the wall it hangs on\ndust.sortOffset = door.pos.y - dust.pos.y;   // an emitter's pos is the top of its\n                                             // spawn area: line it up with the door\nprop.sortOffset = prop.height;               // sort by the bottom of the image\n```\n\nA container that keeps itself sorted picks a change up at its next update (with\n`autoSort` off, call its `sort()`). `\"z\"` and `\"depth\"` sorting ignore it. It must be a finite number (a string\nor `NaN` throws, rather than scrambling every sibling), and a pooled renderable\nstarts again at `0`.\n\nThe exception: under `Camera3d` the world's `sortOn` is `\"depth\"`, and the\ncamera calls `container.sortNow(true)` every frame. Note that mode does **not**\nsort by `pos.z` as a layer index — it sorts by squared distance from the camera\nin world space (nearest first in the array, painted far→near). So changing\n`depth` does reorder there, but as a position change, not as a layer number.\n\n```js\n// ✗ overwritten by autoDepth\nsprite.depth = 10;\nworld.addChild(sprite);\n\n// ✗ no resort happens (except under Camera3d)\nworld.addChild(sprite);\nsprite.depth = 10;\n\n// ✓\nworld.addChild(sprite, 10);\n```\n\n## 4. `removePostEffect()` destroys the effect\n\nTo disable an effect temporarily, toggle it. Removing it frees its GPU\nresources and leaves you holding a dead object.\n\n```js\n// ✗ effect is destroyed; re-adding it will not work\nsprite.removePostEffect(effect);\n\n// ✓\neffect.enabled = false;\n```\n\nException worth knowing: an effect with `effect.shared === true` opts out of\nauto-destroy, because it is reused across renderables.\n\nUse `addPostEffect()` / `getPostEffect()` / `removePostEffect()`. Assigning\n`renderable.shader = …` is deprecated since 19.2.0.\n\nPost effects need a programmable pipeline. A `ShaderEffect` constructed against\nthe Canvas renderer (`renderer.shaderLanguage === null`) logs a warning, stays\nat `enabled === false` and no-ops every method rather than throwing — and\n`beginPostEffect` filters disabled effects out. Since `video.AUTO` picks the\nbackend at runtime, an effect that works on your machine may do nothing on\nanother.\n\n## 5. Opacity cascades; `setChildsProperty` assigns\n\n`alpha` composes down the tree. Fading a container fades everything inside\nit, and the values multiply — a child at `0.5` inside a parent at `0.5` draws\nat `0.25`. Each renderable keeps its own value; the composition happens at\ndraw time and is undone when the renderable is finished.\n\n```js\nmodel.alpha = 0.3;                             // fades the whole rig\ncontainer.setChildsProperty(\"alpha\", 0.3, true); // REWRITES each child's alpha\n```\n\nThe two are not interchangeable. `setChildsProperty` is a bulk assignment: it\nchanges what the children *are*, so their own `alpha` now reads `0.3` and\nstays that way after the container goes back to full. Reach for it when you\nmean to change the children, and for `alpha` when you mean to fade a subtree.\n\nDo not assign alpha down a hierarchy by hand. The renderer composes it as it\nwalks the tree, so setting it at more than one level multiplies: in a rig\nnested three deep, `0.3` applied at every level lands parts on `0.3`, `0.09`\nand `0.027` depending where they sit. The symptom does not look like an\nopacity problem — a model whose parts hold *different* opacities, and which\nappears to change opacity as it turns, because turning changes which parts\nface the camera.\n\nA useful consequence: a `GLTFModel` is a `Container`, so `model.alpha` fades\nevery part of a loaded rig — which is what you want for a ghost, a fade-in,\nor a reflection.\n\n## 6. `Vector3d.set(x, y)` silently zeroes `z`\n\n```js\nv.set(10, 20);        // z becomes 0, not \"unchanged\"\nv.set(10, 20, v.z);   // keep it\n```\n\n`pos` is an `ObservableVector3d`, whose `set(x = 0, y = 0, z = 0)` does the same\n— so `this.pos.set(x, y)` wipes the object's depth.\n\n`Renderable#setPosition(x, y, z?)` is the renderable-level form and does not:\nomit `z` and the depth is left exactly as it was. Reach for it whenever you\nare moving an object rather than editing a vector, and keep `v.set(x, y, v.z)`\nfor the vector itself.\n\n## 7. A settings key the class never reads is silently ignored\n\nA settings object is a plain literal: nothing rejects a key, so a name that\nbelongs somewhere else does nothing at all and says nothing about it.\n\n```js\n// WRONG — only the RENDERER reads a `blendMode` setting, never a renderable\nconst glow = new Sprite3d(x, y, { image: \"sun\", blendMode: \"additive\" });\n\n// right: it is a property\nglow.blendMode = \"additive\";\n```\n\nThe same shape catches `transparent` on a `GLTFModel` (its parts carry it —\n`model.setChildsProperty(\"transparent\", true, true)`) and any `Mesh` setting\nhanded to a class that forwards only a curated subset. If a visual option\nappears to do nothing, check whether the class actually reads it before\ntuning the value.\n\n## 8. `isRenderable` does not hide anything\n\nIt gates `updateBounds`, not drawing. Setting it `false` leaves the object on\nscreen and merely stops its bounds tracking, which is worse than doing nothing.\n\n```js\nsprite.isRenderable = false;   // WRONG — still drawn, bounds now stale\nsprite.alpha = 0;              // hides it\n```\n\nUse `alpha` for a blink, or remove the child for a long absence.\n\n## A mask belongs to you, not to the renderable\n\n`renderable.mask` takes a shape you created, and `destroy()` releases the\nreference without recycling it: the same instance may still be masking\nsomething else, so the engine has no claim on it. If you pool masks, release\nthem yourself.\n\nBefore 20.7.0 `destroy()` tried to recycle it and **threw** for every type\n`mask` accepts, because it used the legacy name-keyed pool and no geometry\nclass is registered there. It failed mid-teardown, so a masked renderable\ncould not be destroyed or removed without keepalive at all.\n\n## Update and draw\n\n```js\nclass Enemy extends Renderable {\n    update(dt) {\n        this.pos.x += this.speed * (dt / 1000);\n        return true;                       // ← true means \"redraw me\"\n    }\n    draw(renderer) { /* … */ }\n}\n```\n\n`update()` returns a boolean meaning *\"this object changed and needs\nredrawing\"*. The parent accumulates it into the container's `isDirty`, and the\napplication skips the whole draw pass while nothing is dirty — so returning\nnothing (falsy) can leave the scene frozen even though state is updating. The\nbase implementation returns `this.isDirty`, so `return super.update(dt)` is the\nother correct ending.\n\n`update()` is only called when `obj.inViewport === true` or\n`obj.alwaysUpdate === true`. An off-screen object with the default\n`alwaysUpdate = false` does not tick at all.\n\n## Anchors and floating\n\n- `anchorPoint` defaults to centre `(0.5, 0.5)` on `Renderable`, but `Container`\n  forces `(0, 0)`. Mixing them up shifts children by half their size.\n- `floating = true` pins a renderable to the screen rather than the world, which\n  is what you want for HUD elements. It opts the object out of camera transforms\n  and out of ancestor position accumulation.\n\n## Prefer built-in features\n\nBefore writing custom `draw()` code, check whether the engine already covers it —\n`ParticleEmitter`, `Tween`, the `ShaderEffect` presets, `UISpriteElement` and\n`UITextButton`, the physics adapters, and the Tiled tilemap renderers all exist.\nHand-rolled equivalents miss the batching and the multi-backend support.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| object draws at its parent's origin, ignores its position | `draw()` not reading `this.pos` |\n| pointer handler registered but never fires | `isKinematic` left at its `true` default |\n| wrong draw order | anything other than `addChild(child, z)` — see section 3 |\n| `me.pool: object ... cannot be recycled` while destroying | a mask on a renderable, before 20.7.0 |\n| effect cannot be re-enabled after removal | `removePostEffect()` destroyed it |\n| effect silently does nothing | running on the Canvas renderer |\n| object appears frozen while its state changes | `update()` not returning `true` |\n| off-screen object never ticks | `update()` is gated on `inViewport` — set `alwaysUpdate = true` |\n| custom draw offset by half the size | centred `anchorPoint` default not zeroed |\n| spatial query never finds an object | `isKinematic` left `true` — not in the broadphase |\n| `z` unexpectedly 0 after a `set` | `Vector3d.set(x, y)` defaults `z` to 0 — `setPosition(x, y)` on the renderable leaves the depth alone |\n| a settings option appears to do nothing | the class never reads that key — see section 7 |\n| `isRenderable = false` did not hide the object | it gates bounds, not drawing — use `alpha` |\n\n## Related skills\n- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this\n\n- `melonjs-getting-started` — Application lifecycle and the scene graph\n- `melonjs-20-migration` — the deprecated `shader` property and other pre-20 APIs\n\n<!-- File: packages/melonjs/skills/melonjs-renderer-backends/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-renderer-backends\n\nPath: `packages/melonjs/skills/melonjs-renderer-backends/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-renderer-backends/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-scenes-and-state\n\nPath: `packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-scenes-and-state/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs-sprites-and-animation/SKILL.md (Tokens: ~2057 | Category: anthropic-skill) -->\n---\nname: melonjs-sprites-and-animation\ndescription: \"Use this skill when creating sprites, playing frame animations, or working with texture atlases in melonJS. Covers Sprite construction from a plain image or an atlas region, addAnimation/setCurrentAnimation, TextureAtlas for TexturePacker/ShoeBox/Aseprite, tint, flip, NineSliceSprite, and the centred anchorPoint that offsets naive positioning. Triggers on: Sprite, new Sprite, addAnimation, setCurrentAnimation, isCurrentAnimation, TextureAtlas, TexturePacker, Aseprite, ShoeBox, atlas, region, framewidth, frameheight, tint, flipX, flipY, NineSliceSprite, animation.\"\nlicense: MIT\n---\n\n# Sprites, animation and atlases\n\n> Creating sprites from images or atlases, driving frame animation, and the\n> positioning trap that catches almost everyone.\n\n## `pos` is the centre, not the top-left\n\n`Renderable.anchorPoint` defaults to **`(0.5, 0.5)`**, so a sprite's `pos` is its\n**centre**. `Container` forces `(0, 0)` instead, which is why containers and\nsprites appear to disagree.\n\n```js\n// a 64×64 sprite at (100, 100) covers 68..132, not 100..164\nconst s = new Sprite(100, 100, { image: \"hero\", framewidth: 64, frameheight: 64 });\n\ns.anchorPoint.set(0, 0);   // switch to top-left positioning if you prefer\n```\n\nWriting top-left maths against the centred default puts everything half a sprite\noff, with no error. It is the most common source of \"close but subtly wrong\"\nplacement.\n\n## Constructing a sprite\n\nFrom a plain image:\n\n```js\nconst hero = new Sprite(x, y, {\n    image: \"hero\",          // preloaded asset name\n    framewidth: 32,\n    frameheight: 32,\n});\n```\n\nFrom a texture atlas region:\n\n```js\nconst atlas = new TextureAtlas(loader.getJSON(\"sprites\"), loader.getImage(\"sprites\"));\nconst coin = new Sprite(x, y, { image: atlas, region: \"coin_01.png\" });\n```\n\n`framewidth` / `frameheight` define the animation grid on a sheet. Omit them and\nthe whole image is one frame.\n\n## Animation\n\n```js\nhero.addAnimation(\"idle\", [0, 1, 2, 3], 100);   // frames, ms per frame\nhero.addAnimation(\"walk\", [4, 5, 6, 7]);\nhero.addAnimation(\"die\",  [8, 9], 150);\n\nhero.setCurrentAnimation(\"walk\");\nhero.setCurrentAnimation(\"die\", \"idle\");                          // chain to idle when it ends\nhero.setCurrentAnimation(\"die\", { loop: false });                 // play once, hold the last frame\nhero.setCurrentAnimation(\"die\", { loop: false, onComplete: fn }); // …and call fn when it does\nhero.setCurrentAnimation(\"use\", { next: \"idle\", onComplete: fn }); // call fn, then chain to idle\nhero.setCurrentAnimation(\"run\", { onFrame: (frame) => {            // each frame playback reaches\n    if (frame === 3 || frame === 8) footstep();\n} });\n\nif (hero.isCurrentAnimation(\"walk\")) { /* … */ }\n```\n\nFrame indices are into the `framewidth`/`frameheight` grid. With an atlas, use\n`atlas.getAnimationSettings([regionNames])` and spread it into the Sprite\nsettings, or `atlas.createAnimationFromName([regionNames])`, which returns a\nready-made Sprite. On a fixed-cell spritesheet only numeric indices are\naccepted — passing region names throws.\n\nThe second argument to `setCurrentAnimation` is polymorphic. A **string** chains\nto that animation when this one ends. An **options object** takes\n`{ loop, next, speed, onComplete, onFrame }`, and only an explicit `loop: false`\nstops the looping. A **bare function** is the legacy form: it fires at *every* cycle end,\nand only holds the last frame if it returns `false`. Omit the argument entirely\nand the animation loops forever.\n\n`onFrame(frame)` is called with frame 0 when the animation starts, then with each\nframe playback reaches, so a footstep or a hit lands on its frame without polling\n`getCurrentAnimationFrame()`. A long tick reports every frame it goes past. It is\nnot called for a frame set with `setAnimationFrame`, nor for the frame 0 a\nplay-once animation would wrap to. Keep the callback in a field when you call\n`setCurrentAnimation` every frame: re-selecting keeps the first one anyway.\n\nWith both `next` and `onComplete`, the callback runs first and the chained\nanimation starts with none of its own, so a one-off action (a pickup, an\nattack) needs no manual switch back to idle.\n\nRe-selecting the animation that is already current is a no-op, so calling\n`setCurrentAnimation` every frame is safe, and an `isCurrentAnimation` check\naround it adds nothing. An unknown id throws.\n\n## Texture atlases\n\n`TextureAtlas` reads several formats:\n\n| source | how |\n|---|---|\n| TexturePacker / Free Texture Packer JSON | `new TextureAtlas(loader.getJSON(name), loader.getImage(name))` |\n| Aseprite **JSON export** | same as TexturePacker — the format is detected from `meta.app` |\n| Aseprite **`.aseprite` / `.ase` binary** | loader type `\"aseprite\"`, which stores the composited image *and* a JSON sidecar under the one asset name |\n| ShoeBox | JSON export using the melonJS exporter settings file; same call as TexturePacker |\n| fixed-cell spritesheet | `new TextureAtlas({ framewidth, frameheight, anchorPoint }, loader.getImage(name))` |\n| multi-page | pass an array of atlas JSON objects as the first argument |\n\nHelpers: `createSpriteFromName(name)`, `createAnimationFromName([names])`,\n`getAnimationSettings([names])`.\n\nAtlases matter for performance: the GPU batchers bind several textures at once\n(up to `renderer.maxTextures`), so a handful of separate images still batch —\nbut once a frame crosses that many distinct textures the batch has to flush.\nPacking into one atlas keeps everything on a single binding.\n\n## Visual properties\n\n```js\nsprite.setTint(\"#ff8000\");   // multiply tint\nsprite.alpha = 0.5;\nsprite.flipX(true);                   // mirror horizontally\nsprite.flipY(true);\nsprite.blendMode = \"additive\";        // see the effects skill for the full list\nsprite.scale(2, 2);                   // ← multiplicative, see below\n```\n\n**`scale()` is multiplicative, not absolute.** Calling it every frame compounds.\nFor absolute scaling reset first:\n\n```js\nsprite.currentTransform.identity();\nsprite.currentTransform.scale(s, s, 1);\n```\n\nThis bites hardest with pooled sprites, which arrive carrying their previous\nlife's transform.\n\n## `NineSliceSprite`\n\nFor panels and dialogue boxes that stretch without distorting their corners:\n\n```js\nnew NineSliceSprite(x, y, {\n    image: \"panel\", width: 300, height: 120,\n    insetx: 12, insety: 12,     // lowercase x/y — `insetX` is silently ignored\n});\n```\n\n`width` and `height` are mandatory and the constructor throws without them.\nThe insets default to a quarter of the frame.\n\n## Pooling\n\nFrequently spawned sprites should be pooled:\n\n```js\nimport { createPool } from \"melonjs\";\n\nconst bulletPool = createPool((x, y) => {    // me.pool is deprecated\n    const instance = new Bullet(x, y);\n    return { instance, reset: (x, y) => instance.onResetEvent(x, y) };\n});\nconst b = bulletPool.get(x, y);             // new Bullet(x, y), or reset(x, y) on a reused one\nworld.removeChild(b);                       // returns it to bulletPool automatically\n```\n\nTwo consequences:\n\n- A recyclable class's `onResetEvent` must reset **everything** its lifetime\n  mutates — alpha, tint, scale, animation state. Anything you forget carries\n  over to the next use.\n- Pool-recycled objects do **not** fire `onDestroyEvent` on removal. Pair event\n  subscriptions with `onActivateEvent` / `onDeactivateEvent` instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| everything offset by half a sprite | top-left maths against the centred `anchorPoint` |\n| sprite grows every frame | `scale()` is multiplicative — reset the transform |\n| a recycled sprite keeps its old look | `onResetEvent` does not restore every mutated property |\n| event handlers leak on pooled objects | bound in the constructor rather than `onActivateEvent` |\n| animation never advances | no `framewidth`/`frameheight`, so the sheet is one frame |\n| animation plays once and stops | `{ loop: false }`, or a callback that returned `false` |\n| sprites do not batch | more distinct textures in one frame than `renderer.maxTextures` |\n| nine-slice corners stretch anyway | `insetX`/`insetY` instead of `insetx`/`insety` |\n\n## Related skills\n\n- `melonjs-renderables` — anchors, draw order, custom draw\n- `melonjs-getting-started` — preloading images and atlases\n\n<!-- File: packages/melonjs/skills/melonjs-tilemaps/SKILL.md (Tokens: ~4675 | Category: anthropic-skill) -->\n---\nname: melonjs-tilemaps\ndescription: \"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 registerTiledObjectClass and 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, registerTiledObjectClass, registerTiledObjectFactory, TMXObject, resolvedProperty, resolvedProperties, tile class, tile properties.\"\nlicense: MIT\n---\n\n# Tiled maps\n\n> melonJS's flagship content pipeline. Maps are authored in\n> [Tiled](https://www.mapeditor.org/) and loaded through the `level` director —\n> you rarely touch the `TMX*` classes directly.\n\n## Loading a level\n\nPreload the map **and** everything it references — the `.tmx`, any external\n`.tsx` tilesets, and the tileset images:\n\n```js\nawait loader.preload([\n    { name: \"map1\",    type: \"tmx\", src: \"data/map/map1.tmx\" },\n    { name: \"tileset\", type: \"tsx\", src: \"data/map/tileset.tsx\" },\n    { name: \"tiles\",   type: \"image\", src: \"data/img/tiles.png\" },\n]);\n\nlevel.load(\"map1\");           // inside Stage.onResetEvent\n```\n\nAsset `type` is `\"tmx\"` for maps and `\"tsx\"` for external tilesets — those are\nthe only two type strings the loader knows. The *file* can be `.tmx` / `.tsx`\n(XML), or `.json` / `.tmj` / `.tsj`; the parser branches on the extension, not\non the type. `type: \"tmj\"` throws `unknown or invalid resource type`. You can\nalso skip `src` and pass the map inline via `data` (with `format: \"json\"` or\n`\"xml\"`).\n\n`level.load(levelId, options)` accepts `container` (default `game.world`),\n`onLoaded` (default `game.onLevelLoaded`), `flatten` (default `game.mergeGroup`),\n`setViewportBounds` (default **`true`**) and `async` (default `false`). It throws\n`level <id> not found` for an unknown id — synchronously, in both forms, because\nthat is a typo rather than a load failure.\n\nThose `game.` defaults are the engine's own, and `game` is whichever\n`Application` finished `init()` last. **Pass `container` explicitly** from a\n`Stage`, which has the right one: `level.load(\"map\", { container: this.app.world })`.\n\n**`level.load` is deferred while the game loop is running.** It calls\n`state.stop()` and finishes the load on a timer, after the current frame, so by\ndefault it returns `true` before anything is in the world. Two ways to sequence\nwork after it:\n\n```js\n// await it\nawait level.load(\"map1\", { async: true });\n// the world is populated here\n\n// ...or use the callback / event, which fire in both forms\nlevel.load(\"map1\", { onLoaded: () => this.spawnPlayer() });\n```\n\n`async: true` is the only thing that changes the return value — everything else\nbehaves identically, `onLoaded` included. Without it the call returns a boolean,\nso `await level.load(\"map1\")` is not an error and does not await the load:\n`await true` resolves immediately, while the load is still sitting on a timer.\nPass the flag when you mean to await.\n\n`level.reload()`, `level.next()` and `level.previous()` take the same `async`\noption and resolve the same value they return — so `if (level.next())` becomes\n`if (await level.next({ async: true }))`. Running out of levels reports `false`\neither way rather than throwing. `level.getCurrentLevelId()` and\n`level.levelCount()` round out the namespace. `flatten: false` wraps each Tiled\nobject group in its own `Container` named after the group.\n\n## Spawning entities from Tiled objects\n\nObjects are matched against a factory registry, in this order: the object's\nTiled **class**, then its **name**, then, for a tile object with no class of\nits own, its **tile's class**, then the structural fallbacks `\"text\"`,\n`\"tile\"` and `\"shape\"`.\n\n```js\nimport { registerTiledObjectClass, registerTiledObjectFactory } from \"melonjs\";\n\nregisterTiledObjectClass(\"Enemy\", Enemy);   // ← matches Tiled class OR name\nregisterTiledObjectFactory(\"Spine\", (settings, map) => { /* → Renderable */ });\nlevel.load(\"map1\");\n```\n\nThese are the entry points to use. The registry is Tiled's own, held by the\nobject factory, and has nothing to do with object pooling.\n\n`pool.register(className, classObj)` **also** works and registers a Tiled\nfactory as a side effect, under the name and again under an `me.`-prefixed\nalias, unless you set `pool.autoRegisterTiled = false`. That is the older\nspelling, kept working for maps and games written against it, and it now\nprints a one-off deprecation notice: `me.pool` is deprecated since 18.0.0.\n\nIts third argument no longer buys anything here. It used to make Tiled\nobjects recyclable; they are constructed directly now, and the container\nreturns children to typed pools rather than to the name-keyed one. To pool a\nclass you spawn constantly, give it a `createPool` and register it for Tiled\nseparately:\n\n```js\nregisterTiledObjectClass(\"mainPlayer\", PlayerEntity);\n```\n\nRegister **before** loading. With no match the object falls through to the\nstructural factory and becomes a plain `Renderable` with a static body — it does\nnot error, it just has none of your behaviour.\n\n`registerTiledObjectClass` **throws** if you register a *different* constructor\nunder a name already taken (re-registering the same one is a no-op).\n`registerTiledObjectFactory` overwrites and only `console.warn`s, so it is the\none to use for replacing something already registered.\n\n**Overriding a built-in name always works.** The engine's own classes are\nDEFAULTS: they fill a name nothing else has claimed, and yours takes it over\nwhenever you register, before or after a map has loaded.\n\n```js\nregisterTiledObjectClass(\"Trigger\", MyTrigger);   // whenever you like\n```\n\nThe throw is for a clash between two of *your* classes under one name. For\nthat case `registerTiledObjectFactory` overwrites and only warns.\n\n### Classes and properties set on tiles\n\nIn Tiled, a tile can carry a class and custom properties (tileset editor), and\nevery object placed from that tile shows them as its own. melonJS follows:\n\n- **The tile's class.** A tile object with no class is built by the class\n  registered under its tile's class, so a designer sets \"Chest\" once on the\n  chest tile and every chest dropped in the map is a `Chest`. The object's\n  own class or registered name still comes first, and a tile classed like a\n  built-in (`Sprite`, `Trigger`, `shape`...) stays a plain tile.\n- **The tile's properties.** Read them with `settings.resolvedProperty(name)`\n  or `settings.resolvedProperties()`, which merge the tile's properties under\n  the object's own, as Tiled's scripting API of the same names does. They are\n  **not** set on `settings` by name: only the object's own properties are,\n  as before, so a tile property named like a sprite setting never reaches a\n  plain tile sprite.\n\n```ts\nimport { registerTiledObjectClass, Sprite, type TMXObject } from \"melonjs\";\n\n// the custom properties set on the object itself, by name\ninterface DoorSettings extends TMXObject {\n    locked?: boolean;\n}\n\nclass Door extends Sprite {\n    locked: boolean;\n    facing: string;\n    constructor(x: number, y: number, settings: DoorSettings) {\n        super(x, y, { image: \"door\" });\n        this.locked = settings.locked ?? false;\n        // on the object, or else on the door tile in the tileset\n        this.facing = settings.resolvedProperty(\"facing\") ?? \"N\";\n    }\n}\nregisterTiledObjectClass(\"Door\", Door);   // and give the door tile the class \"Door\"\n```\n\n`TMXObject` is the type of the `settings` a registered class (or factory)\nreceives: the object as read from the map, with `tile` (a `Tile`) on a tile\nobject. Extend it with the properties your class reads.\n\nFor a class that is not a `Sprite` itself (a `Container` holding the tile and\nsomething else, say), `settings.tile.getRenderable()` returns the tile's sprite\nas a plain tile object would get it: from a tileset image, an image collection\nor an animated tile alike, with no region names to build by hand.\n\nmelonJS pre-registers `Renderable`, `Sprite`, `NineSliceSprite`, `Text`,\n`BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable`, `Trigger`\nand `Entity` as Tiled classes, so `Collectable` and `Trigger` work out of the\nbox. Each is registered under its `me.`-prefixed alias as well, so a map\nauthored against melonJS 1.x finds them where it looks.\n\nA `Trigger` forwards a **fixed list** of settings to `level.load()` — not\nwhatever you pass it. Today that list is `container`, `onLoaded`, `flatten`,\n`setViewportBounds` and the glTF scene options (`scale`, `rightHanded`,\n`lights`, `lightIntensityScale`, `castGroundShadow`, `shadowGroundY`); see the\n3D assets skill. Anything outside it is **dropped silently**, which is worth\nknowing before you spend an afternoon on a map that loads at the wrong scale.\n`async` is excluded deliberately — the trigger needs to sequence its own\ntransition.\n\n## Camera bounds\n\n`level.load` sets the viewport bounds to the map size for you —\n`setViewportBounds` defaults to `true`, and the handler is re-run on every\nviewport resize, centring the map when it is smaller than the screen. You only\nneed to do it by hand when you opted out:\n\n```js\nlevel.load(\"map1\", { setViewportBounds: false });\napp.viewport.setBounds(0, 0, mapWidth, mapHeight);   // now it's on you\n```\n\nThe same is true if you drive `TMXTileMap.addTo(container, flatten, setViewportBounds)`\ndirectly — note that method's `setViewportBounds` defaults to **`false`**, the\nopposite of `level.load`'s.\n\n## Collision shapes\n\nDraw collision geometry in Tiled on an object layer and melonJS turns it into\nworld shapes automatically — rectangles, polygons, polylines, ellipses and\npoints all work.\n\nThe contract is the **layer name**: melonJS lowercases it and checks whether it\n*contains* `\"collision\"` (so `collision`, `Collision`, `world collision` all\nmatch). Inside such a layer, objects **with no name** get\n`collisionType = collision.types.WORLD_SHAPE` and `type = \"static\"`. A *named*\nobject in a collision layer keeps whatever its factory gave it — that is how you\nmix triggers into the collision layer.\n\nAn object that matches no factory and carries no text or tile data falls to the\n`\"shape\"` factory: a plain `Renderable` with a static `bodyDef` built from its\ngeometry, which the world auto-registers with the active adapter on `addChild`.\n\nFor per-object physics, set the object's custom properties in Tiled; they arrive\non the settings object passed to your entity's constructor. Properties set on\nthe object's tile are read with `settings.resolvedProperty(name)`.\n\n## Map orientations\n\nOrthogonal, **isometric**, hexagonal, staggered and oblique are all supported;\nanything else throws `<orientation> type TMX Tile Map not supported!` at load.\nIsometric maps need y-sorting so objects overlap correctly:\n\n```js\napp.world.sortOn = \"y\";       // after level.load\n```\n\nThe world then keeps its objects in order as they move (`autoSort`, on by\ndefault): a character walking around passes in front of and behind the scenery\nwith no `sort()` call.\n\n`sortOn` only accepts `\"x\"`, `\"y\"`, `\"z\"` (the default) or `\"depth\"` and throws\non anything else. `Vector2d`, `Vector3d`, their observable variants and `Polygon`\nall carry `.toIso()` / `.to2d()` for converting between screen and map space.\n\n### What the y-sort can and cannot sort\n\n`sortOn = \"y\"` sorts **objects**, never tiles. A tile layer is a single\nrenderable drawn in one piece, so a character ends up in front of all of it or\nbehind all of it. Anything a character walks both in front of and behind\n(walls, trees, furniture) has to be on an **object layer, as tile objects**,\nand in the **same** object layer as the characters: each Tiled layer gets its\nown `z`, and the y-sort compares `z` before `y`. Keep the floor on a tile layer\nbelow them. This holds for any y-sorted map, isometric or a top-down\northogonal one.\n\n```\nfloor      tile layer     the ground, drawn first\ndungeon    object layer   walls, props, chests, doors, the hero\ncollision  object layer   the shapes (not drawn)\n```\n\nTile objects with no class are built for you: each one is a `Sprite` with its\ndefault body, static, over its whole image box and typed `ENEMY_OBJECT`. A\ncharacter keeping the default `collisionMask` (every type) collides with all of\nthem, so when the scenery's real collision is drawn in the collision layer,\nmask the character to it:\n\n```js\nthis.body.setCollisionMask(collision.types.WORLD_SHAPE);\n```\n\nGive the objects with behaviour a Tiled class and register it, as above.\n\nThe `y` it sorts on is `pos.y`, and Tiled objects default to a `(0, 0)` anchor,\nso that is the **top** of each image. Sorting by the top only matches sorting by\nthe feet when every image is the same height and stands on the bottom of its\nbox, the usual layout of an isometric pack. A shorter image sorts in front of\nthings it stands behind. Either pad the art to one height, or line every object\nup by the bottom of its image with `sortOffset`, which changes the order and\nnever the position:\n\n```js\nlevel.load(\"map1\");\napp.world.sortOn = \"y\";\nfor (const child of app.world.getChildren()) {\n    child.sortOffset = child.height;   // sort by the bottom of each image\n}\n```\n\nA character you create in code follows the same convention as the scenery: by\nthe top of a frame as tall as the scenery's (`anchorPoint` `(0, 0)`), or by its\nfeet when everything sorts by the bottom.\n\nTwo equal positions keep insertion order, and the one added **last** is drawn\n**behind**: a decoration placed at its wall's exact position disappears. Give\nit `sortOffset = 1`. A `Container` or a particle emitter is sorted by its top\nleft corner (an emitter's by the top of its spawn area); give it the offset to\nthe point it belongs with, such as `door.pos.y - dust.pos.y`.\n\n## Image layers\n\n`ImageLayer` gives parallax backgrounds with a ratio per axis, set in Tiled or in\ncode. Repeat behaviour matches Tiled's.\n\n## Compressed maps need a plugin\n\nA map saved with **gzip, zlib or zstd** compressed layer data throws\n`No inflate function set — GZIP/ZLIB decompression not supported!` at parse time\nwithout:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { TiledInflatePlugin } from \"@melonjs/tiled-inflate-plugin\";\n\nplugin.register(TiledInflatePlugin);   // before preloading any compressed map\n```\n\nUncompressed base64 and CSV layer data need no plugin. If a map loads in Tiled\nbut not in melonJS, check the layer compression setting first.\n\n## Rendering\n\nEach tile layer resolves to one of three `renderMode`s — `\"shader\"`,\n`\"prerender\"` or `\"perTile\"` — auto-selected in that order of preference, or\nforced per layer. The `\"shader\"` path is a single quad per tileset with a\nper-pixel GID lookup in the fragment shader. It requires **all** of:\n\n- a renderer that reports GPU tile-layer support (WebGL 2 / WebGPU — not Canvas),\n- `world.gpuTilemap !== false` (the `gpuTilemap` application setting, default `true`),\n- an **orthogonal** layer — isometric, hexagonal, staggered and oblique always\n  fall back,\n- no collection-of-image tileset, no non-zero `tileoffset`, and tile overflow\n  within the shader's 4-cell limit.\n\nWhen a layer falls back, melonJS `console.warn`s once with the reason. Practical\nconsequences: fewer tilesets means fewer batches (one packed tileset beats\nseveral small ones), and on the Canvas fallback large maps get noticeably\nslower. `world.preRender` (or a `preRender` layer property in Tiled) opts an\nunanimated layer into the offscreen-bake path instead.\n\n## Symptom → cause\n\n| symptom | cause |\n|---|---|\n| 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` |\n| the world is still empty right after `level.load` | the load is deferred to a timer while the loop runs — `await level.load(id, { async: true })`, or use `onLoaded` / `LEVEL_LOADED` |\n| `await level.load(id)` returned `true` rather than a promise | without `async: true` the call returns a boolean; `await true` resolves immediately and the load has not run yet — pass the flag when you mean to await |\n| `level <id> not found` | the map was never preloaded, or the asset `name` differs from the id passed to `load` |\n| `unknown or invalid resource type` | asset `type` set to `\"tmj\"` / `\"tsj\"` — use `\"tmx\"` / `\"tsx\"` with the `.tmj` / `.tsj` file |\n| camera will not scroll | `setViewportBounds: false`, or the map was added with `addTo()` (which defaults to `false`) |\n| `No inflate function set` at parse time | compressed layers without the tiled-inflate plugin |\n| collision shapes are not solid | the object layer's name does not contain `\"collision\"`, or the objects are named |\n| isometric objects overlap wrongly | `world.sortOn` not set to `\"y\"` |\n| a character is always in front of (or behind) every wall | the walls are on a tile layer, or on another object layer than the character: each layer has its own `z`, compared before `y` |\n| a small prop draws over a character standing in front of it | images of different heights: the y-sort uses `pos.y`, the top of a Tiled object's image. Pad the art, or set `sortOffset = height` on every object to sort by the bottom |\n| a decoration placed at the same position as a wall disappears behind it | a tie keeps insertion order and draws the later one behind: `sortOffset = 1` |\n| particles (or a container) at the foot of something draw over a character standing in front of it | they sort by the top of their spawn area (or the container's top left): set `sortOffset` to line them up with what they belong to |\n| a character is stopped short of walls and props placed as tile objects | it collides with their default bodies (static, `ENEMY_OBJECT`, the whole image box); mask it to `collision.types.WORLD_SHAPE` when the collision layer holds the real shapes |\n| a property set on a tile is undefined on the object's `settings` | tile properties are not copied onto `settings`: read them with `settings.resolvedProperty(name)` |\n| objects placed from a classed tile come out as plain sprites | that class is not registered, it is a built-in name, or the object has a class or a registered name of its own, which come first |\n| missing tiles or a blank map | the `.tsx` or tileset image was not preloaded |\n| tilemap slow on some machines | layer fell back off the shader path — check the console warning for the reason |\n\n## Related skills\n\n- `melonjs-physics` — what the collision shapes become once loaded\n- `melonjs-getting-started` — preloading and the asset type list\n\n<!-- File: packages/melonjs/skills/melonjs-ui-and-text/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# melonjs-ui-and-text\n\nPath: `packages/melonjs/skills/melonjs-ui-and-text/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/packages/melonjs/skills/melonjs-ui-and-text/SKILL.md)\n\n<!-- File: packages/melonjs/skills/melonjs/SKILL.md (Tokens: ~2874 | Category: anthropic-skill) -->\n---\nname: melonjs\ndescription: \"Use this skill FIRST for any melonJS task; it routes to the right specialised skill and lists the companion packages. melonJS is an open-source 2D, 2.5D and 3D HTML5 game engine running on WebGPU, WebGL 2 or Canvas. Covers Application setup, the scene graph, renderables, sprites, tilemaps, physics, audio, particles, shaders, and the 3D tier — perspective cameras, meshes and glTF scenes. Triggers on: melonjs, melonJS, me.game, Application, app.init, app.world, Renderable, Sprite, Stage, state.change, Camera2d, Camera3d, Mesh, tilemap, Tiled, TMX, ParticleEmitter, ShaderEffect, how do I make a game, HTML5 game engine.\"\nlicense: MIT\n---\n\n# melonJS\n\nEntry point for the melonJS skill collection. melonJS is an open-source HTML5\ngame engine for indie developers, covering 2D, 2.5D and 3D — perspective and\northogonal cameras, GPU-accelerated tilemap rendering, post-processing effects,\ncustom shaders, 3D meshes, polygon-accurate physics and modern Tiled workflows.\nIt runs on WebGPU, WebGL 2 or Canvas with automatic fallback, and has no runtime\ndependencies.\n\n## How to use this skill\n\n1. Find the specialised skill below that matches the task and follow it.\n2. If the task involves anything 3D, read `melonjs-3d` **first** — melonJS's\n   coordinate conventions are the inverse of OpenGL's, so untutored 3D instincts\n   are backwards.\n3. If you are writing melonJS from prior knowledge rather than from the current\n   docs, read `melonjs-20-migration` — most published melonJS material predates\n   20.0 and its bootstrap no longer exists.\n4. If none of them covers the task, use the escape hatch below rather than\n   guessing at an API.\n\n## Skill router\n\n| Skill | Load when… |\n|---|---|\n| [melonjs-getting-started](../melonjs-getting-started/SKILL.md) | Bootstrapping a game, `new Application`, `await app.init()`, choosing a renderer, preloading assets. |\n| [melonjs-20-migration](../melonjs-20-migration/SKILL.md) | Code based on melonJS 19.x or earlier, or an API that \"should exist\" but doesn't — `video.init`, `Entity`, `renderable.shader =`. |\n| [melonjs-scenes-and-state](../melonjs-scenes-and-state/SKILL.md) | `Stage` subclasses, the state manager, transitions, pausing, the update loop, timers, tweens, saved data. |\n| [melonjs-renderables](../melonjs-renderables/SKILL.md) | Subclassing `Renderable`, custom `draw()`, draw order, anchors, `floating`, post effects on an object. |\n| [melonjs-sprites-and-animation](../melonjs-sprites-and-animation/SKILL.md) | `Sprite`, frame animation, texture atlases (TexturePacker / Aseprite / ShoeBox), tint, flip, pooling. |\n| [melonjs-input](../melonjs-input/SKILL.md) | Keyboard actions, pointer and touch handling, drag, gamepad, virtual controls. |\n| [melonjs-physics](../melonjs-physics/SKILL.md) | Collision, bodies, movement, spatial queries, and the planck / matter adapters. |\n| [melonjs-math](../melonjs-math/SKILL.md) | Vectors and matrices: what mutates, what allocates, and interpolating with lerp, slerp, damp or moveTowards. |\n| [melonjs-tilemaps](../melonjs-tilemaps/SKILL.md) | Tiled maps — TMX/TSX loading, spawning entities from objects, collision layers, isometric maps. |\n| [melonjs-audio](../melonjs-audio/SKILL.md) | Sound effects, music, audio sprites, spatial audio, procedural tone and noise. |\n| [melonjs-effects-and-shaders](../melonjs-effects-and-shaders/SKILL.md) | Post effects, custom GLSL/WGSL shaders, blend modes, colour grading, screen capture. |\n| [melonjs-3d](../melonjs-3d/SKILL.md) | Anything 3D or 2.5D — `Camera3d`, meshes, instancing, `Sprite3d` billboards, `Light3d`, distance fog, glTF scenes. |\n| [melonjs-3d-assets](../melonjs-3d-assets/SKILL.md) | Loading glTF/GLB or OBJ models — materials, imported lights, instancing, and what is not supported. |\n| [melonjs-camera-and-drawing](../melonjs-camera-and-drawing/SKILL.md) | Camera follow, bounds, shake and fade, coordinate conversion, and immediate-mode shape drawing. |\n| [melonjs-ui-and-text](../melonjs-ui-and-text/SKILL.md) | HUDs, buttons, menus, drag-and-drop, `Text` and `BitmapText`, web fonts, panels. |\n| [melonjs-particles-and-trails](../melonjs-particles-and-trails/SKILL.md) | `ParticleEmitter`, burst versus stream, `referenceSpace`, and the `Trail` renderable. |\n| [melonjs-lighting](../melonjs-lighting/SKILL.md) | 2D dynamic lighting — `Light2d`, stage ambient, normal-mapped sprites. |\n| [melonjs-deployment](../melonjs-deployment/SKILL.md) | Scaffolding a project, building, browser support and transpiling, native and mini-game platforms. |\n| [melonjs-loading-assets](../melonjs-loading-assets/SKILL.md) | Resource descriptors, the asset type list, base URLs, cross-origin, progress events, unloading. |\n| [melonjs-renderer-backends](../melonjs-renderer-backends/SKILL.md) | A feature works on one machine but not another; choosing or pinning a backend; degrading gracefully. |\n| [melonjs-display-and-scaling](../melonjs-display-and-scaling/SKILL.md) | Blurry, soft or cropped output; the design resolution, how the canvas fills its page, and pixel-perfect integer scaling. |\n| [melonjs-events](../melonjs-events/SKILL.md) | The event bus, per-frame and lifecycle events, and subscribing without leaking. |\n| [melonjs-performance](../melonjs-performance/SKILL.md) | Dropped frames, draw-call counts, pooling, batching, culling, baking, profiling. |\n| [melonjs-plugins](../melonjs-plugins/SKILL.md) | Registering the debug, Spine or Tiled-inflate plugins, physics adapters, or writing your own plugin. |\n\n\n## Escape hatch: the full API index\n\nThese skills are hand-written and deliberately partial — they cover the paths\npeople actually take and the mistakes those paths invite. When the task needs\nsomething they do not mention, do **not** infer an API from the shape of the\nengine. Fetch the generated index instead:\n\n<https://melonjs.github.io/melonJS/llms.txt>\n\nIt is regenerated on every docs build from the same TSDoc comments the reference\npages render, so it is always current for `master`. It lists every exported\nclass, function, interface, type and namespace with a one-line summary and a\nlink to its page — fetch the pages you need from there.\n\nTwo things it gives you that this collection cannot:\n\n- **Members.** The skills name classes; `llms.txt` links to the page listing\n  every method, property, default and `@since` tag on them.\n- **Deprecations.** Entries marked `**deprecated**` are exactly the APIs that a\n  model trained on older melonJS material will reach for. If something you were\n  about to write appears with that marker, follow the page for the replacement.\n\nIf a class, function, namespace or type is absent from `llms.txt`, it is absent\nfrom the engine's public API — that is the useful negative answer, and it is\nworth acting on rather than writing code around something that does not exist.\nThe index lists top-level exports only, so a *member* missing from it proves\nnothing: follow the owning class's page to check that.\n\n## The three rules that catch everyone\n\nWhatever the task, these produce code that runs and is wrong:\n\n1. **`await app.init()` is mandatory** since 20.0. The constructor builds the\n   world but not the renderer: a plain renderable can still be added and simply\n   never draws, while `new Sprite(...)` / `new Text(...)` throw a `TypeError`\n   on the global `game`, which is unset until the first `init()` resolves.\n   Neither symptom names the missing call.\n2. **`addChild(child, z)` is the only way to set draw order.** `renderable.z`\n   does not exist; setting `depth` before `addChild` is overwritten, and setting\n   it after does not resort (except under `Camera3d`).\n3. **`isKinematic` defaults to `true`**, which silently excludes a renderable\n   from pointer events and from the physics broadphase. Plain `Renderable` and\n   `Sprite` subclasses need `this.isKinematic = false`.\n\n## Companion packages\n\nmelonJS ships optional packages alongside the engine. Reach for these rather\nthan reimplementing what they cover — and note the physics adapters and the\nTiled inflate plugin are *required* for their use cases, not merely convenient.\n\n| Package | Use when |\n|---|---|\n| [`@melonjs/planck-adapter`](https://www.npmjs.com/package/@melonjs/planck-adapter) | You want full rigid-body dynamics — stacking, joints, realistic restitution. Pass it as the Application's `physic` setting; the portable `raycast` and `queryAABB` calls work unchanged across adapters (`querySphere` and `raycast3d` are built-in-adapter only). |\n| [`@melonjs/matter-adapter`](https://www.npmjs.com/package/@melonjs/matter-adapter) | Same, backed by matter-js. Choose one adapter per game. |\n| [`@melonjs/tiled-inflate-plugin`](https://www.npmjs.com/package/@melonjs/tiled-inflate-plugin) | **Required** to load gzip-, zlib- or zstd-compressed Tiled maps. Without it, parsing such a map throws `No inflate function set`. |\n| [`@melonjs/spine-plugin`](https://www.npmjs.com/package/@melonjs/spine-plugin) | Skeletal animation authored in Spine, as a melonJS renderable. |\n| [`@melonjs/debug-plugin`](https://www.npmjs.com/package/@melonjs/debug-plugin) | An in-game debug panel — FPS, draw counts, collision-shape and bounds overlays. Register it after `app.init()`. |\n| [`@melonjs/capacitor-plugin`](https://www.npmjs.com/package/@melonjs/capacitor-plugin) | Wrapping a game as a native iOS/Android app: it bridges Capacitor's lifecycle — pause/resume, the hardware back button, orientation lock, splash screen. |\n| [`create-melonjs`](https://www.npmjs.com/package/create-melonjs) | Scaffolding a new project — start here rather than assembling a build by hand. |\n\nPlugins are registered **after** `init()`, into a global registry:\n\n```js\nimport { plugin } from \"melonjs\";\nimport { DebugPanelPlugin } from \"@melonjs/debug-plugin\";\n\nawait app.init();\nplugin.register(DebugPanelPlugin, \"debugPanel\");\n```\n\nPhysics adapters are different — they are an `Application` setting, not a\nplugin:\n\n```js\nimport { PlanckAdapter } from \"@melonjs/planck-adapter\";\n\nconst app = new Application(800, 600, {\n    parent: \"screen\",\n    // gravity is in pixels/s²; pixelsPerMeter converts it to the adapter's\n    // metres. Both are shown at their defaults; subSteps defaults to 1\n    physic: new PlanckAdapter({\n        gravity: { x: 0, y: 320 },\n        pixelsPerMeter: 32,\n        subSteps: 2,\n    }),\n});\nawait app.init();\n```\n\n`melonjs-plugins` covers the registry, the version gate and writing your own.\n\n## Prefer built-in features\n\nmelonJS covers a lot that is easy to reimplement badly. Before hand-rolling,\ncheck for: `ParticleEmitter` and `Trail`, `Tween` with easing, the built-in\n`ShaderEffect` presets (vignette, scanline, chromatic aberration, glow, dissolve\nand more), `Light2d` / `Light3d`, `UIBaseElement` / `UISpriteElement` /\n`UITextButton`, `NineSliceSprite`, `TextureAtlas` (TexturePacker, ShoeBox,\nAseprite), `BitmapText`, `CanvasRenderTarget` for bake-once drawing,\n`NoiseTexture2d`, `viewport.shake` / `fadeIn` / `fadeOut`, and `save` for\nlocalStorage-backed persistence.\n\n<!-- File: .cursorrules (Tokens: ~300 | Category: cursor-rule) -->\n# Cursor IDE Native Rules\n\nPath: `.cursorrules`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/.cursorrules)\n\n<!-- File: CLAUDE.md (Tokens: ~300 | Category: claude-rule) -->\n# Claude Agent Guidelines & System Prompt\n\nPath: `CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/CLAUDE.md)\n\n<!-- File: GEMINI.md (Tokens: ~300 | Category: gemini-rule) -->\n# Gemini Agent & Antigravity Instructions\n\nPath: `GEMINI.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/melonjs/melonJS/HEAD/GEMINI.md)\n\n<!-- ... +35 more modular skills available via /api/skills/melonjs/melonJS?path={path} -->\n</yakaai_skills>"}