@woosh/meep-engine 2.145.0 → 2.147.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 (99) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
  3. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
  5. package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -352
  6. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -14
  7. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  8. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +20 -8
  9. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  10. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -546
  11. package/src/engine/control/first-person/TODO.md +13 -11
  12. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
  13. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  14. package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
  15. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  16. package/src/engine/control/first-person/abilities/Mantle.js +195 -188
  17. package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
  18. package/src/engine/control/first-person/abilities/WallJump.js +11 -3
  19. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  20. package/src/engine/control/first-person/abilities/WallRun.js +183 -163
  21. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  22. package/src/engine/control/first-person/collision/KinematicMover.js +634 -592
  23. package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
  24. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
  25. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
  26. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
  27. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
  28. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  29. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
  30. package/src/engine/ecs/EntityManager.d.ts +34 -11
  31. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  32. package/src/engine/ecs/EntityManager.js +71 -42
  33. package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
  34. package/src/engine/interpolation/Interpoland.d.ts +48 -0
  35. package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
  36. package/src/engine/interpolation/Interpoland.js +49 -0
  37. package/src/engine/interpolation/Interpolated.d.ts +101 -0
  38. package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
  39. package/src/engine/interpolation/Interpolated.js +149 -0
  40. package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
  41. package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
  42. package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
  43. package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
  44. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
  45. package/src/engine/interpolation/InterpolationSystem.js +233 -0
  46. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
  47. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
  48. package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
  49. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
  50. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
  51. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
  52. package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
  53. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
  54. package/src/engine/interpolation/pose_interpoland.js +27 -0
  55. package/src/engine/network/NetworkSession.d.ts +2 -2
  56. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  57. package/src/engine/network/NetworkSession.js +2 -2
  58. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
  59. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
  60. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
  61. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
  62. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
  63. package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
  64. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
  65. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
  66. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
  67. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
  68. package/src/engine/physics/PLAN.md +944 -809
  69. package/src/engine/physics/body/BodyStorage.d.ts +9 -0
  70. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  71. package/src/engine/physics/body/BodyStorage.js +23 -0
  72. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  73. package/src/engine/physics/broadphase/generate_pairs.js +7 -0
  74. package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
  75. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
  76. package/src/engine/physics/ccd/linear_sweep.js +238 -0
  77. package/src/engine/physics/ecs/PhysicsSystem.d.ts +82 -3
  78. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  79. package/src/engine/physics/ecs/PhysicsSystem.js +227 -8
  80. package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
  81. package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
  82. package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
  83. package/src/engine/physics/narrowphase/box_triangle_contact.js +814 -811
  84. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  85. package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
  86. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
  87. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
  88. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
  89. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  90. package/src/engine/physics/narrowphase/narrowphase_step.js +97 -13
  91. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  92. package/src/engine/physics/queries/overlap_shape.js +185 -183
  93. package/src/engine/simulation/Ticker.d.ts +14 -0
  94. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  95. package/src/engine/simulation/Ticker.js +136 -1
  96. package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
  97. package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
  98. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
  99. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
@@ -94,17 +94,19 @@ The legacy `_moveAndSlide` + scalar-resolver regime was replaced by the
94
94
  `collision/KinematicMover` rebuild (Phases 1–4; see DESIGN_COLLISION.md).
95
95
  Slide-along-walls (axis + oblique), vertical anti-tunnel (floors and
96
96
  ceilings), depenetration, slopes, stick-to-ground, and stairs all landed
97
- there. Remaining:
98
-
99
- ### Route WallRun through the mover
100
- Slide already routes its motion through the mover (it calls
101
- `_integrateVerticalAndResolveGround` `_moveViaMover`). **WallRun still
102
- self-integrates** its reduced-gravity model and so doesn't get
103
- sweep-and-slide a wall-runner can still drift into / off geometry the
104
- mover would have resolved. Thread WallRun's per-tick motion through
105
- `mover.move()` (keeping its reduced-gravity as the vertical input). The
106
- gating issue (a usable surface normal) is solved now — `result.groundNormal`
107
- and the narrowphase contact normals are available.
97
+ there.
98
+
99
+ ### Route WallRun through the mover ✅ done
100
+ WallRun used to self-integrate its reduced-gravity model (`position._add`
101
+ + a hand-rolled groundResolver catch), so it got no sweep-and-slide and
102
+ could drift into / off geometry. It now applies its reduced gravity and
103
+ routes the move through the shared `_resolveMotion` (the mover when
104
+ physics is present, else the flat-ground fallback) — same motor as base
105
+ locomotion and Slide. So **every** motion path goes through one collision
106
+ seam now: it can't drift into a wall (the sweep clips it) nor sail off a
107
+ ledge / through a floor the mover would catch. Guard: `WallRun.spec.js`
108
+ "physics: a wall-runner slides along a real wall and is caught by the
109
+ mover's real floor". No remaining collision follow-ups.
108
110
 
109
111
  ## Other open notes
110
112
 
@@ -49,10 +49,15 @@
49
49
  * @copyright Company Named Limited (c) 2026
50
50
  */
51
51
  export class LedgeGrab extends Ability {
52
- /** @private World-space ledge edge position captured at onActivate. */
53
- private _edgeX;
52
+ /** @private Wall-face anchor (x,z), the face's outward normal (x,z),
53
+ * and the top-edge height (y) — captured at onActivate. The hang pose
54
+ * is built from the WALL, not from the (possibly oblique) body
55
+ * facing, so it stays just outside the wall at any approach angle. */
56
+ private _anchorX;
57
+ _anchorZ: number;
58
+ _nx: number;
59
+ _nz: number;
54
60
  _edgeY: number;
55
- _edgeZ: number;
56
61
  canActivate(controller: any, runtime: any, sensors: any): boolean;
57
62
  onActivate(controller: any, runtime: any): void;
58
63
  canInterrupt(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"LedgeGrab.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/abilities/LedgeGrab.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH;IAMQ,uEAAuE;IACvE,eAAe;IACf,eAAe;IACf,eAAe;IAGnB,kEAkBC;IAED,gDA8BC;IAED,wBAKC;IAED,wFAwEC;CACJ;wBApMuB,cAAc"}
1
+ {"version":3,"file":"LedgeGrab.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/abilities/LedgeGrab.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH;IAMQ;;;2EAGuE;IACvE,iBAAiB;IACjB,iBAAiB;IACjB,YAAY;IACZ,YAAY;IACZ,eAAe;IAGnB,kEAkBC;IAED,gDAkCC;IAED,wBAKC;IAED,wFA6EC;CACJ;wBAlNuB,cAAc"}
@@ -1,199 +1,213 @@
1
- import { clamp } from "../../../../core/math/clamp.js";
2
- import { FirstPersonPosture } from "../pose/FirstPersonPosture.js";
3
- import { Ability } from "./Ability.js";
4
-
5
- /**
6
- * Ledge-grab ability — snap to a forward ledge while descending and hang
7
- * from it until the player chooses (or is forced) to release.
8
- *
9
- * Activation:
10
- * - `sensors.ledgeAhead.hit` (a forward+up obstacle probe found a grab-
11
- * able edge — typically the same probe mantle uses).
12
- * - `runtime.velocityY <= 0` — the player is descending or static. We
13
- * don't auto-catch ledges on the way UP — that would feel like the
14
- * world is grabbing the player mid-jump.
15
- * - `state.airborneTime >= minAirborneTime` — leaves a small window
16
- * after takeoff where ledge-grab can't fire, even if the player is
17
- * instantly at apex (which would otherwise satisfy `velocityY <= 0`).
18
- * - Forward intent (`intent.move.y > 0.1`) — the player must be
19
- * committing into the ledge. Walking backwards off an edge shouldn't
20
- * auto-catch the lip.
21
- *
22
- * Behaviour:
23
- * - Body position snaps to a "hang from edge" pose: at the ledge edge
24
- * X/Z (with a small back-offset so the body is pulled against the
25
- * wall below), and Y offset down by ~bodyHeight so the hands grip
26
- * the edge with the body suspended below.
27
- * - Velocity is zeroed each tick — the body is parked.
28
- * - Exertion rises at `cfg.exertionRiseRate` per second, scaled by
29
- * mass. Climbing fatigue: hang too long and the next exit is
30
- * forced (a slip).
31
- * - Grounded stays false (the player is hanging in air).
32
- *
33
- * Exit:
34
- * - Jump pressed (rising edge) → mantle-up. Release with a small
35
- * upward velocity so mantle's ledgeAhead probe still hits next
36
- * tick; mantle (priority 30) then takes over and animates the
37
- * climb-onto-surface path.
38
- * - Back-intent (`intent.move.y < -0.1`) → drop. Release with zero
39
- * velocity; the player falls under base gravity.
40
- * - Crouch pressed → drop. Same as back-intent.
41
- * - Exertion saturates (`>= 1`) → slip. Release with zero velocity.
42
- * - `sensors.ledgeAhead.hit` becomes false → shuffled off the edge.
43
- * Release with zero velocity.
44
- *
45
- * Priority 40 — above mantle (30), below wall-run (50). Mantle CAN'T
46
- * preempt an active ledge-grab — that's the point of placing ledge
47
- * higher. Otherwise mantle would fire mid-hang and snap the body away.
48
- * Wall-run and wall-jump can preempt — but in practice they need lateral
49
- * walls + speed (wall-run) or a jump press near a side wall (wall-jump),
50
- * conditions which rarely hold while hanging.
51
- *
52
- * @author Alex Goldring
53
- * @copyright Company Named Limited (c) 2026
54
- */
55
- export class LedgeGrab extends Ability {
56
- constructor() {
57
- super();
58
- this.name = "LedgeGrab";
59
- this.priority = 40;
60
-
61
- /** @private World-space ledge edge position captured at onActivate. */
62
- this._edgeX = 0;
63
- this._edgeY = 0;
64
- this._edgeZ = 0;
65
- }
66
-
67
- canActivate(controller, runtime, sensors) {
68
- const cfg = controller.config.ledgeGrab;
69
- if (!cfg) return false;
70
- if (sensors === undefined || sensors === null) return false;
71
-
72
- if (!sensors.ledgeAhead.hit) return false;
73
- if (runtime.velocityY > 0) return false; // not on the way up
74
- if (controller.state.grounded) return false;
75
- if (controller.state.airborneTime < cfg.minAirborneTime) return false;
76
- if (controller.intent.move.y < 0.1) return false;
77
- // Crouch or exhausted: don't re-grab. These are the same exit
78
- // conditions that would IMMEDIATELY release after activation, so
79
- // gating them here prevents a same-tick re-activation flicker
80
- // after a crouch-release or slip.
81
- if (controller.intent.crouch) return false;
82
- if (controller.state.exertion >= 1.0) return false;
83
-
84
- return true;
85
- }
86
-
87
- onActivate(controller, runtime) {
88
- const sensors = runtime.sensors;
89
- const edge = sensors.ledgeAhead.point;
90
-
91
- this._edgeX = edge.x;
92
- this._edgeY = edge.y;
93
- this._edgeZ = edge.z;
94
-
95
- // Zero velocity the body is now parked on the ledge.
96
- runtime.velocityX = 0;
97
- runtime.velocityY = 0;
98
- runtime.velocityZ = 0;
99
-
100
- // Clear any in-progress jump state — hanging supersedes them.
101
- runtime.midJump = false;
102
- runtime.apexFired = false;
103
- controller.state.isVariableJumpCut = false;
104
- controller.state.isAscending = false;
105
-
106
- // The body is hanging — not on the ground.
107
- controller.state.grounded = false;
108
-
109
- // Body is suspended by the hands; no striding feet, head near
110
- // the hands gripping the edge. L2 reads posture to gate gait
111
- // and set eye height.
112
- controller.state.posture = FirstPersonPosture.Hang;
113
-
114
- controller.signals.onLedgeGrab.send1({
115
- ledgeHeight: this._edgeY - controller.pose.rootPosition.y,
116
- });
117
- }
118
-
119
- canInterrupt() {
120
- // Wall-run / wall-jump can preempt if their conditions
121
- // somehow hold (unusual mid-hang). Higher-priority abilities
122
- // shouldn't be blocked.
123
- return true;
124
- }
125
-
126
- tick(controller, runtime, bodyTransform, dt, _system) {
127
- const cfg = controller.config.ledgeGrab;
128
- const sensors = runtime.sensors;
129
-
130
- // Re-assert posture each tick (defence against any layer
131
- // resetting it before L2 reads).
132
- controller.state.posture = FirstPersonPosture.Hang;
133
- // Body is parked against the wall under the ledge — no lean.
134
- runtime.leanTargetRad = 0;
135
-
136
- // -- Exit: lost the ledge (shuffled past the edge, ledge ended).
137
- if (!sensors.ledgeAhead.hit) {
138
- controller.signals.onLedgeRelease.send1({ reason: "shuffle-off" });
139
- return false;
140
- }
141
-
142
- // -- Exit: jump rising-edge mantle-up. Apply small upward
143
- // velocity so the next tick's mantle.canActivate finds an
144
- // obstacleAhead + ledgeAhead at the right relative height.
145
- if (controller.intent.jump && !runtime.prevJumpHeld) {
146
- runtime.velocityY = runtime.jumpInitialVy * cfg.mantleUpUpFactor;
147
- runtime.prevJumpHeld = true;
148
- runtime.midJump = true;
149
- controller.state.isAscending = true;
150
- controller.signals.onLedgeRelease.send1({ reason: "mantle-up" });
151
- return false;
152
- }
153
-
154
- // -- Exit: back-intent drop. Zero velocity (let gravity take over
155
- // on the next tick).
156
- if (controller.intent.move.y < -0.1) {
157
- controller.signals.onLedgeRelease.send1({ reason: "drop" });
158
- return false;
159
- }
160
-
161
- // -- Exit: crouch → drop.
162
- if (controller.intent.crouch) {
163
- controller.signals.onLedgeRelease.send1({ reason: "drop" });
164
- return false;
165
- }
166
-
167
- // -- Exit: exertion saturated → slip.
168
- if (controller.state.exertion >= 1.0) {
169
- controller.signals.onLedgeRelease.send1({ reason: "slip" });
170
- return false;
171
- }
172
-
173
- // -- Hold: snap to hang position, zero velocity, tick exertion.
174
- // Pull the body slightly back from the ledge edge along the
175
- // body's forward direction so it visually grips the lip rather
176
- // than sitting directly on it.
177
- const fx = Math.sin(runtime.bodyYaw);
178
- const fz = Math.cos(runtime.bodyYaw);
179
- bodyTransform.position.set(
180
- this._edgeX + fx * cfg.hangOffsetForward,
181
- this._edgeY + cfg.hangOffsetY,
182
- this._edgeZ + fz * cfg.hangOffsetForward,
183
- );
184
- runtime.velocityX = 0;
185
- runtime.velocityY = 0;
186
- runtime.velocityZ = 0;
187
- controller.state.grounded = false;
188
-
189
- // Exertion rises while hanging fatigue. Mass-scaled like the
190
- // rest of the exertion system.
191
- controller.state.exertion = clamp(
192
- controller.state.exertion + cfg.exertionRiseRate * dt
193
- * runtime.massRatios.exertionRiseScale,
194
- 0, 1,
195
- );
196
-
197
- return true;
198
- }
199
- }
1
+ import { clamp } from "../../../../core/math/clamp.js";
2
+ import { FirstPersonPosture } from "../pose/FirstPersonPosture.js";
3
+ import { Ability } from "./Ability.js";
4
+
5
+ /**
6
+ * Ledge-grab ability — snap to a forward ledge while descending and hang
7
+ * from it until the player chooses (or is forced) to release.
8
+ *
9
+ * Activation:
10
+ * - `sensors.ledgeAhead.hit` (a forward+up obstacle probe found a grab-
11
+ * able edge — typically the same probe mantle uses).
12
+ * - `runtime.velocityY <= 0` — the player is descending or static. We
13
+ * don't auto-catch ledges on the way UP — that would feel like the
14
+ * world is grabbing the player mid-jump.
15
+ * - `state.airborneTime >= minAirborneTime` — leaves a small window
16
+ * after takeoff where ledge-grab can't fire, even if the player is
17
+ * instantly at apex (which would otherwise satisfy `velocityY <= 0`).
18
+ * - Forward intent (`intent.move.y > 0.1`) — the player must be
19
+ * committing into the ledge. Walking backwards off an edge shouldn't
20
+ * auto-catch the lip.
21
+ *
22
+ * Behaviour:
23
+ * - Body position snaps to a "hang from edge" pose: at the ledge edge
24
+ * X/Z (with a small back-offset so the body is pulled against the
25
+ * wall below), and Y offset down by ~bodyHeight so the hands grip
26
+ * the edge with the body suspended below.
27
+ * - Velocity is zeroed each tick — the body is parked.
28
+ * - Exertion rises at `cfg.exertionRiseRate` per second, scaled by
29
+ * mass. Climbing fatigue: hang too long and the next exit is
30
+ * forced (a slip).
31
+ * - Grounded stays false (the player is hanging in air).
32
+ *
33
+ * Exit:
34
+ * - Jump pressed (rising edge) → mantle-up. Release with a small
35
+ * upward velocity so mantle's ledgeAhead probe still hits next
36
+ * tick; mantle (priority 30) then takes over and animates the
37
+ * climb-onto-surface path.
38
+ * - Back-intent (`intent.move.y < -0.1`) → drop. Release with zero
39
+ * velocity; the player falls under base gravity.
40
+ * - Crouch pressed → drop. Same as back-intent.
41
+ * - Exertion saturates (`>= 1`) → slip. Release with zero velocity.
42
+ * - `sensors.ledgeAhead.hit` becomes false → shuffled off the edge.
43
+ * Release with zero velocity.
44
+ *
45
+ * Priority 40 — above mantle (30), below wall-run (50). Mantle CAN'T
46
+ * preempt an active ledge-grab — that's the point of placing ledge
47
+ * higher. Otherwise mantle would fire mid-hang and snap the body away.
48
+ * Wall-run and wall-jump can preempt — but in practice they need lateral
49
+ * walls + speed (wall-run) or a jump press near a side wall (wall-jump),
50
+ * conditions which rarely hold while hanging.
51
+ *
52
+ * @author Alex Goldring
53
+ * @copyright Company Named Limited (c) 2026
54
+ */
55
+ export class LedgeGrab extends Ability {
56
+ constructor() {
57
+ super();
58
+ this.name = "LedgeGrab";
59
+ this.priority = 40;
60
+
61
+ /** @private Wall-face anchor (x,z), the face's outward normal (x,z),
62
+ * and the top-edge height (y) — captured at onActivate. The hang pose
63
+ * is built from the WALL, not from the (possibly oblique) body
64
+ * facing, so it stays just outside the wall at any approach angle. */
65
+ this._anchorX = 0;
66
+ this._anchorZ = 0;
67
+ this._nx = 0;
68
+ this._nz = 0;
69
+ this._edgeY = 0;
70
+ }
71
+
72
+ canActivate(controller, runtime, sensors) {
73
+ const cfg = controller.config.ledgeGrab;
74
+ if (!cfg) return false;
75
+ if (sensors === undefined || sensors === null) return false;
76
+
77
+ if (!sensors.ledgeAhead.hit) return false;
78
+ if (runtime.velocityY > 0) return false; // not on the way up
79
+ if (controller.state.grounded) return false;
80
+ if (controller.state.airborneTime < cfg.minAirborneTime) return false;
81
+ if (controller.intent.move.y < 0.1) return false;
82
+ // Crouch or exhausted: don't re-grab. These are the same exit
83
+ // conditions that would IMMEDIATELY release after activation, so
84
+ // gating them here prevents a same-tick re-activation flicker
85
+ // after a crouch-release or slip.
86
+ if (controller.intent.crouch) return false;
87
+ if (controller.state.exertion >= 1.0) return false;
88
+
89
+ return true;
90
+ }
91
+
92
+ onActivate(controller, runtime) {
93
+ const sensors = runtime.sensors;
94
+
95
+ // Anchor on the wall face + its outward normal (ledgeAhead is only
96
+ // probed when obstacleAhead hits, so the face data is always present
97
+ // here). The top-edge height comes from the ledge probe.
98
+ this._anchorX = sensors.obstacleAhead.point.x;
99
+ this._anchorZ = sensors.obstacleAhead.point.z;
100
+ this._nx = sensors.obstacleAhead.normal.x;
101
+ this._nz = sensors.obstacleAhead.normal.z;
102
+ this._edgeY = sensors.ledgeAhead.point.y;
103
+
104
+ // Zero velocity — the body is now parked on the ledge.
105
+ runtime.velocityX = 0;
106
+ runtime.velocityY = 0;
107
+ runtime.velocityZ = 0;
108
+
109
+ // Clear any in-progress jump state hanging supersedes them.
110
+ runtime.midJump = false;
111
+ runtime.apexFired = false;
112
+ controller.state.isVariableJumpCut = false;
113
+ controller.state.isAscending = false;
114
+
115
+ // The body is hanging — not on the ground.
116
+ controller.state.grounded = false;
117
+
118
+ // Body is suspended by the hands; no striding feet, head near
119
+ // the hands gripping the edge. L2 reads posture to gate gait
120
+ // and set eye height.
121
+ controller.state.posture = FirstPersonPosture.Hang;
122
+
123
+ controller.signals.onLedgeGrab.send1({
124
+ ledgeHeight: this._edgeY - controller.pose.rootPosition.y,
125
+ });
126
+ }
127
+
128
+ canInterrupt() {
129
+ // Wall-run / wall-jump can preempt if their conditions
130
+ // somehow hold (unusual mid-hang). Higher-priority abilities
131
+ // shouldn't be blocked.
132
+ return true;
133
+ }
134
+
135
+ tick(controller, runtime, bodyTransform, dt, _system) {
136
+ const cfg = controller.config.ledgeGrab;
137
+ const sensors = runtime.sensors;
138
+
139
+ // Re-assert posture each tick (defence against any layer
140
+ // resetting it before L2 reads).
141
+ controller.state.posture = FirstPersonPosture.Hang;
142
+ // Body is parked against the wall under the ledge — no lean.
143
+ runtime.leanTargetRad = 0;
144
+
145
+ // -- Exit: lost the ledge (shuffled past the edge, ledge ended).
146
+ if (!sensors.ledgeAhead.hit) {
147
+ controller.signals.onLedgeRelease.send1({ reason: "shuffle-off" });
148
+ return false;
149
+ }
150
+
151
+ // -- Exit: jump rising-edge → mantle-up. Apply small upward
152
+ // velocity so the next tick's mantle.canActivate finds an
153
+ // obstacleAhead + ledgeAhead at the right relative height.
154
+ // (On a top too thin to STAND on, mantle is gated, so this currently
155
+ // just bumps you up and re-grabs — a clean "pull up & OVER" there
156
+ // needs a scripted vault, tracked as a follow-up.)
157
+ if (controller.intent.jump && !runtime.prevJumpHeld) {
158
+ runtime.velocityY = runtime.jumpInitialVy * cfg.mantleUpUpFactor;
159
+ runtime.prevJumpHeld = true;
160
+ runtime.midJump = true;
161
+ controller.state.isAscending = true;
162
+ controller.signals.onLedgeRelease.send1({ reason: "mantle-up" });
163
+ return false;
164
+ }
165
+
166
+ // -- Exit: back-intent → drop. Zero velocity (let gravity take over
167
+ // on the next tick).
168
+ if (controller.intent.move.y < -0.1) {
169
+ controller.signals.onLedgeRelease.send1({ reason: "drop" });
170
+ return false;
171
+ }
172
+
173
+ // -- Exit: crouch drop.
174
+ if (controller.intent.crouch) {
175
+ controller.signals.onLedgeRelease.send1({ reason: "drop" });
176
+ return false;
177
+ }
178
+
179
+ // -- Exit: exertion saturated → slip.
180
+ if (controller.state.exertion >= 1.0) {
181
+ controller.signals.onLedgeRelease.send1({ reason: "slip" });
182
+ return false;
183
+ }
184
+
185
+ // -- Hold: snap to hang position, zero velocity, tick exertion.
186
+ // Hang just OUTSIDE the wall face, gripping the top edge: the body
187
+ // centre sits a capsule radius (+ a small face gap) out from the face
188
+ // along its outward normal. Anchoring on the wall — not the body's
189
+ // facing keeps the body clear of the wall even when the ledge was
190
+ // reached at an oblique angle (e.g. off a wall-run into the wall),
191
+ // where a facing-relative offset would sink it into a thin wall.
192
+ const standoff = controller.config.body.radius + cfg.hangOffsetForward;
193
+ bodyTransform.position.set(
194
+ this._anchorX + this._nx * standoff,
195
+ this._edgeY + cfg.hangOffsetY,
196
+ this._anchorZ + this._nz * standoff,
197
+ );
198
+ runtime.velocityX = 0;
199
+ runtime.velocityY = 0;
200
+ runtime.velocityZ = 0;
201
+ controller.state.grounded = false;
202
+
203
+ // Exertion rises while hanging — fatigue. Mass-scaled like the
204
+ // rest of the exertion system.
205
+ controller.state.exertion = clamp(
206
+ controller.state.exertion + cfg.exertionRiseRate * dt
207
+ * runtime.massRatios.exertionRiseScale,
208
+ 0, 1,
209
+ );
210
+
211
+ return true;
212
+ }
213
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"Mantle.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/abilities/Mantle.js"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH;IAOQ,6CAA6C;IAC7C,WAAW;IACX,uCAAuC;IACvC,kBAAoB;IACpB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,cAAc;IACd,cAAc;IACd,cAAc;IAGlB,kEAwBC;IAED,gDAyCC;IAED,wBAGC;IAED,wFAwCC;IAED,oDAEC;CACJ;wBA1LuB,cAAc"}
1
+ {"version":3,"file":"Mantle.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/abilities/Mantle.js"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH;IAOQ,6CAA6C;IAC7C,WAAW;IACX,uCAAuC;IACvC,kBAAoB;IACpB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,cAAc;IACd,cAAc;IACd,cAAc;IAGlB,kEA+BC;IAED,gDAyCC;IAED,wBAGC;IAED,wFAwCC;IAED,oDAEC;CACJ;wBAjMuB,cAAc"}