@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,254 @@
|
|
|
1
|
+
import { AXIS_X, AXIS_Y, AXIS_Z, moveAxis } from './collide/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* A kinematic body that has stopped making progress: find the cheapest way out, or say it is
|
|
4
|
+
* sealed.
|
|
5
|
+
*
|
|
6
|
+
* **Nothing else in this engine does this, and every rule in it was paid for in a shipping game.**
|
|
7
|
+
* A character controller resolves a move against geometry; it has no opinion about a body that has
|
|
8
|
+
* been asking to move for half a second and going nowhere. That gap is where players get stuck,
|
|
9
|
+
* and the cases are specific enough that a consumer meeting them for the first time will get them
|
|
10
|
+
* wrong in the same order everybody does:
|
|
11
|
+
*
|
|
12
|
+
* - a lamp base that arrests a fall on a contact no rule calls support, so the body is *grounded*
|
|
13
|
+
* and cannot jump;
|
|
14
|
+
* - a notch with a ceiling a hand's width overhead, where every jump lifts three centimetres and
|
|
15
|
+
* drops back — so a per-tick displacement test never arms, while the body sits at the same
|
|
16
|
+
* coordinates for as long as the key is held;
|
|
17
|
+
* - a deck edge that carries **no collider at all**, refused from a ground query alone, so a
|
|
18
|
+
* horizontal probe reports clear air and taking it walks the body into a slab;
|
|
19
|
+
* - an analytic deck that lifts a body back every tick it is pushed down, which with a collider
|
|
20
|
+
* set that reports clear air below is a treadmill neither system is wrong about;
|
|
21
|
+
* - two columns a body will never fit between, where a held input undoes every nudge — a player
|
|
22
|
+
* who was never stuck, and who must not be killed for pressing into a wall;
|
|
23
|
+
* - and a player merely *leaning* on scenery, who is not stuck at all and for whom being carried
|
|
24
|
+
* sideways at a fixed rate with the controls doing nothing reads as the game shoving them.
|
|
25
|
+
*
|
|
26
|
+
* **Sealed is reported, never acted on.** This says there is nowhere to put the body; what that
|
|
27
|
+
* costs a player is the consumer's decision, and a capability that killed a character would be
|
|
28
|
+
* making a game's.
|
|
29
|
+
*/
|
|
30
|
+
/** Nothing happened: the body is getting somewhere, is not asking, or can jump out. */
|
|
31
|
+
/**
|
|
32
|
+
* A vector's length, and **`Math.sqrt` rather than `Math.hypot`**.
|
|
33
|
+
*
|
|
34
|
+
* ECMAScript specifies `sqrt` exactly — it is one of the two operations it pins to IEEE-754 — and
|
|
35
|
+
* leaves `hypot` to the implementation, so two engines may disagree by an ulp. A solver whose
|
|
36
|
+
* replays and ghosts have to match cannot spend that, and `scripts/determinism.test.mjs` refuses it
|
|
37
|
+
* by name.
|
|
38
|
+
*
|
|
39
|
+
* What `hypot` buys and this gives up is intermediate overflow at magnitudes near `1e154` and
|
|
40
|
+
* underflow near `1e-162`. These are metres.
|
|
41
|
+
*/
|
|
42
|
+
function length(x, y, z) {
|
|
43
|
+
return Math.sqrt(x * x + y * y + z * z);
|
|
44
|
+
}
|
|
45
|
+
export const ESCAPE_IDLE = 0;
|
|
46
|
+
/** The body was pushed along `axis`/`sign`, through the sweep, so geometry still decided. */
|
|
47
|
+
export const ESCAPE_PUSHED = 1;
|
|
48
|
+
/** Nothing had room, so the body was eased upward by hand. */
|
|
49
|
+
export const ESCAPE_EASED = 2;
|
|
50
|
+
/** No room in any of the six directions, and the budget is spent. The consumer decides. */
|
|
51
|
+
export const ESCAPE_SEALED = 3;
|
|
52
|
+
export class StallEscape {
|
|
53
|
+
/** Which axis the last push took, and which way along it. Read after `ESCAPE_PUSHED`. */
|
|
54
|
+
axis = AXIS_Y;
|
|
55
|
+
sign = 1;
|
|
56
|
+
progressM;
|
|
57
|
+
stalledTicks;
|
|
58
|
+
refusedTicksNeeded;
|
|
59
|
+
pushM;
|
|
60
|
+
budgetM;
|
|
61
|
+
probeM;
|
|
62
|
+
headroomM;
|
|
63
|
+
verticalBias;
|
|
64
|
+
stalled = 0;
|
|
65
|
+
refused = 0;
|
|
66
|
+
spent = 0;
|
|
67
|
+
anchorX = 0;
|
|
68
|
+
anchorY = 0;
|
|
69
|
+
anchorZ = 0;
|
|
70
|
+
anchored = false;
|
|
71
|
+
lastX = 0;
|
|
72
|
+
lastY = 0;
|
|
73
|
+
lastZ = 0;
|
|
74
|
+
constructor(options = {}) {
|
|
75
|
+
this.progressM = options.progressM ?? 0.5;
|
|
76
|
+
this.stalledTicks = options.stalledTicks ?? 30;
|
|
77
|
+
this.refusedTicksNeeded = options.refusedTicks ?? 20;
|
|
78
|
+
this.pushM = options.pushM ?? 0.04;
|
|
79
|
+
this.budgetM = options.budgetM ?? 4;
|
|
80
|
+
this.probeM = options.probeM ?? 0.4;
|
|
81
|
+
this.headroomM = options.headroomM ?? 0.5;
|
|
82
|
+
this.verticalBias = options.verticalBias ?? 0.35;
|
|
83
|
+
}
|
|
84
|
+
/** Back to watching, for a respawn or a restart. */
|
|
85
|
+
reset() {
|
|
86
|
+
this.stalled = 0;
|
|
87
|
+
this.refused = 0;
|
|
88
|
+
this.spent = 0;
|
|
89
|
+
this.anchored = false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Watch this tick, and act where the body has genuinely stopped getting anywhere.
|
|
93
|
+
*
|
|
94
|
+
* Call it last in a tick, after the move has been resolved: it reads where the body ended up.
|
|
95
|
+
*/
|
|
96
|
+
step(body, colliders, input, veto) {
|
|
97
|
+
const escaping = this.spent > 0;
|
|
98
|
+
if (!this.anchored) {
|
|
99
|
+
this.anchorX = body.x;
|
|
100
|
+
this.anchorY = body.y;
|
|
101
|
+
this.anchorZ = body.z;
|
|
102
|
+
this.lastX = body.x;
|
|
103
|
+
this.lastY = body.y;
|
|
104
|
+
this.lastZ = body.z;
|
|
105
|
+
this.anchored = true;
|
|
106
|
+
}
|
|
107
|
+
if (!escaping) {
|
|
108
|
+
const fromAnchor = length(body.x - this.anchorX, body.y - this.anchorY, body.z - this.anchorZ);
|
|
109
|
+
if (!input.trying || fromAnchor > this.progressM)
|
|
110
|
+
return this.standDown(body);
|
|
111
|
+
if (input.refused)
|
|
112
|
+
this.refused++;
|
|
113
|
+
if (++this.stalled < this.stalledTicks) {
|
|
114
|
+
this.remember(body);
|
|
115
|
+
return ESCAPE_IDLE;
|
|
116
|
+
}
|
|
117
|
+
/* And the world was pushing back for most of it. See `refused`. */
|
|
118
|
+
if (this.refused < this.refusedTicksNeeded)
|
|
119
|
+
return this.standDown(body);
|
|
120
|
+
}
|
|
121
|
+
/*
|
|
122
|
+
* Under way again — by more than the push itself. An escape clears any small threshold by
|
|
123
|
+
* construction, so measuring against one would have it read its own nudge as success and
|
|
124
|
+
* stand down on the tick after it starts.
|
|
125
|
+
*/
|
|
126
|
+
const moved = length(body.x - this.lastX, body.y - this.lastY, body.z - this.lastZ);
|
|
127
|
+
this.remember(body);
|
|
128
|
+
if (escaping && moved > this.pushM * 2) {
|
|
129
|
+
this.stalled = 0;
|
|
130
|
+
this.refused = 0;
|
|
131
|
+
this.spent = 0;
|
|
132
|
+
return ESCAPE_IDLE;
|
|
133
|
+
}
|
|
134
|
+
/*
|
|
135
|
+
* **A body that can jump out is not stuck**, and asking here rather than at the top means the
|
|
136
|
+
* probe is paid for only on the rare tick where something is about to be pushed.
|
|
137
|
+
*/
|
|
138
|
+
if (this.canJumpOut(body, colliders, input))
|
|
139
|
+
return this.standDown(body);
|
|
140
|
+
if (this.spent >= this.budgetM) {
|
|
141
|
+
this.stalled = 0;
|
|
142
|
+
this.refused = 0;
|
|
143
|
+
this.spent = 0;
|
|
144
|
+
/*
|
|
145
|
+
* **Asked without the veto, because this question is a different one.** Above, the veto
|
|
146
|
+
* answers *which way out is worth taking*; here the answer decides whether the consumer is
|
|
147
|
+
* told there is nowhere to put the body, and a direction that exists but gets undone is not
|
|
148
|
+
* the same thing as no direction at all. Conflating them turned a body wedged under a deck,
|
|
149
|
+
* which had real room below it, into one reported as sealed for holding forward.
|
|
150
|
+
*/
|
|
151
|
+
return this.bestEscape(body, colliders, undefined) <= this.pushM
|
|
152
|
+
? ESCAPE_SEALED
|
|
153
|
+
: ESCAPE_IDLE;
|
|
154
|
+
}
|
|
155
|
+
const room = this.bestEscape(body, colliders, veto);
|
|
156
|
+
this.spent += this.pushM;
|
|
157
|
+
if (room <= this.pushM) {
|
|
158
|
+
/*
|
|
159
|
+
* Genuinely enclosed — no direction has room for even one push. The vertical ease by hand is
|
|
160
|
+
* the only move left, and it is what this whole mechanism was originally written for: a
|
|
161
|
+
* pocket at an intersection that no single axis can retrace.
|
|
162
|
+
*/
|
|
163
|
+
body.y += this.pushM;
|
|
164
|
+
this.remember(body);
|
|
165
|
+
return ESCAPE_EASED;
|
|
166
|
+
}
|
|
167
|
+
/*
|
|
168
|
+
* Otherwise take the freest direction, **through the sweep** so solid geometry still decides.
|
|
169
|
+
*
|
|
170
|
+
* Which is the whole point: an earlier version *bailed* whenever any direction had room, on
|
|
171
|
+
* the reasoning that a body with room is not wedged — true, and beside the point. Both of the
|
|
172
|
+
* traps this exists for had two free directions the entire time; what neither had was a way
|
|
173
|
+
* for the *consumer's inputs* to reach one, because holding forward asks for the direction
|
|
174
|
+
* that is refused and a dead stop asks for nothing at all. A body with somewhere to go and no
|
|
175
|
+
* way to get there is stuck, and the sum over six probes cannot tell the difference.
|
|
176
|
+
*/
|
|
177
|
+
moveAxis(body, colliders, this.axis, this.sign * this.pushM);
|
|
178
|
+
this.remember(body);
|
|
179
|
+
return ESCAPE_PUSHED;
|
|
180
|
+
}
|
|
181
|
+
/** Clear the window and re-anchor where the body is now. */
|
|
182
|
+
standDown(body) {
|
|
183
|
+
this.stalled = 0;
|
|
184
|
+
this.refused = 0;
|
|
185
|
+
this.spent = 0;
|
|
186
|
+
this.anchorX = body.x;
|
|
187
|
+
this.anchorY = body.y;
|
|
188
|
+
this.anchorZ = body.z;
|
|
189
|
+
this.remember(body);
|
|
190
|
+
return ESCAPE_IDLE;
|
|
191
|
+
}
|
|
192
|
+
remember(body) {
|
|
193
|
+
this.lastX = body.x;
|
|
194
|
+
this.lastY = body.y;
|
|
195
|
+
this.lastZ = body.z;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Is the body standing on something, with room over its head to get off it?
|
|
199
|
+
*
|
|
200
|
+
* The one question that separates a trap from a player leaning on scenery. **Grounded** and
|
|
201
|
+
* **supported** are two different facts; **headroom** is what the notch lacked.
|
|
202
|
+
*
|
|
203
|
+
* Probed through the sweep rather than assumed, and the body is put back: a question may not
|
|
204
|
+
* move anything.
|
|
205
|
+
*/
|
|
206
|
+
canJumpOut(body, colliders, input) {
|
|
207
|
+
if (!input.grounded || !input.supported)
|
|
208
|
+
return false;
|
|
209
|
+
const wasY = body.y;
|
|
210
|
+
const room = moveAxis(body, colliders, AXIS_Y, this.headroomM);
|
|
211
|
+
body.y = wasY;
|
|
212
|
+
return room >= this.headroomM;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The freest of the six axis directions, in metres of room. Writes the winner into
|
|
216
|
+
* `axis`/`sign`.
|
|
217
|
+
*
|
|
218
|
+
* Every probe is undone before the next: a question may not move anything.
|
|
219
|
+
*/
|
|
220
|
+
bestEscape(body, colliders, veto) {
|
|
221
|
+
const x = body.x;
|
|
222
|
+
const y = body.y;
|
|
223
|
+
const z = body.z;
|
|
224
|
+
let bestRoom = 0;
|
|
225
|
+
let bestScore = 0;
|
|
226
|
+
this.axis = AXIS_Y;
|
|
227
|
+
this.sign = 1;
|
|
228
|
+
for (let i = 0; i < 6; i++) {
|
|
229
|
+
const axis = i < 2 ? AXIS_X : i < 4 ? AXIS_Y : AXIS_Z;
|
|
230
|
+
const sign = i % 2 === 0 ? 1 : -1;
|
|
231
|
+
const room = Math.abs(moveAxis(body, colliders, axis, sign * this.probeM));
|
|
232
|
+
const reached = axis === AXIS_X ? body.x : axis === AXIS_Z ? body.z : 0;
|
|
233
|
+
body.x = x;
|
|
234
|
+
body.y = y;
|
|
235
|
+
body.z = z;
|
|
236
|
+
if (veto !== undefined &&
|
|
237
|
+
(axis === AXIS_X || axis === AXIS_Z) &&
|
|
238
|
+
room > this.pushM &&
|
|
239
|
+
veto.horizontal(axis, reached)) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (veto !== undefined && axis === AXIS_Y && sign < 0 && veto.below())
|
|
243
|
+
continue;
|
|
244
|
+
const score = axis === AXIS_Y ? room * this.verticalBias : room;
|
|
245
|
+
if (score <= bestScore)
|
|
246
|
+
continue;
|
|
247
|
+
bestScore = score;
|
|
248
|
+
bestRoom = room;
|
|
249
|
+
this.axis = axis;
|
|
250
|
+
this.sign = sign;
|
|
251
|
+
}
|
|
252
|
+
return bestRoom;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Axis, Body } from './collide/index.ts';
|
|
2
|
+
import type { ColliderSet } from './colliderSet.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Moving a kinematic body horizontally, one axis at a time, through a world that has opinions.
|
|
5
|
+
*
|
|
6
|
+
* **The half a sweep does not cover.** `moveAxis` answers "what stopped me"; it has nothing to say
|
|
7
|
+
* about whether the thing that stopped you was a kerb you should have stepped over, a face the
|
|
8
|
+
* world calls solid but carries no collider for, a facet seam you clipped in mid-air, or a corner
|
|
9
|
+
* that has just trapped you because both axes refused in the same tick. Every one of those is a
|
|
10
|
+
* rule rather than a query, and every one of them is the same in any game.
|
|
11
|
+
*
|
|
12
|
+
* **Three opinions the geometry cannot hold**, taken structurally through `WorldOpinion` — the
|
|
13
|
+
* third time this package has needed that shape, after `GroundProbe` and `EscapeVeto`, and for the
|
|
14
|
+
* same reason each time: a consumer with an analytic surface knows things a collider set does not.
|
|
15
|
+
* A deck drawn as a smooth ribbon carries no boxes at all, so its *edge* is as solid as a wall and
|
|
16
|
+
* nothing in the collider set says so.
|
|
17
|
+
*
|
|
18
|
+
* **What it will not do is decide what you are standing on.** That question drags in every noun a
|
|
19
|
+
* game has — what the surface is made of, whether it burns, whether it launches you — and it stays
|
|
20
|
+
* with the consumer. This owns the move; the consumer owns the meaning.
|
|
21
|
+
*/
|
|
22
|
+
/** Anything with a planar velocity the sweep may redirect. */
|
|
23
|
+
export interface SweptState {
|
|
24
|
+
velX: number;
|
|
25
|
+
velZ: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What the world knows that its colliders do not.
|
|
29
|
+
*
|
|
30
|
+
* Declared structurally, so nothing is imported in either direction.
|
|
31
|
+
*/
|
|
32
|
+
export interface WorldOpinion {
|
|
33
|
+
/**
|
|
34
|
+
* Is there a face standing where the body has just moved to, that it can neither step onto nor
|
|
35
|
+
* pass under? `from` is where it started on `axis`.
|
|
36
|
+
*
|
|
37
|
+
* The case this exists for: a deck drawn as a smooth surface carries no collision boxes, so
|
|
38
|
+
* every one of its edges is scenery. A body walks into the side of a raised slab and keeps
|
|
39
|
+
* going, which looks exactly like clipping through the world because it is.
|
|
40
|
+
*/
|
|
41
|
+
blocked(axis: Axis, from: number): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* How far the world's own surface would rise to meet a body standing at `(x, z)` with its feet
|
|
44
|
+
* at `feetY`, or null where it has no opinion. Negative for a descent.
|
|
45
|
+
*/
|
|
46
|
+
riseTo(x: number, z: number, feetY: number): number | null;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the world vouches for its own rises *here*.
|
|
49
|
+
*
|
|
50
|
+
* The retry below overrules a collider that refused a move, on the grounds that the surface says
|
|
51
|
+
* the ground rises to meet it. That is right on ground the world authored as travelable and
|
|
52
|
+
* wrong everywhere else: a terrace wall, a station lip and a courtyard edge are real, intended walls
|
|
53
|
+
* and the hull is the only honest answer for them. A consumer with no such distinction answers
|
|
54
|
+
* the same thing every time and pays nothing.
|
|
55
|
+
*/
|
|
56
|
+
vouches(): boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface SweptStepOptions {
|
|
59
|
+
/** How high a lip may be and still be stepped over. */
|
|
60
|
+
stepHeight?: number;
|
|
61
|
+
/**
|
|
62
|
+
* How much of a shortfall counts as a graze rather than a wall, for a body in the air, metres.
|
|
63
|
+
*
|
|
64
|
+
* A facet seam is a few centimetres and mid-flight is exactly where one is most likely to be
|
|
65
|
+
* clipped in passing rather than run into. A genuine wall does not pass it: hit one square and
|
|
66
|
+
* the move is near zero regardless of how small it was.
|
|
67
|
+
*/
|
|
68
|
+
grazeM?: number;
|
|
69
|
+
/**
|
|
70
|
+
* How wide something has to be before it is scenery rather than a post, metres.
|
|
71
|
+
*
|
|
72
|
+
* A deck's own geometry spans most of a cross-section by construction, metres in at least one
|
|
73
|
+
* direction; a lamp post is fourteen centimetres. The width test alone tells them apart, and it
|
|
74
|
+
* is what keeps the vouched retry from walking a body through a real obstacle.
|
|
75
|
+
*/
|
|
76
|
+
propFootprintM?: number;
|
|
77
|
+
/** How much exploratory lift a vouched retry is worth where the surface reports no rise. */
|
|
78
|
+
facetClearM?: number;
|
|
79
|
+
/** A rise smaller than this is not a rise. Matches whatever the consumer's own snap ignores. */
|
|
80
|
+
riseSlackM?: number;
|
|
81
|
+
/**
|
|
82
|
+
* The least horizontal contact normal that counts as a wall.
|
|
83
|
+
*
|
|
84
|
+
* Below it the contact is a floor or a ceiling, and redirecting horizontal motion along one of
|
|
85
|
+
* those would let a body climb.
|
|
86
|
+
*/
|
|
87
|
+
wallNormalMin?: number;
|
|
88
|
+
/** How hard a body slides off something it is merely perched on, metres per second squared. */
|
|
89
|
+
slipAccel?: number;
|
|
90
|
+
/** The speed a slip starts at, so it is not defeated by a body pressing into the contact. */
|
|
91
|
+
slipNudge?: number;
|
|
92
|
+
/** How little movement counts as a slip having achieved nothing, metres. */
|
|
93
|
+
slipBlockedM?: number;
|
|
94
|
+
/** How long after a step up a rim is still treated as a plateau rather than a wall, seconds. */
|
|
95
|
+
stepGraceSec?: number;
|
|
96
|
+
}
|
|
97
|
+
export declare class SweptStep {
|
|
98
|
+
/**
|
|
99
|
+
* Whether the world refused a horizontal move this tick.
|
|
100
|
+
*
|
|
101
|
+
* Read by anything that wants to tell a body that is stuck from one that is going in circles —
|
|
102
|
+
* `StallEscape` is the obvious consumer, and the two were written for each other.
|
|
103
|
+
*/
|
|
104
|
+
moveRefused: boolean;
|
|
105
|
+
/** Seconds since the last successful step up. See `stepGraceSec`. */
|
|
106
|
+
sinceStepUp: number;
|
|
107
|
+
private readonly stepHeight;
|
|
108
|
+
private readonly grazeM;
|
|
109
|
+
private readonly propFootprintM;
|
|
110
|
+
private readonly facetClearM;
|
|
111
|
+
private readonly riseSlackM;
|
|
112
|
+
private readonly wallNormalMin;
|
|
113
|
+
private readonly slipAccel;
|
|
114
|
+
private readonly slipNudge;
|
|
115
|
+
private readonly slipBlockedM;
|
|
116
|
+
private readonly stepGraceSec;
|
|
117
|
+
/**
|
|
118
|
+
* Open a tick: forget last tick's refusal and age the step-up grace.
|
|
119
|
+
*
|
|
120
|
+
* **Both counters live here or they live in two places**, which is exactly the bug this seam
|
|
121
|
+
* shipped with for one commit: the consumer kept its own `sinceStepUp`, advanced it, and the
|
|
122
|
+
* engine's stayed pinned at whatever the last step set — so the grace below never expired and a
|
|
123
|
+
* body perched on a rim was told, for ever, that it had just climbed onto a plateau.
|
|
124
|
+
*/
|
|
125
|
+
beginTick(dt: number): void;
|
|
126
|
+
/**
|
|
127
|
+
* Scratch for the prop query, claimed once and sized for the worst measured.
|
|
128
|
+
*
|
|
129
|
+
* A dense cluster of geometry can return dozens of candidates — measured at 102 in one query on
|
|
130
|
+
* a wide bank — and a buffer smaller than that silently drops whatever does not fit, dropping
|
|
131
|
+
* the one post among them exactly as often as it keeps it.
|
|
132
|
+
*/
|
|
133
|
+
private readonly propProbe;
|
|
134
|
+
constructor(options?: SweptStepOptions);
|
|
135
|
+
slipOffContact(body: Body, state: SweptState, dt: number, colliders: ColliderSet): boolean;
|
|
136
|
+
moveHorizontal(body: Body, state: SweptState, axis: Axis, want: number, colliders: ColliderSet, supported: boolean, world: WorldOpinion): void;
|
|
137
|
+
/**
|
|
138
|
+
* Is a standalone prop — not the deck's own wedge geometry — actually
|
|
139
|
+
* standing anywhere along this axis's swept move? The one distinction the
|
|
140
|
+
* on-Way trust in `moveHorizontal` cannot make from the smooth surface
|
|
141
|
+
* alone: the ground query answers for the deck itself, so a lamp, a
|
|
142
|
+
* brazier's stand or an arch post planted exactly where the deck reads
|
|
143
|
+
* clear is invisible to it. Real obstacles still have to collide — a lamp
|
|
144
|
+
* or any other placed item in the path must still register an impact — so
|
|
145
|
+
* before the deck's own hull is overruled, this asks whether anything
|
|
146
|
+
* narrow enough to be a *post*
|
|
147
|
+
* rather than a *slab* occupies the path. The deck's own wedges span most
|
|
148
|
+
* of a cross-section by construction, metres in at least one horizontal
|
|
149
|
+
* direction; a lamp post is fourteen centimetres. `this.propFootprintM` sits
|
|
150
|
+
* comfortably between the two, so the width test alone tells them apart.
|
|
151
|
+
*
|
|
152
|
+
* The box spans `before` to the full `want`, not only the endpoint —
|
|
153
|
+
* `moveAxis` already stopped short of a real prop, so the prop itself sits
|
|
154
|
+
* somewhere inside that span, never exactly on the far end this checks
|
|
155
|
+
* against. A box at the endpoint alone missed it outright: placed items
|
|
156
|
+
* such as lamps were ignored and a body passed straight through them.
|
|
157
|
+
*
|
|
158
|
+
* A dense cluster of wedges nearby can return dozens of candidates —
|
|
159
|
+
* measured at 102 in one query on a wide bank — well past the buffer this
|
|
160
|
+
* used to carry, which silently dropped whatever did not fit and dropped
|
|
161
|
+
* the one lamp among them exactly as often as it kept it. `propProbe` is
|
|
162
|
+
* sized for the worst of what was actually measured, with headroom.
|
|
163
|
+
*/
|
|
164
|
+
private blockedByProp;
|
|
165
|
+
/**
|
|
166
|
+
* Is there a walkable face standing in the body's current column that the character can
|
|
167
|
+
* neither step onto nor pass under? See `moveHorizontal` for why this exists.
|
|
168
|
+
*
|
|
169
|
+
* Uses its own hit rather than `this.hit`, which `resolveSurface` owns for the whole
|
|
170
|
+
* tick and reads after both axes have moved.
|
|
171
|
+
*/
|
|
172
|
+
private tryStepUp;
|
|
173
|
+
}
|