@flighthq/physics2d 0.4.0-next.1815.e47cc3c → 0.4.0-next.1821.efb1cbc

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.
@@ -1 +1 @@
1
- {"version":3,"file":"enablePhysics2DGuards.d.ts","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"AAaA,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAK7C;AAwBD,wBAAgB,qBAAqB,IAAI,IAAI,CAK5C"}
1
+ {"version":3,"file":"enablePhysics2DGuards.d.ts","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"AAaA,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED,wBAAgB,sBAAsB,IAAI,IAAI,CAK7C;AAyBD,wBAAgB,qBAAqB,IAAI,IAAI,CAK5C"}
@@ -29,10 +29,11 @@ export function disablePhysics2DGuards() {
29
29
  // precisely so unused solvers tree-shake out, which makes forgetting `registerBuiltInPhysics2DJointSolvers`
30
30
  // an ordinary mistake rather than an exotic one.
31
31
  //
32
- // The third is a collider the contact dispatcher has no arm for. `collideContactManifold2D` is a closed
33
- // switch, so unlike 3D there is nothing to REGISTER and no way to forget to — but its coverage is
34
- // narrower than the collider type allows, and a kind it does not answer for is reported as
35
- // non-overlapping. A body carrying one falls through the floor with nothing failing anywhere.
32
+ // The third is a collider the contact dispatcher has no arm for — in practice an area-less `segment` or
33
+ // `point` used as a rigid body's shape. `collideContactManifold2D` is a closed switch, so unlike 3D there
34
+ // is nothing to REGISTER and no way to forget to, but its coverage is narrower than the collider type
35
+ // allows and a kind it does not answer for is reported as non-overlapping. A body carrying one falls
36
+ // through the floor with nothing failing anywhere.
36
37
  //
37
38
  // Applications that never import this module shed both the message text and `@flighthq/log`.
38
39
  export function enablePhysics2DGuards() {
@@ -41,31 +42,6 @@ export function enablePhysics2DGuards() {
41
42
  setPhysics2DContactIntakeGuard(warnOnUndetectablePhysics2DColliders);
42
43
  physics2DGuardsEnabled = true;
43
44
  }
44
- // Warns once per distinct set of undetectable collider kinds.
45
- //
46
- // The advice differs by kind, because the two causes are not the same problem. `segment` and `point` are
47
- // area-less by definition and carry no contact to find, so putting one on a rigid body is a modelling
48
- // mistake with no fix in this package. A `capsule` has no pair functions YET, which is a gap rather than
49
- // a rule — and a caller deserves to be told which of the two it has hit.
50
- function warnOnUndetectablePhysics2DColliders(world) {
51
- const explanation = explainPhysics2DCollision(world);
52
- if (explanation.status === 'ready')
53
- return;
54
- const kinds = explanation.unsupportedKinds;
55
- const arealess = kinds.filter((kind) => kind === 'segment' || kind === 'point');
56
- const unimplemented = kinds.filter((kind) => kind !== 'segment' && kind !== 'point');
57
- const areaFix = arealess.length > 0
58
- ? ` ${arealess.join(' and ')} carr${arealess.length === 1 ? 'ies' : 'y'} no area and so no contact, and cannot be a rigid body's collider — give the body a shape with area.`
59
- : '';
60
- const gapFix = unimplemented.length > 0
61
- ? ` collideContactManifold2D has no pair functions for ${unimplemented.join(', ')} yet, so ${unimplemented.length === 1 ? 'it is' : 'they are'} usable for queries and raycasts but not as a simulated collider.`
62
- : '';
63
- logOnce(`physics2d:${explanation.status}:${kinds.join(',')}`, LogLevel.Warn, {
64
- kinds,
65
- message: `stepPhysics2D: collider kind${kinds.length === 1 ? '' : 's'} ${kinds.join(', ')} generate${kinds.length === 1 ? 's' : ''} no contacts, so the bodies carrying ${kinds.length === 1 ? 'it' : 'them'} pass through everything.${areaFix}${gapFix} Call explainPhysics2DCollision(world) for the same finding as data.`,
66
- status: explanation.status,
67
- }, 'physics2d');
68
- }
69
45
  // The names of the flags that are false, in the explanation's own field order so the key is stable for a
70
46
  // given set of faults however they arose.
71
47
  function getFailingPhysics2DPreconditions(explanation) {
@@ -90,6 +66,21 @@ function getUnresolvedPhysics2DJointFaults(explanation) {
90
66
  }
91
67
  return [...faults].sort();
92
68
  }
69
+ // Warns once per distinct set of undetectable collider kinds.
70
+ //
71
+ // Keyed on the kinds rather than logged per step, so a 60Hz loop says this once — and a world that later
72
+ // gains a second undetectable kind still says so.
73
+ function warnOnUndetectablePhysics2DColliders(world) {
74
+ const explanation = explainPhysics2DCollision(world);
75
+ if (explanation.status === 'ready')
76
+ return;
77
+ const kinds = explanation.unsupportedKinds;
78
+ logOnce(`physics2d:${explanation.status}:${kinds.join(',')}`, LogLevel.Warn, {
79
+ kinds,
80
+ message: `stepPhysics2D: collider kind${kinds.length === 1 ? '' : 's'} ${kinds.join(', ')} enclose${kinds.length === 1 ? 's' : ''} no area and so generate${kinds.length === 1 ? 's' : ''} no contacts, and the bodies carrying ${kinds.length === 1 ? 'it' : 'them'} pass through everything — give the body a shape with area. Call explainPhysics2DCollision(world) for the same finding as data.`,
81
+ status: explanation.status,
82
+ }, 'physics2d');
83
+ }
93
84
  // Warns once per distinct set of unresolved joint faults.
94
85
  //
95
86
  // The two fault families need different advice, so the message names whichever applies rather than
@@ -1 +1 @@
1
- {"version":3,"file":"enablePhysics2DGuards.js","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"enablePhysics2DGuards.js","sourceRoot":"","sources":["../src/enablePhysics2DGuards.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"explainPhysics2DCollision.d.ts","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwB,6BAA6B,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAepH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,6BAA6B,CAaxG"}
1
+ {"version":3,"file":"explainPhysics2DCollision.d.ts","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwB,6BAA6B,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAiBpH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,6BAA6B,CAaxG"}
@@ -3,14 +3,16 @@
3
3
  //
4
4
  // This exists because the dispatcher's coverage is NARROWER than the collider type allows. Every kind in
5
5
  // `CollisionBuiltInShape2D` may be authored onto a body, but `collideContactManifold2D` answers only for
6
- // the area shapes it has pair functions for; anything else is reported as non-overlapping. That is the
7
- // right behaviour for `segment` and `point`, which are area-less by definition and carry no contact to
8
- // find — and it is a GAP for `capsule`, whose pair functions are not written yet.
6
+ // the shapes that enclose an area; `segment` and `point` are area-less by definition and carry no contact
7
+ // to find, so a body built from one is reported as touching nothing and falls through the world.
9
8
  //
10
- // The distinction matters to a caller and is deliberately not made here: this reports what does not
11
- // collide, not why. A segment collider on a body is a modelling mistake; a capsule one is a missing
12
- // feature. Both look identical from inside the simulation, which is exactly why the question needs
13
- // answering from outside it.
9
+ // That is a modelling mistake with no fix inside this package, and it is invisible from inside the
10
+ // simulation nothing fails, nothing is slow, the body simply never collides. Which is exactly why the
11
+ // question has to be answerable from outside it.
12
+ //
13
+ // The list stays a general query rather than a hardcoded "segment or point" test, because the answer is a
14
+ // property of the dispatcher and not of this file: a kind added to the collider union without pair
15
+ // functions would show up here on its own, which is how the capsule surfaced before its were written.
14
16
  export function explainPhysics2DCollision(world) {
15
17
  const unsupported = new Set();
16
18
  for (const body of world.bodies) {
@@ -30,6 +32,6 @@ export function explainPhysics2DCollision(world) {
30
32
  // through the dispatcher because asking the dispatcher would mean synthesizing a shape of each kind to
31
33
  // test with, and a diagnostic must not be able to change what it measures.
32
34
  function isPhysics2DContactSupportedKind(kind) {
33
- return kind === 'circle' || kind === 'aabb' || kind === 'obb' || kind === 'polygon';
35
+ return kind === 'circle' || kind === 'capsule' || kind === 'aabb' || kind === 'obb' || kind === 'polygon';
34
36
  }
35
37
  //# sourceMappingURL=explainPhysics2DCollision.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"explainPhysics2DCollision.js","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAEA,wGAAwG;AACxG,+CAA+C;AAC/C,EAAE;AACF,yGAAyG;AACzG,yGAAyG;AACzG,uGAAuG;AACvG,uGAAuG;AACvG,kFAAkF;AAClF,EAAE;AACF,oGAAoG;AACpG,oGAAoG;AACpG,mGAAmG;AACnG,6BAA6B;AAC7B,MAAM,UAAU,yBAAyB,CAAC,KAA+B;IACvE,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,+BAA+B,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnE,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,MAAM,gBAAgB,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO;QACL,MAAM,EAAE,gBAAgB,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB;QAC3E,gBAAgB;KACjB,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,uGAAuG;AACvG,2EAA2E;AAC3E,SAAS,+BAA+B,CAAC,IAA0B;IACjE,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,SAAS,CAAC;AACtF,CAAC"}
1
+ {"version":3,"file":"explainPhysics2DCollision.js","sourceRoot":"","sources":["../src/explainPhysics2DCollision.ts"],"names":[],"mappings":"AAEA,wGAAwG;AACxG,+CAA+C;AAC/C,EAAE;AACF,yGAAyG;AACzG,yGAAyG;AACzG,0GAA0G;AAC1G,iGAAiG;AACjG,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,iDAAiD;AACjD,EAAE;AACF,0GAA0G;AAC1G,mGAAmG;AACnG,sGAAsG;AACtG,MAAM,UAAU,yBAAyB,CAAC,KAA+B;IACvE,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,+BAA+B,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnE,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,MAAM,gBAAgB,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO;QACL,MAAM,EAAE,gBAAgB,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,yBAAyB;QAC3E,gBAAgB;KACjB,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,uGAAuG;AACvG,2EAA2E;AAC3E,SAAS,+BAA+B,CAAC,IAA0B;IACjE,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,SAAS,CAAC;AAC5G,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/physics2d",
3
- "version": "0.4.0-next.1815.e47cc3c",
3
+ "version": "0.4.0-next.1821.efb1cbc",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,11 +38,11 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/collision": "0.4.0-next.1815.e47cc3c",
42
- "@flighthq/log": "0.4.0-next.1815.e47cc3c",
43
- "@flighthq/math": "0.4.0-next.1815.e47cc3c",
44
- "@flighthq/spatial": "0.4.0-next.1815.e47cc3c",
45
- "@flighthq/types": "0.4.0-next.1815.e47cc3c"
41
+ "@flighthq/collision": "0.4.0-next.1821.efb1cbc",
42
+ "@flighthq/log": "0.4.0-next.1821.efb1cbc",
43
+ "@flighthq/math": "0.4.0-next.1821.efb1cbc",
44
+ "@flighthq/spatial": "0.4.0-next.1821.efb1cbc",
45
+ "@flighthq/types": "0.4.0-next.1821.efb1cbc"
46
46
  },
47
47
  "devDependencies": {
48
48
  "typescript": "^5.3.0"
@@ -102,14 +102,13 @@ describe('disablePhysics2DGuards', () => {
102
102
  });
103
103
 
104
104
  describe('enablePhysics2DGuards', () => {
105
- it('reports a collider kind the contact dispatcher has no arm for', () => {
106
- // The third seam, and the one that stops a capsule from being a silent trap: it is a valid collider
107
- // the type system admits and the solver cannot see, so without this a body carrying one falls
108
- // through the world with nothing failing anywhere.
105
+ it('reports an area-less collider used as a body shape', () => {
106
+ // The third seam. A segment on a rigid body is a modelling mistake the simulation cannot show you:
107
+ // nothing fails, nothing is slow, the body simply never collides with anything.
109
108
  enablePhysics2DGuards();
110
109
  const world = createPhysics2DWorld(0, -10);
111
110
  const body = createRigidBody2D('dynamic', 0, 0);
112
- body.colliders.push(createPhysics2DCollider({ kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.4 }, STONE));
111
+ body.colliders.push(createPhysics2DCollider({ kind: 'segment', x0: 0, y0: 0, x1: 1, y1: 0 }, STONE));
113
112
  addPhysics2DBody(world, body);
114
113
 
115
114
  const entries = captureLog(() => stepPhysics2D(world, 1 / 60));
@@ -117,24 +116,21 @@ describe('enablePhysics2DGuards', () => {
117
116
  expect(entries).toHaveLength(1);
118
117
  const data = entries[0].data as { kinds: string[]; message: string; status: string };
119
118
  expect(data.status).toBe('missing-contact-support');
120
- expect(data.kinds).toEqual(['capsule']);
121
- // Named as a gap rather than as a modelling mistake, which is the distinction a caller acts on.
122
- expect(data.message).toContain('no pair functions');
123
- expect(data.message).not.toContain('no area');
119
+ expect(data.kinds).toEqual(['segment']);
120
+ expect(data.message).toContain('no area');
124
121
  });
125
122
 
126
- it('tells an area-less collider apart from an unimplemented one', () => {
123
+ it('says nothing about a capsule, which the dispatcher answers for', () => {
124
+ // This test is the inverse of one that used to assert the opposite. The capsule was named by this
125
+ // guard while its pair functions were missing and stopped being named when they landed, which is the
126
+ // seam working in both directions rather than a list someone edits by hand.
127
127
  enablePhysics2DGuards();
128
128
  const world = createPhysics2DWorld(0, -10);
129
129
  const body = createRigidBody2D('dynamic', 0, 0);
130
- body.colliders.push(createPhysics2DCollider({ kind: 'segment', x0: 0, y0: 0, x1: 1, y1: 0 }, STONE));
130
+ body.colliders.push(createPhysics2DCollider({ kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.4 }, STONE));
131
131
  addPhysics2DBody(world, body);
132
132
 
133
- const entries = captureLog(() => stepPhysics2D(world, 1 / 60));
134
-
135
- const data = entries[0].data as { message: string };
136
- expect(data.message).toContain('no area');
137
- expect(data.message).not.toContain('no pair functions');
133
+ expect(captureLog(() => stepPhysics2D(world, 1 / 60))).toHaveLength(0);
138
134
  });
139
135
 
140
136
  it('says nothing about colliders the dispatcher does answer for', () => {
@@ -24,6 +24,7 @@ describe('explainPhysics2DCollision', () => {
24
24
  const world = worldWith(
25
25
  { kind: 'circle', x: 0, y: 0, radius: 1 },
26
26
  { kind: 'aabb', minX: -1, minY: -1, maxX: 1, maxY: 1 },
27
+ { kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.5 },
27
28
  { kind: 'polygon', points: [0, 0, 1, 0, 1, 1] },
28
29
  );
29
30
  expect(explainPhysics2DCollision(world)).toEqual({ status: 'ready', unsupportedKinds: [] });
@@ -33,12 +34,12 @@ describe('explainPhysics2DCollision', () => {
33
34
  expect(explainPhysics2DCollision(createPhysics2DWorld(0, -10)).status).toBe('ready');
34
35
  });
35
36
 
36
- it('names a capsule, whose pair functions are not written yet', () => {
37
+ it('reports ready for a capsule, which the dispatcher now answers for', () => {
38
+ // This test previously asserted the opposite, and the change is the point: the capsule was named
39
+ // here while its pair functions were missing, and it stopped being named when they landed. That is
40
+ // the seam doing its job in both directions rather than a list someone has to remember to edit.
37
41
  const world = worldWith({ kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.5 });
38
- expect(explainPhysics2DCollision(world)).toEqual({
39
- status: 'missing-contact-support',
40
- unsupportedKinds: ['capsule'],
41
- });
42
+ expect(explainPhysics2DCollision(world)).toEqual({ status: 'ready', unsupportedKinds: [] });
42
43
  });
43
44
 
44
45
  it('names the area-less kinds too, since they are equally invisible to the solver', () => {
@@ -50,11 +51,11 @@ describe('explainPhysics2DCollision', () => {
50
51
  // A level with four hundred capsules is one mistake. Sorted so the answer does not depend on the
51
52
  // order bodies were added, which would make the same world report differently twice.
52
53
  const world = worldWith(
54
+ { kind: 'point', x: 0, y: 0 },
53
55
  { kind: 'segment', x0: 0, y0: 0, x1: 1, y1: 0 },
54
- { kind: 'capsule', x0: 0, y0: 0, x1: 1, y1: 0, radius: 0.5 },
55
- { kind: 'capsule', x0: 2, y0: 0, x1: 3, y1: 0, radius: 0.5 },
56
+ { kind: 'segment', x0: 2, y0: 0, x1: 3, y1: 0 },
56
57
  { kind: 'circle', x: 0, y: 0, radius: 1 },
57
58
  );
58
- expect(explainPhysics2DCollision(world).unsupportedKinds).toEqual(['capsule', 'segment']);
59
+ expect(explainPhysics2DCollision(world).unsupportedKinds).toEqual(['point', 'segment']);
59
60
  });
60
61
  });
package/src/step.test.ts CHANGED
@@ -1611,6 +1611,99 @@ describe('stepPhysics2D with breakable joints', () => {
1611
1611
  });
1612
1612
  });
1613
1613
 
1614
+ describe('stepPhysics2D with capsule colliders', () => {
1615
+ // The behaviour the capsule's two-point floor manifold exists for. A capsule held by a single contact
1616
+ // point pivots about it, and a player sees a character that will not stand still — which is why this is
1617
+ // tested through the STEP rather than only at the manifold.
1618
+ it('settles a horizontal capsule flat on the ground without rocking, and lets it sleep', () => {
1619
+ const world = createPhysics2DWorld(0, -10);
1620
+ const floor = createRigidBody2D('static', 0, 0);
1621
+ floor.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -20, minY: -1, maxX: 20, maxY: 0 }, STONE));
1622
+ addPhysics2DBody(world, floor);
1623
+ const capsule = createRigidBody2D('dynamic', 0, 2);
1624
+ capsule.colliders.push(
1625
+ createPhysics2DCollider({ kind: 'capsule', x0: -1, y0: 0, x1: 1, y1: 0, radius: 0.5 }, STONE),
1626
+ );
1627
+ addPhysics2DBody(world, capsule);
1628
+
1629
+ let worstAngleAfterSettling = 0;
1630
+ for (let step = 0; step < 900; step++) {
1631
+ stepPhysics2D(world, 1 / 240);
1632
+ if (step > 500) worstAngleAfterSettling = Math.max(worstAngleAfterSettling, Math.abs(capsule.angle));
1633
+ }
1634
+
1635
+ // Resting on its radius, less the solver's penetration slop.
1636
+ expect(capsule.y).toBeCloseTo(0.5, 2);
1637
+ // Flat, and not merely flat at the last sample: nothing after settling rotated it either.
1638
+ expect(worstAngleAfterSettling).toBeLessThan(1e-6);
1639
+ expect(capsule.sleeping).toBe(true);
1640
+ });
1641
+
1642
+ it('stops a capsule bullet at a thin wall instead of tunnelling through it', () => {
1643
+ // The reason the capsule needed a sweep arm at all. Without one the CCD path finds no impact for a
1644
+ // capsule and the body teleports past the wall in a single step — and it fails SILENTLY, because a
1645
+ // sweep that reports no hit is indistinguishable from a clear path.
1646
+ const world = createPhysics2DWorld(0, 0);
1647
+ world.config.continuousCollision = true;
1648
+ const wall = createRigidBody2D('static', 0, 0);
1649
+ wall.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: 9.9, minY: -5, maxX: 10, maxY: 5 }, STONE));
1650
+ addPhysics2DBody(world, wall);
1651
+ const bullet = createRigidBody2D('dynamic', 0, 0);
1652
+ bullet.bullet = true;
1653
+ bullet.colliders.push(
1654
+ createPhysics2DCollider({ kind: 'capsule', x0: -0.2, y0: 0, x1: 0.2, y1: 0, radius: 0.1 }, STONE),
1655
+ );
1656
+ addPhysics2DBody(world, bullet);
1657
+ // Fast enough to clear the wall's whole thickness in one step several times over.
1658
+ bullet.velocityX = 600;
1659
+
1660
+ for (let step = 0; step < 10; step++) stepPhysics2D(world, 1 / 60);
1661
+
1662
+ expect(bullet.x).toBeLessThan(10);
1663
+ });
1664
+
1665
+ it('rolls a capsule down a slope rather than sticking on a corner', () => {
1666
+ // A rounded end on an inclined face is the corner case the vertex axes exist for. With the wrong
1667
+ // contact normal the capsule catches on the slope instead of sliding down it.
1668
+ const world = createPhysics2DWorld(0, -10);
1669
+ const slope = createRigidBody2D('static', 0, 0);
1670
+ slope.angle = -0.3;
1671
+ slope.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -20, minY: -1, maxX: 20, maxY: 0 }, STONE));
1672
+ addPhysics2DBody(world, slope);
1673
+ const capsule = createRigidBody2D('dynamic', -2, 2);
1674
+ capsule.colliders.push(
1675
+ createPhysics2DCollider({ kind: 'capsule', x0: -0.4, y0: 0, x1: 0.4, y1: 0, radius: 0.3 }, STONE),
1676
+ );
1677
+ addPhysics2DBody(world, capsule);
1678
+
1679
+ for (let step = 0; step < 600; step++) stepPhysics2D(world, 1 / 240);
1680
+
1681
+ expect(Number.isFinite(capsule.x)).toBe(true);
1682
+ // Downhill is +x for a slope tilted this way, and it must not have sunk through.
1683
+ expect(capsule.x).toBeGreaterThan(-2);
1684
+ expect(capsule.y).toBeGreaterThan(-2);
1685
+ });
1686
+
1687
+ it('stacks a capsule on a capsule without either falling through', () => {
1688
+ const world = createPhysics2DWorld(0, -10);
1689
+ const floor = createRigidBody2D('static', 0, 0);
1690
+ floor.colliders.push(createPhysics2DCollider({ kind: 'aabb', minX: -20, minY: -1, maxX: 20, maxY: 0 }, STONE));
1691
+ addPhysics2DBody(world, floor);
1692
+ const lower = createRigidBody2D('dynamic', 0, 1);
1693
+ lower.colliders.push(createPhysics2DCollider({ kind: 'capsule', x0: -1, y0: 0, x1: 1, y1: 0, radius: 0.5 }, STONE));
1694
+ addPhysics2DBody(world, lower);
1695
+ const upper = createRigidBody2D('dynamic', 0, 2.5);
1696
+ upper.colliders.push(createPhysics2DCollider({ kind: 'capsule', x0: -1, y0: 0, x1: 1, y1: 0, radius: 0.5 }, STONE));
1697
+ addPhysics2DBody(world, upper);
1698
+
1699
+ for (let step = 0; step < 1200; step++) stepPhysics2D(world, 1 / 240);
1700
+
1701
+ expect(lower.y).toBeCloseTo(0.5, 1);
1702
+ expect(upper.y).toBeCloseTo(1.5, 1);
1703
+ expect(upper.y).toBeGreaterThan(lower.y + 0.9);
1704
+ });
1705
+ });
1706
+
1614
1707
  describe('stepPhysics2D with joints', () => {
1615
1708
  const DISTANCE = 'Distance';
1616
1709