melonjs 19.4.0 → 19.5.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 (75) hide show
  1. package/README.md +18 -2
  2. package/build/application/application.d.ts.map +1 -1
  3. package/build/application/settings.d.ts +25 -2
  4. package/build/application/settings.d.ts.map +1 -1
  5. package/build/audio/audio.d.ts +77 -253
  6. package/build/audio/audio.d.ts.map +1 -1
  7. package/build/audio/backend.d.ts +121 -0
  8. package/build/audio/backend.d.ts.map +1 -0
  9. package/build/audio/playback.d.ts +157 -0
  10. package/build/audio/playback.d.ts.map +1 -0
  11. package/build/audio/procedural.d.ts +105 -0
  12. package/build/audio/procedural.d.ts.map +1 -0
  13. package/build/audio/types.d.ts +205 -0
  14. package/build/audio/types.d.ts.map +1 -0
  15. package/build/index.d.ts +6 -3
  16. package/build/index.d.ts.map +1 -1
  17. package/build/index.js +2273 -379
  18. package/build/index.js.map +4 -4
  19. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  20. package/build/level/tiled/TMXTileset.d.ts +12 -0
  21. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  22. package/build/level/tiled/factories/shape.d.ts +1 -1
  23. package/build/level/tiled/factories/shape.d.ts.map +1 -1
  24. package/build/level/tiled/factories/tile.d.ts.map +1 -1
  25. package/build/loader/loader.d.ts +2 -2
  26. package/build/loader/loader.d.ts.map +1 -1
  27. package/build/loader/parsers/aseprite.d.ts +37 -0
  28. package/build/loader/parsers/aseprite.d.ts.map +1 -0
  29. package/build/physics/adapter.d.ts +560 -0
  30. package/build/physics/adapter.d.ts.map +1 -0
  31. package/build/physics/bounds.d.ts +18 -5
  32. package/build/physics/bounds.d.ts.map +1 -1
  33. package/build/physics/builtin/body.d.ts +605 -0
  34. package/build/physics/builtin/body.d.ts.map +1 -0
  35. package/build/physics/builtin/builtin-adapter.d.ts +91 -0
  36. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -0
  37. package/build/physics/builtin/detector.d.ts +167 -0
  38. package/build/physics/builtin/detector.d.ts.map +1 -0
  39. package/build/physics/builtin/quadtree.d.ts +112 -0
  40. package/build/physics/builtin/quadtree.d.ts.map +1 -0
  41. package/build/physics/builtin/raycast.d.ts +4 -0
  42. package/build/physics/builtin/raycast.d.ts.map +1 -0
  43. package/build/physics/{sat.d.ts → builtin/sat.d.ts} +7 -7
  44. package/build/physics/builtin/sat.d.ts.map +1 -0
  45. package/build/physics/world.d.ts +61 -26
  46. package/build/physics/world.d.ts.map +1 -1
  47. package/build/renderable/collectable.d.ts +7 -1
  48. package/build/renderable/collectable.d.ts.map +1 -1
  49. package/build/renderable/container.d.ts +0 -13
  50. package/build/renderable/container.d.ts.map +1 -1
  51. package/build/renderable/renderable.d.ts +78 -17
  52. package/build/renderable/renderable.d.ts.map +1 -1
  53. package/build/renderable/trigger.d.ts +14 -1
  54. package/build/renderable/trigger.d.ts.map +1 -1
  55. package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
  56. package/build/system/timer.d.ts +0 -5
  57. package/build/system/timer.d.ts.map +1 -1
  58. package/build/video/canvas/canvas_renderer.d.ts +0 -130
  59. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  60. package/build/video/renderer.d.ts +111 -0
  61. package/build/video/renderer.d.ts.map +1 -1
  62. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  63. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  64. package/build/video/webgl/effects/shine.d.ts +87 -0
  65. package/build/video/webgl/effects/shine.d.ts.map +1 -0
  66. package/build/video/webgl/webgl_renderer.d.ts +0 -106
  67. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/build/physics/body.d.ts +0 -351
  70. package/build/physics/body.d.ts.map +0 -1
  71. package/build/physics/detector.d.ts +0 -72
  72. package/build/physics/detector.d.ts.map +0 -1
  73. package/build/physics/quadtree.d.ts +0 -69
  74. package/build/physics/quadtree.d.ts.map +0 -1
  75. package/build/physics/sat.d.ts.map +0 -1
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * melonJS Game Engine - 19.4.0
2
+ * melonJS Game Engine - 19.5.0
3
3
  * http://www.melonjs.org
4
4
  * melonjs is licensed under the MIT License.
5
5
  * http://www.opensource.org/licenses/mit-license
@@ -799,11 +799,11 @@ var require_internal_state = __commonJS({
799
799
  };
800
800
  var getterFor = function(TYPE) {
801
801
  return function(it) {
802
- var state2;
803
- if (!isObject(it) || (state2 = get2(it)).type !== TYPE) {
802
+ var state3;
803
+ if (!isObject(it) || (state3 = get2(it)).type !== TYPE) {
804
804
  throw new TypeError2("Incompatible receiver, " + TYPE + " required");
805
805
  }
806
- return state2;
806
+ return state3;
807
807
  };
808
808
  };
809
809
  if (NATIVE_WEAK_MAP || shared.state) {
@@ -894,9 +894,9 @@ var require_make_built_in = __commonJS({
894
894
  } else if (value.prototype) value.prototype = void 0;
895
895
  } catch (error) {
896
896
  }
897
- var state2 = enforceInternalState(value);
898
- if (!hasOwn(state2, "source")) {
899
- state2.source = join(TEMPLATE, typeof name == "string" ? name : "");
897
+ var state3 = enforceInternalState(value);
898
+ if (!hasOwn(state3, "source")) {
899
+ state3.source = join(TEMPLATE, typeof name == "string" ? name : "");
900
900
  }
901
901
  return value;
902
902
  };
@@ -1819,7 +1819,7 @@ var require_howler = __commonJS({
1819
1819
  }
1820
1820
  };
1821
1821
  var Howler3 = new HowlerGlobal2();
1822
- var Howl3 = function(o) {
1822
+ var Howl4 = function(o) {
1823
1823
  var self2 = this;
1824
1824
  if (!o.src || o.src.length === 0) {
1825
1825
  console.error("An array of source files must be passed with any new Howl.");
@@ -1827,7 +1827,7 @@ var require_howler = __commonJS({
1827
1827
  }
1828
1828
  self2.init(o);
1829
1829
  };
1830
- Howl3.prototype = {
1830
+ Howl4.prototype = {
1831
1831
  /**
1832
1832
  * Initialize a new Howl group object.
1833
1833
  * @param {Object} o Passed in properties for this group.
@@ -3245,23 +3245,23 @@ var require_howler = __commonJS({
3245
3245
  define([], function() {
3246
3246
  return {
3247
3247
  Howler: Howler3,
3248
- Howl: Howl3
3248
+ Howl: Howl4
3249
3249
  };
3250
3250
  });
3251
3251
  }
3252
3252
  if (typeof exports !== "undefined") {
3253
3253
  exports.Howler = Howler3;
3254
- exports.Howl = Howl3;
3254
+ exports.Howl = Howl4;
3255
3255
  }
3256
3256
  if (typeof global !== "undefined") {
3257
3257
  global.HowlerGlobal = HowlerGlobal2;
3258
3258
  global.Howler = Howler3;
3259
- global.Howl = Howl3;
3259
+ global.Howl = Howl4;
3260
3260
  global.Sound = Sound2;
3261
3261
  } else if (typeof window !== "undefined") {
3262
3262
  window.HowlerGlobal = HowlerGlobal2;
3263
3263
  window.Howler = Howler3;
3264
- window.Howl = Howl3;
3264
+ window.Howl = Howl4;
3265
3265
  window.Sound = Sound2;
3266
3266
  }
3267
3267
  })();
@@ -4853,6 +4853,30 @@ var Bounds = class _Bounds {
4853
4853
  }
4854
4854
  }
4855
4855
  }
4856
+ /**
4857
+ * Expand this bounds to include every shape in `shapes`. Each shape
4858
+ * contributes its own `.getBounds()` (so `Rect`, `Polygon`, `Ellipse`,
4859
+ * `Bounds`, and anything else implementing the same getter all work);
4860
+ * shapes without `.getBounds()` are silently ignored. Use `clear=true`
4861
+ * to compute a fresh union (matches the `add` / `addBounds` shape).
4862
+ *
4863
+ * Useful for sizing a renderable from its `bodyDef.shapes` before
4864
+ * the underlying physics body has been constructed — used by the
4865
+ * TMX shape factory and {@link Trigger}.
4866
+ * @param shapes - one shape or an iterable of shapes
4867
+ * @param [clear] - reset the bounds before unioning
4868
+ */
4869
+ addShapes(shapes, clear = false) {
4870
+ if (clear) {
4871
+ this.clear();
4872
+ }
4873
+ const iterable = typeof shapes[Symbol.iterator] === "function" ? shapes : [shapes];
4874
+ for (const s of iterable) {
4875
+ if (typeof s?.getBounds === "function") {
4876
+ this.addBounds(s.getBounds());
4877
+ }
4878
+ }
4879
+ }
4856
4880
  /**
4857
4881
  * Adds the given point to the bounds definition.
4858
4882
  * @param point - The point to add to the bounds.
@@ -4977,18 +5001,6 @@ var Bounds = class _Bounds {
4977
5001
  bounds.addBounds(this);
4978
5002
  return bounds;
4979
5003
  }
4980
- /**
4981
- * Returns a polygon whose edges are the same as this bounds.
4982
- * @returns A new Polygon that represents this bounds.
4983
- */
4984
- toPolygon() {
4985
- return polygonPool.get(this.x, this.y, [
4986
- new Vector2d(0, 0),
4987
- new Vector2d(this.width, 0),
4988
- new Vector2d(this.width, this.height),
4989
- new Vector2d(0, this.height)
4990
- ]);
4991
- }
4992
5004
  };
4993
5005
  var boundsPool = createPool((vertices) => {
4994
5006
  const instance = new Bounds(vertices);
@@ -10032,7 +10044,9 @@ __export(audio_exports, {
10032
10044
  disable: () => disable,
10033
10045
  enable: () => enable,
10034
10046
  fade: () => fade,
10047
+ getAudioContext: () => getAudioContext,
10035
10048
  getCurrentTrack: () => getCurrentTrack,
10049
+ getMasterGain: () => getMasterGain,
10036
10050
  getVolume: () => getVolume,
10037
10051
  hasAudio: () => hasAudio,
10038
10052
  hasFormat: () => hasFormat,
@@ -10041,6 +10055,7 @@ __export(audio_exports, {
10041
10055
  mute: () => mute,
10042
10056
  muteAll: () => muteAll,
10043
10057
  muted: () => muted,
10058
+ noise: () => noise,
10044
10059
  orientation: () => orientation,
10045
10060
  panner: () => panner,
10046
10061
  pause: () => pause,
@@ -10057,51 +10072,83 @@ __export(audio_exports, {
10057
10072
  stop: () => stop,
10058
10073
  stopOnAudioError: () => stopOnAudioError,
10059
10074
  stopTrack: () => stopTrack,
10075
+ tone: () => tone,
10060
10076
  unload: () => unload,
10061
10077
  unloadAll: () => unloadAll,
10062
10078
  unmute: () => unmute,
10063
10079
  unmuteAll: () => unmuteAll
10064
10080
  });
10081
+
10082
+ // src/audio/backend.ts
10065
10083
  var import_howler = __toESM(require_howler(), 1);
10066
- var audioTracks = {};
10067
- var current_track_id = null;
10068
- var retry_counter = 0;
10069
- var audioExts = [];
10070
- var soundLoadError = function(sound_name, onerror_cb) {
10071
- if (retry_counter++ > 3) {
10084
+ var stopOnAudioError = true;
10085
+ var state = {
10086
+ tracks: {},
10087
+ currentTrackId: null,
10088
+ retryCounter: 0,
10089
+ audioExts: []
10090
+ };
10091
+ function getSoundOrThrow(sound_name) {
10092
+ const sound2 = state.tracks[sound_name];
10093
+ if (!sound2) {
10094
+ throw new Error(`audio clip ${sound_name} does not exist`);
10095
+ }
10096
+ return sound2;
10097
+ }
10098
+ var soundLoadError = function(sound_name, onerror_cb, stopOnError = true) {
10099
+ if (state.retryCounter++ >= 3) {
10072
10100
  const errmsg = `melonJS: failed loading ${sound_name}`;
10073
- if (!stopOnAudioError) {
10074
- disable();
10101
+ if (!stopOnError) {
10102
+ import_howler.Howler.mute(true);
10075
10103
  onerror_cb?.();
10076
- console.log(`${errmsg}, disabling audio`);
10104
+ console.warn(`${errmsg}, disabling audio`);
10077
10105
  } else {
10078
10106
  onerror_cb?.();
10079
10107
  throw new Error(errmsg);
10080
10108
  }
10081
10109
  } else {
10082
- audioTracks[sound_name].load();
10110
+ state.tracks[sound_name]?.load();
10083
10111
  }
10084
10112
  };
10085
- var stopOnAudioError = true;
10086
- function init(format = "mp3") {
10087
- audioExts = format.split(",");
10088
- return !import_howler.Howler.noAudio;
10113
+ function getAudioContext() {
10114
+ if (import_howler.Howler.noAudio) return null;
10115
+ if (import_howler.Howler.usingWebAudio && !import_howler.Howler.ctx) {
10116
+ import_howler.Howler.volume(import_howler.Howler.volume());
10117
+ }
10118
+ return import_howler.Howler.ctx ?? null;
10089
10119
  }
10090
- function hasFormat(codec) {
10091
- return hasAudio() && import_howler.Howler.codecs(codec);
10120
+ function getMasterGain() {
10121
+ if (!getAudioContext()) return null;
10122
+ return import_howler.Howler.masterGain ?? null;
10092
10123
  }
10093
- function hasAudio() {
10094
- return !import_howler.Howler.noAudio;
10124
+ function getGlobalVolume() {
10125
+ return import_howler.Howler.volume();
10095
10126
  }
10096
- function enable() {
10097
- unmuteAll();
10127
+ function setGlobalVolume(v) {
10128
+ import_howler.Howler.volume(v);
10098
10129
  }
10099
- function disable() {
10100
- muteAll();
10130
+ function setGlobalMuted(muted2) {
10131
+ import_howler.Howler.mute(muted2);
10132
+ }
10133
+ function isGlobalMuted() {
10134
+ return import_howler.Howler._muted;
10135
+ }
10136
+ function stopAllPlayback() {
10137
+ import_howler.Howler.stop();
10138
+ }
10139
+ function hasCodec(codec) {
10140
+ if (!isAudioAvailable()) return false;
10141
+ return import_howler.Howler.codecs(codec) === true;
10101
10142
  }
10143
+ function isAudioAvailable() {
10144
+ return !import_howler.Howler.noAudio;
10145
+ }
10146
+
10147
+ // src/audio/playback.ts
10148
+ var import_howler2 = __toESM(require_howler(), 1);
10102
10149
  function load(sound2, onloadcb, onerrorcb, settings = {}) {
10103
10150
  const urls = [];
10104
- if (audioExts.length === 0) {
10151
+ if (state.audioExts.length === 0) {
10105
10152
  throw new Error(
10106
10153
  "target audio extension(s) should be set through me.audio.init() before calling the preloader."
10107
10154
  );
@@ -10109,25 +10156,25 @@ function load(sound2, onloadcb, onerrorcb, settings = {}) {
10109
10156
  if (isDataUrl(sound2.src)) {
10110
10157
  urls.push(sound2.src);
10111
10158
  } else {
10112
- for (let i = 0; i < audioExts.length; i++) {
10159
+ for (let i = 0; i < state.audioExts.length; i++) {
10113
10160
  urls.push(
10114
- `${sound2.src + sound2.name}.${audioExts[i]}${settings.nocache ?? ""}`
10161
+ `${sound2.src + sound2.name}.${state.audioExts[i]}${settings.nocache ?? ""}`
10115
10162
  );
10116
10163
  }
10117
10164
  }
10118
- audioTracks[sound2.name] = new import_howler.Howl({
10165
+ state.tracks[sound2.name] = new import_howler2.Howl({
10119
10166
  src: urls,
10120
- volume: import_howler.Howler.volume(),
10167
+ volume: getGlobalVolume(),
10121
10168
  autoplay: sound2.autoplay === true,
10122
- loop: sound2.loop = true,
10169
+ loop: sound2.loop === true,
10123
10170
  html5: sound2.stream === true || sound2.html5 === true,
10124
10171
  // @ts-expect-error xhrWithCredentials is a valid Howl option but not in the type definitions
10125
10172
  xhrWithCredentials: settings.withCredentials,
10126
10173
  onloaderror() {
10127
- soundLoadError.call(this, sound2.name, onerrorcb);
10174
+ soundLoadError.call(this, sound2.name, onerrorcb, stopOnAudioError);
10128
10175
  },
10129
10176
  onload() {
10130
- retry_counter = 0;
10177
+ state.retryCounter = 0;
10131
10178
  if (typeof onloadcb === "function") {
10132
10179
  onloadcb();
10133
10180
  }
@@ -10136,176 +10183,310 @@ function load(sound2, onloadcb, onerrorcb, settings = {}) {
10136
10183
  return 1;
10137
10184
  }
10138
10185
  function play(sound_name, loop = false, onend, volume) {
10139
- const sound2 = audioTracks[sound_name];
10140
- if (sound2 && typeof sound2 !== "undefined") {
10141
- const id = sound2.play();
10142
- if (typeof loop === "boolean") {
10143
- sound2.loop(loop, id);
10144
- }
10145
- sound2.volume(
10146
- typeof volume === "number" ? clamp(volume, 0, 1) : import_howler.Howler.volume(),
10147
- id
10148
- );
10149
- if (typeof onend === "function") {
10150
- if (loop) {
10151
- sound2.on("end", onend, id);
10152
- } else {
10153
- sound2.once("end", onend, id);
10154
- }
10186
+ const sound2 = getSoundOrThrow(sound_name);
10187
+ const id = sound2.play();
10188
+ sound2.loop(loop, id);
10189
+ sound2.volume(
10190
+ typeof volume === "number" ? clamp(volume, 0, 1) : getGlobalVolume(),
10191
+ id
10192
+ );
10193
+ if (typeof onend === "function") {
10194
+ if (loop) {
10195
+ sound2.on("end", onend, id);
10196
+ } else {
10197
+ sound2.once("end", onend, id);
10155
10198
  }
10156
- return id;
10157
- } else {
10158
- throw new Error(`audio clip ${sound_name} does not exist`);
10159
10199
  }
10200
+ return id;
10160
10201
  }
10161
10202
  function fade(sound_name, from, to, duration, id) {
10162
- const sound2 = audioTracks[sound_name];
10163
- if (sound2 && typeof sound2 !== "undefined") {
10164
- sound2.fade(from, to, duration, id);
10165
- } else {
10166
- throw new Error(`audio clip ${sound_name} does not exist`);
10167
- }
10203
+ getSoundOrThrow(sound_name).fade(from, to, duration, id);
10168
10204
  }
10169
10205
  function seek(sound_name, ...args) {
10170
- const sound2 = audioTracks[sound_name];
10171
- if (sound2 && typeof sound2 !== "undefined") {
10172
- return sound2.seek(...args);
10173
- } else {
10174
- throw new Error(`audio clip ${sound_name} does not exist`);
10175
- }
10206
+ return getSoundOrThrow(sound_name).seek(...args);
10176
10207
  }
10177
10208
  function rate(sound_name, ...args) {
10178
- const sound2 = audioTracks[sound_name];
10179
- if (sound2 && typeof sound2 !== "undefined") {
10180
- return sound2.rate(...args);
10181
- } else {
10182
- throw new Error(`audio clip ${sound_name} does not exist`);
10183
- }
10209
+ return getSoundOrThrow(sound_name).rate(...args);
10184
10210
  }
10185
10211
  function stereo(sound_name, pan, id) {
10186
- const sound2 = audioTracks[sound_name];
10187
- if (sound2 && typeof sound2 !== "undefined") {
10188
- return pan !== void 0 ? sound2.stereo(pan, id) : sound2.stereo();
10189
- } else {
10190
- throw new Error(`audio clip ${sound_name} does not exist`);
10212
+ const sound2 = getSoundOrThrow(sound_name);
10213
+ if (pan === void 0) {
10214
+ return sound2.stereo();
10191
10215
  }
10216
+ sound2.stereo(pan, id);
10192
10217
  }
10193
10218
  function position(sound_name, x, y, z, id) {
10194
- const sound2 = audioTracks[sound_name];
10195
- if (sound2 && typeof sound2 !== "undefined") {
10196
- return sound2.pos(x, y, z, id);
10197
- } else {
10198
- throw new Error(`audio clip ${sound_name} does not exist`);
10219
+ const sound2 = getSoundOrThrow(sound_name);
10220
+ if (x === void 0) {
10221
+ return sound2.pos();
10199
10222
  }
10223
+ sound2.pos(x, y, z, id);
10200
10224
  }
10201
10225
  function orientation(sound_name, x, y, z, id) {
10202
- const sound2 = audioTracks[sound_name];
10203
- if (sound2 && typeof sound2 !== "undefined") {
10204
- return sound2.orientation(x, y, z, id);
10205
- } else {
10206
- throw new Error(`audio clip ${sound_name} does not exist`);
10226
+ const sound2 = getSoundOrThrow(sound_name);
10227
+ if (x === void 0) {
10228
+ return sound2.orientation();
10207
10229
  }
10230
+ sound2.orientation(x, y, z, id);
10208
10231
  }
10209
10232
  function panner(sound_name, attributes, id) {
10210
- const sound2 = audioTracks[sound_name];
10211
- if (sound2 && typeof sound2 !== "undefined") {
10212
- return sound2.pannerAttr(
10213
- attributes,
10214
- id
10215
- );
10216
- } else {
10217
- throw new Error(`audio clip ${sound_name} does not exist`);
10233
+ const sound2 = getSoundOrThrow(sound_name);
10234
+ if (attributes !== void 0) {
10235
+ const attrs = attributes;
10236
+ if (id !== void 0) sound2.pannerAttr(attrs, id);
10237
+ else sound2.pannerAttr(attrs);
10218
10238
  }
10239
+ return id !== void 0 ? sound2.pannerAttr(id) : sound2.pannerAttr();
10219
10240
  }
10220
10241
  function stop(sound_name, id) {
10221
- if (typeof sound_name !== "undefined") {
10222
- const sound2 = audioTracks[sound_name];
10223
- if (sound2 && typeof sound2 !== "undefined") {
10224
- sound2.stop(id);
10225
- sound2.off("end", void 0, id);
10226
- } else {
10227
- throw new Error(`audio clip ${sound_name} does not exist`);
10228
- }
10229
- } else {
10230
- import_howler.Howler.stop();
10242
+ if (sound_name === void 0) {
10243
+ stopAllPlayback();
10244
+ return;
10231
10245
  }
10246
+ const sound2 = getSoundOrThrow(sound_name);
10247
+ sound2.stop(id);
10248
+ sound2.off("end", void 0, id);
10232
10249
  }
10233
10250
  function pause(sound_name, id) {
10234
- const sound2 = audioTracks[sound_name];
10235
- if (sound2 && typeof sound2 !== "undefined") {
10236
- sound2.pause(id);
10237
- } else {
10238
- throw new Error(`audio clip ${sound_name} does not exist`);
10239
- }
10251
+ getSoundOrThrow(sound_name).pause(id);
10240
10252
  }
10241
10253
  function resume(sound_name, id) {
10242
- const sound2 = audioTracks[sound_name];
10243
- if (sound2 && typeof sound2 !== "undefined") {
10244
- sound2.play(id);
10254
+ getSoundOrThrow(sound_name).play(id);
10255
+ }
10256
+
10257
+ // src/audio/procedural.ts
10258
+ function _resumeIfSuspended(ctx) {
10259
+ if (ctx.state === "suspended") {
10260
+ ctx.resume().catch(() => {
10261
+ });
10262
+ }
10263
+ }
10264
+ function _buildGainEnvelope(ctx, t0, t1, attack, duration, gain) {
10265
+ const atk = Math.min(duration / 2, Math.max(1e-3, attack));
10266
+ const env = ctx.createGain();
10267
+ env.gain.setValueAtTime(0, t0);
10268
+ env.gain.linearRampToValueAtTime(gain, t0 + atk);
10269
+ if (gain > 1e-4) {
10270
+ env.gain.exponentialRampToValueAtTime(1e-4, t1);
10245
10271
  } else {
10246
- throw new Error(`audio clip ${sound_name} does not exist`);
10272
+ env.gain.linearRampToValueAtTime(0, t1);
10247
10273
  }
10274
+ return env;
10275
+ }
10276
+ function _connectToOutput(ctx, source, pan, t0) {
10277
+ const out = getMasterGain() ?? ctx.destination;
10278
+ if (pan === 0) {
10279
+ source.connect(out);
10280
+ return null;
10281
+ }
10282
+ const panner2 = ctx.createStereoPanner();
10283
+ panner2.pan.setValueAtTime(Math.max(-1, Math.min(1, pan)), t0);
10284
+ source.connect(panner2).connect(out);
10285
+ return panner2;
10286
+ }
10287
+ function tone(opts) {
10288
+ const ctx = getAudioContext();
10289
+ if (!ctx) return;
10290
+ const {
10291
+ freq,
10292
+ duration,
10293
+ wave = "sine",
10294
+ gain = 0.1,
10295
+ attack = 5e-3,
10296
+ pan = 0,
10297
+ pitchSlide = 1
10298
+ } = opts;
10299
+ const freqs = Array.isArray(freq) ? freq : [freq];
10300
+ if (freqs.length === 0) return;
10301
+ _resumeIfSuspended(ctx);
10302
+ const dur = Math.max(1e-3, duration);
10303
+ const t0 = ctx.currentTime;
10304
+ const t1 = t0 + dur;
10305
+ const env = _buildGainEnvelope(ctx, t0, t1, attack, dur, gain);
10306
+ const panner2 = _connectToOutput(ctx, env, pan, t0);
10307
+ let remaining = freqs.length;
10308
+ for (const f of freqs) {
10309
+ const osc = ctx.createOscillator();
10310
+ osc.type = wave;
10311
+ osc.frequency.setValueAtTime(f, t0);
10312
+ if (pitchSlide !== 1 && f > 0) {
10313
+ osc.frequency.exponentialRampToValueAtTime(
10314
+ Math.max(0.01, f * pitchSlide),
10315
+ t1
10316
+ );
10317
+ }
10318
+ osc.connect(env);
10319
+ osc.start(t0);
10320
+ osc.stop(t1 + 0.02);
10321
+ osc.onended = () => {
10322
+ osc.disconnect();
10323
+ if (--remaining === 0) {
10324
+ env.disconnect();
10325
+ panner2?.disconnect();
10326
+ }
10327
+ };
10328
+ }
10329
+ }
10330
+ function fillNoiseBuffer(data2, type) {
10331
+ const n = data2.length;
10332
+ if (type === "white") {
10333
+ for (let i = 0; i < n; i++) {
10334
+ data2[i] = Math.random() * 2 - 1;
10335
+ }
10336
+ return;
10337
+ }
10338
+ if (type === "pink") {
10339
+ let b0 = 0;
10340
+ let b1 = 0;
10341
+ let b2 = 0;
10342
+ let b3 = 0;
10343
+ let b4 = 0;
10344
+ let b5 = 0;
10345
+ let b6 = 0;
10346
+ for (let i = 0; i < n; i++) {
10347
+ const w = Math.random() * 2 - 1;
10348
+ b0 = 0.99886 * b0 + w * 0.0555179;
10349
+ b1 = 0.99332 * b1 + w * 0.0750759;
10350
+ b2 = 0.969 * b2 + w * 0.153852;
10351
+ b3 = 0.8665 * b3 + w * 0.3104856;
10352
+ b4 = 0.55 * b4 + w * 0.5329522;
10353
+ b5 = -0.7616 * b5 - w * 0.016898;
10354
+ data2[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362) * 0.11;
10355
+ b6 = w * 0.115926;
10356
+ }
10357
+ return;
10358
+ }
10359
+ let last = 0;
10360
+ for (let i = 0; i < n; i++) {
10361
+ const w = Math.random() * 2 - 1;
10362
+ last = (last + 0.02 * w) / 1.02;
10363
+ data2[i] = last * 3.5;
10364
+ }
10365
+ }
10366
+ function noise(opts) {
10367
+ const ctx = getAudioContext();
10368
+ if (!ctx) return;
10369
+ const {
10370
+ duration,
10371
+ type = "white",
10372
+ gain = 0.1,
10373
+ attack = 5e-3,
10374
+ pan = 0,
10375
+ filter,
10376
+ filterSweep = 1
10377
+ } = opts;
10378
+ _resumeIfSuspended(ctx);
10379
+ const dur = Math.max(1e-3, duration);
10380
+ const t0 = ctx.currentTime;
10381
+ const t1 = t0 + dur;
10382
+ const env = _buildGainEnvelope(ctx, t0, t1, attack, dur, gain);
10383
+ const sampleCount = Math.max(1, Math.ceil(dur * ctx.sampleRate));
10384
+ const buffer = ctx.createBuffer(1, sampleCount, ctx.sampleRate);
10385
+ fillNoiseBuffer(buffer.getChannelData(0), type);
10386
+ const src = ctx.createBufferSource();
10387
+ src.buffer = buffer;
10388
+ src.connect(env);
10389
+ let tail = env;
10390
+ if (filter !== void 0) {
10391
+ const biquad = ctx.createBiquadFilter();
10392
+ biquad.type = filter.type;
10393
+ biquad.frequency.setValueAtTime(filter.frequency, t0);
10394
+ if (filter.Q !== void 0) {
10395
+ biquad.Q.setValueAtTime(filter.Q, t0);
10396
+ }
10397
+ if (filterSweep !== 1 && filter.frequency > 0) {
10398
+ biquad.frequency.exponentialRampToValueAtTime(
10399
+ Math.max(0.01, filter.frequency * filterSweep),
10400
+ t1
10401
+ );
10402
+ }
10403
+ tail.connect(biquad);
10404
+ tail = biquad;
10405
+ }
10406
+ const panner2 = _connectToOutput(ctx, tail, pan, t0);
10407
+ src.start(t0);
10408
+ src.stop(t1 + 0.02);
10409
+ src.onended = () => {
10410
+ src.disconnect();
10411
+ env.disconnect();
10412
+ if (tail !== env) tail.disconnect();
10413
+ panner2?.disconnect();
10414
+ };
10415
+ }
10416
+
10417
+ // src/audio/audio.ts
10418
+ function init(format = "mp3") {
10419
+ state.audioExts = format.split(",");
10420
+ return isAudioAvailable();
10421
+ }
10422
+ function hasFormat(codec) {
10423
+ return hasCodec(codec);
10424
+ }
10425
+ function hasAudio() {
10426
+ return isAudioAvailable();
10427
+ }
10428
+ function enable() {
10429
+ unmuteAll();
10430
+ }
10431
+ function disable() {
10432
+ muteAll();
10248
10433
  }
10249
10434
  function playTrack(sound_name, volume) {
10250
- current_track_id = sound_name;
10251
- return play(current_track_id, true, null, volume);
10435
+ state.currentTrackId = sound_name;
10436
+ return play(state.currentTrackId, true, null, volume);
10252
10437
  }
10253
10438
  function stopTrack() {
10254
- if (current_track_id !== null) {
10255
- audioTracks[current_track_id].stop();
10256
- current_track_id = null;
10439
+ if (state.currentTrackId !== null) {
10440
+ state.tracks[state.currentTrackId]?.stop();
10441
+ state.currentTrackId = null;
10257
10442
  }
10258
10443
  }
10259
10444
  function pauseTrack() {
10260
- if (current_track_id !== null) {
10261
- audioTracks[current_track_id].pause();
10445
+ if (state.currentTrackId !== null) {
10446
+ state.tracks[state.currentTrackId]?.pause();
10262
10447
  }
10263
10448
  }
10264
10449
  function resumeTrack() {
10265
- if (current_track_id !== null) {
10266
- audioTracks[current_track_id].play();
10450
+ if (state.currentTrackId !== null) {
10451
+ state.tracks[state.currentTrackId]?.play();
10267
10452
  }
10268
10453
  }
10269
10454
  function getCurrentTrack() {
10270
- return current_track_id;
10455
+ return state.currentTrackId;
10271
10456
  }
10272
10457
  function setVolume(volume) {
10273
- import_howler.Howler.volume(volume);
10458
+ setGlobalVolume(volume);
10274
10459
  }
10275
10460
  function getVolume() {
10276
- return import_howler.Howler.volume();
10461
+ return getGlobalVolume();
10277
10462
  }
10278
10463
  function mute(sound_name, id, shouldMute = true) {
10279
- const sound2 = audioTracks[sound_name];
10280
- if (sound2 && typeof sound2 !== "undefined") {
10281
- sound2.mute(shouldMute, id);
10282
- } else {
10283
- throw new Error(`audio clip ${sound_name} does not exist`);
10284
- }
10464
+ getSoundOrThrow(sound_name).mute(shouldMute, id);
10285
10465
  }
10286
10466
  function unmute(sound_name, id) {
10287
10467
  mute(sound_name, id, false);
10288
10468
  }
10289
10469
  function muteAll() {
10290
- import_howler.Howler.mute(true);
10470
+ setGlobalMuted(true);
10291
10471
  }
10292
10472
  function unmuteAll() {
10293
- import_howler.Howler.mute(false);
10473
+ setGlobalMuted(false);
10294
10474
  }
10295
10475
  function muted() {
10296
- return import_howler.Howler._muted;
10476
+ return isGlobalMuted();
10297
10477
  }
10298
10478
  function unload(sound_name) {
10299
- if (!(sound_name in audioTracks)) {
10479
+ const sound2 = state.tracks[sound_name];
10480
+ if (!sound2) {
10300
10481
  return false;
10301
10482
  }
10302
- audioTracks[sound_name].unload();
10303
- delete audioTracks[sound_name];
10483
+ sound2.unload();
10484
+ delete state.tracks[sound_name];
10304
10485
  return true;
10305
10486
  }
10306
10487
  function unloadAll() {
10307
- for (const sound_name in audioTracks) {
10308
- if (Object.prototype.hasOwnProperty.call(audioTracks, sound_name)) {
10488
+ for (const sound_name in state.tracks) {
10489
+ if (Object.prototype.hasOwnProperty.call(state.tracks, sound_name)) {
10309
10490
  unload(sound_name);
10310
10491
  }
10311
10492
  }
@@ -10341,7 +10522,6 @@ var CameraEffect = class {
10341
10522
  */
10342
10523
  update(_dt) {
10343
10524
  }
10344
- // eslint-disable-line @typescript-eslint/no-unused-vars
10345
10525
  /**
10346
10526
  * Called after the scene renders to draw visual overlays (e.g. color fills for fading).
10347
10527
  * @param _renderer - the renderer to draw with
@@ -10350,7 +10530,6 @@ var CameraEffect = class {
10350
10530
  */
10351
10531
  draw(_renderer2, _width, _height) {
10352
10532
  }
10353
- // eslint-disable-line @typescript-eslint/no-unused-vars
10354
10533
  /**
10355
10534
  * Called when the effect is removed from the camera. Override to clean up resources.
10356
10535
  */
@@ -11277,7 +11456,7 @@ var collision = {
11277
11456
  }
11278
11457
  };
11279
11458
 
11280
- // src/physics/body.js
11459
+ // src/physics/builtin/body.js
11281
11460
  var Body = class {
11282
11461
  /**
11283
11462
  * @param {Renderable|Container|Entity|Sprite|NineSliceSprite} ancestor - the parent object this body is attached to
@@ -11308,11 +11487,16 @@ var Body = class {
11308
11487
  this.friction.set(0, 0);
11309
11488
  this.bounce = 0;
11310
11489
  this.mass = 1;
11490
+ this.angle = 0;
11491
+ this.angularVelocity = 0;
11492
+ this.angularDrag = 0;
11493
+ this.pseudoInertia = 1;
11311
11494
  if (typeof this.maxVel === "undefined") {
11312
11495
  this.maxVel = vector2dPool.get();
11313
11496
  }
11314
11497
  this.maxVel.set(490, 490);
11315
11498
  this.isStatic = false;
11499
+ this.isSensor = false;
11316
11500
  this.gravityScale = 1;
11317
11501
  this.ignoreGravity = false;
11318
11502
  this.falling = false;
@@ -11340,6 +11524,229 @@ var Body = class {
11340
11524
  setStatic(isStatic = true) {
11341
11525
  this.isStatic = isStatic === true;
11342
11526
  }
11527
+ /**
11528
+ * set this body's linear velocity. Portable across physics adapters —
11529
+ * under the builtin adapter this mutates `body.vel`; under Matter it
11530
+ * delegates to `Matter.Body.setVelocity`.
11531
+ * @param {number} x - velocity along the X axis
11532
+ * @param {number} y - velocity along the Y axis
11533
+ */
11534
+ setVelocity(x, y) {
11535
+ this.vel.set(x, y);
11536
+ }
11537
+ /**
11538
+ * read this body's linear velocity into an optional output vector.
11539
+ * @param {Vector2d} [out] - vector to write into; a new Vector2d is
11540
+ * allocated when omitted
11541
+ * @returns {Vector2d}
11542
+ */
11543
+ getVelocity(out) {
11544
+ return (out ?? vector2dPool.get()).copy(this.vel);
11545
+ }
11546
+ /**
11547
+ * accumulate a force on this body for the current step. Repeated calls
11548
+ * within a single update add together; the engine clears the
11549
+ * accumulator at the end of each integration step. Force magnitude
11550
+ * conventions differ between adapters — consult the active adapter's
11551
+ * docs for tuning ranges (builtin: px/frame²; Matter: Newtonian
11552
+ * `force/mass·dt²`, typically ~100× smaller than builtin).
11553
+ * @param {number} x - force along the X axis
11554
+ * @param {number} y - force along the Y axis
11555
+ * @param {number} [pointX] - world X of the application point; when
11556
+ * present (along with `pointY`) and different from the body centroid,
11557
+ * the resulting lever arm generates a torque
11558
+ * `τ = (r.x · F.y) − (r.y · F.x)` that bumps {@link Body#angularVelocity}
11559
+ * by `τ / pseudoInertia`. Omit both `pointX` and `pointY` for the
11560
+ * linear-only behaviour that's compatible with code written before
11561
+ * the angular API was added.
11562
+ * @param {number} [pointY] - world Y of the application point
11563
+ * @example
11564
+ * // pure linear thrust (2-arg form, unchanged behaviour):
11565
+ * ship.body.applyForce(0, -0.05);
11566
+ *
11567
+ * // off-centre push on a crate: the contact point at the top of the
11568
+ * // crate is above its centroid, so the same horizontal force now
11569
+ * // both translates AND tips the crate forward.
11570
+ * const topX = crate.pos.x + crate.width / 2;
11571
+ * const topY = crate.pos.y;
11572
+ * crate.body.applyForce(1.5, 0, topX, topY);
11573
+ *
11574
+ * // wind pushing on the top of a flag-pole: pole tilts, base stays put
11575
+ * // (only meaningful here if the base is anchored / static).
11576
+ * pole.body.applyForce(0.3, 0, pole.pos.x + pole.width / 2, pole.pos.y);
11577
+ */
11578
+ applyForce(x, y, pointX, pointY) {
11579
+ this.force.x += x;
11580
+ this.force.y += y;
11581
+ if (typeof pointX === "number" && typeof pointY === "number") {
11582
+ const bounds = this.bounds;
11583
+ const rx = pointX - bounds.centerX;
11584
+ const ry = pointY - bounds.centerY;
11585
+ const torque = rx * y - ry * x;
11586
+ if (this.pseudoInertia > 0) {
11587
+ this.angularVelocity += torque / this.pseudoInertia;
11588
+ }
11589
+ }
11590
+ }
11591
+ /**
11592
+ * Apply an instantaneous angular impulse: `Δω = τ / pseudoInertia`.
11593
+ * The angular analog of {@link Body#applyImpulse} — bypasses the
11594
+ * lever-arm computation in {@link Body#applyForce} for the
11595
+ * "just spin this up directly" case (a power-up's intrinsic spin,
11596
+ * an explicit thruster, a knockback spin effect on hit).
11597
+ * @param {number} torque - angular impulse magnitude. Positive values
11598
+ * produce clockwise rotation on screen (matching the Y-down canvas
11599
+ * convention); negative values rotate counter-clockwise.
11600
+ * @example
11601
+ * // give a pickup a one-shot spin-up when collected:
11602
+ * pickup.body.applyTorque(80);
11603
+ *
11604
+ * // explosion knockback that both pushes and spins:
11605
+ * crate.body.applyImpulse(impulseX, impulseY);
11606
+ * crate.body.applyTorque((Math.random() - 0.5) * 100);
11607
+ */
11608
+ applyTorque(torque) {
11609
+ if (this.pseudoInertia > 0) {
11610
+ this.angularVelocity += torque / this.pseudoInertia;
11611
+ }
11612
+ }
11613
+ /**
11614
+ * Set angular velocity directly. Bypasses inertia — the value is the
11615
+ * actual rad/frame that integration will apply next step. Use this
11616
+ * for "set and hold" rotation (a coin that always spins at the same
11617
+ * rate); use {@link Body#applyTorque} for impulse-style spin-up.
11618
+ * @param {number} omega - target angular velocity (rad / frame)
11619
+ * @example
11620
+ * // make a fan blade spin at a fixed rate:
11621
+ * fan.body.setAngularVelocity(0.1); // ~6°/frame
11622
+ */
11623
+ setAngularVelocity(omega) {
11624
+ this.angularVelocity = omega;
11625
+ }
11626
+ /**
11627
+ * Read current angular velocity (rad / frame).
11628
+ * @returns {number}
11629
+ * @example
11630
+ * // freeze rotation if the body is spinning too fast:
11631
+ * if (Math.abs(body.getAngularVelocity()) > 2) {
11632
+ * body.setAngularVelocity(0);
11633
+ * }
11634
+ */
11635
+ getAngularVelocity() {
11636
+ return this.angularVelocity;
11637
+ }
11638
+ /**
11639
+ * Set absolute rotation angle (radians). Updates the body's `angle`
11640
+ * field and re-syncs the renderable's `currentTransform` immediately
11641
+ * so the visual rotation reflects the new value without waiting for
11642
+ * the next integration step.
11643
+ * @param {number} rad - target angle in radians
11644
+ * @example
11645
+ * // turret aims at the player every frame:
11646
+ * const dx = player.centerX - turret.centerX;
11647
+ * const dy = player.centerY - turret.centerY;
11648
+ * turret.body.setAngle(Math.atan2(dy, dx));
11649
+ */
11650
+ setAngle(rad) {
11651
+ this.angle = rad;
11652
+ this._syncAngleTransform();
11653
+ }
11654
+ /**
11655
+ * Read absolute rotation angle (radians).
11656
+ * @returns {number}
11657
+ */
11658
+ getAngle() {
11659
+ return this.angle;
11660
+ }
11661
+ /**
11662
+ * Sync `this.angle` to the renderable's `currentTransform`. Pivot is
11663
+ * the body's bounds center (matches the matter adapter's rotation
11664
+ * pivot — see `MatterAdapter.syncFromPhysics`). Internal helper used
11665
+ * by both the per-step integrator and {@link Body#setAngle}.
11666
+ * @ignore
11667
+ */
11668
+ _syncAngleTransform() {
11669
+ const t = this.ancestor?.currentTransform;
11670
+ if (!t) {
11671
+ return;
11672
+ }
11673
+ const bounds = this.bounds;
11674
+ const cx = bounds.centerX - this.ancestor.pos.x;
11675
+ const cy = bounds.centerY - this.ancestor.pos.y;
11676
+ t.identity();
11677
+ if (this.angle !== 0) {
11678
+ if (cx !== 0 || cy !== 0) {
11679
+ t.translate(cx, cy);
11680
+ t.rotate(this.angle);
11681
+ t.translate(-cx, -cy);
11682
+ } else {
11683
+ t.rotate(this.angle);
11684
+ }
11685
+ }
11686
+ }
11687
+ /**
11688
+ * apply an instantaneous impulse to this body — a single-step velocity
11689
+ * change scaled by inverse mass (`dv = J / m`). Useful for one-shot
11690
+ * events like a cue strike, projectile launch, or knockback, where
11691
+ * mass should influence the resulting velocity change. Repeated calls
11692
+ * within a single update accumulate. Static bodies (mass 0) ignore
11693
+ * the call. Identical signature on the Matter adapter, where the
11694
+ * adapter integrates the impulse the same way (matter has no native
11695
+ * `applyImpulse`).
11696
+ * @param {number} x - impulse along the X axis
11697
+ * @param {number} y - impulse along the Y axis
11698
+ */
11699
+ applyImpulse(x, y) {
11700
+ const invMass = this.mass > 0 ? 1 / this.mass : 0;
11701
+ this.vel.x += x * invMass;
11702
+ this.vel.y += y * invMass;
11703
+ }
11704
+ /**
11705
+ * set this body's mass. Useful for variable-mass entities (projectiles
11706
+ * loaded with ammo, weight pickups, characters carrying objects).
11707
+ * Mass affects `applyImpulse` (via `dv = J / m`) and the proportional
11708
+ * push-out response in dynamic-dynamic collisions. A mass of 0 makes
11709
+ * the body inert to forces and impulses (without going static).
11710
+ * @param {number} m - new mass, non-negative
11711
+ */
11712
+ setMass(m) {
11713
+ this.mass = m;
11714
+ }
11715
+ /**
11716
+ * set this body's restitution / bounce factor. `0` = no bounce (energy
11717
+ * absorbed on contact); `1` = perfect elastic rebound; values in
11718
+ * between dampen the rebound. Applied by `Body.respondToCollision` —
11719
+ * see `BuiltinAdapter` docs for the cancellation math.
11720
+ *
11721
+ * Matches the `bodyDef.restitution` field name used at registration
11722
+ * time; the body-side property has historically been called `bounce`,
11723
+ * which is the canonical legacy name and is preserved.
11724
+ * @param {number} r - restitution factor, typically in [0, 1]
11725
+ */
11726
+ setBounce(r) {
11727
+ this.bounce = r;
11728
+ }
11729
+ /**
11730
+ * set this body's per-body gravity multiplier. `1` = world gravity
11731
+ * (default), `0` = ignore world gravity (e.g. flying enemy, underwater
11732
+ * float), `2` = 2× gravity (heavy-feel objects). Multiplied with the
11733
+ * world's `gravity.y` each frame inside `applyGravity`.
11734
+ * @param {number} scale - gravity scale factor
11735
+ */
11736
+ setGravityScale(scale2) {
11737
+ this.gravityScale = scale2;
11738
+ }
11739
+ /**
11740
+ * toggle this body between solid and sensor. Sensor bodies still emit
11741
+ * collision events (`onCollisionStart`, `onCollisionActive`,
11742
+ * `onCollisionEnd`) but the solver does not physically resolve the
11743
+ * contact — same semantics as Matter's `isSensor`. Useful for one-way
11744
+ * platforms, trigger zones, and ground-snap assists.
11745
+ * @param {boolean} [isSensor=true]
11746
+ */
11747
+ setSensor(isSensor = true) {
11748
+ this.isSensor = isSensor === true;
11749
+ }
11343
11750
  /**
11344
11751
  * add a collision shape to this body <br>
11345
11752
  * (note: me.Rect objects will be converted to me.Polygon before being added)
@@ -11352,11 +11759,21 @@ var Body = class {
11352
11759
  * this.body.addShape(me.loader.getJSON("shapesdef").banana);
11353
11760
  */
11354
11761
  addShape(shape) {
11355
- if (shape instanceof Rect || shape instanceof Bounds) {
11762
+ if (shape instanceof Rect) {
11356
11763
  const poly = shape.toPolygon();
11357
11764
  this.shapes.push(poly);
11358
11765
  this.bounds.add(poly.points);
11359
11766
  this.bounds.translate(poly.pos);
11767
+ } else if (shape instanceof Bounds) {
11768
+ const poly = polygonPool.get(shape.x, shape.y, [
11769
+ new Vector2d(0, 0),
11770
+ new Vector2d(shape.width, 0),
11771
+ new Vector2d(shape.width, shape.height),
11772
+ new Vector2d(0, shape.height)
11773
+ ]);
11774
+ this.shapes.push(poly);
11775
+ this.bounds.add(poly.points);
11776
+ this.bounds.translate(poly.pos);
11360
11777
  } else if (shape instanceof Ellipse) {
11361
11778
  if (!this.shapes.includes(shape)) {
11362
11779
  this.shapes.push(shape);
@@ -11377,11 +11794,26 @@ var Body = class {
11377
11794
  } else {
11378
11795
  this.fromJSON(shape);
11379
11796
  }
11797
+ this._recomputePseudoInertia();
11380
11798
  if (typeof this.onBodyUpdate === "function") {
11381
11799
  this.onBodyUpdate(this);
11382
11800
  }
11383
11801
  return this.shapes.length;
11384
11802
  }
11803
+ /**
11804
+ * Recompute the default `pseudoInertia` from the current bounds.
11805
+ * Uses the moment-of-inertia formula for a unit-mass rectangle —
11806
+ * `(width² + height²) / 12` — which gives a value that scales
11807
+ * sensibly with body size: a small body resists rotation less, a
11808
+ * large body more. Clamped to a minimum of 1 to keep divisions
11809
+ * well-defined even on degenerate 0-size bodies.
11810
+ * @ignore
11811
+ */
11812
+ _recomputePseudoInertia() {
11813
+ const w = this.bounds.width;
11814
+ const h = this.bounds.height;
11815
+ this.pseudoInertia = Math.max(1, (w * w + h * h) / 12);
11816
+ }
11385
11817
  /**
11386
11818
  * set the body vertices to the given one
11387
11819
  * @param {Vector2d[]} vertices - an array of me.Vector2d points defining a convex hull
@@ -11545,7 +11977,7 @@ var Body = class {
11545
11977
  this.vel.y -= projVel * ratio * overlapN.y;
11546
11978
  }
11547
11979
  }
11548
- if (overlap.y !== 0 && !this.ignoreGravity) {
11980
+ if (overlap.y !== 0 && !this.ignoreGravity && this.gravityScale !== 0) {
11549
11981
  const dir = this.falling === true ? 1 : this.jumping === true ? -1 : 0;
11550
11982
  this.falling = overlap.y >= dir;
11551
11983
  this.jumping = overlap.y <= -dir;
@@ -11691,6 +12123,13 @@ var Body = class {
11691
12123
  this.falling = this.vel.y * Math.sign(this.force.y) > 0;
11692
12124
  this.jumping = this.falling ? false : this.jumping;
11693
12125
  this.ancestor.pos.add(this.vel);
12126
+ if (this.angularVelocity !== 0 || this.angle !== 0) {
12127
+ if (this.angularDrag > 0 && this.angularVelocity !== 0) {
12128
+ this.angularVelocity *= 1 - this.angularDrag;
12129
+ }
12130
+ this.angle += this.angularVelocity * deltaTime;
12131
+ this._syncAngleTransform();
12132
+ }
11694
12133
  return this.vel.x !== 0 || this.vel.y !== 0;
11695
12134
  }
11696
12135
  /**
@@ -11749,6 +12188,7 @@ var Renderable = class _Renderable extends Rect {
11749
12188
  }
11750
12189
  this.currentTransform.identity();
11751
12190
  this.body = void 0;
12191
+ this.bodyDef = void 0;
11752
12192
  this.GUID = void 0;
11753
12193
  this.onVisibilityChange = void 0;
11754
12194
  this.alwaysUpdate = false;
@@ -12244,28 +12684,74 @@ var Renderable = class _Renderable extends Rect {
12244
12684
  this.isDirty = false;
12245
12685
  }
12246
12686
  /**
12247
- * onCollision callback, triggered in case of collision,
12248
- * when this renderable body is colliding with another one
12249
- * @param {ResponseObject} response - the collision response object
12687
+ * Legacy collision callback fires every frame this renderable body is
12688
+ * overlapping another body. Kept for backward compatibility with code
12689
+ * written against pre-19.5 melonJS; semantics are unchanged from the
12690
+ * 19.4 contract.
12691
+ *
12692
+ * **NOTE — `onCollision` is NOT equivalent to {@link Renderable.onCollisionActive}.**
12693
+ * The two handlers exist side by side and have intentionally different
12694
+ * contracts:
12695
+ *
12696
+ * | | `onCollision` (legacy) | `onCollisionActive` (modern) |
12697
+ * |---|---|---|
12698
+ * | Cadence for dynamic-dynamic pairs | 2× per frame per side | 1× per frame per side |
12699
+ * | `response.a` semantics | Fixed per pair (first body in detector call) | Always the receiver (`response.a === this`) |
12700
+ * | `response.b` semantics | Fixed per pair | Always the partner (`response.b === other`) |
12701
+ * | `response.normal` / `response.depth` | ✗ | ✓ — `normal.y < -0.7` = "push me up" |
12702
+ * | `return false` to skip push-out | ✓ (honored by SAT) | ✗ — use `bodyDef.isSensor` or `setSensor` instead |
12703
+ *
12704
+ * If you're writing new code, prefer `onCollisionActive`. Keep
12705
+ * `onCollision` only when its every-frame, return-false, fixed-`a`/`b`
12706
+ * semantics are what you want.
12707
+ *
12708
+ * @param {import("../physics/adapter.ts").CollisionResponse} response - the collision response object
12250
12709
  * @param {Renderable} other - the other renderable touching this one (a reference to response.a or response.b)
12251
- * @returns {boolean} true if the object should respond to the collision (its position and velocity will be corrected)
12710
+ * @returns {boolean} true if the object should respond to the collision (its position and velocity will be corrected); the return value is only honored by the builtin SAT adapter.
12252
12711
  * @example
12253
- * // collision handler
12712
+ * // legacy collision handler — note the receiver-side check on response.a
12254
12713
  * onCollision(response) {
12255
12714
  * if (response.b.body.collisionType === me.collision.types.ENEMY_OBJECT) {
12256
- * // makes the other object solid, by substracting the overlap vector to the current position
12257
12715
  * this.pos.sub(response.overlapV);
12258
12716
  * this.hurt();
12259
- * // not solid
12260
- * return false;
12717
+ * return false; // skip the SAT push-out
12261
12718
  * }
12262
- * // Make the object solid
12263
12719
  * return true;
12264
- * },
12720
+ * }
12265
12721
  */
12266
- onCollision() {
12267
- return false;
12722
+ onCollision(_response, _other) {
12268
12723
  }
12724
+ /*
12725
+ * Modern collision lifecycle hooks (`onCollisionStart`,
12726
+ * `onCollisionActive`, `onCollisionEnd`) are intentionally NOT
12727
+ * defined on the base class. Subclasses override the ones they
12728
+ * care about; the adapter's dispatch logic checks
12729
+ * `typeof renderable.onCollisionX === "function"` to know whether
12730
+ * to fire them. Defining base stubs would make every renderable
12731
+ * "look like" it implements every hook, which would in turn make
12732
+ * the `onCollisionActive supersedes onCollision` rule fire even
12733
+ * for legacy-only code.
12734
+ *
12735
+ * Documented surface (subclasses implement these as needed):
12736
+ *
12737
+ * onCollisionStart(response, other)
12738
+ * One-shot on first contact. Use for stomp bounces, pickups,
12739
+ * trigger entry. Receiver-symmetric (`response.a === this`,
12740
+ * `response.b === other`). Same contract on every adapter.
12741
+ *
12742
+ * onCollisionActive(response, other)
12743
+ * Every step while two bodies remain in contact. The modern
12744
+ * equivalent of `onCollision`, with different semantics:
12745
+ * - 1× per frame per side (vs `onCollision`'s 2× for dyn-dyn)
12746
+ * - `response.a === this`, `response.b === other`
12747
+ * - `response.normal` is the MTV of the receiver
12748
+ * - `return false` is NOT honored (use `bodyDef.isSensor`)
12749
+ * When defined, `onCollision` is suppressed on the same
12750
+ * renderable. Stomp idiom: `response.normal.y < -0.7`.
12751
+ *
12752
+ * onCollisionEnd(response, other)
12753
+ * One-shot when two bodies separate.
12754
+ */
12269
12755
  /**
12270
12756
  * Destroy function<br>
12271
12757
  * @ignore
@@ -12309,9 +12795,31 @@ var Renderable = class _Renderable extends Rect {
12309
12795
  }
12310
12796
  /**
12311
12797
  * OnDestroy Notification function<br>
12312
- * Called by engine before deleting the object
12798
+ * Called by engine before deleting the object. `Stage.destroy(app)`
12799
+ * forwards the active Application, so subclasses that wire teardown
12800
+ * against the app object can override as `onDestroyEvent(app)`.
12801
+ * @param {...*} _args - forwarded by `destroy(...args)`; Stage passes the Application instance
12313
12802
  */
12314
- onDestroyEvent() {
12803
+ onDestroyEvent(..._args) {
12804
+ }
12805
+ /**
12806
+ * Lifecycle hook fired by {@link Container} when this renderable is
12807
+ * added to a container that is part of the active scene graph.
12808
+ * Override to wire up input handlers, register external listeners,
12809
+ * or grab adapter references — `this.parentApp` is guaranteed to be
12810
+ * available here. Pair with {@link Renderable#onDeactivateEvent}.
12811
+ * @param {...*} _args - forwarded by `Container.addChild`
12812
+ */
12813
+ onActivateEvent(..._args) {
12814
+ }
12815
+ /**
12816
+ * Lifecycle hook fired by {@link Container} when this renderable is
12817
+ * removed from its container or its container is itself removed.
12818
+ * Override to release input handlers, unsubscribe from events, or
12819
+ * drop adapter references. Pair with {@link Renderable#onActivateEvent}.
12820
+ * @param {...*} _args - forwarded by `Container.removeChildNow`
12821
+ */
12822
+ onDeactivateEvent(..._args) {
12315
12823
  }
12316
12824
  };
12317
12825
 
@@ -12420,6 +12928,421 @@ function fetchData(url, responseType, settings = {}) {
12420
12928
  });
12421
12929
  }
12422
12930
 
12931
+ // src/loader/parsers/aseprite.js
12932
+ var ASE_MAGIC_FILE = 42464;
12933
+ var ASE_MAGIC_FRAME = 61946;
12934
+ var CHUNK_OLD_PALETTE_04 = 4;
12935
+ var CHUNK_OLD_PALETTE_11 = 17;
12936
+ var CHUNK_LAYER = 8196;
12937
+ var CHUNK_CEL = 8197;
12938
+ var CHUNK_PALETTE = 8217;
12939
+ var CHUNK_TAGS = 8216;
12940
+ var CHUNK_TILESET = 8227;
12941
+ var CEL_RAW_IMAGE = 0;
12942
+ var CEL_LINKED = 1;
12943
+ var CEL_COMPRESSED_IMAGE = 2;
12944
+ var CEL_COMPRESSED_TILEMAP = 3;
12945
+ var ERR_TILEMAP_MODE = "aseprite: this file uses Aseprite's tilemap mode (tileset chunk or tilemap-cel), which is not supported. Export the sprite to a regular PNG (File \u2192 Export Sprite Sheet) or flatten the tilemap layer to pixel layers.";
12946
+ var DEPTH_RGBA = 32;
12947
+ var DEPTH_GRAYSCALE = 16;
12948
+ var DEPTH_INDEXED = 8;
12949
+ var LAYER_FLAG_VISIBLE = 1;
12950
+ var TAG_DIRECTIONS = ["forward", "reverse", "pingpong", "pingpong_reverse"];
12951
+ var Reader = class {
12952
+ constructor(buffer, offset = 0) {
12953
+ this.view = new DataView(buffer);
12954
+ this.bytes = new Uint8Array(buffer);
12955
+ this.offset = offset;
12956
+ }
12957
+ u8() {
12958
+ return this.view.getUint8(this.offset++);
12959
+ }
12960
+ u16() {
12961
+ const v = this.view.getUint16(this.offset, true);
12962
+ this.offset += 2;
12963
+ return v;
12964
+ }
12965
+ i16() {
12966
+ const v = this.view.getInt16(this.offset, true);
12967
+ this.offset += 2;
12968
+ return v;
12969
+ }
12970
+ u32() {
12971
+ const v = this.view.getUint32(this.offset, true);
12972
+ this.offset += 4;
12973
+ return v;
12974
+ }
12975
+ i32() {
12976
+ const v = this.view.getInt32(this.offset, true);
12977
+ this.offset += 4;
12978
+ return v;
12979
+ }
12980
+ str() {
12981
+ const len = this.u16();
12982
+ const slice = this.bytes.subarray(this.offset, this.offset + len);
12983
+ this.offset += len;
12984
+ return new TextDecoder("utf-8").decode(slice);
12985
+ }
12986
+ skip(n) {
12987
+ this.offset += n;
12988
+ }
12989
+ slice(n) {
12990
+ const out = this.bytes.subarray(this.offset, this.offset + n);
12991
+ this.offset += n;
12992
+ return out;
12993
+ }
12994
+ };
12995
+ async function inflate(bytes) {
12996
+ const stream = new Response(bytes).body.pipeThrough(
12997
+ new DecompressionStream("deflate")
12998
+ );
12999
+ return new Uint8Array(await new Response(stream).arrayBuffer());
13000
+ }
13001
+ function decodeCelPixels(raw, w, h, depth, palette, transparentIndex) {
13002
+ const out = new Uint8ClampedArray(w * h * 4);
13003
+ switch (depth) {
13004
+ case DEPTH_RGBA:
13005
+ out.set(raw);
13006
+ break;
13007
+ case DEPTH_GRAYSCALE:
13008
+ for (let i = 0, j = 0; i < w * h; i++, j += 4) {
13009
+ const v = raw[i * 2];
13010
+ const a = raw[i * 2 + 1];
13011
+ out[j] = v;
13012
+ out[j + 1] = v;
13013
+ out[j + 2] = v;
13014
+ out[j + 3] = a;
13015
+ }
13016
+ break;
13017
+ case DEPTH_INDEXED:
13018
+ for (let i = 0, j = 0; i < w * h; i++, j += 4) {
13019
+ const idx = raw[i];
13020
+ if (idx === transparentIndex) {
13021
+ out[j] = 0;
13022
+ out[j + 1] = 0;
13023
+ out[j + 2] = 0;
13024
+ out[j + 3] = 0;
13025
+ } else {
13026
+ const p = palette[idx];
13027
+ if (p) {
13028
+ out[j] = p[0];
13029
+ out[j + 1] = p[1];
13030
+ out[j + 2] = p[2];
13031
+ out[j + 3] = p[3];
13032
+ }
13033
+ }
13034
+ }
13035
+ break;
13036
+ default:
13037
+ throw new Error("aseprite: unsupported color depth " + depth);
13038
+ }
13039
+ return out;
13040
+ }
13041
+ async function parseAsepriteFile(buffer) {
13042
+ const r = new Reader(buffer);
13043
+ r.u32();
13044
+ const magic = r.u16();
13045
+ if (magic !== ASE_MAGIC_FILE) {
13046
+ throw new Error(
13047
+ "aseprite: invalid file magic 0x" + magic.toString(16) + " (expected 0xA5E0)"
13048
+ );
13049
+ }
13050
+ const frameCount = r.u16();
13051
+ const width = r.u16();
13052
+ const height = r.u16();
13053
+ const depth = r.u16();
13054
+ r.u32();
13055
+ r.u16();
13056
+ r.u32();
13057
+ r.u32();
13058
+ const transparentIndex = r.u8();
13059
+ r.skip(3);
13060
+ r.u16();
13061
+ r.offset = 128;
13062
+ const palette = [];
13063
+ const layers = [];
13064
+ const tags = [];
13065
+ const frames = [];
13066
+ const ancestorVisible = [];
13067
+ for (let f = 0; f < frameCount; f++) {
13068
+ const frameStart = r.offset;
13069
+ const frameSize = r.u32();
13070
+ const fmagic = r.u16();
13071
+ if (fmagic !== ASE_MAGIC_FRAME) {
13072
+ throw new Error(
13073
+ "aseprite: invalid frame magic at frame " + f + " (got 0x" + fmagic.toString(16) + ")"
13074
+ );
13075
+ }
13076
+ let chunkCount = r.u16();
13077
+ const duration = r.u16();
13078
+ r.skip(2);
13079
+ const newChunks = r.u32();
13080
+ if (chunkCount === 65535) {
13081
+ chunkCount = newChunks;
13082
+ }
13083
+ const cels = [];
13084
+ for (let c = 0; c < chunkCount; c++) {
13085
+ const chunkStart = r.offset;
13086
+ const chunkSize = r.u32();
13087
+ const chunkType = r.u16();
13088
+ const chunkEnd = chunkStart + chunkSize;
13089
+ switch (chunkType) {
13090
+ case CHUNK_LAYER: {
13091
+ const flags = r.u16();
13092
+ r.u16();
13093
+ const childLevel = r.u16();
13094
+ r.u16();
13095
+ r.u16();
13096
+ r.u16();
13097
+ r.u8();
13098
+ r.skip(3);
13099
+ const name = r.str();
13100
+ const selfVisible = (flags & LAYER_FLAG_VISIBLE) !== 0;
13101
+ const parentVisible = childLevel === 0 ? true : ancestorVisible[childLevel - 1] !== false;
13102
+ const visible = selfVisible && parentVisible;
13103
+ ancestorVisible[childLevel] = visible;
13104
+ ancestorVisible.length = childLevel + 1;
13105
+ layers.push({ flags, name, visible });
13106
+ break;
13107
+ }
13108
+ case CHUNK_CEL: {
13109
+ const layer = r.u16();
13110
+ const x = r.i16();
13111
+ const y = r.i16();
13112
+ const opacity = r.u8();
13113
+ const celType = r.u16();
13114
+ r.skip(7);
13115
+ if (celType === CEL_RAW_IMAGE) {
13116
+ const cw = r.u16();
13117
+ const ch = r.u16();
13118
+ const pixelBytes = cw * ch * (depth / 8);
13119
+ const raw = r.slice(pixelBytes);
13120
+ cels.push({ layer, x, y, opacity, w: cw, h: ch, raw });
13121
+ } else if (celType === CEL_LINKED) {
13122
+ const linkFrame = r.u16();
13123
+ cels.push({ layer, x, y, opacity, linkFrame });
13124
+ } else if (celType === CEL_COMPRESSED_IMAGE) {
13125
+ const cw = r.u16();
13126
+ const ch = r.u16();
13127
+ const compressed = r.slice(chunkEnd - r.offset);
13128
+ const inflated = await inflate(compressed);
13129
+ cels.push({
13130
+ layer,
13131
+ x,
13132
+ y,
13133
+ opacity,
13134
+ w: cw,
13135
+ h: ch,
13136
+ raw: inflated
13137
+ });
13138
+ } else if (celType === CEL_COMPRESSED_TILEMAP) {
13139
+ throw new Error(ERR_TILEMAP_MODE);
13140
+ }
13141
+ break;
13142
+ }
13143
+ case CHUNK_PALETTE: {
13144
+ const size = r.u32();
13145
+ const first = r.u32();
13146
+ const last = r.u32();
13147
+ r.skip(8);
13148
+ for (let i = first; i <= last; i++) {
13149
+ const flags = r.u16();
13150
+ const cr = r.u8();
13151
+ const cg = r.u8();
13152
+ const cb = r.u8();
13153
+ const ca = r.u8();
13154
+ if (flags & 1) {
13155
+ r.str();
13156
+ }
13157
+ palette[i] = [cr, cg, cb, ca];
13158
+ }
13159
+ void size;
13160
+ break;
13161
+ }
13162
+ case CHUNK_OLD_PALETTE_04:
13163
+ case CHUNK_OLD_PALETTE_11: {
13164
+ const isLegacy6Bit = chunkType === CHUNK_OLD_PALETTE_11;
13165
+ if (palette.length === 0) {
13166
+ const packets = r.u16();
13167
+ let idx = 0;
13168
+ for (let p = 0; p < packets; p++) {
13169
+ idx += r.u8();
13170
+ let entries = r.u8();
13171
+ if (entries === 0) {
13172
+ entries = 256;
13173
+ }
13174
+ for (let e = 0; e < entries; e++) {
13175
+ let cr = r.u8();
13176
+ let cg = r.u8();
13177
+ let cb = r.u8();
13178
+ if (isLegacy6Bit) {
13179
+ cr = cr << 2 | cr >> 4;
13180
+ cg = cg << 2 | cg >> 4;
13181
+ cb = cb << 2 | cb >> 4;
13182
+ }
13183
+ palette[idx++] = [cr, cg, cb, 255];
13184
+ }
13185
+ }
13186
+ }
13187
+ break;
13188
+ }
13189
+ case CHUNK_TAGS: {
13190
+ const count = r.u16();
13191
+ r.skip(8);
13192
+ for (let t = 0; t < count; t++) {
13193
+ const from = r.u16();
13194
+ const to = r.u16();
13195
+ const dir = r.u8();
13196
+ const repeat = r.u16();
13197
+ r.skip(6);
13198
+ r.skip(3);
13199
+ r.skip(1);
13200
+ const name = r.str();
13201
+ tags.push({
13202
+ name,
13203
+ from,
13204
+ to,
13205
+ direction: TAG_DIRECTIONS[dir] || "forward",
13206
+ repeat
13207
+ });
13208
+ }
13209
+ break;
13210
+ }
13211
+ case CHUNK_TILESET:
13212
+ throw new Error(ERR_TILEMAP_MODE);
13213
+ default:
13214
+ break;
13215
+ }
13216
+ r.offset = chunkEnd;
13217
+ }
13218
+ frames.push({ duration, cels });
13219
+ r.offset = frameStart + frameSize;
13220
+ }
13221
+ return {
13222
+ width,
13223
+ height,
13224
+ depth,
13225
+ transparentIndex,
13226
+ palette,
13227
+ layers,
13228
+ tags,
13229
+ frames
13230
+ };
13231
+ }
13232
+ function composite(parsed) {
13233
+ const { width, height, depth, frames, layers, palette, transparentIndex } = parsed;
13234
+ const stripWidth = width * frames.length;
13235
+ const stripHeight = height;
13236
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(stripWidth, stripHeight) : Object.assign(document.createElement("canvas"), {
13237
+ width: stripWidth,
13238
+ height: stripHeight
13239
+ });
13240
+ const ctx = canvas.getContext("2d");
13241
+ ctx.imageSmoothingEnabled = false;
13242
+ for (let f = 0; f < frames.length; f++) {
13243
+ const frame = frames[f];
13244
+ const fx = f * width;
13245
+ const ordered = frame.cels.slice().sort((a, b) => {
13246
+ return a.layer - b.layer;
13247
+ });
13248
+ for (const cel of ordered) {
13249
+ const layer = layers[cel.layer];
13250
+ if (layer && layer.visible === false) {
13251
+ continue;
13252
+ }
13253
+ let source = cel;
13254
+ if (cel.linkFrame !== void 0) {
13255
+ source = frames[cel.linkFrame]?.cels.find((c) => {
13256
+ return c.layer === cel.layer;
13257
+ });
13258
+ if (!source || !source.raw) {
13259
+ continue;
13260
+ }
13261
+ }
13262
+ const rgba = decodeCelPixels(
13263
+ source.raw,
13264
+ source.w,
13265
+ source.h,
13266
+ depth,
13267
+ palette,
13268
+ transparentIndex
13269
+ );
13270
+ const img = new ImageData(rgba, source.w, source.h);
13271
+ const tmp = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(source.w, source.h) : Object.assign(document.createElement("canvas"), {
13272
+ width: source.w,
13273
+ height: source.h
13274
+ });
13275
+ tmp.getContext("2d").putImageData(img, 0, 0);
13276
+ if (cel.opacity !== 255) {
13277
+ ctx.globalAlpha = cel.opacity / 255;
13278
+ }
13279
+ ctx.drawImage(tmp, fx + cel.x, cel.y);
13280
+ if (cel.opacity !== 255) {
13281
+ ctx.globalAlpha = 1;
13282
+ }
13283
+ }
13284
+ }
13285
+ return canvas;
13286
+ }
13287
+ function buildAtlasJSON(parsed, imageName) {
13288
+ const { width, height, frames, tags } = parsed;
13289
+ const out = {};
13290
+ for (let i = 0; i < frames.length; i++) {
13291
+ out[String(i)] = {
13292
+ frame: { x: i * width, y: 0, w: width, h: height },
13293
+ rotated: false,
13294
+ trimmed: false,
13295
+ spriteSourceSize: { x: 0, y: 0, w: width, h: height },
13296
+ sourceSize: { w: width, h: height },
13297
+ duration: frames[i].duration
13298
+ };
13299
+ }
13300
+ return {
13301
+ frames: out,
13302
+ meta: {
13303
+ app: "http://www.aseprite.org/",
13304
+ version: "binary",
13305
+ image: imageName,
13306
+ format: "RGBA8888",
13307
+ size: { w: width * frames.length, h: height },
13308
+ scale: "1",
13309
+ frameTags: tags
13310
+ }
13311
+ };
13312
+ }
13313
+ async function canvasToBitmap(canvas) {
13314
+ if (typeof canvas.transferToImageBitmap === "function" && typeof createImageBitmap === "function") {
13315
+ return canvas.transferToImageBitmap();
13316
+ }
13317
+ if (typeof createImageBitmap === "function") {
13318
+ return await createImageBitmap(canvas);
13319
+ }
13320
+ return canvas;
13321
+ }
13322
+ async function parseAseprite(buffer, imageName = "default") {
13323
+ const parsed = await parseAsepriteFile(buffer);
13324
+ const canvas = composite(parsed);
13325
+ const bitmap = await canvasToBitmap(canvas);
13326
+ const json = buildAtlasJSON(parsed, imageName);
13327
+ return { bitmap, json };
13328
+ }
13329
+ function preloadAseprite(data2, onload2, onerror, settings) {
13330
+ fetchData(data2.src, "arrayBuffer", settings).then(async (buffer) => {
13331
+ const { bitmap, json } = await parseAseprite(buffer, data2.name);
13332
+ imgList[data2.name] = bitmap;
13333
+ jsonList[data2.name] = json;
13334
+ if (typeof onload2 === "function") {
13335
+ onload2();
13336
+ }
13337
+ }).catch((error) => {
13338
+ console.error(`Failed to parse aseprite asset "${data2.name}":`, error);
13339
+ if (typeof onerror === "function") {
13340
+ onerror(error);
13341
+ }
13342
+ });
13343
+ return 1;
13344
+ }
13345
+
12423
13346
  // src/loader/parsers/binary.js
12424
13347
  function preloadBinary(data2, onload2, onerror, settings) {
12425
13348
  fetchData(data2.src, "arrayBuffer", settings).then((response) => {
@@ -13512,6 +14435,13 @@ var isStringArray = (value) => {
13512
14435
  function deferredRemove(child, keepalive) {
13513
14436
  this.removeChildNow(child, keepalive);
13514
14437
  }
14438
+ function registerChildBody(child, worldContainer) {
14439
+ if (child.bodyDef) {
14440
+ worldContainer.adapter.addBody(child, child.bodyDef);
14441
+ } else if (child.body instanceof Body) {
14442
+ worldContainer.addBody(child.body);
14443
+ }
14444
+ }
13515
14445
  var globalFloatingCounter = 0;
13516
14446
  var Container = class _Container extends Renderable {
13517
14447
  /**
@@ -13536,7 +14466,6 @@ var Container = class _Container extends Renderable {
13536
14466
  };
13537
14467
  this.enableChildBoundsUpdate = false;
13538
14468
  this.backgroundColor = colorPool.get(0, 0, 0, 0);
13539
- this.drawCount = 0;
13540
14469
  this.autoTransform = true;
13541
14470
  this.isKinematic = false;
13542
14471
  this.anchorPoint.set(0, 0);
@@ -13601,7 +14530,7 @@ var Container = class _Container extends Renderable {
13601
14530
  throw new Error(`${String(child)} is not an instance of Renderable`);
13602
14531
  }
13603
14532
  if (child.ancestor instanceof _Container) {
13604
- child.ancestor.removeChildNow(child);
14533
+ child.ancestor.removeChildNow(child, true);
13605
14534
  } else {
13606
14535
  child.GUID = createGUID(child.id);
13607
14536
  }
@@ -13619,25 +14548,21 @@ var Container = class _Container extends Renderable {
13619
14548
  if (this.autoSort === true) {
13620
14549
  this.sort();
13621
14550
  }
13622
- if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
13623
- child.onActivateEvent();
13624
- }
13625
- if (this.enableChildBoundsUpdate === true) {
13626
- this.updateBounds();
13627
- }
13628
14551
  if (this.isAttachedToRoot()) {
13629
14552
  const worldContainer = this.getRootAncestor();
13630
- if (child.body instanceof Body) {
13631
- worldContainer.addBody(child.body);
13632
- }
14553
+ registerChildBody(child, worldContainer);
13633
14554
  if (child instanceof _Container) {
13634
14555
  child.forEach((cchild) => {
13635
- if (cchild.body instanceof Body) {
13636
- worldContainer.addBody(cchild.body);
13637
- }
14556
+ registerChildBody(cchild, worldContainer);
13638
14557
  });
13639
14558
  }
13640
14559
  }
14560
+ if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
14561
+ child.onActivateEvent();
14562
+ }
14563
+ if (this.enableChildBoundsUpdate === true) {
14564
+ this.updateBounds();
14565
+ }
13641
14566
  this.isDirty = true;
13642
14567
  this.onChildChange.call(this, this.getChildren().length - 1);
13643
14568
  return child;
@@ -13655,7 +14580,7 @@ var Container = class _Container extends Renderable {
13655
14580
  }
13656
14581
  if (index >= 0 && index <= this.getChildren().length) {
13657
14582
  if (child.ancestor instanceof _Container) {
13658
- child.ancestor.removeChildNow(child);
14583
+ child.ancestor.removeChildNow(child, true);
13659
14584
  } else {
13660
14585
  child.GUID = createGUID();
13661
14586
  }
@@ -13665,25 +14590,21 @@ var Container = class _Container extends Renderable {
13665
14590
  child.floating = false;
13666
14591
  }
13667
14592
  child.updateBounds();
13668
- if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
13669
- child.onActivateEvent();
13670
- }
13671
- if (this.enableChildBoundsUpdate === true) {
13672
- this.updateBounds();
13673
- }
13674
14593
  if (this.isAttachedToRoot()) {
13675
14594
  const worldContainer = this.getRootAncestor();
13676
- if (child.body instanceof Body) {
13677
- worldContainer.addBody(child.body);
13678
- }
14595
+ registerChildBody(child, worldContainer);
13679
14596
  if (child instanceof _Container) {
13680
14597
  child.forEach((cchild) => {
13681
- if (cchild.body instanceof Body) {
13682
- worldContainer.addBody(cchild.body);
13683
- }
14598
+ registerChildBody(cchild, worldContainer);
13684
14599
  });
13685
14600
  }
13686
14601
  }
14602
+ if (typeof child.onActivateEvent === "function" && this.isAttachedToRoot()) {
14603
+ child.onActivateEvent();
14604
+ }
14605
+ if (this.enableChildBoundsUpdate === true) {
14606
+ this.updateBounds();
14607
+ }
13687
14608
  this.isDirty = true;
13688
14609
  this.onChildChange.call(this, index);
13689
14610
  return child;
@@ -13928,8 +14849,11 @@ var Container = class _Container extends Renderable {
13928
14849
  }
13929
14850
  /**
13930
14851
  * @ignore
14852
+ * @param {...*} _args - reserved; widens the signature so subclass
14853
+ * overrides like `onActivateEvent(app)` remain structurally
14854
+ * assignable to the base Container/Renderable type.
13931
14855
  */
13932
- onActivateEvent() {
14856
+ onActivateEvent(..._args) {
13933
14857
  this.forEach((child) => {
13934
14858
  if (typeof child.onActivateEvent === "function") {
13935
14859
  child.onActivateEvent();
@@ -13961,10 +14885,23 @@ var Container = class _Container extends Renderable {
13961
14885
  if (typeof child.onDeactivateEvent === "function") {
13962
14886
  child.onDeactivateEvent();
13963
14887
  }
13964
- if (child.body instanceof Body) {
13965
- const root = this.getRootAncestor();
13966
- if (root) {
13967
- root.removeBody(child.body);
14888
+ const root = this.getRootAncestor();
14889
+ if (root?.broadphase) {
14890
+ if (typeof child.addChild === "function") {
14891
+ root.broadphase.removeContainer(child);
14892
+ }
14893
+ root.broadphase.remove(child);
14894
+ }
14895
+ if (child.body) {
14896
+ const world = root;
14897
+ if (world?.adapter?.removeBody) {
14898
+ world.adapter.removeBody(child);
14899
+ } else if (child.body instanceof Body) {
14900
+ root?.removeBody?.(child.body);
14901
+ } else {
14902
+ console.warn(
14903
+ "melonJS: removeChildNow could not clean up an adapter-specific body \u2014 the container is not attached to a world with an adapter. The body remains in the physics engine."
14904
+ );
13968
14905
  }
13969
14906
  }
13970
14907
  if (!keepalive) {
@@ -14074,8 +15011,11 @@ var Container = class _Container extends Renderable {
14074
15011
  }
14075
15012
  /**
14076
15013
  * @ignore
15014
+ * @param {...*} _args - reserved; widens the signature so subclass
15015
+ * overrides like `onDeactivateEvent(app)` remain structurally
15016
+ * assignable to the base Container/Renderable type.
14077
15017
  */
14078
- onDeactivateEvent() {
15018
+ onDeactivateEvent(..._args) {
14079
15019
  this.forEach((child) => {
14080
15020
  if (typeof child.onDeactivateEvent === "function") {
14081
15021
  child.onDeactivateEvent();
@@ -14160,7 +15100,6 @@ var Container = class _Container extends Renderable {
14160
15100
  */
14161
15101
  draw(renderer2, viewport) {
14162
15102
  const bounds = this.getBounds();
14163
- this.drawCount = 0;
14164
15103
  renderer2.translate(this.pos.x, this.pos.y);
14165
15104
  if (this.root === false && this.clipping === true && bounds.isFinite() === true && Number.isFinite(this.width) === true && Number.isFinite(this.height) === true) {
14166
15105
  renderer2.clipRect(0, 0, this.width, this.height);
@@ -14192,7 +15131,6 @@ var Container = class _Container extends Renderable {
14192
15131
  }
14193
15132
  renderer2.restore();
14194
15133
  }
14195
- this.drawCount++;
14196
15134
  }
14197
15135
  }
14198
15136
  }
@@ -14504,16 +15442,14 @@ var CanvasRenderTarget = class extends RenderTarget {
14504
15442
  * @ignore
14505
15443
  */
14506
15444
  destroy(renderer2) {
14507
- if (renderer2 && typeof renderer2.gl !== "undefined" && typeof this.glTextureUnit !== "undefined") {
15445
+ if (renderer2 && typeof renderer2.gl !== "undefined" && this.canvas && renderer2.cache.has(this.canvas)) {
14508
15446
  renderer2.setBatcher("quad");
14509
- renderer2.currentBatcher.deleteTexture2D(
14510
- renderer2.currentBatcher.getTexture2D(this.glTextureUnit)
14511
- );
14512
- this.glTextureUnit = void 0;
15447
+ renderer2.currentBatcher.deleteTexture2D(renderer2.cache.get(this.canvas));
14513
15448
  }
14514
15449
  if (renderer2) {
14515
15450
  renderer2.cache.delete(this.canvas);
14516
15451
  }
15452
+ this.glTextureUnit = void 0;
14517
15453
  this.context = void 0;
14518
15454
  this.canvas = void 0;
14519
15455
  }
@@ -15512,6 +16448,7 @@ var Renderer = class {
15512
16448
  this.currentColor = this.renderState.currentColor;
15513
16449
  this.currentTint = this.renderState.currentTint;
15514
16450
  this.currentScissor = this.renderState.currentScissor;
16451
+ this.lineWidth = 1;
15515
16452
  this.maskLevel = 0;
15516
16453
  this.projectionMatrix = new Matrix3d();
15517
16454
  this.uvOffset = 0;
@@ -16059,6 +16996,128 @@ var Renderer = class {
16059
16996
  context.drawImage(src, 0, 0);
16060
16997
  return canvasTexture.canvas;
16061
16998
  }
16999
+ /**
17000
+ * Push the current transform / alpha / clip state onto an internal
17001
+ * stack. Pair with {@link Renderer#restore}. Implemented by subclasses.
17002
+ */
17003
+ save() {
17004
+ }
17005
+ /**
17006
+ * Pop the topmost saved state from the stack, restoring transform,
17007
+ * alpha and clip. Pair with {@link Renderer#save}. Implemented by
17008
+ * subclasses.
17009
+ */
17010
+ restore() {
17011
+ }
17012
+ /**
17013
+ * Translate the current transform by `(x, y)`. Implemented by subclasses.
17014
+ * @param {number} x - horizontal translation in pixels
17015
+ * @param {number} y - vertical translation in pixels
17016
+ */
17017
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17018
+ translate(x, y) {
17019
+ }
17020
+ /**
17021
+ * Rotate the current transform by `angle` (radians). Implemented by subclasses.
17022
+ * @param {number} angle - rotation angle in radians
17023
+ */
17024
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17025
+ rotate(angle) {
17026
+ }
17027
+ /**
17028
+ * Scale the current transform by `(x, y)`. Implemented by subclasses.
17029
+ * @param {number} x - horizontal scale factor
17030
+ * @param {number} y - vertical scale factor
17031
+ */
17032
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17033
+ scale(x, y) {
17034
+ }
17035
+ /**
17036
+ * Set the renderer global alpha (0..1) for subsequent draw calls.
17037
+ * Implemented by subclasses.
17038
+ * @param {number} alpha - opacity value in the [0..1] range
17039
+ */
17040
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17041
+ setGlobalAlpha(alpha2) {
17042
+ }
17043
+ /**
17044
+ * Fill an axis-aligned rectangle with the current color.
17045
+ * Implemented by subclasses.
17046
+ * @param {number} x - left edge in viewport pixels
17047
+ * @param {number} y - top edge in viewport pixels
17048
+ * @param {number} width - rectangle width
17049
+ * @param {number} height - rectangle height
17050
+ */
17051
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17052
+ fillRect(x, y, width, height) {
17053
+ }
17054
+ /**
17055
+ * Stroke (outline) an axis-aligned rectangle with the current color
17056
+ * at the current {@link Renderer#lineWidth}. Implemented by subclasses.
17057
+ * @param {number} x - left edge in viewport pixels
17058
+ * @param {number} y - top edge in viewport pixels
17059
+ * @param {number} width - rectangle width
17060
+ * @param {number} height - rectangle height
17061
+ * @param {boolean} [fill=false] - also fill the rectangle
17062
+ */
17063
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17064
+ strokeRect(x, y, width, height, fill) {
17065
+ }
17066
+ /**
17067
+ * Fill an axis-aligned ellipse with the current color.
17068
+ * Implemented by subclasses.
17069
+ * @param {number} x - ellipse center X in viewport pixels
17070
+ * @param {number} y - ellipse center Y in viewport pixels
17071
+ * @param {number} w - horizontal radius
17072
+ * @param {number} h - vertical radius
17073
+ */
17074
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17075
+ fillEllipse(x, y, w, h) {
17076
+ }
17077
+ /**
17078
+ * Stroke (outline) an axis-aligned ellipse with the current color
17079
+ * at the current {@link Renderer#lineWidth}. Implemented by subclasses.
17080
+ * @param {number} x - ellipse center X in viewport pixels
17081
+ * @param {number} y - ellipse center Y in viewport pixels
17082
+ * @param {number} w - horizontal radius
17083
+ * @param {number} h - vertical radius
17084
+ * @param {boolean} [fill=false] - also fill the ellipse
17085
+ */
17086
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17087
+ strokeEllipse(x, y, w, h, fill) {
17088
+ }
17089
+ /**
17090
+ * Draw a straight line between two points with the current color
17091
+ * at the current {@link Renderer#lineWidth}. Implemented by subclasses.
17092
+ * @param {number} startX - start X in viewport pixels
17093
+ * @param {number} startY - start Y in viewport pixels
17094
+ * @param {number} endX - end X in viewport pixels
17095
+ * @param {number} endY - end Y in viewport pixels
17096
+ */
17097
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17098
+ strokeLine(startX, startY, endX, endY) {
17099
+ }
17100
+ /**
17101
+ * Draw an image or sub-image into the framebuffer.
17102
+ * Implemented by subclasses (`CanvasRenderer` / `WebGLRenderer`).
17103
+ *
17104
+ * The 9-argument signature shown here is the canonical form used
17105
+ * throughout the engine; the concrete renderers also accept the
17106
+ * shorter `drawImage(image, dx, dy)` and `drawImage(image, dx, dy,
17107
+ * dWidth, dHeight)` variants from the HTML Canvas2D spec.
17108
+ * @param {HTMLImageElement|SVGImageElement|HTMLVideoElement|HTMLCanvasElement|ImageBitmap|OffscreenCanvas|VideoFrame} image - the source image
17109
+ * @param {number} sx - source rectangle left
17110
+ * @param {number} sy - source rectangle top
17111
+ * @param {number} sw - source rectangle width
17112
+ * @param {number} sh - source rectangle height
17113
+ * @param {number} dx - destination left
17114
+ * @param {number} dy - destination top
17115
+ * @param {number} dw - destination width
17116
+ * @param {number} dh - destination height
17117
+ */
17118
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
17119
+ drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) {
17120
+ }
16062
17121
  /**
16063
17122
  * A mask limits rendering elements to the shape and position of the given mask object.
16064
17123
  * So, if the renderable is larger than the mask, only the intersecting part of the renderable will be visible.
@@ -16161,7 +17220,7 @@ var ArrayMultimap = class {
16161
17220
  };
16162
17221
 
16163
17222
  // src/video/texture/parser/aseprite.js
16164
- function parseAseprite(data2, textureAtlas) {
17223
+ function parseAseprite2(data2, textureAtlas) {
16165
17224
  const atlas = {};
16166
17225
  const frames = data2.frames;
16167
17226
  for (const name in frames) {
@@ -16419,7 +17478,7 @@ var TextureAtlas = class {
16419
17478
  } else {
16420
17479
  this.atlases.set(
16421
17480
  atlas.meta.image || "default",
16422
- parseAseprite(atlas, this)
17481
+ parseAseprite2(atlas, this)
16423
17482
  );
16424
17483
  }
16425
17484
  break;
@@ -20553,6 +21612,65 @@ var TMXTileset = class {
20553
21612
  }
20554
21613
  if (!this.isCollection) {
20555
21614
  this._initAtlas(tileset);
21615
+ this._applyAsepriteFrameTags(tileset);
21616
+ }
21617
+ }
21618
+ /**
21619
+ * If `tileset.image` was loaded via the .aseprite binary parser, walk
21620
+ * its `meta.frameTags` and synthesize per-tile animations. Each tag
21621
+ * `{ from, to }` becomes an animation on tile `from` that cycles through
21622
+ * tiles `[from..to]` using each frame's `duration`.
21623
+ *
21624
+ * Throws on tag configurations that don't map to Tiled's animation model:
21625
+ * non-forward direction (Tiled animations are forward-only) and non-zero
21626
+ * repeat counts (Tiled animations loop forever).
21627
+ * @ignore
21628
+ */
21629
+ _applyAsepriteFrameTags(tileset) {
21630
+ const atlas = getJSON(getBasename("" + tileset.image));
21631
+ if (!atlas || !atlas.meta || typeof atlas.meta.app !== "string") {
21632
+ return;
21633
+ }
21634
+ if (!atlas.meta.app.includes("aseprite")) {
21635
+ return;
21636
+ }
21637
+ const tags = atlas.meta.frameTags;
21638
+ if (!Array.isArray(tags) || tags.length === 0) {
21639
+ return;
21640
+ }
21641
+ const frames = atlas.meta.frames || atlas.frames || {};
21642
+ for (const tag of tags) {
21643
+ if (tag.direction !== "forward") {
21644
+ throw new Error(
21645
+ `melonJS: aseprite tag "${tag.name}" on tileset '${this.name}' uses direction "${tag.direction}" \u2014 Tiled tile animations only support forward playback. Re-export the tag as "forward" in Aseprite.`
21646
+ );
21647
+ }
21648
+ if (typeof tag.repeat === "number" && tag.repeat !== 0) {
21649
+ throw new Error(
21650
+ `melonJS: aseprite tag "${tag.name}" on tileset '${this.name}' has a finite repeat count (${tag.repeat}) \u2014 Tiled tile animations only support infinite looping. Set the tag's repeat to "\u221E" in Aseprite.`
21651
+ );
21652
+ }
21653
+ const anim = [];
21654
+ for (let f = tag.from; f <= tag.to; f++) {
21655
+ const frame = frames[String(f)] || frames[f];
21656
+ anim.push({
21657
+ tileid: f,
21658
+ duration: frame && typeof frame.duration === "number" ? frame.duration : 100
21659
+ });
21660
+ }
21661
+ if (anim.length === 0) {
21662
+ continue;
21663
+ }
21664
+ if (this.animations.has(tag.from)) {
21665
+ continue;
21666
+ }
21667
+ this.isAnimated = true;
21668
+ this.animations.set(tag.from, {
21669
+ dt: 0,
21670
+ idx: 0,
21671
+ frames: anim,
21672
+ cur: anim[0]
21673
+ });
20556
21674
  }
20557
21675
  }
20558
21676
  /**
@@ -21236,9 +22354,14 @@ var TMXTileMap = class {
21236
22354
  settings.tint = parseTintColor(settings.tintcolor);
21237
22355
  }
21238
22356
  const obj = createTMXObject(settings, this);
21239
- if (isCollisionGroup && !settings.name && obj.body) {
21240
- obj.body.collisionType = collision.types.WORLD_SHAPE;
21241
- obj.body.isStatic = true;
22357
+ if (isCollisionGroup && !settings.name) {
22358
+ if (obj.bodyDef) {
22359
+ obj.bodyDef.collisionType = collision.types.WORLD_SHAPE;
22360
+ obj.bodyDef.type = "static";
22361
+ } else if (obj.body) {
22362
+ obj.body.collisionType = collision.types.WORLD_SHAPE;
22363
+ obj.body.isStatic = true;
22364
+ }
21242
22365
  }
21243
22366
  if (obj.isRenderable === true && !(settings instanceof TMXLayer)) {
21244
22367
  if (!settings.visible) {
@@ -21657,6 +22780,7 @@ function setBaseURL(type, url = "./") {
21657
22780
  baseURL["tmx"] = url;
21658
22781
  baseURL["tsx"] = url;
21659
22782
  baseURL["fontface"] = url;
22783
+ baseURL["aseprite"] = url;
21660
22784
  }
21661
22785
  }
21662
22786
  var onload;
@@ -21679,6 +22803,7 @@ function initParsers() {
21679
22803
  setParser("video", preloadVideo);
21680
22804
  setParser("obj", preloadOBJ);
21681
22805
  setParser("mtl", preloadMTL);
22806
+ setParser("aseprite", preloadAseprite);
21682
22807
  parserInitialized = true;
21683
22808
  }
21684
22809
  function completeLoading(onloadcb) {
@@ -21852,6 +22977,16 @@ function unload2(asset) {
21852
22977
  }
21853
22978
  delete mtlList[asset.name];
21854
22979
  return true;
22980
+ case "aseprite": {
22981
+ const hadImage = asset.name in imgList;
22982
+ const hadJson = asset.name in jsonList;
22983
+ if (!hadImage && !hadJson) {
22984
+ return false;
22985
+ }
22986
+ delete imgList[asset.name];
22987
+ delete jsonList[asset.name];
22988
+ return true;
22989
+ }
21855
22990
  default:
21856
22991
  throw new Error(
21857
22992
  "unload : unknown or invalid resource type : " + asset.type
@@ -23799,7 +24934,6 @@ var Camera2d = class extends Renderable {
23799
24934
  }
23800
24935
  }
23801
24936
  /** @ignore */
23802
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
23803
24937
  updateTarget(_dt) {
23804
24938
  if (this.target) {
23805
24939
  targetV.setV(this.pos);
@@ -24528,7 +25662,7 @@ function _endFreeze() {
24528
25662
  const wasOwnedPause = _freezeStartedPause;
24529
25663
  _freezeStartedPause = false;
24530
25664
  if (wasOwnedPause) {
24531
- state.resume(_freezeMusic);
25665
+ state2.resume(_freezeMusic);
24532
25666
  }
24533
25667
  _resolveFreezeWaiters();
24534
25668
  }
@@ -24573,19 +25707,19 @@ function _switchState(stateId) {
24573
25707
  }
24574
25708
  var _app;
24575
25709
  once(BOOT, () => {
24576
- state.set(state.LOADING, new loadingscreen_default());
24577
- state.set(state.DEFAULT, new Stage());
25710
+ state2.set(state2.LOADING, new loadingscreen_default());
25711
+ state2.set(state2.DEFAULT, new Stage());
24578
25712
  on(GAME_INIT, (app) => {
24579
25713
  _app = app;
24580
25714
  });
24581
25715
  once(VIDEO_INIT, () => {
24582
- state.change(state.DEFAULT, true);
25716
+ state2.change(state2.DEFAULT, true);
24583
25717
  });
24584
25718
  on(BLUR, () => {
24585
25719
  _cancelFreeze();
24586
25720
  });
24587
25721
  });
24588
- var state = {
25722
+ var state2 = {
24589
25723
  /**
24590
25724
  * default state ID for Loading Stage
24591
25725
  */
@@ -24875,7 +26009,7 @@ var state = {
24875
26009
  const onComplete = () => {
24876
26010
  defer(
24877
26011
  _switchState,
24878
- state,
26012
+ state2,
24879
26013
  stateId
24880
26014
  );
24881
26015
  };
@@ -24961,7 +26095,7 @@ var state = {
24961
26095
  return _state === stateId;
24962
26096
  }
24963
26097
  };
24964
- var state_default = state;
26098
+ var state_default = state2;
24965
26099
 
24966
26100
  // src/system/timer.ts
24967
26101
  var Timer = class {
@@ -25109,10 +26243,17 @@ var Timer = class {
25109
26243
  return this.delta;
25110
26244
  }
25111
26245
  /**
25112
- * compute the actual frame time and fps rate
26246
+ * update
25113
26247
  * @ignore
25114
26248
  */
25115
- countFPS() {
26249
+ update(time) {
26250
+ this.last = this.now;
26251
+ this.now = time;
26252
+ this.delta = this.now - this.last;
26253
+ if (this.delta < 0) {
26254
+ this.delta = 0;
26255
+ }
26256
+ this.tick = this.delta > this.minstep && this.interpolation ? this.delta / this.step : 1;
25116
26257
  this.framecount++;
25117
26258
  this.framedelta += this.delta;
25118
26259
  if (this.framecount % 10 === 0) {
@@ -25124,19 +26265,6 @@ var Timer = class {
25124
26265
  this.framedelta = 0;
25125
26266
  this.framecount = 0;
25126
26267
  }
25127
- }
25128
- /**
25129
- * update
25130
- * @ignore
25131
- */
25132
- update(time) {
25133
- this.last = this.now;
25134
- this.now = time;
25135
- this.delta = this.now - this.last;
25136
- if (this.delta < 0) {
25137
- this.delta = 0;
25138
- }
25139
- this.tick = this.delta > this.minstep && this.interpolation ? this.delta / this.step : 1;
25140
26268
  this.updateTimers();
25141
26269
  }
25142
26270
  /**
@@ -26075,14 +27203,24 @@ function createShapeObject(settings) {
26075
27203
  settings.height
26076
27204
  );
26077
27205
  const shape = getDefaultShape(settings);
27206
+ if (!shape || Array.isArray(shape) && shape.length === 0) {
27207
+ throw new Error(
27208
+ `melonJS: TMX object (id=${settings.id}, name=${settings.name ?? ""}, type=${settings.type ?? ""}) has no usable collision shape. Check the object's geometry in Tiled.`
27209
+ );
27210
+ }
26078
27211
  obj.anchorPoint.set(0, 0);
26079
27212
  obj.name = settings.name;
26080
27213
  obj.type = settings.type;
26081
27214
  obj.class = settings.class || settings.type;
26082
27215
  obj.id = settings.id;
26083
- obj.body = new Body(obj, shape);
26084
- obj.body.setStatic(true);
26085
- obj.resize(obj.body.getBounds().width, obj.body.getBounds().height);
27216
+ obj.bodyDef = {
27217
+ type: "static",
27218
+ shapes: Array.isArray(shape) ? shape : [shape]
27219
+ };
27220
+ const bounds = boundsPool.get();
27221
+ bounds.addShapes(obj.bodyDef.shapes, true);
27222
+ obj.resize(bounds.width, bounds.height);
27223
+ boundsPool.release(bounds);
26086
27224
  obj.pos.z = settings.z;
26087
27225
  return obj;
26088
27226
  }
@@ -26909,8 +28047,10 @@ function createTextObject(settings) {
26909
28047
  function createTileObject(settings) {
26910
28048
  const shape = getDefaultShape(settings);
26911
28049
  const obj = settings.tile.getRenderable(settings);
26912
- obj.body = new Body(obj, shape);
26913
- obj.body.setStatic(true);
28050
+ obj.bodyDef = {
28051
+ type: "static",
28052
+ shapes: Array.isArray(shape) ? shape : [shape]
28053
+ };
26914
28054
  obj.pos.setMuted(settings.x, settings.y, settings.z);
26915
28055
  return obj;
26916
28056
  }
@@ -27217,10 +28357,14 @@ var Collectable = class extends Sprite {
27217
28357
  vector2dPool.get(this.width, this.height)
27218
28358
  ]);
27219
28359
  }
27220
- this.body = new Body(this, shape);
27221
- this.body.collisionType = collision.types.COLLECTABLE_OBJECT;
27222
- this.body.setCollisionMask(collision.types.PLAYER_OBJECT);
27223
- this.body.setStatic(true);
28360
+ this.bodyDef = {
28361
+ type: "static",
28362
+ shapes: Array.isArray(shape) ? shape : [shape],
28363
+ collisionType: collision.types.COLLECTABLE_OBJECT,
28364
+ // by default only collides with PLAYER_OBJECT
28365
+ collisionMask: collision.types.PLAYER_OBJECT,
28366
+ isSensor: true
28367
+ };
27224
28368
  if (settings.anchorPoint) {
27225
28369
  this.anchorPoint.set(settings.anchorPoint.x, settings.anchorPoint.y);
27226
28370
  } else {
@@ -28182,11 +29326,17 @@ var Trigger = class extends Renderable {
28182
29326
  vector2dPool.get(this.width, this.height)
28183
29327
  ]);
28184
29328
  }
28185
- this.body = new Body(this, shape);
28186
- this.body.collisionType = collision.types.ACTION_OBJECT;
28187
- this.body.setCollisionMask(collision.types.PLAYER_OBJECT);
28188
- this.body.setStatic(true);
28189
- this.resize(this.body.getBounds().width, this.body.getBounds().height);
29329
+ this.bodyDef = {
29330
+ type: "static",
29331
+ shapes: Array.isArray(shape) ? shape : [shape],
29332
+ collisionType: collision.types.ACTION_OBJECT,
29333
+ collisionMask: collision.types.PLAYER_OBJECT,
29334
+ isSensor: true
29335
+ };
29336
+ const bounds = boundsPool.get();
29337
+ bounds.addShapes(this.bodyDef.shapes, true);
29338
+ this.resize(bounds.width, bounds.height);
29339
+ boundsPool.release(bounds);
28190
29340
  }
28191
29341
  /**
28192
29342
  * @ignore
@@ -28277,21 +29427,20 @@ var Trigger = class extends Renderable {
28277
29427
  }
28278
29428
  }
28279
29429
  /**
28280
- * onCollision callback, triggered in case of collision with this trigger
28281
- * @param {ResponseObject} response - the collision response object
28282
- * @param {Renderable} other - the other renderable touching this one (a reference to response.a or response.b)
28283
- * @returns {boolean} true if the object should respond to the collision (its position and velocity will be corrected)
29430
+ * Fire the trigger when an entity first enters the trigger zone.
29431
+ * Using `onCollisionStart` rather than `onCollision` so the event
29432
+ * runs exactly once per entry under the legacy alias this would
29433
+ * fire every frame the entity was inside the trigger.
28284
29434
  */
28285
- onCollision() {
29435
+ onCollisionStart() {
28286
29436
  if (this.name === "Trigger") {
28287
29437
  this.triggerEvent();
28288
29438
  }
28289
- return false;
28290
29439
  }
28291
29440
  };
28292
29441
 
28293
29442
  // src/version.ts
28294
- var version = "19.4.0";
29443
+ var version = "19.5.0";
28295
29444
 
28296
29445
  // src/system/bootstrap.ts
28297
29446
  var initialized = false;
@@ -29527,7 +30676,167 @@ var ResponseObject = class {
29527
30676
  };
29528
30677
  var response_default = ResponseObject;
29529
30678
 
29530
- // src/physics/sat.js
30679
+ // src/physics/builtin/raycast.ts
30680
+ function _cross(ax, ay, bx, by) {
30681
+ return ax * by - ay * bx;
30682
+ }
30683
+ function _computeShapeAbsPos(renderable, shape, outPos) {
30684
+ const ancestor = renderable.ancestor;
30685
+ const ancestorAbs = ancestor && typeof ancestor.getAbsolutePosition === "function" ? ancestor.getAbsolutePosition() : null;
30686
+ const ax = ancestorAbs ? ancestorAbs.x : 0;
30687
+ const ay = ancestorAbs ? ancestorAbs.y : 0;
30688
+ outPos.x = renderable.pos.x + ax + shape.pos.x;
30689
+ outPos.y = renderable.pos.y + ay + shape.pos.y;
30690
+ }
30691
+ var _shapeAbs = new Vector2d(0, 0);
30692
+ function _raycastPolygon(fromX, fromY, dx, dy, shape, shapeAbsX, shapeAbsY) {
30693
+ const points = shape.points;
30694
+ const normals = shape.normals;
30695
+ const len = points.length;
30696
+ let bestT = Infinity;
30697
+ let bestNormalIdx = -1;
30698
+ for (let i = 0; i < len; i++) {
30699
+ const p0 = points[i];
30700
+ const p1 = points[(i + 1) % len];
30701
+ const ax = shapeAbsX + p0.x;
30702
+ const ay = shapeAbsY + p0.y;
30703
+ const ex = p1.x - p0.x;
30704
+ const ey = p1.y - p0.y;
30705
+ const denom = _cross(dx, dy, ex, ey);
30706
+ if (Math.abs(denom) < 1e-9) {
30707
+ continue;
30708
+ }
30709
+ const fx = ax - fromX;
30710
+ const fy = ay - fromY;
30711
+ const t = _cross(fx, fy, ex, ey) / denom;
30712
+ const s = _cross(fx, fy, dx, dy) / denom;
30713
+ if (t >= 0 && t <= 1 && s >= 0 && s <= 1 && t < bestT) {
30714
+ bestT = t;
30715
+ bestNormalIdx = i;
30716
+ }
30717
+ }
30718
+ if (bestNormalIdx === -1) {
30719
+ return null;
30720
+ }
30721
+ const normal = normals[bestNormalIdx];
30722
+ return { t: bestT, normalX: normal.x, normalY: normal.y };
30723
+ }
30724
+ function _raycastEllipse(fromX, fromY, dx, dy, shape, shapeAbsX, shapeAbsY) {
30725
+ const cx = shapeAbsX;
30726
+ const cy = shapeAbsY;
30727
+ const rx = shape.radiusV.x;
30728
+ const ry = shape.radiusV.y;
30729
+ if (rx <= 0 || ry <= 0) {
30730
+ return null;
30731
+ }
30732
+ const ox = (fromX - cx) / rx;
30733
+ const oy = (fromY - cy) / ry;
30734
+ const ddx = dx / rx;
30735
+ const ddy = dy / ry;
30736
+ const A = ddx * ddx + ddy * ddy;
30737
+ const B = 2 * (ddx * ox + ddy * oy);
30738
+ const C = ox * ox + oy * oy - 1;
30739
+ const disc = B * B - 4 * A * C;
30740
+ if (disc < 0 || A < 1e-12) {
30741
+ return null;
30742
+ }
30743
+ const sqrtDisc = Math.sqrt(disc);
30744
+ const t0 = (-B - sqrtDisc) / (2 * A);
30745
+ const t1 = (-B + sqrtDisc) / (2 * A);
30746
+ let t;
30747
+ if (t0 >= 0 && t0 <= 1) {
30748
+ t = t0;
30749
+ } else if (t1 >= 0 && t1 <= 1) {
30750
+ t = t1;
30751
+ } else {
30752
+ return null;
30753
+ }
30754
+ const hitX = fromX + dx * t;
30755
+ const hitY = fromY + dy * t;
30756
+ let nx = (hitX - cx) / (rx * rx);
30757
+ let ny = (hitY - cy) / (ry * ry);
30758
+ const nLen = Math.hypot(nx, ny);
30759
+ if (nLen > 0) {
30760
+ nx /= nLen;
30761
+ ny /= nLen;
30762
+ }
30763
+ return { t, normalX: nx, normalY: ny };
30764
+ }
30765
+ function raycastQuery(world, fromX, fromY, toX, toY) {
30766
+ const dx = toX - fromX;
30767
+ const dy = toY - fromY;
30768
+ const segLen = Math.hypot(dx, dy);
30769
+ if (segLen < 1e-9) {
30770
+ return [];
30771
+ }
30772
+ const line = linePool.get(fromX, fromY, [
30773
+ new Vector2d(0, 0),
30774
+ new Vector2d(dx, dy)
30775
+ ]);
30776
+ const candidates = [];
30777
+ world.broadphase.retrieve(line, void 0, candidates);
30778
+ const hits = [];
30779
+ for (let i = candidates.length - 1; i >= 0; i--) {
30780
+ const objB = candidates[i];
30781
+ if (!objB.body) {
30782
+ continue;
30783
+ }
30784
+ if (!line.getBounds().overlaps(objB.getBounds())) {
30785
+ continue;
30786
+ }
30787
+ const bodyB = objB.body;
30788
+ const shapeCount = bodyB.shapes.length;
30789
+ if (shapeCount === 0) {
30790
+ continue;
30791
+ }
30792
+ let bestT = Infinity;
30793
+ let bestNormalX = 0;
30794
+ let bestNormalY = 0;
30795
+ for (let j = 0; j < shapeCount; j++) {
30796
+ const shapeB = bodyB.getShape(j);
30797
+ _computeShapeAbsPos(objB, shapeB, _shapeAbs);
30798
+ const hit = shapeB.type === "Ellipse" ? _raycastEllipse(
30799
+ fromX,
30800
+ fromY,
30801
+ dx,
30802
+ dy,
30803
+ shapeB,
30804
+ _shapeAbs.x,
30805
+ _shapeAbs.y
30806
+ ) : _raycastPolygon(
30807
+ fromX,
30808
+ fromY,
30809
+ dx,
30810
+ dy,
30811
+ shapeB,
30812
+ _shapeAbs.x,
30813
+ _shapeAbs.y
30814
+ );
30815
+ if (hit !== null && hit.t < bestT) {
30816
+ bestT = hit.t;
30817
+ bestNormalX = hit.normalX;
30818
+ bestNormalY = hit.normalY;
30819
+ }
30820
+ }
30821
+ if (bestT === Infinity) {
30822
+ continue;
30823
+ }
30824
+ const flipSign = bestNormalX * dx + bestNormalY * dy > 0 ? -1 : 1;
30825
+ hits.push({
30826
+ renderable: objB,
30827
+ point: new Vector2d(fromX + dx * bestT, fromY + dy * bestT),
30828
+ normal: new Vector2d(bestNormalX * flipSign, bestNormalY * flipSign),
30829
+ fraction: bestT
30830
+ });
30831
+ }
30832
+ linePool.release(line);
30833
+ hits.sort((a, b) => {
30834
+ return a.fraction - b.fraction;
30835
+ });
30836
+ return hits;
30837
+ }
30838
+
30839
+ // src/physics/builtin/sat.js
29531
30840
  var LEFT_VORNOI_REGION = -1;
29532
30841
  var MIDDLE_VORNOI_REGION = 0;
29533
30842
  var RIGHT_VORNOI_REGION = 1;
@@ -29805,7 +31114,7 @@ function testEllipsePolygon(a, ellipseA, b, polyB, response) {
29805
31114
  return result;
29806
31115
  }
29807
31116
 
29808
- // src/physics/detector.js
31117
+ // src/physics/builtin/detector.js
29809
31118
  var SAT_LOOKUP = {
29810
31119
  PolygonPolygon: testPolygonPolygon,
29811
31120
  PolygonEllipse: testPolygonEllipse,
@@ -29824,15 +31133,6 @@ var SAT_LOOKUP = {
29824
31133
  RectangleRoundRect: testPolygonPolygon,
29825
31134
  RoundRectRectangle: testPolygonPolygon
29826
31135
  };
29827
- var dummyObj = {
29828
- pos: new Vector2d(0, 0),
29829
- ancestor: {
29830
- _absPos: new Vector2d(0, 0),
29831
- getAbsolutePosition: function() {
29832
- return this._absPos;
29833
- }
29834
- }
29835
- };
29836
31136
  var boundsA = new Bounds();
29837
31137
  var boundsB = new Bounds();
29838
31138
  var Detector = class {
@@ -29842,6 +31142,116 @@ var Detector = class {
29842
31142
  constructor(world) {
29843
31143
  this.world = world;
29844
31144
  this.response = new response_default();
31145
+ this._activePairs = /* @__PURE__ */ new Map();
31146
+ this._frameSeen = /* @__PURE__ */ new Map();
31147
+ this._symViews = [
31148
+ {
31149
+ a: null,
31150
+ b: null,
31151
+ overlap: 0,
31152
+ overlapN: { x: 0, y: 0 },
31153
+ overlapV: { x: 0, y: 0 },
31154
+ normal: { x: 0, y: 0 },
31155
+ depth: 0
31156
+ },
31157
+ {
31158
+ a: null,
31159
+ b: null,
31160
+ overlap: 0,
31161
+ overlapN: { x: 0, y: 0 },
31162
+ overlapV: { x: 0, y: 0 },
31163
+ normal: { x: 0, y: 0 },
31164
+ depth: 0
31165
+ }
31166
+ ];
31167
+ }
31168
+ /**
31169
+ * Populate one of the pooled symmetric views from a SAT response.
31170
+ * `flip=false` builds the view for `response.a`'s side (legacy
31171
+ * fields kept as-is, `normal = -overlapN` = MTV of original a).
31172
+ * `flip=true` builds it for `response.b`'s side: `a` / `b` swap,
31173
+ * `overlapN` / `overlapV` negated, `normal = +overlapN` (the MTV
31174
+ * of original b).
31175
+ * @ignore
31176
+ */
31177
+ _fillSymView(slot, satResponse, flip) {
31178
+ const view = this._symViews[slot];
31179
+ const oN = satResponse.overlapN;
31180
+ const oV = satResponse.overlapV;
31181
+ if (flip) {
31182
+ view.a = satResponse.b;
31183
+ view.b = satResponse.a;
31184
+ view.overlapN.x = -oN.x;
31185
+ view.overlapN.y = -oN.y;
31186
+ view.overlapV.x = -oV.x;
31187
+ view.overlapV.y = -oV.y;
31188
+ view.normal.x = oN.x;
31189
+ view.normal.y = oN.y;
31190
+ } else {
31191
+ view.a = satResponse.a;
31192
+ view.b = satResponse.b;
31193
+ view.overlapN.x = oN.x;
31194
+ view.overlapN.y = oN.y;
31195
+ view.overlapV.x = oV.x;
31196
+ view.overlapV.y = oV.y;
31197
+ view.normal.x = -oN.x;
31198
+ view.normal.y = -oN.y;
31199
+ }
31200
+ view.overlap = satResponse.overlap;
31201
+ view.depth = satResponse.overlap;
31202
+ return view;
31203
+ }
31204
+ /**
31205
+ * Called by the adapter at the start of a physics step. Resets the
31206
+ * "seen this frame" set so the end-of-step diff can fire
31207
+ * `onCollisionEnd` for pairs that no longer overlap.
31208
+ * @ignore
31209
+ */
31210
+ beginFrame() {
31211
+ this._frameSeen.clear();
31212
+ }
31213
+ /**
31214
+ * Called by the adapter at the end of a physics step. Diffs the
31215
+ * "seen this frame" set against the previous-frame active pairs:
31216
+ * - pairs in active but not seen → fire onCollisionEnd
31217
+ * - swap active ← seen for the next step's diff
31218
+ * @ignore
31219
+ */
31220
+ endFrame() {
31221
+ for (const [key, pair] of this._activePairs) {
31222
+ if (this._frameSeen.has(key)) {
31223
+ continue;
31224
+ }
31225
+ const [a, b] = pair;
31226
+ const aAttached = a.ancestor != null;
31227
+ const bAttached = b.ancestor != null;
31228
+ if (!aAttached && !bAttached) {
31229
+ continue;
31230
+ }
31231
+ if (aAttached && typeof a.onCollisionEnd === "function") {
31232
+ a.onCollisionEnd(void 0, b);
31233
+ }
31234
+ if (bAttached && typeof b.onCollisionEnd === "function") {
31235
+ b.onCollisionEnd(void 0, a);
31236
+ }
31237
+ }
31238
+ const prev = this._activePairs;
31239
+ this._activePairs = this._frameSeen;
31240
+ this._frameSeen = prev;
31241
+ }
31242
+ /**
31243
+ * Build a stable order-independent key for a pair of renderables,
31244
+ * using their GUID. Returns undefined if either lacks a GUID (defensive
31245
+ * — detached or pool-recycled objects mid-step).
31246
+ * @ignore
31247
+ */
31248
+ _pairKey(a, b) {
31249
+ const ga = a.GUID;
31250
+ const gb = b.GUID;
31251
+ if (ga === void 0 || gb === void 0) {
31252
+ return void 0;
31253
+ }
31254
+ return ga < gb ? `${ga}|${gb}` : `${gb}|${ga}`;
29845
31255
  }
29846
31256
  /**
29847
31257
  * determine if two objects should collide (based on both respective objects body collision mask and type).<br>
@@ -29902,10 +31312,37 @@ var Detector = class {
29902
31312
  if (boundsA.overlaps(boundsB)) {
29903
31313
  if (this.collides(objA.body, objB.body)) {
29904
31314
  collisionCounter++;
29905
- if (objA.onCollision && objA.onCollision(this.response, objB) !== false && objA.body.isStatic === false) {
31315
+ const pairKey = this._pairKey(objA, objB);
31316
+ const firstVisitThisFrame = pairKey !== void 0 && !this._frameSeen.has(pairKey);
31317
+ if (firstVisitThisFrame) {
31318
+ this._frameSeen.set(pairKey, [objA, objB]);
31319
+ const isEntry = !this._activePairs.has(pairKey);
31320
+ const viewA = this._fillSymView(0, this.response, false);
31321
+ const viewB = this._fillSymView(1, this.response, true);
31322
+ if (isEntry) {
31323
+ if (typeof objA.onCollisionStart === "function") {
31324
+ objA.onCollisionStart(viewA, objB);
31325
+ }
31326
+ if (typeof objB.onCollisionStart === "function") {
31327
+ objB.onCollisionStart(viewB, objA);
31328
+ }
31329
+ }
31330
+ if (typeof objA.onCollisionActive === "function") {
31331
+ objA.onCollisionActive(viewA, objB);
31332
+ }
31333
+ if (typeof objB.onCollisionActive === "function") {
31334
+ objB.onCollisionActive(viewB, objA);
31335
+ }
31336
+ }
31337
+ const eitherSensor = objA.body.isSensor === true || objB.body.isSensor === true;
31338
+ const aHasModern = typeof objA.onCollisionActive === "function";
31339
+ const bHasModern = typeof objB.onCollisionActive === "function";
31340
+ const aOptsOut = !aHasModern && typeof objA.onCollision === "function" && objA.onCollision(this.response, objB) === false;
31341
+ if (!aOptsOut && objA.body.isStatic === false && !eitherSensor) {
29906
31342
  objA.body.respondToCollision.call(objA.body, this.response);
29907
31343
  }
29908
- if (objB.onCollision && objB.onCollision(this.response, objA) !== false && objB.body.isStatic === false) {
31344
+ const bOptsOut = !bHasModern && typeof objB.onCollision === "function" && objB.onCollision(this.response, objA) === false;
31345
+ if (!bOptsOut && objB.body.isStatic === false && !eitherSensor) {
29909
31346
  objB.body.respondToCollision.call(objB.body, this.response);
29910
31347
  }
29911
31348
  if (objA.body.shapes.length > 1 || objB.body.shapes.length > 1) {
@@ -29976,41 +31413,275 @@ var Detector = class {
29976
31413
  * }
29977
31414
  */
29978
31415
  rayCast(line, result = []) {
29979
- let collisionCounter = 0;
29980
- const candidates = this.world.broadphase.retrieve(line);
29981
- for (let i = candidates.length, objB; i--, objB = candidates[i]; ) {
29982
- if (objB.body && line.getBounds().overlaps(objB.getBounds())) {
29983
- const bLen = objB.body.shapes.length;
29984
- if (objB.body.shapes.length === 0) {
29985
- continue;
29986
- }
29987
- const shapeA = line;
29988
- let indexB = 0;
29989
- do {
29990
- const shapeB = objB.body.getShape(indexB);
29991
- if (SAT_LOOKUP[shapeA.type + shapeB.type].call(
29992
- this,
29993
- dummyObj,
29994
- // a reference to the object A
29995
- shapeA,
29996
- objB,
29997
- // a reference to the object B
29998
- shapeB
29999
- )) {
30000
- result[collisionCounter] = objB;
30001
- collisionCounter++;
31416
+ const fromX = line.pos.x + line.points[0].x;
31417
+ const fromY = line.pos.y + line.points[0].y;
31418
+ const toX = line.pos.x + line.points[1].x;
31419
+ const toY = line.pos.y + line.points[1].y;
31420
+ const hits = raycastQuery(this.world, fromX, fromY, toX, toY);
31421
+ for (let i = 0; i < hits.length; i++) {
31422
+ result[i] = hits[i].renderable;
31423
+ }
31424
+ result.length = hits.length;
31425
+ return result;
31426
+ }
31427
+ };
31428
+ var detector_default = Detector;
31429
+
31430
+ // src/physics/builtin/builtin-adapter.ts
31431
+ var BuiltinAdapter = class {
31432
+ /**
31433
+ * Short adapter identifier exposed as `world.physic`. Lets user code
31434
+ * branch on the active physics implementation without importing the
31435
+ * concrete adapter class.
31436
+ */
31437
+ physicLabel = "builtin";
31438
+ /**
31439
+ * Advertised capabilities; user code may branch on these.
31440
+ */
31441
+ capabilities = {
31442
+ constraints: false,
31443
+ continuousCollisionDetection: false,
31444
+ sleepingBodies: false,
31445
+ raycasts: true,
31446
+ velocityLimit: true,
31447
+ isGrounded: true
31448
+ };
31449
+ /**
31450
+ * World gravity. Mutate to change at runtime.
31451
+ * @default <0, 0.98>
31452
+ */
31453
+ gravity;
31454
+ /**
31455
+ * Active physics bodies in this simulation.
31456
+ */
31457
+ bodies = /* @__PURE__ */ new Set();
31458
+ /**
31459
+ * Collision detector instance, created in {@link init}.
31460
+ */
31461
+ detector;
31462
+ /**
31463
+ * Back-reference to the owning world, set in {@link init}.
31464
+ */
31465
+ world;
31466
+ constructor(options = {}) {
31467
+ this.gravity = options.gravity ?? new Vector2d(0, 0.98);
31468
+ }
31469
+ init(world) {
31470
+ this.world = world;
31471
+ this.detector = new detector_default(world);
31472
+ }
31473
+ destroy() {
31474
+ this.bodies.clear();
31475
+ }
31476
+ step(dt) {
31477
+ const isPaused = state_default.isPaused();
31478
+ this.detector.beginFrame();
31479
+ for (const body of this.bodies) {
31480
+ const ancestor = body.ancestor;
31481
+ if (!body.isStatic && ancestor) {
31482
+ if (!(isPaused && !ancestor.updateWhenPaused) && (ancestor.inViewport || ancestor.alwaysUpdate)) {
31483
+ this.applyGravity(body);
31484
+ if (body.update(dt)) {
31485
+ ancestor.isDirty = true;
30002
31486
  }
30003
- indexB++;
30004
- } while (indexB < bLen);
31487
+ this.detector.collisions(ancestor);
31488
+ }
31489
+ }
31490
+ body.force.set(0, 0);
31491
+ }
31492
+ this.detector.endFrame();
31493
+ }
31494
+ syncFromPhysics() {
31495
+ }
31496
+ addBody(renderable, def) {
31497
+ let body = renderable.body;
31498
+ const isAlreadyAdapterManaged = body instanceof Body && this.bodies.has(body);
31499
+ if (isAlreadyAdapterManaged) {
31500
+ throw new Error(
31501
+ "BuiltinAdapter.addBody: renderable is already adapter-managed. Use adapter.updateShape() / property mutation / adapter.removeBody() first if you need to change the body."
31502
+ );
31503
+ }
31504
+ if (!(body instanceof Body)) {
31505
+ body = new Body(renderable, def.shapes);
31506
+ renderable.body = body;
31507
+ } else if (def.shapes.length > 0) {
31508
+ body.shapes.length = 0;
31509
+ body.getBounds().clear();
31510
+ for (const s of def.shapes) {
31511
+ body.addShape(s);
30005
31512
  }
30006
31513
  }
30007
- result.length = collisionCounter;
31514
+ body.setStatic(def.type === "static");
31515
+ if (typeof def.collisionType === "number") {
31516
+ body.collisionType = def.collisionType;
31517
+ }
31518
+ if (typeof def.collisionMask === "number") {
31519
+ body.collisionMask = def.collisionMask;
31520
+ }
31521
+ if (def.frictionAir !== void 0) {
31522
+ if (typeof def.frictionAir === "number") {
31523
+ body.setFriction(def.frictionAir, def.frictionAir);
31524
+ } else {
31525
+ body.setFriction(def.frictionAir.x, def.frictionAir.y);
31526
+ }
31527
+ }
31528
+ if (typeof def.restitution === "number") {
31529
+ body.bounce = def.restitution;
31530
+ }
31531
+ if (typeof def.density === "number") {
31532
+ body.mass = def.density;
31533
+ }
31534
+ if (typeof def.gravityScale === "number") {
31535
+ body.gravityScale = def.gravityScale;
31536
+ }
31537
+ if (def.maxVelocity !== void 0) {
31538
+ body.setMaxVelocity(def.maxVelocity.x, def.maxVelocity.y);
31539
+ }
31540
+ if (def.isSensor === true) {
31541
+ body.isSensor = true;
31542
+ }
31543
+ this.bodies.add(body);
31544
+ return body;
31545
+ }
31546
+ removeBody(renderable) {
31547
+ const body = renderable.body;
31548
+ if (body instanceof Body) {
31549
+ this.bodies.delete(body);
31550
+ }
31551
+ }
31552
+ updateShape(renderable, shapes) {
31553
+ const body = renderable.body;
31554
+ if (!(body instanceof Body)) {
31555
+ return;
31556
+ }
31557
+ body.shapes.length = 0;
31558
+ body.getBounds().clear();
31559
+ for (const s of shapes) {
31560
+ body.addShape(s);
31561
+ }
31562
+ }
31563
+ getVelocity(renderable, out) {
31564
+ const body = renderable.body;
31565
+ const target = out ?? new Vector2d();
31566
+ if (!body || !this.bodies.has(body)) {
31567
+ return target.set(0, 0);
31568
+ }
31569
+ return target.set(body.vel.x, body.vel.y);
31570
+ }
31571
+ setVelocity(renderable, v) {
31572
+ renderable.body.vel.setV(v);
31573
+ }
31574
+ applyForce(renderable, force, point) {
31575
+ const body = renderable.body;
31576
+ if (point) {
31577
+ body.applyForce(force.x, force.y, point.x, point.y);
31578
+ } else {
31579
+ body.applyForce(force.x, force.y);
31580
+ }
31581
+ }
31582
+ applyImpulse(renderable, impulse) {
31583
+ const body = renderable.body;
31584
+ const invMass = body.mass > 0 ? 1 / body.mass : 0;
31585
+ body.vel.x += impulse.x * invMass;
31586
+ body.vel.y += impulse.y * invMass;
31587
+ }
31588
+ setPosition(renderable, p) {
31589
+ renderable.pos.x = p.x;
31590
+ renderable.pos.y = p.y;
31591
+ }
31592
+ setAngle(renderable, angle) {
31593
+ renderable.body.setAngle(angle);
31594
+ }
31595
+ getAngle(renderable) {
31596
+ return renderable.body.angle;
31597
+ }
31598
+ setAngularVelocity(renderable, omega) {
31599
+ renderable.body.setAngularVelocity(omega);
31600
+ }
31601
+ getAngularVelocity(renderable) {
31602
+ return renderable.body.angularVelocity;
31603
+ }
31604
+ applyTorque(renderable, torque) {
31605
+ renderable.body.applyTorque(torque);
31606
+ }
31607
+ setStatic(renderable, isStatic) {
31608
+ renderable.body.setStatic(isStatic);
31609
+ }
31610
+ setGravityScale(renderable, scale2) {
31611
+ renderable.body.gravityScale = scale2;
31612
+ }
31613
+ setFrictionAir(renderable, friction) {
31614
+ const body = renderable.body;
31615
+ if (typeof friction === "number") {
31616
+ body.setFriction(friction, friction);
31617
+ } else {
31618
+ body.setFriction(friction.x, friction.y);
31619
+ }
31620
+ }
31621
+ setMaxVelocity(renderable, limit) {
31622
+ renderable.body.setMaxVelocity(limit.x, limit.y);
31623
+ }
31624
+ getMaxVelocity(renderable) {
31625
+ const v = renderable.body.maxVel;
31626
+ return { x: v.x, y: v.y };
31627
+ }
31628
+ setCollisionType(renderable, type) {
31629
+ renderable.body.collisionType = type;
31630
+ }
31631
+ setCollisionMask(renderable, mask) {
31632
+ renderable.body.setCollisionMask(mask);
31633
+ }
31634
+ setSensor(renderable, isSensor) {
31635
+ renderable.body.isSensor = isSensor;
31636
+ }
31637
+ getBodyAABB(renderable, out) {
31638
+ const body = renderable.body;
31639
+ if (!body || !this.bodies.has(body)) {
31640
+ return void 0;
31641
+ }
31642
+ const b = body.bounds;
31643
+ out.setMinMax(b.min.x, b.min.y, b.max.x, b.max.y);
31644
+ return out;
31645
+ }
31646
+ getBodyShapes(renderable) {
31647
+ const body = renderable.body;
31648
+ if (!body || !this.bodies.has(body)) {
31649
+ return [];
31650
+ }
31651
+ return body.shapes;
31652
+ }
31653
+ isGrounded(renderable) {
31654
+ const body = renderable.body;
31655
+ return !body.falling && !body.jumping;
31656
+ }
31657
+ raycast(from, to) {
31658
+ const hits = raycastQuery(this.world, from.x, from.y, to.x, to.y);
31659
+ return hits[0] ?? null;
31660
+ }
31661
+ queryAABB(rect) {
31662
+ const queryBounds = rect.getBounds();
31663
+ const result = [];
31664
+ this.world.broadphase.retrieve(rect, void 0, result);
31665
+ let writeIdx = 0;
31666
+ for (let i = 0, len = result.length; i < len; i++) {
31667
+ const r = result[i];
31668
+ const b = r.getBounds();
31669
+ if (b.overlaps(queryBounds)) {
31670
+ result[writeIdx++] = r;
31671
+ }
31672
+ }
31673
+ result.length = writeIdx;
30008
31674
  return result;
30009
31675
  }
31676
+ applyGravity(body) {
31677
+ if (body.gravityScale !== 0 && !body.ignoreGravity) {
31678
+ body.force.x += body.mass * this.gravity.x * body.gravityScale;
31679
+ body.force.y += body.mass * this.gravity.y * body.gravityScale;
31680
+ }
31681
+ }
30010
31682
  };
30011
- var detector_default = Detector;
30012
31683
 
30013
- // src/physics/quadtree.js
31684
+ // src/physics/builtin/quadtree.js
30014
31685
  var QT_ARRAY = [];
30015
31686
  function QT_ARRAY_POP(world, bounds, max_objects = 4, max_levels = 4, level2 = 0) {
30016
31687
  if (QT_ARRAY.length > 0) {
@@ -30045,6 +31716,8 @@ var QuadTree = class {
30045
31716
  this.level = level2;
30046
31717
  this.objects = [];
30047
31718
  this.nodes = [];
31719
+ this._subtreeCount = 0;
31720
+ this._retrieveScratch = level2 === 0 ? [] : null;
30048
31721
  }
30049
31722
  /*
30050
31723
  * Split the node into 4 subnodes
@@ -30176,6 +31849,7 @@ var QuadTree = class {
30176
31849
  * @param {object} item - object to be added
30177
31850
  */
30178
31851
  insert(item) {
31852
+ this._subtreeCount++;
30179
31853
  if (this.nodes.length > 0) {
30180
31854
  const index = this.getIndex(item);
30181
31855
  if (index !== -1) {
@@ -30201,15 +31875,61 @@ var QuadTree = class {
30201
31875
  }
30202
31876
  }
30203
31877
  /**
30204
- * Return all objects that could collide with the given object
31878
+ * Recursively remove the given container and its descendants from
31879
+ * the quadtree. Mirrors `insertContainer` so the broadphase can be
31880
+ * kept in sync when a subtree is detached via
31881
+ * `Container.removeChildNow` between two `world.update()` rebuilds
31882
+ * (pointer events fire async in that window and would otherwise
31883
+ * hit destroyed renderables).
31884
+ * @param {Container} container - group of objects to be removed
31885
+ */
31886
+ removeContainer(container) {
31887
+ const children = container.getChildren?.();
31888
+ if (!children) {
31889
+ return;
31890
+ }
31891
+ const childrenLength = children.length;
31892
+ for (let i = childrenLength, child; i--, child = children[i]; ) {
31893
+ if (child.isKinematic !== true) {
31894
+ if (typeof child.addChild === "function") {
31895
+ if (child.name !== "rootContainer") {
31896
+ this.remove(child);
31897
+ }
31898
+ this.removeContainer(child);
31899
+ } else if (typeof child.getBounds === "function") {
31900
+ this.remove(child);
31901
+ }
31902
+ }
31903
+ }
31904
+ }
31905
+ /**
31906
+ * Return all objects that could collide with the given object.
31907
+ *
31908
+ * **Re-entrancy contract:** when called with no explicit `result`
31909
+ * argument, this method reuses a single root-level scratch array to
31910
+ * avoid per-frame allocations. The returned reference is therefore
31911
+ * **not safe to retain** past the next `retrieve()` call, AND it is
31912
+ * **not safe to issue another scratch-mode `retrieve()` while iterating
31913
+ * the previous result** — the second call clears the scratch and
31914
+ * refills it, corrupting the outer iteration. In-engine callers
31915
+ * (`pointerevent.ts`, `detector.js`) iterate synchronously and never
31916
+ * recurse into `retrieve()`, so they're fine. User-facing portable
31917
+ * APIs (`adapter.queryAABB`, `adapter.raycast`) pass their own array
31918
+ * via the `result` parameter, which bypasses the scratch entirely
31919
+ * and is safe to call from inside collision handlers.
31920
+ *
30205
31921
  * @param {object} item - object to be checked against
30206
31922
  * @param {object} [fn] - a sorting function for the returned array
31923
+ * @param {object[]} [result] - optional caller-supplied result array.
31924
+ * Pass an explicit (typically empty) array to sidestep the shared
31925
+ * scratch — required for re-entrancy safety.
30207
31926
  * @returns {object[]} array with all detected objects
30208
31927
  */
30209
31928
  retrieve(item, fn, result) {
30210
31929
  const isRoot = typeof result === "undefined";
30211
31930
  if (isRoot) {
30212
- result = [];
31931
+ result = this._retrieveScratch;
31932
+ result.length = 0;
30213
31933
  }
30214
31934
  const objects = this.objects;
30215
31935
  for (let i = 0, len = objects.length; i < len; i++) {
@@ -30245,9 +31965,6 @@ var QuadTree = class {
30245
31965
  const index = this.getIndex(item);
30246
31966
  if (index !== -1) {
30247
31967
  found = this.nodes[index].remove(item);
30248
- if (found && this.nodes[index].isPrunable()) {
30249
- this.nodes[index].clear();
30250
- }
30251
31968
  }
30252
31969
  }
30253
31970
  if (found === false) {
@@ -30257,6 +31974,24 @@ var QuadTree = class {
30257
31974
  found = true;
30258
31975
  }
30259
31976
  }
31977
+ if (found === false && this.nodes.length > 0) {
31978
+ for (let i = 0; i < this.nodes.length; i++) {
31979
+ if (this.nodes[i].remove(item)) {
31980
+ found = true;
31981
+ break;
31982
+ }
31983
+ }
31984
+ }
31985
+ if (found) {
31986
+ this._subtreeCount--;
31987
+ if (this.nodes.length > 0 && this.objects.length === 0 && this._subtreeCount === 0) {
31988
+ for (let i = 0; i < this.nodes.length; i++) {
31989
+ this.nodes[i].clear();
31990
+ QT_ARRAY_PUSH(this.nodes[i]);
31991
+ }
31992
+ this.nodes.length = 0;
31993
+ }
31994
+ }
30260
31995
  return found;
30261
31996
  }
30262
31997
  /**
@@ -30264,20 +31999,14 @@ var QuadTree = class {
30264
31999
  * @returns {boolean} true if the node is prunable
30265
32000
  */
30266
32001
  isPrunable() {
30267
- return !(this.hasChildren() || this.objects.length > 0);
32002
+ return this._subtreeCount === 0;
30268
32003
  }
30269
32004
  /**
30270
32005
  * return true if the node has any children
30271
32006
  * @returns {boolean} true if the node has any children
30272
32007
  */
30273
32008
  hasChildren() {
30274
- for (let i = 0; i < this.nodes.length; i = i + 1) {
30275
- const subnode = this.nodes[i];
30276
- if (subnode.nodes.length > 0 || subnode.objects.length > 0) {
30277
- return true;
30278
- }
30279
- }
30280
- return false;
32009
+ return this._subtreeCount > this.objects.length;
30281
32010
  }
30282
32011
  /**
30283
32012
  * clear the quadtree
@@ -30290,6 +32019,7 @@ var QuadTree = class {
30290
32019
  QT_ARRAY_PUSH(this.nodes[i]);
30291
32020
  }
30292
32021
  this.nodes.length = 0;
32022
+ this._subtreeCount = 0;
30293
32023
  if (typeof bounds !== "undefined") {
30294
32024
  this.bounds.setMinMax(
30295
32025
  bounds.min.x,
@@ -30302,36 +32032,76 @@ var QuadTree = class {
30302
32032
  };
30303
32033
 
30304
32034
  // src/physics/world.js
32035
+ var EMPTY_BODIES = Object.freeze(/* @__PURE__ */ new Set());
30305
32036
  var World = class extends Container {
30306
32037
  /**
30307
32038
  * @param {number} [x=0] - position of the container (accessible via the inherited pos.x property)
30308
32039
  * @param {number} [y=0] - position of the container (accessible via the inherited pos.y property)
30309
32040
  * @param {number} [width=Infinity] - width of the world container
30310
32041
  * @param {number} [height=Infinity] - height of the world container
32042
+ * @param {PhysicsAdapter} [adapter] - physics adapter to use; defaults to a new {@link BuiltinAdapter} instance
30311
32043
  */
30312
- constructor(x = 0, y = 0, width = Infinity, height = Infinity) {
32044
+ constructor(x = 0, y = 0, width = Infinity, height = Infinity, adapter = void 0) {
30313
32045
  super(x, y, width, height, true);
30314
32046
  this.name = "rootContainer";
30315
32047
  this.anchorPoint.set(0, 0);
30316
32048
  this.app = void 0;
30317
32049
  this.physic = "builtin";
30318
32050
  this.fps = 60;
30319
- this.gravity = new Vector2d(0, 0.98);
30320
32051
  this.preRender = false;
30321
32052
  this.gpuTilemap = true;
30322
- this.bodies = /* @__PURE__ */ new Set();
32053
+ this.adapter = adapter ?? new BuiltinAdapter();
32054
+ this.adapter.init?.(this);
30323
32055
  this.broadphase = new QuadTree(
30324
32056
  this,
30325
32057
  this.getBounds().clone(),
30326
32058
  collision.maxChildren,
30327
32059
  collision.maxDepth
30328
32060
  );
30329
- this.detector = new detector_default(this);
30330
32061
  on(GAME_RESET, this.reset, this);
30331
32062
  on(LEVEL_LOADED, () => {
30332
32063
  this.broadphase.clear(this.getBounds());
30333
32064
  });
30334
32065
  }
32066
+ /**
32067
+ * Active physics bodies in this simulation. Backed by the active
32068
+ * adapter; mutating this set directly is no longer the recommended
32069
+ * pattern — use `world.adapter.addBody(...)` / `removeBody(...)`.
32070
+ *
32071
+ * Adapters that don't expose a native `bodies` Set (e.g. third-party
32072
+ * integrations that own their own body storage) cause this getter
32073
+ * to return a frozen empty Set, so any `world.bodies.add(...)`
32074
+ * attempt throws `TypeError` instead of silently mutating a
32075
+ * throwaway.
32076
+ * @returns {Set<Body>}
32077
+ */
32078
+ get bodies() {
32079
+ return (
32080
+ /** @type {{ bodies?: Set<Body> }} */
32081
+ this.adapter.bodies ?? EMPTY_BODIES
32082
+ );
32083
+ }
32084
+ /**
32085
+ * world gravity. Mutate to change at runtime.
32086
+ * @returns {Vector2d}
32087
+ */
32088
+ get gravity() {
32089
+ return this.adapter.gravity;
32090
+ }
32091
+ set gravity(v) {
32092
+ this.adapter.gravity = v;
32093
+ }
32094
+ /**
32095
+ * the collision detector instance used by this world instance.
32096
+ * Available only when the active adapter is {@link BuiltinAdapter}.
32097
+ * @returns {Detector | undefined}
32098
+ */
32099
+ get detector() {
32100
+ return (
32101
+ /** @type {{ detector?: Detector }} */
32102
+ this.adapter.detector
32103
+ );
32104
+ }
30335
32105
  /**
30336
32106
  * reset the game world
30337
32107
  */
@@ -30339,28 +32109,40 @@ var World = class extends Container {
30339
32109
  this.broadphase.clear();
30340
32110
  this.anchorPoint.set(0, 0);
30341
32111
  super.reset();
30342
- const persistentBodies = [];
30343
- this.bodies.forEach((value) => {
30344
- if (value.ancestor && value.ancestor.isPersistent) {
30345
- persistentBodies.push(value);
30346
- }
30347
- });
30348
- this.bodies.clear();
30349
- if (persistentBodies.length > 0) {
30350
- persistentBodies.forEach((body) => {
30351
- this.addBody(body);
32112
+ const bodies = (
32113
+ /** @type {{ bodies?: Set<Body> }} */
32114
+ this.adapter.bodies
32115
+ );
32116
+ if (bodies !== void 0) {
32117
+ const persistentBodies = [];
32118
+ bodies.forEach((value) => {
32119
+ if (value.ancestor && value.ancestor.isPersistent) {
32120
+ persistentBodies.push(value);
32121
+ }
30352
32122
  });
32123
+ bodies.clear();
32124
+ if (persistentBodies.length > 0) {
32125
+ persistentBodies.forEach((body) => {
32126
+ this.addBody(body);
32127
+ });
32128
+ }
30353
32129
  }
30354
32130
  }
30355
32131
  /**
30356
- * Add a physic body to the game world
32132
+ * Add a physic body to the game world. Legacy API for code that
32133
+ * constructed `new Body(...)` directly and now wants to register it
32134
+ * with the active physics adapter.
30357
32135
  * @see Container.addChild
30358
32136
  * @param {Body} body
30359
32137
  * @returns {World} this game world
30360
32138
  */
30361
32139
  addBody(body) {
30362
32140
  if (this.physic === "builtin") {
30363
- this.bodies.add(body);
32141
+ const bodies = (
32142
+ /** @type {{ bodies?: Set<Body> }} */
32143
+ this.adapter.bodies
32144
+ );
32145
+ bodies?.add(body);
30364
32146
  }
30365
32147
  return this;
30366
32148
  }
@@ -30372,21 +32154,22 @@ var World = class extends Container {
30372
32154
  */
30373
32155
  removeBody(body) {
30374
32156
  if (this.physic === "builtin") {
30375
- this.bodies.delete(body);
32157
+ const bodies = (
32158
+ /** @type {{ bodies?: Set<Body> }} */
32159
+ this.adapter.bodies
32160
+ );
32161
+ bodies?.delete(body);
30376
32162
  }
30377
32163
  return this;
30378
32164
  }
30379
32165
  /**
30380
- * Apply gravity to the given body
32166
+ * Apply gravity to the given body. Backward-compat shim; the actual
32167
+ * simulation runs through the active adapter.
30381
32168
  * @private
30382
32169
  * @param {Body} body
30383
32170
  */
30384
32171
  bodyApplyGravity(body) {
30385
- if (!body.ignoreGravity && body.gravityScale !== 0) {
30386
- const gravity = this.gravity;
30387
- body.force.x += body.mass * gravity.x * body.gravityScale;
30388
- body.force.y += body.mass * gravity.y * body.gravityScale;
30389
- }
32172
+ this.adapter.applyGravity?.(body);
30390
32173
  }
30391
32174
  /**
30392
32175
  * update the game world
@@ -30402,28 +32185,13 @@ var World = class extends Container {
30402
32185
  return super.update(dt);
30403
32186
  }
30404
32187
  /**
30405
- * update the builtin physic simulation by one step (called by the game world update method)
32188
+ * update the physics simulation by one step (called by the game world update method)
30406
32189
  * @param {number} dt - the time passed since the last frame update
30407
32190
  */
30408
32191
  step(dt) {
30409
- if (this.physic === "builtin") {
30410
- const isPaused = state_default.isPaused();
30411
- for (const body of this.bodies) {
30412
- if (!body.isStatic) {
30413
- const ancestor = body.ancestor;
30414
- if (!ancestor) {
30415
- continue;
30416
- }
30417
- if (!(isPaused && !ancestor.updateWhenPaused) && (ancestor.inViewport || ancestor.alwaysUpdate)) {
30418
- this.bodyApplyGravity(body);
30419
- if (body.update(dt) === true) {
30420
- ancestor.isDirty = true;
30421
- }
30422
- this.detector.collisions(ancestor);
30423
- body.force.set(0, 0);
30424
- }
30425
- }
30426
- }
32192
+ if (this.physic !== "none") {
32193
+ this.adapter.step(dt);
32194
+ this.adapter.syncFromPhysics();
30427
32195
  }
30428
32196
  emit(WORLD_STEP, dt);
30429
32197
  }
@@ -31576,16 +33344,14 @@ var MaterialBatcher = class extends Batcher {
31576
33344
  );
31577
33345
  }
31578
33346
  } else if (typeof globalThis.OffscreenCanvas !== "undefined" && pixels instanceof globalThis.OffscreenCanvas) {
31579
- const imageBitmap = pixels.transferToImageBitmap();
31580
33347
  gl.texImage2D(
31581
33348
  gl.TEXTURE_2D,
31582
33349
  0,
31583
33350
  gl.RGBA,
31584
33351
  gl.RGBA,
31585
33352
  gl.UNSIGNED_BYTE,
31586
- imageBitmap
33353
+ pixels
31587
33354
  );
31588
- imageBitmap.close();
31589
33355
  } else {
31590
33356
  gl.texImage2D(
31591
33357
  gl.TEXTURE_2D,
@@ -34913,6 +36679,16 @@ function onresize(game2) {
34913
36679
  }
34914
36680
 
34915
36681
  // src/application/application.ts
36682
+ function resolvePhysicSetting(physic) {
36683
+ if (physic === "none") {
36684
+ return { adapter: void 0, physicLabel: "none" };
36685
+ }
36686
+ if (physic === void 0 || physic === "builtin") {
36687
+ return { adapter: void 0, physicLabel: "builtin" };
36688
+ }
36689
+ const adapter = typeof physic === "object" && "adapter" in physic ? physic.adapter : physic;
36690
+ return { adapter, physicLabel: adapter?.physicLabel ?? "builtin" };
36691
+ }
34916
36692
  var Application = class {
34917
36693
  /**
34918
36694
  * the parent HTML element holding the main canvas of this application
@@ -35135,10 +36911,30 @@ var Application = class {
35135
36911
  if (this.settings.consoleHeader) {
35136
36912
  consoleHeader(this);
35137
36913
  }
35138
- this.world = new World(0, 0, this.settings.width, this.settings.height);
36914
+ const { adapter, physicLabel } = resolvePhysicSetting(this.settings.physic);
36915
+ this.world = new World(
36916
+ 0,
36917
+ 0,
36918
+ this.settings.width,
36919
+ this.settings.height,
36920
+ adapter
36921
+ );
35139
36922
  this.world.app = this;
35140
- this.world.physic = this.settings.physic;
36923
+ this.world.physic = physicLabel;
35141
36924
  this.world.gpuTilemap = this.settings.gpuTilemap;
36925
+ if (this.settings.consoleHeader) {
36926
+ if (this.world.physic === "none") {
36927
+ console.log("physics: disabled");
36928
+ } else if (this.world.adapter) {
36929
+ const a = this.world.adapter;
36930
+ const label = a.name ?? a.constructor.name;
36931
+ const version2 = a.version ? ` ${a.version}` : "";
36932
+ const url = a.url ? ` | ${a.url}` : "";
36933
+ console.log(`physics: ${label}${version2}${url}`);
36934
+ } else {
36935
+ console.log("physics: enabled (no adapter)");
36936
+ }
36937
+ }
35142
36938
  if (this.settings.gpuTilemap && // duck-type rather than `instanceof WebGLRenderer` to avoid a
35143
36939
  // runtime import; only the WebGL renderer carries `WebGLVersion`
35144
36940
  this.renderer.WebGLVersion !== 2) {
@@ -36586,7 +38382,6 @@ var UIBaseElement = class extends Container {
36586
38382
  * @param _event - the event object
36587
38383
  * @returns return false if we need to stop propagating the event
36588
38384
  */
36589
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36590
38385
  onClick(_event) {
36591
38386
  return true;
36592
38387
  }
@@ -36620,14 +38415,12 @@ var UIBaseElement = class extends Container {
36620
38415
  * function called when the pointer is moved over the object
36621
38416
  * @param _event - the event object
36622
38417
  */
36623
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36624
38418
  onMove(_event) {
36625
38419
  }
36626
38420
  /**
36627
38421
  * function called when the pointer is over the object
36628
38422
  * @param _event - the event object
36629
38423
  */
36630
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36631
38424
  onOver(_event) {
36632
38425
  }
36633
38426
  /**
@@ -36649,7 +38442,6 @@ var UIBaseElement = class extends Container {
36649
38442
  * function called when the pointer is leaving the object area
36650
38443
  * @param _event - the event object
36651
38444
  */
36652
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36653
38445
  onOut(_event) {
36654
38446
  }
36655
38447
  /**
@@ -36670,7 +38462,6 @@ var UIBaseElement = class extends Container {
36670
38462
  * @param _event - the event object
36671
38463
  * @returns return false if we need to stop propagating the event
36672
38464
  */
36673
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36674
38465
  onRelease(_event) {
36675
38466
  return true;
36676
38467
  }
@@ -36835,7 +38626,6 @@ var UISpriteElement = class extends Sprite {
36835
38626
  * @param _event - the event object
36836
38627
  * @returns return false if we need to stop propagating the event
36837
38628
  */
36838
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36839
38629
  onClick(_event) {
36840
38630
  return false;
36841
38631
  }
@@ -36852,7 +38642,6 @@ var UISpriteElement = class extends Sprite {
36852
38642
  * function called when the pointer is over the object
36853
38643
  * @param _event - the event object
36854
38644
  */
36855
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36856
38645
  onOver(_event) {
36857
38646
  }
36858
38647
  /**
@@ -36869,7 +38658,6 @@ var UISpriteElement = class extends Sprite {
36869
38658
  * function called when the pointer is leaving the object area
36870
38659
  * @param _event - the event object
36871
38660
  */
36872
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36873
38661
  onOut(_event) {
36874
38662
  }
36875
38663
  /**
@@ -36890,7 +38678,6 @@ var UISpriteElement = class extends Sprite {
36890
38678
  * @param _event - the event object
36891
38679
  * @returns return false if we need to stop propagating the event
36892
38680
  */
36893
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
36894
38681
  onRelease(_event) {
36895
38682
  return false;
36896
38683
  }
@@ -37714,6 +39501,111 @@ var SepiaEffect = class extends ColorMatrixEffect {
37714
39501
  }
37715
39502
  };
37716
39503
 
39504
+ // src/video/webgl/effects/shine.js
39505
+ var ShineEffect = class extends ShaderEffect {
39506
+ /**
39507
+ * @param {import("../webgl_renderer.js").default} renderer - the current renderer instance
39508
+ * @param {object} [options] - effect options
39509
+ * @param {number[]} [options.color=[1.0, 1.0, 1.0]] - shine color as [r, g, b] (0.0–1.0)
39510
+ * @param {number} [options.speed=0.5] - sweeps per second
39511
+ * @param {number} [options.width=0.15] - glint half-width as a fraction of one tile (0.0–1.0)
39512
+ * @param {number} [options.intensity=0.5] - maximum highlight strength
39513
+ * @param {number} [options.angle=0.5] - sweep direction in radians (0 = horizontal L→R, π/2 = vertical T→B)
39514
+ * @param {number} [options.bands=1.0] - number of parallel glints (1 = single shine; ~14.5 mimics a coin's etched-rim look)
39515
+ * @param {number} [options.pulseDepth=0.0] - subtle base-brightness pulse amplitude (0 disables the pulse)
39516
+ * @param {number} [options.pulseSpeed=3.0] - pulse oscillation rate (radians/second)
39517
+ */
39518
+ constructor(renderer2, options = {}) {
39519
+ super(
39520
+ renderer2,
39521
+ `
39522
+ uniform vec3 uShineColor;
39523
+ uniform float uShineWidth;
39524
+ uniform float uShineSpeed;
39525
+ uniform float uShineIntensity;
39526
+ uniform float uShineAngle;
39527
+ uniform float uShineBands;
39528
+ uniform float uPulseDepth;
39529
+ uniform float uPulseSpeed;
39530
+ uniform float uTime;
39531
+ vec4 apply(vec4 color, vec2 uv) {
39532
+ if (color.a == 0.0) return color;
39533
+ // Optional brightness pulse on the base color.
39534
+ float pulse = (1.0 - uPulseDepth) + uPulseDepth * sin(uTime * uPulseSpeed);
39535
+ // Project uv along the sweep axis (uShineAngle).
39536
+ float pos = uv.x * cos(uShineAngle) + uv.y * sin(uShineAngle);
39537
+ // Tile by uShineBands so we get N parallel glints in unison.
39538
+ // localX and sweep both live in [0,1] tile-space; the
39539
+ // wrap-around distance (min(d, 1-d)) lets the glint exit
39540
+ // the right edge of a tile and immediately re-enter from
39541
+ // the left edge of the next, so there is NO perceived
39542
+ // pause between sweeps. Previous implementation padded
39543
+ // the sweep range to [-W, 1+W] which created a visible
39544
+ // off-screen gap of duration ~2W/(1+2W) per cycle.
39545
+ float localX = fract(pos * uShineBands);
39546
+ float sweep = fract(uTime * uShineSpeed);
39547
+ float d = abs(localX - sweep);
39548
+ float dist = min(d, 1.0 - d);
39549
+ float glint = smoothstep(uShineWidth, 0.0, dist) * uShineIntensity;
39550
+ // color.rgb is already premultiplied by alpha \u2014 multiply the
39551
+ // added glint by color.a so it stays premultiplied too, but
39552
+ // do NOT scale the whole result by color.a (that would dim
39553
+ // antialiased edges twice).
39554
+ vec3 result = color.rgb * pulse + uShineColor * glint * color.a;
39555
+ return vec4(result, color.a);
39556
+ }
39557
+ `
39558
+ );
39559
+ this.setUniform(
39560
+ "uShineColor",
39561
+ new Float32Array(options.color ?? [1, 1, 1])
39562
+ );
39563
+ this.setUniform("uShineWidth", options.width ?? 0.15);
39564
+ this.setUniform("uShineSpeed", options.speed ?? 0.5);
39565
+ this.setUniform("uShineIntensity", options.intensity ?? 0.5);
39566
+ this.setUniform("uShineAngle", options.angle ?? 0.5);
39567
+ this.setUniform("uShineBands", options.bands ?? 1);
39568
+ this.setUniform("uPulseDepth", options.pulseDepth ?? 0);
39569
+ this.setUniform("uPulseSpeed", options.pulseSpeed ?? 3);
39570
+ this.setUniform("uTime", 0);
39571
+ }
39572
+ /**
39573
+ * set the current time (call each frame for animation)
39574
+ * @param {number} time - time in seconds
39575
+ */
39576
+ setTime(time) {
39577
+ this.setUniform("uTime", time);
39578
+ }
39579
+ /**
39580
+ * set the shine color
39581
+ * @param {number[]} color - shine color as [r, g, b] (0.0–1.0)
39582
+ */
39583
+ setColor(color) {
39584
+ this.setUniform("uShineColor", new Float32Array(color));
39585
+ }
39586
+ /**
39587
+ * set the sweep speed
39588
+ * @param {number} value - sweeps per second
39589
+ */
39590
+ setSpeed(value) {
39591
+ this.setUniform("uShineSpeed", value);
39592
+ }
39593
+ /**
39594
+ * set the highlight intensity
39595
+ * @param {number} value - maximum highlight strength
39596
+ */
39597
+ setIntensity(value) {
39598
+ this.setUniform("uShineIntensity", value);
39599
+ }
39600
+ /**
39601
+ * set the number of parallel glints
39602
+ * @param {number} value - 1 for a single shine, >1 for tiled stripes
39603
+ */
39604
+ setBands(value) {
39605
+ this.setUniform("uShineBands", value);
39606
+ }
39607
+ };
39608
+
37717
39609
  // src/video/webgl/effects/tintPulse.js
37718
39610
  var TintPulseEffect = class extends ShaderEffect {
37719
39611
  /**
@@ -38385,6 +40277,7 @@ export {
38385
40277
  BlurEffect,
38386
40278
  Body,
38387
40279
  Bounds,
40280
+ BuiltinAdapter,
38388
40281
  CANVAS,
38389
40282
  Camera2d,
38390
40283
  CameraEffect,
@@ -38449,6 +40342,7 @@ export {
38449
40342
  SepiaEffect,
38450
40343
  ShaderEffect,
38451
40344
  ShakeEffect,
40345
+ ShineEffect,
38452
40346
  Sprite,
38453
40347
  Stage,
38454
40348
  TMXHexagonalRenderer,