@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.
- package/README.md +116 -15
- package/dist/browser.d.ts +87 -55
- package/dist/browser.js +47 -52
- package/dist/certification.d.ts +32 -3
- package/dist/certification.js +88 -3
- package/dist/cjs/browser.js +259 -87
- package/dist/cjs/certification.js +87 -2
- package/dist/cjs/collision.js +58 -0
- 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 +199 -132
- package/dist/cjs/math.js +342 -15
- package/dist/cjs/movement3d.js +230 -1
- package/dist/cjs/node.js +5 -1
- package/dist/cjs/noise.js +58 -0
- 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/random.js +49 -0
- package/dist/cjs/replay-bundle.js +127 -20
- package/dist/cjs/sample-scenes.js +3 -0
- package/dist/cjs/sdk-offline.js +6 -32
- package/dist/cjs/sdk-session.js +156 -0
- package/dist/cjs/session-build.js +42 -0
- package/dist/cjs/testing.js +107 -0
- package/dist/cli.js +40 -12
- package/dist/collision.d.ts +12 -0
- package/dist/collision.js +52 -0
- 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 +94 -89
- package/dist/index.js +45 -56
- package/dist/math.d.ts +59 -6
- package/dist/math.js +342 -15
- package/dist/movement3d.d.ts +73 -0
- package/dist/movement3d.js +229 -1
- package/dist/node.d.ts +4 -0
- package/dist/node.js +2 -0
- package/dist/noise.d.ts +7 -0
- package/dist/noise.js +51 -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/random.d.ts +7 -0
- package/dist/random.js +49 -0
- package/dist/replay-bundle.d.ts +40 -1
- package/dist/replay-bundle.js +126 -20
- package/dist/sample-scenes.js +3 -0
- package/dist/sdk-offline.js +6 -32
- package/dist/sdk-session.d.ts +47 -0
- package/dist/sdk-session.js +153 -0
- package/dist/session-build.d.ts +21 -0
- package/dist/session-build.js +39 -0
- package/dist/testing.d.ts +52 -0
- package/dist/testing.js +45 -0
- package/dist/tools/vite-plugin.d.ts +8 -0
- package/dist/tools/vite-plugin.js +63 -9
- 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
|
|
248
|
+
return sinTurnsImpl(turns);
|
|
89
249
|
},
|
|
90
250
|
cosTurns(turns) {
|
|
91
251
|
assertFixed(turns);
|
|
92
|
-
return
|
|
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(
|
|
119
|
-
w: normalizeZero(
|
|
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
|
}
|
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();
|