@series-inc/rundot-syncplay 5.23.0-beta.8 → 5.23.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.
- package/README.md +61 -6
- package/dist/browser.d.ts +86 -57
- package/dist/browser.js +46 -53
- package/dist/certification.d.ts +20 -3
- package/dist/certification.js +78 -2
- package/dist/cjs/browser.js +259 -90
- package/dist/cjs/certification.js +78 -2
- package/dist/cjs/creator.js +2 -1
- package/dist/cjs/engine-complete.js +213 -3
- package/dist/cjs/engine-parity-matrix.js +19 -4
- package/dist/cjs/index.js +193 -139
- package/dist/cjs/movement3d.js +230 -1
- package/dist/cjs/node.js +3 -1
- package/dist/cjs/physics-cert.js +147 -6
- package/dist/cjs/physics2d.js +10 -4
- package/dist/cjs/physics3d-joints.js +637 -0
- package/dist/cjs/physics3d-shared.js +288 -0
- package/dist/cjs/physics3d-solver.js +1351 -0
- package/dist/cjs/physics3d-vehicle.js +467 -0
- package/dist/cjs/physics3d.js +1057 -223
- package/dist/cjs/replay-bundle.js +127 -20
- package/dist/cjs/sample-scenes.js +3 -0
- package/dist/cjs/testing.js +107 -0
- package/dist/cli.js +17 -9
- package/dist/creator.d.ts +1 -1
- package/dist/creator.js +1 -1
- package/dist/engine-complete.js +214 -4
- package/dist/engine-parity-matrix.js +19 -4
- package/dist/index.d.ts +93 -90
- package/dist/index.js +44 -56
- package/dist/movement3d.d.ts +73 -0
- package/dist/movement3d.js +229 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.js +1 -0
- package/dist/physics-cert.d.ts +1 -1
- package/dist/physics-cert.js +147 -6
- package/dist/physics2d.js +10 -4
- package/dist/physics3d-joints.d.ts +94 -0
- package/dist/physics3d-joints.js +634 -0
- package/dist/physics3d-shared.d.ts +72 -0
- package/dist/physics3d-shared.js +257 -0
- package/dist/physics3d-solver.d.ts +197 -0
- package/dist/physics3d-solver.js +1346 -0
- package/dist/physics3d-vehicle.d.ts +84 -0
- package/dist/physics3d-vehicle.js +463 -0
- package/dist/physics3d.d.ts +108 -8
- package/dist/physics3d.js +1006 -177
- package/dist/replay-bundle.d.ts +40 -1
- package/dist/replay-bundle.js +126 -20
- package/dist/sample-scenes.js +3 -0
- package/dist/testing.d.ts +52 -0
- package/dist/testing.js +45 -0
- package/package.json +11 -3
package/dist/cjs/movement3d.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stepDeterministicKcc3D = stepDeterministicKcc3D;
|
|
4
|
+
exports.stepDeterministicCarry3D = stepDeterministicCarry3D;
|
|
4
5
|
exports.runDeterministicKcc3DFilterForceFixture = runDeterministicKcc3DFilterForceFixture;
|
|
5
6
|
exports.runDeterministicKcc3DCallbackFilterFixture = runDeterministicKcc3DCallbackFilterFixture;
|
|
6
7
|
exports.runDeterministicKcc3DCrowdFixture = runDeterministicKcc3DCrowdFixture;
|
|
@@ -8,6 +9,7 @@ exports.runDeterministicKcc3DAdversarialFixture = runDeterministicKcc3DAdversari
|
|
|
8
9
|
exports.runDeterministicKcc3DGeometryFixture = runDeterministicKcc3DGeometryFixture;
|
|
9
10
|
const canonical_js_1 = require("./canonical.js");
|
|
10
11
|
const physics3d_js_1 = require("./physics3d.js");
|
|
12
|
+
const physics3d_shared_js_1 = require("./physics3d-shared.js");
|
|
11
13
|
function stepDeterministicKcc3D(body, input, world, options) {
|
|
12
14
|
let next = applyProcessors(body, input, options.frame, options.processors ?? []);
|
|
13
15
|
const events = [];
|
|
@@ -82,6 +84,9 @@ function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
82
84
|
coyoteFrames = 0;
|
|
83
85
|
events.push(next.grounded ? 'jump' : 'coyote-jump');
|
|
84
86
|
}
|
|
87
|
+
// The KCC's intended (pre-slide) horizontal position and velocity — push
|
|
88
|
+
// impulses are computed here because the slide blocks the KCC off the body.
|
|
89
|
+
const pushCandidate = { x, z, vx, vz };
|
|
85
90
|
if (options.physics !== undefined) {
|
|
86
91
|
const resolved = resolvePhysicsSlide3D(options.physics, next, { x, y, z, vx, vz }, new Set(options.ignoredPhysicsBodyIds ?? []), options.frame, options.collisionFilters ?? []);
|
|
87
92
|
physicsQueryCount += resolved.queryCount;
|
|
@@ -96,6 +101,37 @@ function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
96
101
|
events.push(resolved.swept ? 'physics-sweep' : resolved.slid ? 'physics-slide' : 'physics-correction');
|
|
97
102
|
}
|
|
98
103
|
}
|
|
104
|
+
const geometryForInteraction = normalizeKccGeometry3D(next);
|
|
105
|
+
let physicsImpulses = [];
|
|
106
|
+
// Stand on a dynamic body directly beneath the feet: attach as a platform and
|
|
107
|
+
// inherit its per-frame delta (its velocity), mirroring the kinematic path.
|
|
108
|
+
if (options.physics !== undefined && options.standOnDynamicBodies === true && vy <= 0) {
|
|
109
|
+
const ground = dynamicGroundUnderKcc(options.physics, x, y, z, geometryForInteraction, new Set(options.ignoredPhysicsBodyIds ?? []));
|
|
110
|
+
if (ground !== undefined) {
|
|
111
|
+
y = ground.topY;
|
|
112
|
+
vy = 0;
|
|
113
|
+
grounded = true;
|
|
114
|
+
platformId = ground.bodyId;
|
|
115
|
+
// Carry the rider with the crate this frame (position delta) AND inherit its
|
|
116
|
+
// velocity, mirroring the kinematic moving-platform path — otherwise the
|
|
117
|
+
// rider lags one frame and slides off a moving crate.
|
|
118
|
+
x += ground.deltaX;
|
|
119
|
+
z += ground.deltaZ;
|
|
120
|
+
vx += ground.deltaX;
|
|
121
|
+
vz += ground.deltaZ;
|
|
122
|
+
events.push('stand-on-dynamic');
|
|
123
|
+
if (ground.deltaX !== 0 || ground.deltaZ !== 0) {
|
|
124
|
+
events.push('platform-velocity-transfer');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Push the dynamic bodies the KCC walked into this frame.
|
|
129
|
+
if (options.physics !== undefined && (options.maxPushImpulse ?? 0) > 0) {
|
|
130
|
+
physicsImpulses = computeKccPushImpulses(options.physics, pushCandidate.x, y, pushCandidate.z, { vx: pushCandidate.vx, vz: pushCandidate.vz }, geometryForInteraction, options.maxPushImpulse ?? 0, new Set(options.ignoredPhysicsBodyIds ?? []));
|
|
131
|
+
if (physicsImpulses.length > 0) {
|
|
132
|
+
events.push('push-dynamic');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
99
135
|
const boundsCorrection = clampBodyToBounds({ x, y, z, vx, vy, vz, grounded, coyoteFrames, jumpBufferFrames, platformId }, world);
|
|
100
136
|
if (boundsCorrection.corrected) {
|
|
101
137
|
correctionCount += boundsCorrection.corrections;
|
|
@@ -114,9 +150,202 @@ function stepDeterministicKcc3D(body, input, world, options) {
|
|
|
114
150
|
physicsQueryCount,
|
|
115
151
|
collisionFilterCalls,
|
|
116
152
|
collisionFilterRejectedHits,
|
|
117
|
-
|
|
153
|
+
physicsImpulses,
|
|
154
|
+
checksum: (0, canonical_js_1.defaultChecksum)({ body: finalBody, events, correctionCount, physicsQueryCount, collisionFilterCalls, collisionFilterRejectedHits, physicsImpulses }),
|
|
118
155
|
};
|
|
119
156
|
}
|
|
157
|
+
const KCC_PUSH_MASS = 1;
|
|
158
|
+
/** Dynamic physics bodies the KCC capsule overlaps at (x,y,z), sorted by id. */
|
|
159
|
+
function overlappingDynamicBodies(physics, x, y, z, geometry, ignoredBodyIds) {
|
|
160
|
+
const query = {
|
|
161
|
+
shape: { type: 'capsule', radius: geometry.radius + geometry.skinWidth, halfHeight: geometry.halfHeight },
|
|
162
|
+
x,
|
|
163
|
+
y,
|
|
164
|
+
z,
|
|
165
|
+
hitStatics: false,
|
|
166
|
+
hitDynamics: true,
|
|
167
|
+
hitKinematics: false,
|
|
168
|
+
};
|
|
169
|
+
const hitIds = new Set((0, physics3d_js_1.overlapDeterministicPhysics3D)(physics, query));
|
|
170
|
+
return physics.bodies
|
|
171
|
+
.filter((body) => body.kind === 'dynamic' && hitIds.has(body.id) && !ignoredBodyIds.has(body.id))
|
|
172
|
+
.sort((left, right) => compareStrings(left.id, right.id));
|
|
173
|
+
}
|
|
174
|
+
function computeKccPushImpulses(physics, x, y, z, velocity, geometry, maxPushImpulse, ignoredBodyIds) {
|
|
175
|
+
const impulses = [];
|
|
176
|
+
for (const body of overlappingDynamicBodies(physics, x, y, z, geometry, ignoredBodyIds)) {
|
|
177
|
+
// push along the horizontal direction from the KCC toward the body, scaled
|
|
178
|
+
// by the KCC's horizontal speed, capped by maxPushImpulse.
|
|
179
|
+
const toBodyX = body.x - x;
|
|
180
|
+
const toBodyZ = body.z - z;
|
|
181
|
+
const dirLen = deterministicKccSqrtLength(toBodyX, toBodyZ);
|
|
182
|
+
const dirX = dirLen === 0 ? (velocity.vx >= 0 ? 1 : -1) : toBodyX / dirLen;
|
|
183
|
+
const dirZ = dirLen === 0 ? 0 : toBodyZ / dirLen;
|
|
184
|
+
const speedIntoBody = Math.max(0, velocity.vx * dirX + velocity.vz * dirZ);
|
|
185
|
+
if (speedIntoBody === 0) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const magnitude = Math.min(speedIntoBody * KCC_PUSH_MASS, maxPushImpulse);
|
|
189
|
+
impulses.push({
|
|
190
|
+
bodyId: body.id,
|
|
191
|
+
impulse: { x: dirX * magnitude, y: 0, z: dirZ * magnitude },
|
|
192
|
+
point: { x: body.x, y, z: body.z },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return impulses;
|
|
196
|
+
}
|
|
197
|
+
function deterministicKccSqrtLength(dx, dz) {
|
|
198
|
+
const squared = dx * dx + dz * dz;
|
|
199
|
+
if (squared <= 0) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
let estimate = 1;
|
|
203
|
+
let magnitude = squared;
|
|
204
|
+
while (magnitude >= 4) {
|
|
205
|
+
magnitude /= 4;
|
|
206
|
+
estimate *= 2;
|
|
207
|
+
}
|
|
208
|
+
if (magnitude > 1) {
|
|
209
|
+
estimate *= 2;
|
|
210
|
+
}
|
|
211
|
+
for (let iteration = 0; iteration < 8; iteration += 1) {
|
|
212
|
+
estimate = (estimate + squared / estimate) / 2;
|
|
213
|
+
}
|
|
214
|
+
return estimate;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* World-space Y half-extent of a ground body's top surface, honoring full
|
|
218
|
+
* quaternion orientation (the feet probe is orientation-aware, so the snap height
|
|
219
|
+
* must be too, or a tilted prop would float/sink). Spheres are orientation
|
|
220
|
+
* invariant; box/capsule project their local extents onto world +Y.
|
|
221
|
+
*/
|
|
222
|
+
/**
|
|
223
|
+
* World-space top-surface Y of a body directly under the point (x,z), or
|
|
224
|
+
* undefined if (x,z) is outside the body's horizontal footprint (so a KCC beside
|
|
225
|
+
* a body — a side/wall contact — is not treated as standing on it). Compound
|
|
226
|
+
* bodies resolve per child, so an L-shaped prop reports the flat part's height
|
|
227
|
+
* when the KCC is over the flat part, not the tallest child's.
|
|
228
|
+
*/
|
|
229
|
+
function groundContactTopAt(body, x, z) {
|
|
230
|
+
const q = body.orientation ?? { x: 0, y: 0, z: 0, w: 1 };
|
|
231
|
+
const shape = body.shape;
|
|
232
|
+
if (shape.type === 'box' || shape.type === 'sphere' || shape.type === 'capsule') {
|
|
233
|
+
const ext = primitiveHalfExtent(shape, (0, physics3d_shared_js_1.quatToMat3)(q));
|
|
234
|
+
if (Math.abs(x - body.x) > ext.x || Math.abs(z - body.z) > ext.z) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return body.y + ext.y;
|
|
238
|
+
}
|
|
239
|
+
if (shape.type === 'compound') {
|
|
240
|
+
let top;
|
|
241
|
+
for (const child of shape.children) {
|
|
242
|
+
const center = { x: body.x, y: body.y, z: body.z };
|
|
243
|
+
const worldOffset = (0, physics3d_shared_js_1.rotatePointByQuat)(q, child.offset);
|
|
244
|
+
const childCenterX = center.x + worldOffset.x;
|
|
245
|
+
const childCenterZ = center.z + worldOffset.z;
|
|
246
|
+
const childQ = (0, physics3d_shared_js_1.quatMultiply)(q, (0, physics3d_shared_js_1.quaternionFromQuarterTurns)(child.quarterTurns ?? { x: 0, y: 0, z: 0 }));
|
|
247
|
+
const childExt = primitiveHalfExtent(child.shape, (0, physics3d_shared_js_1.quatToMat3)(childQ));
|
|
248
|
+
if (Math.abs(x - childCenterX) > childExt.x || Math.abs(z - childCenterZ) > childExt.z) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const childTop = center.y + worldOffset.y + childExt.y;
|
|
252
|
+
top = top === undefined ? childTop : Math.max(top, childTop);
|
|
253
|
+
}
|
|
254
|
+
return top;
|
|
255
|
+
}
|
|
256
|
+
return undefined; // mesh: static-only, not a stand-on platform
|
|
257
|
+
}
|
|
258
|
+
/** World half-extents (x,y,z) of a primitive shape under rotation matrix `m`. */
|
|
259
|
+
function primitiveHalfExtent(shape, m) {
|
|
260
|
+
if (shape.type === 'sphere') {
|
|
261
|
+
return { x: shape.radius, y: shape.radius, z: shape.radius };
|
|
262
|
+
}
|
|
263
|
+
if (shape.type === 'box') {
|
|
264
|
+
return {
|
|
265
|
+
x: Math.abs(m[0]) * shape.halfX + Math.abs(m[1]) * shape.halfY + Math.abs(m[2]) * shape.halfZ,
|
|
266
|
+
y: Math.abs(m[3]) * shape.halfX + Math.abs(m[4]) * shape.halfY + Math.abs(m[5]) * shape.halfZ,
|
|
267
|
+
z: Math.abs(m[6]) * shape.halfX + Math.abs(m[7]) * shape.halfY + Math.abs(m[8]) * shape.halfZ,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// capsule: segment along local Y, spherical caps radius r
|
|
271
|
+
return {
|
|
272
|
+
x: Math.abs(m[1]) * shape.halfHeight + shape.radius,
|
|
273
|
+
y: Math.abs(m[4]) * shape.halfHeight + shape.radius,
|
|
274
|
+
z: Math.abs(m[7]) * shape.halfHeight + shape.radius,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/** A dynamic body directly under the KCC feet, with its per-frame delta. */
|
|
278
|
+
function dynamicGroundUnderKcc(physics, x, y, z, geometry, ignoredBodyIds) {
|
|
279
|
+
const feetY = y - geometry.halfHeight - geometry.skinWidth;
|
|
280
|
+
const probe = {
|
|
281
|
+
shape: { type: 'sphere', radius: geometry.radius },
|
|
282
|
+
x,
|
|
283
|
+
y: feetY,
|
|
284
|
+
z,
|
|
285
|
+
hitStatics: false,
|
|
286
|
+
hitDynamics: true,
|
|
287
|
+
hitKinematics: false,
|
|
288
|
+
};
|
|
289
|
+
const hitIds = new Set((0, physics3d_js_1.overlapDeterministicPhysics3D)(physics, probe));
|
|
290
|
+
// The KCC must stand OVER the body's horizontal footprint, not beside it —
|
|
291
|
+
// otherwise walking into a crate's side (a feet-level overlap with a body whose
|
|
292
|
+
// top towers above the feet) would vault the capsule onto the top.
|
|
293
|
+
const grounds = physics.bodies
|
|
294
|
+
.filter((body) => body.kind === 'dynamic' && hitIds.has(body.id) && !ignoredBodyIds.has(body.id))
|
|
295
|
+
.map((body) => ({ body, top: groundContactTopAt(body, x, z) }))
|
|
296
|
+
.filter((candidate) => candidate.top !== undefined)
|
|
297
|
+
.sort((left, right) => right.top - left.top || compareStrings(left.body.id, right.body.id));
|
|
298
|
+
const ground = grounds[0];
|
|
299
|
+
if (ground === undefined) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
bodyId: ground.body.id,
|
|
304
|
+
topY: ground.top + geometry.halfHeight + geometry.skinWidth,
|
|
305
|
+
deltaX: ground.body.vx,
|
|
306
|
+
deltaZ: ground.body.vz,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const CARRY_GRAB_MAX_FORCE = 40;
|
|
310
|
+
/**
|
|
311
|
+
* Drive a `grab` joint target from a carrier pose (the pick-up/carry/throw loop).
|
|
312
|
+
* While held, returns the grab joint pointing at a hold point ahead of the
|
|
313
|
+
* carrier; on release, returns a throw impulse (carrier velocity + throw
|
|
314
|
+
* strength along forward).
|
|
315
|
+
*/
|
|
316
|
+
function stepDeterministicCarry3D(carry, carrierPose) {
|
|
317
|
+
const forwardLen = deterministicKccSqrtLength3(carrierPose.forwardX, carrierPose.forwardY, carrierPose.forwardZ);
|
|
318
|
+
const fx = forwardLen === 0 ? 0 : carrierPose.forwardX / forwardLen;
|
|
319
|
+
const fy = forwardLen === 0 ? 0 : carrierPose.forwardY / forwardLen;
|
|
320
|
+
const fz = forwardLen === 0 ? 0 : carrierPose.forwardZ / forwardLen;
|
|
321
|
+
if (carry.released === true) {
|
|
322
|
+
return {
|
|
323
|
+
throwImpulse: {
|
|
324
|
+
bodyId: carry.bodyId,
|
|
325
|
+
impulse: {
|
|
326
|
+
x: carrierPose.vx + fx * carry.throwStrength,
|
|
327
|
+
y: carrierPose.vy + fy * carry.throwStrength,
|
|
328
|
+
z: carrierPose.vz + fz * carry.throwStrength,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
joint: {
|
|
335
|
+
id: `carry-${carry.bodyId}`,
|
|
336
|
+
type: 'grab',
|
|
337
|
+
body: carry.bodyId,
|
|
338
|
+
anchor: { x: 0, y: 0, z: 0 },
|
|
339
|
+
targetX: carrierPose.x + fx * carry.holdDistance,
|
|
340
|
+
targetY: carrierPose.y + fy * carry.holdDistance,
|
|
341
|
+
targetZ: carrierPose.z + fz * carry.holdDistance,
|
|
342
|
+
maxForce: CARRY_GRAB_MAX_FORCE,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function deterministicKccSqrtLength3(dx, dy, dz) {
|
|
347
|
+
return deterministicKccSqrtLength(deterministicKccSqrtLength(dx, dy), dz);
|
|
348
|
+
}
|
|
120
349
|
function runDeterministicKcc3DFilterForceFixture() {
|
|
121
350
|
const first = runKcc3DFilterForceCases();
|
|
122
351
|
const second = runKcc3DFilterForceCases();
|
package/dist/cjs/node.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.stableEngineParityJson = exports.runEngineParitySuite = exports.explainEngineParityFixture = exports.runChaosCertificationSuite = exports.explainChaosFixture = exports.startLocalWebSocketProtocolServer = exports.runLocalWebSocketAdapterTest = exports.runNetworkedInputAuthoritySpike = exports.defaultNetworkedInputAuthoritySpikeOptions = exports.verifyReplay = exports.createSyncplaySession = exports.createOfflineSession = void 0;
|
|
17
|
+
exports.stableEngineParityJson = exports.runEngineParitySuite = exports.explainEngineParityFixture = exports.runChaosCertificationSuite = exports.explainChaosFixture = exports.startLocalWebSocketProtocolServer = exports.runLocalWebSocketAdapterTest = exports.runNetworkedInputAuthoritySpike = exports.defaultNetworkedInputAuthoritySpikeOptions = exports.verifyReplayScoreAgainstTrustedRuntime = exports.verifyReplay = exports.createSyncplaySession = exports.createOfflineSession = void 0;
|
|
18
18
|
__exportStar(require("./multiplayer-service.js"), exports);
|
|
19
19
|
__exportStar(require("./input-authority.js"), exports);
|
|
20
20
|
__exportStar(require("./local-authority-wire.js"), exports);
|
|
@@ -31,6 +31,8 @@ __exportStar(require("./session-promotion.js"), exports);
|
|
|
31
31
|
var replay_js_1 = require("./replay.js");
|
|
32
32
|
Object.defineProperty(exports, "verifyReplay", { enumerable: true, get: function () { return replay_js_1.verifyReplay; } });
|
|
33
33
|
__exportStar(require("./replay-scoring.js"), exports);
|
|
34
|
+
var replay_bundle_js_1 = require("./replay-bundle.js");
|
|
35
|
+
Object.defineProperty(exports, "verifyReplayScoreAgainstTrustedRuntime", { enumerable: true, get: function () { return replay_bundle_js_1.verifyReplayScoreAgainstTrustedRuntime; } });
|
|
34
36
|
__exportStar(require("./match-log.js"), exports);
|
|
35
37
|
__exportStar(require("./authority-room.js"), exports);
|
|
36
38
|
__exportStar(require("./ws-frame.js"), exports);
|
package/dist/cjs/physics-cert.js
CHANGED
|
@@ -37,6 +37,8 @@ exports.runPhysicsCert = runPhysicsCert;
|
|
|
37
37
|
exports.runRapierPhysicsCert = runRapierPhysicsCert;
|
|
38
38
|
const canonical_js_1 = require("./canonical.js");
|
|
39
39
|
const collision_js_1 = require("./collision.js");
|
|
40
|
+
const physics3d_js_1 = require("./physics3d.js");
|
|
41
|
+
const physics3d_vehicle_js_1 = require("./physics3d-vehicle.js");
|
|
40
42
|
const rapier2dCompatPackage = '@dimforge/rapier2d-compat';
|
|
41
43
|
const physicsCertResults = {
|
|
42
44
|
true: 'pass',
|
|
@@ -44,8 +46,9 @@ const physicsCertResults = {
|
|
|
44
46
|
};
|
|
45
47
|
function runPhysicsCert(options) {
|
|
46
48
|
validateOptions(options);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
+
const simulate = options.engine === 'builtin-rigid' ? runRigidSimulation : runPhysicsSimulation;
|
|
50
|
+
const uninterrupted = simulate(options, false);
|
|
51
|
+
const restored = simulate(options, true);
|
|
49
52
|
const snapshotBytes = [...restored.snapshotBytes].sort((left, right) => left - right);
|
|
50
53
|
const stepTimings = [...restored.stepTimings].sort((left, right) => left - right);
|
|
51
54
|
const snapshotTimings = [...restored.snapshotTimings].sort((left, right) => left - right);
|
|
@@ -59,11 +62,11 @@ function runPhysicsCert(options) {
|
|
|
59
62
|
snapshotHashStable;
|
|
60
63
|
return {
|
|
61
64
|
engine: options.engine,
|
|
62
|
-
engineVersion: 'builtin-fixed@1',
|
|
65
|
+
engineVersion: options.engine === 'builtin-rigid' ? 'builtin-rigid@1' : 'builtin-fixed@1',
|
|
63
66
|
adapterVersion: 'physics-cert@1',
|
|
64
67
|
shape: options.shape,
|
|
65
68
|
players: options.players,
|
|
66
|
-
bodies: createInitialBodies(options).length,
|
|
69
|
+
bodies: options.engine === 'builtin-rigid' ? countRigidArenaBodies(options) : createInitialBodies(options).length,
|
|
67
70
|
frames: options.frames,
|
|
68
71
|
snapshotEvery: options.snapshotEvery,
|
|
69
72
|
seed: options.seed,
|
|
@@ -267,6 +270,144 @@ function quantizeRapierNumber(value) {
|
|
|
267
270
|
function countRapierDynamicBodies(options) {
|
|
268
271
|
return options.players * (options.shape === 'fixed-arena' ? 2 : 4);
|
|
269
272
|
}
|
|
273
|
+
function arenaVehicleWheel(localAnchor, steered) {
|
|
274
|
+
return {
|
|
275
|
+
localAnchor,
|
|
276
|
+
suspensionDir: { x: 0, y: -1, z: 0 },
|
|
277
|
+
suspensionRestLength: 0.6,
|
|
278
|
+
suspensionMaxTravel: 0.3,
|
|
279
|
+
radius: 0.3,
|
|
280
|
+
stiffness: 60,
|
|
281
|
+
damping: 220,
|
|
282
|
+
grip: 1,
|
|
283
|
+
maxSteerAngleTurns: 0.1,
|
|
284
|
+
isSteered: steered,
|
|
285
|
+
isDriven: steered,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/** Scripted arena driving: launch, weave, then brake to a stop. */
|
|
289
|
+
function arenaVehicleInput(frame) {
|
|
290
|
+
if (frame >= 400) {
|
|
291
|
+
return { throttle: 0, steer: 0, brake: 10 };
|
|
292
|
+
}
|
|
293
|
+
return { throttle: 10, steer: Math.trunc(frame / 32) % 2 === 0 ? 10 : -10, brake: 0 };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Mixed rigid arena exercising every solver feature the determinism gates must
|
|
297
|
+
* cover: a settling stack, a hinge door with a motor, a grab-target box, and a
|
|
298
|
+
* high-speed ccd projectile per player.
|
|
299
|
+
*/
|
|
300
|
+
function buildRigidArena(options) {
|
|
301
|
+
const bodies = [{
|
|
302
|
+
id: 'arena-floor', kind: 'static', x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0,
|
|
303
|
+
shape: { type: 'box', halfX: 400, halfY: 0.5, halfZ: 400 }, layer: 1, mask: 1,
|
|
304
|
+
material: { friction: 10, restitution: 0 },
|
|
305
|
+
}];
|
|
306
|
+
const joints = [];
|
|
307
|
+
const ident = { x: 0, y: 0, z: 0, w: 1 };
|
|
308
|
+
for (let slot = 0; slot < options.players; slot += 1) {
|
|
309
|
+
const baseX = slot * 12 + (options.seed % 3);
|
|
310
|
+
// stack of 3 boxes
|
|
311
|
+
for (let layer = 0; layer < 3; layer += 1) {
|
|
312
|
+
bodies.push({
|
|
313
|
+
id: `stack-${slot}-${layer}`, kind: 'dynamic', x: baseX, y: 0.5 + 0.5 + layer, z: 0,
|
|
314
|
+
vx: 0, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 },
|
|
315
|
+
shape: { type: 'box', halfX: 0.5, halfY: 0.5, halfZ: 0.5 }, layer: 1, mask: 1,
|
|
316
|
+
material: { friction: 8, restitution: 0 },
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
// hinge door
|
|
320
|
+
bodies.push({ id: `frame-${slot}`, kind: 'static', x: baseX + 3, y: 2, z: 0, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 0.1, halfY: 0.5, halfZ: 0.5 }, layer: 1, mask: 1 });
|
|
321
|
+
bodies.push({ id: `door-${slot}`, kind: 'dynamic', x: baseX + 4, y: 2, z: 0, vx: 0, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'box', halfX: 1, halfY: 0.5, halfZ: 0.1 }, layer: 1, mask: 1 });
|
|
322
|
+
joints.push({ id: `hinge-${slot}`, type: 'hinge', a: `frame-${slot}`, b: `door-${slot}`, anchorA: { x: 0, y: 0, z: 0 }, anchorB: { x: -1, y: 0, z: 0 }, axisA: { x: 0, y: 1, z: 0 }, motorSpeed: 0.02, maxMotorTorque: 50 });
|
|
323
|
+
// grab-target box
|
|
324
|
+
bodies.push({ id: `grab-${slot}`, kind: 'dynamic', x: baseX + 6, y: 3, z: 0, vx: 0, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'box', halfX: 0.5, halfY: 0.5, halfZ: 0.5 }, layer: 1, mask: 1 });
|
|
325
|
+
joints.push({ id: `carry-${slot}`, type: 'grab', body: `grab-${slot}`, anchor: { x: 0, y: 0, z: 0 }, targetX: baseX + 6, targetY: 4, targetZ: 0, maxForce: 20 });
|
|
326
|
+
// ccd projectile toward a thin wall
|
|
327
|
+
bodies.push({ id: `wall-${slot}`, kind: 'static', x: baseX + 9, y: 2, z: 0, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 0.05, halfY: 2, halfZ: 2 }, layer: 1, mask: 1 });
|
|
328
|
+
bodies.push({ id: `bullet-${slot}`, kind: 'dynamic', x: baseX - 20, y: 2, z: 0, vx: 60, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, ccd: true, linearDamping: 1, angularDamping: 1, shape: { type: 'box', halfX: 0.2, halfY: 0.2, halfZ: 0.2 }, layer: 1, mask: 1 });
|
|
329
|
+
// soft spring: bob hung 2 below a static anchor, restLength 1 → oscillates and settles
|
|
330
|
+
bodies.push({ id: `spring-anchor-${slot}`, kind: 'static', x: baseX + 2, y: 6, z: 3, vx: 0, vy: 0, vz: 0, shape: { type: 'box', halfX: 0.1, halfY: 0.1, halfZ: 0.1 }, layer: 2, mask: 2 });
|
|
331
|
+
bodies.push({ id: `spring-bob-${slot}`, kind: 'dynamic', x: baseX + 2, y: 4, z: 3, vx: 0, vy: 0, vz: 0, mass: 1, orientation: ident, angularVel: { x: 0, y: 0, z: 0 }, shape: { type: 'box', halfX: 0.3, halfY: 0.3, halfZ: 0.3 }, layer: 2, mask: 2 });
|
|
332
|
+
joints.push({ id: `spring-${slot}`, type: 'spring', a: `spring-anchor-${slot}`, b: `spring-bob-${slot}`, anchorA: { x: 0, y: 0, z: 0 }, anchorB: { x: 0, y: 0, z: 0 }, restLength: 1, frequencyHz: 2, dampingRatio: 0.5 });
|
|
333
|
+
}
|
|
334
|
+
// raycast-wheel vehicle: dropped onto the floor, then driven by a scripted
|
|
335
|
+
// input sequence (see arenaVehicleInput), clear of the per-slot clutter
|
|
336
|
+
bodies.push({
|
|
337
|
+
id: 'arena-car', kind: 'dynamic', x: -6, y: 2.55, z: 12,
|
|
338
|
+
vx: 0, vy: 0, vz: 0, mass: 1200, orientation: ident, angularVel: { x: 0, y: 0, z: 0 },
|
|
339
|
+
shape: { type: 'box', halfX: 1, halfY: 0.25, halfZ: 2 }, layer: 1, mask: 1,
|
|
340
|
+
});
|
|
341
|
+
const vehicle = {
|
|
342
|
+
chassisId: 'arena-car',
|
|
343
|
+
driveForce: 12,
|
|
344
|
+
brakeForce: 40,
|
|
345
|
+
wheels: [
|
|
346
|
+
arenaVehicleWheel({ x: -0.9, y: -0.25, z: 1.5 }, true),
|
|
347
|
+
arenaVehicleWheel({ x: 0.9, y: -0.25, z: 1.5 }, true),
|
|
348
|
+
arenaVehicleWheel({ x: -0.9, y: -0.25, z: -1.5 }, false),
|
|
349
|
+
arenaVehicleWheel({ x: 0.9, y: -0.25, z: -1.5 }, false),
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
return { world: (0, physics3d_js_1.createDeterministicPhysicsWorld3D)(bodies), joints, vehicle };
|
|
353
|
+
}
|
|
354
|
+
function countRigidArenaBodies(options) {
|
|
355
|
+
return 2 + options.players * 10; // floor + arena car + (3 stack + frame + door + grab + wall + bullet + spring anchor + spring bob)
|
|
356
|
+
}
|
|
357
|
+
function runRigidSimulation(options, withRestore) {
|
|
358
|
+
const { world: initialWorld, joints, vehicle } = buildRigidArena(options);
|
|
359
|
+
let world = initialWorld;
|
|
360
|
+
const collisionEvents = [];
|
|
361
|
+
const stepTimings = [];
|
|
362
|
+
const snapshotTimings = [];
|
|
363
|
+
const restoreTimings = [];
|
|
364
|
+
const snapshotBytes = [];
|
|
365
|
+
const snapshotChecksums = [];
|
|
366
|
+
let queryOrderStable = true;
|
|
367
|
+
for (let frame = 0; frame < options.frames; frame += 1) {
|
|
368
|
+
const stepStartedAt = performance.now();
|
|
369
|
+
const drive = (0, physics3d_vehicle_js_1.stepDeterministicVehicle3D)(vehicle, arenaVehicleInput(frame), world, { dtTicks: 1 });
|
|
370
|
+
world = (0, physics3d_js_1.applyDeterministicImpulses3D)(world, drive.impulses.map((impulse) => ({
|
|
371
|
+
bodyId: impulse.bodyId,
|
|
372
|
+
impulse: impulse.impulse,
|
|
373
|
+
worldPoint: impulse.point,
|
|
374
|
+
})));
|
|
375
|
+
const step = (0, physics3d_js_1.stepDeterministicPhysicsWorld3D)(world, { gravityY: -0.02, joints, computeChecksum: false });
|
|
376
|
+
world = step.world;
|
|
377
|
+
stepTimings.push(performance.now() - stepStartedAt);
|
|
378
|
+
for (const contact of step.contacts) {
|
|
379
|
+
collisionEvents.push(`${frame + 1}:${contact.pair}:${contact.type}`);
|
|
380
|
+
}
|
|
381
|
+
if (withRestore && frame % options.snapshotEvery === 0) {
|
|
382
|
+
const snapshotStartedAt = performance.now();
|
|
383
|
+
// snapshot the WHOLE world including contactCache/jointCache — dropping the
|
|
384
|
+
// cache here would diverge (the negative case the spike identified).
|
|
385
|
+
const snapshot = JSON.stringify(world);
|
|
386
|
+
snapshotTimings.push(performance.now() - snapshotStartedAt);
|
|
387
|
+
snapshotBytes.push(Buffer.byteLength(snapshot));
|
|
388
|
+
snapshotChecksums.push({ first: (0, canonical_js_1.defaultChecksum)(snapshot), second: (0, canonical_js_1.defaultChecksum)(JSON.stringify(world)) });
|
|
389
|
+
const restoreStartedAt = performance.now();
|
|
390
|
+
world = JSON.parse(snapshot);
|
|
391
|
+
restoreTimings.push(performance.now() - restoreStartedAt);
|
|
392
|
+
}
|
|
393
|
+
const hits = (0, physics3d_js_1.raycastDeterministicPhysics3D)(world, { x: -1000, y: 2, z: 0, dx: 1, dy: 0, dz: 0, maxDistance: 5000, layerMask: 1 });
|
|
394
|
+
for (let index = 1; index < hits.length; index += 1) {
|
|
395
|
+
if (hits[index].distance < hits[index - 1].distance) {
|
|
396
|
+
queryOrderStable = false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
finalChecksum: (0, canonical_js_1.defaultChecksum)(world),
|
|
402
|
+
collisionEvents,
|
|
403
|
+
queryOrderStable,
|
|
404
|
+
stepTimings,
|
|
405
|
+
snapshotTimings,
|
|
406
|
+
restoreTimings,
|
|
407
|
+
snapshotBytes,
|
|
408
|
+
snapshotChecksums,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
270
411
|
function runPhysicsSimulation(options, withRestore) {
|
|
271
412
|
let state = {
|
|
272
413
|
frame: 0,
|
|
@@ -396,8 +537,8 @@ function physicsCertResult(pass) {
|
|
|
396
537
|
return physicsCertResults[String(pass)];
|
|
397
538
|
}
|
|
398
539
|
function validateOptions(options) {
|
|
399
|
-
if (options.engine !== 'builtin-fixed') {
|
|
400
|
-
throw new Error('physics certification currently supports engine=builtin-fixed');
|
|
540
|
+
if (options.engine !== 'builtin-fixed' && options.engine !== 'builtin-rigid') {
|
|
541
|
+
throw new Error('physics certification currently supports engine=builtin-fixed or engine=builtin-rigid');
|
|
401
542
|
}
|
|
402
543
|
validateCommonOptions(options);
|
|
403
544
|
}
|
package/dist/cjs/physics2d.js
CHANGED
|
@@ -272,18 +272,22 @@ function separateDynamicPair(a, b) {
|
|
|
272
272
|
const correctionA = Math.ceil(overlapX / 2);
|
|
273
273
|
const correctionB = Math.ceil(overlapX / 2);
|
|
274
274
|
return {
|
|
275
|
-
a: { ...a, x: a.x + direction * correctionA, vx: normalizeZero(Math.trunc((-a.vx * material.restitution) / 10)) },
|
|
276
|
-
b: { ...b, x: b.x - direction * correctionB, vx: normalizeZero(Math.trunc((-b.vx * material.restitution) / 10)) },
|
|
275
|
+
a: { ...a, x: a.x + direction * correctionA, vx: normalizeZero(Math.trunc((-a.vx * material.restitution) / 10)), vy: applyTangentFriction2D(a.vy, material.friction) },
|
|
276
|
+
b: { ...b, x: b.x - direction * correctionB, vx: normalizeZero(Math.trunc((-b.vx * material.restitution) / 10)), vy: applyTangentFriction2D(b.vy, material.friction) },
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
279
|
const direction = a.y <= b.y ? -1 : 1;
|
|
280
280
|
const correctionA = Math.ceil(overlapY / 2);
|
|
281
281
|
const correctionB = Math.ceil(overlapY / 2);
|
|
282
282
|
return {
|
|
283
|
-
a: { ...a, y: a.y + direction * correctionA, vy: normalizeZero(Math.trunc((-a.vy * material.restitution) / 10)) },
|
|
284
|
-
b: { ...b, y: b.y - direction * correctionB, vy: normalizeZero(Math.trunc((-b.vy * material.restitution) / 10)) },
|
|
283
|
+
a: { ...a, y: a.y + direction * correctionA, vy: normalizeZero(Math.trunc((-a.vy * material.restitution) / 10)), vx: applyTangentFriction2D(a.vx, material.friction) },
|
|
284
|
+
b: { ...b, y: b.y - direction * correctionB, vy: normalizeZero(Math.trunc((-b.vy * material.restitution) / 10)), vx: applyTangentFriction2D(b.vx, material.friction) },
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
|
+
/** Integer tangential-friction scaling, mirroring physics3d's legacy pusher semantics. */
|
|
288
|
+
function applyTangentFriction2D(velocity, friction) {
|
|
289
|
+
return normalizeZero(Math.trunc((velocity * Math.max(0, 10 - friction)) / 10));
|
|
290
|
+
}
|
|
287
291
|
function separateFromStatic(dynamicBody, staticBody) {
|
|
288
292
|
const dynamicBounds = bodyBounds(dynamicBody);
|
|
289
293
|
const staticBounds = bodyBounds(staticBody);
|
|
@@ -299,6 +303,7 @@ function separateFromStatic(dynamicBody, staticBody) {
|
|
|
299
303
|
...dynamicBody,
|
|
300
304
|
x: dynamicBody.x + direction * overlapX,
|
|
301
305
|
vx: normalizeZero(Math.trunc((-dynamicBody.vx * material.restitution) / 10)),
|
|
306
|
+
vy: applyTangentFriction2D(dynamicBody.vy, material.friction),
|
|
302
307
|
};
|
|
303
308
|
}
|
|
304
309
|
const direction = dynamicBody.y < staticBody.y ? -1 : 1;
|
|
@@ -306,6 +311,7 @@ function separateFromStatic(dynamicBody, staticBody) {
|
|
|
306
311
|
...dynamicBody,
|
|
307
312
|
y: dynamicBody.y + direction * overlapY,
|
|
308
313
|
vy: normalizeZero(Math.trunc((-dynamicBody.vy * material.restitution) / 10)),
|
|
314
|
+
vx: applyTangentFriction2D(dynamicBody.vx, material.friction),
|
|
309
315
|
};
|
|
310
316
|
}
|
|
311
317
|
function detectContacts(bodies) {
|