@woosh/meep-engine 2.146.0 → 2.148.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 (105) hide show
  1. package/package.json +1 -1
  2. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts +4 -4
  3. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts.map +1 -1
  4. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.js +48 -52
  5. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts +23 -21
  6. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts.map +1 -1
  7. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js +41 -406
  8. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts +5 -4
  9. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts.map +1 -1
  10. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.js +400 -395
  11. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +0 -11
  12. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  13. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +8 -6
  14. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  15. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -551
  16. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
  17. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  18. package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
  19. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  20. package/src/engine/control/first-person/abilities/Mantle.js +195 -188
  21. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  22. package/src/engine/control/first-person/abilities/WallRun.js +183 -175
  23. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
  24. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
  25. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
  26. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
  27. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  28. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
  29. package/src/engine/ecs/EntityManager.d.ts +34 -11
  30. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  31. package/src/engine/ecs/EntityManager.js +71 -42
  32. package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
  33. package/src/engine/interpolation/Interpoland.d.ts +48 -0
  34. package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
  35. package/src/engine/interpolation/Interpoland.js +49 -0
  36. package/src/engine/interpolation/Interpolated.d.ts +101 -0
  37. package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
  38. package/src/engine/interpolation/Interpolated.js +149 -0
  39. package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
  40. package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
  41. package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
  42. package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
  43. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
  44. package/src/engine/interpolation/InterpolationSystem.js +233 -0
  45. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
  46. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
  47. package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
  48. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
  49. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
  50. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
  51. package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
  52. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
  53. package/src/engine/interpolation/pose_interpoland.js +27 -0
  54. package/src/engine/network/NetworkSession.d.ts +2 -2
  55. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  56. package/src/engine/network/NetworkSession.js +2 -2
  57. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
  58. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
  59. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
  60. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
  61. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
  62. package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
  63. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
  64. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
  65. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
  66. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
  67. package/src/engine/physics/PLAN.md +10 -9
  68. package/src/engine/physics/body/SolverBodyState.d.ts +142 -0
  69. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -0
  70. package/src/engine/physics/body/SolverBodyState.js +251 -0
  71. package/src/engine/physics/broadphase/generate_pairs.d.ts +2 -1
  72. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  73. package/src/engine/physics/broadphase/generate_pairs.js +5 -3
  74. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  75. package/src/engine/physics/constraint/solve_constraints.js +691 -673
  76. package/src/engine/physics/ecs/PhysicsSystem.d.ts +82 -15
  77. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  78. package/src/engine/physics/ecs/PhysicsSystem.js +387 -87
  79. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +23 -0
  80. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  81. package/src/engine/physics/inertia/world_inverse_inertia.js +116 -77
  82. package/src/engine/physics/integration/integrate_position.d.ts +11 -1
  83. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  84. package/src/engine/physics/integration/integrate_position.js +97 -79
  85. package/src/engine/physics/integration/integrate_velocity.d.ts +12 -3
  86. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  87. package/src/engine/physics/integration/integrate_velocity.js +201 -160
  88. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  89. package/src/engine/physics/narrowphase/box_box_manifold.js +750 -665
  90. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  91. package/src/engine/physics/narrowphase/box_triangle_contact.js +19 -46
  92. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts +16 -0
  93. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -0
  94. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +49 -0
  95. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  96. package/src/engine/physics/narrowphase/narrowphase_step.js +52 -4
  97. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  98. package/src/engine/physics/queries/raycast.js +7 -4
  99. package/src/engine/physics/solver/solve_contacts.d.ts +2 -2
  100. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  101. package/src/engine/physics/solver/solve_contacts.js +1341 -1173
  102. package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
  103. package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
  104. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
  105. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
@@ -1,175 +1,183 @@
1
- import { DEG_TO_RAD } from "../../../../core/math/DEG_TO_RAD.js";
2
- import { Ability } from "./Ability.js";
3
-
4
- /**
5
- * Wall-run ability. Activates when the player is airborne with momentum
6
- * and a wall is detected to their left or right. While active:
7
- *
8
- * - Gravity is reduced to `cfg.wallRun.gravityFactor × base` (typically
9
- * 25%), letting the player hang on the wall for longer than a normal
10
- * jump arc.
11
- * - Horizontal velocity is projected onto the wall's tangent each tick,
12
- * so the body sticks to the wall and slides along it. Forward
13
- * momentum is preserved; into-wall and out-of-wall velocity
14
- * components are zeroed.
15
- * - Camera tilts into the wall via the shared lean spring (writes the
16
- * wall-tilt to `runtime.leanTargetRad` each tick — same channel
17
- * base writes its lat-accel-derived lean to, so transitions in and
18
- * out share the spring's smoothing).
19
- *
20
- * Exits on:
21
- * - Timer (`cfg.wallRun.maxDuration`)
22
- * - Lost wall contact (sensor's hit flag becomes false)
23
- * - Re-grounded (we walked onto a floor at the wall's end)
24
- * - Wall-jump preempts (higher priority — WallJump declares priority 60)
25
- *
26
- * Priority 50 — above Mantle (30), Slide (10), below Wall-jump (60).
27
- *
28
- * @author Alex Goldring
29
- * @copyright Company Named Limited (c) 2026
30
- */
31
- export class WallRun extends Ability {
32
- constructor() {
33
- super();
34
- this.name = "WallRun";
35
- this.priority = 50;
36
-
37
- /** @private Seconds since this ability activated. */
38
- this._elapsed = 0;
39
- /** @private "L" or "R" — which wall we're running on. */
40
- this._side = "L";
41
- }
42
-
43
- canActivate(controller, runtime, sensors) {
44
- const cfg = controller.config.wallRun;
45
- if (!cfg) return false;
46
- if (sensors === undefined || sensors === null) return false;
47
- if (controller.state.grounded) return false;
48
- if (controller.state.airborneTime < cfg.minAirborneTime) return false;
49
-
50
- const speed = Math.hypot(runtime.velocityX, runtime.velocityZ);
51
- if (speed < cfg.minSpeed) return false;
52
-
53
- // Need a wall on left OR right (not front — that's a wall to bump into).
54
- if (!sensors.wallLeft.hit && !sensors.wallRight.hit) return false;
55
-
56
- // Forward intent required wall-run is a committed action.
57
- if (controller.intent.move.y < 0.1) return false;
58
-
59
- // Don't (re)grab a wall you're moving AWAY from. The wall normal
60
- // points off the wall toward the player, so velocity·normal > 0 means
61
- // leaving itmost importantly right after a wall-JUMP pushed you off
62
- // (else the run would re-attach next tick and the tangent projection
63
- // would cancel the kick). Approaching (dot < 0) or running along it
64
- // (dot 0) is fine.
65
- const hit = sensors.wallLeft.hit
66
- && (!sensors.wallRight.hit || sensors.wallLeft.distance <= sensors.wallRight.distance)
67
- ? sensors.wallLeft : sensors.wallRight;
68
- const awaySpeed = runtime.velocityX * hit.normal.x + runtime.velocityZ * hit.normal.z;
69
- if (awaySpeed > cfg.minSpeed * 0.5) return false;
70
-
71
- return true;
72
- }
73
-
74
- onActivate(controller, runtime) {
75
- const sensors = runtime.sensors;
76
- this._elapsed = 0;
77
-
78
- // Pick the closer wall if both detected.
79
- if (sensors.wallLeft.hit
80
- && (!sensors.wallRight.hit
81
- || sensors.wallLeft.distance <= sensors.wallRight.distance)) {
82
- this._side = "L";
83
- } else {
84
- this._side = "R";
85
- }
86
-
87
- // Clear any in-progress jump state — the wall-run takes over the
88
- // vertical model.
89
- runtime.midJump = false;
90
- runtime.apexFired = false;
91
- controller.state.isVariableJumpCut = false;
92
- controller.state.isAscending = false;
93
-
94
- controller.signals.onLeaveGround.send1({ reason: "wallrun" });
95
- }
96
-
97
- /**
98
- * Wall-jump (priority 60) should be allowed to preempt freely; that's
99
- * the canonical chain (wall-run → wall-jump). Any other higher-priority
100
- * ability is also fine — wall-run is a transient state.
101
- */
102
- canInterrupt() {
103
- return true;
104
- }
105
-
106
- tick(controller, runtime, bodyTransform, dt, system) {
107
- const cfg = controller.config.wallRun;
108
- this._elapsed += dt;
109
-
110
- // -- Exit: timer.
111
- if (this._elapsed >= cfg.maxDuration) {
112
- return false;
113
- }
114
- // -- Exit: re-grounded (we ran onto a floor).
115
- if (controller.state.grounded) {
116
- return false;
117
- }
118
-
119
- const sensors = runtime.sensors;
120
- const sensorHit = this._side === "L" ? sensors.wallLeft : sensors.wallRight;
121
-
122
- // -- Exit: lost the wall.
123
- if (!sensorHit.hit) {
124
- return false;
125
- }
126
-
127
- // -- Project velocity onto the wall's tangent.
128
- // The wall's normal is in the XZ plane (vertical wall). Zero the
129
- // component of velocity along that normal. Forward momentum
130
- // (along the wall's tangent direction) is preserved.
131
- const nx = sensorHit.normal.x;
132
- const nz = sensorHit.normal.z;
133
- const dotN = runtime.velocityX * nx + runtime.velocityZ * nz;
134
- runtime.velocityX -= dotN * nx;
135
- runtime.velocityZ -= dotN * nz;
136
-
137
- // -- Camera roll: write directly to the shared lean target.
138
- // Sign convention: positive engine roll = head tilts RIGHT. Wall
139
- // on LEFT → tilt LEFT → negative roll. Wall on RIGHT → tilt
140
- // RIGHT positive roll. L2.f spring-steps from this value each
141
- // tick; base will overwrite once the ability releases, and the
142
- // spring smooths the transition.
143
- const rollSign = this._side === "L" ? -1 : 1;
144
- runtime.leanTargetRad = rollSign * cfg.cameraRollDeg * DEG_TO_RAD;
145
-
146
- // -- Reduced gravity. This is wall-run's OWN vertical model (a
147
- // fraction of base gravity, no fall multiplier), applied here rather
148
- // than via the system's standard gravity step.
149
- runtime.velocityY -= runtime.gravity * cfg.gravityFactor * dt;
150
-
151
- // -- Resolve the move through the shared motor. With the reduced-
152
- // gravity velocity set above, route it through the same sweep-and-
153
- // slide + ground-categorize the base locomotion uses (the mover when
154
- // physics is present, else the flat-ground fallback). _resolveMotion
155
- // does NOT re-apply gravity. This replaces a raw `position._add` plus
156
- // a hand-rolled groundResolver catch: the wall-runner now (a) can't
157
- // drift INTO geometry (the sweep clips it) nor sail OFF a ledge the
158
- // mover would have caught, and (b) gets the motor's anti-tunnel
159
- // ground catch for free the same reason base no longer free-falls
160
- // past a floor. The motor sets `state.grounded` and fires land/leave.
161
- system._resolveMotion(controller, runtime, bodyTransform, dt);
162
-
163
- // -- Exit: the motor caught a floor at the wall's foot (we ran out
164
- // onto the ground). Release; base resumes next tick.
165
- if (controller.state.grounded) {
166
- return false;
167
- }
168
-
169
- return true;
170
- }
171
-
172
- // No onDeactivate cleanup needed: on the tick we release, base runs
173
- // and overwrites runtime.leanTargetRad with the natural value. The
174
- // lean spring smooths the transition.
175
- }
1
+ import { DEG_TO_RAD } from "../../../../core/math/DEG_TO_RAD.js";
2
+ import { Ability } from "./Ability.js";
3
+
4
+ /**
5
+ * Wall-run ability. Activates when the player is airborne with momentum
6
+ * and a wall is detected to their left or right. While active:
7
+ *
8
+ * - Gravity is reduced to `cfg.wallRun.gravityFactor × base` (typically
9
+ * 25%), letting the player hang on the wall for longer than a normal
10
+ * jump arc.
11
+ * - Horizontal velocity is projected onto the wall's tangent each tick,
12
+ * so the body sticks to the wall and slides along it. Forward
13
+ * momentum is preserved; into-wall and out-of-wall velocity
14
+ * components are zeroed.
15
+ * - Camera tilts into the wall via the shared lean spring (writes the
16
+ * wall-tilt to `runtime.leanTargetRad` each tick — same channel
17
+ * base writes its lat-accel-derived lean to, so transitions in and
18
+ * out share the spring's smoothing).
19
+ *
20
+ * Exits on:
21
+ * - Timer (`cfg.wallRun.maxDuration`)
22
+ * - Lost wall contact (sensor's hit flag becomes false)
23
+ * - Re-grounded (we walked onto a floor at the wall's end)
24
+ * - Wall-jump preempts (higher priority — WallJump declares priority 60)
25
+ *
26
+ * Priority 50 — above Mantle (30), Slide (10), below Wall-jump (60).
27
+ *
28
+ * @author Alex Goldring
29
+ * @copyright Company Named Limited (c) 2026
30
+ */
31
+ export class WallRun extends Ability {
32
+ constructor() {
33
+ super();
34
+ this.name = "WallRun";
35
+ this.priority = 50;
36
+
37
+ /** @private Seconds since this ability activated. */
38
+ this._elapsed = 0;
39
+ /** @private "L" or "R" — which wall we're running on. */
40
+ this._side = "L";
41
+ }
42
+
43
+ canActivate(controller, runtime, sensors) {
44
+ const cfg = controller.config.wallRun;
45
+ if (!cfg) return false;
46
+ if (sensors === undefined || sensors === null) return false;
47
+ if (controller.state.grounded) return false;
48
+ if (controller.state.airborneTime < cfg.minAirborneTime) return false;
49
+
50
+ const speed = Math.hypot(runtime.velocityX, runtime.velocityZ);
51
+ if (speed < cfg.minSpeed) return false;
52
+
53
+ // Need a wall on left OR right (not front — that's a wall to bump into).
54
+ if (!sensors.wallLeft.hit && !sensors.wallRight.hit) return false;
55
+
56
+ // Don't (re)grab the wall when a grabbable ledge is right ahead: you've
57
+ // reached its top, so ledge-grab/mantle should take over (hang or
58
+ // vault) instead of the run re-attaching and riding you back DOWN the
59
+ // wall you just climbed. Only bites when facing INTO the wall (the
60
+ // forward probe has to hit for a ledge ahead), so a normal run ALONG
61
+ // the wallfacing down its length is unaffected.
62
+ if (sensors.ledgeAhead.hit) return false;
63
+
64
+ // Forward intent required — wall-run is a committed action.
65
+ if (controller.intent.move.y < 0.1) return false;
66
+
67
+ // Don't (re)grab a wall you're moving AWAY from. The wall normal
68
+ // points off the wall toward the player, so velocity·normal > 0 means
69
+ // leaving it most importantly right after a wall-JUMP pushed you off
70
+ // (else the run would re-attach next tick and the tangent projection
71
+ // would cancel the kick). Approaching (dot < 0) or running along it
72
+ // (dot ≈ 0) is fine.
73
+ const hit = sensors.wallLeft.hit
74
+ && (!sensors.wallRight.hit || sensors.wallLeft.distance <= sensors.wallRight.distance)
75
+ ? sensors.wallLeft : sensors.wallRight;
76
+ const awaySpeed = runtime.velocityX * hit.normal.x + runtime.velocityZ * hit.normal.z;
77
+ if (awaySpeed > cfg.minSpeed * 0.5) return false;
78
+
79
+ return true;
80
+ }
81
+
82
+ onActivate(controller, runtime) {
83
+ const sensors = runtime.sensors;
84
+ this._elapsed = 0;
85
+
86
+ // Pick the closer wall if both detected.
87
+ if (sensors.wallLeft.hit
88
+ && (!sensors.wallRight.hit
89
+ || sensors.wallLeft.distance <= sensors.wallRight.distance)) {
90
+ this._side = "L";
91
+ } else {
92
+ this._side = "R";
93
+ }
94
+
95
+ // Clear any in-progress jump state — the wall-run takes over the
96
+ // vertical model.
97
+ runtime.midJump = false;
98
+ runtime.apexFired = false;
99
+ controller.state.isVariableJumpCut = false;
100
+ controller.state.isAscending = false;
101
+
102
+ controller.signals.onLeaveGround.send1({ reason: "wallrun" });
103
+ }
104
+
105
+ /**
106
+ * Wall-jump (priority 60) should be allowed to preempt freely; that's
107
+ * the canonical chain (wall-run → wall-jump). Any other higher-priority
108
+ * ability is also fine — wall-run is a transient state.
109
+ */
110
+ canInterrupt() {
111
+ return true;
112
+ }
113
+
114
+ tick(controller, runtime, bodyTransform, dt, system) {
115
+ const cfg = controller.config.wallRun;
116
+ this._elapsed += dt;
117
+
118
+ // -- Exit: timer.
119
+ if (this._elapsed >= cfg.maxDuration) {
120
+ return false;
121
+ }
122
+ // -- Exit: re-grounded (we ran onto a floor).
123
+ if (controller.state.grounded) {
124
+ return false;
125
+ }
126
+
127
+ const sensors = runtime.sensors;
128
+ const sensorHit = this._side === "L" ? sensors.wallLeft : sensors.wallRight;
129
+
130
+ // -- Exit: lost the wall.
131
+ if (!sensorHit.hit) {
132
+ return false;
133
+ }
134
+
135
+ // -- Project velocity onto the wall's tangent.
136
+ // The wall's normal is in the XZ plane (vertical wall). Zero the
137
+ // component of velocity along that normal. Forward momentum
138
+ // (along the wall's tangent direction) is preserved.
139
+ const nx = sensorHit.normal.x;
140
+ const nz = sensorHit.normal.z;
141
+ const dotN = runtime.velocityX * nx + runtime.velocityZ * nz;
142
+ runtime.velocityX -= dotN * nx;
143
+ runtime.velocityZ -= dotN * nz;
144
+
145
+ // -- Camera roll: write directly to the shared lean target.
146
+ // Sign convention: positive engine roll = head tilts RIGHT. Wall
147
+ // on LEFT tilt LEFT negative roll. Wall on RIGHT → tilt
148
+ // RIGHT positive roll. L2.f spring-steps from this value each
149
+ // tick; base will overwrite once the ability releases, and the
150
+ // spring smooths the transition.
151
+ const rollSign = this._side === "L" ? -1 : 1;
152
+ runtime.leanTargetRad = rollSign * cfg.cameraRollDeg * DEG_TO_RAD;
153
+
154
+ // -- Reduced gravity. This is wall-run's OWN vertical model (a
155
+ // fraction of base gravity, no fall multiplier), applied here rather
156
+ // than via the system's standard gravity step.
157
+ runtime.velocityY -= runtime.gravity * cfg.gravityFactor * dt;
158
+
159
+ // -- Resolve the move through the shared motor. With the reduced-
160
+ // gravity velocity set above, route it through the same sweep-and-
161
+ // slide + ground-categorize the base locomotion uses (the mover when
162
+ // physics is present, else the flat-ground fallback). _resolveMotion
163
+ // does NOT re-apply gravity. This replaces a raw `position._add` plus
164
+ // a hand-rolled groundResolver catch: the wall-runner now (a) can't
165
+ // drift INTO geometry (the sweep clips it) nor sail OFF a ledge the
166
+ // mover would have caught, and (b) gets the motor's anti-tunnel
167
+ // ground catch for free — the same reason base no longer free-falls
168
+ // past a floor. The motor sets `state.grounded` and fires land/leave.
169
+ system._resolveMotion(controller, runtime, bodyTransform, dt);
170
+
171
+ // -- Exit: the motor caught a floor at the wall's foot (we ran out
172
+ // onto the ground). Release; base resumes next tick.
173
+ if (controller.state.grounded) {
174
+ return false;
175
+ }
176
+
177
+ return true;
178
+ }
179
+
180
+ // No onDeactivate cleanup needed: on the tick we release, base runs
181
+ // and overwrites runtime.leanTargetRad with the natural value. The
182
+ // lean spring smooths the transition.
183
+ }
@@ -51,6 +51,15 @@ export class FirstPersonSensors {
51
51
  * obstacle's top finds empty space, indicating a grabbable edge.
52
52
  */
53
53
  ledgeAhead: SensorHit;
54
+ /**
55
+ * True when `ledgeAhead`'s top surface is wide enough to STAND on
56
+ * (a standing capsule fits forward of the edge), false when it's a
57
+ * knife-edge / thin wall you could only hang from. Mantle reads this
58
+ * to refuse vaulting onto a top it would immediately fall off; ledge
59
+ * grab ignores it (you can hang from a thin edge). Default false so a
60
+ * mantle never fires on stale data.
61
+ */
62
+ ledgeStandable: boolean;
54
63
  /** Clear all sensor slots — call at the start of each tick. */
55
64
  clearAll(): void;
56
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FirstPersonSensors.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensors.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IAEQ,aAAgB;IAChB,4DAA4D;IAC5D,iBAAiB;IACjB,iCAAiC;IACjC,eAA0B;IAC1B,yEAAyE;IACzE,gBAAkC;IAClC,6DAA6D;IAC7D,gBAAsB;IAG1B,mEAAmE;IACnE,cAMC;CACJ;AAED;;;;;;;;;;;;;;GAcG;AACH;IAEQ,2DAA2D;IAC3D,oBAA+B;IAC/B,4DAA4D;IAC5D,qBAAgC;IAChC,0CAA0C;IAC1C,qBAAgC;IAChC;;;;OAIG;IACH,yBAAoC;IACpC;;;;OAIG;IACH,sBAAiC;IAGrC,+DAA+D;IAC/D,iBAMC;CACJ;oBA5EmB,kCAAkC"}
1
+ {"version":3,"file":"FirstPersonSensors.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensors.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IAEQ,aAAgB;IAChB,4DAA4D;IAC5D,iBAAiB;IACjB,iCAAiC;IACjC,eAA0B;IAC1B,yEAAyE;IACzE,gBAAkC;IAClC,6DAA6D;IAC7D,gBAAsB;IAG1B,mEAAmE;IACnE,cAMC;CACJ;AAED;;;;;;;;;;;;;;GAcG;AACH;IAEQ,2DAA2D;IAC3D,oBAA+B;IAC/B,4DAA4D;IAC5D,qBAAgC;IAChC,0CAA0C;IAC1C,qBAAgC;IAChC;;;;OAIG;IACH,yBAAoC;IACpC;;;;OAIG;IACH,sBAAiC;IACjC;;;;;;;OAOG;IACH,wBAA2B;IAG/B,+DAA+D;IAC/D,iBAOC;CACJ;oBAtFmB,kCAAkC"}
@@ -1,77 +1,87 @@
1
- import Vector3 from "../../../../core/geom/Vector3.js";
2
-
3
- /**
4
- * Single spatial-query result. Set fields when `hit` is true; ignore them
5
- * otherwise. `surfaceTag` is optional — populated by future surface-tag
6
- * systems (grass/wood/metal/etc.) so abilities and audio can react to the
7
- * material.
8
- */
9
- export class SensorHit {
10
- constructor() {
11
- this.hit = false;
12
- /** Distance from query origin to contact point (meters). */
13
- this.distance = 0;
14
- /** World-space contact point. */
15
- this.point = new Vector3();
16
- /** Surface normal at contact (unit vector, points away from surface). */
17
- this.normal = new Vector3(0, 1, 0);
18
- /** Optional material/surface tag — e.g. "grass", "metal". */
19
- this.surfaceTag = null;
20
- }
21
-
22
- /** Reset to "no hit" — call before re-using the slot each tick. */
23
- clear() {
24
- this.hit = false;
25
- this.distance = 0;
26
- this.point.set(0, 0, 0);
27
- this.normal.set(0, 1, 0);
28
- this.surfaceTag = null;
29
- }
30
- }
31
-
32
- /**
33
- * Cached spatial-query results for one first-person controller. Populated
34
- * by {@link FirstPersonSensorsSystem} once per fixed step, read by
35
- * abilities and (where useful) by the base controller.
36
- *
37
- * The point of cacheing is dedup: mantle and ledge-grab both want a
38
- * "forward + up obstacle probe". Compute once, both read the cached hit.
39
- *
40
- * Sensor slots cleared each tick before population. Abilities should
41
- * treat any `hit === false` slot as "absent" — never read distance/point
42
- * when hit is false (they're stale or zero, depending on clear-mode).
43
- *
44
- * @author Alex Goldring
45
- * @copyright Company Named Limited (c) 2026
46
- */
47
- export class FirstPersonSensors {
48
- constructor() {
49
- /** Wall to the body-local left, probed at chest height. */
50
- this.wallLeft = new SensorHit();
51
- /** Wall to the body-local right, probed at chest height. */
52
- this.wallRight = new SensorHit();
53
- /** Wall directly in front of the body. */
54
- this.wallFront = new SensorHit();
55
- /**
56
- * Obstacle ahead at chest/waist height — used by mantle/vault.
57
- * Distance is along the body's forward direction; normal is the
58
- * face of the obstacle the player is approaching.
59
- */
60
- this.obstacleAhead = new SensorHit();
61
- /**
62
- * Ledge edge ahead — for ledge grab. Populated when the forward
63
- * obstacle probe hits and a downward probe just past the
64
- * obstacle's top finds empty space, indicating a grabbable edge.
65
- */
66
- this.ledgeAhead = new SensorHit();
67
- }
68
-
69
- /** Clear all sensor slots call at the start of each tick. */
70
- clearAll() {
71
- this.wallLeft.clear();
72
- this.wallRight.clear();
73
- this.wallFront.clear();
74
- this.obstacleAhead.clear();
75
- this.ledgeAhead.clear();
76
- }
77
- }
1
+ import Vector3 from "../../../../core/geom/Vector3.js";
2
+
3
+ /**
4
+ * Single spatial-query result. Set fields when `hit` is true; ignore them
5
+ * otherwise. `surfaceTag` is optional — populated by future surface-tag
6
+ * systems (grass/wood/metal/etc.) so abilities and audio can react to the
7
+ * material.
8
+ */
9
+ export class SensorHit {
10
+ constructor() {
11
+ this.hit = false;
12
+ /** Distance from query origin to contact point (meters). */
13
+ this.distance = 0;
14
+ /** World-space contact point. */
15
+ this.point = new Vector3();
16
+ /** Surface normal at contact (unit vector, points away from surface). */
17
+ this.normal = new Vector3(0, 1, 0);
18
+ /** Optional material/surface tag — e.g. "grass", "metal". */
19
+ this.surfaceTag = null;
20
+ }
21
+
22
+ /** Reset to "no hit" — call before re-using the slot each tick. */
23
+ clear() {
24
+ this.hit = false;
25
+ this.distance = 0;
26
+ this.point.set(0, 0, 0);
27
+ this.normal.set(0, 1, 0);
28
+ this.surfaceTag = null;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Cached spatial-query results for one first-person controller. Populated
34
+ * by {@link FirstPersonSensorsSystem} once per fixed step, read by
35
+ * abilities and (where useful) by the base controller.
36
+ *
37
+ * The point of cacheing is dedup: mantle and ledge-grab both want a
38
+ * "forward + up obstacle probe". Compute once, both read the cached hit.
39
+ *
40
+ * Sensor slots cleared each tick before population. Abilities should
41
+ * treat any `hit === false` slot as "absent" — never read distance/point
42
+ * when hit is false (they're stale or zero, depending on clear-mode).
43
+ *
44
+ * @author Alex Goldring
45
+ * @copyright Company Named Limited (c) 2026
46
+ */
47
+ export class FirstPersonSensors {
48
+ constructor() {
49
+ /** Wall to the body-local left, probed at chest height. */
50
+ this.wallLeft = new SensorHit();
51
+ /** Wall to the body-local right, probed at chest height. */
52
+ this.wallRight = new SensorHit();
53
+ /** Wall directly in front of the body. */
54
+ this.wallFront = new SensorHit();
55
+ /**
56
+ * Obstacle ahead at chest/waist height — used by mantle/vault.
57
+ * Distance is along the body's forward direction; normal is the
58
+ * face of the obstacle the player is approaching.
59
+ */
60
+ this.obstacleAhead = new SensorHit();
61
+ /**
62
+ * Ledge edge ahead — for ledge grab. Populated when the forward
63
+ * obstacle probe hits and a downward probe just past the
64
+ * obstacle's top finds empty space, indicating a grabbable edge.
65
+ */
66
+ this.ledgeAhead = new SensorHit();
67
+ /**
68
+ * True when `ledgeAhead`'s top surface is wide enough to STAND on
69
+ * (a standing capsule fits forward of the edge), false when it's a
70
+ * knife-edge / thin wall you could only hang from. Mantle reads this
71
+ * to refuse vaulting onto a top it would immediately fall off; ledge
72
+ * grab ignores it (you can hang from a thin edge). Default false so a
73
+ * mantle never fires on stale data.
74
+ */
75
+ this.ledgeStandable = false;
76
+ }
77
+
78
+ /** Clear all sensor slots — call at the start of each tick. */
79
+ clearAll() {
80
+ this.wallLeft.clear();
81
+ this.wallRight.clear();
82
+ this.wallFront.clear();
83
+ this.obstacleAhead.clear();
84
+ this.ledgeAhead.clear();
85
+ this.ledgeStandable = false;
86
+ }
87
+ }
@@ -58,6 +58,14 @@ export class FirstPersonSensorsSystem extends System<any, any, any, any, any> {
58
58
  * @private
59
59
  */
60
60
  private _populateForEntity;
61
+ /**
62
+ * Down-probe a point on (or just past) a ledge top: returns true if a
63
+ * surface is still there near `edgeY` — i.e. the top is wide enough for a
64
+ * standing capsule. A miss (or a much-lower hit) means the surface dropped
65
+ * away: a thin wall / lip you can only hang from, not stand on.
66
+ * @private
67
+ */
68
+ private _probeStandable;
61
69
  /**
62
70
  * Raycast through {@link PhysicsSystem.raycast}, populating the slot
63
71
  * if anything is hit. Direction is assumed unit (the controller's
@@ -1 +1 @@
1
- {"version":3,"file":"FirstPersonSensorsSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;IACI,cAsCC;IAnCG,wEAA4D;IAE5D;;;;;OAKG;IACH,kBAFU,iCAAiC,GAAC,IAAI,CAEpB;IAE5B;;;;;OAKG;IACH,eAFU,aAAa,GAAC,IAAI,CAEH;IAEzB;;;;;;OAMG;IACH,mBAA4B;IAE5B;;;;;;OAMG;IACH,mBAA2C;IAG/C,2CAaC;IAED,2BAIC;IAED;;OAEG;IACH,2BAoEC;IAED;;;;;;;;;;;OAWG;IACH,kBAiBC;CACJ;uBA/LsB,wBAAwB;0BACrB,qCAAqC;4CACnB,mCAAmC;kDAC7B,yCAAyC;8BAL7D,uCAAuC"}
1
+ {"version":3,"file":"FirstPersonSensorsSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;IACI,cAsCC;IAnCG,wEAA4D;IAE5D;;;;;OAKG;IACH,kBAFU,iCAAiC,GAAC,IAAI,CAEpB;IAE5B;;;;;OAKG;IACH,eAFU,aAAa,GAAC,IAAI,CAEH;IAEzB;;;;;;OAMG;IACH,mBAA4B;IAE5B;;;;;;OAMG;IACH,mBAA2C;IAG/C,2CAaC;IAED,2BAIC;IAED;;OAEG;IACH,2BAmFC;IAED;;;;;;OAMG;IACH,wBASC;IAED;;;;;;;;;;;OAWG;IACH,kBAiBC;CACJ;uBAhOsB,wBAAwB;0BACrB,qCAAqC;4CACnB,mCAAmC;kDAC7B,yCAAyC;8BAL7D,uCAAuC"}