@aethervtt/shard-physics 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Dias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @aethervtt/shard-physics
2
+
3
+ Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
@@ -0,0 +1,62 @@
1
+ import { type PhysicsWorld } from './world.js';
2
+ type Vec = {
3
+ x: number;
4
+ y: number;
5
+ z: number;
6
+ };
7
+ type Rot = {
8
+ x: number;
9
+ y: number;
10
+ z: number;
11
+ w: number;
12
+ };
13
+ /** Columns of one table, looked up once per table. */
14
+ interface Columns {
15
+ radius: Float32Array;
16
+ up: Uint8Array;
17
+ fixedUp: Float32Array;
18
+ gravity: Float32Array;
19
+ jumpSpeed: Float32Array;
20
+ airControl: Float32Array;
21
+ pushForce: Float32Array;
22
+ layers: Uint16Array;
23
+ mask: Uint16Array;
24
+ align: Uint8Array;
25
+ move: Float32Array;
26
+ jump: Uint8Array;
27
+ grounded: Uint8Array;
28
+ groundNormal: Float32Array;
29
+ groundEntity: Float64Array;
30
+ velocity: Float32Array;
31
+ stateUp: Float32Array;
32
+ airTime: Float32Array;
33
+ translation: Float32Array;
34
+ rotation: Float32Array;
35
+ }
36
+ /**
37
+ * Moves every character by its intent (spec 0029): up from gravity, velocity from intent, gravity,
38
+ * and jumps, Rapier's controller for the move, then state, ground events, pushes, and platforms.
39
+ */
40
+ export declare const characterSystem: import("@aethervtt/shard-core").SystemDef<{
41
+ controllers: import("@aethervtt/shard-core").Query;
42
+ query: import("@aethervtt/shard-core").Query;
43
+ sources: import("@aethervtt/shard-core").Query;
44
+ gravity: import("./gravity.js").GravitySources;
45
+ cols: Columns;
46
+ pose: import("./pose.js").Pose;
47
+ g: Float64Array<ArrayBuffer>;
48
+ tmp: Float64Array<ArrayBuffer>;
49
+ q: Float64Array<ArrayBuffer>;
50
+ arc: Float64Array<ArrayBuffer>;
51
+ pos: Float64Array<ArrayBuffer>;
52
+ carry: Float64Array<ArrayBuffer>;
53
+ ray: InstanceType<PhysicsWorld['R']['Ray']> | undefined;
54
+ hit: InstanceType<PhysicsWorld['R']['CharacterCollision']> | undefined;
55
+ t1: Vec;
56
+ t2: Vec;
57
+ r1: Rot;
58
+ r2: Rot;
59
+ moved: Vec;
60
+ rootIdentity: boolean;
61
+ }>;
62
+ export {};
@@ -0,0 +1,509 @@
1
+ import { defineSystem } from "@aethervtt/shard-core";
2
+ import { FixedTime } from "@aethervtt/shard-runtime";
3
+ import { GlobalTransform, GridFramesResource, Transform } from "@aethervtt/shard-transform";
4
+ import {
5
+ CharacterController,
6
+ CharacterGroundEvent,
7
+ CharacterIntent,
8
+ CharacterState,
9
+ GravitySource
10
+ } from "./components.js";
11
+ import { createGravitySources, gatherGravitySources, sampleGravity } from "./gravity.js";
12
+ import { createPose, parentOf, poseOf, rotate, worldToLocal } from "./pose.js";
13
+ import { CHARACTER_OFFSET, Physics } from "./world.js";
14
+ const KIND_KINEMATIC_POSITION = 2;
15
+ const KIND_KINEMATIC_VELOCITY = 3;
16
+ function columns(table, out) {
17
+ out.radius = table.column(CharacterController, "radius");
18
+ out.up = table.column(CharacterController, "up");
19
+ out.fixedUp = table.column(CharacterController, "fixedUp");
20
+ out.gravity = table.column(CharacterController, "gravity");
21
+ out.jumpSpeed = table.column(CharacterController, "jumpSpeed");
22
+ out.airControl = table.column(CharacterController, "airControl");
23
+ out.pushForce = table.column(CharacterController, "pushForce");
24
+ out.layers = table.column(CharacterController, "layers");
25
+ out.mask = table.column(CharacterController, "mask");
26
+ out.align = table.column(CharacterController, "alignRotation");
27
+ out.move = table.column(CharacterIntent, "move");
28
+ out.jump = table.column(CharacterIntent, "jump");
29
+ out.grounded = table.column(CharacterState, "grounded");
30
+ out.groundNormal = table.column(CharacterState, "groundNormal");
31
+ out.groundEntity = table.column(CharacterState, "groundEntity");
32
+ out.velocity = table.column(CharacterState, "velocity");
33
+ out.stateUp = table.column(CharacterState, "up");
34
+ out.airTime = table.column(CharacterState, "airTime");
35
+ out.translation = table.column(Transform, "translation");
36
+ out.rotation = table.column(Transform, "rotation");
37
+ }
38
+ const vec = () => ({ x: 0, y: 0, z: 0 });
39
+ const rot = () => ({ x: 0, y: 0, z: 0, w: 1 });
40
+ const characterSystem = defineSystem({
41
+ name: "physics/character",
42
+ description: "Moves CharacterController entities by their CharacterIntent: up and rotation from gravity, walking, jumping, stepping, sliding, pushing bodies, riding platforms. Writes CharacterState.",
43
+ setup: (world) => ({
44
+ controllers: world.query({ with: [CharacterController] }),
45
+ query: world.query({ with: [CharacterController, CharacterIntent, CharacterState, Transform] }),
46
+ sources: world.query({ with: [GravitySource, GlobalTransform] }),
47
+ gravity: createGravitySources(),
48
+ cols: {},
49
+ pose: createPose(),
50
+ g: new Float64Array(4),
51
+ tmp: new Float64Array(3),
52
+ q: new Float64Array(4),
53
+ arc: new Float64Array(4),
54
+ pos: new Float64Array(3),
55
+ carry: new Float64Array(3),
56
+ ray: void 0,
57
+ hit: void 0,
58
+ t1: vec(),
59
+ t2: vec(),
60
+ r1: rot(),
61
+ r2: rot(),
62
+ moved: vec(),
63
+ rootIdentity: true
64
+ }),
65
+ run: (s, world, ctx) => {
66
+ const p = world.tryResource(Physics);
67
+ if (!p) return;
68
+ const since = ctx.lastRunTick;
69
+ for (let t = 0; t < s.controllers.tables.length; t++) {
70
+ const table = s.controllers.tables[t];
71
+ if (table.lastChanged(CharacterController) <= since) continue;
72
+ const ticks = table.changedTicks(CharacterController);
73
+ for (let row = 0; row < table.count; row++) {
74
+ if (ticks[row] > since) p.createCharacter(world, table.entities[row]);
75
+ }
76
+ }
77
+ if (p.characters.size === 0 || p.config.paused) return;
78
+ const dt = world.resource(FixedTime).step;
79
+ p.raw.timestep = dt;
80
+ gatherGravitySources(s.sources, s.gravity);
81
+ s.hit ??= new p.R.CharacterCollision();
82
+ s.rootIdentity = world.tryResource(GridFramesResource)?.rootIdentity !== false;
83
+ for (let t = 0; t < s.query.tables.length; t++) {
84
+ const table = s.query.tables[t];
85
+ columns(table, s.cols);
86
+ for (let row = 0; row < table.count; row++) {
87
+ const record = p.characters.get(table.entities[row]);
88
+ if (record && !record.body.parked) stepCharacter(s, world, p, table, row, record, dt);
89
+ }
90
+ table.markChanged(CharacterState);
91
+ table.markChanged(Transform);
92
+ }
93
+ }
94
+ });
95
+ function stepCharacter(s, world, p, table, row, record, dt) {
96
+ const c = s.cols;
97
+ const dim = p.dim;
98
+ const entity = record.entity;
99
+ const body = record.body.body;
100
+ const o3 = row * 3;
101
+ const pose = poseOf(world, entity, s.pose);
102
+ const bt = body.translation(s.t1);
103
+ const ex = bt.x - pose[0];
104
+ const ey = bt.y - pose[1];
105
+ const ez = dim === 3 ? bt.z - pose[2] : 0;
106
+ const tol = 1e-4 + 1e-6 * (Math.abs(pose[0]) + Math.abs(pose[1]) + Math.abs(pose[2]));
107
+ if (ex * ex + ey * ey + ez * ez > tol * tol) {
108
+ body.setTranslation(p.vec(pose[0], pose[1], pose[2]), true);
109
+ body.setRotation(p.rotation(pose, 3), true);
110
+ p.raw.propagateModifiedBodyPositionsToColliders();
111
+ }
112
+ const px = pose[0];
113
+ const py = pose[1];
114
+ const pz = pose[2];
115
+ let ux;
116
+ let uy;
117
+ let uz;
118
+ let ax = 0;
119
+ let ay = 0;
120
+ let az = 0;
121
+ if (c.up[row] === 0) {
122
+ ux = c.fixedUp[o3];
123
+ uy = c.fixedUp[o3 + 1];
124
+ uz = dim === 3 ? c.fixedUp[o3 + 2] : 0;
125
+ const len = Math.sqrt(ux * ux + uy * uy + uz * uz);
126
+ if (len < 1e-9) {
127
+ ux = 0;
128
+ uy = 1;
129
+ uz = 0;
130
+ } else {
131
+ ux /= len;
132
+ uy /= len;
133
+ uz /= len;
134
+ }
135
+ const g = c.gravity[row];
136
+ ax = -ux * g;
137
+ ay = -uy * g;
138
+ az = -uz * g;
139
+ } else {
140
+ const g = s.g;
141
+ sampleGravity(s.gravity, dim, px, py, pz, true, g);
142
+ if (g[3] < 1e-9) {
143
+ const cg = p.config.gravity;
144
+ g[0] = cg[0];
145
+ g[1] = cg[1];
146
+ g[2] = dim === 3 ? cg[2] : 0;
147
+ g[3] = Math.sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]);
148
+ }
149
+ if (g[3] > 1e-9) {
150
+ ax = g[0];
151
+ ay = g[1];
152
+ az = g[2];
153
+ ux = -ax / g[3];
154
+ uy = -ay / g[3];
155
+ uz = -az / g[3];
156
+ } else {
157
+ ux = c.stateUp[o3];
158
+ uy = c.stateUp[o3 + 1];
159
+ uz = dim === 3 ? c.stateUp[o3 + 2] : 0;
160
+ const len = Math.sqrt(ux * ux + uy * uy + uz * uz) || 1;
161
+ ux /= len;
162
+ uy /= len;
163
+ uz /= len;
164
+ }
165
+ }
166
+ const q = s.q;
167
+ q[0] = pose[3];
168
+ q[1] = pose[4];
169
+ q[2] = pose[5];
170
+ q[3] = pose[6];
171
+ if (c.align[row] === 1) alignUp(q, ux, uy, uz, s.arc, s.tmp);
172
+ const mx = c.move[o3];
173
+ const mz = dim === 3 ? c.move[o3 + 2] : 0;
174
+ rotate(s.tmp, 0, q, 0, mx, 0, mz);
175
+ let hx = s.tmp[0];
176
+ let hy = s.tmp[1];
177
+ let hz = dim === 3 ? s.tmp[2] : 0;
178
+ const hu = hx * ux + hy * uy + hz * uz;
179
+ hx -= ux * hu;
180
+ hy -= uy * hu;
181
+ hz -= uz * hu;
182
+ const speed = Math.sqrt(mx * mx + mz * mz);
183
+ const hl = Math.sqrt(hx * hx + hy * hy + hz * hz);
184
+ if (hl > 1e-9) {
185
+ hx *= speed / hl;
186
+ hy *= speed / hl;
187
+ hz *= speed / hl;
188
+ } else {
189
+ hx = hy = hz = 0;
190
+ }
191
+ const wasGrounded = c.grounded[row] === 1;
192
+ let vx;
193
+ let vy;
194
+ let vz;
195
+ let jumped = false;
196
+ if (wasGrounded) {
197
+ const nx = c.groundNormal[o3];
198
+ const ny = c.groundNormal[o3 + 1];
199
+ const nz = c.groundNormal[o3 + 2];
200
+ const nu = nx * ux + ny * uy + nz * uz;
201
+ const k = nu > 1e-3 ? Math.max(0, hx * nx + hy * ny + hz * nz) / nu : 0;
202
+ vx = hx - ux * k;
203
+ vy = hy - uy * k;
204
+ vz = hz - uz * k;
205
+ if (c.jump[row] === 1) {
206
+ const j = c.jumpSpeed[row];
207
+ vx += ux * j;
208
+ vy += uy * j;
209
+ vz += uz * j;
210
+ jumped = true;
211
+ c.jump[row] = 0;
212
+ table.markChanged(CharacterIntent, row);
213
+ }
214
+ } else {
215
+ vx = c.velocity[o3];
216
+ vy = c.velocity[o3 + 1];
217
+ vz = dim === 3 ? c.velocity[o3 + 2] : 0;
218
+ const vu = vx * ux + vy * uy + vz * uz;
219
+ let wx = vx - ux * vu;
220
+ let wy = vy - uy * vu;
221
+ let wz = vz - uz * vu;
222
+ const air = c.airControl[row];
223
+ const blend = air >= 1 ? 1 : 1 - (1 - air) ** (dt * 60);
224
+ wx += (hx - wx) * blend;
225
+ wy += (hy - wy) * blend;
226
+ wz += (hz - wz) * blend;
227
+ vx = wx + ux * vu;
228
+ vy = wy + uy * vu;
229
+ vz = wz + uz * vu;
230
+ }
231
+ const carry = s.carry;
232
+ carry[0] = carry[1] = carry[2] = 0;
233
+ if (wasGrounded) platformCarry(s, p, c.groundEntity[row], px, py, pz, dt, carry);
234
+ const half = wasGrounded && !jumped ? 0 : 0.5 * dt * dt;
235
+ const dx = vx * dt + ax * half + carry[0];
236
+ const dy = vy * dt + ay * half + carry[1];
237
+ const dz = vz * dt + az * half + carry[2];
238
+ const rising = jumped || !wasGrounded && vx * ux + vy * uy + vz * uz > 0;
239
+ if (record.snapDistance > 0 && record.snapping === rising) {
240
+ record.snapping = !rising;
241
+ if (rising) record.kcc.disableSnapToGround();
242
+ else record.kcc.enableSnapToGround(record.snapDistance);
243
+ }
244
+ const kcc = record.kcc;
245
+ const rv = p.rawVec;
246
+ rv.x = ux;
247
+ rv.y = uy;
248
+ if (dim === 3) rv.z = uz;
249
+ kcc.raw.setUp(rv);
250
+ rv.x = dx;
251
+ rv.y = dy;
252
+ if (dim === 3) rv.z = dz;
253
+ const raw = p.raw;
254
+ kcc.raw.computeColliderMovement(
255
+ dt,
256
+ raw.broadPhase.raw,
257
+ raw.narrowPhase.raw,
258
+ raw.bodies.raw,
259
+ raw.colliders.raw,
260
+ record.collider.handle,
261
+ rv,
262
+ false,
263
+ void 0,
264
+ p.R.QueryFilterFlags.EXCLUDE_SENSORS,
265
+ (c.layers[row] & 65535) << 16 | c.mask[row] & 65535,
266
+ void 0
267
+ );
268
+ const mv = record.kcc.computedMovement(s.moved);
269
+ const mvx = mv.x;
270
+ const mvy = mv.y;
271
+ const mvz = dim === 3 ? mv.z : 0;
272
+ const grounded = record.kcc.computedGrounded();
273
+ const pos = s.pos;
274
+ pos[0] = Math.fround(px + mvx);
275
+ pos[1] = Math.fround(py + mvy);
276
+ pos[2] = Math.fround(pz + mvz);
277
+ const groups = (c.layers[row] & 65535) << 16 | c.mask[row] & 65535;
278
+ let groundDot = -2;
279
+ let gnx = ux;
280
+ let gny = uy;
281
+ let gnz = uz;
282
+ let groundEntity = -1;
283
+ const hit = s.hit;
284
+ const push = c.pushForce[row] * dt;
285
+ const n = record.kcc.numComputedCollisions();
286
+ for (let i = 0; i < n; i++) {
287
+ if (!record.kcc.computedCollision(i, hit)) continue;
288
+ const normal = hit.normal1;
289
+ const nx = normal.x;
290
+ const ny = normal.y;
291
+ const nz = dim === 3 ? normal.z : 0;
292
+ const nd = nx * ux + ny * uy + nz * uz;
293
+ const collider = hit.collider;
294
+ if (nd >= record.cosMaxSlope - 1e-3) {
295
+ if (nd > groundDot && collider) {
296
+ groundDot = nd;
297
+ gnx = nx;
298
+ gny = ny;
299
+ gnz = nz;
300
+ const e = p.colliderEntity.get(collider.handle);
301
+ groundEntity = e === void 0 ? -1 : p.colliderOwner.get(e) ?? e;
302
+ }
303
+ } else if (collider && push > 0 && speed > 1e-6) {
304
+ pushBody(s, p, collider, hx / speed, hy / speed, hz / speed, speed, push);
305
+ }
306
+ if (!grounded) {
307
+ const d = vx * nx + vy * ny + vz * nz;
308
+ if (d < 0) {
309
+ vx -= nx * d;
310
+ vy -= ny * d;
311
+ vz -= nz * d;
312
+ }
313
+ }
314
+ }
315
+ if (grounded && groundEntity === -1) {
316
+ const g = s.g;
317
+ if (probeGround(s, p, record, pos, ux, uy, uz, groups, g)) {
318
+ gnx = g[0];
319
+ gny = g[1];
320
+ gnz = g[2];
321
+ groundEntity = g[3];
322
+ } else if (wasGrounded) {
323
+ gnx = c.groundNormal[o3];
324
+ gny = c.groundNormal[o3 + 1];
325
+ gnz = c.groundNormal[o3 + 2];
326
+ groundEntity = c.groundEntity[row];
327
+ }
328
+ }
329
+ if (grounded) {
330
+ const d = vx * gnx + vy * gny + vz * gnz;
331
+ vx -= gnx * d;
332
+ vy -= gny * d;
333
+ vz -= gnz * d;
334
+ } else {
335
+ vx += ax * dt;
336
+ vy += ay * dt;
337
+ vz += az * dt;
338
+ }
339
+ body.setNextKinematicTranslation(p.vec(pos[0], pos[1], pos[2]));
340
+ body.setNextKinematicRotation(p.rotation(q, 0));
341
+ const parent = parentOf(world, entity);
342
+ if (parent === void 0 && s.rootIdentity) {
343
+ c.translation[o3] = pos[0];
344
+ c.translation[o3 + 1] = pos[1];
345
+ c.translation[o3 + 2] = pos[2];
346
+ c.rotation[row * 4] = q[0];
347
+ c.rotation[row * 4 + 1] = q[1];
348
+ c.rotation[row * 4 + 2] = q[2];
349
+ c.rotation[row * 4 + 3] = q[3];
350
+ } else {
351
+ worldToLocal(world, parent, entity, pos, q, c.translation, o3, c.rotation, row * 4);
352
+ }
353
+ c.grounded[row] = grounded ? 1 : 0;
354
+ c.groundNormal[o3] = gnx;
355
+ c.groundNormal[o3 + 1] = gny;
356
+ c.groundNormal[o3 + 2] = gnz;
357
+ c.groundEntity[row] = grounded ? groundEntity : -1;
358
+ c.velocity[o3] = vx;
359
+ c.velocity[o3 + 1] = vy;
360
+ c.velocity[o3 + 2] = vz;
361
+ c.stateUp[o3] = ux;
362
+ c.stateUp[o3 + 1] = uy;
363
+ c.stateUp[o3 + 2] = uz;
364
+ c.airTime[row] = grounded ? 0 : c.airTime[row] + dt;
365
+ if (grounded !== wasGrounded) world.send(CharacterGroundEvent, { entity, grounded });
366
+ }
367
+ function alignUp(q, ux, uy, uz, arc, tmp) {
368
+ rotate(tmp, 0, q, 0, 0, 1, 0);
369
+ const yx = tmp[0];
370
+ const yy = tmp[1];
371
+ const yz = tmp[2];
372
+ const d = yx * ux + yy * uy + yz * uz;
373
+ if (d > 1 - 1e-12) return;
374
+ if (d < -1 + 1e-9) {
375
+ rotate(tmp, 0, q, 0, 1, 0, 0);
376
+ arc[0] = tmp[0];
377
+ arc[1] = tmp[1];
378
+ arc[2] = tmp[2];
379
+ arc[3] = 0;
380
+ } else {
381
+ arc[0] = yy * uz - yz * uy;
382
+ arc[1] = yz * ux - yx * uz;
383
+ arc[2] = yx * uy - yy * ux;
384
+ arc[3] = 1 + d;
385
+ const len2 = Math.sqrt(arc[0] ** 2 + arc[1] ** 2 + arc[2] ** 2 + arc[3] ** 2);
386
+ arc[0] /= len2;
387
+ arc[1] /= len2;
388
+ arc[2] /= len2;
389
+ arc[3] /= len2;
390
+ }
391
+ const bx = q[0];
392
+ const by = q[1];
393
+ const bz = q[2];
394
+ const bw = q[3];
395
+ const ax = arc[0];
396
+ const ay = arc[1];
397
+ const az = arc[2];
398
+ const aw = arc[3];
399
+ let x = aw * bx + ax * bw + ay * bz - az * by;
400
+ let y = aw * by - ax * bz + ay * bw + az * bx;
401
+ let z = aw * bz + ax * by - ay * bx + az * bw;
402
+ let w = aw * bw - ax * bx - ay * by - az * bz;
403
+ const len = Math.sqrt(x * x + y * y + z * z + w * w) || 1;
404
+ x /= len;
405
+ y /= len;
406
+ z /= len;
407
+ w /= len;
408
+ q[0] = x;
409
+ q[1] = y;
410
+ q[2] = z;
411
+ q[3] = w;
412
+ }
413
+ function probeGround(s, p, record, pos, ux, uy, uz, groups, out) {
414
+ s.ray ??= new p.R.Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: -1, z: 0 });
415
+ const ray = s.ray;
416
+ const o = ray.origin;
417
+ const d = ray.dir;
418
+ o.x = pos[0];
419
+ o.y = pos[1];
420
+ d.x = -ux;
421
+ d.y = -uy;
422
+ if (p.dim === 3) {
423
+ o.z = pos[2];
424
+ d.z = -uz;
425
+ }
426
+ const reach = record.bottom + CHARACTER_OFFSET + record.snapDistance + 0.05;
427
+ const hit = p.raw.castRayAndGetNormal(
428
+ ray,
429
+ reach,
430
+ true,
431
+ p.R.QueryFilterFlags.EXCLUDE_SENSORS,
432
+ groups,
433
+ void 0,
434
+ record.body.body
435
+ );
436
+ if (!hit) return false;
437
+ const n = hit.normal;
438
+ const nz = p.dim === 3 ? n.z : 0;
439
+ if (n.x * ux + n.y * uy + nz * uz < record.cosMaxSlope - 1e-3) return false;
440
+ out[0] = n.x;
441
+ out[1] = n.y;
442
+ out[2] = nz;
443
+ const e = p.colliderEntity.get(hit.collider.handle);
444
+ out[3] = e === void 0 ? -1 : p.colliderOwner.get(e) ?? e;
445
+ return true;
446
+ }
447
+ function platformCarry(s, p, ground, px, py, pz, dt, out) {
448
+ if (ground < 0) return;
449
+ const record = p.bodies.get(ground);
450
+ if (!record || p.characters.has(record.entity)) return;
451
+ const body = record.body;
452
+ const t = body.translation(s.t1);
453
+ const rx = px - t.x;
454
+ const ry = py - t.y;
455
+ const rz = p.dim === 3 ? pz - t.z : 0;
456
+ if (record.kind === KIND_KINEMATIC_POSITION) {
457
+ const nt = body.nextTranslation(s.t2);
458
+ if (p.dim === 3) {
459
+ const r = body.rotation(s.r1);
460
+ const nr = body.nextRotation(s.r2);
461
+ const tmp = s.tmp;
462
+ const q = s.arc;
463
+ q[0] = -r.x;
464
+ q[1] = -r.y;
465
+ q[2] = -r.z;
466
+ q[3] = r.w;
467
+ rotate(tmp, 0, q, 0, rx, ry, rz);
468
+ q[0] = nr.x;
469
+ q[1] = nr.y;
470
+ q[2] = nr.z;
471
+ q[3] = nr.w;
472
+ rotate(tmp, 0, q, 0, tmp[0], tmp[1], tmp[2]);
473
+ out[0] = nt.x + tmp[0] - px;
474
+ out[1] = nt.y + tmp[1] - py;
475
+ out[2] = nt.z + tmp[2] - pz;
476
+ } else {
477
+ const a = body.nextRotation() - body.rotation();
478
+ const cos = Math.cos(a);
479
+ const sin = Math.sin(a);
480
+ out[0] = nt.x + rx * cos - ry * sin - px;
481
+ out[1] = nt.y + rx * sin + ry * cos - py;
482
+ }
483
+ } else if (record.kind === KIND_KINEMATIC_VELOCITY) {
484
+ const lv = body.linvel(s.t2);
485
+ if (p.dim === 3) {
486
+ const av = body.angvel(s.r1);
487
+ out[0] = (lv.x + av.y * rz - av.z * ry) * dt;
488
+ out[1] = (lv.y + av.z * rx - av.x * rz) * dt;
489
+ out[2] = (lv.z + av.x * ry - av.y * rx) * dt;
490
+ } else {
491
+ const w = body.angvel();
492
+ out[0] = (lv.x - w * ry) * dt;
493
+ out[1] = (lv.y + w * rx) * dt;
494
+ }
495
+ }
496
+ }
497
+ function pushBody(s, p, collider, dx, dy, dz, speed, impulse) {
498
+ const body = collider.parent();
499
+ if (!body?.isDynamic()) return;
500
+ const v = body.linvel(s.t2);
501
+ const along = v.x * dx + v.y * dy + (p.dim === 3 ? v.z * dz : 0);
502
+ const dv = speed - along;
503
+ if (dv <= 0) return;
504
+ const j = Math.min(impulse, body.mass() * dv);
505
+ body.applyImpulse(p.vec(dx * j, dy * j, dz * j), true);
506
+ }
507
+ export {
508
+ characterSystem
509
+ };