@gg-web-engine/matter 0.0.71 → 0.0.73
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/dist/components/matter-character-controller.component.d.ts +191 -0
- package/dist/components/matter-character-controller.component.js +498 -0
- package/dist/components/matter-rigid-body.component.d.ts +17 -2
- package/dist/components/matter-rigid-body.component.js +27 -2
- package/dist/components/matter-trigger.component.d.ts +46 -4
- package/dist/components/matter-trigger.component.js +74 -4
- package/dist/components/matter-world.component.d.ts +32 -5
- package/dist/components/matter-world.component.js +119 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/matter-factory.d.ts +8 -1
- package/dist/matter-factory.js +116 -11
- package/dist/types.d.ts +2 -0
- package/package.json +5 -4
- package/test/components/matter-character-controller.component.spec.ts +298 -0
- package/test/components/matter-rigid-body-collision.spec.ts +27 -13
- package/test/components/matter-rigid-body.component.spec.ts +58 -0
- package/test/components/matter-trigger.component.spec.ts +41 -13
- package/test/components/matter-world.component.spec.ts +44 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/matter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.73",
|
|
4
4
|
"description": "An attempt to create open source game engine for browser",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/AndyGura/gg-web-engine#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"poly-decomp": "^0.3.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
|
-
"@
|
|
33
|
-
"@gg-web-engine/core": "0.0.71",
|
|
33
|
+
"@gg-web-engine/core": "0.0.73",
|
|
34
34
|
"@types/jest": "^30.0.0",
|
|
35
|
+
"@types/matter-js": "0.20.2",
|
|
35
36
|
"@types/node": "^26.4.0",
|
|
36
37
|
"jest": "^30.5.0",
|
|
37
38
|
"jest-environment-jsdom": "^30.5.0",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"typescript": "~6.0.3"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
|
-
"@gg-web-engine/core": "0.0.
|
|
46
|
+
"@gg-web-engine/core": "0.0.73",
|
|
46
47
|
"matter-js": "0.20.0",
|
|
47
48
|
"rxjs": "7.8.2"
|
|
48
49
|
},
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { CharacterController2dOptions, Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterCharacterControllerComponent', () => {
|
|
5
|
+
let world: MatterWorldComponent;
|
|
6
|
+
let factory: MatterFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new MatterWorldComponent();
|
|
13
|
+
factory = new MatterFactory(world);
|
|
14
|
+
await world.init();
|
|
15
|
+
world.gravity = Pnt2.O; // this component never relies on gravity - see the interface's own doc
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const CHAR_OPTIONS: CharacterController2dOptions = { radius: 0.4, centersDistance: 1.0 };
|
|
23
|
+
// center-to-feet distance for the capsule above: radius + centersDistance / 2
|
|
24
|
+
const HALF_HEIGHT = CHAR_OPTIONS.radius + CHAR_OPTIONS.centersDistance / 2;
|
|
25
|
+
|
|
26
|
+
// A static floor slab spanning [centerX - sizeX/2, centerX + sizeX/2] on X, with its top face (the
|
|
27
|
+
// side facing `up` = `{x:0,y:-1}`, i.e. the smaller-Y side) at world-space `topY`.
|
|
28
|
+
const addFloor = (centerX: number, sizeX: number, topY: number, thickness = 1) => {
|
|
29
|
+
const floor = factory.createRigidBody(
|
|
30
|
+
{
|
|
31
|
+
shape: { shape: 'BOX', dimensions: { x: sizeX, y: thickness } },
|
|
32
|
+
body: { bodyType: 'static', mass: 0 },
|
|
33
|
+
},
|
|
34
|
+
{ position: { x: centerX, y: topY + thickness / 2 } },
|
|
35
|
+
);
|
|
36
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
37
|
+
return floor;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// A static vertical wall whose near face is at world-space X `x`.
|
|
41
|
+
const addWall = (x: number, thickness = 1) => {
|
|
42
|
+
const wall = factory.createRigidBody(
|
|
43
|
+
{
|
|
44
|
+
shape: { shape: 'BOX', dimensions: { x: thickness, y: 20 } },
|
|
45
|
+
body: { bodyType: 'static', mass: 0 },
|
|
46
|
+
},
|
|
47
|
+
{ position: { x: x + thickness / 2, y: 0 } },
|
|
48
|
+
);
|
|
49
|
+
wall.addToWorld({ physicsWorld: world } as any);
|
|
50
|
+
return wall;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Unlike Ammo/Rapier, matter-js has no broad-phase-registration lag at all - `Composite.add`
|
|
54
|
+
// synchronously updates `Composite.allBodies`, which is exactly what this component's own
|
|
55
|
+
// `Query.collides`-based queries read - so there is no "world hasn't stepped yet" pitfall here to
|
|
56
|
+
// work around with a settle call. Kept only as a documented no-op for structural parity with the
|
|
57
|
+
// other adapters' test suites.
|
|
58
|
+
const settleWorld = () => world.simulate(0);
|
|
59
|
+
|
|
60
|
+
it('should create a character controller and add it to the world', () => {
|
|
61
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
62
|
+
expect(character.radius).toBe(0.4);
|
|
63
|
+
expect(character.centersDistance).toBe(1.0);
|
|
64
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
65
|
+
expect(world.children).toContain(character);
|
|
66
|
+
expect(character.position).toEqual({ x: 0, y: -5 });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should settle on a flat floor and report grounded state', () => {
|
|
70
|
+
addFloor(0, 20, 0);
|
|
71
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
72
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
73
|
+
settleWorld();
|
|
74
|
+
|
|
75
|
+
expect(character.isGrounded).toBe(false);
|
|
76
|
+
|
|
77
|
+
character.move({ x: 0, y: 10 });
|
|
78
|
+
|
|
79
|
+
// resolved fully synchronously - no world.simulate() call happened after settleWorld() above
|
|
80
|
+
expect(character.isGrounded).toBe(true);
|
|
81
|
+
expect(character.position.y).toBeCloseTo(-HALF_HEIGHT, 1);
|
|
82
|
+
expect(character.groundNormal).not.toBeNull();
|
|
83
|
+
expect(character.groundNormal!.y).toBeLessThan(-0.9);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should slide to a stop against a wall instead of passing through it', () => {
|
|
87
|
+
addFloor(0, 20, 0);
|
|
88
|
+
addWall(2);
|
|
89
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
90
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
91
|
+
});
|
|
92
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
93
|
+
settleWorld();
|
|
94
|
+
// settle onto the floor first
|
|
95
|
+
character.move({ x: 0, y: 1 });
|
|
96
|
+
expect(character.isGrounded).toBe(true);
|
|
97
|
+
|
|
98
|
+
character.move({ x: 5, y: 0 });
|
|
99
|
+
|
|
100
|
+
// wall's near face is at x=2; the capsule (radius 0.4) plus its skin offset must stop short of it
|
|
101
|
+
expect(character.position.x).toBeLessThan(1.6);
|
|
102
|
+
expect(character.position.x).toBeGreaterThan(0.5);
|
|
103
|
+
// remained grounded throughout - sliding along the wall, not falling
|
|
104
|
+
expect(character.isGrounded).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it(
|
|
108
|
+
'two character controllers should block each other instead of passing through (regression: a ' +
|
|
109
|
+
"character's own phantom body is never added to Composite/engine.world, so it was invisible to " +
|
|
110
|
+
"any other character's obstacle collection, which only ever enumerated Composite.allBodies)",
|
|
111
|
+
() => {
|
|
112
|
+
addFloor(0, 20, 0);
|
|
113
|
+
const characterA = factory.createCharacterController(CHAR_OPTIONS, {
|
|
114
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
115
|
+
});
|
|
116
|
+
const characterB = factory.createCharacterController(CHAR_OPTIONS, {
|
|
117
|
+
position: { x: 3, y: -(HALF_HEIGHT + 0.5) },
|
|
118
|
+
});
|
|
119
|
+
characterA.addToWorld({ physicsWorld: world } as any);
|
|
120
|
+
characterB.addToWorld({ physicsWorld: world } as any);
|
|
121
|
+
settleWorld();
|
|
122
|
+
characterA.move({ x: 0, y: 1 });
|
|
123
|
+
characterB.move({ x: 0, y: 1 });
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < 20; i++) {
|
|
126
|
+
characterA.move({ x: 0.2, y: 0 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// characterB sits at x=3 and never moves - characterA must be blocked well short of it, not
|
|
130
|
+
// walk straight through to make its full requested 4 units of horizontal progress.
|
|
131
|
+
expect(characterA.position.x).toBeLessThan(2.5);
|
|
132
|
+
expect(characterB.position.x).toBe(3);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
it(
|
|
137
|
+
'walks straight through a body added to ignoredBodies instead of sliding to a stop against it ' +
|
|
138
|
+
"(regression: collision groups alone can't express excluding just one specific body while both " +
|
|
139
|
+
'it and the character still need to collide with the rest of the world - see ' +
|
|
140
|
+
'`ICharacterController2dComponent.ignoredBodies`\'s doc)',
|
|
141
|
+
() => {
|
|
142
|
+
addFloor(0, 20, 0);
|
|
143
|
+
const wall = addWall(2);
|
|
144
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
145
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
146
|
+
});
|
|
147
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
148
|
+
character.ignoredBodies.add(wall);
|
|
149
|
+
settleWorld();
|
|
150
|
+
character.move({ x: 0, y: 1 });
|
|
151
|
+
|
|
152
|
+
character.move({ x: 5, y: 0 });
|
|
153
|
+
|
|
154
|
+
// unlike the baseline test above, nothing stopped it short of the wall's x=2 near face
|
|
155
|
+
expect(character.position.x).toBeCloseTo(5);
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
it('removing a body from ignoredBodies makes it block the character again', () => {
|
|
160
|
+
addFloor(0, 20, 0);
|
|
161
|
+
const wall = addWall(2);
|
|
162
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
163
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
164
|
+
});
|
|
165
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
166
|
+
character.ignoredBodies.add(wall);
|
|
167
|
+
character.ignoredBodies.delete(wall);
|
|
168
|
+
settleWorld();
|
|
169
|
+
character.move({ x: 0, y: 1 });
|
|
170
|
+
|
|
171
|
+
character.move({ x: 5, y: 0 });
|
|
172
|
+
|
|
173
|
+
expect(character.position.x).toBeLessThan(1.6);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should step up a ledge shorter than maxStepHeight without getting stuck', () => {
|
|
177
|
+
const maxStepHeight = 0.3;
|
|
178
|
+
const stepHeight = 0.2; // below maxStepHeight
|
|
179
|
+
addFloor(-5, 10, 0); // lower floor, spans x in [-10, 0]
|
|
180
|
+
addFloor(5, 10, -stepHeight); // upper ledge, spans x in [0, 10], step wall at x=0
|
|
181
|
+
|
|
182
|
+
const character = factory.createCharacterController(
|
|
183
|
+
{ ...CHAR_OPTIONS, maxStepHeight },
|
|
184
|
+
{ position: { x: -5, y: -(HALF_HEIGHT + 0.5) } },
|
|
185
|
+
);
|
|
186
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
187
|
+
settleWorld();
|
|
188
|
+
character.move({ x: 0, y: 1 });
|
|
189
|
+
expect(character.isGrounded).toBe(true);
|
|
190
|
+
expect(character.position.y).toBeCloseTo(-HALF_HEIGHT, 1);
|
|
191
|
+
|
|
192
|
+
// walk forward across the step in small increments, each with a slight downward bias so the
|
|
193
|
+
// character does not need to rely on gravity (which this component never applies itself)
|
|
194
|
+
for (let i = 0; i < 60; i++) {
|
|
195
|
+
character.move({ x: 0.15, y: 0.05 });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
expect(character.isGrounded).toBe(true);
|
|
199
|
+
expect(character.position.x).toBeGreaterThan(2); // made meaningful forward progress past the step
|
|
200
|
+
expect(character.position.y).toBeCloseTo(-(stepHeight + HALF_HEIGHT), 1); // stepped up onto the ledge
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('a small upward move (a jump takeoff tick) actually rises instead of being snapped back to the floor, even though it stays well within the default snapToGroundDistance of 0.3', () => {
|
|
204
|
+
addFloor(0, 20, 0);
|
|
205
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
206
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.5) },
|
|
207
|
+
});
|
|
208
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
209
|
+
settleWorld();
|
|
210
|
+
character.move({ x: 0, y: 1 });
|
|
211
|
+
expect(character.isGrounded).toBe(true);
|
|
212
|
+
const groundedY = character.position.y;
|
|
213
|
+
|
|
214
|
+
// one tick's worth of a typical jumpSpeed (5 units/s) at 60fps - well under the 0.3 snapToGroundDistance
|
|
215
|
+
character.move({ x: 0, y: -0.083 });
|
|
216
|
+
|
|
217
|
+
expect(character.position.y).toBeCloseTo(groundedY - 0.083, 2);
|
|
218
|
+
expect(character.isGrounded).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("move()'s effects must be visible immediately, without an extra world.step() in between", () => {
|
|
222
|
+
addFloor(0, 20, 0);
|
|
223
|
+
addWall(1);
|
|
224
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, {
|
|
225
|
+
position: { x: 0, y: -(HALF_HEIGHT + 0.01) },
|
|
226
|
+
});
|
|
227
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
228
|
+
settleWorld();
|
|
229
|
+
|
|
230
|
+
// no world.simulate() call below this point - move() alone must fully resolve state
|
|
231
|
+
character.move({ x: 10, y: 0 });
|
|
232
|
+
|
|
233
|
+
expect(character.position.x).toBeLessThan(0.6);
|
|
234
|
+
expect(character.position.x).toBeGreaterThan(-0.1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should clone with the same shape/options but independent from the world', () => {
|
|
238
|
+
const character = factory.createCharacterController(
|
|
239
|
+
{ radius: 0.3, centersDistance: 0.8, maxStepHeight: 0.25 },
|
|
240
|
+
{ position: { x: 1, y: 2 } },
|
|
241
|
+
);
|
|
242
|
+
const clone = character.clone();
|
|
243
|
+
expect(clone.radius).toBe(character.radius);
|
|
244
|
+
expect(clone.centersDistance).toBe(character.centersDistance);
|
|
245
|
+
expect(clone.position).toEqual(character.position);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should clone from the CURRENT position/rotation, not the construction-time spawn point', () => {
|
|
249
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
250
|
+
character.addToWorld({ physicsWorld: world } as any);
|
|
251
|
+
settleWorld();
|
|
252
|
+
// move the character away from its construction-time position via the position setter, the
|
|
253
|
+
// same code path that leaves any construction-time transform stale once the component is added
|
|
254
|
+
// to a world and driven directly
|
|
255
|
+
character.position = { x: 10, y: 20 };
|
|
256
|
+
|
|
257
|
+
const clone = character.clone();
|
|
258
|
+
|
|
259
|
+
expect(clone.position).toEqual(character.position);
|
|
260
|
+
expect(clone.position).not.toEqual({ x: 0, y: -5 });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it(
|
|
264
|
+
'should clone from the CURRENT up/ownCollisionGroups/interactWithCollisionGroups, not whatever ' +
|
|
265
|
+
'was passed at construction time (regression: clone() used to rebuild from the frozen ' +
|
|
266
|
+
"construction-time options object, silently discarding any of these three fields' live " +
|
|
267
|
+
'mutations via their own setters)',
|
|
268
|
+
() => {
|
|
269
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
270
|
+
character.up = { x: 1, y: 0 };
|
|
271
|
+
character.ownCollisionGroups = [world.registerCollisionGroup()];
|
|
272
|
+
character.interactWithCollisionGroups = [world.registerCollisionGroup()];
|
|
273
|
+
|
|
274
|
+
const clone = character.clone();
|
|
275
|
+
|
|
276
|
+
expect(clone.up).toEqual(character.up);
|
|
277
|
+
expect(clone.ownCollisionGroups).toEqual(character.ownCollisionGroups);
|
|
278
|
+
expect(clone.interactWithCollisionGroups).toEqual(character.interactWithCollisionGroups);
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
it('move() before addToWorld() should be a silent no-op, not throw', () => {
|
|
283
|
+
const character = factory.createCharacterController(CHAR_OPTIONS, { position: { x: 0, y: -5 } });
|
|
284
|
+
expect(() => character.move({ x: 1, y: 0 })).not.toThrow();
|
|
285
|
+
expect(character.position).toEqual({ x: 0, y: -5 });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should normalize a non-unit-length up vector, matching the 3D adapter', () => {
|
|
289
|
+
const character = factory.createCharacterController(
|
|
290
|
+
{ ...CHAR_OPTIONS, up: { x: 0, y: 2 } },
|
|
291
|
+
{ position: { x: 0, y: -5 } },
|
|
292
|
+
);
|
|
293
|
+
expect(character.up).toEqual({ x: 0, y: 1 });
|
|
294
|
+
|
|
295
|
+
character.up = { x: 0, y: 3 };
|
|
296
|
+
expect(character.up).toEqual({ x: 0, y: 1 });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -22,7 +22,7 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
22
22
|
it('should fire onCollisionStart reciprocally, with a sane position/normal, when a falling body lands on a floor', () => {
|
|
23
23
|
// static floor, top edge sits at y = -5
|
|
24
24
|
const floor = factory.createRigidBody(
|
|
25
|
-
{ shape: { shape: '
|
|
25
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
26
26
|
{ position: { x: 0, y: 0 } },
|
|
27
27
|
);
|
|
28
28
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
@@ -67,18 +67,25 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
67
67
|
|
|
68
68
|
it('should not keep re-firing onCollisionStart while a body rests stably on another', () => {
|
|
69
69
|
const floor = factory.createRigidBody(
|
|
70
|
-
{ shape: { shape: '
|
|
70
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
71
71
|
{ position: { x: 0, y: 0 } },
|
|
72
72
|
);
|
|
73
73
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
74
74
|
|
|
75
|
+
// A BOX, not a CIRCLE: a circle has no flat resting face, so under this engine's default (no
|
|
76
|
+
// sleeping - see `gg-engine-physics-adapter-matter`'s own note) it keeps finding tiny amounts of
|
|
77
|
+
// torque to roll away on indefinitely instead of ever truly settling, which is exactly the kind
|
|
78
|
+
// of never-stops-touching-and-un-touching motion this test's own premise ("rests stably") needs
|
|
79
|
+
// to rule out by construction. Also left at matter's own default `frictionAir` (not zeroed like
|
|
80
|
+
// the other tests in this file) - some velocity damping is what actually lets a resting body's
|
|
81
|
+
// energy bleed off instead of slowly building up over many hundreds of steps (see this file's own
|
|
82
|
+
// history for how thoroughly this was verified empirically).
|
|
75
83
|
const ball = factory.createRigidBody(
|
|
76
|
-
{ shape: { shape: '
|
|
84
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
77
85
|
{ position: { x: 0, y: -20 } },
|
|
78
86
|
);
|
|
79
87
|
ball.addToWorld({ physicsWorld: world } as any);
|
|
80
88
|
ball.linearVelocity = { x: 0, y: 10 };
|
|
81
|
-
ball.nativeBody.frictionAir = 0;
|
|
82
89
|
|
|
83
90
|
world.gravity = { x: 0, y: 1 };
|
|
84
91
|
|
|
@@ -101,28 +108,35 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
101
108
|
|
|
102
109
|
it('should fire onCollisionEnd when a body is knocked away and separates', () => {
|
|
103
110
|
const floor = factory.createRigidBody(
|
|
104
|
-
{ shape: { shape: '
|
|
111
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
105
112
|
{ position: { x: 0, y: 0 } },
|
|
106
113
|
);
|
|
107
114
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
108
115
|
|
|
116
|
+
// BOX, not CIRCLE - see the previous test's own doc for why.
|
|
109
117
|
const ball = factory.createRigidBody(
|
|
110
|
-
{ shape: { shape: '
|
|
118
|
+
{ shape: { shape: 'BOX', dimensions: { x: 2, y: 2 } }, body: { bodyType: 'dynamic', mass: 1 } },
|
|
111
119
|
{ position: { x: 0, y: -20 } },
|
|
112
120
|
);
|
|
113
121
|
ball.addToWorld({ physicsWorld: world } as any);
|
|
114
122
|
ball.linearVelocity = { x: 0, y: 10 };
|
|
115
|
-
ball.nativeBody.frictionAir = 0;
|
|
116
123
|
|
|
117
124
|
let endEvents: (MatterRigidBodyComponent | null)[] = [];
|
|
125
|
+
// Knock it away the instant it actually touches the floor (rather than after some fixed,
|
|
126
|
+
// speed/distance-dependent step count guessed to be "enough time to have touched by now") - the
|
|
127
|
+
// exact number of steps a fall of this distance/speed takes to make real contact isn't this
|
|
128
|
+
// test's concern and shouldn't be baked into a magic constant.
|
|
129
|
+
let knocked = false;
|
|
130
|
+
ball.onCollisionStart.subscribe(() => {
|
|
131
|
+
if (!knocked) {
|
|
132
|
+
knocked = true;
|
|
133
|
+
ball.linearVelocity = { x: 0, y: -10 };
|
|
134
|
+
}
|
|
135
|
+
});
|
|
118
136
|
ball.onCollisionEnd.subscribe(e => endEvents.push(e));
|
|
119
137
|
|
|
120
138
|
for (let i = 0; i < 200 && endEvents.length === 0; i++) {
|
|
121
139
|
world.simulate(16);
|
|
122
|
-
if (i === 50) {
|
|
123
|
-
// knock it straight back up, away from the floor, once it's had a chance to touch
|
|
124
|
-
ball.linearVelocity = { x: 0, y: -10 };
|
|
125
|
-
}
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
expect(endEvents.length).toBeGreaterThan(0);
|
|
@@ -131,7 +145,7 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
131
145
|
|
|
132
146
|
it('should emit onCollisionEnd(null) on the remaining body when the other body is removed from the world while still touching', () => {
|
|
133
147
|
const floor = factory.createRigidBody(
|
|
134
|
-
{ shape: { shape: '
|
|
148
|
+
{ shape: { shape: 'BOX', dimensions: { x: 50, y: 10 } }, body: { bodyType: 'static', mass: 0 } },
|
|
135
149
|
{ position: { x: 0, y: 0 } },
|
|
136
150
|
);
|
|
137
151
|
floor.addToWorld({ physicsWorld: world } as any);
|
|
@@ -161,7 +175,7 @@ describe('MatterRigidBodyComponent collision events', () => {
|
|
|
161
175
|
});
|
|
162
176
|
|
|
163
177
|
it('should not fire the rigid body onCollisionStart for a trigger overlapping a rigid body', () => {
|
|
164
|
-
const trigger = factory.createTrigger({ shape: '
|
|
178
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 50, y: 10 } }, { position: { x: 0, y: 0 } });
|
|
165
179
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
166
180
|
|
|
167
181
|
const ball = factory.createRigidBody(
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Pnt2 } from '@gg-web-engine/core';
|
|
2
|
+
import { MatterFactory, MatterWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('MatterRigidBodyComponent.bodyOptions', () => {
|
|
5
|
+
let world: MatterWorldComponent;
|
|
6
|
+
let factory: MatterFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new MatterWorldComponent();
|
|
13
|
+
factory = new MatterFactory(world);
|
|
14
|
+
await world.init();
|
|
15
|
+
world.gravity = Pnt2.O;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reads back the mass/friction/restitution a dynamic body was actually created with', () => {
|
|
23
|
+
const body = factory.createRigidBody(
|
|
24
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'dynamic', mass: 5, friction: 0.3, restitution: 0.7 } },
|
|
25
|
+
{ position: { x: 0, y: 0 } },
|
|
26
|
+
);
|
|
27
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
28
|
+
|
|
29
|
+
expect(body.bodyOptions.bodyType).toBe('dynamic');
|
|
30
|
+
expect(body.bodyOptions.mass).toBe(5);
|
|
31
|
+
expect(body.bodyOptions.friction).toBe(0.3);
|
|
32
|
+
expect(body.bodyOptions.restitution).toBe(0.7);
|
|
33
|
+
expect(body.bodyOptions.ccd).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('echoes the originally-requested bodyType/ccd even though matter-js degrades both', () => {
|
|
37
|
+
const body = factory.createRigidBody(
|
|
38
|
+
{ shape: { shape: 'BOX', dimensions: { x: 1, y: 1 } }, body: { bodyType: 'kinematic_pos', mass: 1, ccd: true } },
|
|
39
|
+
{ position: { x: 0, y: 0 } },
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(body.bodyOptions.bodyType).toBe('kinematic_pos');
|
|
43
|
+
expect(body.bodyOptions.ccd).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('reflects a mutated ownCollisionGroups/interactWithCollisionGroups live', () => {
|
|
47
|
+
const body = factory.createRigidBody(
|
|
48
|
+
{ shape: { shape: 'CIRCLE', radius: 1 }, body: { bodyType: 'static', mass: 0 } },
|
|
49
|
+
{ position: { x: 0, y: 0 } },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
body.ownCollisionGroups = [3];
|
|
53
|
+
body.interactWithCollisionGroups = [5, 6];
|
|
54
|
+
|
|
55
|
+
expect(body.bodyOptions.ownCollisionGroups).toEqual([3]);
|
|
56
|
+
expect(body.bodyOptions.interactWithCollisionGroups).toEqual([5, 6]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -19,12 +19,9 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
19
19
|
world.dispose();
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
// FIXME matter.js logic is kinda not intuitive. Spawning objects on some coordinates seems to cause collisions with
|
|
23
|
-
// all the objects that intersect the line between (0, 0) and desired position, sensors are flying away to the
|
|
24
|
-
// infinity just by setting it's position and other wonderful miracles. Investigate why these tests fail and try to fix.
|
|
25
22
|
// Note: this test is identical to rapier2d trigger test, and it is expected
|
|
26
|
-
it
|
|
27
|
-
const trigger = factory.createTrigger({ shape: '
|
|
23
|
+
it(`should detect object intersection`, async () => {
|
|
24
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
28
25
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
29
26
|
const circle = factory.createRigidBody({
|
|
30
27
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -52,8 +49,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
52
49
|
expect(exitRegistered).toBe(false);
|
|
53
50
|
});
|
|
54
51
|
|
|
55
|
-
it
|
|
56
|
-
const trigger = factory.createTrigger({ shape: '
|
|
52
|
+
it(`should detect end of object intersection`, async () => {
|
|
53
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
57
54
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
58
55
|
const circle = factory.createRigidBody({
|
|
59
56
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -74,8 +71,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
74
71
|
expect(exitRegistered).toBe(true);
|
|
75
72
|
});
|
|
76
73
|
|
|
77
|
-
it
|
|
78
|
-
const trigger = factory.createTrigger({ shape: '
|
|
74
|
+
it(`should fire object intersection if spawned inside`, async () => {
|
|
75
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
79
76
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
80
77
|
const circle = factory.createRigidBody({
|
|
81
78
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -91,8 +88,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
91
88
|
expect(enterRegistered).toBe(true);
|
|
92
89
|
});
|
|
93
90
|
|
|
94
|
-
it
|
|
95
|
-
const trigger = factory.createTrigger({ shape: '
|
|
91
|
+
it(`should fire end of object intersection if trigger removed`, async () => {
|
|
92
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
96
93
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
97
94
|
const circle = factory.createRigidBody({
|
|
98
95
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -109,8 +106,8 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
109
106
|
expect(exitRegistered).toBe(true);
|
|
110
107
|
});
|
|
111
108
|
|
|
112
|
-
it
|
|
113
|
-
const trigger = factory.createTrigger({ shape: '
|
|
109
|
+
it(`should fire end of object intersection if object removed`, async () => {
|
|
110
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
114
111
|
trigger.addToWorld({ physicsWorld: world } as any);
|
|
115
112
|
const circle = factory.createRigidBody({
|
|
116
113
|
shape: { shape: 'CIRCLE', radius: 1 },
|
|
@@ -124,8 +121,39 @@ describe(`MatterTriggerComponent`, () => {
|
|
|
124
121
|
world.simulate(1);
|
|
125
122
|
trigger.checkOverlaps();
|
|
126
123
|
circle.removeFromWorld({ physicsWorld: world } as any);
|
|
124
|
+
// the removed$ reaction drops the overlap synchronously but emits onEntityLeft on a microtask
|
|
125
|
+
// (see MatterTriggerComponent.removedSub's own doc) - nothing a later step/checkOverlaps() poll
|
|
126
|
+
// could still notice, so give that microtask a chance to run
|
|
127
|
+
await Promise.resolve();
|
|
127
128
|
world.simulate(1);
|
|
128
129
|
trigger.checkOverlaps();
|
|
129
130
|
expect(exitRegistered).toBe(true);
|
|
130
131
|
});
|
|
132
|
+
|
|
133
|
+
it(`should fire end of object intersection on the next microtask when the object is removed, with no step or checkOverlaps() poll`, async () => {
|
|
134
|
+
// Composite.remove never fires a native collisionEnd for the body it removes, so the only thing
|
|
135
|
+
// that can fire onEntityLeft for it is the trigger's own removed$ reaction. That reaction must
|
|
136
|
+
// not emit inline from inside the removal's own call stack (a removal can happen partway
|
|
137
|
+
// through an unrelated component's own lifecycle operation, e.g. a character's crouch/stand
|
|
138
|
+
// capsule swap - see MatterTriggerComponent.removedSub's own doc), so the emission is deferred
|
|
139
|
+
// to a microtask: not yet observable synchronously after removeFromWorld, observable right after
|
|
140
|
+
// one microtask hop, with no simulate()/checkOverlaps() call in between at all.
|
|
141
|
+
const trigger = factory.createTrigger({ shape: 'BOX', dimensions: { x: 10, y: 10 } });
|
|
142
|
+
trigger.addToWorld({ physicsWorld: world } as any);
|
|
143
|
+
const circle = factory.createRigidBody({
|
|
144
|
+
shape: { shape: 'CIRCLE', radius: 1 },
|
|
145
|
+
body: { bodyType: 'dynamic', mass: 1 },
|
|
146
|
+
}, { position: Pnt2.O });
|
|
147
|
+
circle.addToWorld({ physicsWorld: world } as any);
|
|
148
|
+
world.simulate(1);
|
|
149
|
+
trigger.checkOverlaps();
|
|
150
|
+
let exitRegistered = false;
|
|
151
|
+
trigger.onEntityLeft.subscribe(((obj) => {
|
|
152
|
+
exitRegistered = obj === circle;
|
|
153
|
+
}));
|
|
154
|
+
circle.removeFromWorld({ physicsWorld: world } as any);
|
|
155
|
+
expect(exitRegistered).toBe(false);
|
|
156
|
+
await Promise.resolve();
|
|
157
|
+
expect(exitRegistered).toBe(true);
|
|
158
|
+
});
|
|
131
159
|
});
|