melonjs 20.0.0 → 20.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * melonJS Game Engine - 20.0.0
2
+ * melonJS Game Engine - 20.1.1
3
3
  * http://www.melonjs.org
4
4
  * melonjs is licensed under the MIT License.
5
5
  * http://www.opensource.org/licenses/mit-license
@@ -6665,8 +6665,8 @@ function resetGUID(base, index = 0) {
6665
6665
  GUID_base = toHex(base.toUpperCase());
6666
6666
  GUID_index = index;
6667
6667
  }
6668
- function createGUID(index = 1) {
6669
- GUID_index += index;
6668
+ function createGUID(index) {
6669
+ GUID_index++;
6670
6670
  return `${GUID_base}-${index || GUID_index}`;
6671
6671
  }
6672
6672
  var isStringArray = (value) => {
@@ -12366,7 +12366,10 @@ var Timer = class {
12366
12366
  * @ignore
12367
12367
  */
12368
12368
  updateTimers() {
12369
- for (const timer2 of this.timers) {
12369
+ for (const timer2 of [...this.timers]) {
12370
+ if (!this.timers.includes(timer2)) {
12371
+ continue;
12372
+ }
12370
12373
  if (!(timer2.pauseable && state_default.isPaused())) {
12371
12374
  timer2.elapsed += this.delta;
12372
12375
  }
@@ -13193,9 +13196,15 @@ var FadeEffect = class extends CameraEffect {
13193
13196
  }
13194
13197
  }
13195
13198
  destroy() {
13196
- this.tween.stop();
13197
- tweenPool.release(this.tween);
13198
- colorPool.release(this.color);
13199
+ if (this.tween !== void 0) {
13200
+ this.tween.stop();
13201
+ tweenPool.release(this.tween);
13202
+ this.tween = void 0;
13203
+ }
13204
+ if (this.color !== void 0) {
13205
+ colorPool.release(this.color);
13206
+ this.color = void 0;
13207
+ }
13199
13208
  }
13200
13209
  };
13201
13210
 
@@ -13594,13 +13603,22 @@ var MaskEffect = class extends CameraEffect {
13594
13603
  r.restore();
13595
13604
  }
13596
13605
  destroy() {
13597
- this.tween.stop();
13598
- tweenPool.release(this.tween);
13599
- colorPool.release(this.color);
13600
- if (this._maskShape.type === "Ellipse") {
13601
- ellipsePool.release(this._maskShape);
13602
- } else {
13603
- polygonPool.release(this._maskShape);
13606
+ if (this.tween !== void 0) {
13607
+ this.tween.stop();
13608
+ tweenPool.release(this.tween);
13609
+ this.tween = void 0;
13610
+ }
13611
+ if (this.color !== void 0) {
13612
+ colorPool.release(this.color);
13613
+ this.color = void 0;
13614
+ }
13615
+ if (this._maskShape !== void 0) {
13616
+ if (this._maskShape.type === "Ellipse") {
13617
+ ellipsePool.release(this._maskShape);
13618
+ } else {
13619
+ polygonPool.release(this._maskShape);
13620
+ }
13621
+ this._maskShape = void 0;
13604
13622
  }
13605
13623
  }
13606
13624
  };
@@ -16678,6 +16696,7 @@ var collision = {
16678
16696
  };
16679
16697
 
16680
16698
  // src/physics/builtin/body.js
16699
+ var shapeContactIdCounter = 0;
16681
16700
  var Body = class {
16682
16701
  /**
16683
16702
  * @param {Renderable|Container|Entity|Sprite|NineSliceSprite} ancestor - the parent object this body is attached to
@@ -17046,6 +17065,9 @@ var Body = class {
17046
17065
  * @private
17047
17066
  */
17048
17067
  _initShapeCollision(stored, source) {
17068
+ if (stored._contactId === void 0) {
17069
+ stored._contactId = ++shapeContactIdCounter;
17070
+ }
17049
17071
  if (source === void 0) {
17050
17072
  stored.collisionType = void 0;
17051
17073
  stored.collisionMask = void 0;
@@ -17502,6 +17524,7 @@ var Body = class {
17502
17524
  shape.collisionMask = void 0;
17503
17525
  shape.isTrigger = false;
17504
17526
  shape.isActive = true;
17527
+ shape._contactId = void 0;
17505
17528
  if (shape instanceof Point) {
17506
17529
  pointPool.release(shape);
17507
17530
  } else if (shape instanceof Line) {
@@ -17587,7 +17610,7 @@ var Renderable = class _Renderable extends Rect {
17587
17610
  */
17588
17611
  get parentApp() {
17589
17612
  if (!this._parentApp && this.ancestor) {
17590
- this._parentApp = this.ancestor.getRootAncestor().app;
17613
+ this._parentApp = this.ancestor.getRootAncestor()?.app;
17591
17614
  }
17592
17615
  return this._parentApp;
17593
17616
  }
@@ -21056,6 +21079,9 @@ var FrameAnimation = class {
21056
21079
  this.resetAnim = () => {
21057
21080
  if (typeof onComplete === "function") {
21058
21081
  onComplete();
21082
+ if (!this._isCurrentAnimationLive()) {
21083
+ return false;
21084
+ }
21059
21085
  }
21060
21086
  this.setCurrentAnimation(next, null, true);
21061
21087
  };
@@ -21063,6 +21089,9 @@ var FrameAnimation = class {
21063
21089
  this.resetAnim = () => {
21064
21090
  if (typeof onComplete === "function") {
21065
21091
  onComplete();
21092
+ if (!this._isCurrentAnimationLive()) {
21093
+ return false;
21094
+ }
21066
21095
  }
21067
21096
  this._animDone = true;
21068
21097
  return false;
@@ -21148,6 +21177,26 @@ var FrameAnimation = class {
21148
21177
  * @param {number} id - the frame id
21149
21178
  * @returns {object} the frame data
21150
21179
  */
21180
+ /**
21181
+ * Whether the current animation is still resolvable.
21182
+ *
21183
+ * `update()` hands control to user code in the middle of its frame loop:
21184
+ * `onended`, and the completion callback behind `resetAnim`. That code is
21185
+ * free to tear the host down — "remove me once the animation finishes" is
21186
+ * an entirely reasonable reaction, and `removeChildNow()` destroys the
21187
+ * sprite, whose `destroy()` empties `anim`. Everything after the callback
21188
+ * reads out of `anim`, so the loop has to check it is still there rather
21189
+ * than resume into a map the callback just cleared.
21190
+ *
21191
+ * A callback that switches animations with `setCurrentAnimation()` leaves
21192
+ * this true, which is the point: only a genuinely torn-down host stops the
21193
+ * loop.
21194
+ * @ignore
21195
+ * @returns {boolean} true while the current animation can still be read
21196
+ */
21197
+ _isCurrentAnimationLive() {
21198
+ return this.current !== void 0 && this.current !== null && this.anim[this.current.name] !== void 0;
21199
+ }
21151
21200
  getAnimationFrameObjectByIndex(id) {
21152
21201
  return this.anim[this.current.name].frames[id];
21153
21202
  }
@@ -21181,6 +21230,9 @@ var FrameAnimation = class {
21181
21230
  */
21182
21231
  update(dt) {
21183
21232
  let changed = false;
21233
+ if (!this._isCurrentAnimationLive()) {
21234
+ return changed;
21235
+ }
21184
21236
  if (!this.animationpause && !this._animDone && this.current.length > 1) {
21185
21237
  let duration = this.getAnimationFrameObjectByIndex(
21186
21238
  this.current.idx
@@ -21194,9 +21246,16 @@ var FrameAnimation = class {
21194
21246
  if (this.current.idx === 0) {
21195
21247
  if (typeof this.host.onended === "function") {
21196
21248
  this.host.onended();
21249
+ if (!this._isCurrentAnimationLive()) {
21250
+ break;
21251
+ }
21197
21252
  }
21198
21253
  if (typeof this.resetAnim === "function") {
21199
- if (this.resetAnim() === false) {
21254
+ const keepPlaying = this.resetAnim();
21255
+ if (!this._isCurrentAnimationLive()) {
21256
+ break;
21257
+ }
21258
+ if (keepPlaying === false) {
21200
21259
  this.setAnimationFrame(this.current.length - 1);
21201
21260
  this.dt %= duration;
21202
21261
  break;
@@ -23294,11 +23353,13 @@ function preloadBinary(data2, onload2, onerror, settings) {
23294
23353
  // src/loader/parsers/fontface.js
23295
23354
  function preloadFontFace(data2, onload2, onerror) {
23296
23355
  const fontFaceSet = typeof globalThis.document !== "undefined" ? globalThis.document.fonts : void 0;
23297
- if (!data2.src.startsWith("url(") && !data2.src.startsWith("local(")) {
23298
- data2.src = "url(" + data2.src + ")";
23356
+ let src = data2.src;
23357
+ if (!src.startsWith("url(") && !src.startsWith("local(")) {
23358
+ const escaped = src.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
23359
+ src = `url('${escaped}')`;
23299
23360
  }
23300
23361
  if (typeof fontFaceSet !== "undefined") {
23301
- const font = new FontFace(data2.name, data2.src);
23362
+ const font = new FontFace(data2.name, src);
23302
23363
  font.load().then(
23303
23364
  () => {
23304
23365
  fontList[data2.name] = font;
@@ -26691,9 +26752,12 @@ var Container = class _Container extends Renderable {
26691
26752
  let isFloating = false;
26692
26753
  const isPaused = state_default.isPaused();
26693
26754
  const children = this.getChildren();
26694
- const childrenLength = children.length;
26695
26755
  const cameras = state_default.current().cameras;
26696
- for (let i = childrenLength, obj; i--, obj = children[i]; ) {
26756
+ const pass = children.slice();
26757
+ for (let i = pass.length, obj; i--, obj = pass[i]; ) {
26758
+ if (obj.ancestor !== this) {
26759
+ continue;
26760
+ }
26697
26761
  if (isPaused && !obj.updateWhenPaused) {
26698
26762
  continue;
26699
26763
  }
@@ -27033,6 +27097,9 @@ var GLTFModel = class extends Container {
27033
27097
  this.resetAnim = () => {
27034
27098
  if (typeof onComplete === "function") {
27035
27099
  onComplete();
27100
+ if (!this._isLive()) {
27101
+ return false;
27102
+ }
27036
27103
  }
27037
27104
  this.setCurrentAnimation(next);
27038
27105
  };
@@ -27040,6 +27107,9 @@ var GLTFModel = class extends Container {
27040
27107
  this.resetAnim = () => {
27041
27108
  if (typeof onComplete === "function") {
27042
27109
  onComplete();
27110
+ if (!this._isLive()) {
27111
+ return false;
27112
+ }
27043
27113
  }
27044
27114
  this._animDone = true;
27045
27115
  return false;
@@ -27119,6 +27189,9 @@ var GLTFModel = class extends Container {
27119
27189
  if (this.current.time >= duration) {
27120
27190
  if (typeof this.onended === "function") {
27121
27191
  this.onended();
27192
+ if (!this._isLive()) {
27193
+ return super.update(dt);
27194
+ }
27122
27195
  }
27123
27196
  if (typeof this.resetAnim === "function") {
27124
27197
  if (this.resetAnim() === false) {
@@ -27130,6 +27203,9 @@ var GLTFModel = class extends Container {
27130
27203
  this.current.time %= duration;
27131
27204
  }
27132
27205
  }
27206
+ if (!this._isLive()) {
27207
+ return super.update(dt);
27208
+ }
27133
27209
  this._pose();
27134
27210
  this.isDirty = true;
27135
27211
  }
@@ -27141,6 +27217,24 @@ var GLTFModel = class extends Container {
27141
27217
  * not animate use their cached rest matrix.
27142
27218
  * @ignore
27143
27219
  */
27220
+ /**
27221
+ * Whether this model is still alive.
27222
+ *
27223
+ * `update()` hands control to user code — `onended`, and the completion
27224
+ * callback behind `resetAnim` — and then poses the hierarchy. A callback
27225
+ * that removed the model (a death animation removing its own model is the
27226
+ * obvious case) has had `Renderable.destroy()` run, which releases `pos` to
27227
+ * the pool and leaves it undefined on every part. `_pose()` writes
27228
+ * `mesh.pos`, so it must not run on a torn-down model.
27229
+ *
27230
+ * `pos` and not `ancestor`: a model that was never added to a container is
27231
+ * perfectly poseable, and several callers do exactly that.
27232
+ * @ignore
27233
+ * @returns {boolean} true while the model can still be posed
27234
+ */
27235
+ _isLive() {
27236
+ return this.pos !== void 0;
27237
+ }
27144
27238
  _pose() {
27145
27239
  const clip = this.current.name ? this.anim[this.current.name] : null;
27146
27240
  const t = this.current.time;
@@ -47811,6 +47905,56 @@ var Detector = class {
47811
47905
  isTriggerContact: false
47812
47906
  }
47813
47907
  ];
47908
+ this._activeShapePairs = /* @__PURE__ */ new Map();
47909
+ this._frameShapeSeen = /* @__PURE__ */ new Map();
47910
+ this._shapeViews = [
47911
+ {
47912
+ a: null,
47913
+ b: null,
47914
+ shapeA: null,
47915
+ shapeB: null,
47916
+ overlap: 0,
47917
+ overlapN: { x: 0, y: 0 },
47918
+ overlapV: { x: 0, y: 0 },
47919
+ normal: { x: 0, y: 0 },
47920
+ depth: 0,
47921
+ overlapNZ: 0,
47922
+ overlapZ: 0,
47923
+ normalZ: 0,
47924
+ indexShapeA: -1,
47925
+ indexShapeB: -1,
47926
+ isTrigger: false
47927
+ },
47928
+ {
47929
+ a: null,
47930
+ b: null,
47931
+ shapeA: null,
47932
+ shapeB: null,
47933
+ overlap: 0,
47934
+ overlapN: { x: 0, y: 0 },
47935
+ overlapV: { x: 0, y: 0 },
47936
+ normal: { x: 0, y: 0 },
47937
+ depth: 0,
47938
+ overlapNZ: 0,
47939
+ overlapZ: 0,
47940
+ normalZ: 0,
47941
+ indexShapeA: -1,
47942
+ indexShapeB: -1,
47943
+ isTrigger: false
47944
+ }
47945
+ ];
47946
+ this._contactObjA = null;
47947
+ this._contactObjB = null;
47948
+ this._onShapeContact = (shapeA, indexA, shapeB, indexB, isTrigger, res) => {
47949
+ this._dispatchShapeContact(
47950
+ shapeA,
47951
+ indexA,
47952
+ shapeB,
47953
+ indexB,
47954
+ isTrigger,
47955
+ res
47956
+ );
47957
+ };
47814
47958
  }
47815
47959
  /**
47816
47960
  * Populate one of the pooled symmetric views from a SAT response.
@@ -47869,6 +48013,7 @@ var Detector = class {
47869
48013
  */
47870
48014
  beginFrame() {
47871
48015
  this._frameSeen.clear();
48016
+ this._frameShapeSeen.clear();
47872
48017
  }
47873
48018
  /**
47874
48019
  * Called by the adapter at the end of a physics step. Diffs the
@@ -47895,9 +48040,58 @@ var Detector = class {
47895
48040
  b.onCollisionEnd(void 0, a);
47896
48041
  }
47897
48042
  }
48043
+ for (const [key, entry2] of this._activeShapePairs) {
48044
+ if (this._frameShapeSeen.has(key)) {
48045
+ continue;
48046
+ }
48047
+ const [a, b, shapeA, shapeB] = entry2;
48048
+ const aAttached = a.ancestor != null;
48049
+ const bAttached = b.ancestor != null;
48050
+ if (!aAttached && !bAttached) {
48051
+ continue;
48052
+ }
48053
+ if (aAttached && typeof a.onShapeCollisionEnd === "function") {
48054
+ a.onShapeCollisionEnd(this._fillEndedView(0, a, b, shapeA, shapeB), b);
48055
+ }
48056
+ if (bAttached && typeof b.onShapeCollisionEnd === "function") {
48057
+ b.onShapeCollisionEnd(this._fillEndedView(1, b, a, shapeB, shapeA), a);
48058
+ }
48059
+ }
47898
48060
  const prev = this._activePairs;
47899
48061
  this._activePairs = this._frameSeen;
47900
48062
  this._frameSeen = prev;
48063
+ const prevShapes = this._activeShapePairs;
48064
+ this._activeShapePairs = this._frameShapeSeen;
48065
+ this._frameShapeSeen = prevShapes;
48066
+ this._frameShapeSeen.clear();
48067
+ }
48068
+ /**
48069
+ * Identity-only shape-contact view for `onShapeCollisionEnd`: the shapes
48070
+ * have separated, so every geometric field is zeroed rather than left
48071
+ * holding the last overlap, which would read as a live contact.
48072
+ * @ignore
48073
+ */
48074
+ _fillEndedView(slot, receiver, partner, ownShape, otherShape) {
48075
+ const view = this._shapeViews[slot];
48076
+ view.a = receiver;
48077
+ view.b = partner;
48078
+ view.shapeA = ownShape;
48079
+ view.shapeB = otherShape;
48080
+ view.indexShapeA = receiver.body?.shapes.indexOf(ownShape) ?? -1;
48081
+ view.indexShapeB = partner.body?.shapes.indexOf(otherShape) ?? -1;
48082
+ view.isTrigger = ownShape?.isTrigger === true || otherShape?.isTrigger === true;
48083
+ view.overlap = 0;
48084
+ view.depth = 0;
48085
+ view.overlapN.x = 0;
48086
+ view.overlapN.y = 0;
48087
+ view.overlapV.x = 0;
48088
+ view.overlapV.y = 0;
48089
+ view.normal.x = 0;
48090
+ view.normal.y = 0;
48091
+ view.overlapNZ = 0;
48092
+ view.overlapZ = 0;
48093
+ view.normalZ = 0;
48094
+ return view;
47901
48095
  }
47902
48096
  /**
47903
48097
  * Build a stable order-independent key for a pair of renderables,
@@ -47913,6 +48107,188 @@ var Detector = class {
47913
48107
  }
47914
48108
  return ga < gb ? `${ga}|${gb}` : `${gb}|${ga}`;
47915
48109
  }
48110
+ /**
48111
+ * Re-run the narrowphase for a remembered shape pair and repopulate
48112
+ * `response` from it.
48113
+ *
48114
+ * The scan clears `response` on every test, so whichever pair won has to be
48115
+ * measured again before handlers and the solver read it. Indices are looked
48116
+ * up here rather than carried from the scan because an enumeration handler
48117
+ * may have mutated either body in between: `removeShape()` re-indexes, and
48118
+ * `destroy()` empties the array outright. A shape that is no longer on its
48119
+ * body has no contact left to resolve, so this reports failure and the
48120
+ * caller falls through instead of throwing.
48121
+ * @ignore
48122
+ * @returns {boolean} true when the pair was re-measured
48123
+ */
48124
+ _retest(bodyA, bodyB, shapeA, shapeB, response) {
48125
+ const indexA = bodyA.shapes.indexOf(shapeA);
48126
+ const indexB = bodyB.shapes.indexOf(shapeB);
48127
+ if (indexA < 0 || indexB < 0) {
48128
+ return false;
48129
+ }
48130
+ const test = SAT_LOOKUP[shapeA.type + shapeB.type];
48131
+ if (test === void 0) {
48132
+ return false;
48133
+ }
48134
+ test.call(
48135
+ this,
48136
+ bodyA.ancestor,
48137
+ shapeA,
48138
+ bodyB.ancestor,
48139
+ shapeB,
48140
+ response.clear()
48141
+ );
48142
+ response.indexShapeA = indexA;
48143
+ response.indexShapeB = indexB;
48144
+ return true;
48145
+ }
48146
+ /**
48147
+ * Stable, order-independent key for one SHAPE pair (#1596).
48148
+ *
48149
+ * The renderable GUIDs order the pair, exactly as `_pairKey` does, and the
48150
+ * two shape ids MUST swap alongside them: keying `guidA|guidB|idA|idB`
48151
+ * without that swap gives the same physical contact two different keys
48152
+ * depending on which object the outer loop visited first, and the contact
48153
+ * would then End and Start every step.
48154
+ *
48155
+ * Shape identity is `_contactId`, stamped once in `Body#addShape` and never
48156
+ * derived from array position, because `removeShape()` re-indexes every
48157
+ * surviving shape.
48158
+ * @ignore
48159
+ */
48160
+ _shapePairKey(a, b, shapeA, shapeB) {
48161
+ const ga = a.GUID;
48162
+ const gb = b.GUID;
48163
+ const sa = shapeA?._contactId;
48164
+ const sb = shapeB?._contactId;
48165
+ if (ga === void 0 || gb === void 0 || sa === void 0 || sb === void 0) {
48166
+ return void 0;
48167
+ }
48168
+ const left = `${ga}#${sa}`;
48169
+ const right = `${gb}#${sb}`;
48170
+ return left < right ? `${left}|${right}` : `${right}|${left}`;
48171
+ }
48172
+ /**
48173
+ * True when an object subscribes to any shape-contact event.
48174
+ *
48175
+ * This is the opt-in gate: `collides()` only runs the full M x N scan when
48176
+ * this returns true for one of the two objects, so an application that
48177
+ * never declares these handlers performs exactly the narrowphase work it
48178
+ * did before the feature existed.
48179
+ * @ignore
48180
+ */
48181
+ _wantsShapeContacts(obj) {
48182
+ return typeof obj.onShapeCollisionStart === "function" || typeof obj.onShapeCollisionActive === "function" || typeof obj.onShapeCollisionEnd === "function";
48183
+ }
48184
+ /**
48185
+ * Populate a pooled shape-contact view, same flip convention as
48186
+ * `_fillSymView`: `flip=false` builds the view for `response.a`'s side,
48187
+ * `flip=true` for `response.b`'s, so `shapeA` is always the receiver's.
48188
+ * @ignore
48189
+ */
48190
+ _fillShapeView(slot, satResponse, flip, shapeA, shapeB, isTrigger) {
48191
+ const view = this._shapeViews[slot];
48192
+ const oN = satResponse.overlapN;
48193
+ const oV = satResponse.overlapV;
48194
+ const oNZ = satResponse.overlapNZ;
48195
+ const oZ = satResponse.overlapZ;
48196
+ view.overlap = satResponse.overlap;
48197
+ view.depth = satResponse.overlap;
48198
+ view.isTrigger = isTrigger === true;
48199
+ if (flip) {
48200
+ view.a = satResponse.b;
48201
+ view.b = satResponse.a;
48202
+ view.shapeA = shapeB;
48203
+ view.shapeB = shapeA;
48204
+ view.indexShapeA = satResponse.indexShapeB;
48205
+ view.indexShapeB = satResponse.indexShapeA;
48206
+ view.overlapN.x = -oN.x;
48207
+ view.overlapN.y = -oN.y;
48208
+ view.overlapV.x = -oV.x;
48209
+ view.overlapV.y = -oV.y;
48210
+ view.overlapNZ = -oNZ;
48211
+ view.overlapZ = -oZ;
48212
+ view.normal.x = oN.x;
48213
+ view.normal.y = oN.y;
48214
+ view.normalZ = oNZ;
48215
+ } else {
48216
+ view.a = satResponse.a;
48217
+ view.b = satResponse.b;
48218
+ view.shapeA = shapeA;
48219
+ view.shapeB = shapeB;
48220
+ view.indexShapeA = satResponse.indexShapeA;
48221
+ view.indexShapeB = satResponse.indexShapeB;
48222
+ view.overlapN.x = oN.x;
48223
+ view.overlapN.y = oN.y;
48224
+ view.overlapV.x = oV.x;
48225
+ view.overlapV.y = oV.y;
48226
+ view.overlapNZ = oNZ;
48227
+ view.overlapZ = oZ;
48228
+ view.normal.x = -oN.x;
48229
+ view.normal.y = -oN.y;
48230
+ view.normalZ = -oNZ;
48231
+ }
48232
+ return view;
48233
+ }
48234
+ /**
48235
+ * Fire Start / Active for one overlapping shape pair. Called back from
48236
+ * `collides()` once per pair, in scan order.
48237
+ * @ignore
48238
+ */
48239
+ _dispatchShapeContact(shapeA, shapeB, isTrigger, response) {
48240
+ const objA = this._contactObjA;
48241
+ const objB = this._contactObjB;
48242
+ const key = this._shapePairKey(objA, objB, shapeA, shapeB);
48243
+ if (key === void 0 || this._frameShapeSeen.has(key)) {
48244
+ return;
48245
+ }
48246
+ this._frameShapeSeen.set(key, [objA, objB, shapeA, shapeB]);
48247
+ const isEntry = !this._activeShapePairs.has(key);
48248
+ const viewA = this._fillShapeView(
48249
+ 0,
48250
+ response,
48251
+ false,
48252
+ shapeA,
48253
+ shapeB,
48254
+ isTrigger
48255
+ );
48256
+ const viewB = this._fillShapeView(
48257
+ 1,
48258
+ response,
48259
+ true,
48260
+ shapeA,
48261
+ shapeB,
48262
+ isTrigger
48263
+ );
48264
+ if (isEntry) {
48265
+ if (typeof objA.onShapeCollisionStart === "function") {
48266
+ objA.onShapeCollisionStart(viewA, objB);
48267
+ }
48268
+ if (typeof objB.onShapeCollisionStart === "function") {
48269
+ objB.onShapeCollisionStart(viewB, objA);
48270
+ }
48271
+ }
48272
+ if (typeof objA.onShapeCollisionActive === "function") {
48273
+ objA.onShapeCollisionActive(viewA, objB);
48274
+ }
48275
+ if (typeof objB.onShapeCollisionActive === "function") {
48276
+ objB.onShapeCollisionActive(viewB, objA);
48277
+ }
48278
+ }
48279
+ /**
48280
+ * Drop the references the enumeration callback reads.
48281
+ *
48282
+ * They are plain fields rather than a per-pair closure so the hot path
48283
+ * allocates nothing, which means they outlive the pair unless cleared, and
48284
+ * would hold two renderables (and everything they reach) against garbage
48285
+ * collection until the next subscribed pair happened to overwrite them.
48286
+ * @ignore
48287
+ */
48288
+ _clearContactPair() {
48289
+ this._contactObjA = null;
48290
+ this._contactObjB = null;
48291
+ }
47916
48292
  /**
47917
48293
  * determine if two objects should collide (based on both respective objects body collision mask and type).<br>
47918
48294
  * you can redefine this function if you need any specific rules over what should collide with what.
@@ -47931,9 +48307,12 @@ var Detector = class {
47931
48307
  * @param {Body} bodyB - a reference to body B.
47932
48308
  * @returns {boolean} true if colliding
47933
48309
  */
47934
- collides(bodyA, bodyB, response = this.response) {
47935
- let triggerIndexA = -1;
47936
- let triggerIndexB = -1;
48310
+ collides(bodyA, bodyB, response = this.response, onContact = void 0) {
48311
+ const enumerate = onContact !== void 0;
48312
+ let solidShapeA = null;
48313
+ let solidShapeB = null;
48314
+ let triggerShapeA = null;
48315
+ let triggerShapeB = null;
47937
48316
  for (let indexA = bodyA.shapes.length, shapeA; indexA--, shapeA = bodyA.shapes[indexA]; ) {
47938
48317
  for (let indexB = bodyB.shapes.length, shapeB; indexB--, shapeB = bodyB.shapes[indexB]; ) {
47939
48318
  if (shapeA.isActive === false || shapeB.isActive === false) {
@@ -47968,10 +48347,24 @@ var Detector = class {
47968
48347
  // clear response object before reusing
47969
48348
  response.clear()
47970
48349
  ) === true) {
47971
- if (shapeA.isTrigger === true || shapeB.isTrigger === true) {
47972
- if (triggerIndexA < 0) {
47973
- triggerIndexA = indexA;
47974
- triggerIndexB = indexB;
48350
+ const pairIsTrigger = shapeA.isTrigger === true || shapeB.isTrigger === true;
48351
+ if (enumerate === true) {
48352
+ response.indexShapeA = indexA;
48353
+ response.indexShapeB = indexB;
48354
+ response.isTriggerContact = pairIsTrigger;
48355
+ onContact(shapeA, shapeB, pairIsTrigger, response);
48356
+ }
48357
+ if (pairIsTrigger) {
48358
+ if (triggerShapeA === null) {
48359
+ triggerShapeA = shapeA;
48360
+ triggerShapeB = shapeB;
48361
+ }
48362
+ continue;
48363
+ }
48364
+ if (enumerate === true) {
48365
+ if (solidShapeA === null) {
48366
+ solidShapeA = shapeA;
48367
+ solidShapeB = shapeB;
47975
48368
  }
47976
48369
  continue;
47977
48370
  }
@@ -47981,22 +48374,15 @@ var Detector = class {
47981
48374
  }
47982
48375
  }
47983
48376
  }
47984
- if (triggerIndexA >= 0) {
47985
- const shapeA = bodyA.shapes[triggerIndexA];
47986
- const shapeB = bodyB.shapes[triggerIndexB];
47987
- SAT_LOOKUP[shapeA.type + shapeB.type].call(
47988
- this,
47989
- bodyA.ancestor,
47990
- shapeA,
47991
- bodyB.ancestor,
47992
- shapeB,
47993
- response.clear()
47994
- );
47995
- response.indexShapeA = triggerIndexA;
47996
- response.indexShapeB = triggerIndexB;
47997
- response.isTriggerContact = true;
48377
+ if (solidShapeA !== null && this._retest(bodyA, bodyB, solidShapeA, solidShapeB, response)) {
47998
48378
  return true;
47999
48379
  }
48380
+ if (triggerShapeA !== null) {
48381
+ if (this._retest(bodyA, bodyB, triggerShapeA, triggerShapeB, response)) {
48382
+ response.isTriggerContact = true;
48383
+ return true;
48384
+ }
48385
+ }
48000
48386
  return false;
48001
48387
  }
48002
48388
  /**
@@ -48016,7 +48402,23 @@ var Detector = class {
48016
48402
  boundsB.addBounds(objB.getBounds(), true);
48017
48403
  boundsB.addBounds(objB.body.getBounds());
48018
48404
  if (boundsA.overlaps(boundsB)) {
48019
- if (this.collides(objA.body, objB.body)) {
48405
+ const wantsContacts = this._wantsShapeContacts(objA) || this._wantsShapeContacts(objB);
48406
+ let onContact;
48407
+ if (wantsContacts === true) {
48408
+ this._contactObjA = objA;
48409
+ this._contactObjB = objB;
48410
+ onContact = this._onShapeContact;
48411
+ }
48412
+ const didCollide = this.collides(
48413
+ objA.body,
48414
+ objB.body,
48415
+ this.response,
48416
+ onContact
48417
+ );
48418
+ if (wantsContacts === true) {
48419
+ this._clearContactPair();
48420
+ }
48421
+ if (didCollide) {
48020
48422
  collisionCounter++;
48021
48423
  const pairKey = this._pairKey(objA, objB);
48022
48424
  const firstVisitThisFrame = pairKey !== void 0 && !this._frameSeen.has(pairKey);
@@ -48040,14 +48442,23 @@ var Detector = class {
48040
48442
  objB.onCollisionActive(viewB, objA);
48041
48443
  }
48042
48444
  }
48445
+ if (objA.body === void 0 || objB.body === void 0) {
48446
+ continue;
48447
+ }
48043
48448
  const eitherSensor = objA.body.isSensor === true || objB.body.isSensor === true || this.response.isTriggerContact === true;
48044
48449
  const aHasModern = typeof objA.onCollisionActive === "function";
48045
48450
  const bHasModern = typeof objB.onCollisionActive === "function";
48046
48451
  const aOptsOut = !aHasModern && typeof objA.onCollision === "function" && objA.onCollision(this.response, objB) === false;
48452
+ if (objA.body === void 0 || objB.body === void 0) {
48453
+ continue;
48454
+ }
48047
48455
  if (!aOptsOut && objA.body.isStatic === false && !eitherSensor) {
48048
48456
  objA.body.respondToCollision.call(objA.body, this.response);
48049
48457
  }
48050
48458
  const bOptsOut = !bHasModern && typeof objB.onCollision === "function" && objB.onCollision(this.response, objA) === false;
48459
+ if (objA.body === void 0 || objB.body === void 0) {
48460
+ continue;
48461
+ }
48051
48462
  if (!bOptsOut && objB.body.isStatic === false && !eitherSensor) {
48052
48463
  objB.body.respondToCollision.call(objB.body, this.response);
48053
48464
  }
@@ -48323,8 +48734,10 @@ var BuiltinAdapter = class {
48323
48734
  this.detector.collisions(ancestor);
48324
48735
  }
48325
48736
  }
48326
- body.force.set(0, 0);
48327
- body.forceZ = 0;
48737
+ if (body.force !== void 0) {
48738
+ body.force.set(0, 0);
48739
+ body.forceZ = 0;
48740
+ }
48328
48741
  }
48329
48742
  this.detector.endFrame();
48330
48743
  }
@@ -50064,7 +50477,7 @@ var Trigger = class extends Renderable {
50064
50477
  };
50065
50478
 
50066
50479
  // src/version.ts
50067
- var version = "20.0.0";
50480
+ var version = "20.1.1";
50068
50481
 
50069
50482
  // src/system/bootstrap.ts
50070
50483
  var initialized = false;