@woosh/meep-engine 2.139.0 → 2.140.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 (172) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts +3 -3
  3. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts.map +1 -1
  4. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js +4 -4
  5. package/src/{engine/physics/broadphase/aabb_transform_oriented.d.ts → core/geom/3d/aabb/aabb3_transform_oriented.d.ts} +2 -2
  6. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
  7. package/src/{engine/physics/broadphase/aabb_transform_oriented.js → core/geom/3d/aabb/aabb3_transform_oriented.js} +1 -1
  8. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
  9. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
  10. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
  11. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
  12. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  13. package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
  14. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
  15. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
  16. package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
  17. package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
  18. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
  19. package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
  20. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  21. package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
  22. package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
  23. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
  24. package/src/core/geom/3d/shape/Triangle3D.js +318 -0
  25. package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
  26. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
  27. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
  28. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
  29. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
  30. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
  31. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
  32. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
  33. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  34. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
  35. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
  36. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
  37. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
  38. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
  39. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
  40. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
  41. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
  42. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
  43. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
  44. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
  45. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
  46. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
  47. package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
  48. package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
  49. package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
  50. package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
  51. package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
  52. package/src/core/geom/vec3/v3_negate_array.js +2 -2
  53. package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
  54. package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
  55. package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
  56. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
  57. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
  58. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
  59. package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
  60. package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
  61. package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
  62. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
  63. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  64. package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
  65. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
  66. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  67. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
  68. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
  69. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  70. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
  71. package/src/engine/control/first-person/TODO.md +173 -127
  72. package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
  73. package/src/engine/control/first-person/abilities/Slide.js +9 -1
  74. package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
  75. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
  76. package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
  77. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
  78. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
  79. package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
  80. package/src/engine/physics/BULLET_REVIEW.md +945 -0
  81. package/src/engine/physics/CANNON_REVIEW.md +1300 -0
  82. package/src/engine/physics/JOLT_REVIEW.md +913 -0
  83. package/src/engine/physics/PLAN.md +461 -236
  84. package/src/engine/physics/RAPIER_REVIEW.md +934 -0
  85. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
  86. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
  87. package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
  88. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  89. package/src/engine/physics/contact/ManifoldStore.js +608 -499
  90. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
  91. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
  92. package/src/engine/physics/ecs/PhysicsSystem.d.ts +128 -20
  93. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  94. package/src/engine/physics/ecs/PhysicsSystem.js +1301 -1159
  95. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  96. package/src/engine/physics/fluid/FluidSimulator.js +2 -1
  97. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +28 -6
  98. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  99. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +39 -17
  100. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
  101. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
  102. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
  103. package/src/engine/physics/gjk/gjk.d.ts +28 -2
  104. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  105. package/src/engine/physics/gjk/gjk.js +421 -378
  106. package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
  107. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
  108. package/src/engine/physics/gjk/minkowski_support.js +75 -0
  109. package/src/engine/physics/gjk/mpr.d.ts +56 -0
  110. package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
  111. package/src/engine/physics/gjk/mpr.js +344 -0
  112. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
  113. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  114. package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
  115. package/src/engine/physics/integration/integrate_position.d.ts +25 -7
  116. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  117. package/src/engine/physics/integration/integrate_position.js +43 -12
  118. package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
  119. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  120. package/src/engine/physics/integration/integrate_velocity.js +82 -1
  121. package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
  122. package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
  123. package/src/engine/physics/narrowphase/PosedShape.js +28 -30
  124. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  125. package/src/engine/physics/narrowphase/box_box_manifold.js +113 -17
  126. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
  127. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
  128. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
  129. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  130. package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
  131. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
  132. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
  133. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
  134. package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
  135. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
  136. package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
  137. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
  138. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
  139. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
  140. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
  141. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
  142. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
  143. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
  144. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
  145. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
  146. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
  147. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
  148. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
  149. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
  150. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
  151. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
  152. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +8 -2
  153. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  154. package/src/engine/physics/narrowphase/narrowphase_step.js +1422 -382
  155. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  156. package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
  157. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
  158. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
  159. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
  160. package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
  161. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
  162. package/src/engine/physics/queries/overlap_shape.js +183 -0
  163. package/src/engine/physics/queries/shape_cast.d.ts +56 -0
  164. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
  165. package/src/engine/physics/queries/shape_cast.js +387 -0
  166. package/src/engine/physics/solver/solve_contacts.d.ts +116 -30
  167. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  168. package/src/engine/physics/solver/solve_contacts.js +641 -223
  169. package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
  170. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
  171. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
  172. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
@@ -1,211 +1,211 @@
1
- import Signal from "../../../core/events/signal/Signal.js";
2
- import Vector2 from "../../../core/geom/Vector2.js";
3
- import Vector3 from "../../../core/geom/Vector3.js";
4
- import { AbilitySet } from "./abilities/AbilitySet.js";
5
- import { FirstPersonPlayerControllerConfig } from "./FirstPersonPlayerControllerConfig.js";
6
- import { MasterySet } from "./mastery/MasterySet.js";
7
- import { FirstPersonActionState, FirstPersonLocomotionMode, FirstPersonPose } from "./pose/FirstPersonPose.js";
8
- import { FirstPersonPosture } from "./pose/FirstPersonPosture.js";
9
-
10
- /**
11
- * Intent — what the player (or AI) wants the body to do. All fields are
12
- * latched ("hold-state") except `look`, which accumulates deltas between
13
- * fixed updates and is zeroed by the system on consume.
14
- *
15
- * The system performs its own edge detection on `jump` (and on `crouch`
16
- * when toggle-mode is configured). This is what guarantees a held jump
17
- * button does not produce repeated jumps.
18
- */
19
- class FirstPersonIntent {
20
- /** [-1..1, -1..1] forward+/right+ in body-local. @type {Vector2} */
21
- move = new Vector2(0, 0);
22
-
23
- /**
24
- * Accumulated look delta (radians). x = yaw, y = pitch. Consumed and
25
- * zeroed by the system in fixedUpdate.
26
- * @type {Vector2}
27
- */
28
- look = new Vector2(0, 0);
29
-
30
- /** Hold state — true while jump button is held. @type {boolean} */
31
- jump = false;
32
-
33
- /** Hold state — meaning depends on config.crouch.mode. @type {boolean} */
34
- crouch = false;
35
-
36
- /** Hold state — true while sprint button is held. @type {boolean} */
37
- sprint = false;
38
- }
39
-
40
- /**
41
- * Derived state — read-only outputs. Useful for HUDs, sound systems, AI
42
- * sensors that need to know "is the player crouched / sprinting / etc."
43
- */
44
- class FirstPersonState {
45
- grounded = true;
46
- timeSinceGrounded = 0;
47
-
48
- /** Set by the physics layer (or the prototype's flat-ground integrator). */
49
- groundNormal = new Vector3(0, 1, 0);
50
- surfaceTag = null;
51
-
52
- speed = 0;
53
- /** [0..1] vs sprint speed — drives bob amp and FOV kick. */
54
- speedNormalized = 0;
55
- /** Horizontal locomotion mode. See {@link FirstPersonLocomotionMode}. */
56
- locomotionMode = FirstPersonLocomotionMode.Idle;
57
-
58
- verticalSpeed = 0;
59
- airborneTime = 0;
60
- fallDistance = 0;
61
-
62
- jumpBufferRemaining = 0;
63
- inJumpAnticipation = false;
64
- isVariableJumpCut = false;
65
- /** Set to true while jumping ascent until apex; gates the cut behavior. */
66
- isAscending = false;
67
-
68
- exertion = 0;
69
- breathPhase = 0;
70
- breathRateHz = 0;
71
- breathAmplitudeM = 0;
72
-
73
- stridePhase = 0;
74
- stepCount = 0;
75
- /** Which foot fires next: 0 = right, 1 = left. */
76
- nextFoot = 0;
77
-
78
- eyeHeight = 1.80;
79
-
80
- /** Set true when crouch was released but a ceiling above prevents standing. */
81
- crouchBlocked = false;
82
- /** True while in crouch pose (independent of intent — see crouchBlocked). */
83
- crouchActive = false;
84
-
85
- /**
86
- * Body posture. Set each tick by whichever layer owns motion: the
87
- * base derives Stand/Crouch from intent; an active ability can
88
- * force Prone (slide) or Hang (ledge-grab). See
89
- * {@link FirstPersonPosture}. Drives eye-height target and gait
90
- * gating (stride, footsteps, bob).
91
- * @type {number}
92
- */
93
- posture = FirstPersonPosture.Stand;
94
-
95
- leanRollRad = 0;
96
- }
97
-
98
- /**
99
- * Event surface. All signals are sent from fixedUpdate so listeners see
100
- * consistent state across the same simulation tick.
101
- */
102
- class FirstPersonSignals {
103
- /** {side:"L"|"R", speed:number, surfaceTag:string?} */
104
- onFootStep = new Signal();
105
- /** {peakHeight:number} fired at impulse, after anticipation finishes. */
106
- onJumpStart = new Signal();
107
- /** No payload. */
108
- onJumpApex = new Signal();
109
- /** {reason:"jump"|"fall"} */
110
- onLeaveGround = new Signal();
111
- /** {verticalSpeed:number, kind:"soft"|"hard"} */
112
- onLand = new Signal();
113
- onCrouchEnter = new Signal();
114
- onCrouchExit = new Signal();
115
- onSprintStart = new Signal();
116
- onSprintStop = new Signal();
117
- /** {amplitude:number, rateHz:number} fired at peak inhale (phase 0.25). */
118
- onBreathIn = new Signal();
119
- /** {amplitude:number, rateHz:number} fired at peak exhale (phase 0.75). */
120
- onBreathOut = new Signal();
121
- /** {ledgeHeight:number} — fired when LedgeGrab activates. */
122
- onLedgeGrab = new Signal();
123
- /** {reason:"mantle-up"|"drop"|"slip"|"shuffle-off"} */
124
- onLedgeRelease = new Signal();
125
- }
126
-
127
- /**
128
- * First-person camera + body controller. See DESIGN.md (sibling file) for
129
- * the goals, layered architecture, and tuning rationale.
130
- *
131
- * Lifecycle: only `config` is serialized. `intent`, `state`, `pose`,
132
- * `signals`, and `eyeEntity` are transient — rebuilt by the system on
133
- * link.
134
- *
135
- * @author Alex Goldring
136
- * @copyright Company Named Limited (c) 2026
137
- */
138
- export class FirstPersonPlayerController {
139
- static typeName = "FirstPersonPlayerController";
140
-
141
- constructor() {
142
- this.config = new FirstPersonPlayerControllerConfig();
143
- this.intent = new FirstPersonIntent();
144
- this.state = new FirstPersonState();
145
- this.pose = new FirstPersonPose();
146
- this.signals = new FirstPersonSignals();
147
-
148
- /**
149
- * Mastery layer — registry of {@link MasteryEvaluator}s and the
150
- * telemetry score they feed. Designers register evaluators here;
151
- * the controller's system queries them at each decision point.
152
- * Empty by default (no mastery effects unless the game adds them).
153
- * @type {MasterySet}
154
- */
155
- this.mastery = new MasterySet();
156
-
157
- /**
158
- * Movement abilities (wall-run, mantle, slide, etc.). Empty by
159
- * default — base locomotion is unchanged unless the game adds
160
- * abilities here.
161
- * @type {AbilitySet}
162
- */
163
- this.abilities = new AbilitySet();
164
-
165
- /**
166
- * ID of the entity carrying the eye Transform + Camera. Created by
167
- * the system on link if -1. Held as a plain number rather than
168
- * EntityReference to keep serialization simple — the relationship
169
- * is rebuilt on link anyway.
170
- * @type {number}
171
- */
172
- this.eyeEntity = -1;
173
- }
174
-
175
- toJSON() {
176
- return { config: this.config.toJSON() };
177
- }
178
-
179
- fromJSON(json) {
180
- if (json && json.config) {
181
- this.config.fromJSON(json.config);
182
- }
183
- }
184
-
185
- /**
186
- * @param {FirstPersonPlayerController} other
187
- */
188
- copy(other) {
189
- this.config.copy(other.config);
190
- // intent/state/pose/signals/eyeEntity are transient
191
- }
192
-
193
- clone() {
194
- const c = new FirstPersonPlayerController();
195
- c.copy(this);
196
- return c;
197
- }
198
-
199
- equals(other) {
200
- // config-only equality (the rest is runtime state)
201
- return JSON.stringify(this.config.toJSON()) === JSON.stringify(other.config.toJSON());
202
- }
203
-
204
- hash() {
205
- // hashing transient state is undesirable; the config is large; return 0
206
- return 0;
207
- }
208
- }
209
-
210
- FirstPersonPlayerController.ActionState = FirstPersonActionState;
211
- FirstPersonPlayerController.LocomotionMode = FirstPersonLocomotionMode;
1
+ import Signal from "../../../core/events/signal/Signal.js";
2
+ import Vector2 from "../../../core/geom/Vector2.js";
3
+ import Vector3 from "../../../core/geom/Vector3.js";
4
+ import { AbilitySet } from "./abilities/AbilitySet.js";
5
+ import { FirstPersonPlayerControllerConfig } from "./FirstPersonPlayerControllerConfig.js";
6
+ import { MasterySet } from "./mastery/MasterySet.js";
7
+ import { FirstPersonActionState, FirstPersonLocomotionMode, FirstPersonPose } from "./pose/FirstPersonPose.js";
8
+ import { FirstPersonPosture } from "./pose/FirstPersonPosture.js";
9
+
10
+ /**
11
+ * Intent — what the player (or AI) wants the body to do. All fields are
12
+ * latched ("hold-state") except `look`, which accumulates deltas between
13
+ * fixed updates and is zeroed by the system on consume.
14
+ *
15
+ * The system performs its own edge detection on `jump` (and on `crouch`
16
+ * when toggle-mode is configured). This is what guarantees a held jump
17
+ * button does not produce repeated jumps.
18
+ */
19
+ class FirstPersonIntent {
20
+ /** [-1..1, -1..1] forward+/right+ in body-local. @type {Vector2} */
21
+ move = new Vector2(0, 0);
22
+
23
+ /**
24
+ * Accumulated look delta (radians). x = yaw, y = pitch. Consumed and
25
+ * zeroed by the system in fixedUpdate.
26
+ * @type {Vector2}
27
+ */
28
+ look = new Vector2(0, 0);
29
+
30
+ /** Hold state — true while jump button is held. @type {boolean} */
31
+ jump = false;
32
+
33
+ /** Hold state — meaning depends on config.crouch.mode. @type {boolean} */
34
+ crouch = false;
35
+
36
+ /** Hold state — true while sprint button is held. @type {boolean} */
37
+ sprint = false;
38
+ }
39
+
40
+ /**
41
+ * Derived state — read-only outputs. Useful for HUDs, sound systems, AI
42
+ * sensors that need to know "is the player crouched / sprinting / etc."
43
+ */
44
+ class FirstPersonState {
45
+ grounded = true;
46
+ timeSinceGrounded = 0;
47
+
48
+ /** Set by the physics layer (or the prototype's flat-ground integrator). */
49
+ groundNormal = new Vector3(0, 1, 0);
50
+ surfaceTag = null;
51
+
52
+ speed = 0;
53
+ /** [0..1] vs sprint speed — drives bob amp and FOV kick. */
54
+ speedNormalized = 0;
55
+ /** Horizontal locomotion mode. See {@link FirstPersonLocomotionMode}. */
56
+ locomotionMode = FirstPersonLocomotionMode.Idle;
57
+
58
+ verticalSpeed = 0;
59
+ airborneTime = 0;
60
+ fallDistance = 0;
61
+
62
+ jumpBufferRemaining = 0;
63
+ inJumpAnticipation = false;
64
+ isVariableJumpCut = false;
65
+ /** Set to true while jumping ascent until apex; gates the cut behavior. */
66
+ isAscending = false;
67
+
68
+ exertion = 0;
69
+ breathPhase = 0;
70
+ breathRateHz = 0;
71
+ breathAmplitudeM = 0;
72
+
73
+ stridePhase = 0;
74
+ stepCount = 0;
75
+ /** Which foot fires next: 0 = right, 1 = left. */
76
+ nextFoot = 0;
77
+
78
+ eyeHeight = 1.80;
79
+
80
+ /** Set true when crouch was released but a ceiling above prevents standing. */
81
+ crouchBlocked = false;
82
+ /** True while in crouch pose (independent of intent — see crouchBlocked). */
83
+ crouchActive = false;
84
+
85
+ /**
86
+ * Body posture. Set each tick by whichever layer owns motion: the
87
+ * base derives Stand/Crouch from intent; an active ability can
88
+ * force Prone (slide) or Hang (ledge-grab). See
89
+ * {@link FirstPersonPosture}. Drives eye-height target and gait
90
+ * gating (stride, footsteps, bob).
91
+ * @type {number}
92
+ */
93
+ posture = FirstPersonPosture.Stand;
94
+
95
+ leanRollRad = 0;
96
+ }
97
+
98
+ /**
99
+ * Event surface. All signals are sent from fixedUpdate so listeners see
100
+ * consistent state across the same simulation tick.
101
+ */
102
+ class FirstPersonSignals {
103
+ /** {side:"L"|"R", speed:number, surfaceTag:string?} */
104
+ onFootStep = new Signal();
105
+ /** {peakHeight:number} fired at impulse, after anticipation finishes. */
106
+ onJumpStart = new Signal();
107
+ /** No payload. */
108
+ onJumpApex = new Signal();
109
+ /** {reason:"jump"|"fall"} */
110
+ onLeaveGround = new Signal();
111
+ /** {verticalSpeed:number, kind:"soft"|"hard"} */
112
+ onLand = new Signal();
113
+ onCrouchEnter = new Signal();
114
+ onCrouchExit = new Signal();
115
+ onSprintStart = new Signal();
116
+ onSprintStop = new Signal();
117
+ /** {amplitude:number, rateHz:number} fired at peak inhale (phase 0.25). */
118
+ onBreathIn = new Signal();
119
+ /** {amplitude:number, rateHz:number} fired at peak exhale (phase 0.75). */
120
+ onBreathOut = new Signal();
121
+ /** {ledgeHeight:number} — fired when LedgeGrab activates. */
122
+ onLedgeGrab = new Signal();
123
+ /** {reason:"mantle-up"|"drop"|"slip"|"shuffle-off"} */
124
+ onLedgeRelease = new Signal();
125
+ }
126
+
127
+ /**
128
+ * First-person camera + body controller. See DESIGN.md (sibling file) for
129
+ * the goals, layered architecture, and tuning rationale.
130
+ *
131
+ * Lifecycle: only `config` is serialized. `intent`, `state`, `pose`,
132
+ * `signals`, and `eyeEntity` are transient — rebuilt by the system on
133
+ * link.
134
+ *
135
+ * @author Alex Goldring
136
+ * @copyright Company Named Limited (c) 2026
137
+ */
138
+ export class FirstPersonPlayerController {
139
+ static typeName = "FirstPersonPlayerController";
140
+
141
+ constructor() {
142
+ this.config = new FirstPersonPlayerControllerConfig();
143
+ this.intent = new FirstPersonIntent();
144
+ this.state = new FirstPersonState();
145
+ this.pose = new FirstPersonPose();
146
+ this.signals = new FirstPersonSignals();
147
+
148
+ /**
149
+ * Mastery layer — registry of {@link MasteryEvaluator}s and the
150
+ * telemetry score they feed. Designers register evaluators here;
151
+ * the controller's system queries them at each decision point.
152
+ * Empty by default (no mastery effects unless the game adds them).
153
+ * @type {MasterySet}
154
+ */
155
+ this.mastery = new MasterySet();
156
+
157
+ /**
158
+ * Movement abilities (wall-run, mantle, slide, etc.). Empty by
159
+ * default — base locomotion is unchanged unless the game adds
160
+ * abilities here.
161
+ * @type {AbilitySet}
162
+ */
163
+ this.abilities = new AbilitySet();
164
+
165
+ /**
166
+ * ID of the entity carrying the eye Transform + Camera. Created by
167
+ * the system on link if -1. Held as a plain number rather than
168
+ * EntityReference to keep serialization simple — the relationship
169
+ * is rebuilt on link anyway.
170
+ * @type {number}
171
+ */
172
+ this.eyeEntity = -1;
173
+ }
174
+
175
+ toJSON() {
176
+ return { config: this.config.toJSON() };
177
+ }
178
+
179
+ fromJSON(json) {
180
+ if (json && json.config) {
181
+ this.config.fromJSON(json.config);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * @param {FirstPersonPlayerController} other
187
+ */
188
+ copy(other) {
189
+ this.config.copy(other.config);
190
+ // intent/state/pose/signals/eyeEntity are transient
191
+ }
192
+
193
+ clone() {
194
+ const c = new FirstPersonPlayerController();
195
+ c.copy(this);
196
+ return c;
197
+ }
198
+
199
+ equals(other) {
200
+ // config-only equality (the rest is runtime state)
201
+ return JSON.stringify(this.config.toJSON()) === JSON.stringify(other.config.toJSON());
202
+ }
203
+
204
+ hash() {
205
+ // hashing transient state is undesirable; the config is large; return 0
206
+ return 0;
207
+ }
208
+ }
209
+
210
+ FirstPersonPlayerController.ActionState = FirstPersonActionState;
211
+ FirstPersonPlayerController.LocomotionMode = FirstPersonLocomotionMode;
@@ -40,8 +40,27 @@ export class FirstPersonPlayerControllerConfig {
40
40
  crouchSpeed: number;
41
41
  /** [0..1] how much intent influences velocity while airborne. */
42
42
  airControl: number;
43
- /** m/s^2 horizontal acceleration toward intent on ground. */
44
- groundAccel: number;
43
+ /**
44
+ * Half-life (seconds) for the mono-exponential approach to top
45
+ * sprint speed on the ground. The controller's ground-accel
46
+ * model is Hill-based:
47
+ * v(t) = v_max · (1 − e^(−t / τ))
48
+ * with τ = halfLife / ln 2. At each tick the velocity halves
49
+ * the remaining gap to `targetSpeed` every `halfLife` seconds.
50
+ *
51
+ * Default `0.116 s` gives ~95 % of `sprintSpeed` reached in
52
+ * 500 ms — the Apex Legends / Titanfall / modern CoD ramp
53
+ * window. The curve is naturally non-uniform: roughly 45 %
54
+ * of top speed in the first 100 ms, then the last 5 % takes
55
+ * 300 ms — so a quick tap-and-go still feels snappy while
56
+ * follow-up moves (slide, sprint-jump) require committed
57
+ * sprint time to be at top speed.
58
+ *
59
+ * Literature: Hill 1927 / Furusawa-Hill 1928 (force-velocity
60
+ * curve, original analytical formulation); Morin & Samozino
61
+ * 2016 (modern sprint profiling using the same mono-exp fit).
62
+ */
63
+ groundAccelHalfLife: number;
45
64
  /** m/s^2 horizontal deceleration toward 0 when intent is zero. */
46
65
  groundDecel: number;
47
66
  /** m/s^2 horizontal acceleration while airborne. */
@@ -191,8 +210,21 @@ export class FirstPersonPlayerControllerConfig {
191
210
  * on the controller; unused if Slide is not in use.
192
211
  */
193
212
  slide: {
194
- /** Required horizontal speed to start a slide. Below this, crouch+
195
- * sprint just transitions to a normal crouched walk. */
213
+ /**
214
+ * Required horizontal speed to start a slide. Below this,
215
+ * crouch + sprint just transitions to a normal crouched walk.
216
+ *
217
+ * The default — `7.5 m/s` — sits between walkSpeed (4.5) and
218
+ * sprintSpeed (9), at ~83 % of sprint. Earning the slide is
219
+ * enforced by the ground-accel model: with
220
+ * `groundAccelHalfLife = 0.116 s`, reaching this threshold
221
+ * from standstill takes ~300 ms of sustained sprint — well
222
+ * past a normal reaction window. Walking off a ledge or
223
+ * accelerating down a slope into a slide is explicitly
224
+ * supported: anything that brings the player's horizontal
225
+ * speed above this threshold lets crouch engage a slide.
226
+ * (Following Apex Legends' velocity-only gate at 200 hu/s.)
227
+ */
196
228
  minEntrySpeed: number;
197
229
  /** Auto-exit threshold — slide ends when speed drops below this. */
198
230
  endSpeed: number;
@@ -441,8 +473,27 @@ export class FirstPersonPlayerControllerConfig {
441
473
  crouchSpeed: number;
442
474
  /** [0..1] how much intent influences velocity while airborne. */
443
475
  airControl: number;
444
- /** m/s^2 horizontal acceleration toward intent on ground. */
445
- groundAccel: number;
476
+ /**
477
+ * Half-life (seconds) for the mono-exponential approach to top
478
+ * sprint speed on the ground. The controller's ground-accel
479
+ * model is Hill-based:
480
+ * v(t) = v_max · (1 − e^(−t / τ))
481
+ * with τ = halfLife / ln 2. At each tick the velocity halves
482
+ * the remaining gap to `targetSpeed` every `halfLife` seconds.
483
+ *
484
+ * Default `0.116 s` gives ~95 % of `sprintSpeed` reached in
485
+ * 500 ms — the Apex Legends / Titanfall / modern CoD ramp
486
+ * window. The curve is naturally non-uniform: roughly 45 %
487
+ * of top speed in the first 100 ms, then the last 5 % takes
488
+ * 300 ms — so a quick tap-and-go still feels snappy while
489
+ * follow-up moves (slide, sprint-jump) require committed
490
+ * sprint time to be at top speed.
491
+ *
492
+ * Literature: Hill 1927 / Furusawa-Hill 1928 (force-velocity
493
+ * curve, original analytical formulation); Morin & Samozino
494
+ * 2016 (modern sprint profiling using the same mono-exp fit).
495
+ */
496
+ groundAccelHalfLife: number;
446
497
  /** m/s^2 horizontal deceleration toward 0 when intent is zero. */
447
498
  groundDecel: number;
448
499
  /** m/s^2 horizontal acceleration while airborne. */
@@ -561,8 +612,21 @@ export class FirstPersonPlayerControllerConfig {
561
612
  nearWallMaxDistance: number;
562
613
  };
563
614
  slide: {
564
- /** Required horizontal speed to start a slide. Below this, crouch+
565
- * sprint just transitions to a normal crouched walk. */
615
+ /**
616
+ * Required horizontal speed to start a slide. Below this,
617
+ * crouch + sprint just transitions to a normal crouched walk.
618
+ *
619
+ * The default — `7.5 m/s` — sits between walkSpeed (4.5) and
620
+ * sprintSpeed (9), at ~83 % of sprint. Earning the slide is
621
+ * enforced by the ground-accel model: with
622
+ * `groundAccelHalfLife = 0.116 s`, reaching this threshold
623
+ * from standstill takes ~300 ms of sustained sprint — well
624
+ * past a normal reaction window. Walking off a ledge or
625
+ * accelerating down a slope into a slide is explicitly
626
+ * supported: anything that brings the player's horizontal
627
+ * speed above this threshold lets crouch engage a slide.
628
+ * (Following Apex Legends' velocity-only gate at 200 hu/s.)
629
+ */
566
630
  minEntrySpeed: number;
567
631
  /** Auto-exit threshold — slide ends when speed drops below this. */
568
632
  endSpeed: number;
@@ -1 +1 @@
1
- {"version":3,"file":"FirstPersonPlayerControllerConfig.d.ts","sourceRoot":"","sources":["../../../../../src/engine/control/first-person/FirstPersonPlayerControllerConfig.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FirstPersonPlayerControllerConfig.d.ts","sourceRoot":"","sources":["../../../../../src/engine/control/first-person/FirstPersonPlayerControllerConfig.js"],"names":[],"mappings":"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"}
@@ -45,8 +45,27 @@ export class FirstPersonPlayerControllerConfig {
45
45
  crouchSpeed: 2.2,
46
46
  /** [0..1] how much intent influences velocity while airborne. */
47
47
  airControl: 0.5,
48
- /** m/s^2 horizontal acceleration toward intent on ground. */
49
- groundAccel: 60, // ~6g start-up near-instant feel
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,
50
69
  /** m/s^2 horizontal deceleration toward 0 when intent is zero. */
51
70
  groundDecel: 75, // crisp stops, no skating
52
71
  /** m/s^2 horizontal acceleration while airborne. */
@@ -204,9 +223,22 @@ export class FirstPersonPlayerControllerConfig {
204
223
  * on the controller; unused if Slide is not in use.
205
224
  */
206
225
  slide = {
207
- /** Required horizontal speed to start a slide. Below this, crouch+
208
- * sprint just transitions to a normal crouched walk. */
209
- minEntrySpeed: 5.0,
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,
210
242
  /** Auto-exit threshold — slide ends when speed drops below this. */
211
243
  endSpeed: 2.0,
212
244
  /** Horizontal deceleration (m/s²) while sliding. Lower than the