@series-inc/rundot-syncplay 5.23.0-beta.7 → 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.
Files changed (75) hide show
  1. package/README.md +116 -15
  2. package/dist/browser.d.ts +87 -55
  3. package/dist/browser.js +47 -52
  4. package/dist/certification.d.ts +32 -3
  5. package/dist/certification.js +88 -3
  6. package/dist/cjs/browser.js +259 -87
  7. package/dist/cjs/certification.js +87 -2
  8. package/dist/cjs/collision.js +58 -0
  9. package/dist/cjs/creator.js +2 -1
  10. package/dist/cjs/engine-complete.js +213 -3
  11. package/dist/cjs/engine-parity-matrix.js +19 -4
  12. package/dist/cjs/index.js +199 -132
  13. package/dist/cjs/math.js +342 -15
  14. package/dist/cjs/movement3d.js +230 -1
  15. package/dist/cjs/node.js +5 -1
  16. package/dist/cjs/noise.js +58 -0
  17. package/dist/cjs/physics-cert.js +147 -6
  18. package/dist/cjs/physics2d.js +10 -4
  19. package/dist/cjs/physics3d-joints.js +637 -0
  20. package/dist/cjs/physics3d-shared.js +288 -0
  21. package/dist/cjs/physics3d-solver.js +1351 -0
  22. package/dist/cjs/physics3d-vehicle.js +467 -0
  23. package/dist/cjs/physics3d.js +1057 -223
  24. package/dist/cjs/random.js +49 -0
  25. package/dist/cjs/replay-bundle.js +127 -20
  26. package/dist/cjs/sample-scenes.js +3 -0
  27. package/dist/cjs/sdk-offline.js +6 -32
  28. package/dist/cjs/sdk-session.js +156 -0
  29. package/dist/cjs/session-build.js +42 -0
  30. package/dist/cjs/testing.js +107 -0
  31. package/dist/cli.js +40 -12
  32. package/dist/collision.d.ts +12 -0
  33. package/dist/collision.js +52 -0
  34. package/dist/creator.d.ts +1 -1
  35. package/dist/creator.js +1 -1
  36. package/dist/engine-complete.js +214 -4
  37. package/dist/engine-parity-matrix.js +19 -4
  38. package/dist/index.d.ts +94 -89
  39. package/dist/index.js +45 -56
  40. package/dist/math.d.ts +59 -6
  41. package/dist/math.js +342 -15
  42. package/dist/movement3d.d.ts +73 -0
  43. package/dist/movement3d.js +229 -1
  44. package/dist/node.d.ts +4 -0
  45. package/dist/node.js +2 -0
  46. package/dist/noise.d.ts +7 -0
  47. package/dist/noise.js +51 -0
  48. package/dist/physics-cert.d.ts +1 -1
  49. package/dist/physics-cert.js +147 -6
  50. package/dist/physics2d.js +10 -4
  51. package/dist/physics3d-joints.d.ts +94 -0
  52. package/dist/physics3d-joints.js +634 -0
  53. package/dist/physics3d-shared.d.ts +72 -0
  54. package/dist/physics3d-shared.js +257 -0
  55. package/dist/physics3d-solver.d.ts +197 -0
  56. package/dist/physics3d-solver.js +1346 -0
  57. package/dist/physics3d-vehicle.d.ts +84 -0
  58. package/dist/physics3d-vehicle.js +463 -0
  59. package/dist/physics3d.d.ts +108 -8
  60. package/dist/physics3d.js +1006 -177
  61. package/dist/random.d.ts +7 -0
  62. package/dist/random.js +49 -0
  63. package/dist/replay-bundle.d.ts +40 -1
  64. package/dist/replay-bundle.js +126 -20
  65. package/dist/sample-scenes.js +3 -0
  66. package/dist/sdk-offline.js +6 -32
  67. package/dist/sdk-session.d.ts +47 -0
  68. package/dist/sdk-session.js +153 -0
  69. package/dist/session-build.d.ts +21 -0
  70. package/dist/session-build.js +39 -0
  71. package/dist/testing.d.ts +52 -0
  72. package/dist/testing.js +45 -0
  73. package/dist/tools/vite-plugin.d.ts +8 -0
  74. package/dist/tools/vite-plugin.js +63 -9
  75. package/package.json +11 -3
package/dist/cjs/math.js CHANGED
@@ -7,9 +7,39 @@ function createDeterministicMath(fixedScale = 1_000, minFixed = DEFAULT_MIN_FIXE
7
7
  if (!Number.isInteger(fixedScale) || fixedScale <= 0) {
8
8
  throw new Error('fixedScale must be a positive integer');
9
9
  }
10
+ if (fixedScale % 4 !== 0) {
11
+ // The trig/angle code uses exact quarter- and half-turns (fixedScale/4,
12
+ // fixedScale/2) as integer angle values.
13
+ throw new Error('fixedScale must be divisible by 4');
14
+ }
10
15
  if (!Number.isInteger(minFixed) || !Number.isInteger(maxFixed) || minFixed >= maxFixed) {
11
16
  throw new Error('fixed range must be integer min < max');
12
17
  }
18
+ const mulFixed = (a, b) => Math.trunc((a * b) / fixedScale);
19
+ // Odd 5th-order minimax sine over a quarter turn (racing-ugc coefficients,
20
+ // generalized off its hardcoded ONE=1000 to fixedScale).
21
+ function sinQuarter(xTurns) {
22
+ const q = fixedScale / 4;
23
+ const u = Math.trunc((xTurns * fixedScale) / q); // fixed in [0, fixedScale]
24
+ const u2 = mulFixed(u, u);
25
+ const u3 = mulFixed(u2, u);
26
+ const u5 = mulFixed(u3, u2);
27
+ return (mulFixed(Math.round(1.5707963 * fixedScale), u) +
28
+ mulFixed(Math.round(-0.6459641 * fixedScale), u3) +
29
+ mulFixed(Math.round(0.0796926 * fixedScale), u5));
30
+ }
31
+ function sinTurnsImpl(turns) {
32
+ const q = fixedScale / 4;
33
+ const t = ((turns % fixedScale) + fixedScale) % fixedScale;
34
+ const c = (v) => (v < -fixedScale ? -fixedScale : v > fixedScale ? fixedScale : v);
35
+ if (t < q)
36
+ return normalizeZero(c(sinQuarter(t)));
37
+ if (t < 2 * q)
38
+ return normalizeZero(c(sinQuarter(2 * q - t)));
39
+ if (t < 3 * q)
40
+ return normalizeZero(c(-sinQuarter(t - 2 * q)));
41
+ return normalizeZero(c(-sinQuarter(fixedScale - t)));
42
+ }
13
43
  return {
14
44
  fixedScale,
15
45
  minFixed,
@@ -83,13 +113,207 @@ function createDeterministicMath(fixedScale = 1_000, minFixed = DEFAULT_MIN_FIXE
83
113
  }
84
114
  return Math.trunc(value / engine) * engine;
85
115
  },
116
+ sqrt(value) {
117
+ assertFixed(value);
118
+ if (value < 0) {
119
+ throw new Error(`DeterministicMath.sqrt: negative ${value}`);
120
+ }
121
+ if (value === 0) {
122
+ return 0;
123
+ }
124
+ const n = value * fixedScale; // ≤ ~2.1e12 < 2^53 for in-range inputs
125
+ let x = n;
126
+ let y = Math.trunc((x + 1) / 2);
127
+ while (y < x) {
128
+ x = y;
129
+ y = Math.trunc((x + Math.trunc(n / x)) / 2);
130
+ }
131
+ return saturate(x, minFixed, maxFixed);
132
+ },
133
+ abs(value) {
134
+ assertFixed(value);
135
+ return saturate(value < 0 ? -value : value, minFixed, maxFixed);
136
+ },
137
+ sign(value) {
138
+ assertFixed(value);
139
+ return value > 0 ? fixedScale : value < 0 ? -fixedScale : 0;
140
+ },
141
+ min(a, b) {
142
+ assertFixed(a);
143
+ assertFixed(b);
144
+ return a < b ? a : b;
145
+ },
146
+ max(a, b) {
147
+ assertFixed(a);
148
+ assertFixed(b);
149
+ return a > b ? a : b;
150
+ },
151
+ lerp(a, b, t) {
152
+ assertFixed(a);
153
+ assertFixed(b);
154
+ assertFixed(t);
155
+ return saturate(a + Math.trunc(((b - a) * t) / fixedScale), minFixed, maxFixed);
156
+ },
157
+ mod(a, b) {
158
+ assertFixed(a);
159
+ assertFixed(b);
160
+ if (b === 0) {
161
+ throw new Error('DeterministicMath.mod: modulus 0');
162
+ }
163
+ const r = a % b;
164
+ return r !== 0 && r < 0 !== b < 0 ? r + b : r;
165
+ },
166
+ inverseLerp(a, b, v) {
167
+ assertFixed(a);
168
+ assertFixed(b);
169
+ assertFixed(v);
170
+ if (a === b) {
171
+ return 0;
172
+ }
173
+ return saturate(Math.trunc(((v - a) * fixedScale) / (b - a)), minFixed, maxFixed);
174
+ },
175
+ remap(inMin, inMax, outMin, outMax, v) {
176
+ return this.lerp(outMin, outMax, this.inverseLerp(inMin, inMax, v));
177
+ },
178
+ smoothstep(edge0, edge1, x) {
179
+ const t = this.clamp(this.inverseLerp(edge0, edge1, x), 0, fixedScale);
180
+ const t2 = Math.trunc((t * t) / fixedScale);
181
+ const t3 = Math.trunc((t2 * t) / fixedScale);
182
+ return saturate(3 * t2 - 2 * t3, minFixed, maxFixed);
183
+ },
184
+ log2Fixed(value) {
185
+ assertFixed(value);
186
+ if (value <= 0) {
187
+ throw new Error(`DeterministicMath.log2Fixed: non-positive ${value}`);
188
+ }
189
+ // value = mant * 2^e, mant in [1,2). e via integer scan on value/scale.
190
+ let e = 0;
191
+ let v = value;
192
+ const one = fixedScale;
193
+ while (v >= 2 * one) {
194
+ v = Math.trunc(v / 2);
195
+ e += 1;
196
+ }
197
+ while (v < one) {
198
+ v = v * 2;
199
+ e -= 1;
200
+ }
201
+ const f = v - one; // fixed fractional in [0, one)
202
+ // Cubic through the origin and the octave boundary (P(1)=1) fitting
203
+ // log2(1+f) over [0,1): a·f − b·f² + c·f³. Interpolated so mant→2 maps
204
+ // exactly to the next octave; ≤~2e-3 poly error before truncation.
205
+ const c1 = Math.round(1.4273 * fixedScale);
206
+ const c2 = Math.round(0.6018 * fixedScale);
207
+ const c3 = Math.round(0.1745 * fixedScale);
208
+ const poly = mulFixed(f, c1 - mulFixed(f, c2 - mulFixed(f, c3)));
209
+ return e * fixedScale + poly;
210
+ },
211
+ log(value) {
212
+ assertFixed(value);
213
+ if (value <= 0) {
214
+ throw new Error(`DeterministicMath.log: non-positive ${value}`);
215
+ }
216
+ const LN2 = Math.round(0.6931472 * fixedScale);
217
+ return saturate(Math.trunc((this.log2Fixed(value) * LN2) / fixedScale), minFixed, maxFixed);
218
+ },
219
+ exp(value) {
220
+ assertFixed(value);
221
+ // exp(x) = 2^(x/ln2). y = x/ln2 (fixed). yi integer part, yf in [0,1).
222
+ const INV_LN2 = Math.round(1.4426950 * fixedScale);
223
+ const y = Math.trunc((value * INV_LN2) / fixedScale);
224
+ let yi = Math.trunc(y / fixedScale);
225
+ let yf = y - yi * fixedScale;
226
+ if (yf < 0) {
227
+ yf += fixedScale;
228
+ yi -= 1;
229
+ }
230
+ const b1 = Math.round(0.6931472 * fixedScale);
231
+ const b2 = Math.round(0.2401596 * fixedScale);
232
+ const b3 = Math.round(0.0521416 * fixedScale);
233
+ let result = fixedScale + mulFixed(yf, b1 + mulFixed(yf, b2 + mulFixed(yf, b3))); // 2^yf fixed
234
+ if (yi >= 0) {
235
+ for (let i = 0; i < yi; i += 1) {
236
+ result = saturate(result * 2, minFixed, maxFixed);
237
+ }
238
+ }
239
+ else {
240
+ for (let i = 0; i < -yi; i += 1) {
241
+ result = Math.trunc(result / 2);
242
+ }
243
+ }
244
+ return saturate(result, minFixed, maxFixed);
245
+ },
86
246
  sinTurns(turns) {
87
247
  assertFixed(turns);
88
- return trigLookup(turns, fixedScale, 'sin');
248
+ return sinTurnsImpl(turns);
89
249
  },
90
250
  cosTurns(turns) {
91
251
  assertFixed(turns);
92
- return trigLookup(turns, fixedScale, 'cos');
252
+ return sinTurnsImpl(turns + fixedScale / 4);
253
+ },
254
+ tanTurns(turns) {
255
+ assertFixed(turns);
256
+ return this.div(this.sinTurns(turns), this.cosTurns(turns));
257
+ },
258
+ atan2(y, x) {
259
+ assertFixed(y);
260
+ assertFixed(x);
261
+ if (x === 0 && y === 0) {
262
+ return 0;
263
+ }
264
+ const ax = x < 0 ? -x : x;
265
+ const ay = y < 0 ? -y : y;
266
+ const swap = ay > ax;
267
+ const z = this.div(swap ? ax : ay, swap ? ay : ax);
268
+ const K1 = this.toFixed(Math.PI / 4);
269
+ const K2 = this.toFixed(0.2447);
270
+ const K3 = this.toFixed(0.0663);
271
+ const atanRad = this.add(this.mul(K1, z), this.mul(this.mul(z, this.sub(fixedScale, z)), this.add(K2, this.mul(K3, z))));
272
+ let turn = this.mul(atanRad, this.toFixed(1 / (2 * Math.PI)));
273
+ if (swap) {
274
+ turn = this.sub(fixedScale / 4, turn);
275
+ }
276
+ let full;
277
+ if (x >= 0 && y >= 0) {
278
+ full = turn;
279
+ }
280
+ else if (x < 0 && y >= 0) {
281
+ full = this.sub(fixedScale / 2, turn);
282
+ }
283
+ else if (x < 0 && y < 0) {
284
+ full = this.add(fixedScale / 2, turn);
285
+ }
286
+ else {
287
+ full = this.sub(fixedScale, turn);
288
+ }
289
+ return ((full % fixedScale) + fixedScale) % fixedScale;
290
+ },
291
+ wrapTurns(t) {
292
+ assertFixed(t);
293
+ return ((t % fixedScale) + fixedScale) % fixedScale;
294
+ },
295
+ angleDeltaTurns(a, b) {
296
+ assertFixed(a);
297
+ assertFixed(b);
298
+ const half = fixedScale / 2;
299
+ let d = (((b - a) % fixedScale) + fixedScale) % fixedScale;
300
+ if (d >= half) {
301
+ d -= fixedScale;
302
+ }
303
+ return d;
304
+ },
305
+ turnsToRadians(t) {
306
+ assertFixed(t);
307
+ return saturate(Math.trunc((t * this.toFixed(2 * Math.PI)) / fixedScale), minFixed, maxFixed);
308
+ },
309
+ radiansToTurns(r) {
310
+ assertFixed(r);
311
+ return saturate(Math.trunc((r * fixedScale) / this.toFixed(2 * Math.PI)), minFixed, maxFixed);
312
+ },
313
+ hypot(x, y) {
314
+ assertFixed(x);
315
+ assertFixed(y);
316
+ return this.sqrt(saturate(this.mul(x, x) + this.mul(y, y), minFixed, maxFixed));
93
317
  },
94
318
  vec2(x, y) {
95
319
  assertFixed(x);
@@ -102,6 +326,66 @@ function createDeterministicMath(fixedScale = 1_000, minFixed = DEFAULT_MIN_FIXE
102
326
  y: saturate(a.y + b.y, minFixed, maxFixed),
103
327
  };
104
328
  },
329
+ vec2Sub(a, b) {
330
+ return { x: this.saturatingSub(a.x, b.x), y: this.saturatingSub(a.y, b.y) };
331
+ },
332
+ vec2Scale(v, s) {
333
+ assertFixed(v.x);
334
+ assertFixed(v.y);
335
+ assertFixed(s);
336
+ return { x: this.mul(v.x, s), y: this.mul(v.y, s) };
337
+ },
338
+ vec2Dot(a, b) {
339
+ return saturate(this.mul(a.x, b.x) + this.mul(a.y, b.y), minFixed, maxFixed);
340
+ },
341
+ vec2Cross(a, b) {
342
+ return saturate(this.mul(a.x, b.y) - this.mul(a.y, b.x), minFixed, maxFixed);
343
+ },
344
+ vec2LengthSq(v) {
345
+ return saturate(this.mul(v.x, v.x) + this.mul(v.y, v.y), minFixed, maxFixed);
346
+ },
347
+ vec2Length(v) {
348
+ return this.hypot(v.x, v.y);
349
+ },
350
+ vec2DistanceSq(a, b) {
351
+ return this.vec2LengthSq(this.vec2Sub(a, b));
352
+ },
353
+ vec2Distance(a, b) {
354
+ return this.hypot(this.saturatingSub(a.x, b.x), this.saturatingSub(a.y, b.y));
355
+ },
356
+ vec2Normalize(v) {
357
+ const len = this.hypot(v.x, v.y);
358
+ return len === 0 ? { x: 0, y: 0 } : { x: this.div(v.x, len), y: this.div(v.y, len) };
359
+ },
360
+ vec2Perp(v) {
361
+ assertFixed(v.x);
362
+ assertFixed(v.y);
363
+ return { x: this.saturatingSub(0, v.y), y: v.x };
364
+ },
365
+ vec2Negate(v) {
366
+ assertFixed(v.x);
367
+ assertFixed(v.y);
368
+ return { x: this.saturatingSub(0, v.x), y: this.saturatingSub(0, v.y) };
369
+ },
370
+ vec2Rotate(v, turns) {
371
+ assertFixed(v.x);
372
+ assertFixed(v.y);
373
+ assertFixed(turns);
374
+ const c = this.cosTurns(turns);
375
+ const s = this.sinTurns(turns);
376
+ return {
377
+ x: this.saturatingSub(this.mul(v.x, c), this.mul(v.y, s)),
378
+ y: this.saturatingAdd(this.mul(v.x, s), this.mul(v.y, c)),
379
+ };
380
+ },
381
+ vec2FromAngleTurns(turns, length) {
382
+ assertFixed(turns);
383
+ assertFixed(length);
384
+ return { x: this.mul(this.cosTurns(turns), length), y: this.mul(this.sinTurns(turns), length) };
385
+ },
386
+ vec2AngleTurns(v) {
387
+ return this.atan2(v.y, v.x);
388
+ },
105
389
  vec2Lerp(a, b, t) {
106
390
  assertFixed(t);
107
391
  return {
@@ -109,14 +393,68 @@ function createDeterministicMath(fixedScale = 1_000, minFixed = DEFAULT_MIN_FIXE
109
393
  y: normalizeZero(a.y + Math.trunc(((b.y - a.y) * t) / fixedScale)),
110
394
  };
111
395
  },
396
+ vec3(x, y, z) {
397
+ assertFixed(x);
398
+ assertFixed(y);
399
+ assertFixed(z);
400
+ return { x, y, z };
401
+ },
402
+ vec3Add(a, b) {
403
+ return {
404
+ x: saturate(a.x + b.x, minFixed, maxFixed),
405
+ y: saturate(a.y + b.y, minFixed, maxFixed),
406
+ z: saturate(a.z + b.z, minFixed, maxFixed),
407
+ };
408
+ },
409
+ vec3Sub(a, b) {
410
+ return {
411
+ x: this.saturatingSub(a.x, b.x),
412
+ y: this.saturatingSub(a.y, b.y),
413
+ z: this.saturatingSub(a.z, b.z),
414
+ };
415
+ },
416
+ vec3Scale(v, s) {
417
+ assertFixed(s);
418
+ return { x: this.mul(v.x, s), y: this.mul(v.y, s), z: this.mul(v.z, s) };
419
+ },
420
+ vec3Dot(a, b) {
421
+ return saturate(this.mul(a.x, b.x) + this.mul(a.y, b.y) + this.mul(a.z, b.z), minFixed, maxFixed);
422
+ },
423
+ vec3Cross(a, b) {
424
+ return {
425
+ x: this.sub(this.mul(a.y, b.z), this.mul(a.z, b.y)),
426
+ y: this.sub(this.mul(a.z, b.x), this.mul(a.x, b.z)),
427
+ z: this.sub(this.mul(a.x, b.y), this.mul(a.y, b.x)),
428
+ };
429
+ },
430
+ vec3LengthSq(v) {
431
+ return saturate(this.mul(v.x, v.x) + this.mul(v.y, v.y) + this.mul(v.z, v.z), minFixed, maxFixed);
432
+ },
433
+ vec3Length(v) {
434
+ return this.sqrt(this.vec3LengthSq(v));
435
+ },
436
+ vec3Distance(a, b) {
437
+ return this.vec3Length(this.vec3Sub(a, b));
438
+ },
439
+ vec3Normalize(v) {
440
+ const len = this.vec3Length(v);
441
+ return len === 0 ? { x: 0, y: 0, z: 0 } : { x: this.div(v.x, len), y: this.div(v.y, len), z: this.div(v.z, len) };
442
+ },
443
+ vec3Lerp(a, b, t) {
444
+ assertFixed(t);
445
+ return { x: this.lerp(a.x, b.x, t), y: this.lerp(a.y, b.y, t), z: this.lerp(a.z, b.z, t) };
446
+ },
447
+ vec3Negate(v) {
448
+ return { x: this.saturatingSub(0, v.x), y: this.saturatingSub(0, v.y), z: this.saturatingSub(0, v.z) };
449
+ },
112
450
  quatFromYawTurns(turns) {
113
451
  assertFixed(turns);
114
452
  const halfTurns = Math.trunc(turns / 2);
115
453
  return {
116
454
  x: 0,
117
455
  y: 0,
118
- z: normalizeZero(trigLookup(halfTurns, fixedScale, 'sin')),
119
- w: normalizeZero(trigLookup(halfTurns, fixedScale, 'cos')),
456
+ z: normalizeZero(sinTurnsImpl(halfTurns)),
457
+ w: normalizeZero(sinTurnsImpl(halfTurns + fixedScale / 4)),
120
458
  };
121
459
  },
122
460
  quatLerp(a, b, t) {
@@ -141,17 +479,6 @@ function assertFixed(value) {
141
479
  function saturate(value, min, max) {
142
480
  return Math.min(max, Math.max(min, value));
143
481
  }
144
- function trigLookup(turns, fixedScale, op) {
145
- const normalized = ((turns % fixedScale) + fixedScale) % fixedScale;
146
- const quadrant = Math.trunc((normalized * 4) / fixedScale) % 4;
147
- const offset = normalized - Math.trunc((quadrant * fixedScale) / 4);
148
- const ramp = Math.trunc((offset * fixedScale * 4) / fixedScale);
149
- const sin = quadrant === 0 ? ramp : quadrant === 1 ? fixedScale - ramp : quadrant === 2 ? -ramp : -fixedScale + ramp;
150
- if (op === 'sin') {
151
- return normalizeZero(sin);
152
- }
153
- return trigLookup(normalized + Math.trunc(fixedScale / 4), fixedScale, 'sin');
154
- }
155
482
  function normalizeZero(value) {
156
483
  return Object.is(value, -0) ? 0 : value;
157
484
  }
@@ -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
- checksum: (0, canonical_js_1.defaultChecksum)({ body: finalBody, events, correctionCount, physicsQueryCount, collisionFilterCalls, collisionFilterRejectedHits }),
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();