@driftengine/physics 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- package/src/world.ts +961 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { collideShapes, createManifold } from './manifold.js';
|
|
2
|
+
import { MAX_MESH_MANIFOLDS, collideMesh, isMesh } from './meshContact.js';
|
|
3
|
+
import { shapeBounds } from './shape.js';
|
|
4
|
+
/**
|
|
5
|
+
* The most parts one body may be made of.
|
|
6
|
+
*
|
|
7
|
+
* **It lives here rather than beside `decomposeConvex`, and that is a measurement rather than a
|
|
8
|
+
* preference.** It was declared in `decompose.ts`, which is offline and which nothing in a frame
|
|
9
|
+
* should reach; importing one integer from there put the whole decomposition — the voxeliser, the
|
|
10
|
+
* flood fill, the merge, the direction table — into every bundle that touches the physics runtime.
|
|
11
|
+
* `physics-only` moved outside its 3% band and an A/B against this one import line was what said so.
|
|
12
|
+
*
|
|
13
|
+
* A constant about how many shapes a body may hold is a fact about bodies. `decompose.ts` imports it
|
|
14
|
+
* from here to cap `maxHulls`, which is the direction the dependency should have run in from the
|
|
15
|
+
* start.
|
|
16
|
+
*/
|
|
17
|
+
export const MAX_BODY_PARTS = 32;
|
|
18
|
+
/**
|
|
19
|
+
* A body made of several convex shapes, against whatever it meets.
|
|
20
|
+
*
|
|
21
|
+
* **The half of convex decomposition that lives in the tick.** `decomposeConvex` turns a concave
|
|
22
|
+
* mesh into hulls offline; without this there is nowhere for them to go, because `BodySet.shape`
|
|
23
|
+
* holds one shape per body and a decomposed body would have to become several bodies held together
|
|
24
|
+
* by joints. `world.ts`'s own refusal message said the runtime already accepted hulls, and it did
|
|
25
|
+
* not.
|
|
26
|
+
*
|
|
27
|
+
* **A part carries no offset and no rotation, and that is not a simplification.** A `ConvexShape` is
|
|
28
|
+
* a point cloud in the body's frame — `shape.ts` says so where it defines `vertices` — so a
|
|
29
|
+
* decomposition's parts are already where they belong. The kinematic `Body.parts` carries `ox/oy/oz`
|
|
30
|
+
* because it wraps shapes a caller built at the origin and placed afterwards; nothing here does
|
|
31
|
+
* that, and a field that is always zero is a field that is eventually set wrong.
|
|
32
|
+
*
|
|
33
|
+
* **This fills a set of manifolds where an ordinary pair fills one**, which is the shape
|
|
34
|
+
* `meshContact.ts` established: `prepareContact` appends a constraint rather than replacing one and
|
|
35
|
+
* `storeWarm` accumulates by pair, so the solver needed nothing new.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* How many contact planes one pair may produce.
|
|
39
|
+
*
|
|
40
|
+
* **The same number as `MAX_BODY_PARTS`, so a compound against a single convex shape can never
|
|
41
|
+
* overflow**: every part gets a plane. Two compounds can, at up to 32 by 32 part pairs against 32
|
|
42
|
+
* slots, and the overflow rule there is the mesh path's — stop when the set is full. That is the one
|
|
43
|
+
* place in this design where a contact can be dropped without anybody being told, and what would
|
|
44
|
+
* make it matter is two 32-part bodies interpenetrating deeply.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_PAIR_MANIFOLDS = MAX_BODY_PARTS;
|
|
47
|
+
/** Whether a body is made of parts rather than of one shape. */
|
|
48
|
+
export function isCompound(parts) {
|
|
49
|
+
return parts !== undefined;
|
|
50
|
+
}
|
|
51
|
+
/** How many shapes a body presents to the narrow phase, whichever way it is built. */
|
|
52
|
+
export function partCount(shape, parts) {
|
|
53
|
+
if (parts !== undefined)
|
|
54
|
+
return parts.length;
|
|
55
|
+
return shape === undefined ? 0 : 1;
|
|
56
|
+
}
|
|
57
|
+
/** One of them, by index. */
|
|
58
|
+
export function partAt(shape, parts, index) {
|
|
59
|
+
if (parts !== undefined)
|
|
60
|
+
return parts[index];
|
|
61
|
+
return shape;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The largest distance from a body's own origin to any point of any part.
|
|
65
|
+
*
|
|
66
|
+
* What `boundsOf` needs, and it is a maximum rather than a sum because every part is already in the
|
|
67
|
+
* body's frame: a part built off to one side carries that offset inside its own `boundRadius`.
|
|
68
|
+
*/
|
|
69
|
+
export function partsBoundRadius(parts) {
|
|
70
|
+
let radius = 0;
|
|
71
|
+
for (const part of parts)
|
|
72
|
+
if (part.boundRadius > radius)
|
|
73
|
+
radius = part.boundRadius;
|
|
74
|
+
return radius;
|
|
75
|
+
}
|
|
76
|
+
/** Scratch spheres, one per part of each side, so pruning allocates nothing. */
|
|
77
|
+
const SPHERES_A = new Float64Array(MAX_BODY_PARTS * 4);
|
|
78
|
+
const SPHERES_B = new Float64Array(MAX_BODY_PARTS * 4);
|
|
79
|
+
const LOCAL = { minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0 };
|
|
80
|
+
const SCRATCH = createManifold();
|
|
81
|
+
const MESH_SCRATCH = Array.from({ length: MAX_MESH_MANIFOLDS }, createManifold);
|
|
82
|
+
/**
|
|
83
|
+
* A bounding sphere per part, in world, so the pair loop is a distance test and not a full collide.
|
|
84
|
+
*
|
|
85
|
+
* Computed once for each side of a pair rather than once per part pair: with 32 parts a side that is
|
|
86
|
+
* 64 bound computations instead of 1,024 of them. `shapeBounds` reads a part's own points, which are
|
|
87
|
+
* in the body frame, so the centre only has to be carried through the pose.
|
|
88
|
+
*/
|
|
89
|
+
function fillSpheres(shape, parts, pose, out) {
|
|
90
|
+
const count = partCount(shape, parts);
|
|
91
|
+
for (let i = 0; i < count; i++) {
|
|
92
|
+
const part = partAt(shape, parts, i);
|
|
93
|
+
shapeBounds(part, LOCAL);
|
|
94
|
+
const cx = (LOCAL.minX + LOCAL.maxX) * 0.5;
|
|
95
|
+
const cy = (LOCAL.minY + LOCAL.maxY) * 0.5;
|
|
96
|
+
const cz = (LOCAL.minZ + LOCAL.maxZ) * 0.5;
|
|
97
|
+
const hx = (LOCAL.maxX - LOCAL.minX) * 0.5;
|
|
98
|
+
const hy = (LOCAL.maxY - LOCAL.minY) * 0.5;
|
|
99
|
+
const hz = (LOCAL.maxZ - LOCAL.minZ) * 0.5;
|
|
100
|
+
rotate(pose, cx, cy, cz, out, i * 4);
|
|
101
|
+
out[i * 4] = out[i * 4] + pose.x;
|
|
102
|
+
out[i * 4 + 1] = out[i * 4 + 1] + pose.y;
|
|
103
|
+
out[i * 4 + 2] = out[i * 4 + 2] + pose.z;
|
|
104
|
+
out[i * 4 + 3] = Math.sqrt(hx * hx + hy * hy + hz * hz);
|
|
105
|
+
}
|
|
106
|
+
return count;
|
|
107
|
+
}
|
|
108
|
+
/** A vector through a pose's rotation, written into `out` at `at`. */
|
|
109
|
+
function rotate(pose, x, y, z, out, at) {
|
|
110
|
+
const { qx, qy, qz, qw } = pose;
|
|
111
|
+
const tx = 2 * (qy * z - qz * y);
|
|
112
|
+
const ty = 2 * (qz * x - qx * z);
|
|
113
|
+
const tz = 2 * (qx * y - qy * x);
|
|
114
|
+
out[at] = x + qw * tx + (qy * tz - qz * ty);
|
|
115
|
+
out[at + 1] = y + qw * ty + (qz * tx - qx * tz);
|
|
116
|
+
out[at + 2] = z + qw * tz + (qx * ty - qy * tx);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Every touching pair of parts, as one manifold each.
|
|
120
|
+
*
|
|
121
|
+
* **A mesh partner is the pair this row exists for**, and it gets a rule of its own. A decomposed
|
|
122
|
+
* body falling onto a level is a dynamic compound against a static mesh, so refusing it would refuse
|
|
123
|
+
* the case. Each part goes through `collideMesh` unchanged, and the budget is divided between the
|
|
124
|
+
* parts that find candidates, **never below one each**. That is a deliberate departure from the mesh
|
|
125
|
+
* path's own first-come rule: `collideMesh` stops at eight and drops the rest, which is defensible
|
|
126
|
+
* when the ninth candidate is a triangle and indefensible when it is a whole limb of the body — one
|
|
127
|
+
* leg of a chair sinking through a floor while the other three hold.
|
|
128
|
+
*/
|
|
129
|
+
export function collideCompound(shapeA, partsA, poseA, shapeB, partsB, poseB, margin, out) {
|
|
130
|
+
const countA = fillSpheres(shapeA, partsA, poseA, SPHERES_A);
|
|
131
|
+
const countB = fillSpheres(shapeB, partsB, poseB, SPHERES_B);
|
|
132
|
+
if (countA === 0 || countB === 0)
|
|
133
|
+
return 0;
|
|
134
|
+
/*
|
|
135
|
+
* A mesh has no parts and never moves, so it is always the other side of the pair. Its budget is
|
|
136
|
+
* shared out below rather than spent by whichever part is tried first.
|
|
137
|
+
*/
|
|
138
|
+
const meshSide = countA === 1 && isMesh(partAt(shapeA, partsA, 0))
|
|
139
|
+
? 0
|
|
140
|
+
: countB === 1 && isMesh(partAt(shapeB, partsB, 0))
|
|
141
|
+
? 1
|
|
142
|
+
: -1;
|
|
143
|
+
if (meshSide >= 0) {
|
|
144
|
+
return collideCompoundMesh(meshSide === 0 ? shapeB : shapeA, meshSide === 0 ? partsB : partsA, meshSide === 0 ? poseB : poseA, partAt(meshSide === 0 ? shapeA : shapeB, meshSide === 0 ? partsA : partsB, 0), meshSide === 0 ? poseA : poseB, meshSide === 0, margin, out);
|
|
145
|
+
}
|
|
146
|
+
let count = 0;
|
|
147
|
+
for (let i = 0; i < countA && count < out.length; i++) {
|
|
148
|
+
const ax = SPHERES_A[i * 4];
|
|
149
|
+
const ay = SPHERES_A[i * 4 + 1];
|
|
150
|
+
const az = SPHERES_A[i * 4 + 2];
|
|
151
|
+
const ar = SPHERES_A[i * 4 + 3] + margin;
|
|
152
|
+
const shapeI = partAt(shapeA, partsA, i);
|
|
153
|
+
for (let j = 0; j < countB && count < out.length; j++) {
|
|
154
|
+
const dx = SPHERES_B[j * 4] - ax;
|
|
155
|
+
const dy = SPHERES_B[j * 4 + 1] - ay;
|
|
156
|
+
const dz = SPHERES_B[j * 4 + 2] - az;
|
|
157
|
+
const reach = ar + SPHERES_B[j * 4 + 3];
|
|
158
|
+
if (dx * dx + dy * dy + dz * dz > reach * reach)
|
|
159
|
+
continue;
|
|
160
|
+
const shapeJ = partAt(shapeB, partsB, j);
|
|
161
|
+
if (!collideShapes(shapeI, poseA, shapeJ, poseB, margin, SCRATCH))
|
|
162
|
+
continue;
|
|
163
|
+
copyInto(SCRATCH, out[count], partKey(i, j));
|
|
164
|
+
count++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return count;
|
|
168
|
+
}
|
|
169
|
+
/** Every part of a compound against one static triangle mesh, sharing the manifold budget. */
|
|
170
|
+
function collideCompoundMesh(shape, parts, pose, mesh, meshPose, meshIsA, margin, out) {
|
|
171
|
+
const count = partCount(shape, parts);
|
|
172
|
+
/*
|
|
173
|
+
* At least one slot each, and the remainder shared. A part that touches the mesh always gets a
|
|
174
|
+
* contact plane; a part that touches it in three places gets three only if the budget allows.
|
|
175
|
+
*/
|
|
176
|
+
const share = Math.max(1, Math.floor(out.length / Math.max(1, count)));
|
|
177
|
+
let filled = 0;
|
|
178
|
+
for (let i = 0; i < count && filled < out.length; i++) {
|
|
179
|
+
const part = partAt(shape, parts, i);
|
|
180
|
+
/*
|
|
181
|
+
* The scratch set is passed whole and the *copy* is what the share limits — slicing it here
|
|
182
|
+
* would allocate an array per part per pair, in the tick, which is the one thing this package
|
|
183
|
+
* does not do.
|
|
184
|
+
*/
|
|
185
|
+
const found = meshIsA
|
|
186
|
+
? collideMesh(mesh, meshPose, part, pose, margin, MESH_SCRATCH)
|
|
187
|
+
: collideMesh(part, pose, mesh, meshPose, margin, MESH_SCRATCH);
|
|
188
|
+
const room = Math.min(found, share, out.length - filled);
|
|
189
|
+
for (let m = 0; m < room && filled < out.length; m++) {
|
|
190
|
+
copyInto(MESH_SCRATCH[m], out[filled], partKey(i, 0));
|
|
191
|
+
filled++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return filled;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The two part indices, folded into one number the feature id can carry.
|
|
198
|
+
*
|
|
199
|
+
* `meshContact.ts` folds a triangle index into every feature id for the same reason: warm starting
|
|
200
|
+
* pairs an accumulated impulse to a contact by identity, and a body resting across two parts would
|
|
201
|
+
* otherwise hand one part's impulse to the other on the tick a contact moves between them.
|
|
202
|
+
*/
|
|
203
|
+
function partKey(a, b) {
|
|
204
|
+
return a * MAX_BODY_PARTS + b;
|
|
205
|
+
}
|
|
206
|
+
/** A manifold copied into the pair's set, with the part pair folded into every feature id. */
|
|
207
|
+
function copyInto(from, to, key) {
|
|
208
|
+
to.nx = from.nx;
|
|
209
|
+
to.ny = from.ny;
|
|
210
|
+
to.nz = from.nz;
|
|
211
|
+
to.count = from.count;
|
|
212
|
+
to.curvedA = from.curvedA;
|
|
213
|
+
to.curvedB = from.curvedB;
|
|
214
|
+
for (let i = 0; i < from.count; i++) {
|
|
215
|
+
to.points[i * 3] = from.points[i * 3] ?? 0;
|
|
216
|
+
to.points[i * 3 + 1] = from.points[i * 3 + 1] ?? 0;
|
|
217
|
+
to.points[i * 3 + 2] = from.points[i * 3 + 2] ?? 0;
|
|
218
|
+
to.separations[i] = from.separations[i] ?? 0;
|
|
219
|
+
to.featureIds[i] = (((key & 0xffff) << 12) ^ (from.featureIds[i] ?? 0)) | 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { QueryFilter } from './query.ts';
|
|
2
|
+
import type { ConvexShape } from './shape.ts';
|
|
3
|
+
import type { PhysicsWorld } from './world.ts';
|
|
4
|
+
/**
|
|
5
|
+
* A kinematic capsule character: the mechanism, and the feel that rides on it.
|
|
6
|
+
*
|
|
7
|
+
* **Kinematic and not a dynamic body.** A dynamic character falls over, cannot be steered crisply,
|
|
8
|
+
* and turns every design question about movement into a question about torque. Every shipping
|
|
9
|
+
* engine ships a kinematic controller for exactly that reason, and this one sweeps the same
|
|
10
|
+
* `PhysicsWorld` everything else queries, so it sees moving bodies as well as scenery.
|
|
11
|
+
*
|
|
12
|
+
* **Feel is here too, and that is a decision `AGENTS.md` records.** That file's worked example for
|
|
13
|
+
* the engine/game boundary once said a controller was game because "friction, coyote time and slide
|
|
14
|
+
* boost are one game's feel". The same file's *audio* rule draws the line correctly: the DSP graph
|
|
15
|
+
* is engine and which sounds play is game. A controller that takes a friction curve and a
|
|
16
|
+
* coyote-time duration takes numbers and time, which is what that file says engine APIs take; what
|
|
17
|
+
* stays game is which numbers. **What would make this wrong** is a parameter that only means
|
|
18
|
+
* something in one game.
|
|
19
|
+
*
|
|
20
|
+
* **Coyote time and jump buffering are counted in ticks**, never accumulated in a float of seconds.
|
|
21
|
+
* That is the determinism contract reaching into the feel layer, and the difference between a
|
|
22
|
+
* replay that matches and one that drifts by a frame after an hour of uptime.
|
|
23
|
+
*/
|
|
24
|
+
export declare const GROUNDED = 0;
|
|
25
|
+
export declare const AIRBORNE = 1;
|
|
26
|
+
export declare const SLIDING = 2;
|
|
27
|
+
export type ControllerState = 0 | 1 | 2;
|
|
28
|
+
export interface ControllerOptions {
|
|
29
|
+
radius?: number;
|
|
30
|
+
/** Half the cylindrical section, so total height is `2 * (halfHeight + radius)`. */
|
|
31
|
+
halfHeight?: number;
|
|
32
|
+
/** How high a lip may be and still be walked over. */
|
|
33
|
+
stepHeight?: number;
|
|
34
|
+
/** Cosine of the steepest walkable slope. Above it the character slides. */
|
|
35
|
+
slopeCos?: number;
|
|
36
|
+
maxSpeed?: number;
|
|
37
|
+
acceleration?: number;
|
|
38
|
+
deceleration?: number;
|
|
39
|
+
airControl?: number;
|
|
40
|
+
jumpSpeed?: number;
|
|
41
|
+
/** Upward velocity is cut to this fraction when the jump is released early. */
|
|
42
|
+
jumpCutoff?: number;
|
|
43
|
+
gravity?: number;
|
|
44
|
+
/** Ticks after leaving the ground during which a jump still counts. */
|
|
45
|
+
coyoteTicks?: number;
|
|
46
|
+
/** Ticks a jump press is remembered for, so pressing just before landing still fires. */
|
|
47
|
+
jumpBufferTicks?: number;
|
|
48
|
+
/** Extra downhill acceleration while sliding on too steep a surface. */
|
|
49
|
+
slideBoost?: number;
|
|
50
|
+
/** Impulse scale applied to dynamic bodies the character walks into. */
|
|
51
|
+
pushStrength?: number;
|
|
52
|
+
filter?: QueryFilter;
|
|
53
|
+
/**
|
|
54
|
+
* A surface to stand on beside the bodies, where the world has one.
|
|
55
|
+
*
|
|
56
|
+
* Consulted *after* the bodies and only when they found nothing walkable, so a consumer with
|
|
57
|
+
* both keeps whatever a collider said — a body is a thing that is there, and a surface is a
|
|
58
|
+
* description of where the ground is. Absent means bodies alone, which is every consumer that
|
|
59
|
+
* has not asked.
|
|
60
|
+
*/
|
|
61
|
+
ground?: GroundProbe;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A surface the controller can stand on that is not a body.
|
|
65
|
+
*
|
|
66
|
+
* **Declared structurally, so nothing is imported in either direction.** `@driftengine/core`'s
|
|
67
|
+
* `GroundSurface` satisfies this exactly and neither package names the other — the same shape
|
|
68
|
+
* `ragdollFromBones` uses for a pose, and for the same reason: this package imports no other
|
|
69
|
+
* engine package, and a surface that knew about splines and ribbon meshes could not live here.
|
|
70
|
+
*
|
|
71
|
+
* **Why a character needs one at all.** An analytic surface answers *where the ground is in this
|
|
72
|
+
* column*; a collider answers *what is solid*. They are different questions and a banked deck is
|
|
73
|
+
* where they diverge — a world-axis box over a banked strip has a flat top at the strip's highest
|
|
74
|
+
* corner, so a character standing on the box hovers above the track through every corner. A
|
|
75
|
+
* consumer with such a deck could sweep bodies or stand on the deck, and never both.
|
|
76
|
+
*
|
|
77
|
+
* `atY` is the height of whatever is asking, so a route that passes over itself answers with the
|
|
78
|
+
* deck *under* the asker rather than the highest one in the column.
|
|
79
|
+
*
|
|
80
|
+
* **The field names were `nx`, `ny`, `nz` until 2026-08-28 and that was a defect**, because the
|
|
81
|
+
* paragraph above was not true: `SurfaceHit` spells them `normalX`, `normalY`, `normalZ`, so a real
|
|
82
|
+
* `GroundSurface` handed to a controller wrote three fields nothing read and left three fields
|
|
83
|
+
* nothing wrote. TypeScript allows the assignment — method parameters are bivariant — so it
|
|
84
|
+
* compiled, and a *flat* surface still behaved, because the scratch it was handed happens to start
|
|
85
|
+
* at `(0, 1, 0)`. Only a banked one showed it, by giving the character the normal of whatever body
|
|
86
|
+
* it last touched. Renamed rather than aliased: nothing in three consumers implements this
|
|
87
|
+
* interface, and a compile error naming the field is a one-line fix where a stale normal is a week.
|
|
88
|
+
*/
|
|
89
|
+
export interface GroundProbeHit {
|
|
90
|
+
/** Height of the surface in this column. */
|
|
91
|
+
y: number;
|
|
92
|
+
normalX: number;
|
|
93
|
+
normalY: number;
|
|
94
|
+
normalZ: number;
|
|
95
|
+
}
|
|
96
|
+
export interface GroundProbe {
|
|
97
|
+
sample(x: number, z: number, out: GroundProbeHit, atY?: number): boolean;
|
|
98
|
+
}
|
|
99
|
+
export interface ControllerInput {
|
|
100
|
+
/** Desired direction, in world units per second. Length above `maxSpeed` is clamped. */
|
|
101
|
+
moveX: number;
|
|
102
|
+
moveZ: number;
|
|
103
|
+
/**
|
|
104
|
+
* The third component, for a body whose up is not world Y. Absent means zero, which is what
|
|
105
|
+
* every caller with a level floor means and is arithmetically what they got before this existed.
|
|
106
|
+
*
|
|
107
|
+
* **Two components span the tangent plane only while up is world Y**, and `setUp` made up a
|
|
108
|
+
* parameter without making the input one. On a wall whose normal is +Z, up the wall is +Y and no
|
|
109
|
+
* pair of `moveX` and `moveZ` describes it: the input plane collapses onto a line, so a body that
|
|
110
|
+
* could stand on the wall could not be steered along it. Reported from outside by a consumer that
|
|
111
|
+
* gave up on the acceleration curves, the air control and the speed clamp entirely — writing
|
|
112
|
+
* `velX`/`velY`/`velZ` directly each tick with `acceleration: 0, deceleration: 0` so that
|
|
113
|
+
* `applyFeel` would leave the velocity alone — which is the whole of the feel this class exists
|
|
114
|
+
* to provide, bypassed to get one axis back.
|
|
115
|
+
*
|
|
116
|
+
* Read as a world direction and flattened onto the surface, exactly as the other two are. A
|
|
117
|
+
* caller supplying all three describes "forward" in whatever frame it is standing in.
|
|
118
|
+
*/
|
|
119
|
+
moveY?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Whether to flatten the move vector onto the plane the body is standing on. Absent means yes,
|
|
122
|
+
* which is what every caller before this got and what a body moving on one surface wants.
|
|
123
|
+
*
|
|
124
|
+
* **False is for a body between two surfaces, which is the one case the projection cannot
|
|
125
|
+
* serve.** Steering normally holds the along-up component out, steers the rest and puts it back,
|
|
126
|
+
* because that component is gravity and the jump rather than the stick. A body crossing a convex
|
|
127
|
+
* edge has a third thing there: while its frame rotates from the old face to the new, the correct
|
|
128
|
+
* direction of travel is neither face's tangent but a blend of them, held by the contacts already
|
|
129
|
+
* on the far side. Flattened onto the frame's current plane that command is turned aside — by
|
|
130
|
+
* **8.5 degrees early in a 90-degree crossing and more later** — and the body crawls the transfer
|
|
131
|
+
* instead of crossing it. Measured from outside on a sprint across a 90-degree convex edge:
|
|
132
|
+
* contacts decaying from four to none by tick 46, slipping at 46, falling by 51, **21 ticks of 90
|
|
133
|
+
* with nothing under it** against none when the velocity was written directly.
|
|
134
|
+
*
|
|
135
|
+
* **It is the projection and not the acceleration**, which the reporter separated before asking:
|
|
136
|
+
* the same crossing at acceleration times 15, 60, 240 and 2000 of `maxSpeed` gave 21 ticks every
|
|
137
|
+
* time. A lag would have improved.
|
|
138
|
+
*
|
|
139
|
+
* With this false the move vector is the velocity the body is steered toward *whole* — the
|
|
140
|
+
* along-up component included, because a caller that has taken responsibility for the direction
|
|
141
|
+
* has taken responsibility for all of it. Everything else still applies: the speed clamp, the
|
|
142
|
+
* acceleration and deceleration curves, `airControl` when off the ground, and gravity afterwards.
|
|
143
|
+
* That is the difference between this and the direct velocity write it exists to retire, which
|
|
144
|
+
* bypasses all four.
|
|
145
|
+
*/
|
|
146
|
+
projectMove?: boolean;
|
|
147
|
+
/** Held, not edge-triggered: the controller does its own edge detection and buffering. */
|
|
148
|
+
jump: boolean;
|
|
149
|
+
}
|
|
150
|
+
export declare class CharacterController {
|
|
151
|
+
x: number;
|
|
152
|
+
y: number;
|
|
153
|
+
z: number;
|
|
154
|
+
velX: number;
|
|
155
|
+
velY: number;
|
|
156
|
+
velZ: number;
|
|
157
|
+
state: ControllerState;
|
|
158
|
+
/** The surface normal under the feet, meaningful when grounded or sliding. */
|
|
159
|
+
groundX: number;
|
|
160
|
+
groundY: number;
|
|
161
|
+
groundZ: number;
|
|
162
|
+
/**
|
|
163
|
+
* Which way is up for this body, as a unit vector. World up unless a caller says otherwise.
|
|
164
|
+
*
|
|
165
|
+
* **The whole of this class used to assume one axis, and none of it was wrong for doing so.**
|
|
166
|
+
* `velY` was the only representation of vertical velocity, `gravity` was a scalar applied to it,
|
|
167
|
+
* `slopeCos` was measured against world Y, `stepHeight` was a height only because up was fixed,
|
|
168
|
+
* and the three states were decided by a downward probe. That is a complete and correct
|
|
169
|
+
* controller for a body on a floor, and it cannot represent one on a wall at all.
|
|
170
|
+
*
|
|
171
|
+
* A consumer needing one had to reimplement the sweep, the step-over, the slope test and the
|
|
172
|
+
* ground probe — roughly nine hundred lines that would not share this file's contact code, so
|
|
173
|
+
* the two would drift. It is not a gecko's problem: it is every wall-crawler, every
|
|
174
|
+
* arbitrary-gravity level and every walker on the inside of a rotating station.
|
|
175
|
+
*
|
|
176
|
+
* Settable per tick, because a body walking around a corner changes frame continuously and the
|
|
177
|
+
* alternative is rebuilding the controller. Read through `setUp`, which normalises, so the dot
|
|
178
|
+
* products below can assume unit length.
|
|
179
|
+
*/
|
|
180
|
+
upX: number;
|
|
181
|
+
upY: number;
|
|
182
|
+
upZ: number;
|
|
183
|
+
/**
|
|
184
|
+
* Which way this body falls, which is not always the way it stands.
|
|
185
|
+
*
|
|
186
|
+
* Tracks `up` until a caller sets it, and that default is the one that keeps every existing
|
|
187
|
+
* consumer unchanged. Kept separate because a support normal and a fall direction are different
|
|
188
|
+
* questions: a body clinging to a wall stands in the wall's frame and may still fall the way the
|
|
189
|
+
* world falls the moment it lets go.
|
|
190
|
+
*/
|
|
191
|
+
private gravityX;
|
|
192
|
+
private gravityY;
|
|
193
|
+
private gravityZ;
|
|
194
|
+
/** Whether `setUp` should carry the fall direction with it. Cleared by `setGravityDirection`. */
|
|
195
|
+
private gravityFollowsUp;
|
|
196
|
+
/** Which body is being stood on, or −1. */
|
|
197
|
+
groundBody: number;
|
|
198
|
+
readonly shape: ConvexShape;
|
|
199
|
+
/**
|
|
200
|
+
* A ball the size of the capsule's foot, swept down to find ground.
|
|
201
|
+
*
|
|
202
|
+
* **A single ray from the capsule's axis is not enough**, and the failure is specific: a character
|
|
203
|
+
* balanced on the lip of a step has its axis over open air, the ray misses, it is reported
|
|
204
|
+
* airborne, and stepping is disabled — so it stalls on the edge of the very step it was climbing.
|
|
205
|
+
* A ball finds ground under any part of the foot. It is slightly smaller than the capsule so a
|
|
206
|
+
* wall pressed against the side cannot be mistaken for a floor.
|
|
207
|
+
*/
|
|
208
|
+
readonly foot: ConvexShape;
|
|
209
|
+
readonly radius: number;
|
|
210
|
+
readonly halfHeight: number;
|
|
211
|
+
readonly stepHeight: number;
|
|
212
|
+
readonly slopeCos: number;
|
|
213
|
+
readonly maxSpeed: number;
|
|
214
|
+
readonly acceleration: number;
|
|
215
|
+
readonly deceleration: number;
|
|
216
|
+
readonly airControl: number;
|
|
217
|
+
readonly jumpSpeed: number;
|
|
218
|
+
readonly jumpCutoff: number;
|
|
219
|
+
readonly gravity: number;
|
|
220
|
+
readonly coyoteTicks: number;
|
|
221
|
+
readonly jumpBufferTicks: number;
|
|
222
|
+
readonly slideBoost: number;
|
|
223
|
+
readonly pushStrength: number;
|
|
224
|
+
readonly filter: QueryFilter | undefined;
|
|
225
|
+
/** Ticks since the character last had ground under it. */
|
|
226
|
+
private airTicks;
|
|
227
|
+
/** Ticks since a jump was pressed and not yet used. */
|
|
228
|
+
private bufferTicks;
|
|
229
|
+
private jumpHeld;
|
|
230
|
+
private rising;
|
|
231
|
+
private pose;
|
|
232
|
+
private hit;
|
|
233
|
+
/** `tryStep` needs its own, because it sweeps twice and `hit` is still being read afterwards. */
|
|
234
|
+
private stepHit;
|
|
235
|
+
private ground;
|
|
236
|
+
/** The analytic surface, where a consumer supplied one. See `GroundProbe`. */
|
|
237
|
+
private readonly probe;
|
|
238
|
+
/** Scratch for the probe, claimed once: `senseGround` runs every tick. */
|
|
239
|
+
private readonly probed;
|
|
240
|
+
constructor(options?: ControllerOptions);
|
|
241
|
+
/**
|
|
242
|
+
* Point this body's up at a direction, normalising it.
|
|
243
|
+
*
|
|
244
|
+
* A zero-length vector is refused rather than normalised into a NaN frame, because every dot
|
|
245
|
+
* product below would then answer NaN and the states would read as a body that is nowhere. It
|
|
246
|
+
* keeps the frame it had, which is the defined state the two-backends rule asks for in the
|
|
247
|
+
* renderer and is the right answer here for the same reason.
|
|
248
|
+
*/
|
|
249
|
+
setUp(x: number, y: number, z: number): void;
|
|
250
|
+
/**
|
|
251
|
+
* Point the fall direction somewhere other than up, and stop it following.
|
|
252
|
+
*
|
|
253
|
+
* The sign convention is `up`'s: `gravity` is negative, so this is the direction a *rise* goes
|
|
254
|
+
* and gravity is applied against it. That is the same arithmetic as before — `velY += gravity *
|
|
255
|
+
* dt` with a gravity of −20 is an acceleration along −Y — generalised rather than reversed.
|
|
256
|
+
*/
|
|
257
|
+
setGravityDirection(x: number, y: number, z: number): void;
|
|
258
|
+
/** This body's speed along its own up. What `velY` was when up was always world Y. */
|
|
259
|
+
get speedAlongUp(): number;
|
|
260
|
+
/** Set that component, leaving the tangent velocity exactly as it was. */
|
|
261
|
+
private setSpeedAlongUp;
|
|
262
|
+
/** Place the character, clearing whatever it was doing. */
|
|
263
|
+
teleport(x: number, y: number, z: number): void;
|
|
264
|
+
/** Advance one fixed tick. */
|
|
265
|
+
move(world: PhysicsWorld, dt: number, input: ControllerInput): void;
|
|
266
|
+
/**
|
|
267
|
+
* Let the analytic surface stop a descent, the way a body would have.
|
|
268
|
+
*
|
|
269
|
+
* **Without this a probe is a sensor and not a floor, and that was the shape of it until
|
|
270
|
+
* 2026-08-28.** `senseGround` read the surface and set `GROUNDED`, and nothing anywhere put the
|
|
271
|
+
* feet back on it: the sweep resolves bodies, the surface is not a body, so a character over a
|
|
272
|
+
* purely analytic world sank through it at gravity. It reported `GROUNDED` for three ticks while
|
|
273
|
+
* the gap stayed inside the six-centimetre tolerance, then went airborne and fell for ever —
|
|
274
|
+
* measured at ten metres under the floor after one second. Every test the seam had ran a single
|
|
275
|
+
* tick from feet already exactly on the surface, which is the one arrangement that cannot see it.
|
|
276
|
+
*
|
|
277
|
+
* A consumer with terrain therefore had to hold their own character up, which is the hundred
|
|
278
|
+
* lines they reported writing, and to write them again for anything else that stands on ground.
|
|
279
|
+
*
|
|
280
|
+
* **Only a descent this tick could have caused, which is what keeps it a floor rather than a
|
|
281
|
+
* magnet.** A character that is under the surface by more than this tick's fall is under it for
|
|
282
|
+
* some other reason — spawned in a cellar, walked into a tunnel, thrown through by an impulse —
|
|
283
|
+
* and hoisting them would be teleporting them through whatever is over their head. Rising is left
|
|
284
|
+
* alone entirely, so a jump leaves.
|
|
285
|
+
*/
|
|
286
|
+
private standOnSurface;
|
|
287
|
+
private applyFeel;
|
|
288
|
+
/**
|
|
289
|
+
* Sweep, slide along whatever was hit, and try again — a fixed number of times.
|
|
290
|
+
*
|
|
291
|
+
* A step is attempted before sliding: lift by the step height, sweep forward, and drop. A
|
|
292
|
+
* character that slid off every lip would catch on kerbs and stair nosings, which reads as the
|
|
293
|
+
* world being sticky rather than as the character being short.
|
|
294
|
+
*/
|
|
295
|
+
private sweep;
|
|
296
|
+
/**
|
|
297
|
+
* Lift, sweep forward, drop. Returns whether the whole step was taken.
|
|
298
|
+
*
|
|
299
|
+
* **The lift and the drop are along this body's up rather than along world Y**, which is what
|
|
300
|
+
* makes `stepHeight` a height rather than a Y offset. The forward part is the remainder of the
|
|
301
|
+
* move flattened into the tangent plane, for the same reason: what is left of a blocked move
|
|
302
|
+
* includes whatever of it was along up, and lifting by the step height and then also moving up
|
|
303
|
+
* would climb twice.
|
|
304
|
+
*/
|
|
305
|
+
private tryStep;
|
|
306
|
+
/**
|
|
307
|
+
* Sweep the foot ball down, and fall back to a ray when what it finds is not walkable.
|
|
308
|
+
*
|
|
309
|
+
* The ball is what lets a character stand on the lip of a step: a ray from the capsule's axis has
|
|
310
|
+
* open air beneath it there, reports airborne, and disables the stepping that was carrying it up.
|
|
311
|
+
*
|
|
312
|
+
* **But a ball also brushes the side of the ledge it is walking off**, and a wall's normal is
|
|
313
|
+
* horizontal, so the ball alone reported *sliding* — which refused the coyote-time jump the
|
|
314
|
+
* character had every right to. So a non-walkable ball result is not trusted on its own: a ray
|
|
315
|
+
* down the axis decides whether there is genuinely a steep surface underfoot, or merely a wall
|
|
316
|
+
* beside it. Two cheap queries, and each covers the other's blind spot.
|
|
317
|
+
*/
|
|
318
|
+
private senseGround;
|
|
319
|
+
/**
|
|
320
|
+
* Push a dynamic body the character walked into, so a crate is not a wall.
|
|
321
|
+
*
|
|
322
|
+
* **The push carries the character's speed, not its per-tick step.** Adding the displacement —
|
|
323
|
+
* four metres a second comes to 0.067 of a metre in a tick — is less than the impulse ground
|
|
324
|
+
* friction takes off a crate in the same tick, so the crate is pushed and immediately stopped and
|
|
325
|
+
* never moves at all. Measured: a 30 kg crate under 0.5 friction shed 0.082 a tick against 0.067
|
|
326
|
+
* put in.
|
|
327
|
+
*
|
|
328
|
+
* So it sets a *target* rather than accumulating: the body is brought up to the character's own
|
|
329
|
+
* speed into the surface, scaled by `pushStrength`, and never slowed if it is already faster.
|
|
330
|
+
* **What this gives up** is momentum transfer — a heavy crate moves as readily as a light one,
|
|
331
|
+
* because a kinematic character has no mass to trade. **What would make it wrong** is a consumer
|
|
332
|
+
* wanting mass to matter, and the answer then is a dynamic character, which §12 refuses for
|
|
333
|
+
* reasons that have nothing to do with this.
|
|
334
|
+
*/
|
|
335
|
+
private push;
|
|
336
|
+
/**
|
|
337
|
+
* The capsule at this body's position, standing along this body's up.
|
|
338
|
+
*
|
|
339
|
+
* **The quaternion used to be left at identity**, which was right while up was always world Y
|
|
340
|
+
* and is the one thing that cannot be left out once it is not: a body on a wall whose capsule is
|
|
341
|
+
* still world-vertical is swept against the wall by its side rather than its foot, so it rests a
|
|
342
|
+
* radius away instead of standing on it.
|
|
343
|
+
*
|
|
344
|
+
* The shortest arc from world Y to `up`, built directly rather than through an axis-angle,
|
|
345
|
+
* because the half-angle form is one square root. **The half turn is the case that has to be
|
|
346
|
+
* written out**: an up of exactly −Y makes the cross product zero and `w` zero with it, which
|
|
347
|
+
* normalises to NaN, and there is no shortest arc to pick because every arc is the same length.
|
|
348
|
+
* A turn about X is chosen, which is as good as any and is stable.
|
|
349
|
+
*/
|
|
350
|
+
private readPose;
|
|
351
|
+
}
|