@driftengine/physics 3.61.0

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