@rapierphysicsplugin/server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/__tests__/input-buffer.test.d.ts +2 -0
  2. package/dist/__tests__/input-buffer.test.d.ts.map +1 -0
  3. package/dist/__tests__/input-buffer.test.js +53 -0
  4. package/dist/__tests__/input-buffer.test.js.map +1 -0
  5. package/dist/__tests__/integration.test.d.ts +2 -0
  6. package/dist/__tests__/integration.test.d.ts.map +1 -0
  7. package/dist/__tests__/integration.test.js +182 -0
  8. package/dist/__tests__/integration.test.js.map +1 -0
  9. package/dist/__tests__/physics-world-collisions.test.d.ts +2 -0
  10. package/dist/__tests__/physics-world-collisions.test.d.ts.map +1 -0
  11. package/dist/__tests__/physics-world-collisions.test.js +129 -0
  12. package/dist/__tests__/physics-world-collisions.test.js.map +1 -0
  13. package/dist/__tests__/physics-world.test.d.ts +2 -0
  14. package/dist/__tests__/physics-world.test.d.ts.map +1 -0
  15. package/dist/__tests__/physics-world.test.js +164 -0
  16. package/dist/__tests__/physics-world.test.js.map +1 -0
  17. package/dist/__tests__/room.test.d.ts +2 -0
  18. package/dist/__tests__/room.test.d.ts.map +1 -0
  19. package/dist/__tests__/room.test.js +189 -0
  20. package/dist/__tests__/room.test.js.map +1 -0
  21. package/dist/__tests__/state-manager.test.d.ts +2 -0
  22. package/dist/__tests__/state-manager.test.d.ts.map +1 -0
  23. package/dist/__tests__/state-manager.test.js +122 -0
  24. package/dist/__tests__/state-manager.test.js.map +1 -0
  25. package/dist/client-connection.d.ts +18 -0
  26. package/dist/client-connection.d.ts.map +1 -0
  27. package/dist/client-connection.js +41 -0
  28. package/dist/client-connection.js.map +1 -0
  29. package/dist/clock-sync.d.ts +4 -0
  30. package/dist/clock-sync.d.ts.map +1 -0
  31. package/dist/clock-sync.js +10 -0
  32. package/dist/clock-sync.js.map +1 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +35 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/input-buffer.d.ts +11 -0
  38. package/dist/input-buffer.d.ts.map +1 -0
  39. package/dist/input-buffer.js +40 -0
  40. package/dist/input-buffer.js.map +1 -0
  41. package/dist/physics-world.d.ts +33 -0
  42. package/dist/physics-world.d.ts.map +1 -0
  43. package/dist/physics-world.js +326 -0
  44. package/dist/physics-world.js.map +1 -0
  45. package/dist/room.d.ts +60 -0
  46. package/dist/room.d.ts.map +1 -0
  47. package/dist/room.js +393 -0
  48. package/dist/room.js.map +1 -0
  49. package/dist/server.d.ts +17 -0
  50. package/dist/server.d.ts.map +1 -0
  51. package/dist/server.js +268 -0
  52. package/dist/server.js.map +1 -0
  53. package/dist/simulation-loop.d.ts +14 -0
  54. package/dist/simulation-loop.d.ts.map +1 -0
  55. package/dist/simulation-loop.js +42 -0
  56. package/dist/simulation-loop.js.map +1 -0
  57. package/dist/state-manager.d.ts +20 -0
  58. package/dist/state-manager.d.ts.map +1 -0
  59. package/dist/state-manager.js +120 -0
  60. package/dist/state-manager.js.map +1 -0
  61. package/package.json +24 -0
  62. package/src/__tests__/input-buffer.test.ts +64 -0
  63. package/src/__tests__/integration.test.ts +227 -0
  64. package/src/__tests__/physics-world-collisions.test.ts +155 -0
  65. package/src/__tests__/physics-world.test.ts +197 -0
  66. package/src/__tests__/room.test.ts +232 -0
  67. package/src/__tests__/state-manager.test.ts +152 -0
  68. package/src/client-connection.ts +52 -0
  69. package/src/clock-sync.ts +16 -0
  70. package/src/index.ts +40 -0
  71. package/src/input-buffer.ts +46 -0
  72. package/src/physics-world.ts +400 -0
  73. package/src/room.ts +487 -0
  74. package/src/server.ts +312 -0
  75. package/src/simulation-loop.ts +48 -0
  76. package/src/state-manager.ts +136 -0
  77. package/tsconfig.json +11 -0
  78. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,400 @@
1
+ import type RAPIER from '@dimforge/rapier3d-compat';
2
+ import type {
3
+ BodyDescriptor,
4
+ BodyState,
5
+ CollisionEventData,
6
+ ConstraintDescriptor,
7
+ Vec3,
8
+ Quat,
9
+ InputAction,
10
+ } from '@rapierphysicsplugin/shared';
11
+ import { FIXED_TIMESTEP, createJointData } from '@rapierphysicsplugin/shared';
12
+
13
+ export class PhysicsWorld {
14
+ private world: RAPIER.World;
15
+ private rapier: typeof RAPIER;
16
+ private bodyMap: Map<string, RAPIER.RigidBody> = new Map();
17
+ private colliderMap: Map<string, RAPIER.Collider> = new Map();
18
+ private colliderHandleToBodyId: Map<number, string> = new Map();
19
+ private constraintMap: Map<string, RAPIER.ImpulseJoint> = new Map();
20
+ private eventQueue: RAPIER.EventQueue;
21
+
22
+ constructor(rapier: typeof RAPIER, gravity: Vec3 = { x: 0, y: -9.81, z: 0 }) {
23
+ this.rapier = rapier;
24
+ this.world = new rapier.World({ x: gravity.x, y: gravity.y, z: gravity.z });
25
+ this.world.timestep = FIXED_TIMESTEP;
26
+ this.eventQueue = new rapier.EventQueue(true);
27
+ }
28
+
29
+ addBody(descriptor: BodyDescriptor): string {
30
+ const { rapier, world } = this;
31
+ const { id, shape, motionType, position, rotation, mass, centerOfMass, restitution, friction, isTrigger } = descriptor;
32
+
33
+ if (this.bodyMap.has(id)) {
34
+ throw new Error(`Body with id "${id}" already exists`);
35
+ }
36
+
37
+ // Create rigid body description
38
+ let bodyDesc: RAPIER.RigidBodyDesc;
39
+ switch (motionType) {
40
+ case 'dynamic':
41
+ bodyDesc = rapier.RigidBodyDesc.dynamic();
42
+ break;
43
+ case 'static':
44
+ bodyDesc = rapier.RigidBodyDesc.fixed();
45
+ break;
46
+ case 'kinematic':
47
+ bodyDesc = rapier.RigidBodyDesc.kinematicPositionBased();
48
+ break;
49
+ }
50
+
51
+ bodyDesc.setTranslation(position.x, position.y, position.z);
52
+ bodyDesc.setRotation(new rapier.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w));
53
+
54
+ const rigidBody = world.createRigidBody(bodyDesc);
55
+
56
+ // Create collider
57
+ let colliderDesc: RAPIER.ColliderDesc;
58
+ switch (shape.type) {
59
+ case 'box': {
60
+ const p = shape.params as { halfExtents: Vec3 };
61
+ colliderDesc = rapier.ColliderDesc.cuboid(
62
+ p.halfExtents.x,
63
+ p.halfExtents.y,
64
+ p.halfExtents.z
65
+ );
66
+ break;
67
+ }
68
+ case 'sphere': {
69
+ const p = shape.params as { radius: number };
70
+ colliderDesc = rapier.ColliderDesc.ball(p.radius);
71
+ break;
72
+ }
73
+ case 'capsule': {
74
+ const p = shape.params as { halfHeight: number; radius: number };
75
+ colliderDesc = rapier.ColliderDesc.capsule(p.halfHeight, p.radius);
76
+ break;
77
+ }
78
+ case 'mesh': {
79
+ const p = shape.params as { vertices: Float32Array; indices: Uint32Array };
80
+ colliderDesc = rapier.ColliderDesc.trimesh(p.vertices, p.indices);
81
+ break;
82
+ }
83
+ }
84
+
85
+ if (centerOfMass !== undefined && motionType === 'dynamic') {
86
+ const m = mass ?? 1.0;
87
+ colliderDesc.setMassProperties(
88
+ m,
89
+ { x: centerOfMass.x, y: centerOfMass.y, z: centerOfMass.z },
90
+ { x: m / 6, y: m / 6, z: m / 6 },
91
+ { x: 0, y: 0, z: 0, w: 1 },
92
+ );
93
+ } else if (mass !== undefined && motionType === 'dynamic') {
94
+ colliderDesc.setMass(mass);
95
+ }
96
+ if (restitution !== undefined) {
97
+ colliderDesc.setRestitution(restitution);
98
+ }
99
+ if (friction !== undefined) {
100
+ colliderDesc.setFriction(friction);
101
+ }
102
+ if (isTrigger) {
103
+ colliderDesc.setSensor(true);
104
+ }
105
+ colliderDesc.setActiveEvents(rapier.ActiveEvents.COLLISION_EVENTS);
106
+
107
+ const collider = world.createCollider(colliderDesc, rigidBody);
108
+
109
+ this.bodyMap.set(id, rigidBody);
110
+ this.colliderMap.set(id, collider);
111
+ this.colliderHandleToBodyId.set(collider.handle, id);
112
+
113
+ return id;
114
+ }
115
+
116
+ removeBody(id: string): void {
117
+ const body = this.bodyMap.get(id);
118
+ if (!body) return;
119
+
120
+ const collider = this.colliderMap.get(id);
121
+ if (collider) {
122
+ this.colliderHandleToBodyId.delete(collider.handle);
123
+ }
124
+
125
+ this.world.removeRigidBody(body);
126
+ this.bodyMap.delete(id);
127
+ this.colliderMap.delete(id);
128
+ }
129
+
130
+ applyForce(id: string, force: Vec3, point?: Vec3): void {
131
+ const body = this.bodyMap.get(id);
132
+ if (!body) return;
133
+
134
+ if (point) {
135
+ body.addForceAtPoint(
136
+ new this.rapier.Vector3(force.x, force.y, force.z),
137
+ new this.rapier.Vector3(point.x, point.y, point.z),
138
+ true
139
+ );
140
+ } else {
141
+ body.addForce(new this.rapier.Vector3(force.x, force.y, force.z), true);
142
+ }
143
+ }
144
+
145
+ applyImpulse(id: string, impulse: Vec3, point?: Vec3): void {
146
+ const body = this.bodyMap.get(id);
147
+ if (!body) return;
148
+
149
+ if (point) {
150
+ body.applyImpulseAtPoint(
151
+ new this.rapier.Vector3(impulse.x, impulse.y, impulse.z),
152
+ new this.rapier.Vector3(point.x, point.y, point.z),
153
+ true
154
+ );
155
+ } else {
156
+ body.applyImpulse(new this.rapier.Vector3(impulse.x, impulse.y, impulse.z), true);
157
+ }
158
+ }
159
+
160
+ setBodyVelocity(id: string, linVel: Vec3, angVel?: Vec3): void {
161
+ const body = this.bodyMap.get(id);
162
+ if (!body) return;
163
+
164
+ body.setLinvel(new this.rapier.Vector3(linVel.x, linVel.y, linVel.z), true);
165
+ if (angVel) {
166
+ body.setAngvel(new this.rapier.Vector3(angVel.x, angVel.y, angVel.z), true);
167
+ }
168
+ }
169
+
170
+ setBodyPosition(id: string, position: Vec3): void {
171
+ const body = this.bodyMap.get(id);
172
+ if (!body) return;
173
+
174
+ body.setTranslation(new this.rapier.Vector3(position.x, position.y, position.z), true);
175
+ }
176
+
177
+ setBodyRotation(id: string, rotation: Quat): void {
178
+ const body = this.bodyMap.get(id);
179
+ if (!body) return;
180
+
181
+ body.setRotation(
182
+ new this.rapier.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w),
183
+ true
184
+ );
185
+ }
186
+
187
+ applyInput(action: InputAction): void {
188
+ switch (action.type) {
189
+ case 'applyForce':
190
+ if (action.data.force) {
191
+ this.applyForce(action.bodyId, action.data.force, action.data.point);
192
+ }
193
+ break;
194
+ case 'applyImpulse':
195
+ if (action.data.impulse) {
196
+ this.applyImpulse(action.bodyId, action.data.impulse, action.data.point);
197
+ }
198
+ break;
199
+ case 'setVelocity':
200
+ if (action.data.linVel) {
201
+ this.setBodyVelocity(action.bodyId, action.data.linVel, action.data.angVel);
202
+ }
203
+ break;
204
+ case 'setAngularVelocity':
205
+ if (action.data.angVel) {
206
+ const body = this.bodyMap.get(action.bodyId);
207
+ if (body) {
208
+ body.setAngvel(
209
+ new this.rapier.Vector3(action.data.angVel.x, action.data.angVel.y, action.data.angVel.z),
210
+ true
211
+ );
212
+ }
213
+ }
214
+ break;
215
+ case 'setPosition':
216
+ if (action.data.position) {
217
+ this.setBodyPosition(action.bodyId, action.data.position);
218
+ }
219
+ break;
220
+ case 'setRotation':
221
+ if (action.data.rotation) {
222
+ this.setBodyRotation(action.bodyId, action.data.rotation);
223
+ }
224
+ break;
225
+ }
226
+ }
227
+
228
+ addConstraint(descriptor: ConstraintDescriptor): string {
229
+ const { id, bodyIdA, bodyIdB } = descriptor;
230
+
231
+ if (this.constraintMap.has(id)) {
232
+ throw new Error(`Constraint with id "${id}" already exists`);
233
+ }
234
+
235
+ const rbA = this.bodyMap.get(bodyIdA);
236
+ const rbB = this.bodyMap.get(bodyIdB);
237
+ if (!rbA) throw new Error(`Body "${bodyIdA}" not found for constraint "${id}"`);
238
+ if (!rbB) throw new Error(`Body "${bodyIdB}" not found for constraint "${id}"`);
239
+
240
+ const jointData = createJointData(this.rapier, descriptor);
241
+ const joint = this.world.createImpulseJoint(jointData, rbA, rbB, true);
242
+
243
+ if (descriptor.collision === false) {
244
+ joint.setContactsEnabled(false);
245
+ }
246
+
247
+ this.constraintMap.set(id, joint);
248
+ return id;
249
+ }
250
+
251
+ removeConstraint(id: string): void {
252
+ const joint = this.constraintMap.get(id);
253
+ if (!joint) return;
254
+ this.world.removeImpulseJoint(joint, true);
255
+ this.constraintMap.delete(id);
256
+ }
257
+
258
+ hasConstraint(id: string): boolean {
259
+ return this.constraintMap.has(id);
260
+ }
261
+
262
+ step(): CollisionEventData[] {
263
+ this.world.step(this.eventQueue);
264
+
265
+ const events: CollisionEventData[] = [];
266
+
267
+ this.eventQueue.drainCollisionEvents((handle1, handle2, started) => {
268
+ const bodyIdA = this.colliderHandleToBodyId.get(handle1);
269
+ const bodyIdB = this.colliderHandleToBodyId.get(handle2);
270
+ if (!bodyIdA || !bodyIdB) return;
271
+
272
+ const collider1 = this.world.getCollider(handle1);
273
+ const collider2 = this.world.getCollider(handle2);
274
+ if (!collider1 || !collider2) return;
275
+
276
+ const isSensor = collider1.isSensor() || collider2.isSensor();
277
+
278
+ let type: CollisionEventData['type'];
279
+ if (isSensor) {
280
+ type = started ? 'TRIGGER_ENTERED' : 'TRIGGER_EXITED';
281
+ } else {
282
+ type = started ? 'COLLISION_STARTED' : 'COLLISION_FINISHED';
283
+ }
284
+
285
+ let point: Vec3 | null = null;
286
+ let normal: Vec3 | null = null;
287
+ let impulse = 0;
288
+
289
+ if (started && !isSensor) {
290
+ this.world.contactPair(collider1, collider2, (manifold, flipped) => {
291
+ const cp = manifold.localContactPoint1(0);
292
+ if (cp) {
293
+ point = { x: cp.x, y: cp.y, z: cp.z };
294
+ }
295
+ const n = manifold.localNormal1();
296
+ if (n) {
297
+ normal = flipped
298
+ ? { x: -n.x, y: -n.y, z: -n.z }
299
+ : { x: n.x, y: n.y, z: n.z };
300
+ }
301
+ impulse = manifold.contactImpulse(0) ?? 0;
302
+ });
303
+ }
304
+
305
+ events.push({ bodyIdA, bodyIdB, type, point, normal, impulse });
306
+ });
307
+
308
+ return events;
309
+ }
310
+
311
+ getSnapshot(skipSleeping = false): BodyState[] {
312
+ const states: BodyState[] = [];
313
+ for (const [id, body] of this.bodyMap) {
314
+ if (skipSleeping && body.isSleeping()) continue;
315
+ const pos = body.translation();
316
+ const rot = body.rotation();
317
+ const linVel = body.linvel();
318
+ const angVel = body.angvel();
319
+ states.push({
320
+ id,
321
+ position: { x: pos.x, y: pos.y, z: pos.z },
322
+ rotation: { x: rot.x, y: rot.y, z: rot.z, w: rot.w },
323
+ linVel: { x: linVel.x, y: linVel.y, z: linVel.z },
324
+ angVel: { x: angVel.x, y: angVel.y, z: angVel.z },
325
+ });
326
+ }
327
+ return states;
328
+ }
329
+
330
+ isBodySleeping(id: string): boolean {
331
+ const body = this.bodyMap.get(id);
332
+ return body ? body.isSleeping() : false;
333
+ }
334
+
335
+ getBodyState(id: string): BodyState | null {
336
+ const body = this.bodyMap.get(id);
337
+ if (!body) return null;
338
+
339
+ const pos = body.translation();
340
+ const rot = body.rotation();
341
+ const linVel = body.linvel();
342
+ const angVel = body.angvel();
343
+ return {
344
+ id,
345
+ position: { x: pos.x, y: pos.y, z: pos.z },
346
+ rotation: { x: rot.x, y: rot.y, z: rot.z, w: rot.w },
347
+ linVel: { x: linVel.x, y: linVel.y, z: linVel.z },
348
+ angVel: { x: angVel.x, y: angVel.y, z: angVel.z },
349
+ };
350
+ }
351
+
352
+ loadState(bodies: BodyDescriptor[]): void {
353
+ for (const body of bodies) {
354
+ this.addBody(body);
355
+ }
356
+ }
357
+
358
+ reset(bodies: BodyDescriptor[], constraints?: ConstraintDescriptor[]): void {
359
+ // Remove all existing constraints first (joints reference bodies)
360
+ for (const [, joint] of this.constraintMap) {
361
+ this.world.removeImpulseJoint(joint, true);
362
+ }
363
+ this.constraintMap.clear();
364
+
365
+ // Remove all existing bodies
366
+ for (const [, body] of this.bodyMap) {
367
+ this.world.removeRigidBody(body);
368
+ }
369
+ this.bodyMap.clear();
370
+ this.colliderMap.clear();
371
+ this.colliderHandleToBodyId.clear();
372
+
373
+ // Reload from descriptors
374
+ this.loadState(bodies);
375
+
376
+ // Re-create constraints
377
+ if (constraints) {
378
+ for (const c of constraints) {
379
+ this.addConstraint(c);
380
+ }
381
+ }
382
+ }
383
+
384
+ hasBody(id: string): boolean {
385
+ return this.bodyMap.has(id);
386
+ }
387
+
388
+ get bodyCount(): number {
389
+ return this.bodyMap.size;
390
+ }
391
+
392
+ destroy(): void {
393
+ this.constraintMap.clear();
394
+ this.eventQueue.free();
395
+ this.world.free();
396
+ this.bodyMap.clear();
397
+ this.colliderMap.clear();
398
+ this.colliderHandleToBodyId.clear();
399
+ }
400
+ }