@woosh/meep-engine 2.138.17 → 2.138.19

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 (449) hide show
  1. package/README.md +2 -2
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/editor/actions/concrete/ActionUpdateTexture.d.ts +12 -0
  4. package/editor/actions/concrete/ActionUpdateTexture.d.ts.map +1 -0
  5. package/editor/actions/concrete/ArrayCopyAction.d.ts +20 -0
  6. package/editor/actions/concrete/ArrayCopyAction.d.ts.map +1 -0
  7. package/editor/actions/concrete/ComponentRemoveAction.d.ts +11 -0
  8. package/editor/actions/concrete/ComponentRemoveAction.d.ts.map +1 -0
  9. package/editor/actions/concrete/ModifyPatchSampler2DAction.d.ts +47 -0
  10. package/editor/actions/concrete/ModifyPatchSampler2DAction.d.ts.map +1 -0
  11. package/editor/actions/concrete/ModifyPatchTextureArray2DAction.d.ts +38 -0
  12. package/editor/actions/concrete/ModifyPatchTextureArray2DAction.d.ts.map +1 -0
  13. package/editor/actions/concrete/PaintTerrainOverlayAction.d.ts +23 -0
  14. package/editor/actions/concrete/PaintTerrainOverlayAction.d.ts.map +1 -0
  15. package/editor/actions/concrete/PatchTerrainHeightAction.d.ts +19 -0
  16. package/editor/actions/concrete/PatchTerrainHeightAction.d.ts.map +1 -0
  17. package/editor/actions/concrete/SelectionRemoveAction.d.ts +10 -0
  18. package/editor/actions/concrete/SelectionRemoveAction.d.ts.map +1 -0
  19. package/editor/actions/concrete/WriteGridValueAction.d.ts +15 -0
  20. package/editor/actions/concrete/WriteGridValueAction.d.ts.map +1 -0
  21. package/editor/ecs/component/FieldDescriptor.d.ts +27 -0
  22. package/editor/ecs/component/FieldDescriptor.d.ts.map +1 -0
  23. package/editor/ecs/component/FieldValueAdapter.d.ts +7 -0
  24. package/editor/ecs/component/FieldValueAdapter.d.ts.map +1 -0
  25. package/editor/ecs/component/createFieldEditor.d.ts +9 -0
  26. package/editor/ecs/component/createFieldEditor.d.ts.map +1 -0
  27. package/editor/ecs/component/createObjectEditor.d.ts +14 -0
  28. package/editor/ecs/component/createObjectEditor.d.ts.map +1 -0
  29. package/editor/ecs/component/editors/geom/QuaternionEditor.d.ts.map +1 -1
  30. package/editor/ecs/component/findNearestRegisteredType.d.ts +8 -0
  31. package/editor/ecs/component/findNearestRegisteredType.d.ts.map +1 -0
  32. package/editor/process/ObstacleGridDisplayProcess.d.ts.map +1 -1
  33. package/editor/process/symbolic/makeGridPositionSymbolDisplay.d.ts.map +1 -1
  34. package/editor/tools/GridPaintTool.d.ts +17 -0
  35. package/editor/tools/GridPaintTool.d.ts.map +1 -0
  36. package/editor/tools/SelectionTool.d.ts +27 -0
  37. package/editor/tools/SelectionTool.d.ts.map +1 -0
  38. package/editor/tools/TopDownCameraControlTool.d.ts +13 -0
  39. package/editor/tools/TopDownCameraControlTool.d.ts.map +1 -0
  40. package/editor/view/ecs/ComponentControlFactory.d.ts.map +1 -0
  41. package/editor/view/ecs/ComponentControlView.d.ts.map +1 -1
  42. package/editor/view/ecs/EntityEditor.d.ts.map +1 -0
  43. package/editor/view/ecs/EntityList.d.ts.map +1 -0
  44. package/editor/view/ecs/HierarchicalEntityListView.d.ts.map +1 -0
  45. package/editor/view/node-graph/NodeGraphEditorView.d.ts.map +1 -1
  46. package/editor/view/node-graph/NodeGraphView.d.ts.map +1 -1
  47. package/editor/view/node-graph/NodeView.d.ts.map +1 -1
  48. package/editor/view/node-graph/PortView.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/core/binary/BinaryBuffer.d.ts +1 -1
  51. package/src/core/binary/BinaryBuffer.js +1 -1
  52. package/src/core/binary/BitSet.d.ts +1 -1
  53. package/src/core/binary/BitSet.js +1 -1
  54. package/src/core/cache/Cache.js +1 -1
  55. package/src/core/cache/LoadingCache.js +1 -1
  56. package/src/core/collection/list/List.js +1 -1
  57. package/src/core/collection/map/HashMap.js +1 -1
  58. package/src/core/collection/table/RowFirstTable.d.ts +1 -1
  59. package/src/core/collection/table/RowFirstTable.js +1 -1
  60. package/src/core/collection/table/RowFirstTableSpec.d.ts +1 -1
  61. package/src/core/collection/table/RowFirstTableSpec.js +1 -1
  62. package/src/core/color/oklab/compute_max_saturation.d.ts +1 -1
  63. package/src/core/color/oklab/compute_max_saturation.js +1 -1
  64. package/src/core/color/oklab/find_cusp.d.ts +1 -1
  65. package/src/core/color/oklab/find_cusp.js +1 -1
  66. package/src/core/color/oklab/find_gamut_intersection.d.ts +1 -1
  67. package/src/core/color/oklab/find_gamut_intersection.js +1 -1
  68. package/src/core/color/oklab/okhsv_to_linear_srgb.d.ts +1 -1
  69. package/src/core/color/oklab/okhsv_to_linear_srgb.js +1 -1
  70. package/src/core/color/oklab/oklab_to_linear_srgb.d.ts +1 -1
  71. package/src/core/color/oklab/oklab_to_linear_srgb.js +1 -1
  72. package/src/core/color/oklab/oklab_to_xyz.d.ts +1 -1
  73. package/src/core/color/oklab/oklab_to_xyz.js +1 -1
  74. package/src/core/color/oklab/xyz_to_oklab.d.ts +1 -1
  75. package/src/core/color/oklab/xyz_to_oklab.js +1 -1
  76. package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.d.ts +1 -1
  77. package/src/core/geom/2d/convex-hull/fixed_convex_hull_relaxation.js +1 -1
  78. package/src/core/geom/3d/aabb/aabb3_nearest_point_on_surface.d.ts.map +1 -1
  79. package/src/core/geom/3d/aabb/aabb3_nearest_point_on_surface.js +57 -65
  80. package/src/core/geom/3d/octahedra/octahedral_uv_wrap.d.ts +1 -1
  81. package/src/core/geom/3d/octahedra/octahedral_uv_wrap.js +1 -1
  82. package/src/core/geom/3d/quaternion/quat_decode_from_uint32.d.ts +1 -1
  83. package/src/core/geom/3d/quaternion/quat_decode_from_uint32.js +1 -1
  84. package/src/core/geom/3d/quaternion/quat_encode_to_uint32.d.ts +1 -1
  85. package/src/core/geom/3d/quaternion/quat_encode_to_uint32.js +1 -1
  86. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +74 -3
  87. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts +37 -0
  88. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts.map +1 -0
  89. package/src/core/geom/3d/shape/CapsuleShape3D.js +210 -0
  90. package/src/core/geom/3d/shape/PointShape3D.d.ts +5 -0
  91. package/src/core/geom/3d/shape/PointShape3D.d.ts.map +1 -1
  92. package/src/core/geom/3d/shape/PointShape3D.js +52 -14
  93. package/src/core/geom/3d/shape/TransformedShape3D.d.ts +75 -12
  94. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  95. package/src/core/geom/3d/shape/TransformedShape3D.js +12 -3
  96. package/src/core/geom/3d/shape/UnionShape3D.d.ts +47 -5
  97. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  98. package/src/core/geom/3d/shape/UnitCubeShape3D.d.ts +12 -5
  99. package/src/core/geom/3d/shape/UnitCubeShape3D.d.ts.map +1 -1
  100. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +17 -5
  101. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
  102. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  103. package/src/core/geom/3d/shape/json/shape_to_type.js +4 -1
  104. package/src/core/geom/3d/shape/json/type_adapters.d.ts +17 -2
  105. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  106. package/src/core/geom/3d/shape/json/type_adapters.js +18 -2
  107. package/src/core/geom/3d/shape/util/compute_signed_distance_gradient_by_sampling.d.ts.map +1 -1
  108. package/src/core/geom/3d/shape/util/compute_signed_distance_gradient_by_sampling.js +51 -48
  109. package/src/core/geom/3d/tetrahedra/compute_bounding_simplex_3d.d.ts +1 -1
  110. package/src/core/geom/3d/tetrahedra/compute_bounding_simplex_3d.js +1 -1
  111. package/src/core/geom/ConicRay.d.ts +1 -1
  112. package/src/core/geom/ConicRay.js +1 -1
  113. package/src/core/geom/Quaternion.d.ts +2 -2
  114. package/src/core/geom/Quaternion.js +2 -2
  115. package/src/core/geom/Vector1.d.ts +1 -1
  116. package/src/core/geom/Vector1.js +1 -1
  117. package/src/core/geom/Vector2.d.ts +1 -1
  118. package/src/core/geom/Vector2.js +1 -1
  119. package/src/core/geom/Vector3.d.ts +1 -1
  120. package/src/core/geom/Vector3.js +1 -1
  121. package/src/core/geom/packing/miniball/Miniball.d.ts +1 -1
  122. package/src/core/geom/packing/miniball/Miniball.js +1 -1
  123. package/src/core/geom/vec3/serialization/v3_binary_equality_decode.d.ts +1 -1
  124. package/src/core/geom/vec3/serialization/v3_binary_equality_decode.js +1 -1
  125. package/src/core/geom/vec3/serialization/v3_binary_equality_encode.d.ts +1 -1
  126. package/src/core/geom/vec3/serialization/v3_binary_equality_encode.js +1 -1
  127. package/src/core/math/spline/spline3_hermite.d.ts +1 -1
  128. package/src/core/math/spline/spline3_hermite.js +1 -1
  129. package/src/core/math/spline/spline3_hermite_bounds.d.ts +1 -1
  130. package/src/core/math/spline/spline3_hermite_bounds.js +1 -1
  131. package/src/core/math/spline/spline3_hermite_bounds_t.d.ts +1 -1
  132. package/src/core/math/spline/spline3_hermite_bounds_t.js +1 -1
  133. package/src/core/math/spline/spline3_hermite_to_monomial.d.ts +1 -1
  134. package/src/core/math/spline/spline3_hermite_to_monomial.js +1 -1
  135. package/src/core/model/ObservedBoolean.d.ts +1 -1
  136. package/src/core/model/ObservedBoolean.js +1 -1
  137. package/src/core/model/ObservedString.d.ts +1 -1
  138. package/src/core/model/ObservedString.js +1 -1
  139. package/src/core/process/undo/Action.js +1 -1
  140. package/src/core/process/undo/ActionProcessor.js +1 -1
  141. package/src/engine/animation/curve/AnimationCurve.d.ts +1 -1
  142. package/src/engine/animation/curve/AnimationCurve.js +1 -1
  143. package/src/engine/animation/curve/Keyframe.d.ts +1 -1
  144. package/src/engine/animation/curve/Keyframe.js +1 -1
  145. package/src/engine/animation/curve/animation_curve_compute_aabb.d.ts +1 -1
  146. package/src/engine/animation/curve/animation_curve_compute_aabb.js +1 -1
  147. package/src/engine/animation/curve/animation_curve_optimize.d.ts +1 -1
  148. package/src/engine/animation/curve/animation_curve_optimize.js +2 -2
  149. package/src/engine/asset/loaders/image/jpeg/JpegImage.js +1 -1
  150. package/src/engine/asset/loaders/image/png/PNGReader.d.ts.map +1 -1
  151. package/src/engine/asset/loaders/image/png/PNGReader.js +27 -7
  152. package/src/engine/asset/loaders/image/png/crc32.d.ts +1 -1
  153. package/src/engine/asset/loaders/image/png/crc32.js +1 -1
  154. package/src/engine/control/first-person/DESIGN.md +616 -0
  155. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +229 -0
  156. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -0
  157. package/src/engine/control/first-person/FirstPersonPlayerController.js +176 -0
  158. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +251 -0
  159. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -0
  160. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +205 -0
  161. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +151 -0
  162. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -0
  163. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +799 -0
  164. package/src/engine/control/first-person/math/computeJumpFromApex.d.ts +23 -0
  165. package/src/engine/control/first-person/math/computeJumpFromApex.d.ts.map +1 -0
  166. package/src/engine/control/first-person/math/computeJumpFromApex.js +23 -0
  167. package/src/engine/control/first-person/math/criticallyDampedSpring.d.ts +23 -0
  168. package/src/engine/control/first-person/math/criticallyDampedSpring.d.ts.map +1 -0
  169. package/src/engine/control/first-person/math/criticallyDampedSpring.js +34 -0
  170. package/src/engine/control/first-person/math/dampedSpringStep.d.ts +23 -0
  171. package/src/engine/control/first-person/math/dampedSpringStep.d.ts.map +1 -0
  172. package/src/engine/control/first-person/math/dampedSpringStep.js +72 -0
  173. package/src/engine/control/first-person/math/stepTowards.d.ts +12 -0
  174. package/src/engine/control/first-person/math/stepTowards.d.ts.map +1 -0
  175. package/src/engine/control/first-person/math/stepTowards.js +20 -0
  176. package/src/engine/control/first-person/pose/FirstPersonPose.d.ts +69 -0
  177. package/src/engine/control/first-person/pose/FirstPersonPose.d.ts.map +1 -0
  178. package/src/engine/control/first-person/pose/FirstPersonPose.js +91 -0
  179. package/src/engine/control/first-person/prototype_first_person_controller.d.ts +2 -0
  180. package/src/engine/control/first-person/prototype_first_person_controller.d.ts.map +1 -0
  181. package/src/engine/control/first-person/prototype_first_person_controller.js +314 -0
  182. package/src/engine/ecs/Entity.js +1 -1
  183. package/src/engine/ecs/EntityComponentDataset.js +1 -1
  184. package/src/engine/ecs/EntityManager.d.ts +1 -1
  185. package/src/engine/ecs/EntityManager.js +1 -1
  186. package/src/engine/ecs/EntityObserver.js +1 -1
  187. package/src/engine/ecs/EntityReference.d.ts +1 -1
  188. package/src/engine/ecs/EntityReference.js +1 -1
  189. package/src/engine/ecs/System.js +1 -1
  190. package/src/engine/ecs/attachment/AttachmentSystem.d.ts +2 -2
  191. package/src/engine/ecs/attachment/AttachmentSystem.d.ts.map +1 -1
  192. package/src/engine/ecs/grid/HeightMap2AOMap.d.ts.map +1 -1
  193. package/src/engine/ecs/grid/HeightMap2AOMap.js +3 -2
  194. package/src/engine/ecs/guid/UUID.d.ts +1 -1
  195. package/src/engine/ecs/guid/UUID.js +1 -1
  196. package/src/engine/ecs/name/Name.d.ts +1 -1
  197. package/src/engine/ecs/name/Name.js +1 -1
  198. package/src/engine/ecs/parent/ParentEntity.js +1 -1
  199. package/src/engine/ecs/storage/BinaryBufferDeSerializer.d.ts +1 -1
  200. package/src/engine/ecs/storage/BinaryBufferDeSerializer.js +1 -1
  201. package/src/engine/ecs/storage/BinaryBufferSerializer.d.ts +1 -1
  202. package/src/engine/ecs/storage/BinaryBufferSerializer.js +1 -1
  203. package/src/engine/ecs/storage/binary/BinaryClassSerializationAdapter.js +1 -1
  204. package/src/engine/ecs/storage/binary/BinarySerializationRegistry.d.ts +1 -1
  205. package/src/engine/ecs/storage/binary/BinarySerializationRegistry.js +1 -1
  206. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts +1 -1
  207. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.js +1 -1
  208. package/src/engine/ecs/transform/Transform.d.ts +1 -1
  209. package/src/engine/ecs/transform/Transform.js +1 -1
  210. package/src/engine/graphics/ecs/path/PathDisplaySystem.d.ts.map +1 -1
  211. package/src/engine/graphics/impostors/octahedral/ImpostorBaker.d.ts.map +1 -1
  212. package/src/engine/graphics/impostors/octahedral/ImpostorBaker.js +10 -1
  213. package/src/engine/graphics/impostors/voxel/README.md +149 -0
  214. package/src/engine/graphics/impostors/voxel/VoxelImpostorBaker.d.ts +91 -0
  215. package/src/engine/graphics/impostors/voxel/VoxelImpostorBaker.d.ts.map +1 -0
  216. package/src/engine/graphics/impostors/voxel/VoxelImpostorBaker.js +376 -0
  217. package/src/engine/graphics/impostors/voxel/VoxelImpostorDescription.d.ts +128 -0
  218. package/src/engine/graphics/impostors/voxel/VoxelImpostorDescription.d.ts.map +1 -0
  219. package/src/engine/graphics/impostors/voxel/VoxelImpostorDescription.js +141 -0
  220. package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.d.ts +38 -0
  221. package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.d.ts.map +1 -0
  222. package/src/engine/graphics/impostors/voxel/bake/read_atlas_to_samples.js +338 -0
  223. package/src/engine/graphics/impostors/voxel/bake/voxelize_samples.d.ts +43 -0
  224. package/src/engine/graphics/impostors/voxel/bake/voxelize_samples.d.ts.map +1 -0
  225. package/src/engine/graphics/impostors/voxel/bake/voxelize_samples.js +192 -0
  226. package/src/engine/graphics/impostors/voxel/prototype_voxel_impostors.d.ts +2 -0
  227. package/src/engine/graphics/impostors/voxel/prototype_voxel_impostors.d.ts.map +1 -0
  228. package/src/engine/graphics/impostors/voxel/prototype_voxel_impostors.js +343 -0
  229. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.d.ts +13 -0
  230. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.d.ts.map +1 -0
  231. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderDepthV0.js +99 -0
  232. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.d.ts +19 -0
  233. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.d.ts.map +1 -0
  234. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.js +231 -0
  235. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.d.ts +5 -0
  236. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.d.ts.map +1 -0
  237. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderNormalsV0.js +70 -0
  238. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.d.ts +13 -0
  239. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.d.ts.map +1 -0
  240. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderV0.js +127 -0
  241. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.d.ts +5 -0
  242. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.d.ts.map +1 -0
  243. package/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderViewportDepthV0.js +68 -0
  244. package/src/engine/graphics/impostors/voxel/util/make_voxel_impostor_geometry.d.ts +27 -0
  245. package/src/engine/graphics/impostors/voxel/util/make_voxel_impostor_geometry.d.ts.map +1 -0
  246. package/src/engine/graphics/impostors/voxel/util/make_voxel_impostor_geometry.js +96 -0
  247. package/src/engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.d.ts +113 -29
  248. package/src/engine/graphics/render/forward_plus/SPECIFICATION.md +1 -1
  249. package/src/engine/graphics/shaders/AmbientOcclusionShader.d.ts.map +1 -1
  250. package/src/engine/graphics/shaders/AmbientOcclusionShader.js +20 -5
  251. package/src/engine/graphics/texture/sampler/Sampler2D.d.ts +1 -1
  252. package/src/engine/graphics/texture/sampler/Sampler2D.js +1 -1
  253. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_NaiveFlood.d.ts +1 -1
  254. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_NaiveFlood.js +1 -1
  255. package/src/engine/graphics/util/build_max_height_pyramid.d.ts +21 -14
  256. package/src/engine/graphics/util/build_max_height_pyramid.d.ts.map +1 -1
  257. package/src/engine/graphics/util/build_max_height_pyramid.js +107 -70
  258. package/src/engine/input/devices/InputDeviceSwitch.js +1 -1
  259. package/src/engine/input/devices/KeyboardDevice.js +1 -1
  260. package/src/engine/input/devices/PointerDevice.js +1 -1
  261. package/src/engine/intelligence/behavior/Behavior.js +1 -1
  262. package/src/engine/intelligence/behavior/SelectorBehavior.d.ts +1 -1
  263. package/src/engine/intelligence/behavior/SelectorBehavior.js +1 -1
  264. package/src/engine/intelligence/behavior/behavior_to_dot.d.ts +1 -1
  265. package/src/engine/intelligence/behavior/behavior_to_dot.js +1 -1
  266. package/src/engine/intelligence/behavior/composite/CompositeBehavior.d.ts +1 -1
  267. package/src/engine/intelligence/behavior/composite/CompositeBehavior.js +1 -1
  268. package/src/engine/intelligence/behavior/composite/ParallelBehavior.d.ts +1 -1
  269. package/src/engine/intelligence/behavior/composite/ParallelBehavior.js +1 -1
  270. package/src/engine/intelligence/behavior/composite/SequenceBehavior.d.ts +1 -1
  271. package/src/engine/intelligence/behavior/composite/SequenceBehavior.js +1 -1
  272. package/src/engine/intelligence/behavior/decorator/AbstractDecoratorBehavior.d.ts +1 -1
  273. package/src/engine/intelligence/behavior/decorator/AbstractDecoratorBehavior.js +1 -1
  274. package/src/engine/intelligence/behavior/decorator/IgnoreFailureBehavior.d.ts +1 -1
  275. package/src/engine/intelligence/behavior/decorator/IgnoreFailureBehavior.js +1 -1
  276. package/src/engine/intelligence/behavior/decorator/InvertStatusBehavior.d.ts +1 -1
  277. package/src/engine/intelligence/behavior/decorator/InvertStatusBehavior.js +1 -1
  278. package/src/engine/intelligence/behavior/decorator/RepeatBehavior.d.ts +1 -1
  279. package/src/engine/intelligence/behavior/decorator/RepeatBehavior.js +1 -1
  280. package/src/engine/intelligence/behavior/decorator/RepeatUntilSuccessBehavior.d.ts +1 -1
  281. package/src/engine/intelligence/behavior/decorator/RepeatUntilSuccessBehavior.js +1 -1
  282. package/src/engine/intelligence/behavior/ecs/BehaviorComponent.d.ts +1 -1
  283. package/src/engine/intelligence/behavior/ecs/BehaviorComponent.js +1 -1
  284. package/src/engine/intelligence/behavior/ecs/BehaviorSystem.d.ts +1 -1
  285. package/src/engine/intelligence/behavior/ecs/BehaviorSystem.js +1 -1
  286. package/src/engine/intelligence/behavior/ecs/DieBehavior.d.ts +1 -1
  287. package/src/engine/intelligence/behavior/ecs/DieBehavior.js +1 -1
  288. package/src/engine/intelligence/behavior/ecs/EntityBehavior.d.ts +1 -1
  289. package/src/engine/intelligence/behavior/ecs/EntityBehavior.js +1 -1
  290. package/src/engine/intelligence/behavior/ecs/KillBehavior.d.ts +1 -1
  291. package/src/engine/intelligence/behavior/ecs/KillBehavior.js +1 -1
  292. package/src/engine/intelligence/behavior/ecs/SendEventBehavior.d.ts +1 -1
  293. package/src/engine/intelligence/behavior/ecs/SendEventBehavior.js +1 -1
  294. package/src/engine/intelligence/behavior/ecs/WaitForEventBehavior.d.ts +1 -1
  295. package/src/engine/intelligence/behavior/ecs/WaitForEventBehavior.js +1 -1
  296. package/src/engine/intelligence/behavior/primitive/ActionBehavior.d.ts +1 -1
  297. package/src/engine/intelligence/behavior/primitive/ActionBehavior.js +1 -1
  298. package/src/engine/intelligence/behavior/primitive/FailingBehavior.d.ts +1 -1
  299. package/src/engine/intelligence/behavior/primitive/FailingBehavior.js +1 -1
  300. package/src/engine/intelligence/behavior/primitive/PromiseBehavior.d.ts +1 -1
  301. package/src/engine/intelligence/behavior/primitive/PromiseBehavior.js +1 -1
  302. package/src/engine/intelligence/behavior/primitive/SucceedingBehavior.d.ts +1 -1
  303. package/src/engine/intelligence/behavior/primitive/SucceedingBehavior.js +1 -1
  304. package/src/engine/intelligence/behavior/selector/WeightedElement.d.ts +1 -1
  305. package/src/engine/intelligence/behavior/selector/WeightedElement.js +1 -1
  306. package/src/engine/intelligence/behavior/selector/WeightedRandomBehavior.d.ts +1 -1
  307. package/src/engine/intelligence/behavior/selector/WeightedRandomBehavior.js +1 -1
  308. package/src/engine/intelligence/behavior/util/BranchBehavior.d.ts +1 -1
  309. package/src/engine/intelligence/behavior/util/BranchBehavior.js +1 -1
  310. package/src/engine/intelligence/behavior/util/DelayBehavior.d.ts +1 -1
  311. package/src/engine/intelligence/behavior/util/DelayBehavior.js +1 -1
  312. package/src/engine/intelligence/behavior/util/LogMessageBehavior.d.ts +1 -1
  313. package/src/engine/intelligence/behavior/util/LogMessageBehavior.js +1 -1
  314. package/src/engine/intelligence/behavior/util/RandomDelayBehavior.d.ts +1 -1
  315. package/src/engine/intelligence/behavior/util/RandomDelayBehavior.js +1 -1
  316. package/src/engine/intelligence/blackboard/Blackboard.js +1 -1
  317. package/src/engine/intelligence/mcts/MonteCarlo.d.ts +1 -1
  318. package/src/engine/intelligence/mcts/MonteCarlo.js +1 -1
  319. package/src/engine/intelligence/mcts/MoveEdge.d.ts +1 -1
  320. package/src/engine/intelligence/mcts/MoveEdge.js +1 -1
  321. package/src/engine/intelligence/mcts/StateNode.d.ts +1 -1
  322. package/src/engine/intelligence/mcts/StateNode.js +1 -1
  323. package/src/engine/intelligence/resource/ActionSequence.d.ts +1 -1
  324. package/src/engine/intelligence/resource/ActionSequence.js +1 -1
  325. package/src/engine/intelligence/resource/Resource.d.ts +1 -1
  326. package/src/engine/intelligence/resource/Resource.js +1 -1
  327. package/src/engine/intelligence/resource/ResourceAllocation.d.ts +1 -1
  328. package/src/engine/intelligence/resource/ResourceAllocation.js +1 -1
  329. package/src/engine/intelligence/resource/ResourceAllocationBid.d.ts +1 -1
  330. package/src/engine/intelligence/resource/ResourceAllocationBid.js +1 -1
  331. package/src/engine/intelligence/resource/ResourceAllocationSolver.d.ts +1 -1
  332. package/src/engine/intelligence/resource/ResourceAllocationSolver.js +1 -1
  333. package/src/engine/intelligence/resource/StrategicResourceAllocator.d.ts +1 -1
  334. package/src/engine/intelligence/resource/StrategicResourceAllocator.js +1 -1
  335. package/src/engine/intelligence/resource/TacticalModule.d.ts +1 -1
  336. package/src/engine/intelligence/resource/TacticalModule.js +1 -1
  337. package/src/engine/network/NetworkSession.d.ts +1 -1
  338. package/src/engine/network/NetworkSession.js +1 -1
  339. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
  340. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
  341. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
  342. package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
  343. package/src/engine/network/adapters/TransformReplicationAdapter.d.ts +1 -1
  344. package/src/engine/network/adapters/TransformReplicationAdapter.js +1 -1
  345. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
  346. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
  347. package/src/engine/network/core/quantize/quantize_float.d.ts +1 -1
  348. package/src/engine/network/core/quantize/quantize_float.js +1 -1
  349. package/src/engine/network/core/quantize/quantize_position.d.ts +1 -1
  350. package/src/engine/network/core/quantize/quantize_position.js +1 -1
  351. package/src/engine/network/core/sequence/ack_bitfield.d.ts +1 -1
  352. package/src/engine/network/core/sequence/ack_bitfield.js +1 -1
  353. package/src/engine/network/core/sequence/seq16.d.ts +1 -1
  354. package/src/engine/network/core/sequence/seq16.js +1 -1
  355. package/src/engine/network/core/sequence/seq32.d.ts +1 -1
  356. package/src/engine/network/core/sequence/seq32.js +1 -1
  357. package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
  358. package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
  359. package/src/engine/network/diagnostics/ReplayLog.d.ts +1 -1
  360. package/src/engine/network/diagnostics/ReplayLog.js +1 -1
  361. package/src/engine/network/diagnostics/SyncTest.d.ts +1 -1
  362. package/src/engine/network/diagnostics/SyncTest.js +1 -1
  363. package/src/engine/network/ecs/NetworkSystem.d.ts +1 -1
  364. package/src/engine/network/ecs/NetworkSystem.js +1 -1
  365. package/src/engine/network/ecs/components/NetworkIdentity.d.ts +1 -1
  366. package/src/engine/network/ecs/components/NetworkIdentity.js +1 -1
  367. package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.d.ts +1 -1
  368. package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.js +1 -1
  369. package/src/engine/network/orchestrator/NetworkPeer.d.ts +1 -1
  370. package/src/engine/network/orchestrator/NetworkPeer.js +1 -1
  371. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +1 -1
  372. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +1 -1
  373. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +1 -1
  374. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +1 -1
  375. package/src/engine/network/replication/Replicator.d.ts +1 -1
  376. package/src/engine/network/replication/Replicator.js +1 -1
  377. package/src/engine/network/replication/ScopeFilter.d.ts +2 -2
  378. package/src/engine/network/replication/ScopeFilter.js +2 -2
  379. package/src/engine/network/sim/ActionLog.d.ts +1 -1
  380. package/src/engine/network/sim/ActionLog.js +1 -1
  381. package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts +1 -1
  382. package/src/engine/network/sim/BinaryInterpolationAdapter.js +1 -1
  383. package/src/engine/network/sim/InterpolationLog.d.ts +1 -1
  384. package/src/engine/network/sim/InterpolationLog.js +1 -1
  385. package/src/engine/network/sim/ReplicatedComponentRegistry.d.ts +1 -1
  386. package/src/engine/network/sim/ReplicatedComponentRegistry.js +1 -1
  387. package/src/engine/network/sim/RewindEngine.d.ts +1 -1
  388. package/src/engine/network/sim/RewindEngine.js +1 -1
  389. package/src/engine/network/sim/SimAction.d.ts +1 -1
  390. package/src/engine/network/sim/SimAction.js +1 -1
  391. package/src/engine/network/sim/SimActionExecutor.d.ts +1 -1
  392. package/src/engine/network/sim/SimActionExecutor.js +1 -1
  393. package/src/engine/network/sim/SimActionRegistry.d.ts +1 -1
  394. package/src/engine/network/sim/SimActionRegistry.js +1 -1
  395. package/src/engine/network/sim/SmoothingState.js +1 -1
  396. package/src/engine/network/sim/Snapshotter.d.ts +1 -1
  397. package/src/engine/network/sim/Snapshotter.js +1 -1
  398. package/src/engine/network/sim/SpeculationLog.d.ts +1 -1
  399. package/src/engine/network/sim/SpeculationLog.js +1 -1
  400. package/src/engine/network/state/Baseline.d.ts +1 -1
  401. package/src/engine/network/state/Baseline.js +1 -1
  402. package/src/engine/network/state/ChangedEntitySet.d.ts +1 -1
  403. package/src/engine/network/state/ChangedEntitySet.js +1 -1
  404. package/src/engine/network/state/InputRing.d.ts +1 -1
  405. package/src/engine/network/state/InputRing.js +1 -1
  406. package/src/engine/network/state/MutationLedger.d.ts +1 -1
  407. package/src/engine/network/state/MutationLedger.js +1 -1
  408. package/src/engine/network/state/PriorityAccumulator.d.ts +1 -1
  409. package/src/engine/network/state/PriorityAccumulator.js +1 -1
  410. package/src/engine/network/state/ReplicationSlotTable.d.ts +1 -1
  411. package/src/engine/network/state/ReplicationSlotTable.js +1 -1
  412. package/src/engine/network/time/AdaptiveRenderDelay.d.ts +1 -1
  413. package/src/engine/network/time/AdaptiveRenderDelay.js +1 -1
  414. package/src/engine/network/time/JitterBuffer.d.ts +1 -1
  415. package/src/engine/network/time/JitterBuffer.js +1 -1
  416. package/src/engine/network/time/TimeDilation.d.ts +1 -1
  417. package/src/engine/network/time/TimeDilation.js +1 -1
  418. package/src/engine/network/time/TimeSync.d.ts +1 -1
  419. package/src/engine/network/time/TimeSync.js +1 -1
  420. package/src/engine/network/transport/Channel.d.ts.map +1 -1
  421. package/src/engine/network/transport/Channel.js +3 -6
  422. package/src/engine/network/transport/LoopbackTransport.d.ts +1 -1
  423. package/src/engine/network/transport/LoopbackTransport.js +1 -1
  424. package/src/engine/network/transport/ReliableCommandPipeline.d.ts +1 -1
  425. package/src/engine/network/transport/ReliableCommandPipeline.js +1 -1
  426. package/src/engine/network/transport/Transport.d.ts +1 -1
  427. package/src/engine/network/transport/Transport.js +1 -1
  428. package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts +1 -1
  429. package/src/engine/network/transport/adapters/NodeUDPTransport.js +2 -2
  430. package/src/engine/network/transport/adapters/SimulatedTransport.d.ts +1 -1
  431. package/src/engine/network/transport/adapters/SimulatedTransport.js +1 -1
  432. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts +1 -1
  433. package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +1 -1
  434. package/src/engine/network/transport/adapters/WebSocketTransport.d.ts +1 -1
  435. package/src/engine/network/transport/adapters/WebSocketTransport.js +1 -1
  436. package/src/engine/network/transport/adapters/WebTransportTransport.d.ts +1 -1
  437. package/src/engine/network/transport/adapters/WebTransportTransport.js +1 -1
  438. package/src/engine/network/transport/fragments/FragmentAssembler.d.ts +1 -1
  439. package/src/engine/network/transport/fragments/FragmentAssembler.js +1 -1
  440. package/src/engine/network/transport/fragments/FragmentRetention.d.ts +1 -1
  441. package/src/engine/network/transport/fragments/FragmentRetention.js +1 -1
  442. package/src/engine/network/transport/fragments/packet_size.d.ts +1 -1
  443. package/src/engine/network/transport/fragments/packet_size.js +1 -1
  444. package/src/engine/simulation/Ticker.d.ts +1 -1
  445. package/src/engine/simulation/Ticker.js +1 -1
  446. package/src/engine/sound/SoundEngine.js +1 -1
  447. package/src/engine/ui/DraggableAspect.d.ts +1 -1
  448. package/src/engine/ui/DraggableAspect.js +1 -1
  449. package/src/view/View.js +1 -1
@@ -0,0 +1,799 @@
1
+ import Quaternion from "../../../core/geom/Quaternion.js";
2
+ import Vector3 from "../../../core/geom/Vector3.js";
3
+ import { clamp } from "../../../core/math/clamp.js";
4
+ import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
5
+ import { lerp } from "../../../core/math/lerp.js";
6
+ import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
7
+ import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
8
+ import { SerializationMetadata } from "../../ecs/components/SerializationMetadata.js";
9
+ import Entity from "../../ecs/Entity.js";
10
+ import { System } from "../../ecs/System.js";
11
+ import { Transform } from "../../ecs/transform/Transform.js";
12
+ import { Camera } from "../../graphics/ecs/camera/Camera.js";
13
+ import { FirstPersonPlayerController } from "./FirstPersonPlayerController.js";
14
+ import { computeJumpFromApex } from "./math/computeJumpFromApex.js";
15
+ import { criticallyDampedSpringStep } from "./math/criticallyDampedSpring.js";
16
+ import { dampedSpringStep } from "./math/dampedSpringStep.js";
17
+ import { stepTowards } from "./math/stepTowards.js";
18
+ import { FirstPersonActionState } from "./pose/FirstPersonPose.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Scratch allocations — reused per frame to avoid GC pressure
22
+ // ---------------------------------------------------------------------------
23
+ const SCRATCH_V3_A = new Vector3();
24
+ const SCRATCH_V3_B = new Vector3();
25
+ const SCRATCH_V3_C = new Vector3();
26
+ const SCRATCH_Q_A = new Quaternion();
27
+ const SCRATCH_Q_B = new Quaternion();
28
+ const SCRATCH_Q_C = new Quaternion();
29
+
30
+ const TWO_PI = Math.PI * 2;
31
+ const LN2 = Math.log(2);
32
+
33
+ /**
34
+ * Per-entity runtime state the system maintains internally — too transient
35
+ * even for {@link FirstPersonPlayerController}'s `state` member, because it
36
+ * encodes input-edge bookkeeping and timer values the public surface should
37
+ * never see directly.
38
+ */
39
+ class PerEntityRuntime {
40
+ constructor() {
41
+ /** Eye pitch in radians, clamped to config.look limits. */
42
+ this.eyePitch = 0;
43
+ /** Body yaw in radians (around world up). */
44
+ this.bodyYaw = 0;
45
+
46
+ /** Horizontal+vertical velocity. We integrate these inside the system
47
+ * when no external physics layer is attached. */
48
+ this.velocityX = 0;
49
+ this.velocityY = 0;
50
+ this.velocityZ = 0;
51
+
52
+ /** Previous-tick jump intent — for rising/falling edge detection. */
53
+ this.prevJumpHeld = false;
54
+ /** Previous-tick crouch intent — for toggle-mode edge detection. */
55
+ this.prevCrouchHeld = false;
56
+ /** True while crouch toggle is latched on (used only in toggle mode). */
57
+ this.crouchLatched = false;
58
+
59
+ /** Remaining time in jump anticipation, or <= 0 if not anticipating. */
60
+ this.anticipationRemaining = 0;
61
+ /** Cached derived gravity (m/s^2) from peakHeight + timeToApex. */
62
+ this.gravity = 9.81;
63
+ /** Cached derived jump impulse (m/s upward). */
64
+ this.jumpInitialVy = 5.0;
65
+
66
+ /** Spring state for landing dip (mutated in place). */
67
+ this.landSpring = { value: 0, velocity: 0 };
68
+ /** Spring state for FOV. */
69
+ this.fovSpring = { value: 70, velocity: 0 };
70
+ /** Spring state for eye height (crouch). */
71
+ this.eyeHeightSpring = { value: 1.80, velocity: 0 };
72
+ /** Spring state for lean roll (radians). */
73
+ this.leanSpring = { value: 0, velocity: 0 };
74
+
75
+ /** Previous horizontal velocity — for lateral acceleration → lean. */
76
+ this.prevVelocityX = 0;
77
+ this.prevVelocityZ = 0;
78
+
79
+ /** Previous-tick grounded for edge detection. */
80
+ this.prevGrounded = true;
81
+ /** Vertical speed at moment of last "leave ground". */
82
+ this.takeoffVy = 0;
83
+ /** Max vertical position since last takeoff — for jump apex detection. */
84
+ this.peakAltitude = 0;
85
+ /** Set true once a jump has been launched; cleared on land. */
86
+ this.midJump = false;
87
+ /** Apex already fired for this airborne segment? */
88
+ this.apexFired = false;
89
+
90
+ /** Stride phase from previous fixed step — for footstep edge detection. */
91
+ this.prevStridePhase = 0;
92
+ /** Breath phase from previous fixed step — for inhale/exhale edge detection. */
93
+ this.prevBreathPhase = 0;
94
+ /** Which foot fires next — flipped on each footstep signal. */
95
+ this.nextFootSide = "R";
96
+
97
+ /** Cached eye entity ID. -1 until link assigns it. */
98
+ this.eyeEntity = -1;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Drives a first-person camera + body from intent fields. See sibling
104
+ * DESIGN.md for goals, architecture, and the five processing layers (L0..L4).
105
+ *
106
+ * - fixedUpdate runs L1 (locomotion), L2 (pose state), and L4 (events) so
107
+ * the simulation remains deterministic.
108
+ * - update runs L3 (camera composition) at render rate so the eye is never
109
+ * smoother than the screen.
110
+ *
111
+ * The system itself integrates a simple flat-floor at y = `config.gravity.magnitude > 0
112
+ * ? state.groundY : -Infinity` for the prototype. A real physics layer should
113
+ * write `state.grounded`/`state.groundNormal` from outside instead; the
114
+ * built-in resolver is just a convenience to keep the controller usable
115
+ * without dependencies.
116
+ *
117
+ * @author Alex Goldring
118
+ * @copyright Company Named Limited (c) 2026
119
+ */
120
+ export class FirstPersonPlayerControllerSystem extends System {
121
+ constructor() {
122
+ super();
123
+
124
+ this.dependencies = [FirstPersonPlayerController, Transform];
125
+
126
+ this.components_used = [
127
+ ResourceAccessSpecification.from(Transform, ResourceAccessKind.Write),
128
+ ResourceAccessSpecification.from(Camera, ResourceAccessKind.Write),
129
+ ];
130
+
131
+ /**
132
+ * Per-entity runtime, keyed by entity id.
133
+ * @type {Map<number, PerEntityRuntime>}
134
+ */
135
+ this.runtime = new Map();
136
+
137
+ /**
138
+ * If true, the system clamps body y >= groundY and writes
139
+ * state.grounded itself. Turn off when wiring a real physics layer.
140
+ * @type {boolean}
141
+ */
142
+ this.useBuiltInFlatGround = true;
143
+
144
+ /**
145
+ * The flat-ground y for the built-in resolver. Ignored when
146
+ * useBuiltInFlatGround is false.
147
+ * @type {number}
148
+ */
149
+ this.groundY = 0;
150
+ }
151
+
152
+ /**
153
+ * @param {FirstPersonPlayerController} controller
154
+ * @param {Transform} bodyTransform
155
+ * @param {number} entity
156
+ */
157
+ link(controller, bodyTransform, entity) {
158
+ const ecd = this.entityManager.dataset;
159
+
160
+ const runtime = new PerEntityRuntime();
161
+ this.runtime.set(entity, runtime);
162
+
163
+ // Derive gravity + jump impulse from designer-friendly params
164
+ const derived = { gravity: 0, initialVelocity: 0 };
165
+ computeJumpFromApex(controller.config.jump.peakHeight, controller.config.jump.timeToApex, derived);
166
+ runtime.gravity = derived.gravity;
167
+ runtime.jumpInitialVy = derived.initialVelocity;
168
+
169
+ // Seed yaw from the starting body rotation. `toEulerAnglesYXZ`
170
+ // returns (pitch, yaw, roll) — we only care about y.
171
+ bodyTransform.rotation.toEulerAnglesYXZ(SCRATCH_V3_A);
172
+ runtime.bodyYaw = SCRATCH_V3_A.y;
173
+ runtime.eyePitch = 0;
174
+
175
+ // Initialize springs to standing-eye-height baseline
176
+ runtime.eyeHeightSpring.value = controller.config.body.height;
177
+ runtime.fovSpring.value = controller.config.fov.base;
178
+ controller.state.eyeHeight = controller.config.body.height;
179
+
180
+ // Create eye entity if one wasn't supplied
181
+ if (controller.eyeEntity === -1 || !ecd.entityExists(controller.eyeEntity)) {
182
+ const eye = new Entity();
183
+
184
+ const eyeTransform = new Transform();
185
+ const baseEyePos = SCRATCH_V3_A.copy(bodyTransform.position);
186
+ baseEyePos.y += controller.config.body.height;
187
+ eyeTransform.position.copy(baseEyePos);
188
+
189
+ const camera = new Camera();
190
+ camera.active.set(true);
191
+ camera.fov.set(controller.config.fov.base);
192
+ camera.clip_near = 0.05;
193
+ camera.clip_far = 1000;
194
+ camera.autoClip = false;
195
+
196
+ eye.add(eyeTransform);
197
+ eye.add(camera);
198
+ eye.add(SerializationMetadata.Transient);
199
+
200
+ eye.build(ecd);
201
+
202
+ controller.eyeEntity = eye.id;
203
+ }
204
+
205
+ runtime.eyeEntity = controller.eyeEntity;
206
+ }
207
+
208
+ /**
209
+ * @param {FirstPersonPlayerController} controller
210
+ * @param {Transform} bodyTransform
211
+ * @param {number} entity
212
+ */
213
+ unlink(controller, bodyTransform, entity) {
214
+ const ecd = this.entityManager.dataset;
215
+
216
+ if (controller.eyeEntity !== -1 && ecd.entityExists(controller.eyeEntity)) {
217
+ ecd.removeEntity(controller.eyeEntity);
218
+ controller.eyeEntity = -1;
219
+ }
220
+
221
+ this.runtime.delete(entity);
222
+ }
223
+
224
+ /**
225
+ * Deterministic simulation step — L1 + L2 + L4.
226
+ * @param {number} dt
227
+ */
228
+ fixedUpdate(dt) {
229
+ const ecd = this.entityManager.dataset;
230
+ if (ecd === null) return;
231
+
232
+ this._currentDt = dt;
233
+ ecd.traverseComponents(FirstPersonPlayerController, this._tickEntity, this);
234
+ }
235
+
236
+ /**
237
+ * Variable-rate camera composition — L3.
238
+ * @param {number} dt
239
+ */
240
+ update(dt) {
241
+ const ecd = this.entityManager.dataset;
242
+ if (ecd === null) return;
243
+
244
+ this._currentRenderDt = dt;
245
+ ecd.traverseComponents(FirstPersonPlayerController, this._composeEye, this);
246
+ }
247
+
248
+ /**
249
+ * @private
250
+ * @param {FirstPersonPlayerController} controller
251
+ * @param {number} entity
252
+ */
253
+ _tickEntity(controller, entity) {
254
+ const ecd = this.entityManager.dataset;
255
+ const runtime = this.runtime.get(entity);
256
+ if (runtime === undefined) return;
257
+
258
+ const dt = this._currentDt;
259
+ const cfg = controller.config;
260
+ const intent = controller.intent;
261
+ const state = controller.state;
262
+ const sig = controller.signals;
263
+
264
+ const bodyTransform = ecd.getComponent(entity, Transform);
265
+ if (bodyTransform === undefined) return;
266
+
267
+ // -- L1.a: Consume look delta -----------------------------------
268
+ // intent.look is zeroed after consume so accumulated input doesn't
269
+ // re-apply on the next fixed step.
270
+ //
271
+ // Conventions (with raw mouse delta as the source — movementX/Y both
272
+ // positive when moving right/down):
273
+ // look.x > 0 ("mouse right") → turn right
274
+ // look.y > 0 ("mouse down") → look down (flipped by invertY)
275
+ //
276
+ // The yaw sign is negated because the engine uses left-handed
277
+ // coordinates with +Z as forward; a positive Y-axis rotation takes
278
+ // +Z toward +X, which presents to the player as a LEFT turn through
279
+ // the Three.js camera (`quaternion_invert_orientation`). Negating
280
+ // here gives the player-intuitive "mouse right → turn right".
281
+ const yawDelta = -intent.look.x;
282
+ const pitchSign = cfg.look.invertY ? -1 : 1;
283
+ const pitchDelta = intent.look.y * pitchSign;
284
+ intent.look.set(0, 0);
285
+
286
+ runtime.bodyYaw += yawDelta;
287
+ // keep yaw bounded (purely cosmetic — sin/cos handle wraparound fine)
288
+ if (runtime.bodyYaw > Math.PI) runtime.bodyYaw -= TWO_PI;
289
+ else if (runtime.bodyYaw < -Math.PI) runtime.bodyYaw += TWO_PI;
290
+
291
+ runtime.eyePitch = clamp(
292
+ runtime.eyePitch + pitchDelta,
293
+ cfg.look.pitchMinDeg * DEG_TO_RAD,
294
+ cfg.look.pitchMaxDeg * DEG_TO_RAD,
295
+ );
296
+
297
+ // Write body yaw back to transform (pure yaw, no pitch on body)
298
+ bodyTransform.rotation.fromAxisAngle(Vector3.up, runtime.bodyYaw);
299
+
300
+ // -- L1.b: Speed selection --------------------------------------
301
+ const isSprintIntent = intent.sprint && intent.move.y > 0.5 && state.grounded;
302
+ const isCrouchActive = this._resolveCrouchHeld(controller, runtime);
303
+
304
+ let targetSpeed;
305
+ if (isCrouchActive) {
306
+ targetSpeed = cfg.motion.crouchSpeed;
307
+ } else if (isSprintIntent) {
308
+ targetSpeed = cfg.motion.sprintSpeed;
309
+ } else {
310
+ targetSpeed = cfg.motion.walkSpeed;
311
+ }
312
+
313
+ // -- L1.c: Move intent → desired horizontal velocity -----------
314
+ //
315
+ // Player-perceived axes at yaw=0 (looking +Z through the engine's
316
+ // inverted-camera presentation):
317
+ // forward (W) → +Z velocity
318
+ // right (D) → -X velocity (camera presents -X as screen-right)
319
+ //
320
+ // In closed form for arbitrary yaw, screen-forward and screen-right
321
+ // are 90° apart with screen-right being LEFT of screen-forward
322
+ // (the inversion is purely a consequence of camera-side coordinate
323
+ // handling — non-camera entities use the usual right = +X).
324
+ //
325
+ // screen_forward(θ) = ( sin θ, 0, cos θ )
326
+ // screen_right (θ) = (-cos θ, 0, sin θ )
327
+ const sinYaw = Math.sin(runtime.bodyYaw);
328
+ const cosYaw = Math.cos(runtime.bodyYaw);
329
+
330
+ // Normalize the move vector (so diagonal isn't √2× faster)
331
+ const mvX = intent.move.x; // strafe (right+)
332
+ const mvY = intent.move.y; // forward (forward+)
333
+ const mvMag = Math.hypot(mvX, mvY);
334
+ const nmvX = mvMag > 1 ? mvX / mvMag : mvX;
335
+ const nmvY = mvMag > 1 ? mvY / mvMag : mvY;
336
+
337
+ const desiredVx = sinYaw * nmvY + -cosYaw * nmvX;
338
+ const desiredVz = cosYaw * nmvY + sinYaw * nmvX;
339
+
340
+ const desiredHorizontalVx = desiredVx * targetSpeed;
341
+ const desiredHorizontalVz = desiredVz * targetSpeed;
342
+
343
+ // -- L1.d: Apply accel/decel to horizontal velocity -------------
344
+ // Velocity lives on the runtime — we don't depend on Motion existing,
345
+ // because the system runs its own integrator when no external
346
+ // physics layer is attached.
347
+ const intentLen = Math.hypot(nmvX, nmvY);
348
+ let horizAccel;
349
+ if (!state.grounded) {
350
+ horizAccel = cfg.motion.airAccel;
351
+ } else if (intentLen < 1e-4) {
352
+ horizAccel = cfg.motion.groundDecel;
353
+ } else {
354
+ horizAccel = cfg.motion.groundAccel;
355
+ }
356
+
357
+ const maxStep = horizAccel * dt;
358
+ runtime.velocityX = stepTowards(runtime.velocityX, desiredHorizontalVx, maxStep);
359
+ runtime.velocityZ = stepTowards(runtime.velocityZ, desiredHorizontalVz, maxStep);
360
+
361
+ // -- L1.e: Jump (edge-triggered, buffered, coyote-graced) -------
362
+ const jumpPressedEdge = intent.jump && !runtime.prevJumpHeld;
363
+ const jumpReleasedEdge = !intent.jump && runtime.prevJumpHeld;
364
+ runtime.prevJumpHeld = intent.jump;
365
+
366
+ if (jumpPressedEdge) {
367
+ state.jumpBufferRemaining = cfg.jump.bufferTime;
368
+ }
369
+ state.jumpBufferRemaining = Math.max(0, state.jumpBufferRemaining - dt);
370
+
371
+ const canJumpNow =
372
+ (state.grounded || state.timeSinceGrounded < cfg.jump.coyoteTime)
373
+ && state.jumpBufferRemaining > 0
374
+ && !state.inJumpAnticipation
375
+ && !runtime.midJump;
376
+
377
+ if (canJumpNow) {
378
+ // Begin anticipation — squash; impulse fires after duration elapses
379
+ state.inJumpAnticipation = true;
380
+ runtime.anticipationRemaining = cfg.jump.anticipation.duration;
381
+ state.jumpBufferRemaining = 0; // claimed
382
+ }
383
+
384
+ // Variable-height cut: only valid during ascent and once jump has launched
385
+ if (jumpReleasedEdge && runtime.midJump && runtime.velocityY > 0) {
386
+ state.isVariableJumpCut = true;
387
+ }
388
+
389
+ // Anticipation timer; impulse on completion
390
+ if (state.inJumpAnticipation) {
391
+ // If the entity goes airborne mid-anticipation (ground rug-pulled),
392
+ // abandon the queued impulse — fire onLeaveGround{fall} instead.
393
+ if (!state.grounded) {
394
+ state.inJumpAnticipation = false;
395
+ runtime.anticipationRemaining = 0;
396
+ } else {
397
+ runtime.anticipationRemaining -= dt;
398
+ if (runtime.anticipationRemaining <= 0) {
399
+ runtime.velocityY = runtime.jumpInitialVy;
400
+ runtime.midJump = true;
401
+ runtime.apexFired = false;
402
+ runtime.peakAltitude = bodyTransform.position.y;
403
+ state.inJumpAnticipation = false;
404
+ state.isVariableJumpCut = false;
405
+ state.isAscending = true;
406
+ controller.state.exertion = clamp(controller.state.exertion + cfg.exertion.jumpRise, 0, 1);
407
+
408
+ sig.onJumpStart.send1({ peakHeight: cfg.jump.peakHeight });
409
+ sig.onLeaveGround.send1({ reason: "jump" });
410
+ }
411
+ }
412
+ }
413
+
414
+ // -- L1.f: Gravity ---------------------------------------------
415
+ let gMag = runtime.gravity;
416
+ if (runtime.velocityY <= 0) {
417
+ gMag *= cfg.jump.fallGravityMult;
418
+ state.isAscending = false;
419
+ } else if (state.isVariableJumpCut) {
420
+ gMag *= cfg.jump.cutGravityMult;
421
+ }
422
+
423
+ runtime.velocityY -= gMag * dt;
424
+
425
+ // -- L1.g: Integrate position ----------------------------------
426
+ bodyTransform.position._add(
427
+ runtime.velocityX * dt,
428
+ runtime.velocityY * dt,
429
+ runtime.velocityZ * dt,
430
+ );
431
+
432
+ // -- L1.h: Ground resolution (built-in flat floor) -------------
433
+ if (this.useBuiltInFlatGround) {
434
+ if (bodyTransform.position.y <= this.groundY) {
435
+ bodyTransform.position.setY(this.groundY);
436
+
437
+ if (!state.grounded) {
438
+ // Land
439
+ const impactVy = -runtime.velocityY; // positive magnitude
440
+ const kind = impactVy >= cfg.landing.hardThreshold ? "hard"
441
+ : (impactVy >= cfg.landing.softThreshold ? "soft" : "soft");
442
+ sig.onLand.send1({ verticalSpeed: impactVy, kind });
443
+
444
+ // Land dip — drives the under-damped spring downward
445
+ const dip = clamp(impactVy * cfg.landing.recovery.dipPerVy, 0, cfg.landing.recovery.dipMax);
446
+ runtime.landSpring.value = -dip; // start displaced
447
+ runtime.landSpring.velocity = 0;
448
+
449
+ runtime.midJump = false;
450
+ state.isAscending = false;
451
+ state.isVariableJumpCut = false;
452
+ state.fallDistance = 0;
453
+ }
454
+
455
+ state.grounded = true;
456
+ state.verticalSpeed = 0;
457
+ runtime.velocityY = 0;
458
+ state.airborneTime = 0;
459
+ state.timeSinceGrounded = 0;
460
+ } else {
461
+ if (state.grounded) {
462
+ sig.onLeaveGround.send1({ reason: runtime.midJump ? "jump" : "fall" });
463
+ runtime.takeoffVy = runtime.velocityY;
464
+ runtime.peakAltitude = bodyTransform.position.y;
465
+ }
466
+ state.grounded = false;
467
+ state.verticalSpeed = runtime.velocityY;
468
+ state.airborneTime += dt;
469
+ state.timeSinceGrounded += dt;
470
+ state.fallDistance += Math.max(0, -runtime.velocityY * dt);
471
+ }
472
+ } else {
473
+ // External physics is expected to maintain state.grounded /
474
+ // state.verticalSpeed; we still track airborne timer.
475
+ if (state.grounded) {
476
+ state.timeSinceGrounded = 0;
477
+ state.airborneTime = 0;
478
+ } else {
479
+ state.timeSinceGrounded += dt;
480
+ state.airborneTime += dt;
481
+ }
482
+ }
483
+
484
+ // Detect jump apex
485
+ if (runtime.midJump && !runtime.apexFired) {
486
+ if (bodyTransform.position.y > runtime.peakAltitude) {
487
+ runtime.peakAltitude = bodyTransform.position.y;
488
+ } else if (runtime.velocityY <= 0) {
489
+ sig.onJumpApex.send0();
490
+ runtime.apexFired = true;
491
+ }
492
+ }
493
+
494
+ // -- L2.a: speed / moveMode ------------------------------------
495
+ const horizSpeed = Math.hypot(runtime.velocityX, runtime.velocityZ);
496
+ state.speed = horizSpeed;
497
+ state.speedNormalized = clamp(horizSpeed / Math.max(cfg.motion.sprintSpeed, 1e-3), 0, 1);
498
+
499
+ const prevMoveMode = state.moveMode;
500
+ if (!state.grounded) {
501
+ state.moveMode = "Air";
502
+ } else if (isCrouchActive) {
503
+ state.moveMode = "Crouch";
504
+ } else if (isSprintIntent && horizSpeed > 0.1) {
505
+ state.moveMode = "Sprint";
506
+ } else if (horizSpeed > 0.1) {
507
+ state.moveMode = "Walk";
508
+ } else {
509
+ state.moveMode = "Idle";
510
+ }
511
+
512
+ if (state.moveMode === "Sprint" && prevMoveMode !== "Sprint") {
513
+ sig.onSprintStart.send0();
514
+ } else if (prevMoveMode === "Sprint" && state.moveMode !== "Sprint") {
515
+ sig.onSprintStop.send0();
516
+ }
517
+
518
+ // -- L2.b: Exertion --------------------------------------------
519
+ const exertionRise = isSprintIntent ? cfg.exertion.sprintRiseRate : 0;
520
+ const exertionFall = exertionRise > 0 ? 0 : cfg.exertion.idleDecayRate;
521
+ state.exertion = clamp(state.exertion + (exertionRise - exertionFall) * dt, 0, 1);
522
+
523
+ // -- L2.c: Breath ----------------------------------------------
524
+ // breathRate and breathAmplitude lag exertion through separate
525
+ // exponential decays. Rate hangs around longer than amplitude.
526
+ const targetRate = lerp(cfg.breath.rateRestHz, cfg.breath.rateMaxHz, state.exertion);
527
+ const targetAmp = lerp(cfg.breath.amplitudeRestM, cfg.breath.amplitudeMaxM, state.exertion);
528
+ state.breathRateHz = exponentialApproach(state.breathRateHz, targetRate, cfg.exertion.rateDecayHalfLife, dt);
529
+ state.breathAmplitudeM = exponentialApproach(state.breathAmplitudeM, targetAmp, cfg.exertion.ampDecayHalfLife, dt);
530
+
531
+ runtime.prevBreathPhase = state.breathPhase;
532
+ state.breathPhase += state.breathRateHz * dt;
533
+ state.breathPhase -= Math.floor(state.breathPhase); // wrap [0,1)
534
+
535
+ // Breath edge detection — inhale at 0.25, exhale at 0.75
536
+ if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.25)) {
537
+ sig.onBreathIn.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
538
+ }
539
+ if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.75)) {
540
+ sig.onBreathOut.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
541
+ }
542
+
543
+ // -- L2.d: Stride ----------------------------------------------
544
+ runtime.prevStridePhase = state.stridePhase;
545
+ if (state.grounded && horizSpeed > cfg.bob.minStepSpeed) {
546
+ const freq = cfg.bob.stepFreqAtWalk
547
+ * Math.pow(Math.max(horizSpeed, 1e-3) / Math.max(cfg.motion.walkSpeed, 1e-3), cfg.bob.stepFreqExp);
548
+ // 1 full stride cycle = 2 footfalls; phase advances at freq/2 of cycle
549
+ state.stridePhase += (freq * 0.5) * dt;
550
+ state.stridePhase -= Math.floor(state.stridePhase);
551
+ }
552
+ // Footstep on phase wraparound past 0 (R) or past 0.5 (L)
553
+ if (state.grounded && horizSpeed > cfg.bob.minStepSpeed) {
554
+ if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0)) {
555
+ state.stepCount++;
556
+ const side = runtime.nextFootSide === "L" ? "L" : "R";
557
+ runtime.nextFootSide = side === "R" ? "L" : "R";
558
+ sig.onFootStep.send1({ side, speed: horizSpeed, surfaceTag: state.surfaceTag });
559
+ }
560
+ if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0.5)) {
561
+ state.stepCount++;
562
+ const side = runtime.nextFootSide === "L" ? "L" : "R";
563
+ runtime.nextFootSide = side === "R" ? "L" : "R";
564
+ sig.onFootStep.send1({ side, speed: horizSpeed, surfaceTag: state.surfaceTag });
565
+ }
566
+ }
567
+
568
+ // -- L2.e: Crouch eye height -----------------------------------
569
+ const targetEyeH = isCrouchActive ? cfg.body.crouchHeight : cfg.body.height;
570
+ const crouchHalfLife = cfg.crouch.transitionTime / 4; // halfLife is ~quarter of full transition
571
+ criticallyDampedSpringStep(runtime.eyeHeightSpring, targetEyeH, crouchHalfLife, dt);
572
+ state.eyeHeight = runtime.eyeHeightSpring.value;
573
+
574
+ if (isCrouchActive !== state.crouchActive) {
575
+ state.crouchActive = isCrouchActive;
576
+ if (isCrouchActive) sig.onCrouchEnter.send0();
577
+ else sig.onCrouchExit.send0();
578
+ }
579
+
580
+ // -- L2.f: Lean (lateral acceleration → roll) ------------------
581
+ let leanTargetRad = 0;
582
+ if (cfg.lean.enabled) {
583
+ // Lateral acceleration projected onto screen-right.
584
+ // accel_world = (vel - prevVel) / dt; screen_right = (-cos θ, 0, sin θ).
585
+ const accWorldX = (runtime.velocityX - runtime.prevVelocityX) / Math.max(dt, 1e-4);
586
+ const accWorldZ = (runtime.velocityZ - runtime.prevVelocityZ) / Math.max(dt, 1e-4);
587
+ const latAccel = accWorldX * (-cosYaw) + accWorldZ * sinYaw;
588
+ const normalized = clamp(latAccel / 9.81, -2, 2);
589
+ // Negative roll on rightward accel = leaning into the turn
590
+ leanTargetRad = -normalized * cfg.lean.maxRollDeg * DEG_TO_RAD;
591
+ }
592
+ runtime.prevVelocityX = runtime.velocityX;
593
+ runtime.prevVelocityZ = runtime.velocityZ;
594
+ criticallyDampedSpringStep(runtime.leanSpring, leanTargetRad, cfg.lean.spring.halfLife, dt);
595
+ state.leanRollRad = runtime.leanSpring.value;
596
+
597
+ // -- L2.g: Land spring decay (drives the landing recovery dip) -
598
+ // Target is 0; under-damped so it rings.
599
+ dampedSpringStep(
600
+ runtime.landSpring, 0,
601
+ cfg.landing.recovery.spring.halfLife,
602
+ cfg.landing.recovery.spring.zeta,
603
+ dt,
604
+ );
605
+
606
+ // -- L2.h: Publish pose channels --------------------------------
607
+ const pose = controller.pose;
608
+ pose.rootPosition.copy(bodyTransform.position);
609
+ pose.rootYawRad = runtime.bodyYaw;
610
+ pose.headYawRad = runtime.bodyYaw;
611
+ pose.headPitchRad = runtime.eyePitch;
612
+ pose.headRollRad = state.leanRollRad;
613
+ pose.locomotionPhase = state.stridePhase;
614
+ pose.locomotionSpeed = horizSpeed;
615
+ // Strafe component: project velocity onto screen-right (-cos θ, 0, sin θ).
616
+ // Positive = moving to the player's right (animation blend-space convention).
617
+ pose.locomotionStrafe = (runtime.velocityX * (-cosYaw) + runtime.velocityZ * sinYaw)
618
+ / Math.max(cfg.motion.sprintSpeed, 1e-3);
619
+ pose.actionState =
620
+ state.inJumpAnticipation ? FirstPersonActionState.Anticipating
621
+ : !state.grounded ? FirstPersonActionState.Airborne
622
+ : (Math.abs(runtime.landSpring.value) > 0.01 ? FirstPersonActionState.Landing
623
+ : FirstPersonActionState.Grounded);
624
+ const crouchSpan = Math.max(cfg.body.height - cfg.body.crouchHeight, 1e-3);
625
+ pose.crouchAmount = clamp((cfg.body.height - state.eyeHeight) / crouchSpan, 0, 1);
626
+ pose.aimPitch = runtime.eyePitch;
627
+ }
628
+
629
+ /**
630
+ * @private
631
+ * @param {FirstPersonPlayerController} controller
632
+ * @param {PerEntityRuntime} runtime
633
+ * @returns {boolean}
634
+ */
635
+ _resolveCrouchHeld(controller, runtime) {
636
+ const cfg = controller.config;
637
+ const intent = controller.intent;
638
+
639
+ if (cfg.crouch.mode === "toggle") {
640
+ // Edge: rising press flips the latch
641
+ if (intent.crouch && !runtime.prevCrouchHeld) {
642
+ runtime.crouchLatched = !runtime.crouchLatched;
643
+ }
644
+ runtime.prevCrouchHeld = intent.crouch;
645
+ return runtime.crouchLatched;
646
+ }
647
+ // "hold" mode
648
+ runtime.prevCrouchHeld = intent.crouch;
649
+ return intent.crouch;
650
+ }
651
+
652
+ /**
653
+ * Compose the eye transform from body + state-driven offsets.
654
+ * @private
655
+ * @param {FirstPersonPlayerController} controller
656
+ * @param {number} entity
657
+ */
658
+ _composeEye(controller, entity) {
659
+ const ecd = this.entityManager.dataset;
660
+ const runtime = this.runtime.get(entity);
661
+ if (runtime === undefined) return;
662
+
663
+ const dt = this._currentRenderDt;
664
+ const cfg = controller.config;
665
+ const state = controller.state;
666
+
667
+ const bodyTransform = ecd.getComponent(entity, Transform);
668
+ if (bodyTransform === undefined) return;
669
+
670
+ if (controller.eyeEntity === -1) return;
671
+ const eyeTransform = ecd.getComponent(controller.eyeEntity, Transform);
672
+ const camera = ecd.getComponent(controller.eyeEntity, Camera);
673
+ if (eyeTransform === undefined || camera === undefined) return;
674
+
675
+ // -- Body-local eye offset --------------------------------------
676
+ const eyeLocal = SCRATCH_V3_A.set(0, state.eyeHeight, 0);
677
+
678
+ // Bob — only when grounded & moving
679
+ if (state.grounded && state.speed > cfg.bob.minStepSpeed) {
680
+ const phase = state.stridePhase * TWO_PI; // one full cycle = 2 steps
681
+ const massBoost = (cfg.body.mass - 80) * cfg.bob.ampMassScale;
682
+ const ampV = (cfg.bob.verticalAmpAtWalk + massBoost) * state.speedNormalized;
683
+ const ampL = (cfg.bob.lateralAmpAtWalk + massBoost) * state.speedNormalized;
684
+
685
+ eyeLocal.y += -ampV * Math.abs(Math.sin(phase));
686
+ eyeLocal.x += ampL * Math.sin(phase * 0.5);
687
+ }
688
+
689
+ // Breath — sine + tiny noise
690
+ const breathOffset = -state.breathAmplitudeM
691
+ * Math.sin(state.breathPhase * TWO_PI)
692
+ * (1 + cfg.breath.noiseAmount * (Math.sin(state.breathPhase * 13.7) * 0.5));
693
+ eyeLocal.y += breathOffset;
694
+
695
+ // Landing spring dip
696
+ eyeLocal.y += runtime.landSpring.value;
697
+
698
+ // Jump anticipation dip (linear ramp during anticipation)
699
+ if (state.inJumpAnticipation) {
700
+ const t = 1 - clamp(runtime.anticipationRemaining / Math.max(cfg.jump.anticipation.duration, 1e-3), 0, 1);
701
+ // Ease-out: t * (2 - t)
702
+ const eased = t * (2 - t);
703
+ eyeLocal.y -= cfg.jump.anticipation.dipAmount * eased;
704
+ }
705
+
706
+ // Transform body-local offset into world space (body has yaw only,
707
+ // so rotate by bodyTransform.rotation around Y)
708
+ const worldOffset = SCRATCH_V3_B.copy(eyeLocal);
709
+ worldOffset.applyQuaternion(bodyTransform.rotation);
710
+
711
+ eyeTransform.position.copy(bodyTransform.position);
712
+ eyeTransform.position._add(worldOffset.x, worldOffset.y, worldOffset.z);
713
+
714
+ // -- Eye rotation: body yaw × eye pitch × roll -------------------
715
+ // Bob roll mixes in for a subtle head sway (in phase with lateral bob).
716
+ // Breath pitch is a small extra nod 90° out of phase with vertical
717
+ // breath; merged into the main pitch so we don't pay an extra quat
718
+ // multiply and the composition stays trivially correct.
719
+ let rollTotal = state.leanRollRad;
720
+ if (state.grounded && state.speed > cfg.bob.minStepSpeed) {
721
+ const phase = state.stridePhase * TWO_PI;
722
+ const ampRoll = cfg.bob.rollAtWalkDeg * DEG_TO_RAD * state.speedNormalized;
723
+ rollTotal += ampRoll * Math.sin(phase * 0.5);
724
+ }
725
+
726
+ const breathPitch = lerp(cfg.breath.pitchAmpRestDeg, cfg.breath.pitchAmpMaxDeg, state.exertion)
727
+ * DEG_TO_RAD
728
+ * Math.cos(state.breathPhase * TWO_PI);
729
+ const pitchTotal = runtime.eyePitch + breathPitch;
730
+
731
+ // composition: yaw * pitch * roll
732
+ // pitch around world X — yaw applied after, so effective axis is camera-local right
733
+ // roll around world Z — yaw and pitch applied after, so effective axis is camera-local forward
734
+ const qYaw = SCRATCH_Q_A.fromAxisAngle(Vector3.up, runtime.bodyYaw);
735
+ const qPitch = SCRATCH_Q_B.fromAxisAngle(Vector3.right, pitchTotal);
736
+ const qRoll = SCRATCH_Q_C.fromAxisAngle(Vector3.forward, rollTotal);
737
+
738
+ eyeTransform.rotation.multiplyQuaternions(qYaw, qPitch);
739
+ eyeTransform.rotation.multiply(qRoll);
740
+
741
+ // -- FOV ---------------------------------------------------------
742
+ let fovTarget = cfg.fov.base;
743
+ if (cfg.fov.sprintAdd !== 0) {
744
+ // Add proportionally — at sprint-speed cap we hit sprintAdd
745
+ const sprintness = clamp((state.speed - cfg.motion.walkSpeed)
746
+ / Math.max(cfg.motion.sprintSpeed - cfg.motion.walkSpeed, 1e-3), 0, 1);
747
+ fovTarget += cfg.fov.sprintAdd * sprintness;
748
+ }
749
+ if (state.crouchActive) fovTarget += cfg.fov.crouchAdd;
750
+
751
+ criticallyDampedSpringStep(runtime.fovSpring, fovTarget, cfg.fov.smoothHalfLife, dt);
752
+ // Write directly to the underlying Three.js camera. Going through
753
+ // camera.fov.set() fires onChanged which triggers a full camera
754
+ // rebuild in CameraSystem — far too expensive to do per frame.
755
+ // The CameraSystem's visibility-construction hook calls
756
+ // updateProjectionMatrix() each frame anyway.
757
+ if (camera.object !== null) {
758
+ camera.object.fov = runtime.fovSpring.value;
759
+ }
760
+ }
761
+ }
762
+
763
+ // ---------------------------------------------------------------------------
764
+ // helpers
765
+ // ---------------------------------------------------------------------------
766
+
767
+ /**
768
+ * Exponential approach with half-life parameterization.
769
+ * @param {number} current
770
+ * @param {number} target
771
+ * @param {number} halfLife
772
+ * @param {number} dt
773
+ * @returns {number}
774
+ */
775
+ function exponentialApproach(current, target, halfLife, dt) {
776
+ if (halfLife <= 0) return target;
777
+ const alpha = 1 - Math.exp(-LN2 * dt / halfLife);
778
+ return current + (target - current) * alpha;
779
+ }
780
+
781
+ /**
782
+ * Detect that phase value crossed a boundary in [0,1) between two ticks.
783
+ * Handles the wraparound case where phase jumps from e.g. 0.95 to 0.05.
784
+ *
785
+ * @param {number} prev previous phase in [0,1)
786
+ * @param {number} next current phase in [0,1)
787
+ * @param {number} boundary in [0,1)
788
+ * @returns {boolean}
789
+ */
790
+ function phaseCrossed(prev, next, boundary) {
791
+ if (next >= prev) {
792
+ // no wrap
793
+ return prev < boundary && next >= boundary;
794
+ } else {
795
+ // wrapped past 1.0
796
+ return prev < boundary || next >= boundary;
797
+ }
798
+ }
799
+