@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.
Files changed (136) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +503 -0
  4. package/dist/bodies.d.ts +121 -0
  5. package/dist/bodies.js +298 -0
  6. package/dist/buoyancy.d.ts +46 -0
  7. package/dist/buoyancy.js +45 -0
  8. package/dist/cloth.d.ts +226 -0
  9. package/dist/cloth.js +587 -0
  10. package/dist/collide/aabb.d.ts +10 -0
  11. package/dist/collide/aabb.js +11 -0
  12. package/dist/collide/body.d.ts +202 -0
  13. package/dist/collide/body.js +142 -0
  14. package/dist/collide/eject.d.ts +39 -0
  15. package/dist/collide/eject.js +142 -0
  16. package/dist/collide/index.d.ts +18 -0
  17. package/dist/collide/index.js +5 -0
  18. package/dist/collide/scratch.d.ts +8 -0
  19. package/dist/collide/scratch.js +9 -0
  20. package/dist/collide/segment.d.ts +12 -0
  21. package/dist/collide/segment.js +109 -0
  22. package/dist/collide/sweep.d.ts +3 -0
  23. package/dist/collide/sweep.js +1036 -0
  24. package/dist/colliderSet.d.ts +239 -0
  25. package/dist/colliderSet.js +546 -0
  26. package/dist/compoundContact.d.ts +70 -0
  27. package/dist/compoundContact.js +221 -0
  28. package/dist/controller.d.ts +351 -0
  29. package/dist/controller.js +641 -0
  30. package/dist/cylinder.d.ts +78 -0
  31. package/dist/cylinder.js +294 -0
  32. package/dist/cylinderContact.d.ts +7 -0
  33. package/dist/cylinderContact.js +704 -0
  34. package/dist/decompose.d.ts +105 -0
  35. package/dist/decompose.js +979 -0
  36. package/dist/events.d.ts +43 -0
  37. package/dist/events.js +95 -0
  38. package/dist/executor.d.ts +147 -0
  39. package/dist/executor.js +173 -0
  40. package/dist/faces.d.ts +26 -0
  41. package/dist/faces.js +34 -0
  42. package/dist/fingerprint.d.ts +61 -0
  43. package/dist/fingerprint.js +139 -0
  44. package/dist/groundMotor.d.ts +34 -0
  45. package/dist/groundMotor.js +41 -0
  46. package/dist/heightfieldShape.d.ts +128 -0
  47. package/dist/heightfieldShape.js +328 -0
  48. package/dist/index.d.ts +73 -0
  49. package/dist/index.js +47 -0
  50. package/dist/island.d.ts +55 -0
  51. package/dist/island.js +227 -0
  52. package/dist/islandSolve.d.ts +70 -0
  53. package/dist/islandSolve.js +171 -0
  54. package/dist/islandWorker.d.ts +18 -0
  55. package/dist/islandWorker.js +125 -0
  56. package/dist/joints.d.ts +180 -0
  57. package/dist/joints.js +1094 -0
  58. package/dist/manifold.d.ts +89 -0
  59. package/dist/manifold.js +608 -0
  60. package/dist/mass.d.ts +53 -0
  61. package/dist/mass.js +353 -0
  62. package/dist/meshContact.d.ts +57 -0
  63. package/dist/meshContact.js +393 -0
  64. package/dist/meshShape.d.ts +76 -0
  65. package/dist/meshShape.js +175 -0
  66. package/dist/pairs.d.ts +62 -0
  67. package/dist/pairs.js +154 -0
  68. package/dist/query.d.ts +71 -0
  69. package/dist/query.js +683 -0
  70. package/dist/ragdoll.d.ts +135 -0
  71. package/dist/ragdoll.js +489 -0
  72. package/dist/shape.d.ts +177 -0
  73. package/dist/shape.js +533 -0
  74. package/dist/solver.d.ts +136 -0
  75. package/dist/solver.js +601 -0
  76. package/dist/stage.d.ts +167 -0
  77. package/dist/stage.js +350 -0
  78. package/dist/stallEscape.d.ts +141 -0
  79. package/dist/stallEscape.js +254 -0
  80. package/dist/sweptStep.d.ts +173 -0
  81. package/dist/sweptStep.js +472 -0
  82. package/dist/tree.d.ts +61 -0
  83. package/dist/tree.js +348 -0
  84. package/dist/vehicle.d.ts +154 -0
  85. package/dist/vehicle.js +343 -0
  86. package/dist/workerPool.d.ts +107 -0
  87. package/dist/workerPool.js +239 -0
  88. package/dist/workers.d.ts +37 -0
  89. package/dist/workers.js +35 -0
  90. package/dist/world.d.ts +298 -0
  91. package/dist/world.js +712 -0
  92. package/package.json +55 -0
  93. package/src/bodies.ts +369 -0
  94. package/src/buoyancy.ts +78 -0
  95. package/src/cloth.ts +734 -0
  96. package/src/collide/aabb.ts +28 -0
  97. package/src/collide/body.ts +336 -0
  98. package/src/collide/eject.ts +156 -0
  99. package/src/collide/index.ts +26 -0
  100. package/src/collide/scratch.ts +10 -0
  101. package/src/collide/segment.ts +127 -0
  102. package/src/collide/sweep.ts +1132 -0
  103. package/src/colliderSet.ts +637 -0
  104. package/src/compoundContact.ts +284 -0
  105. package/src/controller.ts +817 -0
  106. package/src/cylinder.ts +340 -0
  107. package/src/cylinderContact.ts +825 -0
  108. package/src/decompose.ts +1198 -0
  109. package/src/events.ts +108 -0
  110. package/src/executor.ts +224 -0
  111. package/src/faces.ts +37 -0
  112. package/src/fingerprint.ts +146 -0
  113. package/src/groundMotor.ts +53 -0
  114. package/src/heightfieldShape.ts +395 -0
  115. package/src/index.ts +164 -0
  116. package/src/island.ts +219 -0
  117. package/src/islandSolve.ts +218 -0
  118. package/src/islandWorker.ts +141 -0
  119. package/src/joints.ts +1367 -0
  120. package/src/manifold.ts +798 -0
  121. package/src/mass.ts +424 -0
  122. package/src/meshContact.ts +443 -0
  123. package/src/meshShape.ts +261 -0
  124. package/src/pairs.ts +169 -0
  125. package/src/query.ts +844 -0
  126. package/src/ragdoll.ts +662 -0
  127. package/src/shape.ts +656 -0
  128. package/src/solver.ts +873 -0
  129. package/src/stage.ts +406 -0
  130. package/src/stallEscape.ts +362 -0
  131. package/src/sweptStep.ts +609 -0
  132. package/src/tree.ts +366 -0
  133. package/src/vehicle.ts +489 -0
  134. package/src/workerPool.ts +283 -0
  135. package/src/workers.ts +38 -0
  136. 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
+ }