@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,551 +1,552 @@
1
- /**
2
- * Tunable parameters for {@link FirstPersonPlayerController}. The full
3
- * config is the only serialized portion of the component — everything else
4
- * (intent, state, pose, signals) is transient and rebuilt on link.
5
- *
6
- * Defaults aim for a baseline "human soldier" feel — see DESIGN.md §4.1.
7
- * Every value is exposed so designers can mod it to taste.
8
- */
9
- export class FirstPersonPlayerControllerConfig {
10
- body = {
11
- /** Standing eye height, meters above body origin. */
12
- height: 1.80,
13
- /** Crouched eye height. */
14
- crouchHeight: 0.8,
15
- /** Prone eye height (used by slide). Body is horizontal — eye
16
- * just above ground level, head turned toward the direction
17
- * of travel. */
18
- proneHeight: 0.4,
19
- /** Capsule radius, for downstream physics. */
20
- radius: 0.34,
21
- /** Kilograms — affects bob, jump, landing, accel, and exertion via massRatio. */
22
- mass: 80,
23
- /** Reference mass against which `mass` is normalized (massRatio = mass/refMass). */
24
- referenceMass: 80,
25
- /**
26
- * How strongly `mass` couples into derived effects. 0 = mass is
27
- * cosmetic; 1.0 = full coupling (heavier ⇒ lower jump, lower accel,
28
- * harder landing, faster exhaustion). At 1.0 a 25% mass increase
29
- * yields:
30
- * jump peak ≈ 0.83× (∝ 1/√massRatio — kinematically motivated)
31
- * ground accel ≈ 0.80× (∝ 1/massRatio — F = ma at fixed force)
32
- * landing dip ≈ 1.25× (∝ massRatio — higher momentum to absorb)
33
- * exertion rise ≈ 1.25× (∝ massRatio — more body to move)
34
- */
35
- massCouplingStrength: 1.0,
36
- };
37
-
38
- // Defaults are tuned toward an "athlete with juice" feel — plausible
39
- // but biased for fun: snappy accel, fast top speed, generous air
40
- // control, satisfying jump. Realistic-soldier values would roughly
41
- // halve walk/sprint/airControl and use peakHeight ≈ 1.0–1.2.
42
- motion = {
43
- walkSpeed: 4.5, // brisk jog — quicker than a real walk
44
- sprintSpeed: 9.0, // fast but sub-Olympic (~12 m/s is human peak)
45
- crouchSpeed: 2.2,
46
- /** [0..1] how much intent influences velocity while airborne. */
47
- airControl: 0.5,
48
- /**
49
- * Half-life (seconds) for the mono-exponential approach to top
50
- * sprint speed on the ground. The controller's ground-accel
51
- * model is Hill-based:
52
- * v(t) = v_max · (1 − e^(−t / τ))
53
- * with τ = halfLife / ln 2. At each tick the velocity halves
54
- * the remaining gap to `targetSpeed` every `halfLife` seconds.
55
- *
56
- * Default `0.116 s` gives ~95 % of `sprintSpeed` reached in
57
- * 500 ms — the Apex Legends / Titanfall / modern CoD ramp
58
- * window. The curve is naturally non-uniform: roughly 45 %
59
- * of top speed in the first 100 ms, then the last 5 % takes
60
- * 300 ms — so a quick tap-and-go still feels snappy while
61
- * follow-up moves (slide, sprint-jump) require committed
62
- * sprint time to be at top speed.
63
- *
64
- * Literature: Hill 1927 / Furusawa-Hill 1928 (force-velocity
65
- * curve, original analytical formulation); Morin & Samozino
66
- * 2016 (modern sprint profiling using the same mono-exp fit).
67
- */
68
- groundAccelHalfLife: 0.116,
69
- /** m/s^2 horizontal deceleration toward 0 when intent is zero. */
70
- groundDecel: 75, // crisp stops, no skating
71
- /** m/s^2 horizontal acceleration while airborne. */
72
- airAccel: 14, // mid-air agility for course-correction
73
- /**
74
- * Top-speed multiplier applied while the move intent has a backward
75
- * component (intent.move.y < 0). Backpedaling is mechanically slower
76
- * than running forward — this also enforces a hard cap rather than
77
- * letting "backward sprint" reach the same speed as forward sprint.
78
- */
79
- backwardSpeedFactor: 0.65,
80
- /**
81
- * Acceleration multiplier while moving backward. Body has worse
82
- * mechanical advantage when reversing — slower to start, slower to
83
- * stop, sells the "fighting the gait" feel.
84
- */
85
- backwardAccelFactor: 0.65,
86
- };
87
-
88
- jump = {
89
- /** Apex height of a clean jump, meters. */
90
- peakHeight: 1.8, // matches eye height — the iconic "athlete leap"
91
- /** Seconds to reach apex from launch. Together these two derive g. */
92
- timeToApex: 0.35, // snappier rise
93
- coyoteTime: 0.14, // generous for forgiving platforming
94
- bufferTime: 0.16,
95
- /** Gravity multiplier while falling (vy <= 0). */
96
- fallGravityMult: 2.4, // fast fall — classic Mario/Sonic feel
97
- /** Gravity multiplier when jump released early during ascent. */
98
- cutGravityMult: 2.0, // sharper variable-height differentiation
99
- anticipation: {
100
- duration: 0.06, // short — keeps jumps responsive
101
- dipAmount: 0.08, // but visibly squashes
102
- },
103
- };
104
-
105
- landing = {
106
- /** m/s impact threshold; below = soft landing signal. */
107
- softThreshold: 3.0,
108
- /** m/s impact threshold; above = hard landing signal. */
109
- hardThreshold: 7.5,
110
- recovery: {
111
- /** Meters of eye dip per m/s of impact velocity. */
112
- dipPerVy: 0.016, // beefier squash on landing
113
- /** Clamp on total dip. */
114
- dipMax: 0.24,
115
- spring: {
116
- halfLife: 0.11,
117
- /** zeta < 1 → rings once before settling, reads as compression. */
118
- zeta: 0.50, // a touch more bounce
119
- },
120
- },
121
- };
122
-
123
- crouch = {
124
- /** Seconds to lerp eye height between standing and crouched. */
125
- transitionTime: 0.18,
126
- /** "hold" or "toggle" */
127
- mode: "hold",
128
- };
129
-
130
- /**
131
- * Wall-run ability — airborne + lateral wall + speed → run along the
132
- * wall with reduced gravity. Camera tilts into the wall via the
133
- * shared lean spring (consistent with the rest of the camera).
134
- * Fields here are consumed by the {@link WallRun} ability when
135
- * registered.
136
- */
137
- wallRun = {
138
- /**
139
- * Minimum horizontal speed (m/s) to engage wall-run. Set ABOVE
140
- * walkSpeed (4.5) so a wall-run is earned by a committed RUN — a walk
141
- * into the wall, or falling alongside it with leftover momentum,
142
- * shouldn't grab on. Below the ~7 m/s a sprint-into-wall carries.
143
- */
144
- minSpeed: 5.5,
145
- /** Player must be airborne at least this long before wall-run can
146
- * engage — prevents wall-run triggering off a jump's first frames. */
147
- minAirborneTime: 0.1,
148
- /** Maximum wall-run duration (s). Auto-exits at the end. */
149
- maxDuration: 2.0,
150
- /** Multiplier on base gravity while wall-running. Lower = longer hang. */
151
- gravityFactor: 0.25,
152
- /** Camera tilt magnitude (deg) into the wall. Disabled by the
153
- * motion-sickness toggle alongside the standard lean. */
154
- cameraRollDeg: 12,
155
- };
156
-
157
- /**
158
- * Wall-jump ability — instantaneous off-wall impulse. Fires from
159
- * either an active wall-run state or from near-wall airborne (a
160
- * short window where the player is close enough to push off).
161
- */
162
- wallJump = {
163
- /** Impulse magnitude away from the wall (m/s, applied along wall normal). */
164
- outwardImpulse: 6.0,
165
- /** Upward component multiplier on jumpInitialVy. */
166
- upFactor: 0.9,
167
- /** Max distance to wall (m) at which a near-wall-airborne wall-jump
168
- * is allowed (when not already wall-running). */
169
- nearWallMaxDistance: 0.6,
170
- };
171
-
172
- /**
173
- * Ledge-grab ability — snap to a forward ledge while descending and
174
- * hang from it until the player releases. Climbing fatigue is
175
- * modelled via the shared exertion channel: hang too long and you
176
- * slip. Fields here are consumed by the {@link LedgeGrab} ability
177
- * if registered; ignored otherwise.
178
- */
179
- ledgeGrab = {
180
- /** Body Y offset from ledge top while hanging (negative — feet
181
- * below the ledge so the body looks like it's gripping the
182
- * edge). */
183
- hangOffsetY: -1.4,
184
- /** Forward offset from the ledge edge (negative = pulled BACK
185
- * from the edge so the body is against the wall under the
186
- * ledge). */
187
- hangOffsetForward: -0.2,
188
- /** Exertion rise per second while hanging — drives slip-by-
189
- * fatigue. The base exertion rise scale (massRatios) applies. */
190
- exertionRiseRate: 0.15,
191
- /** Player must be airborne at least this long before a ledge-
192
- * grab can engage prevents auto-catching mid-jump. */
193
- minAirborneTime: 0.3,
194
- /** Vertical kick (multiplier on jumpInitialVy) given on a
195
- * mantle-up release. Just enough to put the body above the
196
- * ledge so mantle's ledgeAhead probe still resolves correctly
197
- * on the next tick. */
198
- mantleUpUpFactor: 0.3,
199
- };
200
-
201
- /**
202
- * Mantle ability — auto-triggers when the player approaches a
203
- * reachable obstacle with forward intent. The body follows a scripted
204
- * path (horizontal + vertical eases) onto the surface; player input
205
- * is suspended for the path's duration. Fields here are consumed by
206
- * the {@link Mantle} ability if registered; ignored otherwise.
207
- */
208
- mantle = {
209
- /** Below this height (m above feet), the obstacle is just a step
210
- * the player can walk over no mantle. */
211
- minHeight: 0.4,
212
- /** Above this, the surface is out of reach — no mantle. ~1.4m
213
- * is chest height for a 1.8m body, which is the canonical
214
- * "obstacle you can climb on but not over without help". */
215
- maxHeight: 1.4,
216
- /** Total scripted-path duration (seconds). Short enough that the
217
- * brief loss of player control isn't frustrating. */
218
- duration: 0.5,
219
- /** How far past the obstacle edge the player lands (m). Slight
220
- * forward bias so the player ends on top of the surface, not
221
- * on the edge. */
222
- forwardOffsetOnLand: 0.4,
223
- };
224
-
225
- /**
226
- * Slide ability — activates from sprint+crouch when grounded. The
227
- * fields here are read by the {@link Slide} ability if it's registered
228
- * on the controller; unused if Slide is not in use.
229
- */
230
- slide = {
231
- /**
232
- * Required horizontal speed to start a slide. Below this,
233
- * crouch + sprint just transitions to a normal crouched walk.
234
- *
235
- * The default — `7.5 m/s` — sits between walkSpeed (4.5) and
236
- * sprintSpeed (9), at ~83 % of sprint. Earning the slide is
237
- * enforced by the ground-accel model: with
238
- * `groundAccelHalfLife = 0.116 s`, reaching this threshold
239
- * from standstill takes ~300 ms of sustained sprint — well
240
- * past a normal reaction window. Walking off a ledge or
241
- * accelerating down a slope into a slide is explicitly
242
- * supported: anything that brings the player's horizontal
243
- * speed above this threshold lets crouch engage a slide.
244
- * (Following Apex Legends' velocity-only gate at 200 hu/s.)
245
- */
246
- minEntrySpeed: 7.5,
247
- /** Auto-exit threshold — slide ends when speed drops below this. */
248
- endSpeed: 2.0,
249
- /** Horizontal deceleration (m/s²) while sliding. Lower than the
250
- * normal groundDecel so slides preserve momentum longer. */
251
- friction: 3.0,
252
- /** Non-interruptible window (seconds) after slide start — higher-
253
- * priority abilities can't preempt during this window so the
254
- * player isn't yanked out the instant they commit to a slide. */
255
- startupWindow: 0.15,
256
- /** Multiplier on the jump initial Vy for slide-jump. 1.0 = same
257
- * height as a standing jump; >1 gives slide-jump some extra
258
- * reward for chaining the input. */
259
- slideJumpUpFactor: 1.05,
260
- /** Vertical-impact-spring kick (m/s) at slide entry — the
261
- * satisfying "thud" of dropping into the slide. */
262
- entryImpactVelocity: 0.6,
263
- };
264
-
265
- bob = {
266
- stepFreqAtWalk: 1.8,
267
- stepFreqExp: 0.75, // sub-linear so sprint isn't frantic
268
- verticalAmpAtWalk: 0.055,
269
- lateralAmpAtWalk: 0.030,
270
- /** Extra amplitude per kg above 80kg (reference mass). */
271
- ampMassScale: 0.005,
272
- rollAtWalkDeg: 0.8, // a touch more visible head sway
273
- /** Speeds below this don't tick the stride at all. */
274
- minStepSpeed: 0.2,
275
- /**
276
- * Half-life of the bob-amplitude envelope (seconds). When the player
277
- * starts/stops moving, the bob amplitude springs toward its natural
278
- * value rather than cutting on/off so releasing a movement key
279
- * doesn't whiplash the head from full sway back to neutral. The
280
- * stride phase itself still freezes when stationary; only the
281
- * AMPLITUDE is temporally smoothed.
282
- */
283
- intensityHalfLife: 0.22,
284
- /**
285
- * Vertical impact spring — kicked downward at each footfall. The
286
- * spring's response shapes the actual vertical motion of the head:
287
- * a sharp dip (foot arresting the head's fall), recovery through
288
- * neutral, and a slight overshoot above neutral as the standing
289
- * leg injects energy into the body (the "push" phase). Models the
290
- * gait COM trajectory more accurately than a smooth sinusoid.
291
- */
292
- impactSpringHalfLife: 0.14,
293
- /** zeta < 1 → spring overshoots after impact, sells the leg-push. */
294
- impactSpringZeta: 0.50,
295
- /**
296
- * Multiplier converting "desired peak dip" (verticalAmpAtWalk + mass
297
- * boost, scaled by intensity and back-pedal factor) into the actual
298
- * velocity impulse delivered at footfall. Empirical tuned so the
299
- * default verticalAmpAtWalk produces the expected peak dip with the
300
- * spring params above.
301
- */
302
- impactKickMultiplier: 30,
303
- /**
304
- * Multipliers applied to bob amplitude when the player is moving
305
- * backward (interpolated by "backwardness" the negative dot of
306
- * velocity onto screen-forward, normalized to sprint speed). At
307
- * full back-pedal these are fully applied; at rest they have no
308
- * effect.
309
- *
310
- * Lateral is intentionally larger than vertical: real back-gait
311
- * has worse side-to-side balance than vertical compression, so
312
- * the visual wobble should track that. Roll boosts each footfall's
313
- * head-tilt for the same reason sells "unstable footing".
314
- */
315
- backwardVerticalAmpFactor: 1.2,
316
- backwardLateralAmpFactor: 1.4,
317
- backwardRollFactor: 1.3,
318
- /**
319
- * Bob roll × lean coupling: when the body is leaning into a turn
320
- * (lean spring displaced), the per-stride head roll is biased
321
- * toward the lean direction. At 0 the bob roll is symmetric
322
- * regardless of lean. At 1.0 the bob roll excursions in the lean
323
- * direction are doubled and the opposite-direction excursions go to
324
- * zero. Typical 0.3–0.6 reads as a cohesive "leaning while running"
325
- * gait. Normalized against cfg.lean.maxRollDeg so the coupling
326
- * stays proportional regardless of how aggressive lean is tuned.
327
- */
328
- leanCouplingFactor: 0.45,
329
- };
330
-
331
- breath = {
332
- rateRestHz: 0.23,
333
- rateMaxHz: 0.70,
334
- amplitudeRestM: 0.004,
335
- amplitudeMaxM: 0.022,
336
- pitchAmpRestDeg: 0.10,
337
- pitchAmpMaxDeg: 0.55,
338
- /** ±20% amplitude noise rides on the sine. */
339
- noiseAmount: 0.20,
340
- /**
341
- * Locomotor-respiratory coupling strength at full exertion. The
342
- * breath-rate target is blended between purely metabolic (driven by
343
- * exertion) and stride-aligned (an integer N strides per breath
344
- * cycle). At 0 no coupling; at 1 breath rate is fully pulled toward
345
- * the stride-aligned target.
346
- *
347
- * Coupling is SCALED by exertion — low exertion means the breath is
348
- * dictated by metabolic demand alone, high sustained effort lets
349
- * the diaphragm couple to gait impacts (mechanistically: heavier
350
- * breathing means deeper chest excursions, which the gait's
351
- * vertical accelerations bias toward integer ratios). Matches
352
- * empirical findings in dynamic activity (Bernasconi & Kohl 1993,
353
- * Bramble & Carrier 1983) much weaker than steady-state running.
354
- *
355
- * Frequency-coupled only, NOT phase-locked: breath rate matches a
356
- * sub-multiple of stride rate, but phase drifts. Holds the "loose"
357
- * character the user asked for.
358
- */
359
- locomotorCouplingMax: 0.45,
360
- /** Coupling does not engage below this stride frequency (Hz). At
361
- * walking pace your gait is too slow to mechanically influence
362
- * breath; coupling really shows up at jog+ pace. */
363
- couplingMinStrideFreqHz: 1.5,
364
- };
365
-
366
- exertion = {
367
- // -- Sustained drivers (continuous accumulation per second) ---
368
- /** Per-second accumulation while sprinting (mass-scaled internally). */
369
- sprintRiseRate: 0.20,
370
- /** Per-second decay toward 0 when not under sustained load. */
371
- idleDecayRate: 0.12,
372
-
373
- // -- Impulse drivers (one-shot bumps on discrete events) ------
374
- /** Bump on jump fire the muscular impulse of pushing off. */
375
- jumpRise: 0.08,
376
- /** Bump on crouch-enter — the knee-grip on absorbing into a crouch.
377
- * Small; we don't want crouch-spamming to instantly exhaust. */
378
- crouchEnterRise: 0.03,
379
- /**
380
- * Per-(m/s) impulse on landing — proportional to vertical impact
381
- * speed. A clean low-jump landing barely registers; a hard fall
382
- * delivers a noticeable bump. Clamped internally so an impossible
383
- * fall doesn't max out exertion in one tick.
384
- */
385
- landImpulsePerVy: 0.012,
386
- /** Max impulse magnitude added by any single landing. */
387
- landImpulseMax: 0.20,
388
-
389
- // -- Breath coupling (existing — unchanged contract) ----------
390
- /** How long after exertion subsides before breath rate normalizes. */
391
- rateDecayHalfLife: 4.0,
392
- /** How long after exertion subsides before breath amplitude normalizes. */
393
- ampDecayHalfLife: 2.0,
394
-
395
- // -- Output coupling: exertion affects how the body moves -----
396
- /** At full exertion, bob lateral amp is multiplied by 1 + this.
397
- * Sells "tired wobble" modest effect (10–20% recommended). */
398
- bobLateralBoostAtMax: 0.20,
399
- /** At full exertion, the head droops forward by this many degrees.
400
- * Sells visual fatigue without compromising aim. Subtle. */
401
- headDroopAtMaxDeg: 1.5,
402
- /** Spring half-life for the head-droop transition. */
403
- headDroopHalfLife: 0.6,
404
- };
405
-
406
- lean = {
407
- enabled: true,
408
- /** Degrees of roll per (lateral m/s^2 / 9.81). */
409
- maxRollDeg: 3.2, // more visible lean into turns sells the athleticism
410
- spring: { halfLife: 0.16, zeta: 1.0 },
411
- /**
412
- * Yaw-rate "look-lean": camera banks into mouse-driven turns even
413
- * without strafe input. Disable for motion-sickness sensitivity.
414
- */
415
- lookLeanEnabled: true,
416
- /**
417
- * Degrees of roll per rad/s of yaw rate. ~3 means a 180°/s turn
418
- * (slow camera pan) gives ~16.5° of bank; a fast mouse flick at
419
- * 5 rad/s (≈286°/s) would saturate against the clamp below.
420
- */
421
- lookLeanDegPerRadPerSec: 3.0,
422
- /** Yaw rate is clamped to ±this (rad/s) before scaling — prevents
423
- * single-frame flicks from spiking the lean target. */
424
- lookLeanYawRateClamp: 4.0,
425
- /**
426
- * Multiplier applied to look-lean (yaw-rate banking) when crouched.
427
- * Crouched stance is low and stable; banking from a mouse turn
428
- * reads as unmotivated. Default 0 fully disables; dial up to add
429
- * a subtle tilt without it feeling like you're running. The lateral-
430
- * acceleration lean is NOT affected by this it scales naturally
431
- * with crouch speeds since accel magnitudes drop.
432
- */
433
- crouchLookLeanFactor: 0.0,
434
- };
435
-
436
- fov = {
437
- base: 75, // wider baseline — more energetic
438
- sprintAdd: 7, // dramatic punch on sprint (Apex/Titanfall feel)
439
- crouchAdd: -3,
440
- smoothHalfLife: 0.18, // slightly snappier transitions
441
- };
442
-
443
- look = {
444
- pitchMinDeg: -85,
445
- pitchMaxDeg: 85,
446
- invertY: false,
447
- };
448
-
449
- /**
450
- * Postural shifts driven by intent — distinct from look (which is
451
- * player-driven) and from bob (which is procedural). Models
452
- * Mirror's Edge-style "commitment to motion": sprinting tilts the head
453
- * forward as the body leans into the run. Slow spring so it builds up
454
- * over a second or so rather than snapping it's a *posture* change,
455
- * not a flick.
456
- */
457
- posture = {
458
- /** Forward pitch (deg) applied to the eye at full sprint. Positive = look-down. */
459
- sprintForwardPitchDeg: 4.0,
460
- /** Spring half-life for the sprint pitch transition. */
461
- sprintForwardPitchHalfLife: 0.32,
462
- /** Small forward eye-position shift (m) at full sprint — head leads the hips. */
463
- sprintForwardShiftM: 0.04,
464
- };
465
-
466
- gravity = {
467
- /** Default gravity magnitude (m/s^2). Overridden by computeJumpFromApex once
468
- * the jump tunables are resolved; kept here so non-jumping entities (NPCs,
469
- * paused players) still have a sensible fall behavior. */
470
- magnitude: 9.81,
471
- };
472
-
473
- toJSON() {
474
- return {
475
- body: { ...this.body },
476
- motion: { ...this.motion },
477
- jump: { ...this.jump, anticipation: { ...this.jump.anticipation } },
478
- landing: {
479
- ...this.landing,
480
- recovery: {
481
- ...this.landing.recovery,
482
- spring: { ...this.landing.recovery.spring },
483
- },
484
- },
485
- crouch: { ...this.crouch },
486
- ledgeGrab: { ...this.ledgeGrab },
487
- mantle: { ...this.mantle },
488
- wallRun: { ...this.wallRun },
489
- wallJump: { ...this.wallJump },
490
- slide: { ...this.slide },
491
- bob: { ...this.bob },
492
- breath: { ...this.breath },
493
- exertion: { ...this.exertion },
494
- lean: { ...this.lean, spring: { ...this.lean.spring } },
495
- fov: { ...this.fov },
496
- look: { ...this.look },
497
- posture: { ...this.posture },
498
- gravity: { ...this.gravity },
499
- };
500
- }
501
-
502
- fromJSON(json) {
503
- if (json.body) Object.assign(this.body, json.body);
504
- if (json.motion) Object.assign(this.motion, json.motion);
505
- if (json.jump) {
506
- const { anticipation, ...rest } = json.jump;
507
- Object.assign(this.jump, rest);
508
- if (anticipation) Object.assign(this.jump.anticipation, anticipation);
509
- }
510
- if (json.landing) {
511
- const { recovery, ...rest } = json.landing;
512
- Object.assign(this.landing, rest);
513
- if (recovery) {
514
- const { spring, ...rrest } = recovery;
515
- Object.assign(this.landing.recovery, rrest);
516
- if (spring) Object.assign(this.landing.recovery.spring, spring);
517
- }
518
- }
519
- if (json.crouch) Object.assign(this.crouch, json.crouch);
520
- if (json.ledgeGrab) Object.assign(this.ledgeGrab, json.ledgeGrab);
521
- if (json.mantle) Object.assign(this.mantle, json.mantle);
522
- if (json.wallRun) Object.assign(this.wallRun, json.wallRun);
523
- if (json.wallJump) Object.assign(this.wallJump, json.wallJump);
524
- if (json.slide) Object.assign(this.slide, json.slide);
525
- if (json.bob) Object.assign(this.bob, json.bob);
526
- if (json.breath) Object.assign(this.breath, json.breath);
527
- if (json.exertion) Object.assign(this.exertion, json.exertion);
528
- if (json.lean) {
529
- const { spring, ...rest } = json.lean;
530
- Object.assign(this.lean, rest);
531
- if (spring) Object.assign(this.lean.spring, spring);
532
- }
533
- if (json.fov) Object.assign(this.fov, json.fov);
534
- if (json.look) Object.assign(this.look, json.look);
535
- if (json.posture) Object.assign(this.posture, json.posture);
536
- if (json.gravity) Object.assign(this.gravity, json.gravity);
537
- }
538
-
539
- /**
540
- * @param {FirstPersonPlayerControllerConfig} other
541
- */
542
- copy(other) {
543
- this.fromJSON(other.toJSON());
544
- }
545
-
546
- clone() {
547
- const c = new FirstPersonPlayerControllerConfig();
548
- c.copy(this);
549
- return c;
550
- }
551
- }
1
+ /**
2
+ * Tunable parameters for {@link FirstPersonPlayerController}. The full
3
+ * config is the only serialized portion of the component — everything else
4
+ * (intent, state, pose, signals) is transient and rebuilt on link.
5
+ *
6
+ * Defaults aim for a baseline "human soldier" feel — see DESIGN.md §4.1.
7
+ * Every value is exposed so designers can mod it to taste.
8
+ */
9
+ export class FirstPersonPlayerControllerConfig {
10
+ body = {
11
+ /** Standing eye height, meters above body origin. */
12
+ height: 1.80,
13
+ /** Crouched eye height. */
14
+ crouchHeight: 0.8,
15
+ /** Prone eye height (used by slide). Body is horizontal — eye
16
+ * just above ground level, head turned toward the direction
17
+ * of travel. */
18
+ proneHeight: 0.4,
19
+ /** Capsule radius, for downstream physics. */
20
+ radius: 0.34,
21
+ /** Kilograms — affects bob, jump, landing, accel, and exertion via massRatio. */
22
+ mass: 80,
23
+ /** Reference mass against which `mass` is normalized (massRatio = mass/refMass). */
24
+ referenceMass: 80,
25
+ /**
26
+ * How strongly `mass` couples into derived effects. 0 = mass is
27
+ * cosmetic; 1.0 = full coupling (heavier ⇒ lower jump, lower accel,
28
+ * harder landing, faster exhaustion). At 1.0 a 25% mass increase
29
+ * yields:
30
+ * jump peak ≈ 0.83× (∝ 1/√massRatio — kinematically motivated)
31
+ * ground accel ≈ 0.80× (∝ 1/massRatio — F = ma at fixed force)
32
+ * landing dip ≈ 1.25× (∝ massRatio — higher momentum to absorb)
33
+ * exertion rise ≈ 1.25× (∝ massRatio — more body to move)
34
+ */
35
+ massCouplingStrength: 1.0,
36
+ };
37
+
38
+ // Defaults are tuned toward an "athlete with juice" feel — plausible
39
+ // but biased for fun: snappy accel, fast top speed, generous air
40
+ // control, satisfying jump. Realistic-soldier values would roughly
41
+ // halve walk/sprint/airControl and use peakHeight ≈ 1.0–1.2.
42
+ motion = {
43
+ walkSpeed: 4.5, // brisk jog — quicker than a real walk
44
+ sprintSpeed: 9.0, // fast but sub-Olympic (~12 m/s is human peak)
45
+ crouchSpeed: 2.2,
46
+ /** [0..1] how much intent influences velocity while airborne. */
47
+ airControl: 0.5,
48
+ /**
49
+ * Half-life (seconds) for the mono-exponential approach to top
50
+ * sprint speed on the ground. The controller's ground-accel
51
+ * model is Hill-based:
52
+ * v(t) = v_max · (1 − e^(−t / τ))
53
+ * with τ = halfLife / ln 2. At each tick the velocity halves
54
+ * the remaining gap to `targetSpeed` every `halfLife` seconds.
55
+ *
56
+ * Default `0.116 s` gives ~95 % of `sprintSpeed` reached in
57
+ * 500 ms — the Apex Legends / Titanfall / modern CoD ramp
58
+ * window. The curve is naturally non-uniform: roughly 45 %
59
+ * of top speed in the first 100 ms, then the last 5 % takes
60
+ * 300 ms — so a quick tap-and-go still feels snappy while
61
+ * follow-up moves (slide, sprint-jump) require committed
62
+ * sprint time to be at top speed.
63
+ *
64
+ * Literature: Hill 1927 / Furusawa-Hill 1928 (force-velocity
65
+ * curve, original analytical formulation); Morin & Samozino
66
+ * 2016 (modern sprint profiling using the same mono-exp fit).
67
+ */
68
+ groundAccelHalfLife: 0.116,
69
+ /** m/s^2 horizontal deceleration toward 0 when intent is zero. */
70
+ groundDecel: 75, // crisp stops, no skating
71
+ /** m/s^2 horizontal acceleration while airborne. */
72
+ airAccel: 14, // mid-air agility for course-correction
73
+ /**
74
+ * Top-speed multiplier applied while the move intent has a backward
75
+ * component (intent.move.y < 0). Backpedaling is mechanically slower
76
+ * than running forward — this also enforces a hard cap rather than
77
+ * letting "backward sprint" reach the same speed as forward sprint.
78
+ */
79
+ backwardSpeedFactor: 0.65,
80
+ /**
81
+ * Acceleration multiplier while moving backward. Body has worse
82
+ * mechanical advantage when reversing — slower to start, slower to
83
+ * stop, sells the "fighting the gait" feel.
84
+ */
85
+ backwardAccelFactor: 0.65,
86
+ };
87
+
88
+ jump = {
89
+ /** Apex height of a clean jump, meters. */
90
+ peakHeight: 1.8, // matches eye height — the iconic "athlete leap"
91
+ /** Seconds to reach apex from launch. Together these two derive g. */
92
+ timeToApex: 0.35, // snappier rise
93
+ coyoteTime: 0.14, // generous for forgiving platforming
94
+ bufferTime: 0.16,
95
+ /** Gravity multiplier while falling (vy <= 0). */
96
+ fallGravityMult: 2.4, // fast fall — classic Mario/Sonic feel
97
+ /** Gravity multiplier when jump released early during ascent. */
98
+ cutGravityMult: 2.0, // sharper variable-height differentiation
99
+ anticipation: {
100
+ duration: 0.06, // short — keeps jumps responsive
101
+ dipAmount: 0.08, // but visibly squashes
102
+ },
103
+ };
104
+
105
+ landing = {
106
+ /** m/s impact threshold; below = soft landing signal. */
107
+ softThreshold: 3.0,
108
+ /** m/s impact threshold; above = hard landing signal. */
109
+ hardThreshold: 7.5,
110
+ recovery: {
111
+ /** Meters of eye dip per m/s of impact velocity. */
112
+ dipPerVy: 0.016, // beefier squash on landing
113
+ /** Clamp on total dip. */
114
+ dipMax: 0.24,
115
+ spring: {
116
+ halfLife: 0.11,
117
+ /** zeta < 1 → rings once before settling, reads as compression. */
118
+ zeta: 0.50, // a touch more bounce
119
+ },
120
+ },
121
+ };
122
+
123
+ crouch = {
124
+ /** Seconds to lerp eye height between standing and crouched. */
125
+ transitionTime: 0.18,
126
+ /** "hold" or "toggle" */
127
+ mode: "hold",
128
+ };
129
+
130
+ /**
131
+ * Wall-run ability — airborne + lateral wall + speed → run along the
132
+ * wall with reduced gravity. Camera tilts into the wall via the
133
+ * shared lean spring (consistent with the rest of the camera).
134
+ * Fields here are consumed by the {@link WallRun} ability when
135
+ * registered.
136
+ */
137
+ wallRun = {
138
+ /**
139
+ * Minimum horizontal speed (m/s) to engage wall-run. Set ABOVE
140
+ * walkSpeed (4.5) so a wall-run is earned by a committed RUN — a walk
141
+ * into the wall, or falling alongside it with leftover momentum,
142
+ * shouldn't grab on. Below the ~7 m/s a sprint-into-wall carries.
143
+ */
144
+ minSpeed: 5.5,
145
+ /** Player must be airborne at least this long before wall-run can
146
+ * engage — prevents wall-run triggering off a jump's first frames. */
147
+ minAirborneTime: 0.1,
148
+ /** Maximum wall-run duration (s). Auto-exits at the end. */
149
+ maxDuration: 2.0,
150
+ /** Multiplier on base gravity while wall-running. Lower = longer hang. */
151
+ gravityFactor: 0.25,
152
+ /** Camera tilt magnitude (deg) into the wall. Disabled by the
153
+ * motion-sickness toggle alongside the standard lean. */
154
+ cameraRollDeg: 12,
155
+ };
156
+
157
+ /**
158
+ * Wall-jump ability — instantaneous off-wall impulse. Fires from
159
+ * either an active wall-run state or from near-wall airborne (a
160
+ * short window where the player is close enough to push off).
161
+ */
162
+ wallJump = {
163
+ /** Impulse magnitude away from the wall (m/s, applied along wall normal). */
164
+ outwardImpulse: 6.0,
165
+ /** Upward component multiplier on jumpInitialVy. */
166
+ upFactor: 0.9,
167
+ /** Max distance to wall (m) at which a near-wall-airborne wall-jump
168
+ * is allowed (when not already wall-running). */
169
+ nearWallMaxDistance: 0.6,
170
+ };
171
+
172
+ /**
173
+ * Ledge-grab ability — snap to a forward ledge while descending and
174
+ * hang from it until the player releases. Climbing fatigue is
175
+ * modelled via the shared exertion channel: hang too long and you
176
+ * slip. Fields here are consumed by the {@link LedgeGrab} ability
177
+ * if registered; ignored otherwise.
178
+ */
179
+ ledgeGrab = {
180
+ /** Body Y offset from ledge top while hanging (negative — feet
181
+ * below the ledge so the body looks like it's gripping the
182
+ * edge). */
183
+ hangOffsetY: -1.4,
184
+ /** Gap (m) the hanging body's front sits OUT from the wall face —
185
+ * added to the capsule radius to form the standoff along the wall's
186
+ * outward normal. Small positive = front just clears the face so the
187
+ * body grips the lip without clipping the wall. */
188
+ hangOffsetForward: 0.05,
189
+ /** Exertion rise per second while hanging drives slip-by-
190
+ * fatigue. The base exertion rise scale (massRatios) applies. */
191
+ exertionRiseRate: 0.15,
192
+ /** Player must be airborne at least this long before a ledge-
193
+ * grab can engage — prevents auto-catching mid-jump. */
194
+ minAirborneTime: 0.3,
195
+ /** Vertical kick (multiplier on jumpInitialVy) given on a
196
+ * mantle-up release. Just enough to put the body above the
197
+ * ledge so mantle's ledgeAhead probe still resolves correctly
198
+ * on the next tick. */
199
+ mantleUpUpFactor: 0.3,
200
+ };
201
+
202
+ /**
203
+ * Mantle ability auto-triggers when the player approaches a
204
+ * reachable obstacle with forward intent. The body follows a scripted
205
+ * path (horizontal + vertical eases) onto the surface; player input
206
+ * is suspended for the path's duration. Fields here are consumed by
207
+ * the {@link Mantle} ability if registered; ignored otherwise.
208
+ */
209
+ mantle = {
210
+ /** Below this height (m above feet), the obstacle is just a step
211
+ * the player can walk over — no mantle. */
212
+ minHeight: 0.4,
213
+ /** Above this, the surface is out of reach no mantle. ~1.4m
214
+ * is chest height for a 1.8m body, which is the canonical
215
+ * "obstacle you can climb on but not over without help". */
216
+ maxHeight: 1.4,
217
+ /** Total scripted-path duration (seconds). Short enough that the
218
+ * brief loss of player control isn't frustrating. */
219
+ duration: 0.5,
220
+ /** How far past the obstacle edge the player lands (m). Slight
221
+ * forward bias so the player ends on top of the surface, not
222
+ * on the edge. */
223
+ forwardOffsetOnLand: 0.4,
224
+ };
225
+
226
+ /**
227
+ * Slide ability activates from sprint+crouch when grounded. The
228
+ * fields here are read by the {@link Slide} ability if it's registered
229
+ * on the controller; unused if Slide is not in use.
230
+ */
231
+ slide = {
232
+ /**
233
+ * Required horizontal speed to start a slide. Below this,
234
+ * crouch + sprint just transitions to a normal crouched walk.
235
+ *
236
+ * The default `7.5 m/s` sits between walkSpeed (4.5) and
237
+ * sprintSpeed (9), at ~83 % of sprint. Earning the slide is
238
+ * enforced by the ground-accel model: with
239
+ * `groundAccelHalfLife = 0.116 s`, reaching this threshold
240
+ * from standstill takes ~300 ms of sustained sprint well
241
+ * past a normal reaction window. Walking off a ledge or
242
+ * accelerating down a slope into a slide is explicitly
243
+ * supported: anything that brings the player's horizontal
244
+ * speed above this threshold lets crouch engage a slide.
245
+ * (Following Apex Legends' velocity-only gate at 200 hu/s.)
246
+ */
247
+ minEntrySpeed: 7.5,
248
+ /** Auto-exit threshold — slide ends when speed drops below this. */
249
+ endSpeed: 2.0,
250
+ /** Horizontal deceleration (m/s²) while sliding. Lower than the
251
+ * normal groundDecel so slides preserve momentum longer. */
252
+ friction: 3.0,
253
+ /** Non-interruptible window (seconds) after slide start higher-
254
+ * priority abilities can't preempt during this window so the
255
+ * player isn't yanked out the instant they commit to a slide. */
256
+ startupWindow: 0.15,
257
+ /** Multiplier on the jump initial Vy for slide-jump. 1.0 = same
258
+ * height as a standing jump; >1 gives slide-jump some extra
259
+ * reward for chaining the input. */
260
+ slideJumpUpFactor: 1.05,
261
+ /** Vertical-impact-spring kick (m/s) at slide entry — the
262
+ * satisfying "thud" of dropping into the slide. */
263
+ entryImpactVelocity: 0.6,
264
+ };
265
+
266
+ bob = {
267
+ stepFreqAtWalk: 1.8,
268
+ stepFreqExp: 0.75, // sub-linear so sprint isn't frantic
269
+ verticalAmpAtWalk: 0.055,
270
+ lateralAmpAtWalk: 0.030,
271
+ /** Extra amplitude per kg above 80kg (reference mass). */
272
+ ampMassScale: 0.005,
273
+ rollAtWalkDeg: 0.8, // a touch more visible head sway
274
+ /** Speeds below this don't tick the stride at all. */
275
+ minStepSpeed: 0.2,
276
+ /**
277
+ * Half-life of the bob-amplitude envelope (seconds). When the player
278
+ * starts/stops moving, the bob amplitude springs toward its natural
279
+ * value rather than cutting on/off so releasing a movement key
280
+ * doesn't whiplash the head from full sway back to neutral. The
281
+ * stride phase itself still freezes when stationary; only the
282
+ * AMPLITUDE is temporally smoothed.
283
+ */
284
+ intensityHalfLife: 0.22,
285
+ /**
286
+ * Vertical impact spring kicked downward at each footfall. The
287
+ * spring's response shapes the actual vertical motion of the head:
288
+ * a sharp dip (foot arresting the head's fall), recovery through
289
+ * neutral, and a slight overshoot above neutral as the standing
290
+ * leg injects energy into the body (the "push" phase). Models the
291
+ * gait COM trajectory more accurately than a smooth sinusoid.
292
+ */
293
+ impactSpringHalfLife: 0.14,
294
+ /** zeta < 1 → spring overshoots after impact, sells the leg-push. */
295
+ impactSpringZeta: 0.50,
296
+ /**
297
+ * Multiplier converting "desired peak dip" (verticalAmpAtWalk + mass
298
+ * boost, scaled by intensity and back-pedal factor) into the actual
299
+ * velocity impulse delivered at footfall. Empirical tuned so the
300
+ * default verticalAmpAtWalk produces the expected peak dip with the
301
+ * spring params above.
302
+ */
303
+ impactKickMultiplier: 30,
304
+ /**
305
+ * Multipliers applied to bob amplitude when the player is moving
306
+ * backward (interpolated by "backwardness" the negative dot of
307
+ * velocity onto screen-forward, normalized to sprint speed). At
308
+ * full back-pedal these are fully applied; at rest they have no
309
+ * effect.
310
+ *
311
+ * Lateral is intentionally larger than vertical: real back-gait
312
+ * has worse side-to-side balance than vertical compression, so
313
+ * the visual wobble should track that. Roll boosts each footfall's
314
+ * head-tilt for the same reason — sells "unstable footing".
315
+ */
316
+ backwardVerticalAmpFactor: 1.2,
317
+ backwardLateralAmpFactor: 1.4,
318
+ backwardRollFactor: 1.3,
319
+ /**
320
+ * Bob roll × lean coupling: when the body is leaning into a turn
321
+ * (lean spring displaced), the per-stride head roll is biased
322
+ * toward the lean direction. At 0 the bob roll is symmetric
323
+ * regardless of lean. At 1.0 the bob roll excursions in the lean
324
+ * direction are doubled and the opposite-direction excursions go to
325
+ * zero. Typical 0.3–0.6 reads as a cohesive "leaning while running"
326
+ * gait. Normalized against cfg.lean.maxRollDeg so the coupling
327
+ * stays proportional regardless of how aggressive lean is tuned.
328
+ */
329
+ leanCouplingFactor: 0.45,
330
+ };
331
+
332
+ breath = {
333
+ rateRestHz: 0.23,
334
+ rateMaxHz: 0.70,
335
+ amplitudeRestM: 0.004,
336
+ amplitudeMaxM: 0.022,
337
+ pitchAmpRestDeg: 0.10,
338
+ pitchAmpMaxDeg: 0.55,
339
+ /** ±20% amplitude noise rides on the sine. */
340
+ noiseAmount: 0.20,
341
+ /**
342
+ * Locomotor-respiratory coupling strength at full exertion. The
343
+ * breath-rate target is blended between purely metabolic (driven by
344
+ * exertion) and stride-aligned (an integer N strides per breath
345
+ * cycle). At 0 no coupling; at 1 breath rate is fully pulled toward
346
+ * the stride-aligned target.
347
+ *
348
+ * Coupling is SCALED by exertion low exertion means the breath is
349
+ * dictated by metabolic demand alone, high sustained effort lets
350
+ * the diaphragm couple to gait impacts (mechanistically: heavier
351
+ * breathing means deeper chest excursions, which the gait's
352
+ * vertical accelerations bias toward integer ratios). Matches
353
+ * empirical findings in dynamic activity (Bernasconi & Kohl 1993,
354
+ * Bramble & Carrier 1983) — much weaker than steady-state running.
355
+ *
356
+ * Frequency-coupled only, NOT phase-locked: breath rate matches a
357
+ * sub-multiple of stride rate, but phase drifts. Holds the "loose"
358
+ * character the user asked for.
359
+ */
360
+ locomotorCouplingMax: 0.45,
361
+ /** Coupling does not engage below this stride frequency (Hz). At
362
+ * walking pace your gait is too slow to mechanically influence
363
+ * breath; coupling really shows up at jog+ pace. */
364
+ couplingMinStrideFreqHz: 1.5,
365
+ };
366
+
367
+ exertion = {
368
+ // -- Sustained drivers (continuous accumulation per second) ---
369
+ /** Per-second accumulation while sprinting (mass-scaled internally). */
370
+ sprintRiseRate: 0.20,
371
+ /** Per-second decay toward 0 when not under sustained load. */
372
+ idleDecayRate: 0.12,
373
+
374
+ // -- Impulse drivers (one-shot bumps on discrete events) ------
375
+ /** Bump on jump fire — the muscular impulse of pushing off. */
376
+ jumpRise: 0.08,
377
+ /** Bump on crouch-enter the knee-grip on absorbing into a crouch.
378
+ * Small; we don't want crouch-spamming to instantly exhaust. */
379
+ crouchEnterRise: 0.03,
380
+ /**
381
+ * Per-(m/s) impulse on landing proportional to vertical impact
382
+ * speed. A clean low-jump landing barely registers; a hard fall
383
+ * delivers a noticeable bump. Clamped internally so an impossible
384
+ * fall doesn't max out exertion in one tick.
385
+ */
386
+ landImpulsePerVy: 0.012,
387
+ /** Max impulse magnitude added by any single landing. */
388
+ landImpulseMax: 0.20,
389
+
390
+ // -- Breath coupling (existing unchanged contract) ----------
391
+ /** How long after exertion subsides before breath rate normalizes. */
392
+ rateDecayHalfLife: 4.0,
393
+ /** How long after exertion subsides before breath amplitude normalizes. */
394
+ ampDecayHalfLife: 2.0,
395
+
396
+ // -- Output coupling: exertion affects how the body moves -----
397
+ /** At full exertion, bob lateral amp is multiplied by 1 + this.
398
+ * Sells "tired wobble" — modest effect (10–20% recommended). */
399
+ bobLateralBoostAtMax: 0.20,
400
+ /** At full exertion, the head droops forward by this many degrees.
401
+ * Sells visual fatigue without compromising aim. Subtle. */
402
+ headDroopAtMaxDeg: 1.5,
403
+ /** Spring half-life for the head-droop transition. */
404
+ headDroopHalfLife: 0.6,
405
+ };
406
+
407
+ lean = {
408
+ enabled: true,
409
+ /** Degrees of roll per (lateral m/s^2 / 9.81). */
410
+ maxRollDeg: 3.2, // more visible lean into turns — sells the athleticism
411
+ spring: { halfLife: 0.16, zeta: 1.0 },
412
+ /**
413
+ * Yaw-rate "look-lean": camera banks into mouse-driven turns even
414
+ * without strafe input. Disable for motion-sickness sensitivity.
415
+ */
416
+ lookLeanEnabled: true,
417
+ /**
418
+ * Degrees of roll per rad/s of yaw rate. ~3 means a 180°/s turn
419
+ * (slow camera pan) gives ~16.5° of bank; a fast mouse flick at
420
+ * 5 rad/s (≈286°/s) would saturate against the clamp below.
421
+ */
422
+ lookLeanDegPerRadPerSec: 3.0,
423
+ /** Yaw rate is clamped to ±this (rad/s) before scaling — prevents
424
+ * single-frame flicks from spiking the lean target. */
425
+ lookLeanYawRateClamp: 4.0,
426
+ /**
427
+ * Multiplier applied to look-lean (yaw-rate banking) when crouched.
428
+ * Crouched stance is low and stable; banking from a mouse turn
429
+ * reads as unmotivated. Default 0 fully disables; dial up to add
430
+ * a subtle tilt without it feeling like you're running. The lateral-
431
+ * acceleration lean is NOT affected by this — it scales naturally
432
+ * with crouch speeds since accel magnitudes drop.
433
+ */
434
+ crouchLookLeanFactor: 0.0,
435
+ };
436
+
437
+ fov = {
438
+ base: 75, // wider baseline more energetic
439
+ sprintAdd: 7, // dramatic punch on sprint (Apex/Titanfall feel)
440
+ crouchAdd: -3,
441
+ smoothHalfLife: 0.18, // slightly snappier transitions
442
+ };
443
+
444
+ look = {
445
+ pitchMinDeg: -85,
446
+ pitchMaxDeg: 85,
447
+ invertY: false,
448
+ };
449
+
450
+ /**
451
+ * Postural shifts driven by intent — distinct from look (which is
452
+ * player-driven) and from bob (which is procedural). Models
453
+ * Mirror's Edge-style "commitment to motion": sprinting tilts the head
454
+ * forward as the body leans into the run. Slow spring so it builds up
455
+ * over a second or so rather than snapping — it's a *posture* change,
456
+ * not a flick.
457
+ */
458
+ posture = {
459
+ /** Forward pitch (deg) applied to the eye at full sprint. Positive = look-down. */
460
+ sprintForwardPitchDeg: 4.0,
461
+ /** Spring half-life for the sprint pitch transition. */
462
+ sprintForwardPitchHalfLife: 0.32,
463
+ /** Small forward eye-position shift (m) at full sprint — head leads the hips. */
464
+ sprintForwardShiftM: 0.04,
465
+ };
466
+
467
+ gravity = {
468
+ /** Default gravity magnitude (m/s^2). Overridden by computeJumpFromApex once
469
+ * the jump tunables are resolved; kept here so non-jumping entities (NPCs,
470
+ * paused players) still have a sensible fall behavior. */
471
+ magnitude: 9.81,
472
+ };
473
+
474
+ toJSON() {
475
+ return {
476
+ body: { ...this.body },
477
+ motion: { ...this.motion },
478
+ jump: { ...this.jump, anticipation: { ...this.jump.anticipation } },
479
+ landing: {
480
+ ...this.landing,
481
+ recovery: {
482
+ ...this.landing.recovery,
483
+ spring: { ...this.landing.recovery.spring },
484
+ },
485
+ },
486
+ crouch: { ...this.crouch },
487
+ ledgeGrab: { ...this.ledgeGrab },
488
+ mantle: { ...this.mantle },
489
+ wallRun: { ...this.wallRun },
490
+ wallJump: { ...this.wallJump },
491
+ slide: { ...this.slide },
492
+ bob: { ...this.bob },
493
+ breath: { ...this.breath },
494
+ exertion: { ...this.exertion },
495
+ lean: { ...this.lean, spring: { ...this.lean.spring } },
496
+ fov: { ...this.fov },
497
+ look: { ...this.look },
498
+ posture: { ...this.posture },
499
+ gravity: { ...this.gravity },
500
+ };
501
+ }
502
+
503
+ fromJSON(json) {
504
+ if (json.body) Object.assign(this.body, json.body);
505
+ if (json.motion) Object.assign(this.motion, json.motion);
506
+ if (json.jump) {
507
+ const { anticipation, ...rest } = json.jump;
508
+ Object.assign(this.jump, rest);
509
+ if (anticipation) Object.assign(this.jump.anticipation, anticipation);
510
+ }
511
+ if (json.landing) {
512
+ const { recovery, ...rest } = json.landing;
513
+ Object.assign(this.landing, rest);
514
+ if (recovery) {
515
+ const { spring, ...rrest } = recovery;
516
+ Object.assign(this.landing.recovery, rrest);
517
+ if (spring) Object.assign(this.landing.recovery.spring, spring);
518
+ }
519
+ }
520
+ if (json.crouch) Object.assign(this.crouch, json.crouch);
521
+ if (json.ledgeGrab) Object.assign(this.ledgeGrab, json.ledgeGrab);
522
+ if (json.mantle) Object.assign(this.mantle, json.mantle);
523
+ if (json.wallRun) Object.assign(this.wallRun, json.wallRun);
524
+ if (json.wallJump) Object.assign(this.wallJump, json.wallJump);
525
+ if (json.slide) Object.assign(this.slide, json.slide);
526
+ if (json.bob) Object.assign(this.bob, json.bob);
527
+ if (json.breath) Object.assign(this.breath, json.breath);
528
+ if (json.exertion) Object.assign(this.exertion, json.exertion);
529
+ if (json.lean) {
530
+ const { spring, ...rest } = json.lean;
531
+ Object.assign(this.lean, rest);
532
+ if (spring) Object.assign(this.lean.spring, spring);
533
+ }
534
+ if (json.fov) Object.assign(this.fov, json.fov);
535
+ if (json.look) Object.assign(this.look, json.look);
536
+ if (json.posture) Object.assign(this.posture, json.posture);
537
+ if (json.gravity) Object.assign(this.gravity, json.gravity);
538
+ }
539
+
540
+ /**
541
+ * @param {FirstPersonPlayerControllerConfig} other
542
+ */
543
+ copy(other) {
544
+ this.fromJSON(other.toJSON());
545
+ }
546
+
547
+ clone() {
548
+ const c = new FirstPersonPlayerControllerConfig();
549
+ c.copy(this);
550
+ return c;
551
+ }
552
+ }