@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,472 @@
|
|
|
1
|
+
import { AXIS_X, AXIS_Y, AXIS_Z, moveAxis } from './collide/index.js';
|
|
2
|
+
/** Floating-point slack for "did the move arrive". */
|
|
3
|
+
const EPSILON = 1e-9;
|
|
4
|
+
export class SweptStep {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the world refused a horizontal move this tick.
|
|
7
|
+
*
|
|
8
|
+
* Read by anything that wants to tell a body that is stuck from one that is going in circles —
|
|
9
|
+
* `StallEscape` is the obvious consumer, and the two were written for each other.
|
|
10
|
+
*/
|
|
11
|
+
moveRefused = false;
|
|
12
|
+
/** Seconds since the last successful step up. See `stepGraceSec`. */
|
|
13
|
+
sinceStepUp = Infinity;
|
|
14
|
+
stepHeight;
|
|
15
|
+
grazeM;
|
|
16
|
+
propFootprintM;
|
|
17
|
+
facetClearM;
|
|
18
|
+
riseSlackM;
|
|
19
|
+
wallNormalMin;
|
|
20
|
+
slipAccel;
|
|
21
|
+
slipNudge;
|
|
22
|
+
slipBlockedM;
|
|
23
|
+
stepGraceSec;
|
|
24
|
+
/**
|
|
25
|
+
* Open a tick: forget last tick's refusal and age the step-up grace.
|
|
26
|
+
*
|
|
27
|
+
* **Both counters live here or they live in two places**, which is exactly the bug this seam
|
|
28
|
+
* shipped with for one commit: the consumer kept its own `sinceStepUp`, advanced it, and the
|
|
29
|
+
* engine's stayed pinned at whatever the last step set — so the grace below never expired and a
|
|
30
|
+
* body perched on a rim was told, for ever, that it had just climbed onto a plateau.
|
|
31
|
+
*/
|
|
32
|
+
beginTick(dt) {
|
|
33
|
+
this.moveRefused = false;
|
|
34
|
+
this.sinceStepUp += dt;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Scratch for the prop query, claimed once and sized for the worst measured.
|
|
38
|
+
*
|
|
39
|
+
* A dense cluster of geometry can return dozens of candidates — measured at 102 in one query on
|
|
40
|
+
* a wide bank — and a buffer smaller than that silently drops whatever does not fit, dropping
|
|
41
|
+
* the one post among them exactly as often as it keeps it.
|
|
42
|
+
*/
|
|
43
|
+
propProbe = new Int32Array(256);
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
this.stepHeight = options.stepHeight ?? 0.35;
|
|
46
|
+
this.grazeM = options.grazeM ?? 0.12;
|
|
47
|
+
this.propFootprintM = options.propFootprintM ?? 1.2;
|
|
48
|
+
this.facetClearM = options.facetClearM ?? 0.3;
|
|
49
|
+
this.riseSlackM = options.riseSlackM ?? 0.02;
|
|
50
|
+
this.wallNormalMin = options.wallNormalMin ?? 0.2;
|
|
51
|
+
this.slipAccel = options.slipAccel ?? 18;
|
|
52
|
+
this.slipNudge = options.slipNudge ?? 3.5;
|
|
53
|
+
this.slipBlockedM = options.slipBlockedM ?? 0.002;
|
|
54
|
+
this.stepGraceSec = options.stepGraceSec ?? 0.1;
|
|
55
|
+
}
|
|
56
|
+
slipOffContact(body, state, dt, colliders) {
|
|
57
|
+
let nx = body.contactNormalX ?? 0;
|
|
58
|
+
let nz = body.contactNormalZ ?? 0;
|
|
59
|
+
let len = Math.sqrt(nx * nx + nz * nz);
|
|
60
|
+
if (len <= this.wallNormalMin) {
|
|
61
|
+
/* What the character just climbed onto looks exactly like a rim until they settle on it.
|
|
62
|
+
See `this.stepGraceSec` for the plateau this turned into a wall. */
|
|
63
|
+
if (this.sinceStepUp < this.stepGraceSec)
|
|
64
|
+
return true;
|
|
65
|
+
/*
|
|
66
|
+
* **A perch on a rim has a contact normal pointing straight up, and this method used to
|
|
67
|
+
* give up on exactly that case.**
|
|
68
|
+
*
|
|
69
|
+
* The engine refuses to call a contact support unless the body's own column is over the
|
|
70
|
+
* thing holding it, which is right: a foot's corner on a nine-centimetre sliver of a slab
|
|
71
|
+
* is a tip-over, not standing. But the normal it reports is the way out of the *clamp*,
|
|
72
|
+
* and for a downward clamp on a walkable plane that is up. So the horizontal length was
|
|
73
|
+
* zero, this returned, and nothing at all pushed the body off — while `grounded` stayed
|
|
74
|
+
* false, so no jump, and the blocked fall re-clamped the vertical speed every tick, so
|
|
75
|
+
* gravity never accumulated either.
|
|
76
|
+
*
|
|
77
|
+
* Measured at the reporter's own coordinates on a courtyard pillar: **five seconds of held
|
|
78
|
+
* forward input, zero metres of descent**, the character standing in clear air on the corner
|
|
79
|
+
* of a column. Sliding along the rim did not help, because the rim runs the length of the
|
|
80
|
+
* slab: the only direction that ends it is off the edge, and the sweep is the only thing
|
|
81
|
+
* that saw which edge. It reports it now.
|
|
82
|
+
*/
|
|
83
|
+
nx = body.contactRimX ?? 0;
|
|
84
|
+
nz = body.contactRimZ ?? 0;
|
|
85
|
+
len = Math.sqrt(nx * nx + nz * nz);
|
|
86
|
+
/* No normal and no rim: the sweep has named no way off, so there is none to try and
|
|
87
|
+
nothing here can say whether one exists. Left exactly as it was found. */
|
|
88
|
+
if (len <= this.wallNormalMin)
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const ux = nx / len;
|
|
92
|
+
const uz = nz / len;
|
|
93
|
+
/*
|
|
94
|
+
* **Whatever is pushing into the contact is dropped first, and without this the rest of
|
|
95
|
+
* this method loses an argument it cannot win.**
|
|
96
|
+
*
|
|
97
|
+
* A graze grants no grounding, so the drive that runs before this is the *air* one, and
|
|
98
|
+
* `airAccel` in its Quake form is `airAccel x wishSpeed` — 1.4 times a wish of twenty is
|
|
99
|
+
* 28 m/s² pointed straight into the face. `this.slipAccel` is 6, and small on purpose:
|
|
100
|
+
* the note above says a bigger number reads as being shoved by the scenery. So holding
|
|
101
|
+
* forward beat the slip roughly five to one, and it beat gravity too, because a blocked
|
|
102
|
+
* fall re-clamps `velY` to the distance the sweep allowed on *every* tick — the pull never
|
|
103
|
+
* accumulates while the contact keeps refusing it.
|
|
104
|
+
*
|
|
105
|
+
* The result was a character standing in clear air against the flank or the underside of a
|
|
106
|
+
* slab, motionless for as long as the key was held, with nothing drawn under them.
|
|
107
|
+
* Reported from the courtyard lamps, from a pillar row and from the ribbon's own edge, and
|
|
108
|
+
* called what it is: *"like glued while I keep press W."*
|
|
109
|
+
*
|
|
110
|
+
* You cannot accelerate into something you are already touching, so the component of the
|
|
111
|
+
* horizontal velocity pointing into the contact is removed and the tangential part is
|
|
112
|
+
* kept — the same clip a solid surface would apply, applied to a contact solid enough to
|
|
113
|
+
* stop a fall. Sliding *along* the flank survives untouched, which is what keeps a character
|
|
114
|
+
* brushing a wall at speed from being stopped by it. What does not survive is holding
|
|
115
|
+
* yourself onto it.
|
|
116
|
+
*/
|
|
117
|
+
/*
|
|
118
|
+
* **The nudge goes first, because it is also the only honest test of the premise.**
|
|
119
|
+
*
|
|
120
|
+
* Everything below this takes something away from the player — the drive that pushes
|
|
121
|
+
* into the contact — on the understanding that they are hanging off an edge and about to
|
|
122
|
+
* come off it. A body that the world refuses to move is not hanging off anything, and
|
|
123
|
+
* charging it the clip anyway is how a character standing between two lamp bases ends up
|
|
124
|
+
* unable to walk out *and* unable to jump. So: try to leave first, and only pay for the
|
|
125
|
+
* slide if the slide happened.
|
|
126
|
+
*/
|
|
127
|
+
const step = this.slipNudge * dt;
|
|
128
|
+
const slidX = moveAxis(body, colliders, AXIS_X, ux * step);
|
|
129
|
+
const slidZ = moveAxis(body, colliders, AXIS_Z, uz * step);
|
|
130
|
+
if (Math.abs(slidX) + Math.abs(slidZ) < this.slipBlockedM)
|
|
131
|
+
return false;
|
|
132
|
+
const into = state.velX * ux + state.velZ * uz;
|
|
133
|
+
if (into < 0) {
|
|
134
|
+
state.velX -= ux * into;
|
|
135
|
+
state.velZ -= uz * into;
|
|
136
|
+
}
|
|
137
|
+
state.velX += ux * this.slipAccel * dt;
|
|
138
|
+
state.velZ += uz * this.slipAccel * dt;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
moveHorizontal(body, state, axis, want, colliders, supported, world) {
|
|
142
|
+
const before = axis === AXIS_X ? body.x : body.z;
|
|
143
|
+
const moved = moveAxis(body, colliders, axis, want);
|
|
144
|
+
/*
|
|
145
|
+
* A deck's *edge* is as solid as a wall, and no box says so.
|
|
146
|
+
*
|
|
147
|
+
* A ridden deck may carry no collision boxes at all — deliberately, and the reasoning
|
|
148
|
+
* is sound: a world-axis box around a banked cross-section has a
|
|
149
|
+
* flat lid at its highest corner, which is floor where nothing is drawn and a wall
|
|
150
|
+
* where the next slab's lid rises above the character's feet. So a ribbon is solid from
|
|
151
|
+
* *above* only, because that is the one question `GroundSurface` answers.
|
|
152
|
+
*
|
|
153
|
+
* Which leaves every deck edge as scenery. Where a split's branch crosses the main
|
|
154
|
+
* line it stands a little higher, and a character on the lower deck walks straight into
|
|
155
|
+
* the side of the upper slab and keeps going — reported as passing through the link
|
|
156
|
+
* outright, as though the element were not solid. The same hole is under every raised
|
|
157
|
+
* lip and every raised platform.
|
|
158
|
+
*
|
|
159
|
+
* So the surface itself is asked, once per axis, whether the place the body just moved
|
|
160
|
+
* into has a *floor standing in it*. Three bands, and the boundaries are the movement
|
|
161
|
+
* model's own numbers rather than new ones:
|
|
162
|
+
*
|
|
163
|
+
* - a top at or below `feet + stepHeight` is a step, and `tryStepUp` owns it;
|
|
164
|
+
* - a top above the character's head is a flyover, and they pass underneath;
|
|
165
|
+
* - anything between is a face they cannot climb, so it stops them.
|
|
166
|
+
*
|
|
167
|
+
* Cheap and safe on a bank, which was the worry: this tests a single tick's
|
|
168
|
+
* displacement, and even at top speed across the steepest bank the generator authors
|
|
169
|
+
* that is under 0.4 m of rise — inside `stepHeight`, so a tube cannot wall itself off.
|
|
170
|
+
*
|
|
171
|
+
* Grounded characters only. The check predates the deck being solid; for a body in the
|
|
172
|
+
* air the hulls are the honest answer now, and the surface prediction was stopping
|
|
173
|
+
* jumps in open air a stride short of the drawn face — measured on day 9 as 13 m/s
|
|
174
|
+
* dying mid-flight over a gap, the character hanging where nothing is drawn.
|
|
175
|
+
*/
|
|
176
|
+
if (supported && world.blocked(axis, before)) {
|
|
177
|
+
if (axis === AXIS_X)
|
|
178
|
+
body.x = before;
|
|
179
|
+
else
|
|
180
|
+
body.z = before;
|
|
181
|
+
if (axis === AXIS_X)
|
|
182
|
+
state.velX = 0;
|
|
183
|
+
else
|
|
184
|
+
state.velZ = 0;
|
|
185
|
+
// The world pushed back. `unwedge` is the only reader, and it is the difference
|
|
186
|
+
// between a character who is stuck and one who is merely going in circles.
|
|
187
|
+
this.moveRefused = true;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (Math.abs(moved - want) <= EPSILON)
|
|
191
|
+
return;
|
|
192
|
+
if (supported && this.tryStepUp(body, axis, want - moved, colliders, world)) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.moveRefused = true;
|
|
196
|
+
/*
|
|
197
|
+
* A graze, mid-air, is not a wall either — there is simply nothing to lift
|
|
198
|
+
* off of to find out. `tryStepUp` is a grounded character's tool; a body
|
|
199
|
+
* genuinely clear of the deck (`supported` false here means exactly that —
|
|
200
|
+
* not this tick's jump, not coyote, no floor under it at all) has no floor
|
|
201
|
+
* to step up onto, but a facet seam is still only a few centimetres, and
|
|
202
|
+
* mid-flight is exactly where one is most likely to be clipped in passing
|
|
203
|
+
* rather than run into. Measured on a low hop through a hairpin: a 1 cm
|
|
204
|
+
* shortfall — `moved` still 94% of `want` — killed a character's whole speed
|
|
205
|
+
* on the axis it grazed, well clear of the deck by the character's own
|
|
206
|
+
* reckoning. A genuine wall does not pass this: hit one square and `moved`
|
|
207
|
+
* is near zero regardless of how small `want` was that tick, so the ratio
|
|
208
|
+
* test still tells the two apart at a walking pace, not only a sprint.
|
|
209
|
+
*/
|
|
210
|
+
if (!supported &&
|
|
211
|
+
Math.abs(want - moved) <= this.grazeM &&
|
|
212
|
+
Math.abs(moved) >= Math.abs(want) * 0.5) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
/*
|
|
216
|
+
* On a vouched deck, a rise that the surface itself vouches for is worth a
|
|
217
|
+
* second attempt at the same sweep, lifted to meet it. The requirement is
|
|
218
|
+
* that a ridden surface's tilt is followed however extreme — past vertical
|
|
219
|
+
* if the deck says so, which looks wrong and is correct — but only where
|
|
220
|
+
* the surface vouches, never on ordinary geometry. A banked span wide enough to
|
|
221
|
+
* matter builds a hull tall enough that a landing a few centimetres to
|
|
222
|
+
* the wrong side of its own facet seam can refuse *both* axes outright —
|
|
223
|
+
* not a graze, a flat `moved = 0`, on a column the smooth curve answers
|
|
224
|
+
* as perfectly ordinary track. `tryStepUp` already tried this with only
|
|
225
|
+
* `stepHeight` of lift and failed; this tries again with however much
|
|
226
|
+
* the deck's own rise actually calls for, uncapped.
|
|
227
|
+
*
|
|
228
|
+
* The retry is a real sweep, not an assumption — `moveAxis` decides,
|
|
229
|
+
* exactly as it did the first time, and only a *clean* result (the full
|
|
230
|
+
* remaining distance, not merely something nonzero) counts. That is what
|
|
231
|
+
* keeps this from being a licence to walk through anything the surface
|
|
232
|
+
* merely doesn't object to: a wall built across the deck — as opposed to
|
|
233
|
+
* being part of it — has nothing under it to lift into, so a flat deck's
|
|
234
|
+
* zero-rise retry meets exactly the same wall a second time and this
|
|
235
|
+
* falls through to the ordinary response below, same as it always did.
|
|
236
|
+
* `velocity.test.ts`'s own deliberate wall caught the difference between
|
|
237
|
+
* "trust the answer" and "retry the sweep": the surface still called the
|
|
238
|
+
* column ahead ordinary track (a synthetic wall carries no surface
|
|
239
|
+
* opinion at all), and trusting that outright walked a character straight
|
|
240
|
+
* through it.
|
|
241
|
+
*
|
|
242
|
+
* `world.vouches()` is the gate that keeps this off ordinary geometry: a
|
|
243
|
+
* terrace, a raised lip, a courtyard are all real, intended walls the hull
|
|
244
|
+
* is the only honest answer for, and none of them are ridden deck.
|
|
245
|
+
*
|
|
246
|
+
* Not gated on `supported` here — `surfaceRiseTo` already carries its own,
|
|
247
|
+
* tighter version of that question (ridden, or falling and within a snap
|
|
248
|
+
* of the deck below), which is what actually matters for a low hop that
|
|
249
|
+
* clips the same tall wedge without ever losing its claim to the ground
|
|
250
|
+
* underneath it. A real jump over open water is nowhere near a surface
|
|
251
|
+
* answer either way, so it never reaches here regardless.
|
|
252
|
+
*
|
|
253
|
+
* A lift left short of what it promised is put back exactly as it found
|
|
254
|
+
* it, along with the axis — nothing here is allowed to leave the body
|
|
255
|
+
* somewhere the ordinary sweep never actually cleared. Descents are left
|
|
256
|
+
* alone, the same "rises only" rule `preLift` follows: settling onto a
|
|
257
|
+
* lower column is an ordinary fall `resolveSurface` already owns.
|
|
258
|
+
*/
|
|
259
|
+
if (world.vouches()) {
|
|
260
|
+
const targetX = axis === AXIS_X ? before + want : body.x;
|
|
261
|
+
const targetZ = axis === AXIS_Z ? before + want : body.z;
|
|
262
|
+
const rise = world.riseTo(targetX, targetZ, body.y - body.hy);
|
|
263
|
+
if (rise !== null && !this.blockedByProp(body, axis, before, want, colliders)) {
|
|
264
|
+
const savedY = body.y;
|
|
265
|
+
/*
|
|
266
|
+
* A near-flat or descending target still earns a *small* exploratory
|
|
267
|
+
* lift, capped at facet scale rather than left at the bare rise. The
|
|
268
|
+
* deck's own hull sits a `RIBBON_COLLIDER_GIVE_M` give under the
|
|
269
|
+
* curve and a `MAX_FACET_SLIP_M` ridge is normal at any bank — the
|
|
270
|
+
* ordinary sub-step budget nothing here changes — so a target the
|
|
271
|
+
* surface calls level can still be blocked by a genuine facet seam a
|
|
272
|
+
* few centimetres tall the smooth curve was never going to report as
|
|
273
|
+
* a rise. `this.facetClearM` is comfortably past both those tolerances
|
|
274
|
+
* and nowhere near tall enough to matter for a real wall — the same
|
|
275
|
+
* synthetic 12 m wall this whole retry exists to still refuse.
|
|
276
|
+
*/
|
|
277
|
+
const wanted = rise > this.riseSlackM
|
|
278
|
+
? rise - this.riseSlackM
|
|
279
|
+
: rise > -this.facetClearM
|
|
280
|
+
? this.facetClearM
|
|
281
|
+
: 0;
|
|
282
|
+
if (wanted > 0)
|
|
283
|
+
moveAxis(body, colliders, AXIS_Y, wanted);
|
|
284
|
+
const remaining = want - moved;
|
|
285
|
+
const along = moveAxis(body, colliders, axis, remaining);
|
|
286
|
+
if (Math.abs(along - remaining) <= EPSILON)
|
|
287
|
+
return;
|
|
288
|
+
// The deck's own rise did not clear it either — put both back.
|
|
289
|
+
if (axis === AXIS_X)
|
|
290
|
+
body.x = before + moved;
|
|
291
|
+
else
|
|
292
|
+
body.z = before + moved;
|
|
293
|
+
body.y = savedY;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/*
|
|
297
|
+
* A genuine wall. The move is refused — but the *velocity* is redirected along the
|
|
298
|
+
* face rather than deleted on this axis.
|
|
299
|
+
*
|
|
300
|
+
* Zeroing the axis is what turned every corner in the world into a trap. Movement
|
|
301
|
+
* resolves X and Z separately, so at any corner, edge or obliquely-met face both
|
|
302
|
+
* axes refuse in the same tick and both get zeroed: the body ends the tick pressed
|
|
303
|
+
* against the geometry with no speed, and next tick it asks for the same refused
|
|
304
|
+
* move again. Reported from two unrelated places: a character trapped with no jump
|
|
305
|
+
* available to it, able only to go back and fall.
|
|
306
|
+
*
|
|
307
|
+
* Sliding fixes it without weakening anything, because it removes only the
|
|
308
|
+
* component going *into* the surface. Met square on, the contact normal is the
|
|
309
|
+
* direction of travel and the projection takes the whole of it — a wall across the
|
|
310
|
+
* way costs exactly what it always did, which `edgeEscape.test.ts` guards with a
|
|
311
|
+
* twelve-metre slab. Met at an angle, the tangential part survives and the character
|
|
312
|
+
* skims along it, which is both what a skater would do and the escape route that
|
|
313
|
+
* was missing.
|
|
314
|
+
*
|
|
315
|
+
* Walls only, by the contact's own normal: a near-horizontal contact is a floor or
|
|
316
|
+
* a ceiling, and redirecting horizontal motion along one of those would let a body
|
|
317
|
+
* climb. `resolveSurface` owns those.
|
|
318
|
+
*/
|
|
319
|
+
const nx = body.contactNormalX ?? 0;
|
|
320
|
+
const nz = body.contactNormalZ ?? 0;
|
|
321
|
+
const acrossLen = Math.sqrt(nx * nx + nz * nz);
|
|
322
|
+
if (acrossLen > this.wallNormalMin) {
|
|
323
|
+
const ux = nx / acrossLen;
|
|
324
|
+
const uz = nz / acrossLen;
|
|
325
|
+
const into = state.velX * ux + state.velZ * uz;
|
|
326
|
+
// Negative means travelling into the face; a positive dot is already leaving it.
|
|
327
|
+
if (into < 0) {
|
|
328
|
+
state.velX -= ux * into;
|
|
329
|
+
state.velZ -= uz * into;
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (axis === AXIS_X)
|
|
334
|
+
state.velX = 0;
|
|
335
|
+
else
|
|
336
|
+
state.velZ = 0;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Is a standalone prop — not the deck's own wedge geometry — actually
|
|
340
|
+
* standing anywhere along this axis's swept move? The one distinction the
|
|
341
|
+
* on-Way trust in `moveHorizontal` cannot make from the smooth surface
|
|
342
|
+
* alone: the ground query answers for the deck itself, so a lamp, a
|
|
343
|
+
* brazier's stand or an arch post planted exactly where the deck reads
|
|
344
|
+
* clear is invisible to it. Real obstacles still have to collide — a lamp
|
|
345
|
+
* or any other placed item in the path must still register an impact — so
|
|
346
|
+
* before the deck's own hull is overruled, this asks whether anything
|
|
347
|
+
* narrow enough to be a *post*
|
|
348
|
+
* rather than a *slab* occupies the path. The deck's own wedges span most
|
|
349
|
+
* of a cross-section by construction, metres in at least one horizontal
|
|
350
|
+
* direction; a lamp post is fourteen centimetres. `this.propFootprintM` sits
|
|
351
|
+
* comfortably between the two, so the width test alone tells them apart.
|
|
352
|
+
*
|
|
353
|
+
* The box spans `before` to the full `want`, not only the endpoint —
|
|
354
|
+
* `moveAxis` already stopped short of a real prop, so the prop itself sits
|
|
355
|
+
* somewhere inside that span, never exactly on the far end this checks
|
|
356
|
+
* against. A box at the endpoint alone missed it outright: placed items
|
|
357
|
+
* such as lamps were ignored and a body passed straight through them.
|
|
358
|
+
*
|
|
359
|
+
* A dense cluster of wedges nearby can return dozens of candidates —
|
|
360
|
+
* measured at 102 in one query on a wide bank — well past the buffer this
|
|
361
|
+
* used to carry, which silently dropped whatever did not fit and dropped
|
|
362
|
+
* the one lamp among them exactly as often as it kept it. `propProbe` is
|
|
363
|
+
* sized for the worst of what was actually measured, with headroom.
|
|
364
|
+
*/
|
|
365
|
+
blockedByProp(body, axis, before, want, colliders) {
|
|
366
|
+
const half = body.hx + 0.05;
|
|
367
|
+
const lo = Math.min(before, before + want) - half;
|
|
368
|
+
const hi = Math.max(before, before + want) + half;
|
|
369
|
+
const minX = axis === AXIS_X ? lo : body.x - half;
|
|
370
|
+
const maxX = axis === AXIS_X ? hi : body.x + half;
|
|
371
|
+
const minZ = axis === AXIS_Z ? lo : body.z - half;
|
|
372
|
+
const maxZ = axis === AXIS_Z ? hi : body.z + half;
|
|
373
|
+
const q = this.propProbe;
|
|
374
|
+
const n = colliders.query(minX, body.y - body.hy, minZ, maxX, body.y + body.hy, maxZ, q);
|
|
375
|
+
const data = colliders.data;
|
|
376
|
+
for (let k = 0; k < n; k++) {
|
|
377
|
+
const o = q[k] * 6;
|
|
378
|
+
const width = data[o + 3] - data[o];
|
|
379
|
+
const depth = data[o + 5] - data[o + 2];
|
|
380
|
+
if (width < this.propFootprintM && depth < this.propFootprintM)
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Is there a walkable face standing in the body's current column that the character can
|
|
387
|
+
* neither step onto nor pass under? See `moveHorizontal` for why this exists.
|
|
388
|
+
*
|
|
389
|
+
* Uses its own hit rather than `this.hit`, which `resolveSurface` owns for the whole
|
|
390
|
+
* tick and reads after both axes have moved.
|
|
391
|
+
*/
|
|
392
|
+
tryStepUp(body, axis, remaining, colliders, world) {
|
|
393
|
+
const savedX = body.x;
|
|
394
|
+
const savedY = body.y;
|
|
395
|
+
const savedZ = body.z;
|
|
396
|
+
/*
|
|
397
|
+
* The lift is resolved, and it can be refused — but by then it never is by
|
|
398
|
+
* the floor itself: a body inside the deck it stands on is put back on top
|
|
399
|
+
* of it in `integrate`, before anything sweeps. What can still refuse a lift
|
|
400
|
+
* here is a genuine ceiling, and a character under one should not be stepping.
|
|
401
|
+
*/
|
|
402
|
+
/*
|
|
403
|
+
* The budget is `stepHeight`, unless the deck being ridden itself vouches
|
|
404
|
+
* for more — see `surfaceRiseTo`.
|
|
405
|
+
*
|
|
406
|
+
* `stepHeight` is sized for a literal kerb, and it is the wrong number
|
|
407
|
+
* once the axis being swept stops agreeing with the road: `moveHorizontal`
|
|
408
|
+
* resolves X and Z one at a time, so a corner banked steeply enough asks a
|
|
409
|
+
* *pure*-X (or pure-Z) sweep to climb the bank directly whenever the road's
|
|
410
|
+
* own tangent runs mostly along the other axis — a real slope, just not one
|
|
411
|
+
* either isolated axis meets at its own angle. Measured on a hairpin banked
|
|
412
|
+
* 25°: the combined move asked `preLift` for 20 cm and got it, but the
|
|
413
|
+
* following pure-X sweep alone met a wall a full `stepHeight` could not
|
|
414
|
+
* clear, and the character's whole speed on that axis died on a bank the same
|
|
415
|
+
* tick's own pre-lift had already vouched for. Reported on the same class of
|
|
416
|
+
* junction: a body impacting the deck itself while sliding, and sometimes
|
|
417
|
+
* pushed off the track.
|
|
418
|
+
*
|
|
419
|
+
* Only the ridden deck's own rise can raise the budget — `surfaceRiseTo`
|
|
420
|
+
* is the same same-deck, same-normal check `preLift` trusts, so this can
|
|
421
|
+
* still never climb a railing or a prop standing beside the track: ask it
|
|
422
|
+
* about a point under either and it answers with the deck's own gentle
|
|
423
|
+
* rise, not the thing standing on it, and the budget stays at `stepHeight`.
|
|
424
|
+
* What actually stops a step, lift or not, is still `moveAxis`'s own sweep
|
|
425
|
+
* a few lines down — this only ever raises how far that sweep is allowed
|
|
426
|
+
* to *try*.
|
|
427
|
+
*/
|
|
428
|
+
const targetX = axis === AXIS_X ? body.x + remaining : body.x;
|
|
429
|
+
const targetZ = axis === AXIS_Z ? body.z + remaining : body.z;
|
|
430
|
+
const surfaceRise = world.riseTo(targetX, targetZ, body.y - body.hy);
|
|
431
|
+
const liftBudget = surfaceRise !== null && surfaceRise + this.riseSlackM > this.stepHeight
|
|
432
|
+
? surfaceRise + this.riseSlackM
|
|
433
|
+
: this.stepHeight;
|
|
434
|
+
const lifted = moveAxis(body, colliders, AXIS_Y, liftBudget);
|
|
435
|
+
const along = moveAxis(body, colliders, axis, remaining);
|
|
436
|
+
moveAxis(body, colliders, AXIS_Y, -(lifted + 0.002));
|
|
437
|
+
/*
|
|
438
|
+
* Accepted on the physics alone: it went somewhere, and it climbed no more
|
|
439
|
+
* than the budget it was given. `climbed` is the guard that matters — a
|
|
440
|
+
* railing's bar tops out at `stepHeight × 1.4` precisely so an ordinary
|
|
441
|
+
* step can never mount it, and `liftBudget` only ever exceeds `stepHeight`
|
|
442
|
+
* when the ridden deck itself vouched for the extra, which a railing never
|
|
443
|
+
* does (see above) — so the railing's own margin over `stepHeight` still
|
|
444
|
+
* holds exactly as before.
|
|
445
|
+
*
|
|
446
|
+
* There used to be a third condition here, "the step must land on ground
|
|
447
|
+
* the surface recognises", refusing any landing 0.35–0.95 m above the
|
|
448
|
+
* surface's answer. It guarded against stepping onto collider tops standing
|
|
449
|
+
* proud of the drawn deck — which the twisted spans' hulls really did,
|
|
450
|
+
* before they were split into strips. Now a hull is the drawn slab to
|
|
451
|
+
* within centimetres, so anything a step can physically settle on *is*
|
|
452
|
+
* drawn, standable world — and the refusal's only remaining work was false:
|
|
453
|
+
* the finish terrace and the courtyard lips are drawn boxes a stride above a
|
|
454
|
+
* deck whose surface still answers underneath them, and refusing those
|
|
455
|
+
* turned every mixed-surface seam into a wall. Measured on day 8: a clean
|
|
456
|
+
* step onto the terrace refused, 10.7 m/s killed at the lip, and the last
|
|
457
|
+
* metres of the race crossed at a grind.
|
|
458
|
+
*/
|
|
459
|
+
const progressed = Math.abs(along) > 1e-4;
|
|
460
|
+
const climbed = body.y > savedY + 1e-4 && body.y <= savedY + liftBudget + 1e-3;
|
|
461
|
+
if (progressed && climbed) {
|
|
462
|
+
/* What the character just climbed onto reads as a rim until they settle on it, and the
|
|
463
|
+
rim escape would shove them straight back off. See `this.stepGraceSec`. */
|
|
464
|
+
this.sinceStepUp = 0;
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
body.x = savedX;
|
|
468
|
+
body.y = savedY;
|
|
469
|
+
body.z = savedZ;
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
package/dist/tree.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Aabb } from './collide/index.ts';
|
|
2
|
+
/** A node with no child. Chosen over −1 so the arrays can stay unsigned. */
|
|
3
|
+
export declare const NIL = 2147483647;
|
|
4
|
+
export declare class DynamicTree {
|
|
5
|
+
root: number;
|
|
6
|
+
/** minX, minY, minZ, maxX, maxY, maxZ per node. */
|
|
7
|
+
bounds: Float32Array;
|
|
8
|
+
parent: Int32Array;
|
|
9
|
+
child1: Int32Array;
|
|
10
|
+
child2: Int32Array;
|
|
11
|
+
/** The caller's own id for a leaf; −1 on an internal node. */
|
|
12
|
+
proxy: Int32Array;
|
|
13
|
+
height: Int32Array;
|
|
14
|
+
private capacity;
|
|
15
|
+
private free;
|
|
16
|
+
private nodeCount;
|
|
17
|
+
private stack;
|
|
18
|
+
constructor(capacity?: number);
|
|
19
|
+
/** How many nodes are in use. A tree of n leaves has 2n − 1 nodes. */
|
|
20
|
+
get count(): number;
|
|
21
|
+
insert(proxyId: number, box: Aabb): number;
|
|
22
|
+
remove(leaf: number): void;
|
|
23
|
+
/**
|
|
24
|
+
* Re-place a proxy whose tight box has moved. Returns whether the tree changed.
|
|
25
|
+
*
|
|
26
|
+
* A move entirely inside the fat box is free: the stored bounds still enclose the proxy, so no
|
|
27
|
+
* query can miss it. That is the whole point of the margin, and the reason it is a *conservative*
|
|
28
|
+
* enlargement rather than a tolerance.
|
|
29
|
+
*/
|
|
30
|
+
move(leaf: number, box: Aabb): boolean;
|
|
31
|
+
/** Fill `out` with every proxy id whose stored box overlaps `box`. Returns how many. */
|
|
32
|
+
query(box: Aabb, out: Int32Array): number;
|
|
33
|
+
private insertLeaf;
|
|
34
|
+
private removeLeaf;
|
|
35
|
+
private refit;
|
|
36
|
+
/**
|
|
37
|
+
* One AVL-style rotation where a subtree leans by more than one, returning the new subtree root.
|
|
38
|
+
*
|
|
39
|
+
* **Without this the surface-area descent builds a chain, and it does so on the most ordinary
|
|
40
|
+
* scene there is.** Measured: sixty-four boxes in a row along x came out as a tree of height 63,
|
|
41
|
+
* because each new box unions most cheaply with the rightmost leaf and the descent therefore
|
|
42
|
+
* walks the right spine every time. A wall, a stack and a floor strip are all that shape, and a
|
|
43
|
+
* chain makes every query linear, which is the entire cost the tree exists to avoid. A grid was
|
|
44
|
+
* height 14 and hid it.
|
|
45
|
+
*
|
|
46
|
+
* The heuristic chooses *where* to insert and the rotation keeps the result usable; neither
|
|
47
|
+
* substitutes for the other. **What this costs** is a rotation test per node on the way up, all
|
|
48
|
+
* of it integer comparison. **What would make it wrong** is nothing measured yet; the alternative
|
|
49
|
+
* is a full rebuild, which is dearer and not incremental.
|
|
50
|
+
*/
|
|
51
|
+
private balance;
|
|
52
|
+
/** Pull `up` above `a`, keeping `keep` as `a`'s remaining child. */
|
|
53
|
+
private rotate;
|
|
54
|
+
private union;
|
|
55
|
+
/** Surface area of the box enclosing two nodes. The heuristic, and only `+ - *`. */
|
|
56
|
+
private unionArea;
|
|
57
|
+
private allocate;
|
|
58
|
+
private freeNode;
|
|
59
|
+
private growNodes;
|
|
60
|
+
private resetFreeList;
|
|
61
|
+
}
|