melonjs 20.7.0 → 20.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +7 -3
  2. package/build/application/application.d.ts +66 -8
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/defaultApplicationSettings.d.ts +6 -0
  5. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  6. package/build/application/header.d.ts.map +1 -1
  7. package/build/application/resize.d.ts +25 -0
  8. package/build/application/resize.d.ts.map +1 -1
  9. package/build/application/scaleMethods.d.ts +13 -3
  10. package/build/application/scaleMethods.d.ts.map +1 -1
  11. package/build/application/settings.d.ts +144 -5
  12. package/build/application/settings.d.ts.map +1 -1
  13. package/build/audio/audio.d.ts +29 -5
  14. package/build/audio/audio.d.ts.map +1 -1
  15. package/build/audio/autopause.d.ts +36 -0
  16. package/build/audio/autopause.d.ts.map +1 -0
  17. package/build/audio/backend/engine.d.ts +10 -1
  18. package/build/audio/backend/engine.d.ts.map +1 -1
  19. package/build/audio/backend/sound.d.ts +0 -1
  20. package/build/audio/backend/sound.d.ts.map +1 -1
  21. package/build/audio/backend/spatial.d.ts.map +1 -1
  22. package/build/audio/context.d.ts +35 -0
  23. package/build/audio/context.d.ts.map +1 -0
  24. package/build/audio/playback.d.ts.map +1 -1
  25. package/build/audio/procedural.d.ts.map +1 -1
  26. package/build/audio/spatial.d.ts +87 -0
  27. package/build/audio/spatial.d.ts.map +1 -0
  28. package/build/audio/state.d.ts +1 -0
  29. package/build/audio/state.d.ts.map +1 -1
  30. package/build/audio/types.d.ts +46 -6
  31. package/build/audio/types.d.ts.map +1 -1
  32. package/build/camera/camera2d.d.ts.map +1 -1
  33. package/build/camera/camera3d.d.ts +95 -8
  34. package/build/camera/camera3d.d.ts.map +1 -1
  35. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  36. package/build/camera/effects/mask_effect.d.ts.map +1 -1
  37. package/build/camera/effects/shake_effect.d.ts.map +1 -1
  38. package/build/geometries/box3d.d.ts +25 -7
  39. package/build/geometries/box3d.d.ts.map +1 -1
  40. package/build/geometries/ellipse.d.ts +15 -1
  41. package/build/geometries/ellipse.d.ts.map +1 -1
  42. package/build/geometries/line.d.ts +1 -1
  43. package/build/geometries/line.d.ts.map +1 -1
  44. package/build/geometries/point.d.ts +1 -1
  45. package/build/geometries/point.d.ts.map +1 -1
  46. package/build/geometries/polygon.d.ts +15 -1
  47. package/build/geometries/polygon.d.ts.map +1 -1
  48. package/build/geometries/rectangle.d.ts +1 -1
  49. package/build/geometries/rectangle.d.ts.map +1 -1
  50. package/build/geometries/roundrect.d.ts +1 -1
  51. package/build/geometries/roundrect.d.ts.map +1 -1
  52. package/build/geometries/sphere.d.ts +36 -7
  53. package/build/geometries/sphere.d.ts.map +1 -1
  54. package/build/index.d.ts +16 -3
  55. package/build/index.d.ts.map +1 -1
  56. package/build/index.js +38172 -33633
  57. package/build/index.js.map +4 -4
  58. package/build/input/gamepad.d.ts +129 -3
  59. package/build/input/gamepad.d.ts.map +1 -1
  60. package/build/input/pointerevent.d.ts +8 -0
  61. package/build/input/pointerevent.d.ts.map +1 -1
  62. package/build/level/gltf/GLTFModel.d.ts +20 -33
  63. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  64. package/build/level/gltf/GLTFScene.d.ts +40 -1
  65. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  66. package/build/level/level.d.ts +35 -0
  67. package/build/level/level.d.ts.map +1 -1
  68. package/build/level/tiled/TMXLayer.d.ts +5 -2
  69. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  70. package/build/level/tiled/TMXObject.d.ts +0 -1
  71. package/build/level/tiled/TMXObjectFactory.d.ts +3 -2
  72. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  73. package/build/level/tiled/TMXTile.d.ts +2 -1
  74. package/build/level/tiled/TMXTile.d.ts.map +1 -1
  75. package/build/level/tiled/TMXTileMap.d.ts +7 -4
  76. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  77. package/build/level/tiled/TMXTileset.d.ts +1 -1
  78. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  79. package/build/level/tiled/TMXTilesetGroup.d.ts +5 -0
  80. package/build/level/tiled/TMXTilesetGroup.d.ts.map +1 -1
  81. package/build/level/tiled/factories/shape.d.ts +0 -1
  82. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts +6 -5
  83. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  84. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts +5 -0
  85. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  86. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +5 -7
  87. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  88. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts +5 -0
  89. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
  90. package/build/level/tiled/renderer/TMXRenderer.d.ts +19 -4
  91. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  92. package/build/level/tiled/renderer/autodetect.d.ts +0 -4
  93. package/build/level/tiled/renderer/autodetect.d.ts.map +1 -1
  94. package/build/loader/cache.d.ts +220 -0
  95. package/build/loader/cache.d.ts.map +1 -1
  96. package/build/loader/loader.d.ts +20 -340
  97. package/build/loader/loader.d.ts.map +1 -1
  98. package/build/loader/loadingscreen.d.ts +1 -2
  99. package/build/loader/loadingscreen.d.ts.map +1 -1
  100. package/build/loader/parsers/gltf.d.ts +115 -0
  101. package/build/loader/parsers/gltf.d.ts.map +1 -1
  102. package/build/loader/parsers/shader.d.ts +0 -2
  103. package/build/math/color.d.ts +28 -5
  104. package/build/math/color.d.ts.map +1 -1
  105. package/build/math/matrix2d.d.ts +1 -1
  106. package/build/math/matrix2d.d.ts.map +1 -1
  107. package/build/math/matrix3d.d.ts +1 -1
  108. package/build/math/matrix3d.d.ts.map +1 -1
  109. package/build/math/observableVector2d.d.ts +14 -2
  110. package/build/math/observableVector2d.d.ts.map +1 -1
  111. package/build/math/observableVector3d.d.ts +15 -1
  112. package/build/math/observableVector3d.d.ts.map +1 -1
  113. package/build/math/vector2d.d.ts +31 -1
  114. package/build/math/vector2d.d.ts.map +1 -1
  115. package/build/math/vector3d.d.ts +38 -1
  116. package/build/math/vector3d.d.ts.map +1 -1
  117. package/build/math/vertex.d.ts.map +1 -1
  118. package/build/particles/emitter.d.ts.map +1 -1
  119. package/build/particles/particle.d.ts +29 -2
  120. package/build/particles/particle.d.ts.map +1 -1
  121. package/build/particles/settings.d.ts +104 -0
  122. package/build/particles/settings.d.ts.map +1 -1
  123. package/build/physics/adapter.d.ts +9 -1
  124. package/build/physics/adapter.d.ts.map +1 -1
  125. package/build/physics/bounds.d.ts +1 -1
  126. package/build/physics/bounds.d.ts.map +1 -1
  127. package/build/physics/broadphase/octree.d.ts +24 -0
  128. package/build/physics/broadphase/octree.d.ts.map +1 -1
  129. package/build/physics/broadphase/quadtree.d.ts +31 -0
  130. package/build/physics/broadphase/quadtree.d.ts.map +1 -1
  131. package/build/physics/builtin/body.d.ts +17 -2
  132. package/build/physics/builtin/body.d.ts.map +1 -1
  133. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  134. package/build/physics/builtin/detector.d.ts +0 -1
  135. package/build/physics/builtin/detector.d.ts.map +1 -1
  136. package/build/physics/builtin/sat.d.ts +0 -7
  137. package/build/physics/builtin/sat3d.d.ts +46 -0
  138. package/build/physics/builtin/sat3d.d.ts.map +1 -1
  139. package/build/physics/response.d.ts +45 -62
  140. package/build/physics/response.d.ts.map +1 -1
  141. package/build/pool.d.ts +4 -0
  142. package/build/pool.d.ts.map +1 -1
  143. package/build/renderable/collectable.d.ts +6 -1
  144. package/build/renderable/collectable.d.ts.map +1 -1
  145. package/build/renderable/colorlayer.d.ts +17 -4
  146. package/build/renderable/colorlayer.d.ts.map +1 -1
  147. package/build/renderable/container.d.ts +23 -17
  148. package/build/renderable/container.d.ts.map +1 -1
  149. package/build/renderable/entity/entity.d.ts +12 -16
  150. package/build/renderable/entity/entity.d.ts.map +1 -1
  151. package/build/renderable/frameAnimation.d.ts +1 -1
  152. package/build/renderable/groundshadow.d.ts +1 -0
  153. package/build/renderable/groundshadow.d.ts.map +1 -1
  154. package/build/renderable/imagelayer.d.ts +12 -5
  155. package/build/renderable/imagelayer.d.ts.map +1 -1
  156. package/build/renderable/instanced_mesh.d.ts +56 -7
  157. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  158. package/build/renderable/mesh.d.ts +369 -4
  159. package/build/renderable/mesh.d.ts.map +1 -1
  160. package/build/renderable/nineslicesprite.d.ts +3 -13
  161. package/build/renderable/nineslicesprite.d.ts.map +1 -1
  162. package/build/renderable/renderable.d.ts +129 -28
  163. package/build/renderable/renderable.d.ts.map +1 -1
  164. package/build/renderable/sprite.d.ts +7 -20
  165. package/build/renderable/sprite.d.ts.map +1 -1
  166. package/build/renderable/sprite3d.d.ts +31 -16
  167. package/build/renderable/sprite3d.d.ts.map +1 -1
  168. package/build/renderable/text/bitmaptext.d.ts +76 -5
  169. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  170. package/build/renderable/text/bitmaptextdata.d.ts +1 -5
  171. package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
  172. package/build/renderable/text/glypheffect.d.ts +44 -0
  173. package/build/renderable/text/glypheffect.d.ts.map +1 -0
  174. package/build/renderable/text/text.d.ts +22 -3
  175. package/build/renderable/text/text.d.ts.map +1 -1
  176. package/build/renderable/text/textmetrics.d.ts +2 -0
  177. package/build/renderable/text/textmetrics.d.ts.map +1 -1
  178. package/build/renderable/trail.d.ts +0 -1
  179. package/build/renderable/trail.d.ts.map +1 -1
  180. package/build/renderable/trail3d.d.ts +174 -0
  181. package/build/renderable/trail3d.d.ts.map +1 -0
  182. package/build/renderable/trigger.d.ts +28 -5
  183. package/build/renderable/trigger.d.ts.map +1 -1
  184. package/build/renderable/ui/panelpaint.d.ts +5 -0
  185. package/build/renderable/ui/panelpaint.d.ts.map +1 -0
  186. package/build/renderable/ui/progressbar.d.ts +192 -0
  187. package/build/renderable/ui/progressbar.d.ts.map +1 -0
  188. package/build/renderable/ui/uibaseelement.d.ts +48 -3
  189. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  190. package/build/renderable/ui/uipanel.d.ts +245 -0
  191. package/build/renderable/ui/uipanel.d.ts.map +1 -0
  192. package/build/renderable/ui/uispriteelement.d.ts +19 -3
  193. package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
  194. package/build/renderable/ui/uitextbutton.d.ts +30 -1
  195. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  196. package/build/state/state.d.ts.map +1 -1
  197. package/build/system/bootstrap.d.ts.map +1 -1
  198. package/build/system/legacy_pool.d.ts +24 -4
  199. package/build/system/legacy_pool.d.ts.map +1 -1
  200. package/build/system/pool.d.ts +15 -8
  201. package/build/system/pool.d.ts.map +1 -1
  202. package/build/tweens/tween.d.ts +50 -3
  203. package/build/tweens/tween.d.ts.map +1 -1
  204. package/build/video/canvas/canvas_renderer.d.ts +34 -23
  205. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  206. package/build/video/effects/blendEffect.d.ts +0 -1
  207. package/build/video/effects/bloom.d.ts +72 -0
  208. package/build/video/effects/bloom.d.ts.map +1 -0
  209. package/build/video/effects/blur.d.ts +3 -2
  210. package/build/video/effects/blur.d.ts.map +1 -1
  211. package/build/video/effects/chromaticAberration.d.ts +3 -2
  212. package/build/video/effects/chromaticAberration.d.ts.map +1 -1
  213. package/build/video/effects/colorMatrix.d.ts +3 -2
  214. package/build/video/effects/colorMatrix.d.ts.map +1 -1
  215. package/build/video/effects/desaturate.d.ts +10 -2
  216. package/build/video/effects/desaturate.d.ts.map +1 -1
  217. package/build/video/effects/dissolve.d.ts +3 -2
  218. package/build/video/effects/dissolve.d.ts.map +1 -1
  219. package/build/video/effects/dropShadow.d.ts +3 -2
  220. package/build/video/effects/dropShadow.d.ts.map +1 -1
  221. package/build/video/effects/flash.d.ts +3 -2
  222. package/build/video/effects/flash.d.ts.map +1 -1
  223. package/build/video/effects/glow.d.ts +3 -2
  224. package/build/video/effects/glow.d.ts.map +1 -1
  225. package/build/video/effects/hologram.d.ts +3 -2
  226. package/build/video/effects/hologram.d.ts.map +1 -1
  227. package/build/video/effects/invert.d.ts +10 -2
  228. package/build/video/effects/invert.d.ts.map +1 -1
  229. package/build/video/effects/outline.d.ts +3 -2
  230. package/build/video/effects/outline.d.ts.map +1 -1
  231. package/build/video/effects/pixelate.d.ts +3 -2
  232. package/build/video/effects/pixelate.d.ts.map +1 -1
  233. package/build/video/effects/radialGradient.d.ts +8 -3
  234. package/build/video/effects/radialGradient.d.ts.map +1 -1
  235. package/build/video/effects/scanline.d.ts +3 -2
  236. package/build/video/effects/scanline.d.ts.map +1 -1
  237. package/build/video/effects/sepia.d.ts +10 -2
  238. package/build/video/effects/sepia.d.ts.map +1 -1
  239. package/build/video/effects/shadereffect.d.ts +10 -4
  240. package/build/video/effects/shadereffect.d.ts.map +1 -1
  241. package/build/video/effects/shine.d.ts +3 -2
  242. package/build/video/effects/shine.d.ts.map +1 -1
  243. package/build/video/effects/tintPulse.d.ts +3 -7
  244. package/build/video/effects/tintPulse.d.ts.map +1 -1
  245. package/build/video/effects/tonemap.d.ts +121 -0
  246. package/build/video/effects/tonemap.d.ts.map +1 -0
  247. package/build/video/effects/vignette.d.ts +3 -2
  248. package/build/video/effects/vignette.d.ts.map +1 -1
  249. package/build/video/effects/wave.d.ts +3 -7
  250. package/build/video/effects/wave.d.ts.map +1 -1
  251. package/build/video/gpu/batcher.d.ts +8 -2
  252. package/build/video/gpu/batcher.d.ts.map +1 -1
  253. package/build/video/gpu/vertexformat.d.ts +26 -0
  254. package/build/video/gpu/vertexformat.d.ts.map +1 -1
  255. package/build/video/gradient.d.ts +1 -1
  256. package/build/video/gradient.d.ts.map +1 -1
  257. package/build/video/renderer.d.ts +111 -7
  258. package/build/video/renderer.d.ts.map +1 -1
  259. package/build/video/renderstate.d.ts +2 -0
  260. package/build/video/renderstate.d.ts.map +1 -1
  261. package/build/video/rendertarget/canvasrendertarget.d.ts +3 -4
  262. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  263. package/build/video/rendertarget/render_target_pool.d.ts +0 -1
  264. package/build/video/rendertarget/rendertarget.d.ts +12 -5
  265. package/build/video/rendertarget/rendertarget.d.ts.map +1 -1
  266. package/build/video/rendertarget/webglrendertarget.d.ts +0 -1
  267. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
  268. package/build/video/rendertarget/webgpurendertarget.d.ts +24 -14
  269. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  270. package/build/video/texture/atlas.d.ts +2 -1
  271. package/build/video/texture/atlas.d.ts.map +1 -1
  272. package/build/video/texture/cache.d.ts +1 -3
  273. package/build/video/texture/frametexture.d.ts +0 -1
  274. package/build/video/texture/frametexture.d.ts.map +1 -1
  275. package/build/video/texture/noise_texture2d.d.ts +1 -0
  276. package/build/video/texture/noise_texture2d.d.ts.map +1 -1
  277. package/build/video/texture/texture2d.d.ts +6 -0
  278. package/build/video/texture/texture2d.d.ts.map +1 -1
  279. package/build/video/utils/autodetect.d.ts +0 -2
  280. package/build/video/webgl/batchers/batcher.d.ts +0 -9
  281. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  282. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -0
  283. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  284. package/build/video/webgl/batchers/material_batcher.d.ts +4 -2
  285. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  286. package/build/video/webgl/batchers/mesh_batcher.d.ts +0 -3
  287. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  288. package/build/video/webgl/batchers/quad_batcher.d.ts +4 -0
  289. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  290. package/build/video/webgl/buffer/index.d.ts +0 -1
  291. package/build/video/webgl/buffer/retained_geometry.d.ts +0 -1
  292. package/build/video/webgl/glshader.d.ts +5 -3
  293. package/build/video/webgl/glshader.d.ts.map +1 -1
  294. package/build/video/webgl/renderers/tmxlayer/orthogonal.d.ts +0 -4
  295. package/build/video/webgl/texture/store.d.ts +0 -1
  296. package/build/video/webgl/webgl_renderer.d.ts +63 -24
  297. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  298. package/build/video/webgpu/batchers/mesh_batcher.d.ts +0 -3
  299. package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
  300. package/build/video/webgpu/batchers/quad_batcher.d.ts +0 -2
  301. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  302. package/build/video/webgpu/batchers/webgpu_batcher.d.ts +0 -2
  303. package/build/video/webgpu/batchers/webgpu_batcher.d.ts.map +1 -1
  304. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  305. package/build/video/webgpu/renderers/tmxlayer/orthogonal.d.ts +0 -2
  306. package/build/video/webgpu/texture/frametexture.d.ts +0 -2
  307. package/build/video/webgpu/texture/frametexture.d.ts.map +1 -1
  308. package/build/video/webgpu/texture/store.d.ts +0 -1
  309. package/build/video/webgpu/webgpu_renderer.d.ts +7 -75
  310. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  311. package/package.json +2 -2
  312. package/skills/AGENTS.md +1 -1
  313. package/skills/melonjs/SKILL.md +2 -0
  314. package/skills/melonjs-3d/SKILL.md +454 -22
  315. package/skills/melonjs-3d-assets/SKILL.md +111 -10
  316. package/skills/melonjs-audio/SKILL.md +229 -8
  317. package/skills/melonjs-camera-and-drawing/SKILL.md +127 -1
  318. package/skills/melonjs-display-and-scaling/SKILL.md +273 -0
  319. package/skills/melonjs-effects-and-shaders/SKILL.md +341 -29
  320. package/skills/melonjs-events/SKILL.md +8 -6
  321. package/skills/melonjs-input/SKILL.md +147 -7
  322. package/skills/melonjs-math/SKILL.md +313 -0
  323. package/skills/melonjs-particles-and-trails/SKILL.md +189 -13
  324. package/skills/melonjs-performance/SKILL.md +131 -23
  325. package/skills/melonjs-physics/SKILL.md +103 -28
  326. package/skills/melonjs-renderables/SKILL.md +8 -2
  327. package/skills/melonjs-scenes-and-state/SKILL.md +134 -3
  328. package/skills/melonjs-sprites-and-animation/SKILL.md +9 -4
  329. package/skills/melonjs-tilemaps/SKILL.md +40 -12
  330. package/skills/melonjs-ui-and-text/SKILL.md +546 -7
  331. package/build/level/gltf/srgb.d.ts +0 -20
  332. package/build/level/gltf/srgb.d.ts.map +0 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-particles-and-trails
3
- description: "Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: ParticleEmitter, particles, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, Trail, explosion, smoke, sparks, exhaust, emitter."
3
+ description: "Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, 3D particle motion through elevation and minSpread/maxSpread in 20.8, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: Trail3d, ribbon, exhaust trail, speed streak, ski track, tyre mark, ribbonNormal, ParticleEmitter, particles, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, elevation, minSpread, maxSpread, spawn area, Trail, explosion, smoke, sparks, debris, ring, exhaust, emitter."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -28,6 +28,142 @@ emitter.burstParticles(); // one shot
28
28
  `duration` setting, which defaults to `Infinity` — a stream runs until
29
29
  `stopStream()`. `burstParticles(n)` launches `n`, or `totalParticles`.
30
30
 
31
+ ## Sprite-sheet particles
32
+
33
+ Use `framewidth` and `frameheight` (the same names as on `Sprite`) to divide
34
+ `image` into a regular grid. Each omitted dimension defaults to that of the
35
+ image. Frames are numbered from zero left-to-right, then top-to-bottom; only
36
+ whole cells count, so partial cells along the right/bottom edges are ignored.
37
+ Margins and spacing are not supported.
38
+
39
+ ```js
40
+ const explosion = new ParticleEmitter(x, y, {
41
+ image: loader.getImage("explosion-sheet"),
42
+ framewidth: 32, frameheight: 32,
43
+ frames: [0, 1, 2, 3],
44
+ minLife: 400, maxLife: 800,
45
+ });
46
+ world.addChild(explosion);
47
+ explosion.burstParticles();
48
+ ```
49
+
50
+ Omit `frames` to use every cell. Each particle traverses the sequence once
51
+ over its own lifetime, with equal time for each entry; repeated indices hold
52
+ a frame longer. Existing scale, opacity and rotation still apply. Bounds and
53
+ the rotation pivot use one frame's dimensions, rather than the whole sheet.
54
+
55
+ Set `randomFrame: true` for debris: a particle chooses one entry at birth and
56
+ keeps it until it dies. Different frames share the same image and GPU batch.
57
+ Without sheet settings, particles still draw the whole image as before, and
58
+ that includes `randomFrame` on its own: it needs a grid to choose from, so
59
+ pair it with `framewidth`/`frameheight` or it does nothing.
60
+
61
+ Note the variety is in SHAPE, not colour. `tint` belongs to the emitter, so
62
+ every particle from one emitter is tinted alike however many frames it picks
63
+ between.
64
+
65
+ Frame dimensions must be positive integers that fit inside the image, and an
66
+ explicit `frames` sequence must be nonempty and contain valid integer indices.
67
+ Invalid configurations throw `RangeError` before changing an emitter during
68
+ `reset()`. The frame sequence is copied on construction/reset; resetting changes
69
+ only future particles, so ones already alive finish with their original image,
70
+ frame size and sequence.
71
+
72
+ ## Where particles are born
73
+
74
+ `width` and `height` are the spawn AREA, and both default to `1` — so by
75
+ default every particle launches from the same pixel. That is fine for a
76
+ fountain and wrong for an explosion: additive sprites piled on one point clip
77
+ all three channels there however well they are tinted, so a burst reads as one
78
+ bright flash rather than as debris.
79
+
80
+ Give it the size of whatever broke. The area is measured from the emitter's own
81
+ position, NOT centred on it, so offset by half or the whole burst sits down and
82
+ to the right of its source:
83
+
84
+ ```js
85
+ const area = radius * 2;
86
+ const emitter = new ParticleEmitter(x - area / 2, y - area / 2, {
87
+ width: area, height: area,
88
+ });
89
+ ```
90
+
91
+ ## Particles in 3D
92
+
93
+ By default a particle keeps the depth it was born at for its whole life: it
94
+ moves across the emitter's plane and never toward or away from the camera.
95
+ Under a `Camera3d` that reads as a flat sticker anywhere but dead ahead.
96
+
97
+ `elevation` lifts the launch out of that plane and `elevationVariation` spreads
98
+ it. `angle` is the azimuth within the plane, as it always was:
99
+
100
+ ```js
101
+ const emitter = new ParticleEmitter(x, y, {
102
+ angle: 0, angleVariation: Math.PI * 2,
103
+ elevation: 0, elevationVariation: Math.PI / 2, // a sphere of debris
104
+ speed: 9,
105
+ });
106
+ world.addChild(emitter, z); // the depth slice the burst belongs to
107
+ emitter.burstParticles();
108
+ ```
109
+
110
+ `speed` is the length of the whole 3D vector, so an "all directions" burst
111
+ covers the sphere evenly instead of bunching toward the poles.
112
+
113
+ Both default to `0`, and that is a gate rather than the 3D path with zeros in
114
+ it: an emitter that asks for no elevation runs the same two trig calls it
115
+ always did, writes no depth and starts no sort.
116
+
117
+ **The launch is in WORLD axes.** `vel.x` and `vel.y` add to the particle's
118
+ `pos.x`/`pos.y` and `velZ` adds to its depth, so `angle` and `elevation`
119
+ describe a direction in the world, not on the screen. Building them from a
120
+ camera basis works only while the camera's axes ARE the world axes, which stops
121
+ being true the moment you pose a `Camera3d`. For a direction `d` already in
122
+ world terms the pair is `angle: Math.atan2(-d.y, d.x)`,
123
+ `elevation: Math.asin(d.z)`.
124
+
125
+ ### Cones, rings and spheres: aiming around an axis
126
+
127
+ `angleVariation` and `elevationVariation` are sampled INDEPENDENTLY, spreading
128
+ particles over a rectangle of azimuth by elevation. That cannot describe a
129
+ shape defined against a direction. A ring of debris leaving a point on a sphere
130
+ is the set of directions at ninety degrees to that point's normal, and on a
131
+ rectangle the elevation that satisfies it is a function of the azimuth, so no
132
+ pair of variations gets there.
133
+
134
+ Set `maxSpread` and `angle`/`elevation` become an AXIS instead. Each particle
135
+ leaves at a polar angle between `minSpread` and `maxSpread` off that axis,
136
+ around a uniformly random azimuth:
137
+
138
+ | `minSpread` … `maxSpread` | shape |
139
+ |---|---|
140
+ | `0` … `0.5` | a tight cone along the axis |
141
+ | `0` … `Math.PI` | the whole sphere |
142
+ | either value at `Math.PI / 2` | a flat disc perpendicular to the axis |
143
+
144
+ An explosion on a planet's surface is a ring tangent to it: set the axis to the
145
+ surface normal and use `minSpread: Math.PI / 2 - 0.3, maxSpread: Math.PI / 2`.
146
+ Keeping the band at or inside ninety degrees is what stops debris tilting back
147
+ into the body it came off. `angleVariation` and `elevationVariation` are unused
148
+ while `maxSpread` is set.
149
+
150
+ ### Sorting, and why it is keyed off the blend mode
151
+
152
+ A 3D burst only needs depth sorting when its blend is order dependent, and the
153
+ emitter decides that for you. Additive blending is commutative, so an additive
154
+ burst looks the same however it is ordered and pays nothing; `"normal"` is not,
155
+ so a 3D burst drawn with it is sorted back to front.
156
+
157
+ The two differ in more than cost. Additive sprites overlapping near the centre
158
+ of a burst sum to white however they are tinted, so debris that has to keep its
159
+ colour wants `textureAdditive: false` and accepts the sort. A common shape is
160
+ two emitters: a solid, sorted body of debris with a sparser additive spray of
161
+ sparks over it.
162
+
163
+ The default particle sprite is a filled SQUARE, which additive blending hides
164
+ and normal blending does not. Pass your own soft `image` for non-additive
165
+ debris.
166
+
31
167
  For one-shot effects, `autoDestroyOnComplete: true` removes the emitter once its
32
168
  particles have finished. The `onComplete` callback fires at that same moment
33
169
  either way.
@@ -89,18 +225,48 @@ Omit `target` and feed it yourself with `trail.addPoint(x, y)` — the mode for
89
225
  sword slash, where the ribbon follows a weapon tip rather than an object. Points
90
226
  closer together than `minDistance` (default 4 px) are dropped.
91
227
 
92
- **`Trail` is 2D.** `addPoint` takes `(x, y)`, points are stored without a `z`,
93
- and the ribbon is drawn as connected screen-space quads. Under a `Camera3d` it
94
- draws a flat ribbon that does not recede with perspective — so it is the wrong
95
- tool for a trail behind an object moving *into* the scene. For that:
228
+ **`Trail` is 2D. Use `Trail3d` in a 3D scene** (20.9.0 and later). `Trail`
229
+ stores points without a `z` and draws connected screen-space quads, which carry
230
+ one depth for the whole renderable, so under a `Camera3d` the ribbon stays flat
231
+ and does not narrow along its own length.
232
+
233
+ ```js
234
+ const exhaust = new Trail3d(0, 0, {
235
+ target: ship, // samples pos.x / pos.y / pos.z
236
+ width: 6, // world units, and free to change per frame
237
+ lifetime: 260,
238
+ gradient: ["#eaffff", "#2ea8ff", "#2ea8ff00"],
239
+ });
240
+ exhaust.blendMode = "additive";
241
+ app.world.addChild(exhaust); // no z: it derives its own from its points
242
+ ```
243
+
244
+ `Trail3d` is a `Mesh`, which is most of the difference:
245
+
246
+ - `billboard` is spelled and valued exactly as on `Sprite3d` —
247
+ `"spherical"` (the default here) faces the camera on all axes,
248
+ `true` / `"cylindrical"` faces it but stays upright, `false` uses the
249
+ `ribbonNormal` you give it. `billboard: false, ribbonNormal: [0, -1, 0]`
250
+ lays the ribbon in the ground plane for a tyre mark or a wake.
251
+ - **`tint` works**, and is the per-frame colour lever. On `Trail` it does
252
+ nothing at all, because the primitive batcher reads the draw colour and never
253
+ the tint. `setColor()` / `setGradient()` rebuild the gradient for a change of
254
+ state; they allocate, so they are not per-frame.
255
+
256
+ **If the colour is going to change, author the gradient WHITE.** `tint`
257
+ multiplies, so tinting a cyan ribbon red gives muddy brown rather than red. A
258
+ neutral gradient hands the hue entirely to the tint, which is what lets one
259
+ lever take a trail from white through to red as something overheats.
260
+ - A post effect reaches it properly, where `Trail` has to force a screen
261
+ capture.
262
+ - It is **cheaper**: one indexed draw call, against one filled path per segment.
263
+ - `length` is fixed at construction, because the geometry is sized once.
264
+ Assigning `maxPoints` throws rather than silently truncating the ribbon, and
265
+ a `length` that is not a finite number throws at construction.
266
+ - It needs a `Camera3d`. Under a 2D camera it draws nothing and warns once.
96
267
 
97
- - **`ParticleEmitter` with `referenceSpace: "world"`** — particles stay where
98
- they were dropped while the emitter moves on, and the emitter propagates its
99
- depth to each particle so `Camera3d` projects them correctly. This is the
100
- built-in answer for exhaust, ski tracks and speed trails in 3D.
101
- - **A procedural `Mesh`** built from the object's recent path, with
102
- `mesh.vertexColors` for a per-vertex gradient — more work, but a continuous
103
- ribbon rather than discrete puffs.
268
+ A world-space `ParticleEmitter` (`referenceSpace: "world"`) is still the right
269
+ answer when you want discrete puffs rather than a continuous ribbon.
104
270
 
105
271
  ## Performance
106
272
 
@@ -123,12 +289,22 @@ tool for a trail behind an object moving *into* the scene. For that:
123
289
 
124
290
  | symptom | cause |
125
291
  |---|---|
126
- | trail looks flat under a `Camera3d` | `Trail` is 2D — use a world-space emitter or a ribbon mesh |
292
+ | trail looks flat under a `Camera3d` | `Trail` is 2D; use `Trail3d` |
293
+ | `Trail3d` draws nothing, with a console warning | it needs a `Camera3d`; the app's `cameraClass` is still the 2D default |
294
+ | `trail.tint` does nothing | `Trail` cannot be tinted at all; `Trail3d` can |
295
+ | assigning `maxPoints` throws | `Trail3d` sizes its geometry once; pass `length` to the constructor |
296
+ | `Trail3d: \`length\` must be a finite number` | a computed `length` came out `NaN`; it is refused rather than silently rendering nothing |
127
297
  | emitter added but nothing appears | `streamParticles()` / `burstParticles()` never called |
128
298
  | the cloud follows a moving emitter | default `referenceSpace: "local"` — use `"world"` for trails |
129
299
  | particles do not travel far enough | settings tuned pre-20.2, when drawn travel was doubled |
130
300
  | one-shot emitters accumulate | missing `autoDestroyOnComplete: true` |
131
301
  | frame rate drops with many effects | an emitter per event instead of one re-aimed emitter |
302
+ | a 3D burst is invisible under a `Camera3d` | before 20.8 a particle was culled at twice its depth; update |
303
+ | the whole burst launches from one pixel | `width`/`height` are the spawn area and default to `1` |
304
+ | the burst sits down-right of its source | the spawn area starts at the emitter's position; offset by half |
305
+ | the middle of a burst goes white | additive sprites overlapping — `textureAdditive: false`, or spread them |
306
+ | debris chunks look like squares | the default sprite is a filled square; pass your own `image` |
307
+ | a ring tilts with the camera instead of staying on a surface | the launch axis is in WORLD axes, not the camera basis |
132
308
 
133
309
  ## Related skills
134
310
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-performance
3
- description: "Use this skill when a melonJS game drops frames, allocates heavily, or needs to scale to many objects. Covers object pooling, draw-call batching, atlases, baking with CanvasRenderTarget, instancing, culling and alwaysUpdate, the debug plugin, and where the real costs are. Triggers on: performance, slow, frame rate, fps, lag, pool, pool.register, pool.pull, batching, draw calls, InstancedMesh, culling, alwaysUpdate, inViewport, CanvasRenderTarget, optimise, profiling, debug plugin, memory."
3
+ description: "Use this skill when a melonJS game drops frames, allocates heavily, or needs to scale to many objects. Covers object pooling, draw-call batching, atlases, baking with CanvasRenderTarget, instancing, culling and alwaysUpdate, the debug plugin, and where the real costs are. Triggers on: performance, slow, frame rate, fps, lag, pool, pool.register, pool.pull, batching, draw calls, InstancedMesh, culling, alwaysUpdate, inViewport, CanvasRenderTarget, optimise, profiling, debug plugin, memory, render targets, hdr."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -59,16 +59,28 @@ Anything spawned frequently — bullets, particles, pickups, damage numbers —
59
59
  should be pooled rather than allocated:
60
60
 
61
61
  ```js
62
- pool.register("bullet", Bullet, true); // third arg = enable recycling
63
- const b = pool.pull("bullet", x, y); // recycled: onResetEvent(x, y)
64
- // fresh: new Bullet(x, y)
65
- world.removeChild(b); // returns to the pool automatically
62
+ import { createPool } from "melonjs";
63
+
64
+ const bulletPool = createPool((x, y) => {
65
+ const instance = new Bullet(x, y);
66
+ return { instance, reset: (x, y) => instance.onResetEvent(x, y) };
67
+ });
68
+
69
+ const b = bulletPool.get(x, y); // recycled: reset(x, y)
70
+ // fresh: new Bullet(x, y)
71
+ world.removeChild(b); // returns to bulletPool automatically
66
72
  ```
67
73
 
68
- `register` also publishes the class as a Tiled object factory under that name,
69
- so an object with a matching class or name in a `.tmx` map instantiates it. Set
70
- `pool.autoRegisterTiled = false` around the call for classes that should stay
71
- programmatic.
74
+ The container returns a child to whichever pool built it, so removal is enough;
75
+ you only call `release()` yourself for something that was never added to the
76
+ world.
77
+
78
+ **`me.pool` is the older, string-keyed version and is deprecated since
79
+ 18.0.0.** `pool.register(name, Class, true)` / `pool.pull(name)` still work and
80
+ print a one-off console notice, but a recycled object is no longer returned on
81
+ removal, so pooling through it now costs an allocation per spawn. To register a
82
+ class so a Tiled map can name it, use `registerTiledObjectClass` — that is a
83
+ separate registry and is not deprecated.
72
84
 
73
85
  Two rules that cause subtle bugs when missed:
74
86
 
@@ -76,32 +88,89 @@ Two rules that cause subtle bugs when missed:
76
88
  alpha, tint, scale, animation, velocity. Anything you forget carries into the
77
89
  next use, which looks like a random visual glitch.
78
90
  - **Pooled objects do not fire `onDestroyEvent` on removal.** Removal always
79
- calls `onDeactivateEvent`, then tries `pool.push`; only when that *fails* does
80
- it fall through to `destroy()`, which is what calls `onDestroyEvent`. So a
81
- successfully recycled object never sees it. Pair event subscriptions with
82
- `onActivateEvent` / `onDeactivateEvent` instead, or you leak handlers.
83
-
84
- Engine classes are poolable too: `pool.pull("Tween", target)`. The canonical
85
- names are unprefixed — `Entity`, `Collectable`, `Trigger`, `Light2d`,
86
- `Particle`, `Sprite`, `NineSliceSprite`, `Renderable`, `Text`, `BitmapText`,
87
- `ImageLayer`, `Tween`, `ColorLayer`.
88
-
89
- `pool.register` additionally aliases every name under an `me.` prefix, pointing
90
- at the same entry, so `pool.pull("me.Tween")` resolves identically — and the
91
- same alias is registered with the Tiled object factory, which is why a map
91
+ calls `onDeactivateEvent`, then tries to return the object to the pool that
92
+ built it; only when nothing owns it does it fall through to `destroy()`,
93
+ which is what calls `onDestroyEvent`. So a successfully recycled object never
94
+ sees it. Pair event subscriptions with `onActivateEvent` /
95
+ `onDeactivateEvent` instead, or you leak handlers.
96
+
97
+ - **`removeChild` is DEFERRED, which breaks a pool you re-lend in the same
98
+ frame.** The removal is queued and runs after the update and draw stack has
99
+ unwound, so an object you remove and immediately hand out again is torn out
100
+ from under its new owner when the queue drains: it vanishes, and the frame
101
+ after that the removal throws `Child is not mine.` Take it out immediately
102
+ instead, keeping the instance alive for reuse:
103
+
104
+ ```js
105
+ world.removeChildNow(bullet, true); // keepalive: do NOT destroy it
106
+ this.freeList.push(bullet);
107
+ ```
108
+
109
+ `removeChildNow(child, keepalive)` with `keepalive` true skips the destroy,
110
+ which is what you want for anything you own the lifetime of. This is the
111
+ usual cause of a pooled effect that flickers out one frame after it is
112
+ recycled.
113
+
114
+ **The engine's own classes are NOT in this pool.** They were registered in it
115
+ once, which bought nothing: most had recycling off, so `pool.pull("Sprite")`
116
+ was a plain construction behind a string key. The ones worth recycling have
117
+ typed pools instead, reached by key and fully typed:
118
+
119
+ ```js
120
+ const tween = getPool("tween").get(target);
121
+ const label = getPool("text").get(x, y, { font: "Arial", size: 12 });
122
+ getPool("text").release(label); // yours to release, as with particles
123
+ ```
124
+
125
+ A recycled label comes back with its **base-class** state reset as well as its
126
+ own: `floating`, `blendMode`, `alpha`, `tint`, the transform, the anchor, the
127
+ behaviour flags, any mask and any post effect, and `depth` back to 0. The
128
+ settings object names none of those, so set them again after `get()`. A pooled
129
+ HUD number that is not re-marked `floating` is drawn through the camera, i.e.
130
+ somewhere out in the world rather than on the screen, and `addChild(label, z)`
131
+ is what gives it its depth back.
132
+
133
+ `getPool` covers `vector2d`, `vector3d`, `point`, `matrix2d`, `matrix3d`,
134
+ `bounds`, `color`, `polygon`, `line`, `rectangle`, `roundedRectangle`,
135
+ `ellipse`, `tween`, `particle`, `text`, `colorLayer` and `bitmapTextData`.
136
+
137
+ `pool.register` still aliases every name you give it under an `me.` prefix,
138
+ pointing at the same entry, so `pool.pull("me.Bullet")` resolves identically —
139
+ and the same alias reaches the Tiled object factory, which is why a map
92
140
  authored against melonJS 1.x still finds its classes. Prefer the unprefixed
93
141
  name in new code; do not "correct" an `me.`-prefixed one, it is not broken.
94
142
 
95
143
  ## `scale()` is multiplicative
96
144
 
97
145
  A pooled sprite arrives carrying its previous life's transform, and calling
98
- `scale()` each frame compounds. For absolute scaling:
146
+ `scale()` each frame compounds. `rotate()` is the same. For absolute values,
147
+ reset first:
99
148
 
100
149
  ```js
101
150
  sprite.currentTransform.identity();
102
151
  sprite.currentTransform.scale(s, s, 1);
103
152
  ```
104
153
 
154
+ **Order matters, and it is the opposite of what reads naturally.**
155
+ `Matrix2d.scale` multiplies the matrix ROWS, so it applies AFTER whatever the
156
+ matrix already holds. Scale first and then rotate; rotate first and the scale
157
+ lands in the rotated frame and SHEARS anything non-uniform:
158
+
159
+ ```js
160
+ // correct: a long thin quad, rotated
161
+ t.identity();
162
+ t.scale(length / texW, width / texH);
163
+ t.rotate(angle);
164
+
165
+ // wrong: the same quad arrives as a parallelogram
166
+ t.identity();
167
+ t.rotate(angle);
168
+ t.scale(length / texW, width / texH);
169
+ ```
170
+
171
+ The symptom is a sprite that looks right at 0 and 90 degrees and skews in
172
+ between, which reads as a bad texture rather than as a transform bug.
173
+
105
174
  ## Culling and update cost
106
175
 
107
176
  - `Container` recomputes `inViewport` for every child each frame against every
@@ -231,6 +300,45 @@ SwiftShader, where the absolute numbers are wildly pessimistic and the JS
231
300
  profile shows the time as `(program)`, outside JS entirely. Ratios transfer;
232
301
  milliseconds do not.
233
302
 
303
+ ## Render targets, and what `hdr` costs
304
+
305
+ A render target is a canvas-sized GPU surface. The engine allocates them
306
+ lazily and only where a multi-pass chain actually needs one, which is worth
307
+ knowing before assuming an effect is expensive:
308
+
309
+ - a renderable with **no** post effects: none
310
+ - a renderable with **exactly one** post effect: still none. It takes the
311
+ `customShader` fast path and draws straight into whatever target is already
312
+ bound
313
+ - a **camera** with effects: a pair (one capture, one ping-pong intermediate)
314
+ - a sprite with **two or more** effects: its own pair
315
+
316
+ So the first lever is the count, not the cost of any one effect: going from
317
+ one effect to two on a sprite is what allocates, not going from a cheap shader
318
+ to an expensive one.
319
+
320
+ `hdr: true` changes the format of that camera pair from 8 bits per channel to
321
+ half-float, which is **8 bytes per pixel instead of 4**. At 1920×1080 that is
322
+ about 8 MB per target instead of 4 MB, so roughly +8 MB for the pair. With
323
+ `antiAlias` on, the capture half is also multisampled and its multisampled
324
+ surface takes the same format, so that part doubles too.
325
+
326
+ Nothing else moves. Sprite chains and the advanced-blend target stay 8-bit
327
+ whatever `hdr` says, for a correctness reason rather than a thrifty one: their
328
+ final blit composites with `ONE_MINUS_SRC_ALPHA`, and additive blending
329
+ accumulates alpha past 1 in a float target, which would turn that factor
330
+ negative and subtract the backdrop.
331
+
332
+ **With `hdr` off, nothing changes at all** — every target keeps the 8-bit
333
+ attachment it has always had, and a game that never adds a camera post-effect
334
+ allocates no render targets in the first place. The setting costs nothing
335
+ until something is actually using it.
336
+
337
+ ```js
338
+ // what you actually got, which is not always what you asked for
339
+ if (app.renderer.supportsHDR === false) { /* grade for a chain that clamps */ }
340
+ ```
341
+
234
342
  ## Where the costs actually are
235
343
 
236
344
  In rough order for a typical 2D game:
@@ -258,7 +258,8 @@ built-in-only spelling of this and are deprecated since 20.7.0.
258
258
  | `RoundRect` | exact | exact (decomposed) | sampled down to 12 vertices |
259
259
  | `Ellipse` | exact | circle of the average radius | circle of the average radius |
260
260
  | `Line` | exact | thin quad along the segment | thin quad along the segment |
261
- | `Point`, `Box3d`, `Sphere` | supported | **throws** | **throws** |
261
+ | `Box3d`, `Sphere` | exact, and in 3D | **throws** | **throws** |
262
+ | `Point` | supported | **throws** | **throws** |
262
263
 
263
264
  Two of those are worth knowing before you author geometry. Box2D caps a
264
265
  polygon's vertex count and says nothing when you go over: measured, a
@@ -329,6 +330,29 @@ body's collision shapes are measured from that same frame, so a shape of
329
330
  a shape's own `pos` still offsets it inside that frame (a small hitbox at the
330
331
  feet, a hurtbox at the head).
331
332
 
333
+ **That frame's origin is its TOP-LEFT corner, which is the trap for a small
334
+ shape.** `new Rect(0, 0, w, h)` reads naturally because it spans the frame.
335
+ A circle or a `Sphere` does not span anything, so `new Sphere(0, 0, 0, r)` on
336
+ a centre-anchored sprite sits on the sprite's corner, not on the sprite. Put
337
+ it at half the frame:
338
+
339
+ ```js
340
+ // centred on a 24x128 sprite whose anchorPoint is (0.5, 0.5)
341
+ bolt.bodyDef = { type: "dynamic", shapes: [new Sphere(24 / 2, 128 / 2, 0, r)] };
342
+ ```
343
+
344
+ Measured on exactly that sprite: with the shape at `(0, 0, 0)`,
345
+ `adapter.getBodyShapes()` reported it at `(-12, -64)` — **64 units up the
346
+ screen, four times its own radius** — so the projectile collided well away
347
+ from where the player saw it, in a way that still looked like a working game
348
+ because the targets were large.
349
+
350
+ The offset comes from the renderable's own `width` / `height`, NOT from what
351
+ it draws at. Scale a sprite through `currentTransform` and those stay the
352
+ texture's size, so the half-frame term is half the texture, not half the
353
+ drawn quad. The debug overlay is the quickest confirmation: the red shape
354
+ should sit on the art.
355
+
332
356
  This was broken until 20.7: shapes were measured from `pos` regardless of the
333
357
  anchor, so anything not anchored at its top-left corner collided where it was
334
358
  not drawn, by half a body at the default centred anchor and by a full body
@@ -400,7 +424,30 @@ survives but fights collision resolution and jitters. To teleport, go through
400
424
  The lifecycle is `onCollisionStart` (contact begins), `onCollisionActive` (every
401
425
  step while it persists) and `onCollisionEnd` (contact breaks). None of them are
402
426
  defined on `Renderable` — you just declare the ones you want and the dispatcher
403
- checks `typeof`. The legacy `onCollision` still exists and has a trap:
427
+ checks `typeof`. That is also why they cannot be declared on the base class: a
428
+ field there would SHADOW the prototype method every subclass that implements
429
+ one defines.
430
+
431
+ **In TypeScript the response is `CollisionResponse`**, exported from `melonjs`:
432
+
433
+ ```ts
434
+ import type { CollisionResponse, Renderable } from "melonjs";
435
+
436
+ class Coin extends Sprite {
437
+ onCollisionStart(response: CollisionResponse, other: Renderable) { … }
438
+ }
439
+ ```
440
+
441
+ On a renderable you do not subclass, declare the hooks you want as OPTIONAL
442
+ members of a local interface and assign through one plain cast — that is the
443
+ opt-in, and it needs no `as unknown as`:
444
+
445
+ ```ts
446
+ interface Contacts {
447
+ onCollisionStart?: (response: CollisionResponse, other: Renderable) => void;
448
+ }
449
+ (rock as Contacts).onCollisionStart = (response, other) => { … };
450
+ ``` The legacy `onCollision` still exists and has a trap:
404
451
 
405
452
  > On the built-in world, `onCollision` fires **twice per frame** for
406
453
  > dynamic-dynamic pairs — once per outer-loop visit — and gets the raw SAT
@@ -453,7 +500,8 @@ onCollisionStart(response, other) {
453
500
  What actually happens per adapter:
454
501
 
455
502
  - **built-in** — callbacks fire inline during `step()`. `Container.removeChild`
456
- is deferred, so it is safe; `removeChildNow()` and `destroy()` are immediate,
503
+ is deferred, so it is safe; `removeChildNow()`, and the internal `destroy()`
504
+ it calls, are immediate,
457
505
  and the detector only survives them because it re-checks `body === undefined`
458
506
  after every user callback.
459
507
  - **planck** — `onCollisionStart` / `onCollisionEnd` are dispatched from
@@ -496,50 +544,76 @@ Two prerequisites that produce empty results rather than errors:
496
544
  - On the **built-in** adapter, the broadphase is cleared and rebuilt inside
497
545
  `world.update()`, so querying before the first update returns nothing.
498
546
 
499
- ## 3D collision: `Box3d` and the Z pushback
547
+ ## 3D collision: `Box3d`, `Sphere` and the Z pushback
500
548
 
501
- The narrowphase is not 2D-only. `Box3d` is the one shape with a depth extent,
502
- and a `Box3d`-vs-`Box3d` contact is resolved in three dimensions — it is the
503
- only pair in the engine that can push back along **Z**.
549
+ The narrowphase is not 2D-only. Two shapes carry a depth extent — `Box3d` and
550
+ `Sphere` — and any pair of them is resolved in three dimensions, pushing back
551
+ along **Z** as well as X and Y.
504
552
 
505
553
  ```js
506
- import { Box3d } from "melonjs";
554
+ import { Box3d, Sphere } from "melonjs";
507
555
 
508
- // centre offset from the body, then half-extents on each axis
509
- rock.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));
510
- boat.body.addShape(new Box3d(0, 0, 0, 30, 40, 60));
556
+ // a box: centre offset from the body, then half-extents on each axis
557
+ crate.body.addShape(new Box3d(0, 0, 0, 40, 60, 40));
558
+ // a sphere: centre offset, then one radius
559
+ rock.body.addShape(new Sphere(0, 0, 0, 30));
511
560
  ```
512
561
 
513
- Reach for this for anything moving in the XZ plane under a `Camera3d` — a
562
+ Reach for these for anything moving in the XZ plane under a `Camera3d` — a
514
563
  runner dodging obstacles, a 2.5D platformer, pickups on a course. Hand-rolling
515
564
  a distance check there is the usual mistake, and it throws away the
516
565
  penetration vector the response already carries.
517
566
 
518
- **Read the Z axis off the response, not `overlapN`.** The MTV is a single
519
- axis, so exactly one of `overlapN.x`, `overlapN.y` and `overlapNZ` is
520
- non-zero:
567
+ **Which of the two.** A `Box3d` is world-axis-aligned and cannot be oriented:
568
+ it is right for walls, floors, crates and anything else that stays square to
569
+ the world, and wrong for anything that turns. A `Sphere` has no orientation to
570
+ get wrong, which makes it the shape for things that tumble, and for anything
571
+ laid out over a curved surface — on a globe every object's own "up" points a
572
+ different way, so no single axis-aligned box fits any of them. It is also the
573
+ cheaper test: one distance against a sum of radii.
574
+
575
+ **Z arrives beside `overlapN`, not inside it.** `overlapN` stays the 2D unit
576
+ vector it always was and `overlapNZ` carries the depth component, because
577
+ `Vector3d` does not extend `Vector2d` and widening the field would have broken
578
+ every existing 2D handler. The two together are a 3D unit vector, and
579
+ `overlapZ = overlapNZ * overlap` exactly as `overlapV = overlapN * overlap`.
580
+
581
+ **How much of the normal is in each half depends on the shapes.** A `Box3d`
582
+ pair separates along ONE world axis, so a depth contact leaves the 2D fields
583
+ at zero and a legacy 2D handler correctly applies no push. A `Sphere`
584
+ separates along the line between the two centres, which is generally diagonal:
585
+ all three components are non-zero at once. So subtract both halves rather than
586
+ branching on one:
521
587
 
522
588
  ```js
523
589
  onCollision(response, other) {
524
- if (response.overlapNZ !== 0) {
525
- // a depth-only contact: overlapN / overlapV are BOTH zero here
526
- this.pos.z -= response.overlapZ;
527
- }
528
- return true;
590
+ // works for either shape: the 2D half is zero when there is no 2D push
591
+ this.pos.x -= response.overlapV.x;
592
+ this.pos.y -= response.overlapV.y;
593
+ this.pos.z -= response.overlapZ;
594
+ return false;
529
595
  }
530
596
  ```
531
597
 
532
- That is deliberate: a collision resolved along Z leaves the 2D fields at zero,
533
- so an existing 2D `onCollision` applies no push rather than a wrong one.
598
+ Branching on `if (response.overlapNZ !== 0)` and then moving only `pos.z` is
599
+ the mistake to avoid: correct for a box pair, and for a sphere it throws away
600
+ the planar half of the contact.
534
601
 
535
- **Mixed pairs degrade to 2D.** `Box3d` against a `Polygon`, `Rectangle`,
536
- `RoundRect` or `Ellipse` tests the box's XY footprint and treats the planar
537
- shape as unbounded along Z. Only `Box3d`-vs-`Box3d` gives a depth result, so
538
- give BOTH sides a `Box3d` when you want one.
602
+ **Mixed pairs degrade to 2D.** A `Box3d` or a `Sphere` against a `Polygon`,
603
+ `Rectangle`, `RoundRect` or `Ellipse` tests the 3D shape's XY footprint and
604
+ treats the planar shape as unbounded along Z, which is what keeps a sphere or
605
+ box body colliding with an ordinary Tiled collision layer. `overlapZ` is zero
606
+ for those, since only one side has a depth to resolve. Give BOTH sides a 3D
607
+ shape when you want a depth result.
539
608
 
540
609
  This is the narrowphase and is independent of the 3D broadphase queries above
541
- — `querySphere` / `raycast3d` need `world.sortOn === "depth"`; `Box3d` shapes
542
- do not.
610
+ — `querySphere` / `raycast3d` need `world.sortOn === "depth"`; a 3D body shape
611
+ does not.
612
+
613
+ `raycast3d` measures a `Box3d` body as its box and a body that is one `Sphere`
614
+ as that sphere, both exactly. Anything else, including a body mixing the two,
615
+ is measured as the AABB containing its 3D shapes, and a body with none falls
616
+ back to a bounding sphere derived from the renderable's 2D bounds.
543
617
 
544
618
  **Bounds are what the broadphase sorts by, and they come from the SHAPE.**
545
619
  Every item is filed by its renderable's 2D `getBounds()`, so an object with no
@@ -698,6 +772,7 @@ use them. `adapter.capabilities` (`constraints`,
698
772
  | 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 |
699
773
  | 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 |
700
774
  | 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 |
775
+ | 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 |
701
776
  | a rotated renderable reports an unrotated bounding box | `autoTransform: false` opts `updateBounds()` out of the transform |
702
777
  | 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 |
703
778
  | forces do nothing after switching adapter | magnitude units differ — re-tune, don't reuse numbers |