@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
@@ -1,546 +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
- /** Minimum horizontal speed (m/s) to engage wall-run. */
139
- minSpeed: 4.0,
140
- /** Player must be airborne at least this long before wall-run can
141
- * engage prevents wall-run triggering off a jump's first frames. */
142
- minAirborneTime: 0.1,
143
- /** Maximum wall-run duration (s). Auto-exits at the end. */
144
- maxDuration: 2.0,
145
- /** Multiplier on base gravity while wall-running. Lower = longer hang. */
146
- gravityFactor: 0.25,
147
- /** Camera tilt magnitude (deg) into the wall. Disabled by the
148
- * motion-sickness toggle alongside the standard lean. */
149
- cameraRollDeg: 12,
150
- };
151
-
152
- /**
153
- * Wall-jump ability instantaneous off-wall impulse. Fires from
154
- * either an active wall-run state or from near-wall airborne (a
155
- * short window where the player is close enough to push off).
156
- */
157
- wallJump = {
158
- /** Impulse magnitude away from the wall (m/s, applied along wall normal). */
159
- outwardImpulse: 6.0,
160
- /** Upward component multiplier on jumpInitialVy. */
161
- upFactor: 0.9,
162
- /** Max distance to wall (m) at which a near-wall-airborne wall-jump
163
- * is allowed (when not already wall-running). */
164
- nearWallMaxDistance: 0.6,
165
- };
166
-
167
- /**
168
- * Ledge-grab ability snap to a forward ledge while descending and
169
- * hang from it until the player releases. Climbing fatigue is
170
- * modelled via the shared exertion channel: hang too long and you
171
- * slip. Fields here are consumed by the {@link LedgeGrab} ability
172
- * if registered; ignored otherwise.
173
- */
174
- ledgeGrab = {
175
- /** Body Y offset from ledge top while hanging (negative feet
176
- * below the ledge so the body looks like it's gripping the
177
- * edge). */
178
- hangOffsetY: -1.4,
179
- /** Forward offset from the ledge edge (negative = pulled BACK
180
- * from the edge so the body is against the wall under the
181
- * ledge). */
182
- hangOffsetForward: -0.2,
183
- /** Exertion rise per second while hanging — drives slip-by-
184
- * fatigue. The base exertion rise scale (massRatios) applies. */
185
- exertionRiseRate: 0.15,
186
- /** Player must be airborne at least this long before a ledge-
187
- * grab can engage prevents auto-catching mid-jump. */
188
- minAirborneTime: 0.3,
189
- /** Vertical kick (multiplier on jumpInitialVy) given on a
190
- * mantle-up release. Just enough to put the body above the
191
- * ledge so mantle's ledgeAhead probe still resolves correctly
192
- * on the next tick. */
193
- mantleUpUpFactor: 0.3,
194
- };
195
-
196
- /**
197
- * Mantle ability auto-triggers when the player approaches a
198
- * reachable obstacle with forward intent. The body follows a scripted
199
- * path (horizontal + vertical eases) onto the surface; player input
200
- * is suspended for the path's duration. Fields here are consumed by
201
- * the {@link Mantle} ability if registered; ignored otherwise.
202
- */
203
- mantle = {
204
- /** Below this height (m above feet), the obstacle is just a step
205
- * the player can walk over no mantle. */
206
- minHeight: 0.4,
207
- /** Above this, the surface is out of reach no mantle. ~1.4m
208
- * is chest height for a 1.8m body, which is the canonical
209
- * "obstacle you can climb on but not over without help". */
210
- maxHeight: 1.4,
211
- /** Total scripted-path duration (seconds). Short enough that the
212
- * brief loss of player control isn't frustrating. */
213
- duration: 0.5,
214
- /** How far past the obstacle edge the player lands (m). Slight
215
- * forward bias so the player ends on top of the surface, not
216
- * on the edge. */
217
- forwardOffsetOnLand: 0.4,
218
- };
219
-
220
- /**
221
- * Slide ability activates from sprint+crouch when grounded. The
222
- * fields here are read by the {@link Slide} ability if it's registered
223
- * on the controller; unused if Slide is not in use.
224
- */
225
- slide = {
226
- /**
227
- * Required horizontal speed to start a slide. Below this,
228
- * crouch + sprint just transitions to a normal crouched walk.
229
- *
230
- * The default — `7.5 m/s` — sits between walkSpeed (4.5) and
231
- * sprintSpeed (9), at ~83 % of sprint. Earning the slide is
232
- * enforced by the ground-accel model: with
233
- * `groundAccelHalfLife = 0.116 s`, reaching this threshold
234
- * from standstill takes ~300 ms of sustained sprint well
235
- * past a normal reaction window. Walking off a ledge or
236
- * accelerating down a slope into a slide is explicitly
237
- * supported: anything that brings the player's horizontal
238
- * speed above this threshold lets crouch engage a slide.
239
- * (Following Apex Legends' velocity-only gate at 200 hu/s.)
240
- */
241
- minEntrySpeed: 7.5,
242
- /** Auto-exit threshold slide ends when speed drops below this. */
243
- endSpeed: 2.0,
244
- /** Horizontal deceleration (m/s²) while sliding. Lower than the
245
- * normal groundDecel so slides preserve momentum longer. */
246
- friction: 3.0,
247
- /** Non-interruptible window (seconds) after slide start — higher-
248
- * priority abilities can't preempt during this window so the
249
- * player isn't yanked out the instant they commit to a slide. */
250
- startupWindow: 0.15,
251
- /** Multiplier on the jump initial Vy for slide-jump. 1.0 = same
252
- * height as a standing jump; >1 gives slide-jump some extra
253
- * reward for chaining the input. */
254
- slideJumpUpFactor: 1.05,
255
- /** Vertical-impact-spring kick (m/s) at slide entry the
256
- * satisfying "thud" of dropping into the slide. */
257
- entryImpactVelocity: 0.6,
258
- };
259
-
260
- bob = {
261
- stepFreqAtWalk: 1.8,
262
- stepFreqExp: 0.75, // sub-linear so sprint isn't frantic
263
- verticalAmpAtWalk: 0.055,
264
- lateralAmpAtWalk: 0.030,
265
- /** Extra amplitude per kg above 80kg (reference mass). */
266
- ampMassScale: 0.005,
267
- rollAtWalkDeg: 0.8, // a touch more visible head sway
268
- /** Speeds below this don't tick the stride at all. */
269
- minStepSpeed: 0.2,
270
- /**
271
- * Half-life of the bob-amplitude envelope (seconds). When the player
272
- * starts/stops moving, the bob amplitude springs toward its natural
273
- * value rather than cutting on/off so releasing a movement key
274
- * doesn't whiplash the head from full sway back to neutral. The
275
- * stride phase itself still freezes when stationary; only the
276
- * AMPLITUDE is temporally smoothed.
277
- */
278
- intensityHalfLife: 0.22,
279
- /**
280
- * Vertical impact spring kicked downward at each footfall. The
281
- * spring's response shapes the actual vertical motion of the head:
282
- * a sharp dip (foot arresting the head's fall), recovery through
283
- * neutral, and a slight overshoot above neutral as the standing
284
- * leg injects energy into the body (the "push" phase). Models the
285
- * gait COM trajectory more accurately than a smooth sinusoid.
286
- */
287
- impactSpringHalfLife: 0.14,
288
- /** zeta < 1 spring overshoots after impact, sells the leg-push. */
289
- impactSpringZeta: 0.50,
290
- /**
291
- * Multiplier converting "desired peak dip" (verticalAmpAtWalk + mass
292
- * boost, scaled by intensity and back-pedal factor) into the actual
293
- * velocity impulse delivered at footfall. Empirical — tuned so the
294
- * default verticalAmpAtWalk produces the expected peak dip with the
295
- * spring params above.
296
- */
297
- impactKickMultiplier: 30,
298
- /**
299
- * Multipliers applied to bob amplitude when the player is moving
300
- * backward (interpolated by "backwardness" the negative dot of
301
- * velocity onto screen-forward, normalized to sprint speed). At
302
- * full back-pedal these are fully applied; at rest they have no
303
- * effect.
304
- *
305
- * Lateral is intentionally larger than vertical: real back-gait
306
- * has worse side-to-side balance than vertical compression, so
307
- * the visual wobble should track that. Roll boosts each footfall's
308
- * head-tilt for the same reason sells "unstable footing".
309
- */
310
- backwardVerticalAmpFactor: 1.2,
311
- backwardLateralAmpFactor: 1.4,
312
- backwardRollFactor: 1.3,
313
- /**
314
- * Bob roll × lean coupling: when the body is leaning into a turn
315
- * (lean spring displaced), the per-stride head roll is biased
316
- * toward the lean direction. At 0 the bob roll is symmetric
317
- * regardless of lean. At 1.0 the bob roll excursions in the lean
318
- * direction are doubled and the opposite-direction excursions go to
319
- * zero. Typical 0.3–0.6 reads as a cohesive "leaning while running"
320
- * gait. Normalized against cfg.lean.maxRollDeg so the coupling
321
- * stays proportional regardless of how aggressive lean is tuned.
322
- */
323
- leanCouplingFactor: 0.45,
324
- };
325
-
326
- breath = {
327
- rateRestHz: 0.23,
328
- rateMaxHz: 0.70,
329
- amplitudeRestM: 0.004,
330
- amplitudeMaxM: 0.022,
331
- pitchAmpRestDeg: 0.10,
332
- pitchAmpMaxDeg: 0.55,
333
- /** ±20% amplitude noise rides on the sine. */
334
- noiseAmount: 0.20,
335
- /**
336
- * Locomotor-respiratory coupling strength at full exertion. The
337
- * breath-rate target is blended between purely metabolic (driven by
338
- * exertion) and stride-aligned (an integer N strides per breath
339
- * cycle). At 0 no coupling; at 1 breath rate is fully pulled toward
340
- * the stride-aligned target.
341
- *
342
- * Coupling is SCALED by exertion — low exertion means the breath is
343
- * dictated by metabolic demand alone, high sustained effort lets
344
- * the diaphragm couple to gait impacts (mechanistically: heavier
345
- * breathing means deeper chest excursions, which the gait's
346
- * vertical accelerations bias toward integer ratios). Matches
347
- * empirical findings in dynamic activity (Bernasconi & Kohl 1993,
348
- * Bramble & Carrier 1983)much weaker than steady-state running.
349
- *
350
- * Frequency-coupled only, NOT phase-locked: breath rate matches a
351
- * sub-multiple of stride rate, but phase drifts. Holds the "loose"
352
- * character the user asked for.
353
- */
354
- locomotorCouplingMax: 0.45,
355
- /** Coupling does not engage below this stride frequency (Hz). At
356
- * walking pace your gait is too slow to mechanically influence
357
- * breath; coupling really shows up at jog+ pace. */
358
- couplingMinStrideFreqHz: 1.5,
359
- };
360
-
361
- exertion = {
362
- // -- Sustained drivers (continuous accumulation per second) ---
363
- /** Per-second accumulation while sprinting (mass-scaled internally). */
364
- sprintRiseRate: 0.20,
365
- /** Per-second decay toward 0 when not under sustained load. */
366
- idleDecayRate: 0.12,
367
-
368
- // -- Impulse drivers (one-shot bumps on discrete events) ------
369
- /** Bump on jump fire the muscular impulse of pushing off. */
370
- jumpRise: 0.08,
371
- /** Bump on crouch-enter the knee-grip on absorbing into a crouch.
372
- * Small; we don't want crouch-spamming to instantly exhaust. */
373
- crouchEnterRise: 0.03,
374
- /**
375
- * Per-(m/s) impulse on landingproportional to vertical impact
376
- * speed. A clean low-jump landing barely registers; a hard fall
377
- * delivers a noticeable bump. Clamped internally so an impossible
378
- * fall doesn't max out exertion in one tick.
379
- */
380
- landImpulsePerVy: 0.012,
381
- /** Max impulse magnitude added by any single landing. */
382
- landImpulseMax: 0.20,
383
-
384
- // -- Breath coupling (existing unchanged contract) ----------
385
- /** How long after exertion subsides before breath rate normalizes. */
386
- rateDecayHalfLife: 4.0,
387
- /** How long after exertion subsides before breath amplitude normalizes. */
388
- ampDecayHalfLife: 2.0,
389
-
390
- // -- Output coupling: exertion affects how the body moves -----
391
- /** At full exertion, bob lateral amp is multiplied by 1 + this.
392
- * Sells "tired wobble" — modest effect (10–20% recommended). */
393
- bobLateralBoostAtMax: 0.20,
394
- /** At full exertion, the head droops forward by this many degrees.
395
- * Sells visual fatigue without compromising aim. Subtle. */
396
- headDroopAtMaxDeg: 1.5,
397
- /** Spring half-life for the head-droop transition. */
398
- headDroopHalfLife: 0.6,
399
- };
400
-
401
- lean = {
402
- enabled: true,
403
- /** Degrees of roll per (lateral m/s^2 / 9.81). */
404
- maxRollDeg: 3.2, // more visible lean into turns — sells the athleticism
405
- spring: { halfLife: 0.16, zeta: 1.0 },
406
- /**
407
- * Yaw-rate "look-lean": camera banks into mouse-driven turns even
408
- * without strafe input. Disable for motion-sickness sensitivity.
409
- */
410
- lookLeanEnabled: true,
411
- /**
412
- * Degrees of roll per rad/s of yaw rate. ~3 means a 180°/s turn
413
- * (slow camera pan) gives ~16.5° of bank; a fast mouse flick at
414
- * 5 rad/s (≈286°/s) would saturate against the clamp below.
415
- */
416
- lookLeanDegPerRadPerSec: 3.0,
417
- /** Yaw rate is clamped to ±this (rad/s) before scaling — prevents
418
- * single-frame flicks from spiking the lean target. */
419
- lookLeanYawRateClamp: 4.0,
420
- /**
421
- * Multiplier applied to look-lean (yaw-rate banking) when crouched.
422
- * Crouched stance is low and stable; banking from a mouse turn
423
- * reads as unmotivated. Default 0 fully disables; dial up to add
424
- * a subtle tilt without it feeling like you're running. The lateral-
425
- * acceleration lean is NOT affected by this — it scales naturally
426
- * with crouch speeds since accel magnitudes drop.
427
- */
428
- crouchLookLeanFactor: 0.0,
429
- };
430
-
431
- fov = {
432
- base: 75, // wider baseline more energetic
433
- sprintAdd: 7, // dramatic punch on sprint (Apex/Titanfall feel)
434
- crouchAdd: -3,
435
- smoothHalfLife: 0.18, // slightly snappier transitions
436
- };
437
-
438
- look = {
439
- pitchMinDeg: -85,
440
- pitchMaxDeg: 85,
441
- invertY: false,
442
- };
443
-
444
- /**
445
- * Postural shifts driven by intent — distinct from look (which is
446
- * player-driven) and from bob (which is procedural). Models
447
- * Mirror's Edge-style "commitment to motion": sprinting tilts the head
448
- * forward as the body leans into the run. Slow spring so it builds up
449
- * over a second or so rather than snapping — it's a *posture* change,
450
- * not a flick.
451
- */
452
- posture = {
453
- /** Forward pitch (deg) applied to the eye at full sprint. Positive = look-down. */
454
- sprintForwardPitchDeg: 4.0,
455
- /** Spring half-life for the sprint pitch transition. */
456
- sprintForwardPitchHalfLife: 0.32,
457
- /** Small forward eye-position shift (m) at full sprint — head leads the hips. */
458
- sprintForwardShiftM: 0.04,
459
- };
460
-
461
- gravity = {
462
- /** Default gravity magnitude (m/s^2). Overridden by computeJumpFromApex once
463
- * the jump tunables are resolved; kept here so non-jumping entities (NPCs,
464
- * paused players) still have a sensible fall behavior. */
465
- magnitude: 9.81,
466
- };
467
-
468
- toJSON() {
469
- return {
470
- body: { ...this.body },
471
- motion: { ...this.motion },
472
- jump: { ...this.jump, anticipation: { ...this.jump.anticipation } },
473
- landing: {
474
- ...this.landing,
475
- recovery: {
476
- ...this.landing.recovery,
477
- spring: { ...this.landing.recovery.spring },
478
- },
479
- },
480
- crouch: { ...this.crouch },
481
- ledgeGrab: { ...this.ledgeGrab },
482
- mantle: { ...this.mantle },
483
- wallRun: { ...this.wallRun },
484
- wallJump: { ...this.wallJump },
485
- slide: { ...this.slide },
486
- bob: { ...this.bob },
487
- breath: { ...this.breath },
488
- exertion: { ...this.exertion },
489
- lean: { ...this.lean, spring: { ...this.lean.spring } },
490
- fov: { ...this.fov },
491
- look: { ...this.look },
492
- posture: { ...this.posture },
493
- gravity: { ...this.gravity },
494
- };
495
- }
496
-
497
- fromJSON(json) {
498
- if (json.body) Object.assign(this.body, json.body);
499
- if (json.motion) Object.assign(this.motion, json.motion);
500
- if (json.jump) {
501
- const { anticipation, ...rest } = json.jump;
502
- Object.assign(this.jump, rest);
503
- if (anticipation) Object.assign(this.jump.anticipation, anticipation);
504
- }
505
- if (json.landing) {
506
- const { recovery, ...rest } = json.landing;
507
- Object.assign(this.landing, rest);
508
- if (recovery) {
509
- const { spring, ...rrest } = recovery;
510
- Object.assign(this.landing.recovery, rrest);
511
- if (spring) Object.assign(this.landing.recovery.spring, spring);
512
- }
513
- }
514
- if (json.crouch) Object.assign(this.crouch, json.crouch);
515
- if (json.ledgeGrab) Object.assign(this.ledgeGrab, json.ledgeGrab);
516
- if (json.mantle) Object.assign(this.mantle, json.mantle);
517
- if (json.wallRun) Object.assign(this.wallRun, json.wallRun);
518
- if (json.wallJump) Object.assign(this.wallJump, json.wallJump);
519
- if (json.slide) Object.assign(this.slide, json.slide);
520
- if (json.bob) Object.assign(this.bob, json.bob);
521
- if (json.breath) Object.assign(this.breath, json.breath);
522
- if (json.exertion) Object.assign(this.exertion, json.exertion);
523
- if (json.lean) {
524
- const { spring, ...rest } = json.lean;
525
- Object.assign(this.lean, rest);
526
- if (spring) Object.assign(this.lean.spring, spring);
527
- }
528
- if (json.fov) Object.assign(this.fov, json.fov);
529
- if (json.look) Object.assign(this.look, json.look);
530
- if (json.posture) Object.assign(this.posture, json.posture);
531
- if (json.gravity) Object.assign(this.gravity, json.gravity);
532
- }
533
-
534
- /**
535
- * @param {FirstPersonPlayerControllerConfig} other
536
- */
537
- copy(other) {
538
- this.fromJSON(other.toJSON());
539
- }
540
-
541
- clone() {
542
- const c = new FirstPersonPlayerControllerConfig();
543
- c.copy(this);
544
- return c;
545
- }
546
- }
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
+ }