@gg-web-engine/ammo 0.0.58 → 0.0.60

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.
Binary file
package/dist/types.d.ts CHANGED
@@ -12,4 +12,6 @@ export type AmmoPhysicsTypeDocRepo = {
12
12
  trigger: AmmoTriggerComponent;
13
13
  raycastVehicle: AmmoRaycastVehicleComponent;
14
14
  };
15
- export type AmmoGgWorld = Gg3dWorld<Gg3dWorldTypeDocPPatch<AmmoPhysicsTypeDocRepo>, Gg3dWorldSceneTypeDocPPatch<AmmoPhysicsTypeDocRepo, AmmoWorldComponent>>;
15
+ export type AmmoTypeDoc = Gg3dWorldTypeDocPPatch<AmmoPhysicsTypeDocRepo>;
16
+ export type AmmoSceneTypeDoc = Gg3dWorldSceneTypeDocPPatch<AmmoPhysicsTypeDocRepo, AmmoWorldComponent>;
17
+ export type AmmoGgWorld = Gg3dWorld<AmmoTypeDoc, AmmoSceneTypeDoc>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gg-web-engine/ammo",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
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",
@@ -34,20 +34,21 @@
34
34
  "dependencies": {
35
35
  },
36
36
  "devDependencies": {
37
- "@gg-web-engine/core": "0.0.58",
38
- "@types/jest": "^29.5.12",
39
- "jest": "^29.7.0",
40
- "jest-environment-jsdom": "^29.7.0",
41
- "mini-signals": "2.0.0",
42
- "prettier": "^3.3.3",
43
- "rxjs": "7.8.1",
44
- "ts-jest": "^29.2.4",
45
- "typescript": "~5.5.4"
37
+ "@gg-web-engine/core": "0.0.60",
38
+ "@types/jest": "^30.0.0",
39
+ "@types/node": "^26.4.0",
40
+ "jest": "^30.5.0",
41
+ "jest-environment-jsdom": "^30.5.0",
42
+ "mini-signals": "3.0.0",
43
+ "prettier": "^3.9.6",
44
+ "rxjs": "7.8.2",
45
+ "ts-jest": "^29.4.12",
46
+ "typescript": "~6.0.3"
46
47
  },
47
48
  "peerDependencies": {
48
- "@gg-web-engine/core": "0.0.58",
49
- "mini-signals": "2.0.0",
50
- "rxjs": "7.8.1"
49
+ "@gg-web-engine/core": "0.0.60",
50
+ "mini-signals": "3.0.0",
51
+ "rxjs": "7.8.2"
51
52
  },
52
53
  "jest": {
53
54
  "rootDir": ".",
@@ -0,0 +1,88 @@
1
+ import { Shape3DDescriptor } from '@gg-web-engine/core';
2
+ import { AmmoFactory, AmmoWorldComponent } from '../src';
3
+
4
+ describe('AmmoFactory', () => {
5
+ let world: AmmoWorldComponent;
6
+ let factory: AmmoFactory;
7
+
8
+ beforeEach(async () => {
9
+ if (world) {
10
+ world.dispose();
11
+ }
12
+ world = new AmmoWorldComponent();
13
+ await world.init();
14
+ factory = world.factory;
15
+ world.gravity = { x: 0, y: 0, z: 0 };
16
+ });
17
+
18
+ afterAll(() => {
19
+ world.dispose();
20
+ });
21
+
22
+ const shapes: { name: string; shape: Shape3DDescriptor }[] = [
23
+ { name: 'PLANE', shape: { shape: 'PLANE' } },
24
+ { name: 'BOX', shape: { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } } },
25
+ { name: 'SPHERE', shape: { shape: 'SPHERE', radius: 1 } },
26
+ { name: 'CAPSULE', shape: { shape: 'CAPSULE', radius: 0.5, centersDistance: 1 } },
27
+ { name: 'CYLINDER', shape: { shape: 'CYLINDER', radius: 0.5, height: 1 } },
28
+ { name: 'CONE', shape: { shape: 'CONE', radius: 0.5, height: 1 } },
29
+ {
30
+ name: 'COMPOUND',
31
+ shape: {
32
+ shape: 'COMPOUND',
33
+ children: [
34
+ { shape: { shape: 'BOX', dimensions: { x: 1, y: 1, z: 1 } }, position: { x: 0, y: 0, z: 0 } },
35
+ { shape: { shape: 'SPHERE', radius: 0.5 }, position: { x: 0, y: 0, z: 1 } },
36
+ ],
37
+ },
38
+ },
39
+ {
40
+ name: 'CONVEX_HULL',
41
+ shape: {
42
+ shape: 'CONVEX_HULL',
43
+ vertices: [
44
+ { x: -1, y: -1, z: -1 },
45
+ { x: 1, y: -1, z: -1 },
46
+ { x: 1, y: 1, z: -1 },
47
+ { x: -1, y: 1, z: -1 },
48
+ { x: 0, y: 0, z: 1 },
49
+ ],
50
+ },
51
+ },
52
+ {
53
+ name: 'MESH',
54
+ shape: {
55
+ shape: 'MESH',
56
+ vertices: [
57
+ { x: -1, y: -1, z: 0 },
58
+ { x: 1, y: -1, z: 0 },
59
+ { x: 0, y: 1, z: 0 },
60
+ ],
61
+ faces: [[0, 1, 2]],
62
+ },
63
+ },
64
+ ];
65
+
66
+ describe.each(shapes)('$name shape', ({ shape }) => {
67
+ it('should create a static rigid body without throwing', () => {
68
+ const body = factory.createRigidBody({ shape, body: { dynamic: false, mass: 0 } }, { position: { x: 1, y: 2, z: 3 } });
69
+ body.addToWorld({ physicsWorld: world } as any);
70
+ expect(body.position).toEqual({ x: 1, y: 2, z: 3 });
71
+ world.simulate(16);
72
+ });
73
+
74
+ it('should create a trigger without throwing', () => {
75
+ const trigger = factory.createTrigger(shape, { position: { x: 0, y: 0, z: 0 } });
76
+ trigger.addToWorld({ physicsWorld: world } as any);
77
+ world.simulate(16);
78
+ trigger.checkOverlaps();
79
+ });
80
+ });
81
+
82
+ it('should throw a clear error for an unsupported shape', () => {
83
+ expect(() => factory.createRigidBody({
84
+ shape: { shape: 'NOT_A_SHAPE' } as any,
85
+ body: { dynamic: false, mass: 0 },
86
+ })).toThrow(/not implemented for Ammo\.js/);
87
+ });
88
+ });
@@ -0,0 +1,139 @@
1
+ import { SuspensionOptions, WheelOptions } from '@gg-web-engine/core';
2
+ import { AmmoFactory, AmmoRaycastVehicleComponent, AmmoRigidBodyComponent, AmmoWorldComponent } from '../../src';
3
+
4
+ // Shared suspension/wheel setup mirroring examples/ammo-car-three-ammo, known to produce a
5
+ // stable, quickly-settling vehicle - see that example for the full car setup.
6
+ const suspension: SuspensionOptions = {
7
+ compression: 4.4,
8
+ damping: 2.3,
9
+ restLength: 0.6,
10
+ stiffness: 20,
11
+ };
12
+
13
+ const wheelOptions = (isFront: boolean, isLeft: boolean): WheelOptions => ({
14
+ isFront,
15
+ isLeft,
16
+ tyreRadius: isFront ? 0.35 : 0.4,
17
+ tyreWidth: isFront ? 0.2 : 0.3,
18
+ frictionSlip: 1000,
19
+ rollInfluence: 0.2,
20
+ maxTravel: 5,
21
+ position: {
22
+ x: isLeft ? 1 : -1,
23
+ y: isFront ? 1.7 : -1,
24
+ z: 0.3,
25
+ },
26
+ });
27
+
28
+ const addWheels = (vehicle: AmmoRaycastVehicleComponent) => {
29
+ for (const isFront of [true, false]) {
30
+ for (const isLeft of [true, false]) {
31
+ vehicle.addWheel(wheelOptions(isFront, isLeft), suspension);
32
+ }
33
+ }
34
+ };
35
+
36
+ const createFloor = (
37
+ factory: AmmoFactory,
38
+ topZ: number,
39
+ ): AmmoRigidBodyComponent =>
40
+ factory.createRigidBody(
41
+ {
42
+ shape: { shape: 'BOX', dimensions: { x: 75, y: 75, z: 1 } },
43
+ body: { dynamic: false, mass: 0 },
44
+ },
45
+ { position: { x: 0, y: 0, z: topZ - 0.5 } },
46
+ );
47
+
48
+ const createVehicle = (
49
+ world: AmmoWorldComponent,
50
+ factory: AmmoFactory,
51
+ position: { x: number; y: number; z: number },
52
+ ): AmmoRaycastVehicleComponent => {
53
+ const chassis = factory.createRigidBody(
54
+ { shape: { shape: 'BOX', dimensions: { x: 1.8, y: 4, z: 0.6 } }, body: { dynamic: true, mass: 800 } },
55
+ { position },
56
+ );
57
+ const vehicle = factory.createRaycastVehicle(chassis);
58
+ addWheels(vehicle);
59
+ return vehicle;
60
+ };
61
+
62
+ describe('AmmoRaycastVehicleComponent', () => {
63
+ let world: AmmoWorldComponent;
64
+ let factory: AmmoFactory;
65
+
66
+ beforeEach(async () => {
67
+ if (world) {
68
+ world.dispose();
69
+ }
70
+ world = new AmmoWorldComponent();
71
+ await world.init();
72
+ factory = world.factory;
73
+ // default earth-like gravity, so vehicles actually fall onto the floors below
74
+ world.gravity = { x: 0, y: 0, z: -9.82 };
75
+ });
76
+
77
+ afterAll(() => {
78
+ world.dispose();
79
+ });
80
+
81
+ it('should fall under gravity and come to rest on a floor below it', () => {
82
+ const floor = createFloor(factory, 0);
83
+ floor.addToWorld({ physicsWorld: world } as any);
84
+
85
+ const vehicle = createVehicle(world, factory, { x: 0, y: 0, z: 4 });
86
+ vehicle.addToWorld({ physicsWorld: world } as any);
87
+
88
+ for (let i = 0; i < 100; i++) {
89
+ world.simulate(60);
90
+ }
91
+
92
+ // chassis should have settled just above the floor surface (z = 0), not still falling
93
+ // and not sunk through it
94
+ expect(vehicle.position.z).toBeGreaterThan(-1);
95
+ expect(vehicle.position.z).toBeLessThan(3);
96
+ expect(vehicle.isWheelTouchesGround(0)).toBe(true);
97
+ });
98
+
99
+ it('should let each vehicle fall through a floor with a different collision group and rest ' +
100
+ 'only on the floor sharing its own collision group', () => {
101
+ const groupA = world.registerCollisionGroup();
102
+ const groupB = world.registerCollisionGroup();
103
+
104
+ const floorA = createFloor(factory, 0);
105
+ floorA.addToWorld({ physicsWorld: world } as any);
106
+ floorA.ownCollisionGroups = floorA.interactWithCollisionGroups = [groupA];
107
+
108
+ const floorB = createFloor(factory, -10);
109
+ floorB.addToWorld({ physicsWorld: world } as any);
110
+ floorB.ownCollisionGroups = floorB.interactWithCollisionGroups = [groupB];
111
+
112
+ const vehicleA = createVehicle(world, factory, { x: 0, y: 0, z: 4 });
113
+ vehicleA.addToWorld({ physicsWorld: world } as any);
114
+ vehicleA.ownCollisionGroups = vehicleA.interactWithCollisionGroups = [groupA];
115
+
116
+ const vehicleB = createVehicle(world, factory, { x: 20, y: 0, z: 4 });
117
+ vehicleB.addToWorld({ physicsWorld: world } as any);
118
+ vehicleB.ownCollisionGroups = vehicleB.interactWithCollisionGroups = [groupB];
119
+
120
+ for (let i = 0; i < 100; i++) {
121
+ world.simulate(60);
122
+ }
123
+
124
+ // vehicleA shares its collision group with floorA (z=0) - it should have landed there,
125
+ // both via chassis-body collision and via the raycast vehicle's suspension.
126
+ expect(vehicleA.position.z).toBeGreaterThan(-1);
127
+ expect(vehicleA.position.z).toBeLessThan(3);
128
+ expect(vehicleA.isWheelTouchesGround(0)).toBe(true);
129
+
130
+ // vehicleB does not share a collision group with floorA, so it must fall straight through
131
+ // it (this is what the custom btVehicleRaycaster collision-group patch fixes: without it,
132
+ // the wheel raycast ignores collision groups entirely and would detect floorA as ground,
133
+ // holding the vehicle up via suspension force even though its chassis passes through).
134
+ // It should come to rest on floorB (z=-10) instead.
135
+ expect(vehicleB.position.z).toBeGreaterThan(-11);
136
+ expect(vehicleB.position.z).toBeLessThan(-7);
137
+ expect(vehicleB.isWheelTouchesGround(0)).toBe(true);
138
+ });
139
+ });
@@ -17,6 +17,56 @@ describe('AmmoWorldComponent', () => {
17
17
  world.dispose();
18
18
  });
19
19
 
20
+ describe('Gravity', () => {
21
+ it('should default to earth-like downward gravity on a fresh world', async () => {
22
+ const freshWorld = new AmmoWorldComponent();
23
+ await freshWorld.init();
24
+ expect(freshWorld.gravity).toEqual({ x: 0, y: 0, z: -9.82 });
25
+ freshWorld.dispose();
26
+ });
27
+
28
+ it('should read back a value written to it and apply it to the simulation', () => {
29
+ world.gravity = { x: 1, y: -2, z: 3 };
30
+ expect(world.gravity).toEqual({ x: 1, y: -2, z: 3 });
31
+
32
+ const ball = world.factory.createRigidBody({
33
+ shape: { shape: 'SPHERE', radius: 1 },
34
+ body: { dynamic: true, mass: 1 },
35
+ }, { position: { x: 0, y: 0, z: 0 } });
36
+ ball.addToWorld({ physicsWorld: world } as any);
37
+
38
+ for (let i = 0; i < 20; i++) {
39
+ world.simulate(60);
40
+ }
41
+ // ball should have drifted along every gravity axis in the sign of that axis' gravity
42
+ expect(ball.position.x).toBeGreaterThan(0);
43
+ expect(ball.position.y).toBeLessThan(0);
44
+ expect(ball.position.z).toBeGreaterThan(0);
45
+ });
46
+ });
47
+
48
+ describe('Collision groups', () => {
49
+ it('should register groups starting from 1 (0 is the reserved main collision group)', () => {
50
+ expect(world.mainCollisionGroup).toBe(0);
51
+ expect(world.registerCollisionGroup()).toBe(1);
52
+ expect(world.registerCollisionGroup()).toBe(2);
53
+ });
54
+
55
+ it('should let a deregistered group id be reused', () => {
56
+ const group = world.registerCollisionGroup();
57
+ world.deregisterCollisionGroup(group);
58
+ expect(world.registerCollisionGroup()).toBe(group);
59
+ });
60
+
61
+ it('should throw once all 16 collision groups (0-15) are exhausted', () => {
62
+ // group 0 is implicitly reserved as the main collision group, so 15 more can be registered
63
+ for (let i = 0; i < 15; i++) {
64
+ world.registerCollisionGroup();
65
+ }
66
+ expect(() => world.registerCollisionGroup()).toThrow();
67
+ });
68
+ });
69
+
20
70
  describe('Rigid bodies', () => {
21
71
 
22
72
  it('should simulate inertial motion of rigid body', () => {
package/tsconfig.json CHANGED
@@ -2,7 +2,11 @@
2
2
  "extends": "./../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "baseUrl": "./src/",
5
- "outDir": "./dist/"
5
+ "outDir": "./dist/",
6
+ "rootDir": "./src/",
7
+ "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
8
+ "types": ["jest", "node"]
6
9
  },
7
10
  "include": ["src/*.ts", "src/**/*.ts"],
11
+ "references": [{ "path": "../core" }]
8
12
  }