@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,362 @@
|
|
|
1
|
+
import { AXIS_X, AXIS_Y, AXIS_Z, moveAxis } from './collide/index.ts';
|
|
2
|
+
import type { Axis, Body } from './collide/index.ts';
|
|
3
|
+
import type { ColliderSet } from './colliderSet.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A kinematic body that has stopped making progress: find the cheapest way out, or say it is
|
|
7
|
+
* sealed.
|
|
8
|
+
*
|
|
9
|
+
* **Nothing else in this engine does this, and every rule in it was paid for in a shipping game.**
|
|
10
|
+
* A character controller resolves a move against geometry; it has no opinion about a body that has
|
|
11
|
+
* been asking to move for half a second and going nowhere. That gap is where players get stuck,
|
|
12
|
+
* and the cases are specific enough that a consumer meeting them for the first time will get them
|
|
13
|
+
* wrong in the same order everybody does:
|
|
14
|
+
*
|
|
15
|
+
* - a lamp base that arrests a fall on a contact no rule calls support, so the body is *grounded*
|
|
16
|
+
* and cannot jump;
|
|
17
|
+
* - a notch with a ceiling a hand's width overhead, where every jump lifts three centimetres and
|
|
18
|
+
* drops back — so a per-tick displacement test never arms, while the body sits at the same
|
|
19
|
+
* coordinates for as long as the key is held;
|
|
20
|
+
* - a deck edge that carries **no collider at all**, refused from a ground query alone, so a
|
|
21
|
+
* horizontal probe reports clear air and taking it walks the body into a slab;
|
|
22
|
+
* - an analytic deck that lifts a body back every tick it is pushed down, which with a collider
|
|
23
|
+
* set that reports clear air below is a treadmill neither system is wrong about;
|
|
24
|
+
* - two columns a body will never fit between, where a held input undoes every nudge — a player
|
|
25
|
+
* who was never stuck, and who must not be killed for pressing into a wall;
|
|
26
|
+
* - and a player merely *leaning* on scenery, who is not stuck at all and for whom being carried
|
|
27
|
+
* sideways at a fixed rate with the controls doing nothing reads as the game shoving them.
|
|
28
|
+
*
|
|
29
|
+
* **Sealed is reported, never acted on.** This says there is nowhere to put the body; what that
|
|
30
|
+
* costs a player is the consumer's decision, and a capability that killed a character would be
|
|
31
|
+
* making a game's.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Nothing happened: the body is getting somewhere, is not asking, or can jump out. */
|
|
35
|
+
/**
|
|
36
|
+
* A vector's length, and **`Math.sqrt` rather than `Math.hypot`**.
|
|
37
|
+
*
|
|
38
|
+
* ECMAScript specifies `sqrt` exactly — it is one of the two operations it pins to IEEE-754 — and
|
|
39
|
+
* leaves `hypot` to the implementation, so two engines may disagree by an ulp. A solver whose
|
|
40
|
+
* replays and ghosts have to match cannot spend that, and `scripts/determinism.test.mjs` refuses it
|
|
41
|
+
* by name.
|
|
42
|
+
*
|
|
43
|
+
* What `hypot` buys and this gives up is intermediate overflow at magnitudes near `1e154` and
|
|
44
|
+
* underflow near `1e-162`. These are metres.
|
|
45
|
+
*/
|
|
46
|
+
function length(x: number, y: number, z: number): number {
|
|
47
|
+
return Math.sqrt(x * x + y * y + z * z);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ESCAPE_IDLE = 0;
|
|
51
|
+
/** The body was pushed along `axis`/`sign`, through the sweep, so geometry still decided. */
|
|
52
|
+
export const ESCAPE_PUSHED = 1;
|
|
53
|
+
/** Nothing had room, so the body was eased upward by hand. */
|
|
54
|
+
export const ESCAPE_EASED = 2;
|
|
55
|
+
/** No room in any of the six directions, and the budget is spent. The consumer decides. */
|
|
56
|
+
export const ESCAPE_SEALED = 3;
|
|
57
|
+
|
|
58
|
+
export type EscapeResult = 0 | 1 | 2 | 3;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A world's own veto over a direction the collider set says is clear.
|
|
62
|
+
*
|
|
63
|
+
* **Declared structurally, so nothing is imported in either direction** — the same shape
|
|
64
|
+
* `GroundProbe` uses on `CharacterController`, and for the same reason: a consumer with an
|
|
65
|
+
* analytic surface has a second authority about where a body may be, and a probe that consulted
|
|
66
|
+
* only the colliders would hand back directions the ordinary move refuses.
|
|
67
|
+
*
|
|
68
|
+
* Absent means the collider set is the whole world, which is every consumer that has not asked.
|
|
69
|
+
*/
|
|
70
|
+
export interface EscapeVeto {
|
|
71
|
+
/**
|
|
72
|
+
* Would the world refuse a body that had moved to `reached` along a horizontal `axis`?
|
|
73
|
+
*
|
|
74
|
+
* `reached` is the coordinate on that axis the probe arrived at.
|
|
75
|
+
*/
|
|
76
|
+
horizontal(axis: Axis, reached: number): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Would the world undo a downward escape from where the body is now?
|
|
79
|
+
*
|
|
80
|
+
* Down only. A body *above* its surface falling is the ordinary case and a surface has no
|
|
81
|
+
* opinion about it.
|
|
82
|
+
*/
|
|
83
|
+
below(): boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What the consumer knows about this tick that the geometry cannot say. */
|
|
87
|
+
export interface StallInput {
|
|
88
|
+
/** Is the body being asked to move or jump at all? Standing still is not being stuck. */
|
|
89
|
+
trying: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Did the world refuse this tick's move?
|
|
92
|
+
*
|
|
93
|
+
* Without it the arming window fits a body turning on the spot or wiggling in open ground, and
|
|
94
|
+
* both were being pushed and then killed for it.
|
|
95
|
+
*/
|
|
96
|
+
refused: boolean;
|
|
97
|
+
/** Is there something under the feet? */
|
|
98
|
+
grounded: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Was the contact that stopped the fall genuinely *support*, rather than a graze against a side,
|
|
101
|
+
* an edge or an underside?
|
|
102
|
+
*
|
|
103
|
+
* Two different facts, and conflating them is the courtyard-lamp trap: a body with no support gets
|
|
104
|
+
* no jump however solid the thing under it looks.
|
|
105
|
+
*/
|
|
106
|
+
supported: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface StallEscapeOptions {
|
|
110
|
+
/**
|
|
111
|
+
* How far from where the window opened counts as getting somewhere, metres.
|
|
112
|
+
*
|
|
113
|
+
* **A question about net progress over a window, not about this tick's displacement.** A body
|
|
114
|
+
* jitters in a notch — thirty times a per-tick threshold — while sitting at the same coordinates
|
|
115
|
+
* for as long as the key is held. Displacement is not progress, and a trap that shakes you is
|
|
116
|
+
* still a trap.
|
|
117
|
+
*/
|
|
118
|
+
progressM?: number;
|
|
119
|
+
/** How many ticks of going nowhere before the window is full. */
|
|
120
|
+
stalledTicks?: number;
|
|
121
|
+
/** How many of those the world must have been pushing back for. */
|
|
122
|
+
refusedTicks?: number;
|
|
123
|
+
/** How far each nudge moves, metres. */
|
|
124
|
+
pushM?: number;
|
|
125
|
+
/** How far the whole escape may push before the body is called sealed, metres. */
|
|
126
|
+
budgetM?: number;
|
|
127
|
+
/** How far each of the six probes reaches, metres. */
|
|
128
|
+
probeM?: number;
|
|
129
|
+
/** How much clear air overhead counts as being able to jump out, metres. */
|
|
130
|
+
headroomM?: number;
|
|
131
|
+
/**
|
|
132
|
+
* How much a vertical direction's room is worth against a horizontal one's.
|
|
133
|
+
*
|
|
134
|
+
* Under one, and by a margin. Being eased *upward* out of scenery is the most visible possible
|
|
135
|
+
* correction — it reads as levitating. Slipping sideways off the thing you are caught on reads
|
|
136
|
+
* as slipping off it, which is what actually happened. Up stays available, because a pocket
|
|
137
|
+
* sometimes leaves nothing else.
|
|
138
|
+
*/
|
|
139
|
+
verticalBias?: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class StallEscape {
|
|
143
|
+
/** Which axis the last push took, and which way along it. Read after `ESCAPE_PUSHED`. */
|
|
144
|
+
axis: Axis = AXIS_Y;
|
|
145
|
+
sign = 1;
|
|
146
|
+
|
|
147
|
+
private readonly progressM: number;
|
|
148
|
+
private readonly stalledTicks: number;
|
|
149
|
+
private readonly refusedTicksNeeded: number;
|
|
150
|
+
private readonly pushM: number;
|
|
151
|
+
private readonly budgetM: number;
|
|
152
|
+
private readonly probeM: number;
|
|
153
|
+
private readonly headroomM: number;
|
|
154
|
+
private readonly verticalBias: number;
|
|
155
|
+
|
|
156
|
+
private stalled = 0;
|
|
157
|
+
private refused = 0;
|
|
158
|
+
private spent = 0;
|
|
159
|
+
private anchorX = 0;
|
|
160
|
+
private anchorY = 0;
|
|
161
|
+
private anchorZ = 0;
|
|
162
|
+
private anchored = false;
|
|
163
|
+
private lastX = 0;
|
|
164
|
+
private lastY = 0;
|
|
165
|
+
private lastZ = 0;
|
|
166
|
+
|
|
167
|
+
constructor(options: StallEscapeOptions = {}) {
|
|
168
|
+
this.progressM = options.progressM ?? 0.5;
|
|
169
|
+
this.stalledTicks = options.stalledTicks ?? 30;
|
|
170
|
+
this.refusedTicksNeeded = options.refusedTicks ?? 20;
|
|
171
|
+
this.pushM = options.pushM ?? 0.04;
|
|
172
|
+
this.budgetM = options.budgetM ?? 4;
|
|
173
|
+
this.probeM = options.probeM ?? 0.4;
|
|
174
|
+
this.headroomM = options.headroomM ?? 0.5;
|
|
175
|
+
this.verticalBias = options.verticalBias ?? 0.35;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Back to watching, for a respawn or a restart. */
|
|
179
|
+
reset(): void {
|
|
180
|
+
this.stalled = 0;
|
|
181
|
+
this.refused = 0;
|
|
182
|
+
this.spent = 0;
|
|
183
|
+
this.anchored = false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Watch this tick, and act where the body has genuinely stopped getting anywhere.
|
|
188
|
+
*
|
|
189
|
+
* Call it last in a tick, after the move has been resolved: it reads where the body ended up.
|
|
190
|
+
*/
|
|
191
|
+
step(body: Body, colliders: ColliderSet, input: StallInput, veto?: EscapeVeto): EscapeResult {
|
|
192
|
+
const escaping = this.spent > 0;
|
|
193
|
+
|
|
194
|
+
if (!this.anchored) {
|
|
195
|
+
this.anchorX = body.x;
|
|
196
|
+
this.anchorY = body.y;
|
|
197
|
+
this.anchorZ = body.z;
|
|
198
|
+
this.lastX = body.x;
|
|
199
|
+
this.lastY = body.y;
|
|
200
|
+
this.lastZ = body.z;
|
|
201
|
+
this.anchored = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!escaping) {
|
|
205
|
+
const fromAnchor = length(
|
|
206
|
+
body.x - this.anchorX,
|
|
207
|
+
body.y - this.anchorY,
|
|
208
|
+
body.z - this.anchorZ,
|
|
209
|
+
);
|
|
210
|
+
if (!input.trying || fromAnchor > this.progressM) return this.standDown(body);
|
|
211
|
+
if (input.refused) this.refused++;
|
|
212
|
+
if (++this.stalled < this.stalledTicks) {
|
|
213
|
+
this.remember(body);
|
|
214
|
+
return ESCAPE_IDLE;
|
|
215
|
+
}
|
|
216
|
+
/* And the world was pushing back for most of it. See `refused`. */
|
|
217
|
+
if (this.refused < this.refusedTicksNeeded) return this.standDown(body);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/*
|
|
221
|
+
* Under way again — by more than the push itself. An escape clears any small threshold by
|
|
222
|
+
* construction, so measuring against one would have it read its own nudge as success and
|
|
223
|
+
* stand down on the tick after it starts.
|
|
224
|
+
*/
|
|
225
|
+
const moved = length(body.x - this.lastX, body.y - this.lastY, body.z - this.lastZ);
|
|
226
|
+
this.remember(body);
|
|
227
|
+
if (escaping && moved > this.pushM * 2) {
|
|
228
|
+
this.stalled = 0;
|
|
229
|
+
this.refused = 0;
|
|
230
|
+
this.spent = 0;
|
|
231
|
+
return ESCAPE_IDLE;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* **A body that can jump out is not stuck**, and asking here rather than at the top means the
|
|
236
|
+
* probe is paid for only on the rare tick where something is about to be pushed.
|
|
237
|
+
*/
|
|
238
|
+
if (this.canJumpOut(body, colliders, input)) return this.standDown(body);
|
|
239
|
+
|
|
240
|
+
if (this.spent >= this.budgetM) {
|
|
241
|
+
this.stalled = 0;
|
|
242
|
+
this.refused = 0;
|
|
243
|
+
this.spent = 0;
|
|
244
|
+
/*
|
|
245
|
+
* **Asked without the veto, because this question is a different one.** Above, the veto
|
|
246
|
+
* answers *which way out is worth taking*; here the answer decides whether the consumer is
|
|
247
|
+
* told there is nowhere to put the body, and a direction that exists but gets undone is not
|
|
248
|
+
* the same thing as no direction at all. Conflating them turned a body wedged under a deck,
|
|
249
|
+
* which had real room below it, into one reported as sealed for holding forward.
|
|
250
|
+
*/
|
|
251
|
+
return this.bestEscape(body, colliders, undefined) <= this.pushM
|
|
252
|
+
? ESCAPE_SEALED
|
|
253
|
+
: ESCAPE_IDLE;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const room = this.bestEscape(body, colliders, veto);
|
|
257
|
+
this.spent += this.pushM;
|
|
258
|
+
if (room <= this.pushM) {
|
|
259
|
+
/*
|
|
260
|
+
* Genuinely enclosed — no direction has room for even one push. The vertical ease by hand is
|
|
261
|
+
* the only move left, and it is what this whole mechanism was originally written for: a
|
|
262
|
+
* pocket at an intersection that no single axis can retrace.
|
|
263
|
+
*/
|
|
264
|
+
body.y += this.pushM;
|
|
265
|
+
this.remember(body);
|
|
266
|
+
return ESCAPE_EASED;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* Otherwise take the freest direction, **through the sweep** so solid geometry still decides.
|
|
271
|
+
*
|
|
272
|
+
* Which is the whole point: an earlier version *bailed* whenever any direction had room, on
|
|
273
|
+
* the reasoning that a body with room is not wedged — true, and beside the point. Both of the
|
|
274
|
+
* traps this exists for had two free directions the entire time; what neither had was a way
|
|
275
|
+
* for the *consumer's inputs* to reach one, because holding forward asks for the direction
|
|
276
|
+
* that is refused and a dead stop asks for nothing at all. A body with somewhere to go and no
|
|
277
|
+
* way to get there is stuck, and the sum over six probes cannot tell the difference.
|
|
278
|
+
*/
|
|
279
|
+
moveAxis(body, colliders, this.axis, this.sign * this.pushM);
|
|
280
|
+
this.remember(body);
|
|
281
|
+
return ESCAPE_PUSHED;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Clear the window and re-anchor where the body is now. */
|
|
285
|
+
private standDown(body: Body): EscapeResult {
|
|
286
|
+
this.stalled = 0;
|
|
287
|
+
this.refused = 0;
|
|
288
|
+
this.spent = 0;
|
|
289
|
+
this.anchorX = body.x;
|
|
290
|
+
this.anchorY = body.y;
|
|
291
|
+
this.anchorZ = body.z;
|
|
292
|
+
this.remember(body);
|
|
293
|
+
return ESCAPE_IDLE;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private remember(body: Body): void {
|
|
297
|
+
this.lastX = body.x;
|
|
298
|
+
this.lastY = body.y;
|
|
299
|
+
this.lastZ = body.z;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Is the body standing on something, with room over its head to get off it?
|
|
304
|
+
*
|
|
305
|
+
* The one question that separates a trap from a player leaning on scenery. **Grounded** and
|
|
306
|
+
* **supported** are two different facts; **headroom** is what the notch lacked.
|
|
307
|
+
*
|
|
308
|
+
* Probed through the sweep rather than assumed, and the body is put back: a question may not
|
|
309
|
+
* move anything.
|
|
310
|
+
*/
|
|
311
|
+
private canJumpOut(body: Body, colliders: ColliderSet, input: StallInput): boolean {
|
|
312
|
+
if (!input.grounded || !input.supported) return false;
|
|
313
|
+
const wasY = body.y;
|
|
314
|
+
const room = moveAxis(body, colliders, AXIS_Y, this.headroomM);
|
|
315
|
+
body.y = wasY;
|
|
316
|
+
return room >= this.headroomM;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The freest of the six axis directions, in metres of room. Writes the winner into
|
|
321
|
+
* `axis`/`sign`.
|
|
322
|
+
*
|
|
323
|
+
* Every probe is undone before the next: a question may not move anything.
|
|
324
|
+
*/
|
|
325
|
+
private bestEscape(body: Body, colliders: ColliderSet, veto: EscapeVeto | undefined): number {
|
|
326
|
+
const x = body.x;
|
|
327
|
+
const y = body.y;
|
|
328
|
+
const z = body.z;
|
|
329
|
+
let bestRoom = 0;
|
|
330
|
+
let bestScore = 0;
|
|
331
|
+
this.axis = AXIS_Y;
|
|
332
|
+
this.sign = 1;
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < 6; i++) {
|
|
335
|
+
const axis: Axis = i < 2 ? AXIS_X : i < 4 ? AXIS_Y : AXIS_Z;
|
|
336
|
+
const sign = i % 2 === 0 ? 1 : -1;
|
|
337
|
+
const room = Math.abs(moveAxis(body, colliders, axis, sign * this.probeM));
|
|
338
|
+
const reached = axis === AXIS_X ? body.x : axis === AXIS_Z ? body.z : 0;
|
|
339
|
+
body.x = x;
|
|
340
|
+
body.y = y;
|
|
341
|
+
body.z = z;
|
|
342
|
+
|
|
343
|
+
if (
|
|
344
|
+
veto !== undefined &&
|
|
345
|
+
(axis === AXIS_X || axis === AXIS_Z) &&
|
|
346
|
+
room > this.pushM &&
|
|
347
|
+
veto.horizontal(axis, reached)
|
|
348
|
+
) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (veto !== undefined && axis === AXIS_Y && sign < 0 && veto.below()) continue;
|
|
352
|
+
|
|
353
|
+
const score = axis === AXIS_Y ? room * this.verticalBias : room;
|
|
354
|
+
if (score <= bestScore) continue;
|
|
355
|
+
bestScore = score;
|
|
356
|
+
bestRoom = room;
|
|
357
|
+
this.axis = axis;
|
|
358
|
+
this.sign = sign;
|
|
359
|
+
}
|
|
360
|
+
return bestRoom;
|
|
361
|
+
}
|
|
362
|
+
}
|