@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,1132 @@
|
|
|
1
|
+
import { ColliderSet } from '../colliderSet.ts';
|
|
2
|
+
import type { ConvexShape } from '../shape.ts';
|
|
3
|
+
import type { Axis, Body, BodyFrame, BodyPart } from './body.ts';
|
|
4
|
+
import {
|
|
5
|
+
AXIS_X,
|
|
6
|
+
AXIS_Y,
|
|
7
|
+
AXIS_Z,
|
|
8
|
+
bodyBounds,
|
|
9
|
+
bodyFrame,
|
|
10
|
+
createBodyBounds,
|
|
11
|
+
createBodyFrame,
|
|
12
|
+
} from './body.ts';
|
|
13
|
+
import { hits } from './scratch.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The sweep: movement resolved one axis at a time against static obstacles.
|
|
17
|
+
*
|
|
18
|
+
* Exact, branch-cheap, allocation-free and deterministic, which is what lets a stored replay
|
|
19
|
+
* reproduce a run (AGENTS.md, "Determinism").
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Gap preserved between body and obstacle so faces never interpenetrate. */
|
|
23
|
+
const SKIN = 0.001;
|
|
24
|
+
/** Shrink applied to cross-axis overlap tests to avoid phantom edge catches. */
|
|
25
|
+
const SHRINK = 0.0005;
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The pair currently being swept, held at module scope so the per-axis work
|
|
29
|
+
* carries no arguments and allocates nothing. `sweepShapePair` owns this state;
|
|
30
|
+
* nothing else reads it.
|
|
31
|
+
*/
|
|
32
|
+
let swBodyShape: ConvexShape | undefined;
|
|
33
|
+
let swColShape: ConvexShape | undefined;
|
|
34
|
+
let swBody: Body;
|
|
35
|
+
let swFrame: BodyFrame;
|
|
36
|
+
let swPx = 0;
|
|
37
|
+
let swPy = 0;
|
|
38
|
+
let swPz = 0;
|
|
39
|
+
let swMinX = 0;
|
|
40
|
+
let swMinY = 0;
|
|
41
|
+
let swMinZ = 0;
|
|
42
|
+
let swMaxX = 0;
|
|
43
|
+
let swMaxY = 0;
|
|
44
|
+
let swMaxZ = 0;
|
|
45
|
+
let swTx = 0;
|
|
46
|
+
let swTy = 0;
|
|
47
|
+
let swTz = 0;
|
|
48
|
+
/** Travel window: the pair touches only while every axis window is open at once. */
|
|
49
|
+
let winEnter = 0;
|
|
50
|
+
let winExit = 0;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the axis that decided `winEnter` is a *support* contact: its plane
|
|
53
|
+
* horizontal enough to stand on, with the body arriving from above. The bound
|
|
54
|
+
* is deliberately below any floor/wall line a caller enforces (cos 70° = 0.34),
|
|
55
|
+
* so the engine never claims support on a face the game would call a wall.
|
|
56
|
+
*/
|
|
57
|
+
let winSupport = true;
|
|
58
|
+
/**
|
|
59
|
+
* The outward direction of the axis that decided `winEnter` — from the obstacle
|
|
60
|
+
* toward the body. Companion to `winSupport`, which is a boolean about the same
|
|
61
|
+
* contact; see `Body.contactNormalX`.
|
|
62
|
+
*/
|
|
63
|
+
let winNx = 0;
|
|
64
|
+
let winNy = 0;
|
|
65
|
+
let winNz = 0;
|
|
66
|
+
const SUPPORT_NY = 0.3;
|
|
67
|
+
/** Projection intervals written by the project helpers. */
|
|
68
|
+
let colLo = 0;
|
|
69
|
+
let colHi = 0;
|
|
70
|
+
let bodLo = 0;
|
|
71
|
+
let bodHi = 0;
|
|
72
|
+
|
|
73
|
+
/** Project the collider onto an axis: its shape's points, or its box analytically. */
|
|
74
|
+
function projectCollider(nx: number, ny: number, nz: number): void {
|
|
75
|
+
const shape = swColShape;
|
|
76
|
+
if (shape === undefined) {
|
|
77
|
+
const centre =
|
|
78
|
+
(swMinX + swMaxX) * 0.5 * nx + (swMinY + swMaxY) * 0.5 * ny + (swMinZ + swMaxZ) * 0.5 * nz;
|
|
79
|
+
const reach =
|
|
80
|
+
Math.abs(nx) * (swMaxX - swMinX) * 0.5 +
|
|
81
|
+
Math.abs(ny) * (swMaxY - swMinY) * 0.5 +
|
|
82
|
+
Math.abs(nz) * (swMaxZ - swMinZ) * 0.5;
|
|
83
|
+
colLo = centre - reach;
|
|
84
|
+
colHi = centre + reach;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const v = shape.vertices;
|
|
88
|
+
let lo = Infinity;
|
|
89
|
+
let hi = -Infinity;
|
|
90
|
+
for (let i = 0; i < v.length; i += 3) {
|
|
91
|
+
const d = (v[i] ?? 0) * nx + (v[i + 1] ?? 0) * ny + (v[i + 2] ?? 0) * nz;
|
|
92
|
+
if (d < lo) lo = d;
|
|
93
|
+
if (d > hi) hi = d;
|
|
94
|
+
}
|
|
95
|
+
const grow = shape.radius + discGrowth(shape, nx, ny, nz);
|
|
96
|
+
colLo = lo - grow;
|
|
97
|
+
colHi = hi + grow;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* How far a cylinder's disc rounding reaches along an axis, given in the shape's own coordinates.
|
|
102
|
+
*
|
|
103
|
+
* `sideRadius` grows a shape perpendicular to the segment through its first two points, so along a
|
|
104
|
+
* direction `n` it reaches `sideRadius * sqrt(1 - (n · axis)^2)` — the full radius across the axis
|
|
105
|
+
* and nothing along it. Zero for every shape that is not a cylinder, which is the common case and
|
|
106
|
+
* costs one compare.
|
|
107
|
+
*
|
|
108
|
+
* **This is the exact half of the cylinder's support**, and the axis list this file folds is the
|
|
109
|
+
* approximate half: the candidate axes come from the two shapes' enumerated features, and a curved
|
|
110
|
+
* side has none. Each axis tested is answered exactly; the set of axes tested is a superset of a
|
|
111
|
+
* box's and still not the whole family a cylinder's side spans. A sweep that tests more axes can
|
|
112
|
+
* only stop a body earlier, so what that costs is a kinematic body stopping slightly short of a
|
|
113
|
+
* cylinder at a glancing angle, never passing through one. `collideShapes` is the exact path and
|
|
114
|
+
* is what the dynamics use.
|
|
115
|
+
*/
|
|
116
|
+
function discGrowth(
|
|
117
|
+
shape: { readonly sideRadius: number; readonly vertices: Float32Array },
|
|
118
|
+
nx: number,
|
|
119
|
+
ny: number,
|
|
120
|
+
nz: number,
|
|
121
|
+
): number {
|
|
122
|
+
if (shape.sideRadius <= 0) return 0;
|
|
123
|
+
const v = shape.vertices;
|
|
124
|
+
const ax = (v[3] ?? 0) - (v[0] ?? 0);
|
|
125
|
+
const ay = (v[4] ?? 0) - (v[1] ?? 0);
|
|
126
|
+
const az = (v[5] ?? 0) - (v[2] ?? 0);
|
|
127
|
+
const length = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
128
|
+
if (length < 1e-9) return shape.sideRadius;
|
|
129
|
+
const along = (ax * nx + ay * ny + az * nz) / length;
|
|
130
|
+
return shape.sideRadius * Math.sqrt(Math.max(0, 1 - along * along));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Project the body's shape (or its box) onto an axis, through its frame. */
|
|
134
|
+
function projectBody(nx: number, ny: number, nz: number): void {
|
|
135
|
+
const f = swFrame;
|
|
136
|
+
const centre = swPx * nx + swPy * ny + swPz * nz;
|
|
137
|
+
const shape = swBodyShape;
|
|
138
|
+
if (shape === undefined) {
|
|
139
|
+
const reach =
|
|
140
|
+
Math.abs(f.rx * nx + f.ry * ny + f.rz * nz) * swBody.hx +
|
|
141
|
+
Math.abs(f.ux * nx + f.uy * ny + f.uz * nz) * swBody.hy +
|
|
142
|
+
Math.abs(f.fx * nx + f.fy * ny + f.fz * nz) * swBody.hz;
|
|
143
|
+
bodLo = centre - reach;
|
|
144
|
+
bodHi = centre + reach;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// The axis in the body's own coordinates, so the points stay untransformed.
|
|
148
|
+
const mx = f.rx * nx + f.ry * ny + f.rz * nz;
|
|
149
|
+
const my = f.ux * nx + f.uy * ny + f.uz * nz;
|
|
150
|
+
const mz = f.fx * nx + f.fy * ny + f.fz * nz;
|
|
151
|
+
const v = shape.vertices;
|
|
152
|
+
let lo = Infinity;
|
|
153
|
+
let hi = -Infinity;
|
|
154
|
+
for (let i = 0; i < v.length; i += 3) {
|
|
155
|
+
const d = (v[i] ?? 0) * mx + (v[i + 1] ?? 0) * my + (v[i + 2] ?? 0) * mz;
|
|
156
|
+
if (d < lo) lo = d;
|
|
157
|
+
if (d > hi) hi = d;
|
|
158
|
+
}
|
|
159
|
+
const grow = shape.radius + discGrowth(shape, mx, my, mz);
|
|
160
|
+
bodLo = centre + lo - grow;
|
|
161
|
+
bodHi = centre + hi + grow;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fold one candidate separating axis into the travel window.
|
|
166
|
+
*
|
|
167
|
+
* Both shapes project to an interval, and travel slides the body's at a known
|
|
168
|
+
* rate, so overlap on this axis is a window of travel. Returns false when the
|
|
169
|
+
* axis proves the pair can never touch — separated and not closing, or the
|
|
170
|
+
* combined window has emptied.
|
|
171
|
+
*/
|
|
172
|
+
function foldAxis(nx: number, ny: number, nz: number): boolean {
|
|
173
|
+
const len = nx * nx + ny * ny + nz * nz;
|
|
174
|
+
// Parallel cross products degenerate to nothing; the face axes cover them.
|
|
175
|
+
if (len < 1e-12) return true;
|
|
176
|
+
const inv = 1 / Math.sqrt(len);
|
|
177
|
+
nx *= inv;
|
|
178
|
+
ny *= inv;
|
|
179
|
+
nz *= inv;
|
|
180
|
+
|
|
181
|
+
projectCollider(nx, ny, nz);
|
|
182
|
+
projectBody(nx, ny, nz);
|
|
183
|
+
const rate = swTx * nx + swTy * ny + swTz * nz;
|
|
184
|
+
|
|
185
|
+
if (rate > -1e-9 && rate < 1e-9) {
|
|
186
|
+
/*
|
|
187
|
+
* Travel does not change this projection: either it always overlaps, or
|
|
188
|
+
* never. Shrunk by SKIN, so a body resting its SKIN gap away from a face —
|
|
189
|
+
* which is exactly where contact leaves it — reads as clear and slides
|
|
190
|
+
* along it instead of catching on every seam.
|
|
191
|
+
*/
|
|
192
|
+
return bodHi > colLo + SKIN && bodLo < colHi - SKIN;
|
|
193
|
+
}
|
|
194
|
+
/*
|
|
195
|
+
* Grown by SKIN: contact stops a skin's width short of the face, the same
|
|
196
|
+
* gap the axis-aligned path has always kept. Stopping *at* the face left the
|
|
197
|
+
* body touching it, and a touching body read as "already overlapping" on the
|
|
198
|
+
* next tick — the walk-out rule then waved the same move through, and a
|
|
199
|
+
* character standing on a floor fell through it one tick after landing.
|
|
200
|
+
*/
|
|
201
|
+
let s1 = (colLo - SKIN - bodHi) / rate;
|
|
202
|
+
let s2 = (colHi + SKIN - bodLo) / rate;
|
|
203
|
+
if (s1 > s2) {
|
|
204
|
+
const swap = s1;
|
|
205
|
+
s1 = s2;
|
|
206
|
+
s2 = swap;
|
|
207
|
+
}
|
|
208
|
+
if (s1 > winEnter) {
|
|
209
|
+
winEnter = s1;
|
|
210
|
+
/*
|
|
211
|
+
* Which side of this axis the body is on, at the moment of touch. Both
|
|
212
|
+
* projections slide by `rate · s1` on the body's side before comparing.
|
|
213
|
+
*
|
|
214
|
+
* This one test answers two questions. **Support** — the plane is walkably
|
|
215
|
+
* horizontal and the body arrived on top of it, rather than kissing a side, an
|
|
216
|
+
* edge or an underside. And **which way is out**, which is the same fact without
|
|
217
|
+
* the horizontality requirement: a body above the obstacle's interval escapes
|
|
218
|
+
* along `+n`, one below it along `−n`.
|
|
219
|
+
*/
|
|
220
|
+
const above = bodLo + rate * s1 >= colHi - SKIN * 4;
|
|
221
|
+
const below = bodHi + rate * s1 <= colLo + SKIN * 4;
|
|
222
|
+
if (ny >= SUPPORT_NY) winSupport = above;
|
|
223
|
+
else if (ny <= -SUPPORT_NY) winSupport = below;
|
|
224
|
+
else winSupport = false;
|
|
225
|
+
/*
|
|
226
|
+
* Neither above nor below means the body straddles the obstacle's interval on
|
|
227
|
+
* this axis, which is what "already interpenetrating" looks like from here. The
|
|
228
|
+
* reverse of the travel direction is then the honest answer: it is the one escape
|
|
229
|
+
* this sweep can actually vouch for.
|
|
230
|
+
*/
|
|
231
|
+
if (above) {
|
|
232
|
+
winNx = nx;
|
|
233
|
+
winNy = ny;
|
|
234
|
+
winNz = nz;
|
|
235
|
+
} else if (below) {
|
|
236
|
+
winNx = -nx;
|
|
237
|
+
winNy = -ny;
|
|
238
|
+
winNz = -nz;
|
|
239
|
+
} else {
|
|
240
|
+
winNx = -swTx;
|
|
241
|
+
winNy = -swTy;
|
|
242
|
+
winNz = -swTz;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (s2 < winExit) winExit = s2;
|
|
246
|
+
// No travel satisfies every axis at once, so this obstacle is never touched.
|
|
247
|
+
return winEnter <= winExit;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* How far a body — tilted, shaped, or both — may travel along one world axis
|
|
252
|
+
* before it touches an obstacle that may itself be a shape.
|
|
253
|
+
*
|
|
254
|
+
* This replaces growing bounds into a bigger axis-aligned box: a bigger box is
|
|
255
|
+
* solid where the real body is not, so it stops a character short of geometry
|
|
256
|
+
* they can see clear air beside. The volume that collides is the volume that
|
|
257
|
+
* is drawn, on both sides.
|
|
258
|
+
*
|
|
259
|
+
* Separating axes, generalised: each side contributes its face normals (world
|
|
260
|
+
* axes for a plain box, frame axes for a boxy body), and their edge directions
|
|
261
|
+
* cross-multiply for the edge-on-edge cases. A round side has no features and
|
|
262
|
+
* contributes its sweep direction and point-to-corner axes instead. The axis
|
|
263
|
+
* list is fixed per shape pair — enumerated from build-time data, never
|
|
264
|
+
* iterated to a tolerance — so two machines replay the same run bit for bit.
|
|
265
|
+
*
|
|
266
|
+
* Returns the largest travel in `[0, |delta|]` that stays clear, as a
|
|
267
|
+
* magnitude. A pair already overlapping when the move begins is let out rather
|
|
268
|
+
* than trapped, the same rule the axis-aligned path uses — a body put inside
|
|
269
|
+
* geometry by a spawn or a deck rising under it has to be able to walk out.
|
|
270
|
+
*/
|
|
271
|
+
function sweepShapePair(
|
|
272
|
+
bodyShape: ConvexShape | undefined,
|
|
273
|
+
body: Body,
|
|
274
|
+
f: BodyFrame,
|
|
275
|
+
px: number,
|
|
276
|
+
py: number,
|
|
277
|
+
pz: number,
|
|
278
|
+
colShape: ConvexShape | undefined,
|
|
279
|
+
bMinX: number,
|
|
280
|
+
bMinY: number,
|
|
281
|
+
bMinZ: number,
|
|
282
|
+
bMaxX: number,
|
|
283
|
+
bMaxY: number,
|
|
284
|
+
bMaxZ: number,
|
|
285
|
+
axis: Axis,
|
|
286
|
+
delta: number,
|
|
287
|
+
): number {
|
|
288
|
+
const want = Math.abs(delta);
|
|
289
|
+
const dir = delta < 0 ? -1 : 1;
|
|
290
|
+
swBodyShape = bodyShape;
|
|
291
|
+
swColShape = colShape;
|
|
292
|
+
swBody = body;
|
|
293
|
+
swFrame = f;
|
|
294
|
+
swPx = px;
|
|
295
|
+
swPy = py;
|
|
296
|
+
swPz = pz;
|
|
297
|
+
swMinX = bMinX;
|
|
298
|
+
swMinY = bMinY;
|
|
299
|
+
swMinZ = bMinZ;
|
|
300
|
+
swMaxX = bMaxX;
|
|
301
|
+
swMaxY = bMaxY;
|
|
302
|
+
swMaxZ = bMaxZ;
|
|
303
|
+
swTx = axis === AXIS_X ? dir : 0;
|
|
304
|
+
swTy = axis === AXIS_Y ? dir : 0;
|
|
305
|
+
swTz = axis === AXIS_Z ? dir : 0;
|
|
306
|
+
// True entry time, allowed negative: the sign is what tells a body resting
|
|
307
|
+
// against a face (entry ≈ 0, hold it there) from one genuinely inside it
|
|
308
|
+
// (entry well past, let it out).
|
|
309
|
+
winEnter = -Infinity;
|
|
310
|
+
winExit = Infinity;
|
|
311
|
+
winSupport = true;
|
|
312
|
+
winNx = 0;
|
|
313
|
+
winNy = 0;
|
|
314
|
+
winNz = 0;
|
|
315
|
+
|
|
316
|
+
// The collider's faces — world axes where it is only its box.
|
|
317
|
+
if (colShape === undefined) {
|
|
318
|
+
if (!foldAxis(1, 0, 0) || !foldAxis(0, 1, 0) || !foldAxis(0, 0, 1)) return want;
|
|
319
|
+
} else {
|
|
320
|
+
const fn = colShape.faceNormals;
|
|
321
|
+
for (let i = 0; i < fn.length; i += 3) {
|
|
322
|
+
if (!foldAxis(fn[i] ?? 0, fn[i + 1] ?? 0, fn[i + 2] ?? 0)) return want;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// The body's faces, rotated into the world — frame axes where it is only its box.
|
|
327
|
+
if (bodyShape === undefined) {
|
|
328
|
+
if (!foldAxis(f.rx, f.ry, f.rz) || !foldAxis(f.ux, f.uy, f.uz) || !foldAxis(f.fx, f.fy, f.fz)) {
|
|
329
|
+
return want;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
const fn = bodyShape.faceNormals;
|
|
333
|
+
for (let i = 0; i < fn.length; i += 3) {
|
|
334
|
+
const lx = fn[i] ?? 0;
|
|
335
|
+
const ly = fn[i + 1] ?? 0;
|
|
336
|
+
const lz = fn[i + 2] ?? 0;
|
|
337
|
+
if (
|
|
338
|
+
!foldAxis(
|
|
339
|
+
f.rx * lx + f.ux * ly + f.fx * lz,
|
|
340
|
+
f.ry * lx + f.uy * ly + f.fy * lz,
|
|
341
|
+
f.rz * lx + f.uz * ly + f.fz * lz,
|
|
342
|
+
)
|
|
343
|
+
) {
|
|
344
|
+
return want;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Edge crosses. A round side has no edges; its direction of travel stands in,
|
|
350
|
+
// which is exactly the axis a rolling contact needs.
|
|
351
|
+
const colEdges = colShape === undefined ? 3 : Math.max(1, colShape.edgeDirs.length / 3);
|
|
352
|
+
const bodyEdges = bodyShape === undefined ? 3 : Math.max(1, bodyShape.edgeDirs.length / 3);
|
|
353
|
+
for (let c = 0; c < colEdges; c++) {
|
|
354
|
+
let cx: number;
|
|
355
|
+
let cy: number;
|
|
356
|
+
let cz: number;
|
|
357
|
+
if (colShape === undefined) {
|
|
358
|
+
cx = c === 0 ? 1 : 0;
|
|
359
|
+
cy = c === 1 ? 1 : 0;
|
|
360
|
+
cz = c === 2 ? 1 : 0;
|
|
361
|
+
} else if (colShape.edgeDirs.length === 0) {
|
|
362
|
+
cx = swTx;
|
|
363
|
+
cy = swTy;
|
|
364
|
+
cz = swTz;
|
|
365
|
+
} else {
|
|
366
|
+
cx = colShape.edgeDirs[c * 3] ?? 0;
|
|
367
|
+
cy = colShape.edgeDirs[c * 3 + 1] ?? 0;
|
|
368
|
+
cz = colShape.edgeDirs[c * 3 + 2] ?? 0;
|
|
369
|
+
}
|
|
370
|
+
for (let b = 0; b < bodyEdges; b++) {
|
|
371
|
+
let ex: number;
|
|
372
|
+
let ey: number;
|
|
373
|
+
let ez: number;
|
|
374
|
+
if (bodyShape === undefined) {
|
|
375
|
+
ex = b === 0 ? f.rx : b === 1 ? f.ux : f.fx;
|
|
376
|
+
ey = b === 0 ? f.ry : b === 1 ? f.uy : f.fy;
|
|
377
|
+
ez = b === 0 ? f.rz : b === 1 ? f.uz : f.fz;
|
|
378
|
+
} else if (bodyShape.edgeDirs.length === 0) {
|
|
379
|
+
ex = swTx;
|
|
380
|
+
ey = swTy;
|
|
381
|
+
ez = swTz;
|
|
382
|
+
} else {
|
|
383
|
+
const lx = bodyShape.edgeDirs[b * 3] ?? 0;
|
|
384
|
+
const ly = bodyShape.edgeDirs[b * 3 + 1] ?? 0;
|
|
385
|
+
const lz = bodyShape.edgeDirs[b * 3 + 2] ?? 0;
|
|
386
|
+
ex = f.rx * lx + f.ux * ly + f.fx * lz;
|
|
387
|
+
ey = f.ry * lx + f.uy * ly + f.fy * lz;
|
|
388
|
+
ez = f.rz * lx + f.uz * ly + f.fz * lz;
|
|
389
|
+
}
|
|
390
|
+
if (!foldAxis(cy * ez - cz * ey, cz * ex - cx * ez, cx * ey - cy * ex)) return want;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// A round body meets corners the face axes cannot see: point-to-centre axes.
|
|
395
|
+
// A cylinder counts as round here too: its side is curved, and the corner axes below are the
|
|
396
|
+
// only ones that see a curved surface at all. More axes can only stop a body earlier.
|
|
397
|
+
const bodyRound =
|
|
398
|
+
bodyShape !== undefined && (bodyShape.faceNormals.length === 0 || bodyShape.sideRadius > 0);
|
|
399
|
+
const colRound =
|
|
400
|
+
colShape !== undefined && (colShape.faceNormals.length === 0 || colShape.sideRadius > 0);
|
|
401
|
+
if (bodyRound) {
|
|
402
|
+
if (colShape === undefined) {
|
|
403
|
+
for (let i = 0; i < 8; i++) {
|
|
404
|
+
const vx = i & 1 ? swMaxX : swMinX;
|
|
405
|
+
const vy = i & 2 ? swMaxY : swMinY;
|
|
406
|
+
const vz = i & 4 ? swMaxZ : swMinZ;
|
|
407
|
+
if (!foldAxis(vx - px, vy - py, vz - pz)) return want;
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
const v = colShape.vertices;
|
|
411
|
+
for (let i = 0; i < v.length; i += 3) {
|
|
412
|
+
if (!foldAxis((v[i] ?? 0) - px, (v[i + 1] ?? 0) - py, (v[i + 2] ?? 0) - pz)) return want;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (colRound && !foldAxis(swTx, swTy, swTz)) return want;
|
|
416
|
+
}
|
|
417
|
+
if (colRound && !bodyRound) {
|
|
418
|
+
const cx = colShape.vertices[0] ?? 0;
|
|
419
|
+
const cy = colShape.vertices[1] ?? 0;
|
|
420
|
+
const cz = colShape.vertices[2] ?? 0;
|
|
421
|
+
if (bodyShape === undefined) {
|
|
422
|
+
for (let i = 0; i < 8; i++) {
|
|
423
|
+
const sx = i & 1 ? body.hx : -body.hx;
|
|
424
|
+
const sy = i & 2 ? body.hy : -body.hy;
|
|
425
|
+
const sz = i & 4 ? body.hz : -body.hz;
|
|
426
|
+
const wx = px + f.rx * sx + f.ux * sy + f.fx * sz;
|
|
427
|
+
const wy = py + f.ry * sx + f.uy * sy + f.fy * sz;
|
|
428
|
+
const wz = pz + f.rz * sx + f.uz * sy + f.fz * sz;
|
|
429
|
+
if (!foldAxis(cx - wx, cy - wy, cz - wz)) return want;
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
const v = bodyShape.vertices;
|
|
433
|
+
for (let i = 0; i < v.length; i += 3) {
|
|
434
|
+
const lx = v[i] ?? 0;
|
|
435
|
+
const ly = v[i + 1] ?? 0;
|
|
436
|
+
const lz = v[i + 2] ?? 0;
|
|
437
|
+
const wx = px + f.rx * lx + f.ux * ly + f.fx * lz;
|
|
438
|
+
const wy = py + f.ry * lx + f.uy * ly + f.fy * lz;
|
|
439
|
+
const wz = pz + f.rz * lx + f.uz * ly + f.fz * lz;
|
|
440
|
+
if (!foldAxis(cx - wx, cy - wy, cz - wz)) return want;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Nothing constrained the travel at all.
|
|
446
|
+
if (winEnter === -Infinity) return want;
|
|
447
|
+
/*
|
|
448
|
+
* Genuinely inside when the move begins — a spawn, a deck risen underneath —
|
|
449
|
+
* is let out rather than trapped; that has always been the rule. The
|
|
450
|
+
* threshold placement is load-bearing: a body at rest sits a skin short of
|
|
451
|
+
* the face, so its next identical move computes an entry of exactly −SKIN.
|
|
452
|
+
* A threshold at −SKIN put the resting state on the boundary and let
|
|
453
|
+
* floating point decide, tick by tick, whether the floor existed — measured
|
|
454
|
+
* as the character falling through the world one tick after landing. Two skins
|
|
455
|
+
* puts rest a full skin inside the held band; only real interpenetration
|
|
456
|
+
* beyond that opens the way out.
|
|
457
|
+
*/
|
|
458
|
+
if (winEnter < -SKIN * 2) {
|
|
459
|
+
/*
|
|
460
|
+
* **Out is allowed. *Anywhere* is not, and that difference is the whole of this.**
|
|
461
|
+
*
|
|
462
|
+
* This returned the full travel, unconditionally, which reads as "a body that is already
|
|
463
|
+
* inside something is not constrained by it" — and that is exactly what it did. A character
|
|
464
|
+
* whose body had ended up inside a column could walk straight on through it and out the far
|
|
465
|
+
* side, because every move it asked for was granted in full. Reported over and over as
|
|
466
|
+
* *"the character sometimes pass though"*, and it is the same defect behind the rest of
|
|
467
|
+
* that family: a move nothing clamps leaves `contactSupport` at the `true` it starts with,
|
|
468
|
+
* so the body is told it is standing, and every backstop a consumer has is waiting for a
|
|
469
|
+
* refusal that never comes.
|
|
470
|
+
*
|
|
471
|
+
* Measured on a capture between two courtyard pillars 0.41 m apart with a body 0.70 m wide
|
|
472
|
+
* overlapping both: held forward carried it clean across their span and off the world.
|
|
473
|
+
*
|
|
474
|
+
* The overlap runs from `winEnter` to `winExit` along the direction of travel, so the body
|
|
475
|
+
* leaves in `winExit` going forward and in `-winEnter` going back. Travelling toward the
|
|
476
|
+
* nearer of the two is coming out; travelling toward the further one is going deeper. Only
|
|
477
|
+
* the first is granted, and it is granted whole, so nothing is ever sealed in: whichever
|
|
478
|
+
* way is shortest is always open.
|
|
479
|
+
*
|
|
480
|
+
* `winExit` is `Infinity` where nothing ahead bounds the overlap at all, which is not a way
|
|
481
|
+
* out and compares as one that never arrives.
|
|
482
|
+
*
|
|
483
|
+
* **Held to a body whose middle is between this obstacle's floor and its lid**, which is the
|
|
484
|
+
* same test `ejectFromSolid` is gated on and for the same reason: it separates being *in* a
|
|
485
|
+
* solid from the shallow overlaps ordinary play is full of. A collider is sunk under the
|
|
486
|
+
* surface it draws, so a character standing on one has its feet inside the box by design; a
|
|
487
|
+
* skate corner clips the flank of a slab in passing. Neither is burial, both compute an
|
|
488
|
+
* entry past the threshold above, and restricting either costs mobility where there was
|
|
489
|
+
* never a wall — measured as a character perched on a rim taking 1.77 s to come off it instead
|
|
490
|
+
* of 0.20 s, because a two-centimetre graze on a neighbouring box was being treated as a
|
|
491
|
+
* solid to be respected.
|
|
492
|
+
*/
|
|
493
|
+
/*
|
|
494
|
+
* **Horizontal travel only, and gravity is why.**
|
|
495
|
+
*
|
|
496
|
+
* "The nearer exit is the way out" is a statement about *passing through* something, and
|
|
497
|
+
* passing through is a horizontal idea here. Applied to a falling body it says the opposite
|
|
498
|
+
* of what it means: a body overlapping the flank of a tall wall by a few centimetres has its
|
|
499
|
+
* shortest exit *upward*, so descending reads as taking the long way and the fall is refused.
|
|
500
|
+
* A body pinched between two walls then hangs in clear air for ever, which is the failure
|
|
501
|
+
* this whole rule exists to end rather than one to introduce. Measured on the smallest case
|
|
502
|
+
* that shows it — two walls closer together than the body is wide — as zero metres of descent
|
|
503
|
+
* in five seconds.
|
|
504
|
+
*
|
|
505
|
+
* A body buried under something has `ejectFromSolid` for the vertical, which knows to prefer
|
|
506
|
+
* up and knows how to clear every overlap at once. Nothing needs this axis, and everything
|
|
507
|
+
* that falls needs it left alone.
|
|
508
|
+
*/
|
|
509
|
+
/*
|
|
510
|
+
* **And a ceiling counts as something you are inside, which the middle test alone cannot
|
|
511
|
+
* see.**
|
|
512
|
+
*
|
|
513
|
+
* The test above asks whether the body's *centre* is between this obstacle's floor and its
|
|
514
|
+
* lid, and for anything a body stands on or squeezes between that is exactly right. It is
|
|
515
|
+
* blind to the one arrangement where the body is under the obstacle rather than in it: a
|
|
516
|
+
* deck overhead, low enough that the character's *head* is inside it while the centre is still
|
|
517
|
+
* below the deck's floor. The gate reads false, the rule declines, and the full travel is
|
|
518
|
+
* granted — so the deck stops existing for the whole body, not merely for the head.
|
|
519
|
+
*
|
|
520
|
+
* Measured where a route crosses over itself with 1.75 m of clearance for a 1.70 m body:
|
|
521
|
+
* standing normally on the lower deck, the head is already 0.148 m inside the upper one,
|
|
522
|
+
* and from there `moveAxis` delivers a **full metre** of travel further in against **zero**
|
|
523
|
+
* resistance. What that costs is not a clipped head. It is everything downstream — the
|
|
524
|
+
* character walks on into a pocket, the rescues push them deeper because nothing pushes back,
|
|
525
|
+
* and they come to rest sealed on all six axes with 0.3 mm of headroom. Reported as *"even
|
|
526
|
+
* if the head cannot physically pass through, I'm able to go inside and get stuck"* and
|
|
527
|
+
* *"I shouldn't be able to ENTER there"*.
|
|
528
|
+
*
|
|
529
|
+
* `swMinY >= swBody.y` is the whole of it: an obstacle whose floor is at or above the body's
|
|
530
|
+
* middle is over the body, not under it. Nothing a body rests on can satisfy it, so every
|
|
531
|
+
* surface in the world is untouched — and this is reached only when the travel direction
|
|
532
|
+
* shows real interpenetration in the first place.
|
|
533
|
+
*
|
|
534
|
+
* What it grants is unchanged and is the point: the nearer exit is still open and still
|
|
535
|
+
* whole, so a body under a deck can always back out the way it came. Only going further in
|
|
536
|
+
* is refused.
|
|
537
|
+
*/
|
|
538
|
+
if (axis !== AXIS_Y && ((swBody.y > swMinY && swBody.y < swMaxY) || swMinY >= swBody.y)) {
|
|
539
|
+
return winExit <= -winEnter ? want : 0;
|
|
540
|
+
}
|
|
541
|
+
return want;
|
|
542
|
+
}
|
|
543
|
+
if (winEnter >= want) return want;
|
|
544
|
+
return winEnter > 0 ? winEnter : 0;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Scratch for `moveAxis` and `bodyBounds`; the tick may not allocate. */
|
|
548
|
+
const sweepBounds = createBodyBounds();
|
|
549
|
+
const sweepFrame = createBodyFrame();
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Move `body` along one axis by up to `delta`, clamped by the first obstacle.
|
|
553
|
+
* Returns the distance actually moved (compare with `delta` to detect a hit).
|
|
554
|
+
*/
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* How far past one span's edge a coordinate sits, as a sign: -1 below, +1 above, 0 inside.
|
|
558
|
+
*
|
|
559
|
+
* The direction that takes a body **further off** the face it is perched on the rim of, which
|
|
560
|
+
* is the one thing a caller cannot work out from a contact normal: a rim perch's normal points
|
|
561
|
+
* straight up, because up really is the way out of the clamp. Off is sideways, and only the
|
|
562
|
+
* sweep knows which sideways, because only the sweep saw the obstacle's footprint.
|
|
563
|
+
*/
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Is this column standing over the obstacle itself, rather than merely over its bounding box?
|
|
567
|
+
*
|
|
568
|
+
* **The box is the answer only for a box, and this used to give it for everything.** Support asks
|
|
569
|
+
* whether the body's own column is over the thing holding it up, and a bounding box stands in for
|
|
570
|
+
* the footprint — which is exact for an axis-aligned collider and enormously wrong for a diagonal
|
|
571
|
+
* one. A ribbon's deck is tiled with wedges whose top face is the drawn triangle; a wedge lying
|
|
572
|
+
* across the world axes has a box several times its own area, and every column inside that box was
|
|
573
|
+
* being called standing.
|
|
574
|
+
*
|
|
575
|
+
* What that produced is a walkable ledge in every cut in a deck. A body landing at the mouth of a
|
|
576
|
+
* gap catches the deck's last wedge with the edge of its shoulder, its centre a quarter of a metre
|
|
577
|
+
* out over the hole — and was told it was standing, so nothing slid it off and nothing let it
|
|
578
|
+
* fall. Measured on one route's day 23: a band about a metre wide over an authored gap where a
|
|
579
|
+
* body lands, reports grounded, and stays for ever, resting one collider give below a deck the
|
|
580
|
+
* surface correctly says is not there.
|
|
581
|
+
*
|
|
582
|
+
* For a convex volume the column hits it exactly when the point is inside the shape's shadow on
|
|
583
|
+
* the ground plane, and the faces with a horizontal normal are what bound that shadow — a wedge's
|
|
584
|
+
* sides, a slab's flanks. Faces looking up or down bound nothing there and are skipped, which is
|
|
585
|
+
* also what keeps this from rejecting the middle of a perfectly ordinary lid.
|
|
586
|
+
*
|
|
587
|
+
* Once per move rather than once per candidate: only the obstacle that actually clamped the move
|
|
588
|
+
* has any say in whether the body is standing on something.
|
|
589
|
+
*/
|
|
590
|
+
function overFootprint(
|
|
591
|
+
x: number,
|
|
592
|
+
z: number,
|
|
593
|
+
shape: ConvexShape | undefined,
|
|
594
|
+
minX: number,
|
|
595
|
+
maxX: number,
|
|
596
|
+
minZ: number,
|
|
597
|
+
maxZ: number,
|
|
598
|
+
): boolean {
|
|
599
|
+
if (shape === undefined) {
|
|
600
|
+
footprintOutX = rimOut(x, minX, maxX);
|
|
601
|
+
footprintOutZ = rimOut(z, minZ, maxZ);
|
|
602
|
+
return footprintOutX === 0 && footprintOutZ === 0;
|
|
603
|
+
}
|
|
604
|
+
const normals = shape.faceNormals;
|
|
605
|
+
const vertices = shape.vertices;
|
|
606
|
+
/* A shape's own rounding widens its shadow, and a sphere has no faces at all to bound one. */
|
|
607
|
+
const grow = shape.radius;
|
|
608
|
+
let worstPast = -Infinity;
|
|
609
|
+
footprintOutX = 0;
|
|
610
|
+
footprintOutZ = 0;
|
|
611
|
+
for (let i = 0; i < normals.length; i += 3) {
|
|
612
|
+
const nx = normals[i] ?? 0;
|
|
613
|
+
const nz = normals[i + 2] ?? 0;
|
|
614
|
+
const length = Math.sqrt(nx * nx + nz * nz);
|
|
615
|
+
/* Looking up or down: it bounds the volume, not its shadow. */
|
|
616
|
+
if (length < FOOTPRINT_MIN_HORIZONTAL) continue;
|
|
617
|
+
const ux = nx / length;
|
|
618
|
+
const uz = nz / length;
|
|
619
|
+
/*
|
|
620
|
+
* **Both ends of the line, because that is how the list is stored.**
|
|
621
|
+
*
|
|
622
|
+
* `faceNormals` holds one entry per *direction* — a normal and its opposite appear once
|
|
623
|
+
* between them — so a slab has three entries and not six. Testing only `dot <= max` bounds
|
|
624
|
+
* one side of each and leaves the other open, which is not a small error: it left the far
|
|
625
|
+
* end and one flank of every deck unbounded, so a column past them was still called standing
|
|
626
|
+
* and the ledge this exists to remove survived the fix meant to remove it.
|
|
627
|
+
*/
|
|
628
|
+
let high = -Infinity;
|
|
629
|
+
let low = Infinity;
|
|
630
|
+
for (let v = 0; v < vertices.length; v += 3) {
|
|
631
|
+
const reach = ux * (vertices[v] ?? 0) + uz * (vertices[v + 2] ?? 0);
|
|
632
|
+
if (reach > high) high = reach;
|
|
633
|
+
if (reach < low) low = reach;
|
|
634
|
+
}
|
|
635
|
+
const here = ux * x + uz * z;
|
|
636
|
+
const pastHigh = here - high - grow;
|
|
637
|
+
if (pastHigh > worstPast) {
|
|
638
|
+
/* The face the column is furthest outside is the one it fell off, so its outward normal
|
|
639
|
+
is the way further off — which is what a caller with nothing to stand on needs. */
|
|
640
|
+
worstPast = pastHigh;
|
|
641
|
+
footprintOutX = ux;
|
|
642
|
+
footprintOutZ = uz;
|
|
643
|
+
}
|
|
644
|
+
const pastLow = low - grow - here;
|
|
645
|
+
if (pastLow > worstPast) {
|
|
646
|
+
worstPast = pastLow;
|
|
647
|
+
footprintOutX = -ux;
|
|
648
|
+
footprintOutZ = -uz;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return worstPast <= 0;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Is anything at all under this column at the height the body came to rest?
|
|
656
|
+
*
|
|
657
|
+
* **`overFootprint` answers about one collider; this answers about the floor.** Support is asked
|
|
658
|
+
* of the obstacle that won the clamp, which is exact for a floor made of a single thing and wrong
|
|
659
|
+
* everywhere two colliders abut to make one. A body straddling the join is inside both, and the
|
|
660
|
+
* one that wins is not always the one its centre is over: the box *behind* it wins as readily as
|
|
661
|
+
* the box in front, and then the column is outside the winner's footprint by construction, with a
|
|
662
|
+
* rim that points back the way the body came.
|
|
663
|
+
*
|
|
664
|
+
* What that costs is not a stumble. `contactSupport` is a body's grounding, so a false refusal
|
|
665
|
+
* takes the jump, keeps re-clamping the fall, and hands the consumer a direction to push the body
|
|
666
|
+
* in — and pushing it *back over the join* re-arms the refusal on the next tick. Captured on the
|
|
667
|
+
* back edge of a terrace, where one collider ends at z = 9.4046 and the strip behind it begins:
|
|
668
|
+
* the body accelerates across the join for five ticks, is shoved back 0.06 m with its velocity
|
|
669
|
+
* replaced, and does it again for as long as the key is held. Net progress zero, on flat ground,
|
|
670
|
+
* with nothing drawn there. A jump clears it because a body in the air has no downward clamp to
|
|
671
|
+
* be refused.
|
|
672
|
+
*
|
|
673
|
+
* The height test is what keeps this from becoming "always say yes". A candidate counts as the
|
|
674
|
+
* floor the body is on only if its lid is at the height the body actually came to rest, so a
|
|
675
|
+
* lower deck below a ledge is still a drop and the rim perch this whole test exists to refuse is
|
|
676
|
+
* refused exactly as before: over a hole there is nothing to find. There is deliberately no upper
|
|
677
|
+
* bound — a lid *above* the resting height belongs to something the body is inside rather than on,
|
|
678
|
+
* which is `ejectFromSolid`'s question, and shoving such a body sideways off a rim it is not on is
|
|
679
|
+
* the wrong answer to it.
|
|
680
|
+
*
|
|
681
|
+
* Runs only where the winner already said no, so the common path pays nothing for it, and it
|
|
682
|
+
* reads the same query the sweep already made rather than making another.
|
|
683
|
+
*/
|
|
684
|
+
function columnOverFloor(body: Body, boxes: ColliderSet, count: number, restY: number): boolean {
|
|
685
|
+
const data = boxes.data;
|
|
686
|
+
for (let h = 0; h < count; h++) {
|
|
687
|
+
const index = hits[h] ?? 0;
|
|
688
|
+
const o = index * 6;
|
|
689
|
+
/* Below the feet by more than a join's worth: a floor, but not this one. */
|
|
690
|
+
if ((data[o + 4] ?? 0) < restY - FLOOR_JOIN) continue;
|
|
691
|
+
if (
|
|
692
|
+
overFootprint(
|
|
693
|
+
body.x,
|
|
694
|
+
body.z,
|
|
695
|
+
boxes.shapeAt(index),
|
|
696
|
+
data[o] ?? 0,
|
|
697
|
+
data[o + 3] ?? 0,
|
|
698
|
+
data[o + 2] ?? 0,
|
|
699
|
+
data[o + 5] ?? 0,
|
|
700
|
+
)
|
|
701
|
+
) {
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* How far below the body's feet a lid may be and still be the same floor.
|
|
710
|
+
*
|
|
711
|
+
* Two colliders authored to meet share a lid height exactly, and a body clamped by one rests a
|
|
712
|
+
* `SKIN` above it, so the whole question is decided inside a few thousandths of a metre. Four
|
|
713
|
+
* skins is room for that and for the float noise of a world built hundreds of metres from the
|
|
714
|
+
* origin, and it is far under any step a character would have to fall to.
|
|
715
|
+
*/
|
|
716
|
+
const FLOOR_JOIN = SKIN * 4;
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* How far above the sole a contact may be and still count as underfoot, as a fraction of the
|
|
720
|
+
* body's half-height.
|
|
721
|
+
*
|
|
722
|
+
* Roughly ankle height on a standing character — 0.26 m on a 1.70 m body — and expressed
|
|
723
|
+
* against the body rather than in metres, because what counts as *underfoot* is a fact about
|
|
724
|
+
* the body and not about the world. A compound character's feet and the legs immediately above
|
|
725
|
+
* them fall inside it; its chest and its head do not, and those are the ones this exists to
|
|
726
|
+
* refuse.
|
|
727
|
+
*/
|
|
728
|
+
const UNDERFOOT_BAND = 0.3;
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The lowest point of one part, along the body's own up, in body space.
|
|
732
|
+
*
|
|
733
|
+
* A shape's `vertices` are its local hull and its `radius` is rounding grown around them, so the
|
|
734
|
+
* sole of a rounded skate sits a radius below its lowest vertex. Called only on a downward move
|
|
735
|
+
* by a body made of parts: four parts of eight points on the character this was measured for.
|
|
736
|
+
*/
|
|
737
|
+
function partFloor(part: BodyPart): number {
|
|
738
|
+
const v = part.shape.vertices;
|
|
739
|
+
let lowest = Infinity;
|
|
740
|
+
for (let i = 1; i < v.length; i += 3) {
|
|
741
|
+
const y = v[i] ?? 0;
|
|
742
|
+
if (y < lowest) lowest = y;
|
|
743
|
+
}
|
|
744
|
+
return (lowest === Infinity ? 0 : lowest) + part.oy - part.shape.radius;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Which way is *off* the shape whose shadow `overFootprint` last found a column outside.
|
|
749
|
+
*
|
|
750
|
+
* Written by that call and read straight after it, in the one place both run. A pair of module
|
|
751
|
+
* scratch values rather than an out parameter, because this sits in the tick and the house rules
|
|
752
|
+
* allow no allocation there.
|
|
753
|
+
*/
|
|
754
|
+
let footprintOutX = 0;
|
|
755
|
+
let footprintOutZ = 0;
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* How much horizontal a face normal needs before it says anything about the shadow.
|
|
759
|
+
*
|
|
760
|
+
* A deck's lid is not exactly level — it banks and it climbs — so a threshold of zero would let
|
|
761
|
+
* the lid's own slight lean bound the footprint and cut a walkable surface down to a sliver of
|
|
762
|
+
* itself. A fifth is well past any lid a character can stand on (`MIN_GROUND_NORMAL_Y` is cos 70°,
|
|
763
|
+
* whose horizontal part is 0.94) and well under any side face worth bounding with.
|
|
764
|
+
*/
|
|
765
|
+
const FOOTPRINT_MIN_HORIZONTAL = 0.2;
|
|
766
|
+
|
|
767
|
+
function rimOut(centre: number, min: number, max: number): number {
|
|
768
|
+
if (centre < min) return -1;
|
|
769
|
+
if (centre > max) return 1;
|
|
770
|
+
return 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function moveAxis(body: Body, boxes: ColliderSet, axis: Axis, delta: number): number {
|
|
774
|
+
if (delta === 0) return 0;
|
|
775
|
+
|
|
776
|
+
/*
|
|
777
|
+
* The body's *oriented* bounds, not its nominal half-extents. A body lying along a
|
|
778
|
+
* banked deck occupies a taller, wider column than an upright one, and resolving it
|
|
779
|
+
* against the smaller box is what let a drawn body reach into solid geometry.
|
|
780
|
+
*/
|
|
781
|
+
const bounds = bodyBounds(body, sweepBounds);
|
|
782
|
+
const frame = bodyFrame(body, sweepFrame);
|
|
783
|
+
const minX = body.x - bounds.hx;
|
|
784
|
+
const maxX = body.x + bounds.hx;
|
|
785
|
+
const minY = body.y - bounds.hy;
|
|
786
|
+
const maxY = body.y + bounds.hy;
|
|
787
|
+
const minZ = body.z - bounds.hz;
|
|
788
|
+
const maxZ = body.z + bounds.hz;
|
|
789
|
+
|
|
790
|
+
// Sweep region: the body's box grown along the axis of travel, so nothing
|
|
791
|
+
// the move could reach is ever missed.
|
|
792
|
+
const count = boxes.query(
|
|
793
|
+
axis === AXIS_X && delta < 0 ? minX + delta : minX,
|
|
794
|
+
axis === AXIS_Y && delta < 0 ? minY + delta : minY,
|
|
795
|
+
axis === AXIS_Z && delta < 0 ? minZ + delta : minZ,
|
|
796
|
+
axis === AXIS_X && delta > 0 ? maxX + delta : maxX,
|
|
797
|
+
axis === AXIS_Y && delta > 0 ? maxY + delta : maxY,
|
|
798
|
+
axis === AXIS_Z && delta > 0 ? maxZ + delta : maxZ,
|
|
799
|
+
hits,
|
|
800
|
+
);
|
|
801
|
+
const data = boxes.data;
|
|
802
|
+
/*
|
|
803
|
+
* Where this body's soles are, in its own space: the floor of its lowest part, so a contact
|
|
804
|
+
* can be asked whether the body met it with something it stands on. Computed once here rather
|
|
805
|
+
* than per candidate, and only where the question is asked at all — a downward move by a body
|
|
806
|
+
* made of parts.
|
|
807
|
+
*/
|
|
808
|
+
const soleParts = axis === AXIS_Y && delta <= 0 ? body.parts : undefined;
|
|
809
|
+
let soleFloor = Infinity;
|
|
810
|
+
if (soleParts !== undefined) {
|
|
811
|
+
for (let p = 0; p < soleParts.length; p++) {
|
|
812
|
+
const part = soleParts[p];
|
|
813
|
+
if (part === undefined) continue;
|
|
814
|
+
const floor = partFloor(part);
|
|
815
|
+
if (floor < soleFloor) soleFloor = floor;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// Support of the contact that finally clamps the move; true when unblocked.
|
|
819
|
+
let clampSupport = true;
|
|
820
|
+
/*
|
|
821
|
+
* The obstacle that won the clamp, kept rather than judged on the spot.
|
|
822
|
+
*
|
|
823
|
+
* Whether a contact is *support* is a question about the winner alone, and asking it at every
|
|
824
|
+
* candidate meant asking it several times a move and throwing all but one answer away — which
|
|
825
|
+
* mattered less than the fact that it could only be asked of a box. Kept here, it can be asked
|
|
826
|
+
* of the real volume once. See `overFootprint`.
|
|
827
|
+
*/
|
|
828
|
+
let clampWalkable = true;
|
|
829
|
+
let clampShape: ConvexShape | undefined;
|
|
830
|
+
let clampMinX = 0;
|
|
831
|
+
let clampMaxX = 0;
|
|
832
|
+
let clampMinZ = 0;
|
|
833
|
+
let clampMaxZ = 0;
|
|
834
|
+
/*
|
|
835
|
+
* And which way is *off* it, when a downward contact is a walkable plane the body's own
|
|
836
|
+
* column is not over — the rim perch `clampSupport` exists to refuse. Zero when the refusal
|
|
837
|
+
* had another reason, or when there was no refusal. See `Body.contactRimX`.
|
|
838
|
+
*/
|
|
839
|
+
let clampRimX = 0;
|
|
840
|
+
let clampRimZ = 0;
|
|
841
|
+
// And which way is out of it. Zero until something actually clamps the move.
|
|
842
|
+
let clampNx = 0;
|
|
843
|
+
let clampNy = 0;
|
|
844
|
+
let clampNz = 0;
|
|
845
|
+
|
|
846
|
+
for (let h = 0; h < count; h++) {
|
|
847
|
+
const index = hits[h] ?? 0;
|
|
848
|
+
const o = index * 6;
|
|
849
|
+
const bMinX = data[o] ?? 0;
|
|
850
|
+
const bMinY = data[o + 1] ?? 0;
|
|
851
|
+
const bMinZ = data[o + 2] ?? 0;
|
|
852
|
+
const bMaxX = data[o + 3] ?? 0;
|
|
853
|
+
const bMaxY = data[o + 4] ?? 0;
|
|
854
|
+
const bMaxZ = data[o + 5] ?? 0;
|
|
855
|
+
const colShape = boxes.shapeAt(index);
|
|
856
|
+
|
|
857
|
+
/*
|
|
858
|
+
* Anything with a real volume on either side — a tilted body, a shaped
|
|
859
|
+
* body, a shaped obstacle — is resolved by sweeping the volumes, not by
|
|
860
|
+
* growing axis-aligned boxes around them. The bounds above are the broad
|
|
861
|
+
* phase; `sweepShapePair` is the answer, so a body is stopped by exactly
|
|
862
|
+
* the geometry it would really touch and by nothing else.
|
|
863
|
+
*/
|
|
864
|
+
if (
|
|
865
|
+
frame.tilted ||
|
|
866
|
+
colShape !== undefined ||
|
|
867
|
+
body.shape !== undefined ||
|
|
868
|
+
body.parts !== undefined
|
|
869
|
+
) {
|
|
870
|
+
let room = Math.abs(delta);
|
|
871
|
+
/*
|
|
872
|
+
* The frame rotates about the body's pivot (its feet, for a standing
|
|
873
|
+
* character), so the world position of anything local is
|
|
874
|
+
* `pos + pivot + R·(local − pivot)` — for the upright frame that is
|
|
875
|
+
* exactly `pos + local`, unchanged.
|
|
876
|
+
*/
|
|
877
|
+
const pivotY = body.pivotY ?? 0;
|
|
878
|
+
const baseX = body.x - frame.rx * 0 - frame.ux * pivotY;
|
|
879
|
+
const baseY = body.y + pivotY - frame.uy * pivotY;
|
|
880
|
+
const baseZ = body.z - frame.uz * pivotY;
|
|
881
|
+
const parts = body.parts;
|
|
882
|
+
if (parts !== undefined && parts.length > 0) {
|
|
883
|
+
// A compound body is the nearest contact any of its parts makes.
|
|
884
|
+
for (let p = 0; p < parts.length; p++) {
|
|
885
|
+
const part = parts[p];
|
|
886
|
+
if (part === undefined) continue;
|
|
887
|
+
/*
|
|
888
|
+
* **A part that is not a foot may collide, but it may not hold the body up.**
|
|
889
|
+
*
|
|
890
|
+
* Nothing here stands on its chest. A compound character caught on a deck's edge by
|
|
891
|
+
* the torso — whose box carries the arms — hangs there with its skates a full metre
|
|
892
|
+
* clear of anything, and every rule downstream believes it: the contact plane is
|
|
893
|
+
* walkable, the column is over solid ground, so the body is *standing*, and it stays
|
|
894
|
+
* standing for as long as the player leaves it. Measured at the reporter's own
|
|
895
|
+
* capture as the whole body's fall clamping at **0.0000 m** while the skates alone
|
|
896
|
+
* fall **1.1106 m**, and reported as being *"magnetized in the mid part of the
|
|
897
|
+
* body"* — with a screenshot of an arm resting on the rim, and the reading that
|
|
898
|
+
* settles it: *"it may collide but they should just slip, we don't have any hand
|
|
899
|
+
* holding feature."*
|
|
900
|
+
*
|
|
901
|
+
* So on the way *down* only the feet answer. Sideways every part still collides
|
|
902
|
+
* exactly as before — an arm cannot pass through a deck — and a fall simply carries
|
|
903
|
+
* on past whatever the upper body brushes until something is under the soles. The
|
|
904
|
+
* band is `UNDERFOOT_BAND`, which admits the legs immediately above the skates and
|
|
905
|
+
* nothing higher.
|
|
906
|
+
*
|
|
907
|
+
* Plain and single-shape bodies never reach this: `soleParts` is set only for a
|
|
908
|
+
* downward move by a body made of parts.
|
|
909
|
+
*/
|
|
910
|
+
if (soleParts !== undefined && partFloor(part) - soleFloor > body.hy * UNDERFOOT_BAND) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const px = baseX + frame.rx * part.ox + frame.ux * part.oy + frame.fx * part.oz;
|
|
914
|
+
const py = baseY + frame.ry * part.ox + frame.uy * part.oy + frame.fy * part.oz;
|
|
915
|
+
const pz = baseZ + frame.rz * part.ox + frame.uz * part.oy + frame.fz * part.oz;
|
|
916
|
+
// A part nowhere near this obstacle, even after the whole move,
|
|
917
|
+
// costs six compares instead of an axis sweep.
|
|
918
|
+
const pr = part.shape.boundRadius + Math.abs(delta);
|
|
919
|
+
if (
|
|
920
|
+
px + pr < bMinX ||
|
|
921
|
+
px - pr > bMaxX ||
|
|
922
|
+
py + pr < bMinY ||
|
|
923
|
+
py - pr > bMaxY ||
|
|
924
|
+
pz + pr < bMinZ ||
|
|
925
|
+
pz - pr > bMaxZ
|
|
926
|
+
) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const r = sweepShapePair(
|
|
930
|
+
part.shape,
|
|
931
|
+
body,
|
|
932
|
+
frame,
|
|
933
|
+
px,
|
|
934
|
+
py,
|
|
935
|
+
pz,
|
|
936
|
+
colShape,
|
|
937
|
+
bMinX,
|
|
938
|
+
bMinY,
|
|
939
|
+
bMinZ,
|
|
940
|
+
bMaxX,
|
|
941
|
+
bMaxY,
|
|
942
|
+
bMaxZ,
|
|
943
|
+
axis,
|
|
944
|
+
delta,
|
|
945
|
+
);
|
|
946
|
+
if (r < room) {
|
|
947
|
+
room = r;
|
|
948
|
+
/*
|
|
949
|
+
* Support needs the body's own column over the thing holding it,
|
|
950
|
+
* not only a walkable contact plane: a foot's corner catching the
|
|
951
|
+
* rim of a slab with the body's centre already past the edge is a
|
|
952
|
+
* tip-over, and calling it standing left characters planted in the
|
|
953
|
+
* air a stride past every pad and deck end. The obstacle's bounds
|
|
954
|
+
* stand in for its footprint — conservative for a diagonal slab,
|
|
955
|
+
* exact for the boxes this failure was measured on.
|
|
956
|
+
*/
|
|
957
|
+
clampWalkable = winSupport;
|
|
958
|
+
clampShape = colShape;
|
|
959
|
+
clampMinX = bMinX;
|
|
960
|
+
clampMaxX = bMaxX;
|
|
961
|
+
clampMinZ = bMinZ;
|
|
962
|
+
clampMaxZ = bMaxZ;
|
|
963
|
+
clampNx = winNx;
|
|
964
|
+
clampNy = winNy;
|
|
965
|
+
clampNz = winNz;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
} else {
|
|
969
|
+
const r = sweepShapePair(
|
|
970
|
+
body.shape,
|
|
971
|
+
body,
|
|
972
|
+
frame,
|
|
973
|
+
baseX,
|
|
974
|
+
baseY,
|
|
975
|
+
baseZ,
|
|
976
|
+
colShape,
|
|
977
|
+
bMinX,
|
|
978
|
+
bMinY,
|
|
979
|
+
bMinZ,
|
|
980
|
+
bMaxX,
|
|
981
|
+
bMaxY,
|
|
982
|
+
bMaxZ,
|
|
983
|
+
axis,
|
|
984
|
+
delta,
|
|
985
|
+
);
|
|
986
|
+
if (r < room) {
|
|
987
|
+
room = r;
|
|
988
|
+
clampWalkable = winSupport;
|
|
989
|
+
clampShape = colShape;
|
|
990
|
+
clampMinX = bMinX;
|
|
991
|
+
clampMaxX = bMaxX;
|
|
992
|
+
clampMinZ = bMinZ;
|
|
993
|
+
clampMaxZ = bMaxZ;
|
|
994
|
+
clampNx = winNx;
|
|
995
|
+
clampNy = winNy;
|
|
996
|
+
clampNz = winNz;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (room < Math.abs(delta)) delta = delta < 0 ? -room : room;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (axis !== AXIS_X && (maxX - SHRINK <= bMinX || minX + SHRINK >= bMaxX)) continue;
|
|
1004
|
+
if (axis !== AXIS_Y && (maxY - SHRINK <= bMinY || minY + SHRINK >= bMaxY)) continue;
|
|
1005
|
+
if (axis !== AXIS_Z && (maxZ - SHRINK <= bMinZ || minZ + SHRINK >= bMaxZ)) continue;
|
|
1006
|
+
|
|
1007
|
+
const bodyMin = axis === AXIS_X ? minX : axis === AXIS_Y ? minY : minZ;
|
|
1008
|
+
const bodyMax = axis === AXIS_X ? maxX : axis === AXIS_Y ? maxY : maxZ;
|
|
1009
|
+
const boxMin = axis === AXIS_X ? bMinX : axis === AXIS_Y ? bMinY : bMinZ;
|
|
1010
|
+
const boxMax = axis === AXIS_X ? bMaxX : axis === AXIS_Y ? bMaxY : bMaxZ;
|
|
1011
|
+
|
|
1012
|
+
if (delta > 0 && bodyMax <= boxMin + SKIN) {
|
|
1013
|
+
const room = boxMin - bodyMax - SKIN;
|
|
1014
|
+
if (room < delta) {
|
|
1015
|
+
delta = room > 0 ? room : 0;
|
|
1016
|
+
/*
|
|
1017
|
+
* On this path the clamp is always against the move axis's own face, so out
|
|
1018
|
+
* is simply back the way the body came. No separating-axis search is
|
|
1019
|
+
* involved and none is needed.
|
|
1020
|
+
*/
|
|
1021
|
+
clampNx = axis === AXIS_X ? -1 : 0;
|
|
1022
|
+
clampNy = axis === AXIS_Y ? -1 : 0;
|
|
1023
|
+
clampNz = axis === AXIS_Z ? -1 : 0;
|
|
1024
|
+
}
|
|
1025
|
+
} else if (delta < 0 && bodyMin >= boxMax - SKIN) {
|
|
1026
|
+
const room = boxMax - bodyMin + SKIN;
|
|
1027
|
+
if (room > delta) {
|
|
1028
|
+
delta = room < 0 ? room : 0;
|
|
1029
|
+
clampNx = axis === AXIS_X ? 1 : 0;
|
|
1030
|
+
clampNy = axis === AXIS_Y ? 1 : 0;
|
|
1031
|
+
clampNz = axis === AXIS_Z ? 1 : 0;
|
|
1032
|
+
/*
|
|
1033
|
+
* **The footprint this clamp will be judged against, which this path never
|
|
1034
|
+
* filled in.**
|
|
1035
|
+
*
|
|
1036
|
+
* The downward report asks `overFootprint` whether the body's column is over
|
|
1037
|
+
* whatever stopped it, and it reads these four regardless of which path won the
|
|
1038
|
+
* clamp — but only the shape sweep above ever wrote them. A body with no volume
|
|
1039
|
+
* of its own therefore had its column tested against a zero-area footprint at
|
|
1040
|
+
* the world origin: supported at exactly (0, 0) and refused everywhere else, on
|
|
1041
|
+
* a floor with nothing wrong with it, with a rim that was a compass bearing away
|
|
1042
|
+
* from the origin rather than a way off anything. Measured on a single flat
|
|
1043
|
+
* slab, 400 of 401 columns across a four-metre line.
|
|
1044
|
+
*
|
|
1045
|
+
* A plain collider's box *is* its footprint, so the four bounds are simply the
|
|
1046
|
+
* box, and `clampShape` is cleared because this winner has no shape — a set that
|
|
1047
|
+
* holds both hulls and boxes runs both paths in one move, and the shape left
|
|
1048
|
+
* behind by a candidate that lost would otherwise be used to judge this one.
|
|
1049
|
+
*/
|
|
1050
|
+
clampWalkable = true;
|
|
1051
|
+
clampShape = undefined;
|
|
1052
|
+
clampMinX = bMinX;
|
|
1053
|
+
clampMaxX = bMaxX;
|
|
1054
|
+
clampMinZ = bMinZ;
|
|
1055
|
+
clampMaxZ = bMaxZ;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (axis === AXIS_X) body.x += delta;
|
|
1061
|
+
else if (axis === AXIS_Y) body.y += delta;
|
|
1062
|
+
else body.z += delta;
|
|
1063
|
+
// Only downward moves get the report: it is the question "am I standing on
|
|
1064
|
+
// this", and nothing else asks it. The plain-AABB path clamps against the
|
|
1065
|
+
// move axis's own faces, so a block there is support by construction.
|
|
1066
|
+
if (axis === AXIS_Y && delta <= 0) {
|
|
1067
|
+
if (clampNx !== 0 || clampNy !== 0 || clampNz !== 0) {
|
|
1068
|
+
const over = overFootprint(
|
|
1069
|
+
body.x,
|
|
1070
|
+
body.z,
|
|
1071
|
+
clampShape,
|
|
1072
|
+
clampMinX,
|
|
1073
|
+
clampMaxX,
|
|
1074
|
+
clampMinZ,
|
|
1075
|
+
clampMaxZ,
|
|
1076
|
+
);
|
|
1077
|
+
/*
|
|
1078
|
+
* The way off, from the same call that decided the body is not on it — read out
|
|
1079
|
+
* now, because the floor test below asks the same question of other colliders and
|
|
1080
|
+
* each answer overwrites the last.
|
|
1081
|
+
*/
|
|
1082
|
+
const rimX = footprintOutX;
|
|
1083
|
+
const rimZ = footprintOutZ;
|
|
1084
|
+
/*
|
|
1085
|
+
* **The winner is one collider and support is a question about the floor.**
|
|
1086
|
+
*
|
|
1087
|
+
* Asking only the obstacle that clamped is exact for a floor made of one thing and
|
|
1088
|
+
* wrong wherever two colliders abut to make one: the box *behind* the body can win
|
|
1089
|
+
* the clamp while the body's centre is already over the box in front, and then the
|
|
1090
|
+
* column is outside the winner's footprint by construction, with a rim pointing
|
|
1091
|
+
* back the way the body came. Measured on two slabs meeting at a line, 34 of 401
|
|
1092
|
+
* columns across the join refused support, in one band as wide as the body and all
|
|
1093
|
+
* on the same side of the line — which is what a player feels as a wall they can
|
|
1094
|
+
* cross one way and not the other, and can jump over, since a body in the air has
|
|
1095
|
+
* no downward clamp to be refused.
|
|
1096
|
+
*
|
|
1097
|
+
* That is most of the floor in a real world rather than an edge case: a terrace
|
|
1098
|
+
* against its verge, a verge's strips against each other, every deck wedge against
|
|
1099
|
+
* the next.
|
|
1100
|
+
*
|
|
1101
|
+
* So the column is held if *anything* the move could have landed on is under it at
|
|
1102
|
+
* the height the body came to rest. The rim perch this test exists to refuse is
|
|
1103
|
+
* refused exactly as before, because over a hole there is nothing to find.
|
|
1104
|
+
*/
|
|
1105
|
+
const held = over || columnOverFloor(body, boxes, count, body.y - bounds.hy);
|
|
1106
|
+
clampSupport = clampWalkable && held;
|
|
1107
|
+
clampRimX = clampWalkable && !held ? rimX : 0;
|
|
1108
|
+
clampRimZ = clampWalkable && !held ? rimZ : 0;
|
|
1109
|
+
}
|
|
1110
|
+
body.contactSupport = clampSupport;
|
|
1111
|
+
/*
|
|
1112
|
+
* Written on the same move that decides support, and cleared when support was granted, so
|
|
1113
|
+
* a caller cannot act on a rim it left three ticks ago. See `Body.contactRimX`.
|
|
1114
|
+
*/
|
|
1115
|
+
const rimLength = Math.sqrt(clampRimX * clampRimX + clampRimZ * clampRimZ);
|
|
1116
|
+
body.contactRimX = rimLength > 0 ? clampRimX / rimLength : 0;
|
|
1117
|
+
body.contactRimZ = rimLength > 0 ? clampRimZ / rimLength : 0;
|
|
1118
|
+
}
|
|
1119
|
+
/*
|
|
1120
|
+
* The way out of whatever clamped this move, for every axis and both directions —
|
|
1121
|
+
* unlike `contactSupport`, which is a question only a downward move asks. Written
|
|
1122
|
+
* only when something actually clamped, so a caller that checks "was I blocked"
|
|
1123
|
+
* first is never handed a stale direction as though it were fresh.
|
|
1124
|
+
*/
|
|
1125
|
+
if (clampNx !== 0 || clampNy !== 0 || clampNz !== 0) {
|
|
1126
|
+
const len = Math.sqrt(clampNx * clampNx + clampNy * clampNy + clampNz * clampNz) || 1;
|
|
1127
|
+
body.contactNormalX = clampNx / len;
|
|
1128
|
+
body.contactNormalY = clampNy / len;
|
|
1129
|
+
body.contactNormalZ = clampNz / len;
|
|
1130
|
+
}
|
|
1131
|
+
return delta;
|
|
1132
|
+
}
|