@woosh/meep-engine 3.30.0 → 3.31.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 (1579) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/build/bundle-worker-terrain.js +1 -1
  3. package/package.json +3 -1
  4. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  5. package/src/core/binary/BinaryBuffer.js +42 -14
  6. package/src/core/binary/BitImage2.d.ts.map +1 -1
  7. package/src/core/binary/BitImage2.js +2 -6
  8. package/src/core/binary/BitSet.js +5 -5
  9. package/src/core/binary/compression/chunks_to_stream.d.ts +24 -0
  10. package/src/core/binary/compression/chunks_to_stream.d.ts.map +1 -0
  11. package/src/core/binary/compression/chunks_to_stream.js +35 -0
  12. package/src/core/binary/compression/compress_bytes.d.ts +3 -4
  13. package/src/core/binary/compression/compress_bytes.d.ts.map +1 -1
  14. package/src/core/binary/compression/compress_bytes.js +6 -17
  15. package/src/core/binary/compression/decompress_bytes.d.ts +1 -2
  16. package/src/core/binary/compression/decompress_bytes.d.ts.map +1 -1
  17. package/src/core/binary/compression/decompress_bytes.js +4 -15
  18. package/src/core/binary/compression/decompress_bytes_into.d.ts.map +1 -1
  19. package/src/core/binary/compression/decompress_bytes_into.js +2 -13
  20. package/src/core/binary/ctz32.d.ts +12 -4
  21. package/src/core/binary/ctz32.d.ts.map +1 -1
  22. package/src/core/binary/ctz32.js +23 -41
  23. package/src/core/binary/lsb_32.d.ts +10 -2
  24. package/src/core/binary/lsb_32.d.ts.map +1 -1
  25. package/src/core/binary/lsb_32.js +17 -12
  26. package/src/core/binary/lz4/lz4_compress_block_hc.js +2 -2
  27. package/src/core/binary/type/DataTypeIndices.d.ts +39 -0
  28. package/src/core/binary/type/DataTypeIndices.d.ts.map +1 -1
  29. package/src/core/binary/type/DataTypeIndices.js +38 -1
  30. package/src/core/binary/type/Type.d.ts.map +1 -1
  31. package/src/core/binary/type/Type.js +6 -1
  32. package/src/core/bvh2/bvh3/ebvh_optimize_treelet.js +3 -3
  33. package/src/core/bvh8/bvh8_geometry_validate.d.ts.map +1 -1
  34. package/src/core/bvh8/bvh8_geometry_validate.js +2 -88
  35. package/src/core/bvh8/bvh8_geometry_validate_bounds.d.ts +25 -0
  36. package/src/core/bvh8/bvh8_geometry_validate_bounds.d.ts.map +1 -0
  37. package/src/core/bvh8/bvh8_geometry_validate_bounds.js +105 -0
  38. package/src/core/bvh8/bvh8_geometry_validate_indirect.d.ts.map +1 -1
  39. package/src/core/bvh8/bvh8_geometry_validate_indirect.js +2 -91
  40. package/src/core/clipboard/obtainClipBoard.d.ts +4 -0
  41. package/src/core/clipboard/obtainClipBoard.d.ts.map +1 -1
  42. package/src/core/clipboard/obtainClipBoard.js +29 -8
  43. package/src/core/clipboard/safeClipboardReadText.d.ts.map +1 -1
  44. package/src/core/clipboard/safeClipboardReadText.js +73 -38
  45. package/src/core/clipboard/safeClipboardWriteText.d.ts.map +1 -1
  46. package/src/core/clipboard/safeClipboardWriteText.js +11 -9
  47. package/src/core/collection/array/iterator/ArrayIteratorRandom.d.ts.map +1 -1
  48. package/src/core/collection/array/iterator/ArrayIteratorRandom.js +6 -0
  49. package/src/core/collection/heap/IndexedFloatMinHeap.d.ts +6 -0
  50. package/src/core/collection/heap/IndexedFloatMinHeap.d.ts.map +1 -1
  51. package/src/core/collection/heap/IndexedFloatMinHeap.js +14 -0
  52. package/src/core/collection/list/FilteredListProjection.d.ts.map +1 -1
  53. package/src/core/collection/list/FilteredListProjection.js +7 -54
  54. package/src/core/collection/list/List.d.ts +4 -0
  55. package/src/core/collection/list/List.d.ts.map +1 -1
  56. package/src/core/collection/list/List.js +10 -52
  57. package/src/core/collection/list/SortedListProjection.d.ts +7 -0
  58. package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
  59. package/src/core/collection/list/SortedListProjection.js +12 -0
  60. package/src/core/collection/list/list_apply_ordered_diff.d.ts +37 -0
  61. package/src/core/collection/list/list_apply_ordered_diff.d.ts.map +1 -0
  62. package/src/core/collection/list/list_apply_ordered_diff.js +87 -0
  63. package/src/core/collection/map/BiMap.d.ts.map +1 -1
  64. package/src/core/collection/map/BiMap.js +19 -6
  65. package/src/core/collection/map/HashMap.d.ts.map +1 -1
  66. package/src/core/collection/map/HashMap.js +38 -11
  67. package/src/core/collection/queue/Deque.d.ts +2 -2
  68. package/src/core/collection/queue/Deque.d.ts.map +1 -1
  69. package/src/core/collection/queue/Deque.js +12 -1
  70. package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
  71. package/src/core/collection/table/RowFirstTable.js +15 -2
  72. package/src/core/collection/table/serializeRowFirstTable.d.ts.map +1 -1
  73. package/src/core/collection/table/serializeRowFirstTable.js +2 -3
  74. package/src/core/color/operations/color_lerp.d.ts.map +1 -1
  75. package/src/core/color/operations/color_lerp.js +77 -55
  76. package/src/core/color/sRGB/linear_to_sRGB.d.ts.map +1 -1
  77. package/src/core/color/sRGB/linear_to_sRGB.js +3 -1
  78. package/src/core/debug/matchers/AllOf.d.ts +2 -1
  79. package/src/core/debug/matchers/AllOf.d.ts.map +1 -1
  80. package/src/core/debug/matchers/AllOf.js +1 -1
  81. package/src/core/debug/matchers/AnyOf.d.ts +2 -1
  82. package/src/core/debug/matchers/AnyOf.d.ts.map +1 -1
  83. package/src/core/debug/matchers/AnyOf.js +1 -1
  84. package/src/core/debug/matchers/CombinableMatcher.d.ts +2 -1
  85. package/src/core/debug/matchers/CombinableMatcher.d.ts.map +1 -1
  86. package/src/core/debug/matchers/CombinableMatcher.js +1 -1
  87. package/src/core/debug/matchers/DescribeAs.d.ts +2 -1
  88. package/src/core/debug/matchers/DescribeAs.d.ts.map +1 -1
  89. package/src/core/debug/matchers/DescribeAs.js +2 -1
  90. package/src/core/debug/matchers/IsAnything.d.ts +2 -1
  91. package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
  92. package/src/core/debug/matchers/IsAnything.js +2 -1
  93. package/src/core/debug/matchers/IsEqual.d.ts +2 -1
  94. package/src/core/debug/matchers/IsEqual.d.ts.map +1 -1
  95. package/src/core/debug/matchers/IsEqual.js +2 -1
  96. package/src/core/debug/matchers/IsFinite.d.ts +2 -1
  97. package/src/core/debug/matchers/IsFinite.d.ts.map +1 -1
  98. package/src/core/debug/matchers/IsFinite.js +2 -1
  99. package/src/core/debug/matchers/IsIn.d.ts +2 -1
  100. package/src/core/debug/matchers/IsIn.d.ts.map +1 -1
  101. package/src/core/debug/matchers/IsIn.js +2 -1
  102. package/src/core/debug/matchers/IsInteger.d.ts +2 -1
  103. package/src/core/debug/matchers/IsInteger.d.ts.map +1 -1
  104. package/src/core/debug/matchers/IsInteger.js +2 -1
  105. package/src/core/debug/matchers/IsIterableContaining.d.ts +2 -1
  106. package/src/core/debug/matchers/IsIterableContaining.d.ts.map +1 -1
  107. package/src/core/debug/matchers/IsIterableContaining.js +1 -1
  108. package/src/core/debug/matchers/IsNot.d.ts +2 -1
  109. package/src/core/debug/matchers/IsNot.d.ts.map +1 -1
  110. package/src/core/debug/matchers/IsNot.js +2 -1
  111. package/src/core/debug/matchers/IsNull.d.ts +2 -1
  112. package/src/core/debug/matchers/IsNull.d.ts.map +1 -1
  113. package/src/core/debug/matchers/IsNull.js +2 -1
  114. package/src/core/debug/matchers/IsUndefined.d.ts +2 -1
  115. package/src/core/debug/matchers/IsUndefined.d.ts.map +1 -1
  116. package/src/core/debug/matchers/IsUndefined.js +2 -1
  117. package/src/core/events/signal/Signal.d.ts +15 -0
  118. package/src/core/events/signal/Signal.d.ts.map +1 -1
  119. package/src/core/events/signal/Signal.js +857 -820
  120. package/src/core/fsm/simple/SimpleStateMachineDescription.d.ts.map +1 -1
  121. package/src/core/fsm/simple/SimpleStateMachineDescription.js +4 -3
  122. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_bruteforce.d.ts.map +1 -1
  123. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_bruteforce.js +6 -1
  124. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.d.ts +1 -1
  125. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.d.ts.map +1 -1
  126. package/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.js +6 -7
  127. package/src/core/geom/2d/hash-grid/SpatialHashGrid.d.ts.map +1 -1
  128. package/src/core/geom/2d/hash-grid/SpatialHashGrid.js +23 -6
  129. package/src/core/geom/2d/line/ray2_ray2_compute_intersection_array_2d.d.ts.map +1 -1
  130. package/src/core/geom/2d/line/ray2_ray2_compute_intersection_array_2d.js +13 -1
  131. package/src/core/geom/3d/aabb/aabb3_raycast_hit_point.d.ts.map +1 -1
  132. package/src/core/geom/3d/aabb/aabb3_raycast_hit_point.js +53 -32
  133. package/src/core/geom/3d/atlas/segment/atlas_chart_growth_cost.d.ts +40 -0
  134. package/src/core/geom/3d/atlas/segment/atlas_chart_growth_cost.d.ts.map +1 -0
  135. package/src/core/geom/3d/atlas/segment/atlas_chart_growth_cost.js +54 -0
  136. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.d.ts +21 -3
  137. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.d.ts.map +1 -1
  138. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.js +24 -15
  139. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.d.ts +6 -3
  140. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.d.ts.map +1 -1
  141. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.js +9 -15
  142. package/src/core/geom/3d/cone/cone_compute_plane_side.d.ts.map +1 -1
  143. package/src/core/geom/3d/cone/cone_compute_plane_side.js +31 -19
  144. package/src/core/geom/3d/equirectangular/equirectangular_direction_to_uv.d.ts +11 -1
  145. package/src/core/geom/3d/equirectangular/equirectangular_direction_to_uv.d.ts.map +1 -1
  146. package/src/core/geom/3d/equirectangular/equirectangular_direction_to_uv.js +11 -1
  147. package/src/core/geom/3d/equirectangular/equirectangular_uv_to_direction.d.ts +11 -1
  148. package/src/core/geom/3d/equirectangular/equirectangular_uv_to_direction.d.ts.map +1 -1
  149. package/src/core/geom/3d/equirectangular/equirectangular_uv_to_direction.js +11 -1
  150. package/src/core/geom/3d/frustum/frustum3_compute_closest_point.d.ts.map +1 -1
  151. package/src/core/geom/3d/frustum/frustum3_compute_closest_point.js +55 -0
  152. package/src/core/geom/3d/frustum/frustum_from_projection_matrix_array.d.ts +14 -0
  153. package/src/core/geom/3d/frustum/frustum_from_projection_matrix_array.d.ts.map +1 -1
  154. package/src/core/geom/3d/frustum/frustum_from_projection_matrix_array.js +17 -1
  155. package/src/core/geom/3d/gjk/gjk.d.ts +15 -0
  156. package/src/core/geom/3d/gjk/gjk.d.ts.map +1 -1
  157. package/src/core/geom/3d/gjk/gjk.js +15 -0
  158. package/src/core/geom/3d/gjk/gjk_epa_penetration.d.ts.map +1 -1
  159. package/src/core/geom/3d/gjk/gjk_epa_penetration.js +18 -0
  160. package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts +32 -3
  161. package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts.map +1 -1
  162. package/src/core/geom/3d/hash-grid/PointHashGrid3.js +36 -3
  163. package/src/core/geom/3d/morton/v3_morton_encode_lut.d.ts.map +1 -1
  164. package/src/core/geom/3d/morton/v3_morton_encode_lut.js +11 -4
  165. package/src/core/geom/3d/morton/v3_morton_encode_normalized.d.ts +16 -4
  166. package/src/core/geom/3d/morton/v3_morton_encode_normalized.d.ts.map +1 -1
  167. package/src/core/geom/3d/morton/v3_morton_encode_normalized.js +21 -8
  168. package/src/core/geom/3d/normal/spherical/encode_unit_to_spheremap.d.ts.map +1 -1
  169. package/src/core/geom/3d/normal/spherical/encode_unit_to_spheremap.js +17 -3
  170. package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.d.ts +10 -0
  171. package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.d.ts.map +1 -1
  172. package/src/core/geom/3d/octahedra/octahedral_direction_to_uv.js +46 -49
  173. package/src/core/geom/3d/octahedra/octahedral_uv_to_direction.d.ts +13 -1
  174. package/src/core/geom/3d/octahedra/octahedral_uv_to_direction.d.ts.map +1 -1
  175. package/src/core/geom/3d/octahedra/octahedral_uv_to_direction.js +36 -39
  176. package/src/core/geom/3d/quadric/quadric3_to_tensor_m3_inverse.d.ts +4 -1
  177. package/src/core/geom/3d/quadric/quadric3_to_tensor_m3_inverse.d.ts.map +1 -1
  178. package/src/core/geom/3d/quadric/quadric3_to_tensor_m3_inverse.js +22 -4
  179. package/src/core/geom/3d/quaternion/quat3_canonicalize_sign_run.d.ts +38 -0
  180. package/src/core/geom/3d/quaternion/quat3_canonicalize_sign_run.d.ts.map +1 -0
  181. package/src/core/geom/3d/quaternion/quat3_canonicalize_sign_run.js +76 -0
  182. package/src/core/geom/3d/quaternion/quat3_encode_to_uint32.d.ts +11 -0
  183. package/src/core/geom/3d/quaternion/quat3_encode_to_uint32.d.ts.map +1 -1
  184. package/src/core/geom/3d/quaternion/quat3_encode_to_uint32.js +14 -34
  185. package/src/core/geom/3d/shape/PosedShape3D.d.ts +9 -0
  186. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  187. package/src/core/geom/3d/shape/PosedShape3D.js +43 -0
  188. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  189. package/src/core/geom/3d/shape/UnionShape3D.js +20 -3
  190. package/src/core/geom/3d/shape/json/type_adapters.d.ts +11 -2
  191. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  192. package/src/core/geom/3d/shape/json/type_adapters.js +36 -12
  193. package/src/core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.d.ts +13 -0
  194. package/src/core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.d.ts.map +1 -1
  195. package/src/core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js +17 -4
  196. package/src/core/geom/3d/tetrahedra/tetrahedral_flip_layouts.d.ts +88 -0
  197. package/src/core/geom/3d/tetrahedra/tetrahedral_flip_layouts.d.ts.map +1 -0
  198. package/src/core/geom/3d/tetrahedra/tetrahedral_flip_layouts.js +108 -0
  199. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_23.d.ts.map +1 -1
  200. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_23.js +13 -31
  201. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_32.d.ts.map +1 -1
  202. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_32.js +14 -22
  203. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  204. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +96 -26
  205. package/src/core/geom/3d/tetrahedra/tetrahedron_contains_point.d.ts.map +1 -1
  206. package/src/core/geom/3d/tetrahedra/tetrahedron_contains_point.js +17 -6
  207. package/src/core/geom/3d/topology/struct/binary/io/bt_boundary_loop.d.ts +27 -0
  208. package/src/core/geom/3d/topology/struct/binary/io/bt_boundary_loop.d.ts.map +1 -1
  209. package/src/core/geom/3d/topology/struct/binary/io/bt_boundary_loop.js +101 -0
  210. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.d.ts +3 -0
  211. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.d.ts.map +1 -1
  212. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.js +5 -57
  213. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
  214. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +2 -34
  215. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.d.ts.map +1 -1
  216. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_fill_small_holes.js +2 -43
  217. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +54 -33
  218. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  219. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +229 -68
  220. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill_parallels.d.ts.map +1 -1
  221. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill_parallels.js +4 -15
  222. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_mesh_split_pinched_vertices.d.ts.map +1 -1
  223. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_mesh_split_pinched_vertices.js +13 -1
  224. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_fuse_duplicate_edges.d.ts.map +1 -1
  225. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_fuse_duplicate_edges.js +3 -1
  226. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_replace.d.ts.map +1 -1
  227. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_replace.js +22 -3
  228. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_count_faces.d.ts +17 -2
  229. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_count_faces.d.ts.map +1 -1
  230. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_count_faces.js +28 -4
  231. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_get.d.ts.map +1 -1
  232. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_get.js +8 -1
  233. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_edge_adjacent_faces_weighted.d.ts.map +1 -1
  234. package/src/core/geom/3d/topology/struct/binary/query/bt_face_get_edge_adjacent_faces_weighted.js +10 -2
  235. package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.d.ts.map +1 -1
  236. package/src/core/geom/3d/topology/struct/binary/query/bt_face_island_flood_fill.js +2 -0
  237. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_distance_field.d.ts.map +1 -1
  238. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_boundary_distance_field.js +25 -5
  239. package/src/core/geom/3d/triangle/tri3_compute_normal.d.ts +5 -1
  240. package/src/core/geom/3d/triangle/tri3_compute_normal.d.ts.map +1 -1
  241. package/src/core/geom/3d/triangle/tri3_compute_normal.js +8 -1
  242. package/src/core/geom/3d/triangle/tri3_ray_intersection.d.ts.map +1 -1
  243. package/src/core/geom/3d/triangle/tri3_ray_intersection.js +15 -3
  244. package/src/core/geom/3d/triangle/tri3_ray_intersection_barycentric.d.ts +2 -1
  245. package/src/core/geom/3d/triangle/tri3_ray_intersection_barycentric.d.ts.map +1 -1
  246. package/src/core/geom/3d/triangle/tri3_ray_intersection_barycentric.js +2 -1
  247. package/src/core/geom/3d/triangle/tri3_ray_intersection_barycentric_edge.d.ts +1 -1
  248. package/src/core/geom/3d/triangle/tri3_ray_intersection_barycentric_edge.d.ts.map +1 -1
  249. package/src/core/geom/3d/triangle/tri3_ray_intersection_barycentric_edge.js +8 -1
  250. package/src/core/geom/vec/vector_normalize.d.ts.map +1 -1
  251. package/src/core/geom/vec/vector_normalize.js +5 -1
  252. package/src/core/geom/vec3/v3_slerp.d.ts.map +1 -1
  253. package/src/core/geom/vec3/v3_slerp.js +69 -4
  254. package/src/core/geom/vec4/v4_max_axis.d.ts +28 -0
  255. package/src/core/geom/vec4/v4_max_axis.d.ts.map +1 -0
  256. package/src/core/geom/vec4/v4_max_axis.js +41 -0
  257. package/src/core/graph/coloring/colorizeGraph.js +10 -7
  258. package/src/core/graph/layout/graph_peel_topological_layer.d.ts.map +1 -1
  259. package/src/core/graph/layout/graph_peel_topological_layer.js +16 -2
  260. package/src/core/json/JsonUtils.d.ts.map +1 -1
  261. package/src/core/json/JsonUtils.js +6 -2
  262. package/src/core/json/abstractJSONSerializer.d.ts.map +1 -1
  263. package/src/core/json/abstractJSONSerializer.js +5 -1
  264. package/src/core/lang/reactive/ReactiveOperatorType.d.ts +20 -0
  265. package/src/core/lang/reactive/ReactiveOperatorType.d.ts.map +1 -1
  266. package/src/core/lang/reactive/ReactiveOperatorType.js +34 -12
  267. package/src/core/localization/LanguageMetadata.d.ts +2 -1
  268. package/src/core/localization/LanguageMetadata.d.ts.map +1 -1
  269. package/src/core/localization/LanguageMetadata.js +6 -0
  270. package/src/core/localization/Localization.d.ts.map +1 -1
  271. package/src/core/localization/Localization.js +23 -10
  272. package/src/core/localization/LocalizationEngine.d.ts.map +1 -1
  273. package/src/core/localization/LocalizationEngine.js +4 -0
  274. package/src/core/math/fract.d.ts.map +1 -1
  275. package/src/core/math/fract.js +4 -3
  276. package/src/core/math/hash/squirrel3.d.ts +10 -2
  277. package/src/core/math/hash/squirrel3.d.ts.map +1 -1
  278. package/src/core/math/hash/squirrel3.js +16 -8
  279. package/src/core/math/linalg/m2/m2_polar_decomp_noS.d.ts +7 -0
  280. package/src/core/math/linalg/m2/m2_polar_decomp_noS.d.ts.map +1 -1
  281. package/src/core/math/linalg/m2/m2_polar_decomp_noS.js +20 -1
  282. package/src/core/math/physics/pdf/pdf_normal.d.ts +11 -3
  283. package/src/core/math/physics/pdf/pdf_normal.d.ts.map +1 -1
  284. package/src/core/math/physics/pdf/pdf_normal.js +25 -11
  285. package/src/core/math/solveQuadratic.d.ts.map +1 -1
  286. package/src/core/math/solveQuadratic.js +22 -3
  287. package/src/core/math/statistics/computeStatisticalPartialMedian.d.ts +9 -2
  288. package/src/core/math/statistics/computeStatisticalPartialMedian.d.ts.map +1 -1
  289. package/src/core/math/statistics/computeStatisticalPartialMedian.js +12 -6
  290. package/src/core/math/statistics/computeStatisticalPercentile.d.ts.map +1 -1
  291. package/src/core/math/statistics/computeStatisticalPercentile.js +9 -2
  292. package/src/core/math/statistics/gaussian_amplitude.d.ts +10 -1
  293. package/src/core/math/statistics/gaussian_amplitude.d.ts.map +1 -1
  294. package/src/core/math/statistics/gaussian_amplitude.js +20 -15
  295. package/src/core/model/BooleanVector3.d.ts.map +1 -1
  296. package/src/core/model/BooleanVector3.js +18 -2
  297. package/src/core/model/BoundedValue.d.ts +9 -0
  298. package/src/core/model/BoundedValue.d.ts.map +1 -1
  299. package/src/core/model/BoundedValue.js +30 -3
  300. package/src/core/model/ObservedBoolean.d.ts.map +1 -1
  301. package/src/core/model/ObservedBoolean.js +22 -2
  302. package/src/core/model/ObservedEnum.d.ts.map +1 -1
  303. package/src/core/model/ObservedEnum.js +6 -2
  304. package/src/core/model/node-graph/NodeGraph.d.ts +6 -1
  305. package/src/core/model/node-graph/NodeGraph.d.ts.map +1 -1
  306. package/src/core/model/node-graph/NodeGraph.js +22 -4
  307. package/src/core/model/node-graph/json/deserializeNodeGraphFromJSON.d.ts.map +1 -1
  308. package/src/core/model/node-graph/json/deserializeNodeGraphFromJSON.js +3 -1
  309. package/src/core/model/node-graph/json/serializeNodeGraphToJSON.d.ts.map +1 -1
  310. package/src/core/model/node-graph/json/serializeNodeGraphToJSON.js +5 -0
  311. package/src/core/model/node-graph/type/DataType.d.ts.map +1 -1
  312. package/src/core/model/node-graph/type/DataType.js +8 -0
  313. package/src/core/model/node-graph/type/ParametricDataType.d.ts.map +1 -1
  314. package/src/core/model/node-graph/type/ParametricDataType.js +19 -0
  315. package/src/core/model/node-graph/visual/NodeGraphVisualData.d.ts.map +1 -1
  316. package/src/core/model/node-graph/visual/NodeGraphVisualData.js +5 -0
  317. package/src/core/model/reactive/js/compileReactiveToJS.d.ts +22 -0
  318. package/src/core/model/reactive/js/compileReactiveToJS.d.ts.map +1 -1
  319. package/src/core/model/reactive/js/compileReactiveToJS.js +113 -219
  320. package/src/core/model/reactive/model/ReactiveBinaryExpression.d.ts.map +1 -1
  321. package/src/core/model/reactive/model/ReactiveBinaryExpression.js +8 -2
  322. package/src/core/model/reactive/model/comparative/ReactiveEquals.d.ts.map +1 -1
  323. package/src/core/model/reactive/model/comparative/ReactiveEquals.js +3 -1
  324. package/src/core/model/reactive/model/comparative/ReactiveNotEquals.d.ts.map +1 -1
  325. package/src/core/model/reactive/model/comparative/ReactiveNotEquals.js +2 -1
  326. package/src/core/model/reactive/model/terminal/ReactiveLiteralString.d.ts.map +1 -1
  327. package/src/core/model/reactive/model/terminal/ReactiveLiteralString.js +4 -2
  328. package/src/core/model/stat/Stat.d.ts.map +1 -1
  329. package/src/core/model/stat/Stat.js +4 -0
  330. package/src/core/parser/seedVariablesIntoTemplateString.d.ts.map +1 -1
  331. package/src/core/parser/seedVariablesIntoTemplateString.js +5 -0
  332. package/src/core/parser/simple/readArrayLiteral.d.ts.map +1 -1
  333. package/src/core/parser/simple/readArrayLiteral.js +7 -0
  334. package/src/core/parser/simple/readHexToken.d.ts +3 -0
  335. package/src/core/parser/simple/readHexToken.d.ts.map +1 -1
  336. package/src/core/parser/simple/readHexToken.js +39 -106
  337. package/src/core/parser/simple/readNumberToken.d.ts.map +1 -1
  338. package/src/core/parser/simple/readNumberToken.js +13 -4
  339. package/src/core/parser/simple/readUnsignedIntegerToken.d.ts +2 -0
  340. package/src/core/parser/simple/readUnsignedIntegerToken.d.ts.map +1 -1
  341. package/src/core/parser/simple/readUnsignedIntegerToken.js +31 -71
  342. package/src/core/parser/simple/read_digits.d.ts +31 -0
  343. package/src/core/parser/simple/read_digits.d.ts.map +1 -0
  344. package/src/core/parser/simple/read_digits.js +74 -0
  345. package/src/core/primitives/numbers/number_format_by_thousands.d.ts.map +1 -1
  346. package/src/core/primitives/numbers/number_format_by_thousands.js +16 -1
  347. package/src/core/primitives/numbers/number_pretty_print.d.ts.map +1 -1
  348. package/src/core/primitives/numbers/number_pretty_print.js +6 -1
  349. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  350. package/src/core/process/executor/ConcurrentExecutor.js +36 -0
  351. package/src/core/process/task/Task.d.ts +8 -0
  352. package/src/core/process/task/Task.d.ts.map +1 -1
  353. package/src/core/process/task/Task.js +12 -1
  354. package/src/core/process/task/TaskGroup.d.ts +6 -0
  355. package/src/core/process/task/TaskGroup.d.ts.map +1 -1
  356. package/src/core/process/task/TaskGroup.js +7 -1
  357. package/src/core/process/undo/ActionProcessor.d.ts +12 -0
  358. package/src/core/process/undo/ActionProcessor.d.ts.map +1 -1
  359. package/src/core/process/undo/ActionProcessor.js +41 -15
  360. package/src/core/process/worker/OnDemandWorkerManager.d.ts.map +1 -1
  361. package/src/core/process/worker/OnDemandWorkerManager.js +6 -1
  362. package/src/core/process/worker/WorkerBuilder.d.ts.map +1 -1
  363. package/src/core/process/worker/WorkerBuilder.js +27 -5
  364. package/src/engine/Clock.d.ts.map +1 -1
  365. package/src/engine/Clock.js +11 -0
  366. package/src/engine/Engine.d.ts.map +1 -1
  367. package/src/engine/Engine.js +107 -6
  368. package/src/engine/achievements/AchievementManager.d.ts.map +1 -1
  369. package/src/engine/achievements/AchievementManager.js +16 -1
  370. package/src/engine/animation/clip/ecd_bind_animation_curve.d.ts.map +1 -1
  371. package/src/engine/animation/clip/ecd_bind_animation_curve.js +17 -6
  372. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts +19 -10
  373. package/src/engine/animation/clip/hemisphere_align_quaternion_values.d.ts.map +1 -1
  374. package/src/engine/animation/clip/hemisphere_align_quaternion_values.js +19 -23
  375. package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts +1 -1
  376. package/src/engine/animation/keyed2/AnimationTrackPlayback.d.ts.map +1 -1
  377. package/src/engine/animation/keyed2/AnimationTrackPlayback.js +21 -4
  378. package/src/engine/asset/AssetManager.d.ts.map +1 -1
  379. package/src/engine/asset/AssetManager.js +11 -3
  380. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -0
  381. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  382. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +28 -0
  383. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  384. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +14 -0
  385. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +23 -2
  386. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  387. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +2122 -2078
  388. package/src/engine/ecs/Entity.d.ts.map +1 -1
  389. package/src/engine/ecs/Entity.js +43 -7
  390. package/src/engine/ecs/EntityBlueprint.d.ts.map +1 -1
  391. package/src/engine/ecs/EntityBlueprint.js +13 -4
  392. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  393. package/src/engine/ecs/EntityComponentDataset.js +95 -9
  394. package/src/engine/ecs/EntityManager.d.ts +16 -0
  395. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  396. package/src/engine/ecs/EntityManager.js +112 -37
  397. package/src/engine/ecs/EntityObserver.d.ts +7 -0
  398. package/src/engine/ecs/EntityObserver.d.ts.map +1 -1
  399. package/src/engine/ecs/EntityObserver.js +8 -1
  400. package/src/engine/ecs/System.d.ts +23 -0
  401. package/src/engine/ecs/System.d.ts.map +1 -1
  402. package/src/engine/ecs/System.js +25 -0
  403. package/src/engine/ecs/components/Tag.d.ts.map +1 -1
  404. package/src/engine/ecs/components/Tag.js +4 -1
  405. package/src/engine/ecs/computeComponentClassName.d.ts +13 -1
  406. package/src/engine/ecs/computeComponentClassName.d.ts.map +1 -1
  407. package/src/engine/ecs/computeComponentClassName.js +11 -1
  408. package/src/engine/ecs/dynamic_actions/DynamicActorSystem.d.ts.map +1 -1
  409. package/src/engine/ecs/dynamic_actions/DynamicActorSystem.js +11 -0
  410. package/src/engine/ecs/fow/FogOfWar.d.ts.map +1 -1
  411. package/src/engine/ecs/fow/FogOfWar.js +37 -11
  412. package/src/engine/ecs/parent/EntityNode.d.ts.map +1 -1
  413. package/src/engine/ecs/parent/EntityNode.js +8 -0
  414. package/src/engine/ecs/speaker/VoiceSystem.d.ts.map +1 -1
  415. package/src/engine/ecs/speaker/VoiceSystem.js +9 -0
  416. package/src/engine/ecs/storage/binary/BinaryClassSerializationAdapter.d.ts +15 -0
  417. package/src/engine/ecs/storage/binary/BinaryClassSerializationAdapter.d.ts.map +1 -1
  418. package/src/engine/ecs/storage/binary/BinaryClassSerializationAdapter.js +18 -0
  419. package/src/engine/ecs/system/AbstractContextSystem.d.ts.map +1 -1
  420. package/src/engine/ecs/system/AbstractContextSystem.js +14 -4
  421. package/src/engine/ecs/system/SystemEntityContext.d.ts +12 -0
  422. package/src/engine/ecs/system/SystemEntityContext.d.ts.map +1 -1
  423. package/src/engine/ecs/system/SystemEntityContext.js +23 -0
  424. package/src/engine/ecs/system_validate_class.d.ts.map +1 -1
  425. package/src/engine/ecs/system_validate_class.js +6 -2
  426. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +37 -8
  427. package/src/engine/ecs/terrain/tiles/TerrainTile.d.ts.map +1 -1
  428. package/src/engine/ecs/terrain/tiles/TerrainTile.js +45 -3
  429. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  430. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +5 -8
  431. package/src/engine/graphics/ecs/camera/pp/PerfectPanner.d.ts.map +1 -1
  432. package/src/engine/graphics/ecs/camera/pp/PerfectPanner.js +62 -7
  433. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.d.ts +9 -0
  434. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.d.ts.map +1 -1
  435. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js +13 -0
  436. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshAnimationControllerSystem.d.ts +11 -0
  437. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshAnimationControllerSystem.d.ts.map +1 -1
  438. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshAnimationControllerSystem.js +78 -13
  439. package/src/engine/graphics/ecs/path/tube/build/make_ring_vertices.d.ts.map +1 -1
  440. package/src/engine/graphics/ecs/path/tube/build/make_ring_vertices.js +22 -8
  441. package/src/engine/graphics/geometry/buffered/geometry_compute_vertex_normals_indexed.d.ts.map +1 -1
  442. package/src/engine/graphics/geometry/buffered/geometry_compute_vertex_normals_indexed.js +18 -2
  443. package/src/engine/graphics/geometry/geometry_construct_triangle_normal.d.ts +3 -1
  444. package/src/engine/graphics/geometry/geometry_construct_triangle_normal.d.ts.map +1 -1
  445. package/src/engine/graphics/geometry/geometry_construct_triangle_normal.js +3 -1
  446. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts +32 -0
  447. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.d.ts.map +1 -0
  448. package/src/engine/graphics/geometry/geometry_construct_triangle_normal_area_weighted.js +53 -0
  449. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts +22 -2
  450. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts.map +1 -1
  451. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js +1201 -1160
  452. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseVelocity.d.ts +0 -1
  453. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseVelocity.d.ts.map +1 -1
  454. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseVelocity.js +10 -5
  455. package/src/engine/graphics/particles/particular/engine/simulator/update_parameters.d.ts.map +1 -1
  456. package/src/engine/graphics/particles/particular/engine/simulator/update_parameters.js +23 -0
  457. package/src/engine/graphics/particles/particular/group/itemSizeFromAttributeType.d.ts +1 -1
  458. package/src/engine/graphics/sh3/path_tracer/texture/apply_texture_clamping_to_coordinate.d.ts.map +1 -1
  459. package/src/engine/graphics/sh3/path_tracer/texture/apply_texture_clamping_to_coordinate.js +6 -1
  460. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.d.ts.map +1 -1
  461. package/src/engine/graphics/texture/atlas/ManagedTextureAtlas.js +20 -0
  462. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts +1 -0
  463. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  464. package/src/engine/graphics/texture/atlas/TextureAtlas.js +24 -5
  465. package/src/engine/graphics/texture/sampler/Sampler2D.d.ts.map +1 -1
  466. package/src/engine/graphics/texture/sampler/Sampler2D.js +14 -1
  467. package/src/engine/graphics/texture/sampler/sampler2d_sub_copy_same_item_size.d.ts.map +1 -1
  468. package/src/engine/graphics/texture/sampler/sampler2d_sub_copy_same_item_size.js +16 -5
  469. package/src/engine/graphics/texture/sampler/sampler2d_to_uint8_RGBA.d.ts.map +1 -1
  470. package/src/engine/graphics/texture/sampler/sampler2d_to_uint8_RGBA.js +29 -8
  471. package/src/engine/graphics/texture/sampler/serialization/TextureBinaryBufferSerializer.d.ts.map +1 -1
  472. package/src/engine/graphics/texture/sampler/serialization/TextureBinaryBufferSerializer.js +8 -3
  473. package/src/engine/graphics3/DecalSystem.d.ts +5 -4
  474. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  475. package/src/engine/graphics3/DecalSystem.js +5 -4
  476. package/src/engine/graphics3/GraphicsEngine.d.ts +40 -7
  477. package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -1
  478. package/src/engine/graphics3/GraphicsEngine.js +92 -7
  479. package/src/engine/graphics3/MeshSystem.d.ts +7 -0
  480. package/src/engine/graphics3/MeshSystem.d.ts.map +1 -1
  481. package/src/engine/graphics3/MeshSystem.js +681 -661
  482. package/src/engine/graphics3/ParticleEmitterSystem.d.ts +1 -0
  483. package/src/engine/graphics3/ParticleEmitterSystem.d.ts.map +1 -1
  484. package/src/engine/graphics3/ParticleEmitterSystem.js +1 -0
  485. package/src/engine/graphics3/TerrainSystem.d.ts +10 -40
  486. package/src/engine/graphics3/TerrainSystem.d.ts.map +1 -1
  487. package/src/engine/graphics3/TerrainSystem.js +186 -185
  488. package/src/engine/graphics3/Trail3DSystem.d.ts +28 -19
  489. package/src/engine/graphics3/Trail3DSystem.d.ts.map +1 -1
  490. package/src/engine/graphics3/Trail3DSystem.js +57 -64
  491. package/src/engine/graphics3/debug/gizmo_write_geometry.d.ts.map +1 -1
  492. package/src/engine/graphics3/debug/gizmo_write_geometry.js +7 -59
  493. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  494. package/src/engine/graphics3/decal/GPUDecalRenderer.js +23 -7
  495. package/src/engine/graphics3/decal/shader_cluster_assign_decals.d.ts.map +1 -1
  496. package/src/engine/graphics3/decal/shader_cluster_assign_decals.js +29 -6
  497. package/src/engine/graphics3/path/tube_stream_write_geometry.d.ts.map +1 -1
  498. package/src/engine/graphics3/path/tube_stream_write_geometry.js +7 -56
  499. package/src/engine/graphics3/register_instance_animation.d.ts +3 -3
  500. package/src/engine/graphics3/register_instance_animation.js +3 -3
  501. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts.map +1 -1
  502. package/src/engine/graphics3/terrain/TERRAIN_MATERIAL_TEMPLATE.d.ts +77 -0
  503. package/src/engine/graphics3/terrain/TERRAIN_MATERIAL_TEMPLATE.d.ts.map +1 -0
  504. package/src/engine/graphics3/terrain/TERRAIN_MATERIAL_TEMPLATE.js +485 -0
  505. package/src/engine/graphics3/terrain/TerrainShadeMaterial.d.ts +103 -0
  506. package/src/engine/graphics3/terrain/TerrainShadeMaterial.d.ts.map +1 -0
  507. package/src/engine/graphics3/terrain/TerrainShadeMaterial.js +130 -0
  508. package/src/engine/graphics3/terrain/chunk_terrain_material_surface.d.ts +23 -0
  509. package/src/engine/graphics3/terrain/chunk_terrain_material_surface.d.ts.map +1 -0
  510. package/src/engine/graphics3/terrain/chunk_terrain_material_surface.js +68 -0
  511. package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts.map +1 -1
  512. package/src/engine/graphics3/trail/trail_tube_write_geometry.js +8 -59
  513. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.d.ts.map +1 -1
  514. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.js +10 -0
  515. package/src/engine/input/devices/PointerDevice.d.ts.map +1 -1
  516. package/src/engine/input/devices/PointerDevice.js +58 -5
  517. package/src/engine/input/ecs/ism/InputMapProgram.d.ts.map +1 -1
  518. package/src/engine/input/ecs/ism/InputMapProgram.js +13 -0
  519. package/src/engine/input/ecs/systems/InputControllerSystem.d.ts +1 -0
  520. package/src/engine/input/ecs/systems/InputControllerSystem.d.ts.map +1 -1
  521. package/src/engine/input/ecs/systems/InputControllerSystem.js +52 -2
  522. package/src/engine/intelligence/behavior/composite/ParallelBehavior.d.ts.map +1 -1
  523. package/src/engine/intelligence/behavior/composite/ParallelBehavior.js +8 -0
  524. package/src/engine/intelligence/behavior/decorator/RepeatBehavior.d.ts.map +1 -1
  525. package/src/engine/intelligence/behavior/decorator/RepeatBehavior.js +7 -0
  526. package/src/engine/intelligence/behavior/decorator/RepeatUntilSuccessBehavior.d.ts.map +1 -1
  527. package/src/engine/intelligence/behavior/decorator/RepeatUntilSuccessBehavior.js +14 -0
  528. package/src/engine/intelligence/behavior/ecs/BehaviorSystem.d.ts.map +1 -1
  529. package/src/engine/intelligence/behavior/ecs/BehaviorSystem.js +13 -0
  530. package/src/engine/interpolation/InterpolationLog.d.ts +51 -4
  531. package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -1
  532. package/src/engine/interpolation/InterpolationLog.js +132 -2
  533. package/src/engine/interpolation/InterpolationSystem.d.ts +113 -35
  534. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -1
  535. package/src/engine/interpolation/InterpolationSystem.js +674 -50
  536. package/src/engine/network/NetworkSession.d.ts +176 -47
  537. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  538. package/src/engine/network/NetworkSession.js +1462 -272
  539. package/src/engine/network/README.md +106 -3
  540. package/src/engine/network/SMOOTHNESS_DESIGN.md +392 -0
  541. package/src/engine/network/adapters/PoseCorrection.d.ts +50 -0
  542. package/src/engine/network/adapters/PoseCorrection.d.ts.map +1 -0
  543. package/src/engine/network/adapters/PoseCorrection.js +211 -0
  544. package/src/engine/network/adapters/TransformReplicationAdapter.d.ts +1 -0
  545. package/src/engine/network/adapters/TransformReplicationAdapter.d.ts.map +1 -1
  546. package/src/engine/network/adapters/TransformReplicationAdapter.js +17 -0
  547. package/src/engine/network/adapters/VelocityCorrection.d.ts +31 -0
  548. package/src/engine/network/adapters/VelocityCorrection.d.ts.map +1 -0
  549. package/src/engine/network/adapters/VelocityCorrection.js +71 -0
  550. package/src/engine/network/adapters/replicate_transform64.d.ts +13 -0
  551. package/src/engine/network/adapters/replicate_transform64.d.ts.map +1 -0
  552. package/src/engine/network/adapters/replicate_transform64.js +18 -0
  553. package/src/engine/network/adapters/rigid_body_of.d.ts +22 -0
  554. package/src/engine/network/adapters/rigid_body_of.d.ts.map +1 -0
  555. package/src/engine/network/adapters/rigid_body_of.js +33 -0
  556. package/src/engine/network/correction/Correction.d.ts +120 -0
  557. package/src/engine/network/correction/Correction.d.ts.map +1 -0
  558. package/src/engine/network/correction/Correction.js +143 -0
  559. package/src/engine/network/ecs/NetworkSystem.d.ts +36 -7
  560. package/src/engine/network/ecs/NetworkSystem.d.ts.map +1 -1
  561. package/src/engine/network/ecs/NetworkSystem.js +49 -7
  562. package/src/engine/network/orchestrator/NetworkPeer.d.ts +50 -1
  563. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  564. package/src/engine/network/orchestrator/NetworkPeer.js +143 -10
  565. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +38 -2
  566. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  567. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +58 -10
  568. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +68 -2
  569. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  570. package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +299 -18
  571. package/src/engine/network/replication/Replicator.d.ts +8 -1
  572. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  573. package/src/engine/network/replication/Replicator.js +50 -6
  574. package/src/engine/network/sim/ActionLog.d.ts +5 -2
  575. package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
  576. package/src/engine/network/sim/ActionLog.js +5 -2
  577. package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
  578. package/src/engine/network/sim/RewindEngine.js +6 -0
  579. package/src/engine/network/sim/SimActionExecutor.d.ts +48 -3
  580. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  581. package/src/engine/network/sim/SimActionExecutor.js +152 -6
  582. package/src/engine/network/sim/SmoothingState.d.ts +54 -2
  583. package/src/engine/network/sim/SmoothingState.d.ts.map +1 -1
  584. package/src/engine/network/sim/SmoothingState.js +107 -3
  585. package/src/engine/network/state/MutationLedger.d.ts.map +1 -1
  586. package/src/engine/network/state/MutationLedger.js +18 -2
  587. package/src/engine/network/state/PredictionHistory.d.ts +67 -0
  588. package/src/engine/network/state/PredictionHistory.d.ts.map +1 -0
  589. package/src/engine/network/state/PredictionHistory.js +145 -0
  590. package/src/engine/network/time/RenderPlayout.d.ts +2 -1
  591. package/src/engine/network/time/RenderPlayout.d.ts.map +1 -1
  592. package/src/engine/network/time/RenderPlayout.js +174 -163
  593. package/src/engine/network/transport/DeferredTransport.d.ts +57 -0
  594. package/src/engine/network/transport/DeferredTransport.d.ts.map +1 -0
  595. package/src/engine/network/transport/DeferredTransport.js +143 -0
  596. package/src/engine/network/transport/LoopbackTransport.d.ts +8 -0
  597. package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
  598. package/src/engine/network/transport/LoopbackTransport.js +12 -1
  599. package/src/engine/network/transport/ReliableCommandPipeline.d.ts.map +1 -1
  600. package/src/engine/network/transport/ReliableCommandPipeline.js +20 -0
  601. package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
  602. package/src/engine/network/transport/adapters/NodeUDPTransport.js +11 -2
  603. package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
  604. package/src/engine/network/transport/adapters/WebTransportTransport.js +8 -1
  605. package/src/engine/network/transport/fragments/FragmentAssembler.d.ts +2 -1
  606. package/src/engine/network/transport/fragments/FragmentAssembler.d.ts.map +1 -1
  607. package/src/engine/network/transport/fragments/FragmentAssembler.js +67 -3
  608. package/src/engine/network/transport/fragments/fragment_send.d.ts +8 -0
  609. package/src/engine/network/transport/fragments/fragment_send.d.ts.map +1 -1
  610. package/src/engine/network/transport/fragments/fragment_send.js +17 -7
  611. package/src/engine/network/transport/fragments/packet_size.d.ts +6 -0
  612. package/src/engine/network/transport/fragments/packet_size.d.ts.map +1 -1
  613. package/src/engine/network/transport/fragments/packet_size.js +6 -0
  614. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  615. package/src/engine/physics/ccd/linear_sweep.js +19 -1
  616. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +7 -3
  617. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  618. package/src/engine/physics/cloth/ecs/ClothWorld.js +34 -4
  619. package/src/engine/physics/cloth/solver/cloth_self_find.js +17 -0
  620. package/src/engine/physics/contact/ManifoldStore.d.ts +20 -1
  621. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  622. package/src/engine/physics/contact/ManifoldStore.js +24 -2
  623. package/src/engine/physics/ecs/PhysicsSystem.d.ts +51 -88
  624. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  625. package/src/engine/physics/ecs/PhysicsSystem.js +82 -217
  626. package/src/engine/physics/ecs/is_sensor.d.ts +24 -0
  627. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -1
  628. package/src/engine/physics/ecs/is_sensor.js +39 -0
  629. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts +18 -4
  630. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts.map +1 -1
  631. package/src/engine/physics/fluid/ecs/FluidComponent.js +29 -8
  632. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  633. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts +8 -0
  634. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts.map +1 -1
  635. package/src/engine/physics/fluid/effector/AbstractFluidEffector.js +8 -0
  636. package/src/engine/physics/fluid/solver/NOTES.md +46 -0
  637. package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_scalar.js +1 -1
  638. package/src/engine/physics/fluid/solver/v3_grid_advect_maccormack_velocity.js +1 -1
  639. package/src/engine/physics/fluid/solver/v3_grid_advect_sl_velocity.js +1 -1
  640. package/src/engine/physics/fluid/solver/v3_grid_apply_advection_forward.js +1 -1
  641. package/src/engine/physics/fluid/solver/v3_grid_apply_scalar_advection.js +1 -1
  642. package/src/engine/physics/fluid/solver/v3_grid_apply_vorticity_confinement.d.ts.map +1 -1
  643. package/src/engine/physics/fluid/solver/v3_grid_apply_vorticity_confinement.js +8 -0
  644. package/src/engine/physics/fluid/solver/v3_grid_compute_divergence.d.ts.map +1 -1
  645. package/src/engine/physics/fluid/solver/v3_grid_compute_divergence.js +8 -0
  646. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  647. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +8 -0
  648. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.d.ts +5 -0
  649. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.d.ts.map +1 -1
  650. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.js +5 -0
  651. package/src/engine/physics/integration/integrate_velocity.d.ts +15 -0
  652. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  653. package/src/engine/physics/integration/integrate_velocity.js +15 -0
  654. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  655. package/src/engine/physics/island/IslandBuilder.js +6 -7
  656. package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
  657. package/src/engine/physics/mls-mpm/MLS_MPM.js +29 -13
  658. package/src/engine/physics/mls-mpm/mls_mpm_demo_scene.d.ts +29 -0
  659. package/src/engine/physics/mls-mpm/mls_mpm_demo_scene.d.ts.map +1 -0
  660. package/src/engine/physics/mls-mpm/mls_mpm_demo_scene.js +44 -0
  661. package/src/engine/physics/mls-mpm/testMLS_MPM.js +3 -3
  662. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  663. package/src/engine/physics/narrowphase/narrowphase_step.js +74 -11
  664. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  665. package/src/engine/physics/queries/overlap_shape.js +92 -45
  666. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  667. package/src/engine/physics/queries/raycast.js +55 -43
  668. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  669. package/src/engine/physics/queries/shape_cast.js +185 -174
  670. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  671. package/src/engine/physics/solver/solve_contacts.js +42 -19
  672. package/src/engine/plugin/EnginePluginManager.d.ts +10 -0
  673. package/src/engine/plugin/EnginePluginManager.d.ts.map +1 -1
  674. package/src/engine/plugin/EnginePluginManager.js +54 -1
  675. package/src/engine/plugin/PluginReferenceContext.d.ts.map +1 -1
  676. package/src/engine/plugin/PluginReferenceContext.js +7 -3
  677. package/src/engine/reference/v2/Reference.d.ts +9 -0
  678. package/src/engine/reference/v2/Reference.d.ts.map +1 -1
  679. package/src/engine/reference/v2/Reference.js +39 -2
  680. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  681. package/src/engine/simulation/Ticker.js +20 -1
  682. package/src/engine/sound/ecs/SoundListenerSystem.d.ts +27 -0
  683. package/src/engine/sound/ecs/SoundListenerSystem.d.ts.map +1 -1
  684. package/src/engine/sound/ecs/SoundListenerSystem.js +82 -21
  685. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
  686. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +394 -356
  687. package/src/engine/sound/material/concrete/SoundMaterialComposition.d.ts.map +1 -1
  688. package/src/engine/sound/material/concrete/SoundMaterialComposition.js +54 -17
  689. package/src/engine/sound/material/detector/terrain/TerrainSoundMaterialSurfaceDetector.d.ts.map +1 -1
  690. package/src/engine/sound/material/detector/terrain/TerrainSoundMaterialSurfaceDetector.js +34 -3
  691. package/src/engine/sound/simulation/core/AcousticSolution.d.ts +5 -0
  692. package/src/engine/sound/simulation/core/AcousticSolution.d.ts.map +1 -1
  693. package/src/engine/sound/simulation/core/AcousticSolution.js +5 -0
  694. package/src/engine/sound/simulation/definition/AcousticMaterial.d.ts +8 -0
  695. package/src/engine/sound/simulation/definition/AcousticMaterial.d.ts.map +1 -1
  696. package/src/engine/sound/simulation/definition/AcousticMaterial.js +8 -0
  697. package/src/engine/sound/simulation/probe/probe_visibility_augment.d.ts +6 -4
  698. package/src/engine/sound/simulation/probe/probe_visibility_augment.d.ts.map +1 -1
  699. package/src/engine/sound/simulation/probe/probe_visibility_augment.js +19 -7
  700. package/src/engine/sound/simulation/render/FoaProbeReverbRenderer.d.ts +39 -0
  701. package/src/engine/sound/simulation/render/FoaProbeReverbRenderer.d.ts.map +1 -1
  702. package/src/engine/sound/simulation/render/FoaProbeReverbRenderer.js +130 -1
  703. package/src/engine/sound/simulation/render/ProbeReverbRenderer.d.ts +3 -2
  704. package/src/engine/sound/simulation/render/ProbeReverbRenderer.d.ts.map +1 -1
  705. package/src/engine/sound/simulation/render/ProbeReverbRenderer.js +15 -1
  706. package/src/engine/sound/sopra/SopraEngine.d.ts +10 -0
  707. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
  708. package/src/engine/sound/sopra/SopraEngine.js +486 -440
  709. package/src/engine/sound/sopra/runtime/BusGraph.d.ts +29 -0
  710. package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
  711. package/src/engine/sound/sopra/runtime/BusGraph.js +69 -2
  712. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  713. package/src/engine/sound/sopra/runtime/EventInstance.js +130 -9
  714. package/src/engine/ui/GUIEngine.d.ts +14 -0
  715. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  716. package/src/engine/ui/GUIEngine.js +52 -4
  717. package/src/format/image/avif/api/apply_transformations.d.ts +2 -1
  718. package/src/format/image/avif/api/apply_transformations.d.ts.map +1 -1
  719. package/src/format/image/avif/api/apply_transformations.js +129 -120
  720. package/src/format/image/avif/api/convert_to_rgba.d.ts.map +1 -1
  721. package/src/format/image/avif/api/convert_to_rgba.js +35 -2
  722. package/src/format/image/avif/api/decode_image_item.d.ts +2 -1
  723. package/src/format/image/avif/api/decode_image_item.d.ts.map +1 -1
  724. package/src/format/image/avif/api/decode_image_item.js +322 -234
  725. package/src/format/image/avif/av1/decode/decode_av1_still.js +329 -309
  726. package/src/format/image/avif/av1/encode/encode_tile.js +2349 -2331
  727. package/src/format/image/avif/av1/entropy/symbol_cost.d.ts.map +1 -1
  728. package/src/format/image/avif/av1/entropy/symbol_cost.js +86 -76
  729. package/src/format/image/avif/heif/AvifFile.d.ts +11 -0
  730. package/src/format/image/avif/heif/AvifFile.d.ts.map +1 -1
  731. package/src/format/image/avif/heif/AvifFile.js +154 -142
  732. package/src/format/image/avif/heif/find_item_properties.d.ts +23 -0
  733. package/src/format/image/avif/heif/find_item_properties.d.ts.map +1 -0
  734. package/src/format/image/avif/heif/find_item_properties.js +36 -0
  735. package/src/format/image/avif/heif/parse_avif_file.js +6 -3
  736. package/src/format/image/avif/heif/read_item_data.d.ts.map +1 -1
  737. package/src/format/image/avif/heif/read_item_data.js +131 -97
  738. package/src/format/image/jpeg/decodeScan.d.ts.map +1 -1
  739. package/src/format/image/jpeg/decodeScan.js +17 -0
  740. package/src/format/scene/gltf/gltf_parse_container.d.ts.map +1 -1
  741. package/src/format/scene/gltf/gltf_parse_container.js +61 -41
  742. package/src/format/scene/gltf/gltf_read_accessor.d.ts +21 -0
  743. package/src/format/scene/gltf/gltf_read_accessor.d.ts.map +1 -1
  744. package/src/format/scene/gltf/gltf_read_accessor.js +37 -0
  745. package/src/format/scene/usd/usd_compose_transform.d.ts.map +1 -1
  746. package/src/format/scene/usd/usd_compose_transform.js +56 -6
  747. package/src/format/texture/basis/transcode/fit_endpoints.d.ts +6 -0
  748. package/src/format/texture/basis/transcode/fit_endpoints.d.ts.map +1 -1
  749. package/src/format/texture/basis/transcode/fit_endpoints.js +33 -39
  750. package/src/generation/filtering/numeric/CellFilterCache.d.ts +17 -0
  751. package/src/generation/filtering/numeric/CellFilterCache.d.ts.map +1 -1
  752. package/src/generation/filtering/numeric/CellFilterCache.js +36 -3
  753. package/src/generation/grid/actions/ContinuousGridCellActionSetTerrainObstacle.d.ts +9 -0
  754. package/src/generation/grid/actions/ContinuousGridCellActionSetTerrainObstacle.d.ts.map +1 -1
  755. package/src/generation/grid/actions/ContinuousGridCellActionSetTerrainObstacle.js +37 -6
  756. package/src/generation/grid/actions/ContinuousGridCellActionWriteObstacle.d.ts.map +1 -1
  757. package/src/generation/grid/actions/ContinuousGridCellActionWriteObstacle.js +8 -21
  758. package/src/generation/grid/actions/grid_obstacle_write_grid_point.d.ts +28 -0
  759. package/src/generation/grid/actions/grid_obstacle_write_grid_point.d.ts.map +1 -0
  760. package/src/generation/grid/actions/grid_obstacle_write_grid_point.js +41 -0
  761. package/src/generation/grid/generation/GridTaskDensityMarkerDistribution.d.ts +15 -1
  762. package/src/generation/grid/generation/GridTaskDensityMarkerDistribution.d.ts.map +1 -1
  763. package/src/generation/grid/generation/GridTaskDensityMarkerDistribution.js +17 -3
  764. package/src/generation/markers/actions/MarkerNodeProcessingRuleSet.d.ts.map +1 -1
  765. package/src/generation/markers/actions/MarkerNodeProcessingRuleSet.js +16 -1
  766. package/src/generation/placement/action/GridCellActionWriteFilterToLayer.d.ts.map +1 -1
  767. package/src/generation/placement/action/GridCellActionWriteFilterToLayer.js +8 -1
  768. package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
  769. package/src/generation/theme/TerrainTheme.js +20 -1
  770. package/src/shade/RENDERER_CONTRACT.md +48 -6
  771. package/src/shade/descriptor/texture/format/gpu_texture_formats_copy_compatible.d.ts +23 -0
  772. package/src/shade/descriptor/texture/format/gpu_texture_formats_copy_compatible.d.ts.map +1 -0
  773. package/src/shade/descriptor/texture/format/gpu_texture_formats_copy_compatible.js +28 -0
  774. package/src/shade/descriptor/texture/format/gpu_texture_formats_view_compatible.d.ts +25 -0
  775. package/src/shade/descriptor/texture/format/gpu_texture_formats_view_compatible.d.ts.map +1 -0
  776. package/src/shade/descriptor/texture/format/gpu_texture_formats_view_compatible.js +42 -0
  777. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  778. package/src/shade/device/ShadeGPUCommandContext.js +874 -857
  779. package/src/shade/device/mock/SoftwareGPUBindGroup.d.ts +38 -1
  780. package/src/shade/device/mock/SoftwareGPUBindGroup.d.ts.map +1 -1
  781. package/src/shade/device/mock/SoftwareGPUBindGroup.js +73 -1
  782. package/src/shade/device/mock/SoftwareGPUBuffer.d.ts.map +1 -1
  783. package/src/shade/device/mock/SoftwareGPUBuffer.js +5 -0
  784. package/src/shade/device/mock/SoftwareGPUCommandBuffer.d.ts +34 -0
  785. package/src/shade/device/mock/SoftwareGPUCommandBuffer.d.ts.map +1 -1
  786. package/src/shade/device/mock/SoftwareGPUCommandBuffer.js +71 -1
  787. package/src/shade/device/mock/SoftwareGPUCommandEncoder.d.ts +15 -0
  788. package/src/shade/device/mock/SoftwareGPUCommandEncoder.d.ts.map +1 -1
  789. package/src/shade/device/mock/SoftwareGPUCommandEncoder.js +109 -1
  790. package/src/shade/device/mock/SoftwareGPUComputePassEncoder.d.ts.map +1 -1
  791. package/src/shade/device/mock/SoftwareGPUComputePassEncoder.js +51 -0
  792. package/src/shade/device/mock/SoftwareGPUDevice.d.ts +5 -1
  793. package/src/shade/device/mock/SoftwareGPUDevice.d.ts.map +1 -1
  794. package/src/shade/device/mock/SoftwareGPUDevice.js +58 -1
  795. package/src/shade/device/mock/SoftwareGPUPipeline.d.ts +1 -0
  796. package/src/shade/device/mock/SoftwareGPUPipeline.d.ts.map +1 -1
  797. package/src/shade/device/mock/SoftwareGPUPipeline.js +17 -11
  798. package/src/shade/device/mock/SoftwareGPUQueue.d.ts.map +1 -1
  799. package/src/shade/device/mock/SoftwareGPUQueue.js +41 -0
  800. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.d.ts.map +1 -1
  801. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.js +24 -0
  802. package/src/shade/device/mock/SoftwareGPUTexture.d.ts +11 -1
  803. package/src/shade/device/mock/SoftwareGPUTexture.d.ts.map +1 -1
  804. package/src/shade/device/mock/SoftwareGPUTexture.js +45 -8
  805. package/src/shade/device/mock/SoftwareGPUTextureView.d.ts +20 -1
  806. package/src/shade/device/mock/SoftwareGPUTextureView.d.ts.map +1 -1
  807. package/src/shade/device/mock/SoftwareGPUTextureView.js +28 -1
  808. package/src/shade/device/mock/recorded_binding_kind_mismatches.d.ts +20 -0
  809. package/src/shade/device/mock/recorded_binding_kind_mismatches.d.ts.map +1 -0
  810. package/src/shade/device/mock/recorded_binding_kind_mismatches.js +84 -0
  811. package/src/shade/device/mock/texture_copy.d.ts.map +1 -1
  812. package/src/shade/device/mock/texture_copy.js +41 -3
  813. package/src/shade/device/mock/texture_view_resource.d.ts +26 -0
  814. package/src/shade/device/mock/texture_view_resource.d.ts.map +1 -0
  815. package/src/shade/device/mock/texture_view_resource.js +47 -0
  816. package/src/shade/device/timing/GPUTimer.d.ts.map +1 -1
  817. package/src/shade/device/timing/GPUTimer.js +35 -21
  818. package/src/shade/playground/caldera/caldera_format.d.ts +28 -1
  819. package/src/shade/playground/caldera/caldera_format.d.ts.map +1 -1
  820. package/src/shade/playground/caldera/caldera_format.js +37 -0
  821. package/src/shade/playground/caldera/main.js +12 -2
  822. package/src/shade/playground/frame_rate_meter.d.ts +92 -0
  823. package/src/shade/playground/frame_rate_meter.d.ts.map +1 -0
  824. package/src/shade/playground/frame_rate_meter.js +147 -0
  825. package/src/shade/playground/particle_ecs/main.js +53 -7
  826. package/src/shade/playground/particle_ecs/moonlit_environment.js +2 -2
  827. package/src/shade/playground/particle_stats_readback.d.ts +58 -0
  828. package/src/shade/playground/particle_stats_readback.d.ts.map +1 -0
  829. package/src/shade/playground/particle_stats_readback.js +83 -0
  830. package/src/shade/playground/particle_system/main.js +378 -325
  831. package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
  832. package/src/shade/playground/skinned_blas_refit/main.js +7 -2
  833. package/src/shade/playground/sponza_path_trace/main.d.ts.map +1 -1
  834. package/src/shade/playground/sponza_path_trace/main.js +398 -393
  835. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  836. package/src/shade/playground/volumetrics_froxel/main.js +941 -936
  837. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  838. package/src/shade/renderer/GraphicsContext.js +7 -2
  839. package/src/shade/renderer/Renderer.d.ts +47 -1
  840. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  841. package/src/shade/renderer/Renderer.js +224 -27
  842. package/src/shade/renderer/animation/ANIMATION_CLIP_STATE_STRUCT.d.ts +4 -7
  843. package/src/shade/renderer/animation/ANIMATION_CLIP_STATE_STRUCT.d.ts.map +1 -1
  844. package/src/shade/renderer/animation/ANIMATION_CLIP_STATE_STRUCT.js +19 -16
  845. package/src/shade/renderer/animation/ANIMATION_CURVE_HEADER_STRUCT.js +11 -11
  846. package/src/shade/renderer/animation/ANIMATION_DATABASE_SPEC.d.ts +14 -18
  847. package/src/shade/renderer/animation/ANIMATION_DATABASE_SPEC.d.ts.map +1 -1
  848. package/src/shade/renderer/animation/ANIMATION_DATABASE_SPEC.js +95 -24
  849. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_STRUCT.d.ts +5 -4
  850. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_STRUCT.d.ts.map +1 -1
  851. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_STRUCT.js +7 -4
  852. package/src/shade/renderer/animation/ANIMATION_SKIN_STRUCT.d.ts +6 -12
  853. package/src/shade/renderer/animation/ANIMATION_SKIN_STRUCT.d.ts.map +1 -1
  854. package/src/shade/renderer/animation/ANIMATION_SKIN_STRUCT.js +9 -16
  855. package/src/shade/renderer/animation/ANIMATION_TRACK_BINDING_STRUCT.d.ts +6 -5
  856. package/src/shade/renderer/animation/ANIMATION_TRACK_BINDING_STRUCT.d.ts.map +1 -1
  857. package/src/shade/renderer/animation/ANIMATION_TRACK_BINDING_STRUCT.js +12 -5
  858. package/src/shade/renderer/animation/GPUAnimationManager.d.ts +80 -95
  859. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  860. package/src/shade/renderer/animation/GPUAnimationManager.js +371 -606
  861. package/src/shade/renderer/animation/chunk_animation_curve_evaluate.d.ts +10 -14
  862. package/src/shade/renderer/animation/chunk_animation_curve_evaluate.d.ts.map +1 -1
  863. package/src/shade/renderer/animation/chunk_animation_curve_evaluate.js +40 -67
  864. package/src/shade/renderer/animation/chunk_pose_accumulator.d.ts.map +1 -1
  865. package/src/shade/renderer/animation/chunk_pose_accumulator.js +13 -4
  866. package/src/shade/renderer/animation/shader_animation_apply.d.ts +14 -8
  867. package/src/shade/renderer/animation/shader_animation_apply.d.ts.map +1 -1
  868. package/src/shade/renderer/animation/shader_animation_apply.js +129 -128
  869. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +2 -2
  870. package/src/shade/renderer/animation/skinning/SKINNING_BINDING_STRUCT.d.ts +3 -2
  871. package/src/shade/renderer/animation/skinning/SKINNING_BINDING_STRUCT.d.ts.map +1 -1
  872. package/src/shade/renderer/animation/skinning/SKINNING_BINDING_STRUCT.js +3 -2
  873. package/src/shade/renderer/animation/skinning/shader_skin_matrix_prep.d.ts +12 -7
  874. package/src/shade/renderer/animation/skinning/shader_skin_matrix_prep.d.ts.map +1 -1
  875. package/src/shade/renderer/animation/skinning/shader_skin_matrix_prep.js +107 -104
  876. package/src/shade/renderer/binding/BindingResource.d.ts +36 -1
  877. package/src/shade/renderer/binding/BindingResource.d.ts.map +1 -1
  878. package/src/shade/renderer/binding/BindingResource.js +36 -3
  879. package/src/shade/renderer/binding/BufferBinding.d.ts +8 -0
  880. package/src/shade/renderer/binding/BufferBinding.d.ts.map +1 -1
  881. package/src/shade/renderer/binding/BufferBinding.js +21 -0
  882. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts +19 -0
  883. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
  884. package/src/shade/renderer/buffer/table/GPUDatabase.js +114 -1
  885. package/src/shade/renderer/buffer/table/GPUDatabaseDefinition.d.ts +132 -1
  886. package/src/shade/renderer/buffer/table/GPUDatabaseDefinition.d.ts.map +1 -1
  887. package/src/shade/renderer/buffer/table/GPUDatabaseDefinition.js +384 -1
  888. package/src/shade/renderer/buffer/table/GPUDatabaseReadback.d.ts.map +1 -1
  889. package/src/shade/renderer/buffer/table/GPUDatabaseReadback.js +1332 -1323
  890. package/src/shade/renderer/buffer/table/GPUTableLink.d.ts +67 -0
  891. package/src/shade/renderer/buffer/table/GPUTableLink.d.ts.map +1 -0
  892. package/src/shade/renderer/buffer/table/GPUTableLink.js +76 -0
  893. package/src/shade/renderer/buffer/table/GPUTableRange.d.ts +72 -0
  894. package/src/shade/renderer/buffer/table/GPUTableRange.d.ts.map +1 -0
  895. package/src/shade/renderer/buffer/table/GPUTableRange.js +82 -0
  896. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts +106 -5
  897. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  898. package/src/shade/renderer/buffer/table/GPUTypedTable.js +444 -28
  899. package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.d.ts.map +1 -1
  900. package/src/shade/renderer/buffer/table/GPUTypedTableDescriptor.js +28 -5
  901. package/src/shade/renderer/buffer/table/GPU_DATABASE_NULL.d.ts +14 -0
  902. package/src/shade/renderer/buffer/table/GPU_DATABASE_NULL.d.ts.map +1 -0
  903. package/src/shade/renderer/buffer/table/GPU_DATABASE_NULL.js +13 -0
  904. package/src/shade/renderer/buffer/table/gpu_database_check_links.d.ts +80 -0
  905. package/src/shade/renderer/buffer/table/gpu_database_check_links.d.ts.map +1 -0
  906. package/src/shade/renderer/buffer/table/gpu_database_check_links.js +456 -0
  907. package/src/shade/renderer/buffer/table/gpu_table_field_word.d.ts +17 -0
  908. package/src/shade/renderer/buffer/table/gpu_table_field_word.d.ts.map +1 -0
  909. package/src/shade/renderer/buffer/table/gpu_table_field_word.js +111 -0
  910. package/src/shade/renderer/buffer/table/gpu_table_range_well_formed.d.ts +20 -0
  911. package/src/shade/renderer/buffer/table/gpu_table_range_well_formed.d.ts.map +1 -0
  912. package/src/shade/renderer/buffer/table/gpu_table_range_well_formed.js +27 -0
  913. package/src/shade/renderer/camera/CameraManager.d.ts +11 -0
  914. package/src/shade/renderer/camera/CameraManager.d.ts.map +1 -1
  915. package/src/shade/renderer/camera/CameraManager.js +18 -0
  916. package/src/shade/renderer/create_frame_loop.d.ts +16 -0
  917. package/src/shade/renderer/create_frame_loop.d.ts.map +1 -1
  918. package/src/shade/renderer/create_frame_loop.js +16 -0
  919. package/src/shade/renderer/deferred/chunk_shade_standard_material_direct.d.ts +23 -0
  920. package/src/shade/renderer/deferred/chunk_shade_standard_material_direct.d.ts.map +1 -1
  921. package/src/shade/renderer/deferred/chunk_shade_standard_material_direct.js +27 -32
  922. package/src/shade/renderer/deferred/chunk_shade_standard_material_direct_heavy.d.ts +9 -7
  923. package/src/shade/renderer/deferred/chunk_shade_standard_material_direct_heavy.d.ts.map +1 -1
  924. package/src/shade/renderer/deferred/chunk_shade_standard_material_direct_heavy.js +16 -108
  925. package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.d.ts +135 -0
  926. package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.d.ts.map +1 -0
  927. package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.js +296 -0
  928. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts +2 -2
  929. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts.map +1 -1
  930. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js +10 -131
  931. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts.map +1 -1
  932. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +185 -167
  933. package/src/shade/renderer/dynamic/GPUDynamicMeshResidencyTable.d.ts +49 -0
  934. package/src/shade/renderer/dynamic/GPUDynamicMeshResidencyTable.d.ts.map +1 -0
  935. package/src/shade/renderer/dynamic/GPUDynamicMeshResidencyTable.js +185 -0
  936. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts +24 -0
  937. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts.map +1 -0
  938. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.js +49 -0
  939. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.d.ts +18 -0
  940. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.d.ts.map +1 -0
  941. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.js +54 -0
  942. package/src/shade/renderer/dynamic/dynamic_mesh_live_bounds.d.ts +26 -0
  943. package/src/shade/renderer/dynamic/dynamic_mesh_live_bounds.d.ts.map +1 -0
  944. package/src/shade/renderer/dynamic/dynamic_mesh_live_bounds.js +96 -0
  945. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts +102 -0
  946. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts.map +1 -0
  947. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.js +364 -0
  948. package/src/shade/renderer/dynamic/shader_dynamic_mesh.d.ts.map +1 -1
  949. package/src/shade/renderer/dynamic/shader_dynamic_mesh.js +153 -177
  950. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts +20 -0
  951. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts.map +1 -0
  952. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.js +136 -0
  953. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_occupancy.d.ts +20 -0
  954. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_occupancy.d.ts.map +1 -0
  955. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_occupancy.js +106 -0
  956. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts +32 -0
  957. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts.map +1 -0
  958. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.js +163 -0
  959. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +13 -2
  960. package/src/shade/renderer/fow/GPUFogOfWarRenderer.d.ts.map +1 -1
  961. package/src/shade/renderer/fow/GPUFogOfWarRenderer.js +5 -14
  962. package/src/shade/renderer/fow/fow_obtain_mask.d.ts +17 -0
  963. package/src/shade/renderer/fow/fow_obtain_mask.d.ts.map +1 -0
  964. package/src/shade/renderer/fow/fow_obtain_mask.js +41 -0
  965. package/src/shade/renderer/frame_jitter_applies.d.ts +21 -0
  966. package/src/shade/renderer/frame_jitter_applies.d.ts.map +1 -0
  967. package/src/shade/renderer/frame_jitter_applies.js +23 -0
  968. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  969. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +128 -3
  970. package/src/shade/renderer/geometry/geometry_obtain_attribute.d.ts +42 -0
  971. package/src/shade/renderer/geometry/geometry_obtain_attribute.d.ts.map +1 -0
  972. package/src/shade/renderer/geometry/geometry_obtain_attribute.js +83 -0
  973. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +12 -0
  974. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  975. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +65 -0
  976. package/src/shade/renderer/geometry/meshlet/build/geometry_to_meshlets_greedy.d.ts.map +1 -1
  977. package/src/shade/renderer/geometry/meshlet/build/geometry_to_meshlets_greedy.js +4 -1
  978. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts.map +1 -1
  979. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js +4 -1
  980. package/src/shade/renderer/geometry/normals/encode_normals_to_octahedral_u32.d.ts.map +1 -1
  981. package/src/shade/renderer/geometry/normals/encode_normals_to_octahedral_u32.js +4 -1
  982. package/src/shade/renderer/geometry/normals/geometry_build_tangents.d.ts.map +1 -1
  983. package/src/shade/renderer/geometry/normals/geometry_build_tangents.js +39 -1
  984. package/src/shade/renderer/geometry/util/geometry_merge.d.ts.map +1 -1
  985. package/src/shade/renderer/geometry/util/geometry_merge.js +18 -2
  986. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +77 -39
  987. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_collect_geometries.d.ts.map +1 -1
  988. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_collect_geometries.js +31 -2
  989. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts +18 -0
  990. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts.map +1 -1
  991. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.js +19 -0
  992. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts +8 -0
  993. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts.map +1 -1
  994. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.js +9 -0
  995. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts.map +1 -1
  996. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +10 -1
  997. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts.map +1 -1
  998. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.js +4 -0
  999. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -1
  1000. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.js +12 -0
  1001. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  1002. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +10 -1
  1003. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +21 -0
  1004. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  1005. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +33 -1
  1006. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -1
  1007. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +63 -2
  1008. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts +17 -0
  1009. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts.map +1 -1
  1010. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.js +39 -0
  1011. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -1
  1012. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +7 -0
  1013. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_decode.d.ts.map +1 -1
  1014. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_decode.js +23 -2
  1015. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_decode_from_storage.d.ts.map +1 -1
  1016. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_decode_from_storage.js +22 -1
  1017. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_encode.d.ts.map +1 -1
  1018. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_encode.js +23 -2
  1019. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_encode_to_storage.d.ts.map +1 -1
  1020. package/src/shade/renderer/global_illumination/brick4/gpu/encoding/v1/chunk_brick4_sh3_color_encode_to_storage.js +22 -1
  1021. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.d.ts +2 -1
  1022. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.d.ts.map +1 -1
  1023. package/src/shade/renderer/global_illumination/lpv/lpv_visualise_probes.js +12 -53
  1024. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.d.ts.map +1 -1
  1025. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.js +2 -1
  1026. package/src/shade/renderer/global_illumination/probe/octahedral/sample/chunk_lpv_atlas_patch_texel_coord.d.ts +17 -0
  1027. package/src/shade/renderer/global_illumination/probe/octahedral/sample/chunk_lpv_atlas_patch_texel_coord.d.ts.map +1 -0
  1028. package/src/shade/renderer/global_illumination/probe/octahedral/sample/chunk_lpv_atlas_patch_texel_coord.js +38 -0
  1029. package/src/shade/renderer/global_illumination/probe/octahedral/sample/chunk_lpv_atlas_read_patch_texel.d.ts.map +1 -1
  1030. package/src/shade/renderer/global_illumination/probe/octahedral/sample/chunk_lpv_atlas_read_patch_texel.js +16 -10
  1031. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts +1 -1
  1032. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts.map +1 -1
  1033. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.js +25 -14
  1034. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +94 -0
  1035. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts +10 -7
  1036. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts.map +1 -1
  1037. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.js +10 -7
  1038. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  1039. package/src/shade/renderer/light/GPULightCollection.js +22 -1
  1040. package/src/shade/renderer/light/LIGHT_DATABASE_SPEC.d.ts.map +1 -1
  1041. package/src/shade/renderer/light/LIGHT_DATABASE_SPEC.js +26 -0
  1042. package/src/shade/renderer/light/cluster/chunk_cluster_keep_smallest.d.ts +21 -0
  1043. package/src/shade/renderer/light/cluster/chunk_cluster_keep_smallest.d.ts.map +1 -0
  1044. package/src/shade/renderer/light/cluster/chunk_cluster_keep_smallest.js +47 -0
  1045. package/src/shade/renderer/light/cluster/shader_cluster_assign_lights.d.ts.map +1 -1
  1046. package/src/shade/renderer/light/cluster/shader_cluster_assign_lights.js +12 -8
  1047. package/src/shade/renderer/light/environment/equirectangular_zenith_first_direction_to_uv.d.ts +36 -0
  1048. package/src/shade/renderer/light/environment/equirectangular_zenith_first_direction_to_uv.d.ts.map +1 -0
  1049. package/src/shade/renderer/light/environment/equirectangular_zenith_first_direction_to_uv.js +41 -0
  1050. package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts.map +1 -1
  1051. package/src/shade/renderer/light/environment/execute_filter_environment_map.js +17 -1
  1052. package/src/shade/renderer/light/environment/octahedral_uv_to_direction.d.ts +5 -13
  1053. package/src/shade/renderer/light/environment/octahedral_uv_to_direction.d.ts.map +1 -1
  1054. package/src/shade/renderer/light/environment/octahedral_uv_to_direction.js +10 -30
  1055. package/src/shade/renderer/light/environment/octahedral_z_pole_uv_to_direction.d.ts +31 -0
  1056. package/src/shade/renderer/light/environment/octahedral_z_pole_uv_to_direction.d.ts.map +1 -0
  1057. package/src/shade/renderer/light/environment/octahedral_z_pole_uv_to_direction.js +45 -0
  1058. package/src/shade/renderer/light/environment/resample_cube_to_octahedral.js +2 -2
  1059. package/src/shade/renderer/light/environment/resample_equirectangular_to_octahedral.d.ts.map +1 -1
  1060. package/src/shade/renderer/light/environment/resample_equirectangular_to_octahedral.js +11 -8
  1061. package/src/shade/renderer/light/model/SpotLight.d.ts +10 -1
  1062. package/src/shade/renderer/light/model/SpotLight.d.ts.map +1 -1
  1063. package/src/shade/renderer/light/model/SpotLight.js +11 -3
  1064. package/src/shade/renderer/light/model/io/light_record_directional.d.ts.map +1 -1
  1065. package/src/shade/renderer/light/model/io/light_record_directional.js +3 -1
  1066. package/src/shade/renderer/light/model/io/light_record_point.d.ts.map +1 -1
  1067. package/src/shade/renderer/light/model/io/light_record_point.js +3 -1
  1068. package/src/shade/renderer/light/model/io/light_record_spot.d.ts.map +1 -1
  1069. package/src/shade/renderer/light/model/io/light_record_spot.js +3 -1
  1070. package/src/shade/renderer/loader/gltf/gltf_base_url.d.ts +20 -0
  1071. package/src/shade/renderer/loader/gltf/gltf_base_url.d.ts.map +1 -0
  1072. package/src/shade/renderer/loader/gltf/gltf_base_url.js +27 -0
  1073. package/src/shade/renderer/loader/gltf/gltf_create_material.d.ts.map +1 -1
  1074. package/src/shade/renderer/loader/gltf/gltf_create_material.js +50 -10
  1075. package/src/shade/renderer/loader/gltf/load_gltf.d.ts +4 -0
  1076. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  1077. package/src/shade/renderer/loader/gltf/load_gltf.js +28 -56
  1078. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  1079. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts.map +1 -1
  1080. package/src/shade/renderer/loader/gltf/tiny-gltf.js +843 -843
  1081. package/src/shade/renderer/material/GPUMaterialContext.d.ts +63 -26
  1082. package/src/shade/renderer/material/GPUMaterialContext.d.ts.map +1 -1
  1083. package/src/shade/renderer/material/GPUMaterialContext.js +255 -390
  1084. package/src/shade/renderer/material/GPUMaterialManager.d.ts +51 -1
  1085. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  1086. package/src/shade/renderer/material/GPUMaterialManager.js +345 -91
  1087. package/src/shade/renderer/material/ShadeMaterial.d.ts +21 -0
  1088. package/src/shade/renderer/material/ShadeMaterial.d.ts.map +1 -1
  1089. package/src/shade/renderer/material/ShadeMaterial.js +31 -0
  1090. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  1091. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +25 -6
  1092. package/src/shade/renderer/material/standard/SHADING_MATERIAL_RESOURCE_GROUP.d.ts +10 -0
  1093. package/src/shade/renderer/material/standard/SHADING_MATERIAL_RESOURCE_GROUP.d.ts.map +1 -1
  1094. package/src/shade/renderer/material/standard/SHADING_MATERIAL_RESOURCE_GROUP.js +14 -2
  1095. package/src/shade/renderer/material/standard/STANDARD_MATERIAL_TEMPLATE.d.ts +12 -0
  1096. package/src/shade/renderer/material/standard/STANDARD_MATERIAL_TEMPLATE.d.ts.map +1 -0
  1097. package/src/shade/renderer/material/standard/STANDARD_MATERIAL_TEMPLATE.js +166 -0
  1098. package/src/shade/renderer/material/standard/VT_MATERIAL_TEMPLATE.d.ts +10 -0
  1099. package/src/shade/renderer/material/standard/VT_MATERIAL_TEMPLATE.d.ts.map +1 -0
  1100. package/src/shade/renderer/material/standard/VT_MATERIAL_TEMPLATE.js +141 -0
  1101. package/src/shade/renderer/material/standard/chunk_standard_material_surface.d.ts +29 -0
  1102. package/src/shade/renderer/material/standard/chunk_standard_material_surface.d.ts.map +1 -0
  1103. package/src/shade/renderer/material/standard/chunk_standard_material_surface.js +92 -0
  1104. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts +74 -28
  1105. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  1106. package/src/shade/renderer/material/standard/fragment_gbuffer.js +165 -67
  1107. package/src/shade/renderer/material/standard/fragment_gbuffer_vt.d.ts +27 -18
  1108. package/src/shade/renderer/material/standard/fragment_gbuffer_vt.d.ts.map +1 -1
  1109. package/src/shade/renderer/material/standard/fragment_gbuffer_vt.js +70 -91
  1110. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts +9 -6
  1111. package/src/shade/renderer/material/standard/material_gbuffer_vertex.d.ts.map +1 -1
  1112. package/src/shade/renderer/material/standard/material_gbuffer_vertex.js +13 -14
  1113. package/src/shade/renderer/material/template/GPUMaterialServices.d.ts +73 -0
  1114. package/src/shade/renderer/material/template/GPUMaterialServices.d.ts.map +1 -0
  1115. package/src/shade/renderer/material/template/GPUMaterialServices.js +79 -0
  1116. package/src/shade/renderer/material/template/ShadeMaterialTemplate.d.ts +170 -0
  1117. package/src/shade/renderer/material/template/ShadeMaterialTemplate.d.ts.map +1 -0
  1118. package/src/shade/renderer/material/template/ShadeMaterialTemplate.js +234 -0
  1119. package/src/shade/renderer/material/template/chunk_material_surface.d.ts +47 -0
  1120. package/src/shade/renderer/material/template/chunk_material_surface.d.ts.map +1 -0
  1121. package/src/shade/renderer/material/template/chunk_material_surface.js +116 -0
  1122. package/src/shade/renderer/material/template/flat_material_template_spec_support.d.ts +22 -0
  1123. package/src/shade/renderer/material/template/flat_material_template_spec_support.d.ts.map +1 -0
  1124. package/src/shade/renderer/material/template/flat_material_template_spec_support.js +78 -0
  1125. package/src/shade/renderer/material/template/material_metadata_neutral.d.ts +15 -0
  1126. package/src/shade/renderer/material/template/material_metadata_neutral.d.ts.map +1 -0
  1127. package/src/shade/renderer/material/template/material_metadata_neutral.js +46 -0
  1128. package/src/shade/renderer/material/template/material_template_of.d.ts +10 -0
  1129. package/src/shade/renderer/material/template/material_template_of.d.ts.map +1 -0
  1130. package/src/shade/renderer/material/template/material_template_of.js +20 -0
  1131. package/src/shade/renderer/object_property/chunk_write_object_property.d.ts +5 -0
  1132. package/src/shade/renderer/object_property/chunk_write_object_property.d.ts.map +1 -1
  1133. package/src/shade/renderer/object_property/chunk_write_object_property.js +7 -1
  1134. package/src/shade/renderer/object_property/chunk_write_object_property_node3d.d.ts +7 -1
  1135. package/src/shade/renderer/object_property/chunk_write_object_property_node3d.d.ts.map +1 -1
  1136. package/src/shade/renderer/object_property/chunk_write_object_property_node3d.js +9 -2
  1137. package/src/shade/renderer/particles/DESIGN.md +7 -0
  1138. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.d.ts.map +1 -1
  1139. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.js +81 -68
  1140. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +22 -3
  1141. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  1142. package/src/shade/renderer/particles/isa/ParticleVMISA.js +30 -3
  1143. package/src/shade/renderer/particles/isa/particle_assembly.js +41 -1
  1144. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts +7 -1
  1145. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts.map +1 -1
  1146. package/src/shade/renderer/particles/optimizer/particle_program_analysis.js +23 -17
  1147. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.js +1 -1
  1148. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +10 -9
  1149. package/src/shade/renderer/particles/optimizer/pass_inline_constants.d.ts.map +1 -1
  1150. package/src/shade/renderer/particles/optimizer/pass_inline_constants.js +4 -3
  1151. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.d.ts.map +1 -1
  1152. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.js +1 -1
  1153. package/src/shade/renderer/particles/optimizer/pass_pack_registers.d.ts.map +1 -1
  1154. package/src/shade/renderer/particles/optimizer/pass_pack_registers.js +12 -4
  1155. package/src/shade/renderer/particles/optimizer/pass_peephole.d.ts.map +1 -1
  1156. package/src/shade/renderer/particles/optimizer/pass_peephole.js +11 -10
  1157. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.d.ts.map +1 -1
  1158. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.js +12 -10
  1159. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.d.ts.map +1 -1
  1160. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.js +12 -4
  1161. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  1162. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +7 -0
  1163. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.d.ts +4 -0
  1164. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.d.ts.map +1 -1
  1165. package/src/shade/renderer/particles/shaders/chunk_particle_avboit_contribution.js +47 -88
  1166. package/src/shade/renderer/particles/shaders/shader_particle_avboit_occupancy.d.ts.map +1 -1
  1167. package/src/shade/renderer/particles/shaders/shader_particle_avboit_occupancy.js +22 -68
  1168. package/src/shade/renderer/particles/shaders/shader_particle_avboit_splat.d.ts.map +1 -1
  1169. package/src/shade/renderer/particles/shaders/shader_particle_avboit_splat.js +5 -3
  1170. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  1171. package/src/shade/renderer/particles/vm/ParticleVMReference.js +17 -1
  1172. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
  1173. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +12 -2
  1174. package/src/shade/renderer/postprocess/TemporalJitterSequence.d.ts +44 -0
  1175. package/src/shade/renderer/postprocess/TemporalJitterSequence.d.ts.map +1 -0
  1176. package/src/shade/renderer/postprocess/TemporalJitterSequence.js +104 -0
  1177. package/src/shade/renderer/postprocess/chunk_find_nearest_depth.d.ts +25 -0
  1178. package/src/shade/renderer/postprocess/chunk_find_nearest_depth.d.ts.map +1 -0
  1179. package/src/shade/renderer/postprocess/chunk_find_nearest_depth.js +74 -0
  1180. package/src/shade/renderer/postprocess/denoise/shader_temporal_reprojection_luma.d.ts.map +1 -1
  1181. package/src/shade/renderer/postprocess/denoise/shader_temporal_reprojection_luma.js +19 -4
  1182. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_recombine.d.ts.map +1 -1
  1183. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_recombine.js +12 -1
  1184. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.d.ts.map +1 -1
  1185. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.js +8 -0
  1186. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.d.ts.map +1 -1
  1187. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.js +18 -3
  1188. package/src/shade/renderer/postprocess/eye/shader_autoexposure_read_histogram.d.ts.map +1 -1
  1189. package/src/shade/renderer/postprocess/eye/shader_autoexposure_read_histogram.js +17 -4
  1190. package/src/shade/renderer/postprocess/gtao/GTAO_CONSTANTS.d.ts +29 -0
  1191. package/src/shade/renderer/postprocess/gtao/GTAO_CONSTANTS.d.ts.map +1 -0
  1192. package/src/shade/renderer/postprocess/gtao/GTAO_CONSTANTS.js +69 -0
  1193. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts +4 -16
  1194. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  1195. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +10 -52
  1196. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts +0 -16
  1197. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  1198. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +23 -55
  1199. package/src/shade/renderer/postprocess/motion_blur/MotionBlur.d.ts +4 -1
  1200. package/src/shade/renderer/postprocess/motion_blur/MotionBlur.d.ts.map +1 -1
  1201. package/src/shade/renderer/postprocess/motion_blur/MotionBlur.js +4 -1
  1202. package/src/shade/renderer/postprocess/motion_blur/chunk_motion_blur_resolution.d.ts +18 -0
  1203. package/src/shade/renderer/postprocess/motion_blur/chunk_motion_blur_resolution.d.ts.map +1 -0
  1204. package/src/shade/renderer/postprocess/motion_blur/chunk_motion_blur_resolution.js +43 -0
  1205. package/src/shade/renderer/postprocess/motion_blur/graph_postprocess_motion_blur.d.ts +5 -1
  1206. package/src/shade/renderer/postprocess/motion_blur/graph_postprocess_motion_blur.d.ts.map +1 -1
  1207. package/src/shade/renderer/postprocess/motion_blur/graph_postprocess_motion_blur.js +12 -2
  1208. package/src/shade/renderer/postprocess/motion_blur/shader_motion_blur_reconstruct.d.ts.map +1 -1
  1209. package/src/shade/renderer/postprocess/motion_blur/shader_motion_blur_reconstruct.js +33 -6
  1210. package/src/shade/renderer/postprocess/nss/NSS.d.ts +16 -8
  1211. package/src/shade/renderer/postprocess/nss/NSS.d.ts.map +1 -1
  1212. package/src/shade/renderer/postprocess/nss/NSS.js +37 -35
  1213. package/src/shade/renderer/postprocess/nss/chunk_nss_find_nearest_depth.d.ts +2 -2
  1214. package/src/shade/renderer/postprocess/nss/chunk_nss_find_nearest_depth.d.ts.map +1 -1
  1215. package/src/shade/renderer/postprocess/nss/chunk_nss_find_nearest_depth.js +28 -61
  1216. package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.d.ts.map +1 -1
  1217. package/src/shade/renderer/postprocess/nss/shader_nss_postprocess.js +296 -291
  1218. package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.d.ts.map +1 -1
  1219. package/src/shade/renderer/postprocess/nss/shader_nss_preprocess.js +13 -0
  1220. package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
  1221. package/src/shade/renderer/postprocess/ssr/SSR.js +9 -7
  1222. package/src/shade/renderer/postprocess/ssr/depth_max_shader.d.ts +14 -0
  1223. package/src/shade/renderer/postprocess/ssr/depth_max_shader.d.ts.map +1 -1
  1224. package/src/shade/renderer/postprocess/ssr/depth_max_shader.js +53 -25
  1225. package/src/shade/renderer/postprocess/ssr/graph_build_depth_max_pyramid.d.ts +11 -1
  1226. package/src/shade/renderer/postprocess/ssr/graph_build_depth_max_pyramid.d.ts.map +1 -1
  1227. package/src/shade/renderer/postprocess/ssr/graph_build_depth_max_pyramid.js +11 -1
  1228. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +46 -0
  1229. package/src/shade/renderer/postprocess/ssr/prefilter/shader_ssr_prefilter.d.ts.map +1 -1
  1230. package/src/shade/renderer/postprocess/ssr/prefilter/shader_ssr_prefilter.js +7 -0
  1231. package/src/shade/renderer/postprocess/ssr/shader/chunk_ssr_depth_thickness.d.ts +32 -0
  1232. package/src/shade/renderer/postprocess/ssr/shader/chunk_ssr_depth_thickness.d.ts.map +1 -0
  1233. package/src/shade/renderer/postprocess/ssr/shader/chunk_ssr_depth_thickness.js +41 -0
  1234. package/src/shade/renderer/postprocess/ssr/ssr_trace_shader.d.ts.map +1 -1
  1235. package/src/shade/renderer/postprocess/ssr/ssr_trace_shader.js +11 -6
  1236. package/src/shade/renderer/postprocess/taa/TAA.d.ts +13 -4
  1237. package/src/shade/renderer/postprocess/taa/TAA.d.ts.map +1 -1
  1238. package/src/shade/renderer/postprocess/taa/TAA.js +29 -48
  1239. package/src/shade/renderer/postprocess/taa/chunk_taa_tonemap.d.ts +11 -0
  1240. package/src/shade/renderer/postprocess/taa/chunk_taa_tonemap.d.ts.map +1 -1
  1241. package/src/shade/renderer/postprocess/taa/chunk_taa_tonemap.js +25 -4
  1242. package/src/shade/renderer/postprocess/taa/shader_depth_occlusion_clip.d.ts +8 -0
  1243. package/src/shade/renderer/postprocess/taa/shader_depth_occlusion_clip.d.ts.map +1 -1
  1244. package/src/shade/renderer/postprocess/taa/shader_depth_occlusion_clip.js +14 -42
  1245. package/src/shade/renderer/rasterize/RasterizationJob.d.ts +4 -2
  1246. package/src/shade/renderer/rasterize/RasterizationJob.d.ts.map +1 -1
  1247. package/src/shade/renderer/rasterize/RasterizationJob.js +3 -1
  1248. package/src/shade/renderer/rasterize/TemplatePassDescriptor.d.ts +57 -0
  1249. package/src/shade/renderer/rasterize/TemplatePassDescriptor.d.ts.map +1 -0
  1250. package/src/shade/renderer/rasterize/TemplatePassDescriptor.js +133 -0
  1251. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts.map +1 -1
  1252. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.js +17 -0
  1253. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  1254. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +30 -4
  1255. package/src/shade/renderer/rasterize/bucket/shader_count_meshlets_by_material.d.ts.map +1 -1
  1256. package/src/shade/renderer/rasterize/bucket/shader_count_meshlets_by_material.js +74 -68
  1257. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
  1258. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +11 -1
  1259. package/src/shade/renderer/rasterize/bucket/shader_sort_meshlets_by_material.d.ts.map +1 -1
  1260. package/src/shade/renderer/rasterize/bucket/shader_sort_meshlets_by_material.js +70 -64
  1261. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.d.ts.map +1 -1
  1262. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.js +3 -4
  1263. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  1264. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +8 -2
  1265. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.d.ts.map +1 -1
  1266. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.js +3 -4
  1267. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  1268. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +7 -5
  1269. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
  1270. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +3 -4
  1271. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.d.ts.map +1 -1
  1272. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.js +3 -4
  1273. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.d.ts.map +1 -1
  1274. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.js +5 -2
  1275. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.d.ts.map +1 -1
  1276. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.js +3 -4
  1277. package/src/shade/renderer/rasterize/forward/SHADING_SCENE_RESOURCE_GROUP.d.ts +20 -0
  1278. package/src/shade/renderer/rasterize/forward/SHADING_SCENE_RESOURCE_GROUP.d.ts.map +1 -1
  1279. package/src/shade/renderer/rasterize/forward/SHADING_SCENE_RESOURCE_GROUP.js +25 -0
  1280. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
  1281. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.js +2 -1
  1282. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
  1283. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.js +2 -1
  1284. package/src/shade/renderer/rasterize/forward/forward_template_pipeline.d.ts +40 -0
  1285. package/src/shade/renderer/rasterize/forward/forward_template_pipeline.d.ts.map +1 -0
  1286. package/src/shade/renderer/rasterize/forward/forward_template_pipeline.js +86 -0
  1287. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts +11 -0
  1288. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts.map +1 -1
  1289. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.js +13 -2
  1290. package/src/shade/renderer/rasterize/native/alpha_tested_pass_resources.d.ts +59 -1
  1291. package/src/shade/renderer/rasterize/native/alpha_tested_pass_resources.d.ts.map +1 -1
  1292. package/src/shade/renderer/rasterize/native/alpha_tested_pass_resources.js +109 -7
  1293. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +16 -1
  1294. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  1295. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +13 -1
  1296. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +247 -0
  1297. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +20 -2
  1298. package/src/shade/renderer/rasterize/native/avboit/avboit_draw_fragment_body.js +1 -1
  1299. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_coverage.d.ts +17 -0
  1300. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_coverage.d.ts.map +1 -0
  1301. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_coverage.js +25 -0
  1302. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_fold_pairs.d.ts +4 -5
  1303. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_fold_pairs.d.ts.map +1 -1
  1304. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_fold_pairs.js +7 -6
  1305. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_mark.d.ts +17 -0
  1306. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_mark.d.ts.map +1 -0
  1307. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_occupancy_mark.js +51 -0
  1308. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_premultiplied_contribution.d.ts +23 -0
  1309. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_premultiplied_contribution.d.ts.map +1 -0
  1310. package/src/shade/renderer/rasterize/native/avboit/chunk_avboit_premultiplied_contribution.js +77 -0
  1311. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  1312. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +16 -20
  1313. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_brick4.d.ts +8 -2
  1314. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_brick4.d.ts.map +1 -1
  1315. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_brick4.js +57 -43
  1316. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_ibl.d.ts +8 -2
  1317. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_ibl.d.ts.map +1 -1
  1318. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw_ibl.js +57 -43
  1319. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_occupancy.d.ts.map +1 -1
  1320. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_occupancy.js +8 -2
  1321. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.d.ts +7 -2
  1322. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.d.ts.map +1 -1
  1323. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_splat.js +67 -58
  1324. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.d.ts +8 -2
  1325. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  1326. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js +186 -156
  1327. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.d.ts +2 -2
  1328. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.d.ts.map +1 -1
  1329. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.js +36 -28
  1330. package/src/shade/renderer/rasterize/native/material_draw_indirect.d.ts +14 -12
  1331. package/src/shade/renderer/rasterize/native/material_draw_indirect.d.ts.map +1 -1
  1332. package/src/shade/renderer/rasterize/native/material_draw_indirect.js +29 -23
  1333. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  1334. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +16 -22
  1335. package/src/shade/renderer/rasterize/native/oit/oit_resolve_fragment_body.d.ts +33 -0
  1336. package/src/shade/renderer/rasterize/native/oit/oit_resolve_fragment_body.d.ts.map +1 -0
  1337. package/src/shade/renderer/rasterize/native/oit/oit_resolve_fragment_body.js +123 -0
  1338. package/src/shade/renderer/rasterize/native/oit/shader_oit_generate_moments.d.ts +7 -2
  1339. package/src/shade/renderer/rasterize/native/oit/shader_oit_generate_moments.d.ts.map +1 -1
  1340. package/src/shade/renderer/rasterize/native/oit/shader_oit_generate_moments.js +227 -218
  1341. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.d.ts +8 -2
  1342. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.d.ts.map +1 -1
  1343. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.js +112 -180
  1344. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments_ibl.d.ts +8 -2
  1345. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments_ibl.d.ts.map +1 -1
  1346. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments_ibl.js +110 -178
  1347. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.d.ts +7 -2
  1348. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  1349. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.js +242 -204
  1350. package/src/shade/renderer/restir/reservoir/shader.d.ts.map +1 -1
  1351. package/src/shade/renderer/restir/reservoir/shader.js +27 -1
  1352. package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -1
  1353. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  1354. package/src/shade/renderer/scene/GPUSceneContext.js +11 -9
  1355. package/src/shade/renderer/scene/SCENE_DATABASE_SPEC.js +22 -0
  1356. package/src/shade/renderer/scene/SceneManager.d.ts +10 -0
  1357. package/src/shade/renderer/scene/SceneManager.d.ts.map +1 -1
  1358. package/src/shade/renderer/scene/SceneManager.js +17 -0
  1359. package/src/shade/renderer/scene/rows/GPUEntityBoundsReadback.d.ts.map +1 -1
  1360. package/src/shade/renderer/scene/rows/GPUEntityBoundsReadback.js +164 -144
  1361. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts +10 -0
  1362. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  1363. package/src/shade/renderer/scene/rows/GPUSceneRows.js +14 -0
  1364. package/src/shade/renderer/scene/serialization/serialize_scene.d.ts.map +1 -1
  1365. package/src/shade/renderer/scene/serialization/serialize_scene.js +2 -6
  1366. package/src/shade/renderer/shader/ComputeShader.d.ts +5 -0
  1367. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  1368. package/src/shade/renderer/shader/ComputeShader.js +358 -353
  1369. package/src/shade/renderer/shader/chunk/atmosphere/chunk_integrate_scattering.d.ts.map +1 -1
  1370. package/src/shade/renderer/shader/chunk/atmosphere/chunk_integrate_scattering.js +15 -1
  1371. package/src/shade/renderer/shader/chunk/binary/chunk_encode_f16.d.ts +11 -2
  1372. package/src/shade/renderer/shader/chunk/binary/chunk_encode_f16.d.ts.map +1 -1
  1373. package/src/shade/renderer/shader/chunk/binary/chunk_encode_f16.js +33 -13
  1374. package/src/shade/renderer/shader/chunk/brdf/chunk_D_GGX.d.ts.map +1 -1
  1375. package/src/shade/renderer/shader/chunk/brdf/chunk_D_GGX.js +31 -3
  1376. package/src/shade/renderer/shader/chunk/bvh/morton/chunk_decode32_morton2.d.ts +10 -1
  1377. package/src/shade/renderer/shader/chunk/bvh/morton/chunk_decode32_morton2.d.ts.map +1 -1
  1378. package/src/shade/renderer/shader/chunk/bvh/morton/chunk_decode32_morton2.js +23 -8
  1379. package/src/shade/renderer/shader/chunk/camera/chunk_pixel_radius_to_world.d.ts +8 -1
  1380. package/src/shade/renderer/shader/chunk/camera/chunk_pixel_radius_to_world.d.ts.map +1 -1
  1381. package/src/shade/renderer/shader/chunk/camera/chunk_pixel_radius_to_world.js +12 -5
  1382. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_distance_to_ray.d.ts +15 -1
  1383. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_distance_to_ray.d.ts.map +1 -1
  1384. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_distance_to_ray.js +24 -28
  1385. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_distance_to_ray_v2.d.ts +5 -0
  1386. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_distance_to_ray_v2.d.ts.map +1 -1
  1387. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_distance_to_ray_v2.js +18 -13
  1388. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_intersects_ray.d.ts.map +1 -1
  1389. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_intersects_ray.js +1 -47
  1390. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_raycast.d.ts +10 -0
  1391. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_raycast.d.ts.map +1 -1
  1392. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_raycast.js +23 -8
  1393. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_raycast_positive.d.ts +7 -0
  1394. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_raycast_positive.d.ts.map +1 -1
  1395. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_raycast_positive.js +12 -25
  1396. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_slab_span.d.ts +32 -0
  1397. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_slab_span.d.ts.map +1 -0
  1398. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_slab_span.js +52 -0
  1399. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_view_depth_range.d.ts +20 -0
  1400. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_view_depth_range.d.ts.map +1 -0
  1401. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_view_depth_range.js +42 -0
  1402. package/src/shade/renderer/shader/chunk/geometry/octahedron/chunk_octahedral_uv_wrap.d.ts +10 -1
  1403. package/src/shade/renderer/shader/chunk/geometry/octahedron/chunk_octahedral_uv_wrap.d.ts.map +1 -1
  1404. package/src/shade/renderer/shader/chunk/geometry/octahedron/chunk_octahedral_uv_wrap.js +18 -3
  1405. package/src/shade/renderer/shader/chunk/geometry/quaternion/chunk_quat_encode_u32.d.ts.map +1 -1
  1406. package/src/shade/renderer/shader/chunk/geometry/quaternion/chunk_quat_encode_u32.js +8 -4
  1407. package/src/shade/renderer/shader/chunk/grid/chunk_folded_workgroup_index.d.ts +24 -0
  1408. package/src/shade/renderer/shader/chunk/grid/chunk_folded_workgroup_index.d.ts.map +1 -0
  1409. package/src/shade/renderer/shader/chunk/grid/chunk_folded_workgroup_index.js +36 -0
  1410. package/src/shade/renderer/shader/chunk/grid/chunk_grid3d_to_index.d.ts +6 -0
  1411. package/src/shade/renderer/shader/chunk/grid/chunk_grid3d_to_index.d.ts.map +1 -1
  1412. package/src/shade/renderer/shader/chunk/grid/chunk_grid3d_to_index.js +6 -0
  1413. package/src/shade/renderer/shader/chunk/light/INCIDENT_LIGHT_STRUCT.d.ts.map +1 -1
  1414. package/src/shade/renderer/shader/chunk/light/INCIDENT_LIGHT_STRUCT.js +6 -1
  1415. package/src/shade/renderer/shader/chunk/light/io/chunk_get_spot_light_info.d.ts.map +1 -1
  1416. package/src/shade/renderer/shader/chunk/light/io/chunk_get_spot_light_info.js +6 -1
  1417. package/src/shade/renderer/shader/chunk/material/chunk_albedo_unpremultiply.d.ts +24 -0
  1418. package/src/shade/renderer/shader/chunk/material/chunk_albedo_unpremultiply.d.ts.map +1 -0
  1419. package/src/shade/renderer/shader/chunk/material/chunk_albedo_unpremultiply.js +31 -0
  1420. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg2d.js +2 -2
  1421. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg3d16.js +1 -1
  1422. package/src/shade/renderer/shader/chunk/random/pcg/chunk_pcg4d.js +1 -1
  1423. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_validate_hit.d.ts.map +1 -1
  1424. package/src/shade/renderer/shader/chunk/screen_space/chunk_ffx_sssr_validate_hit.js +8 -4
  1425. package/src/shade/renderer/shader/chunk/sdf/chunk_mesh_sdf_soft_shadow.d.ts.map +1 -1
  1426. package/src/shade/renderer/shader/chunk/sdf/chunk_mesh_sdf_soft_shadow.js +17 -3
  1427. package/src/shade/renderer/shader/graph/compute/graph_compute_indirect_pass.d.ts +4 -0
  1428. package/src/shade/renderer/shader/graph/compute/graph_compute_indirect_pass.d.ts.map +1 -1
  1429. package/src/shade/renderer/shader/graph/compute/graph_compute_indirect_pass.js +4 -0
  1430. package/src/shade/renderer/shader/pass/RenderPassDescriptor.d.ts +8 -0
  1431. package/src/shade/renderer/shader/pass/RenderPassDescriptor.d.ts.map +1 -1
  1432. package/src/shade/renderer/shader/pass/RenderPassDescriptor.js +203 -192
  1433. package/src/shade/renderer/shader/resource/ShaderResourceGroupDescriptor.d.ts.map +1 -1
  1434. package/src/shade/renderer/shader/resource/ShaderResourceGroupDescriptor.js +12 -0
  1435. package/src/shade/renderer/shader/type/WebGPUArray.d.ts.map +1 -1
  1436. package/src/shade/renderer/shader/type/WebGPUArray.js +25 -1
  1437. package/src/shade/renderer/shader/type/queue_typed_buffer_update.d.ts.map +1 -1
  1438. package/src/shade/renderer/shader/type/queue_typed_buffer_update.js +15 -0
  1439. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  1440. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +10 -9
  1441. package/src/shade/renderer/shadow/map/ShadowmapDrawSelector.d.ts +12 -2
  1442. package/src/shade/renderer/shadow/map/ShadowmapDrawSelector.d.ts.map +1 -1
  1443. package/src/shade/renderer/shadow/map/ShadowmapDrawSelector.js +210 -196
  1444. package/src/shade/renderer/shadow/map/csm/shader/shader_csm_caster_range.d.ts +10 -3
  1445. package/src/shade/renderer/shadow/map/csm/shader/shader_csm_caster_range.d.ts.map +1 -1
  1446. package/src/shade/renderer/shadow/map/csm/shader/shader_csm_caster_range.js +219 -194
  1447. package/src/shade/renderer/shadow/map/shadowmap_budget_carry_over.d.ts +32 -0
  1448. package/src/shade/renderer/shadow/map/shadowmap_budget_carry_over.d.ts.map +1 -0
  1449. package/src/shade/renderer/shadow/map/shadowmap_budget_carry_over.js +47 -0
  1450. package/src/shade/renderer/shadow/vsm/VirtualShadowMap.d.ts.map +1 -1
  1451. package/src/shade/renderer/shadow/vsm/VirtualShadowMap.js +11 -1
  1452. package/src/shade/renderer/texture/ShadeTexture.d.ts +16 -0
  1453. package/src/shade/renderer/texture/ShadeTexture.d.ts.map +1 -1
  1454. package/src/shade/renderer/texture/ShadeTexture.js +17 -0
  1455. package/src/shade/renderer/texture/TextureManager.d.ts +22 -5
  1456. package/src/shade/renderer/texture/TextureManager.d.ts.map +1 -1
  1457. package/src/shade/renderer/texture/TextureManager.js +109 -6
  1458. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.d.ts.map +1 -1
  1459. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.js +8 -2
  1460. package/src/shade/renderer/texture/download_texture_data.d.ts.map +1 -1
  1461. package/src/shade/renderer/texture/download_texture_data.js +12 -4
  1462. package/src/shade/renderer/texture/encode/shader_texture_encode_astc_4x4.d.ts.map +1 -1
  1463. package/src/shade/renderer/texture/encode/shader_texture_encode_astc_4x4.js +66 -63
  1464. package/src/shade/renderer/texture/encode/shader_texture_encode_bc1.d.ts.map +1 -1
  1465. package/src/shade/renderer/texture/encode/shader_texture_encode_bc1.js +63 -60
  1466. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4.d.ts.map +1 -1
  1467. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4.js +79 -76
  1468. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4_snorm.d.ts.map +1 -1
  1469. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4_snorm.js +67 -64
  1470. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5.d.ts.map +1 -1
  1471. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5.js +70 -67
  1472. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5_snorm.d.ts.map +1 -1
  1473. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5_snorm.js +66 -63
  1474. package/src/shade/renderer/texture/encode/shader_texture_encode_bc6h.d.ts.map +1 -1
  1475. package/src/shade/renderer/texture/encode/shader_texture_encode_bc6h.js +65 -62
  1476. package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.d.ts.map +1 -1
  1477. package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.js +61 -58
  1478. package/src/shade/renderer/texture/format/texture_format_from_shade_image.d.ts +7 -1
  1479. package/src/shade/renderer/texture/format/texture_format_from_shade_image.d.ts.map +1 -1
  1480. package/src/shade/renderer/texture/format/texture_format_from_shade_image.js +100 -95
  1481. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts.map +1 -1
  1482. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +59 -68
  1483. package/src/shade/renderer/texture/mipmaps/filters/mipmap_shader_code_wronski2021.d.ts +30 -1
  1484. package/src/shade/renderer/texture/mipmaps/filters/mipmap_shader_code_wronski2021.d.ts.map +1 -1
  1485. package/src/shade/renderer/texture/mipmaps/filters/mipmap_shader_code_wronski2021.js +37 -20
  1486. package/src/shade/renderer/texture/texture_descriptor_from_texture_shade.js +73 -73
  1487. package/src/shade/renderer/view/ViewManager.d.ts +8 -0
  1488. package/src/shade/renderer/view/ViewManager.d.ts.map +1 -1
  1489. package/src/shade/renderer/view/ViewManager.js +15 -0
  1490. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  1491. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +516 -507
  1492. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.d.ts.map +1 -1
  1493. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.js +272 -260
  1494. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.d.ts.map +1 -1
  1495. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.js +160 -134
  1496. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts +3 -0
  1497. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  1498. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +243 -62
  1499. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  1500. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +110 -7
  1501. package/src/shade/wgsl/validate_wgsl_source.d.ts.map +1 -1
  1502. package/src/shade/wgsl/validate_wgsl_source.js +57 -0
  1503. package/src/view/asset/AssetLoaderStatusView.d.ts.map +1 -1
  1504. package/src/view/asset/AssetLoaderStatusView.js +8 -0
  1505. package/src/view/common/LabelView.d.ts.map +1 -1
  1506. package/src/view/common/LabelView.js +4 -0
  1507. package/src/view/common/ListView.d.ts +6 -2
  1508. package/src/view/common/ListView.d.ts.map +1 -1
  1509. package/src/view/common/ListView.js +27 -7
  1510. package/src/view/common/TabbedView.d.ts.map +1 -1
  1511. package/src/view/common/TabbedView.js +8 -0
  1512. package/src/view/common/VirtualListView.d.ts.map +1 -1
  1513. package/src/view/common/VirtualListView.js +17 -1
  1514. package/src/view/common/dnd/DropTarget.d.ts.map +1 -1
  1515. package/src/view/common/dnd/DropTarget.js +18 -0
  1516. package/src/view/elements/DropDownSelectionView.d.ts.map +1 -1
  1517. package/src/view/elements/DropDownSelectionView.js +8 -0
  1518. package/src/view/elements/image/SvgImageView.d.ts.map +1 -1
  1519. package/src/view/elements/image/SvgImageView.js +8 -0
  1520. package/src/view/elements/navigation/ViewStack.d.ts.map +1 -1
  1521. package/src/view/elements/navigation/ViewStack.js +4 -0
  1522. package/src/view/elements/notify/NotificationView.d.ts.map +1 -1
  1523. package/src/view/elements/notify/NotificationView.js +4 -0
  1524. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts.map +1 -1
  1525. package/src/view/elements/progress/segmented/SegmentedResourceBarView.js +8 -0
  1526. package/src/view/elements/radial/RadialMenu.d.ts.map +1 -1
  1527. package/src/view/elements/radial/RadialMenu.js +4 -0
  1528. package/src/view/minimap/Minimap.d.ts.map +1 -1
  1529. package/src/view/minimap/Minimap.js +8 -0
  1530. package/src/view/minimap/MinimapCanvasView.d.ts.map +1 -1
  1531. package/src/view/minimap/MinimapCanvasView.js +21 -1
  1532. package/src/view/minimap/dom/MinimapCameraView.d.ts.map +1 -1
  1533. package/src/view/minimap/dom/MinimapCameraView.js +232 -220
  1534. package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
  1535. package/src/view/minimap/dom/MinimapMarkerView.js +8 -0
  1536. package/src/view/minimap/minimap_focus_area.d.ts +5 -0
  1537. package/src/view/minimap/minimap_focus_area.d.ts.map +1 -1
  1538. package/src/view/minimap/minimap_focus_area.js +22 -0
  1539. package/src/view/tooltip/TooltipManager.d.ts.map +1 -1
  1540. package/src/view/tooltip/TooltipManager.js +16 -4
  1541. package/src/core/geom/3d/atlas/atlas_bench_lib.js +0 -315
  1542. package/src/engine/graphics/ecs/mesh/MeshEvents.d.ts +0 -6
  1543. package/src/engine/graphics/ecs/mesh/MeshEvents.d.ts.map +0 -1
  1544. package/src/engine/graphics/ecs/mesh/MeshEvents.js +0 -16
  1545. package/src/engine/graphics/ecs/mesh/commonObject3DFastMatrixUpdate.d.ts +0 -2
  1546. package/src/engine/graphics/ecs/mesh/commonObject3DFastMatrixUpdate.d.ts.map +0 -1
  1547. package/src/engine/graphics/ecs/mesh/commonObject3DFastMatrixUpdate.js +0 -37
  1548. package/src/engine/graphics/ecs/mesh/copyToVector3.d.ts +0 -8
  1549. package/src/engine/graphics/ecs/mesh/copyToVector3.d.ts.map +0 -1
  1550. package/src/engine/graphics/ecs/mesh/copyToVector3.js +0 -18
  1551. package/src/engine/graphics/ecs/mesh/object3DFastMatrixUpdate.d.ts +0 -6
  1552. package/src/engine/graphics/ecs/mesh/object3DFastMatrixUpdate.d.ts.map +0 -1
  1553. package/src/engine/graphics/ecs/mesh/object3DFastMatrixUpdate.js +0 -18
  1554. package/src/engine/graphics/ecs/mesh/rootObject3DFastMatrixUpdate.d.ts +0 -6
  1555. package/src/engine/graphics/ecs/mesh/rootObject3DFastMatrixUpdate.d.ts.map +0 -1
  1556. package/src/engine/graphics/ecs/mesh/rootObject3DFastMatrixUpdate.js +0 -14
  1557. package/src/engine/graphics/ecs/mesh/setMesh.d.ts +0 -16
  1558. package/src/engine/graphics/ecs/mesh/setMesh.d.ts.map +0 -1
  1559. package/src/engine/graphics/ecs/mesh/setMesh.js +0 -55
  1560. package/src/engine/graphics/ecs/mesh/skeleton/BoneMapping.d.ts +0 -13
  1561. package/src/engine/graphics/ecs/mesh/skeleton/BoneMapping.d.ts.map +0 -1
  1562. package/src/engine/graphics/ecs/mesh/skeleton/BoneMapping.js +0 -99
  1563. package/src/engine/graphics/ecs/mesh/skeleton/HumanoidBoneType.d.ts +0 -25
  1564. package/src/engine/graphics/ecs/mesh/skeleton/HumanoidBoneType.d.ts.map +0 -1
  1565. package/src/engine/graphics/ecs/mesh/skeleton/HumanoidBoneType.js +0 -29
  1566. package/src/engine/graphics/ecs/mesh/updateMeshTransform.d.ts +0 -6
  1567. package/src/engine/graphics/ecs/mesh/updateMeshTransform.d.ts.map +0 -1
  1568. package/src/engine/graphics/ecs/mesh/updateMeshTransform.js +0 -15
  1569. package/src/engine/graphics/ecs/mesh/updateNodeByTransformAndBBB.d.ts +0 -8
  1570. package/src/engine/graphics/ecs/mesh/updateNodeByTransformAndBBB.d.ts.map +0 -1
  1571. package/src/engine/graphics/ecs/mesh/updateNodeByTransformAndBBB.js +0 -14
  1572. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.js +0 -481
  1573. package/src/engine/graphics3/terrain/pack_terrain_row_table.js +0 -48
  1574. package/src/engine/graphics3/terrain/shader_terrain_splat.js +0 -215
  1575. package/src/shade/renderer/animation/ANIMATION_BOUND_TRACK_GROUP_STRUCT.js +0 -57
  1576. package/src/shade/renderer/animation/ANIMATION_KEYFRAME_BLOCK_STRUCT.js +0 -50
  1577. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_BLOCK_STRUCT.js +0 -62
  1578. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts +0 -46
  1579. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts.map +0 -1
@@ -1,2078 +1,2122 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
3
- import { CapsuleShape3D } from "../../../core/geom/3d/shape/CapsuleShape3D.js";
4
- import { TransformedShape3D } from "../../../core/geom/3d/shape/TransformedShape3D.js";
5
- import Quaternion from "../../../core/geom/Quaternion.js";
6
- import Vector3 from "../../../core/geom/Vector3.js";
7
- import { clamp } from "../../../core/math/clamp.js";
8
- import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
9
- import { lerp } from "../../../core/math/lerp.js";
10
- import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
11
- import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
12
- import { SerializationMetadata } from "../../ecs/components/SerializationMetadata.js";
13
- import Entity from "../../ecs/Entity.js";
14
- import { System } from "../../ecs/System.js";
15
- import { Transform64 } from "../../ecs/transform/Transform64.js";
16
- import { Camera } from "../../graphics/ecs/camera/Camera.js";
17
- import { BodyKind } from "../../physics/ecs/BodyKind.js";
18
- import { Collider } from "../../physics/ecs/Collider.js";
19
- import { PhysicsSystem } from "../../physics/ecs/PhysicsSystem.js";
20
- import { RigidBody } from "../../physics/ecs/RigidBody.js";
21
- import { PhysicsSurfacePoint } from "../../physics/queries/PhysicsSurfacePoint.js";
22
- import { KinematicMover } from "./collision/KinematicMover.js";
23
- import { EyeOffsetStack } from "./composer/EyeOffsetStack.js";
24
- import { FirstPersonPlayerController } from "./FirstPersonPlayerController.js";
25
- import { DecisionPoint } from "./mastery/DecisionPoint.js";
26
- import { computeJumpFromApex } from "./math/computeJumpFromApex.js";
27
- import { computeLRCBreathRate } from "./math/computeLRCBreathRate.js";
28
- import { computeMassRatios } from "./math/computeMassRatios.js";
29
- import { jumpForceForHeight, launchVelocityForForce } from "./math/jumpDynamics.js";
30
- import { Spring } from "./math/Spring.js";
31
- import { stepTowards } from "./math/stepTowards.js";
32
- import { FirstPersonActionState, FirstPersonLocomotionMode } from "./pose/FirstPersonPose.js";
33
- import { FirstPersonPosture } from "./pose/FirstPersonPosture.js";
34
- import { FirstPersonSensors } from "./sensors/FirstPersonSensors.js";
35
- import { t64_announce_change } from "../../ecs/transform/t64_announce_change.js";
36
-
37
- // ---------------------------------------------------------------------------
38
- // Scratch allocations — reused per frame to avoid GC pressure
39
- // ---------------------------------------------------------------------------
40
- const SCRATCH_V3_A = new Vector3();
41
- const SCRATCH_V3_B = new Vector3();
42
- const SCRATCH_V3_C = new Vector3();
43
- const SCRATCH_Q_A = new Quaternion();
44
- const SCRATCH_Q_B = new Quaternion();
45
- const SCRATCH_Q_C = new Quaternion();
46
- /** the transform holds its rotation as four numbers, so euler and axis-angle work goes through here */
47
- const SCRATCH_Q_EULER = new Quaternion();
48
-
49
- const TWO_PI = Math.PI * 2;
50
- const LN2 = Math.log(2);
51
-
52
- /**
53
- * Build a posture-sized player capsule: a {@link CapsuleShape3D} of
54
- * `radius` and the appropriate cylinder height, wrapped in a
55
- * {@link TransformedShape3D} whose Y offset puts the capsule's bottom
56
- * exactly at the wrapped shape's local origin. The entity's
57
- * `transform.position` then represents the player's feet — and a
58
- * posture-driven shrink doesn't yank the feet up the way a centred
59
- * capsule would, nor dip them below the floor.
60
- *
61
- * The capsule's lowest point in its own local frame is at
62
- * `-(cylinderHeight/2 + radius) = -max(totalHeight/2, radius)`.
63
- * Offsetting the wrapper by the magnitude of that puts the bottom at
64
- * Y = 0:
65
- * - Stand (`H = 1.8`, `r = 0.34`): cylHeight = 1.12, offset = 0.9.
66
- * Bottom = -0.9 + 0.9 = 0. Top = +0.9 + 0.9 = 1.8.
67
- * - Crouch (`H = 0.8`, `r = 0.34`): cylHeight = 0.12, offset = 0.4.
68
- * Bottom = -0.4 + 0.4 = 0. Top = +0.4 + 0.4 = 0.8.
69
- * - Prone (`H = 0.4`, `r = 0.34`): cylHeight = 0 (capsule collapses
70
- * to a sphere of radius), offset = max(0.2, 0.34) = 0.34.
71
- * Bottom = -0.34 + 0.34 = 0. Top = +0.34 + 0.34 = 0.68. The
72
- * `totalHeight = 0.4` value is honoured for the offset budget
73
- * but the actual Y extent floors at `2·radius`.
74
- *
75
- * Picking `totalHeight/2` blindly (the obvious choice) would put the
76
- * Prone capsule's bottom at `0.2 - 0.34 = -0.14` — dipping below the
77
- * feet, and into any physics floor that's flush with feet level. On
78
- * a physics ground slab, every horizontal shape_cast from inside the
79
- * floor returns t = 0, `advance = max(0, t - SKIN) = 0`, and the
80
- * slide freezes in place — see SlideMotion.spec.js for the
81
- * regression test that pins this.
82
- *
83
- * @param {number} radius — capsule radius in metres
84
- * @param {number} totalHeight — desired full Y extent; ignored below
85
- * `2·radius` (the capsule's intrinsic minimum extent)
86
- * @returns {TransformedShape3D}
87
- */
88
- function makePostureCapsule(radius, totalHeight) {
89
- const cylinderHeight = Math.max(0, totalHeight - 2 * radius);
90
- const yOffset = Math.max(totalHeight / 2, radius);
91
- return TransformedShape3D.from_translation(
92
- CapsuleShape3D.from(radius, cylinderHeight),
93
- [0, yOffset, 0],
94
- );
95
- }
96
-
97
- /**
98
- * Per-entity runtime state the system maintains internally — too transient
99
- * even for {@link FirstPersonPlayerController}'s `state` member, because it
100
- * encodes input-edge bookkeeping and timer values the public surface should
101
- * never see directly.
102
- */
103
- class PerEntityRuntime {
104
- /**
105
- * Co-attached kinematic body. Set by {@link FirstPersonPlayerControllerSystem.link}
106
- * after asserting it's present. The controller writes Transform64.position
107
- * directly (existing motion logic); physics derives the body's velocity
108
- * from the per-step delta. Other physics systems (raycasts, contact
109
- * events) see the player through this body.
110
- * @type {RigidBody|null}
111
- */
112
- rigidBody = null;
113
-
114
- /**
115
- * Co-attached collider, cached at link. Same source the physics
116
- * narrowphase uses, so move-and-slide casts the player's
117
- * actual collision shape against the world.
118
- * @type {Collider|null}
119
- */
120
- collider = null;
121
-
122
- /**
123
- * Pre-built capsule colliders, one per posture. Cached at link
124
- * from `config.body.{height, crouchHeight, proneHeight, radius}`
125
- * so {@link _syncColliderShape} can swap the collider's shape on
126
- * a posture change with zero per-tick allocation. Hang reuses
127
- * Stand (the player's body is full-extent, just hanging below
128
- * the ledge — the rig animates the arms-up pose). Sentinel
129
- * `lastPosture = -1` forces a sync on the first tick after
130
- * link, so the initial shape always matches Stand.
131
- * @private
132
- * @type {TransformedShape3D|null}
133
- */
134
- colliderShapeStand = null;
135
-
136
- /** @private @type {TransformedShape3D|null} */
137
- colliderShapeCrouch = null;
138
-
139
- /** @private @type {TransformedShape3D|null} */
140
- colliderShapeProne = null;
141
-
142
- /** @private */
143
- lastPosture = -1;
144
-
145
- /** Eye pitch in radians, clamped to config.look limits. */
146
- eyePitch = 0;
147
-
148
- /** Body yaw in radians (around world up). */
149
- bodyYaw = 0;
150
-
151
- /** Yaw rate (rad/s) computed in look consumption — for evaluators. */
152
- yawRateRadPerSec = 0;
153
-
154
- // -- Render-rate look + position interpolation ---------------------
155
- // The camera DIRECTION (yaw/pitch) is consumed at RENDER rate (see the
156
- // controller's _consumeLook), so it tracks the mouse smoothly instead of
157
- // being aliased by the 60Hz sim sampling — consuming the accumulated
158
- // mouse delta once per fixed step beats against an unaligned mouse
159
- // report rate and makes a slow turn lurch (the reported snap). The body
160
- // POSITION still advances only at the fixed step, so it's blended
161
- // between the last two steps by the sub-step alpha to stay smooth on a
162
- // high-refresh display.
163
- /** Body yaw at the end of the previous fixed step — used to derive the
164
- * per-step turn RATE that drives look-lean, decoupled from per-frame
165
- * input jitter. */
166
- prevFixedYaw = 0;
167
-
168
- renderPrevPos = new Vector3();
169
- renderCurPos = new Vector3();
170
-
171
- /** When true, the next position snapshot collapses prev=cur (no blend)
172
- * — set on a body teleport (e.g. the ledge-grab hang snap) so the
173
- * camera doesn't glide across the jump. */
174
- renderSnap = false;
175
-
176
- /** Horizontal+vertical velocity. We integrate these inside the system
177
- * when no external physics layer is attached. */
178
- velocityX = 0;
179
-
180
- velocityY = 0;
181
- velocityZ = 0;
182
-
183
- /** Previous-tick jump intent — for rising/falling edge detection. */
184
- prevJumpHeld = false;
185
-
186
- /** Previous-tick crouch intent — for toggle-mode edge detection. */
187
- prevCrouchHeld = false;
188
-
189
- /** True while crouch toggle is latched on (used only in toggle mode). */
190
- crouchLatched = false;
191
-
192
- /** Remaining time in jump anticipation, or <= 0 if not anticipating. */
193
- anticipationRemaining = 0;
194
-
195
- /** Cached derived gravity (m/s^2) from peakHeight + timeToApex. */
196
- gravity = 9.81;
197
-
198
- /**
199
- * Cached jump PUSH-OFF FORCE (newtons) — the physical leg force that
200
- * reaches `peakHeight` at the reference mass, the quantity jump power is
201
- * expressed in. Derived at link via {@link jumpForceForHeight}; abilities
202
- * (e.g. the scramble wall-kick) launch off this same force.
203
- */
204
- jumpForce = 0;
205
-
206
- /** Cached derived jump impulse (m/s upward), post-mass-scaling. */
207
- jumpInitialVy = 5.0;
208
-
209
- /**
210
- * Cached mass scaling factors — computed once at link. See
211
- * {@link computeMassRatios}. Heavier ⇒ lower jumpV0Scale, lower
212
- * groundAccelScale, higher landingDipScale + exertionRiseScale.
213
- */
214
- massRatios = null;
215
-
216
- /** Spring for landing dip (under-damped → rings after impact). */
217
- landSpring = new Spring();
218
-
219
- /** Spring for FOV (critically damped). */
220
- fovSpring = new Spring(70);
221
-
222
- /** Spring for eye height (crouch transition). */
223
- eyeHeightSpring = new Spring(1.80);
224
-
225
- /** Spring for lean roll (radians) — banks into lateral acceleration. */
226
- leanSpring = new Spring();
227
-
228
- /**
229
- * Lean target this tick (radians). Always set; L2.f spring-steps
230
- * toward this value. Whoever owned motion this tick wrote it:
231
- * base writes the lat-accel + look-lean derived value at the end
232
- * of {@link _runBaseLocomotion}; abilities that want to override
233
- * (WallRun → tilt-into-wall, Slide/Mantle/LedgeGrab → zero) write
234
- * their own value in tick. Uniform channel — no null sentinel.
235
- */
236
- leanTargetRad = 0;
237
-
238
- /** Previous horizontal velocity — for lateral acceleration → lean. */
239
- prevVelocityX = 0;
240
-
241
- prevVelocityZ = 0;
242
-
243
- /** Previous-tick grounded for edge detection. */
244
- prevGrounded = true;
245
-
246
- /** Vertical speed at moment of last "leave ground". */
247
- takeoffVy = 0;
248
-
249
- /** Max vertical position since last takeoff — for jump apex detection. */
250
- peakAltitude = 0;
251
-
252
- /** Set true once a jump has been launched; cleared on land. */
253
- midJump = false;
254
-
255
- /** Apex already fired for this airborne segment? */
256
- apexFired = false;
257
-
258
- /** Stride phase from previous fixed step — for footstep edge detection. */
259
- prevStridePhase = 0;
260
-
261
- /** Breath phase from previous fixed step — for inhale/exhale edge detection. */
262
- prevBreathPhase = 0;
263
-
264
- /** Which foot fires next — flipped on each footstep signal. */
265
- nextFootSide = "R";
266
-
267
- /**
268
- * Which foot is currently bearing the body's weight (the foot that
269
- * most recently landed). Drives the lateral-bob direction: at R
270
- * midstance the COM is over the right foot, so the head shifts
271
- * laterally toward screen-right; at L midstance the opposite.
272
- * Coupled to the same signal the footstep emits, so anything that
273
- * listens to onFootStep.side will see the bob agree.
274
- * Initialized "L" so the very first footstep fires "R" and the
275
- * standingFoot updates to "R" — putting the head laterally right
276
- * during the first half-stride, as expected.
277
- */
278
- standingFoot = "L";
279
-
280
- /**
281
- * [0..1] How "backward" the player is currently moving. Derived in
282
- * fixedUpdate from velocity · screen-forward, normalized to sprint
283
- * speed. Drives the gait wobble amplifier on the L3 camera-composition
284
- * pass. Stored on runtime (rather than state) because it's a render-
285
- * side input — downstream observers should look at velocity directly.
286
- */
287
- backwardness = 0;
288
-
289
- /**
290
- * Smoothed bob amplitude envelope. Target = max(speedNormalized,
291
- * backwardness) when grounded, 0 airborne. Spring decay prevents
292
- * the whiplash where stopping motion would snap the bob to neutral.
293
- */
294
- bobIntensitySpring = new Spring();
295
-
296
- /**
297
- * Vertical impact spring — kicked downward at each footfall, decays
298
- * with a slight under-damped overshoot. Produces the impact-arrest +
299
- * leg-push curve. value units: meters (added directly to eyeLocal.y).
300
- */
301
- verticalImpactSpring = new Spring();
302
-
303
- /**
304
- * Sprint-posture spring — eye pitches forward as the player commits
305
- * to a sprint, returns to neutral when they slow. Value is in
306
- * radians; slower half-life than other springs so it feels like
307
- * a posture change rather than an input twitch. See cfg.posture.
308
- */
309
- sprintPostureSpring = new Spring();
310
-
311
- /**
312
- * Head-droop spring — additional forward pitch as exertion rises.
313
- * Sells fatigue subtly. Target tracks exertion-driven max droop
314
- * angle; spring lag keeps the transition slow and physical.
315
- */
316
- headDroopSpring = new Spring();
317
-
318
- /**
319
- * [0..1] sprintness — how much of the walk→sprint speed range the
320
- * body is currently in. Computed in fixedUpdate, read by L3 for FOV
321
- * and the sprint-posture pitch / forward-shift offset.
322
- */
323
- sprintness = 0;
324
-
325
- /**
326
- * Cached sin/cos of current body yaw — written once per fixedUpdate
327
- * after look intent is consumed, read by every downstream step
328
- * (locomotion, backwardness, lean look-rate, pose channels). Avoids
329
- * recomputing the trig 3+ times per tick.
330
- */
331
- sinYaw = 0;
332
-
333
- cosYaw = 1;
334
-
335
- /** Cached horizontal speed (m/s) for this tick — written in derived-state. */
336
- horizSpeed = 0;
337
-
338
- /** Cached stride frequency (Hz) for this tick — written in breath block, read by stride. */
339
- strideFreqHz = 0;
340
-
341
- /**
342
- * Additive accumulator for body-local eye-position offsets. The
343
- * system pushes its own contributions (bob, breath, landing,
344
- * sprint posture) each render frame; external systems can push
345
- * recoil/shake/knockback contributions via the same interface.
346
- */
347
- eyeOffsetStack = new EyeOffsetStack();
348
-
349
- /**
350
- * Spatial-query results populated by {@link FirstPersonSensorsSystem}
351
- * (when present). Abilities and the locomotion FSM read this.
352
- * Lives on runtime so other systems can populate it without
353
- * touching the controller component's public surface.
354
- */
355
- sensors = new FirstPersonSensors();
356
-
357
- /** Cached eye entity ID. -1 until link assigns it. */
358
- eyeEntity = -1;
359
-
360
- /**
361
- * One-shot hand-off flag: LedgeGrab sets it on a pull-up release so
362
- * Mantle (next tick) knows this is a ledge pull-up — it then allows a
363
- * too-thin top (auto-mantle still requires a standable one) and vaults
364
- * OVER instead of onto. Mantle consumes it in onActivate.
365
- */
366
- ledgePullUpRequest = false;
367
-
368
- /**
369
- * Seconds remaining in the post-release ledge re-grab refractory
370
- * window. Set by {@link LedgeGrab} on deactivation, aged down by dt
371
- * each fixed step, and read by LedgeGrab.canActivate — so a deliberate
372
- * dismount can fall clear of the grab window without the auto-catch
373
- * snapping the body straight back onto the same lip.
374
- */
375
- ledgeRegrabCooldown = 0;
376
- }
377
-
378
- /**
379
- * Drives a first-person camera + body from intent fields. See sibling
380
- * DESIGN.md for goals, architecture, and the five processing layers (L0..L4).
381
- *
382
- * - fixedUpdate runs L1 (locomotion), L2 (pose state), and L4 (events) so
383
- * the simulation remains deterministic.
384
- * - update runs L3 (camera composition) at render rate so the eye is never
385
- * smoother than the screen.
386
- *
387
- * The system itself integrates a simple flat-floor at y = `config.gravity.magnitude > 0
388
- * ? state.groundY : -Infinity` for the prototype. A real physics layer should
389
- * write `state.grounded`/`state.groundNormal` from outside instead; the
390
- * built-in resolver is just a convenience to keep the controller usable
391
- * without dependencies.
392
- *
393
- * @author Alex Goldring
394
- * @copyright Company Named Limited (c) 2026
395
- */
396
- export class FirstPersonPlayerControllerSystem extends System {
397
- // Dependencies kept to (controller, transform) so we can ASSERT on
398
- // RigidBody at link time and emit a clear error if missing. If
399
- // RigidBody were a hard dep, entities lacking one would silently
400
- // never link — the controller would appear inert with no
401
- // diagnostic. The assert below catches the missing-body case
402
- // explicitly.
403
- dependencies = [FirstPersonPlayerController, Transform64];
404
-
405
- components_used = [
406
- ResourceAccessSpecification.from(Transform64, ResourceAccessKind.Write),
407
- ResourceAccessSpecification.from(Camera, ResourceAccessKind.Write),
408
- ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Write),
409
- ];
410
-
411
- /**
412
- * Per-entity runtime, keyed by entity id.
413
- * @type {Map<number, PerEntityRuntime>}
414
- */
415
- runtime = new Map();
416
-
417
- /**
418
- * If true, the system clamps body y >= groundY and writes
419
- * state.grounded itself. Turn off when wiring a real physics layer.
420
- * @type {boolean}
421
- */
422
- useBuiltInFlatGround = true;
423
-
424
- /**
425
- * The flat-ground y for the built-in resolver. Ignored when
426
- * useBuiltInFlatGround is false.
427
- * @type {number}
428
- */
429
- groundY = 0;
430
-
431
- /**
432
- * Optional callback that returns the surface Y under the player
433
- * for ground resolution. Called each tick with the player's
434
- * current (x, y, z); returns the world-Y of the ground below,
435
- * or null if no ground is below (gap / void).
436
- *
437
- * Combines with `useBuiltInFlatGround`: the effective ground for
438
- * the tick is `max(this.groundY when enabled, resolver(...))`.
439
- * Set both off (`useBuiltInFlatGround=false`, `groundResolver=null`)
440
- * to defer to external physics entirely.
441
- *
442
- * Designed for prototypes / gyms that need elevated platforms
443
- * without a full physics layer. Production should wire a real
444
- * physics system instead.
445
- *
446
- * @type {((x:number, y:number, z:number) => number|null) | null}
447
- */
448
- groundResolver = null;
449
-
450
- /**
451
- * PhysicsSystem reference. Auto-acquired at startup; can be
452
- * overridden by the caller. When present, collision is resolved
453
- * by the {@link KinematicMover}; when null, the controller uses
454
- * the flat-ground integrator ({@link _moveFlatGround}) — useful
455
- * for spec setups that don't wire physics.
456
- * @type {PhysicsSystem|null}
457
- */
458
- physicsSystem = null;
459
-
460
- /**
461
- * The collision solver — the authoritative move resolver whenever
462
- * a {@link PhysicsSystem} is present (recover + unified
463
- * sweep-and-slide + ground-categorize; see DESIGN_COLLISION.md).
464
- * Lazily built on first use. With no physics the controller falls
465
- * back to a flat-ground integrator ({@link _moveFlatGround}).
466
- * @private
467
- * @type {KinematicMover|null}
468
- */
469
- _mover = null;
470
-
471
- /** @private Scratch velocity handed to the mover each tick. */
472
- _moverVelocity = new Vector3();
473
-
474
- /** @private Scratch for the posture-headroom up-cast. */
475
- _postureRay = new Ray3();
476
-
477
- /** @private @type {PhysicsSurfacePoint} */
478
- _postureHit = new PhysicsSurfacePoint();
479
-
480
- async startup(entityManager) {
481
- this.entityManager = entityManager;
482
- if (this.physicsSystem === null) {
483
- const ps = entityManager.getSystem(PhysicsSystem);
484
- if (ps !== null) this.physicsSystem = ps;
485
- }
486
- }
487
-
488
- /**
489
- * @param {FirstPersonPlayerController} controller
490
- * @param {Transform64} bodyTransform
491
- * @param {number} entity
492
- */
493
- link(controller, bodyTransform, entity) {
494
- const ecd = this.entityManager.dataset;
495
-
496
- // The controller assumes a kinematic-position RigidBody is co-
497
- // attached on this entity. The body is the spatial proxy used
498
- // for sensor raycasts and physics-side observers (other entities
499
- // raycasting against the player, dynamic bodies colliding with
500
- // the capsule, etc.). The controller writes Transform directly,
501
- // physics derives velocity from the per-step delta. If a body is
502
- // missing the controller could still drive the camera, but the
503
- // physics integration silently breaks — assert here so the
504
- // misconfiguration is caught at link time.
505
- const rigidBody = ecd.getComponent(entity, RigidBody);
506
- assert.ok(rigidBody !== undefined,
507
- "FirstPersonPlayerController entity must have a co-attached RigidBody "
508
- + "(kinematic capsule). See prototype_first_person_controller.js for setup.");
509
- assert.equal(rigidBody.kind, BodyKind.KinematicPosition,
510
- "FirstPersonPlayerController RigidBody must be BodyKind.KinematicPosition; "
511
- + "the controller owns the Transform and physics derives velocity.");
512
- // Collider is also required — the KinematicMover casts this shape
513
- // against the world (recover / sweep / ground probe). Asserted
514
- // here so a missing collider surfaces at link rather than
515
- // producing a null-deref at the first cast attempt.
516
- const collider = ecd.getComponent(entity, Collider);
517
- assert.ok(collider !== undefined,
518
- "FirstPersonPlayerController entity must have a co-attached Collider. "
519
- + "The controller's move-and-slide casts this shape to detect blockers.");
520
-
521
- const runtime = new PerEntityRuntime();
522
- runtime.rigidBody = rigidBody;
523
- runtime.collider = collider;
524
-
525
- // Pre-build one capsule per posture from cfg.body. Eye-height
526
- // doubles as collider-top by convention here — the prototype's
527
- // `buildPlayerEntity` uses the same approximation (`totalHeight =
528
- // bodyCfg.height`). The +Y offset puts the capsule bottom at
529
- // transform.position so the player's "feet" stay anchored across
530
- // posture changes; only the head drops/rises.
531
- const radius = controller.config.body.radius;
532
- runtime.colliderShapeStand = makePostureCapsule(radius, controller.config.body.height);
533
- runtime.colliderShapeCrouch = makePostureCapsule(radius, controller.config.body.crouchHeight);
534
- runtime.colliderShapeProne = makePostureCapsule(radius, controller.config.body.proneHeight);
535
- // Cache each posture capsule's ACTUAL top (bbox maxY) — not the
536
- // nominal config height. A capsule's height floors at 2·radius,
537
- // so e.g. a 0.4 m "prone" height actually yields a 0.68 m
538
- // capsule. The headroom up-cast sweeps by the difference of real
539
- // tops; using config heights would over-/under-estimate the grow
540
- // and clamp to the wrong posture (Prone instead of Crouch).
541
- const bbox = new Float64Array(6);
542
- runtime.colliderShapeStand.compute_bounding_box(bbox);
543
- runtime.standTop = bbox[4];
544
- runtime.colliderShapeCrouch.compute_bounding_box(bbox);
545
- runtime.crouchTop = bbox[4];
546
- runtime.colliderShapeProne.compute_bounding_box(bbox);
547
- runtime.proneTop = bbox[4];
548
- // Force a shape sync on the first tick: even though the caller
549
- // built a Stand-sized collider, we rebuild it from cfg here so a
550
- // post-link config tweak (e.g. crouchHeight changed for a unit
551
- // test) is reflected on the live collider without a relink.
552
- runtime.lastPosture = -1;
553
-
554
- this.runtime.set(entity, runtime);
555
-
556
- // Derive gravity + jump impulse from designer-friendly params, then
557
- // mass-scale the initial velocity (heavier ⇒ lower jump).
558
- runtime.massRatios = computeMassRatios(
559
- controller.config.body.mass,
560
- controller.config.body.referenceMass,
561
- controller.config.body.massCouplingStrength,
562
- );
563
- const derived = { gravity: 0, initialVelocity: 0 };
564
- computeJumpFromApex(controller.config.jump.peakHeight, controller.config.jump.timeToApex, derived);
565
- runtime.gravity = derived.gravity;
566
- // Express jump power as a physical push-off FORCE (newtons): the force the
567
- // legs deliver to reach peakHeight at the REFERENCE mass. Stored so other
568
- // moves (the scramble wall-kick) can launch off the SAME force.
569
- const bodyCfg = controller.config.body;
570
- runtime.jumpForce = jumpForceForHeight(
571
- bodyCfg.referenceMass, runtime.gravity, bodyCfg.jumpPushoffDistance, controller.config.jump.peakHeight,
572
- );
573
- // Force → launch velocity for the reference body (== √(2·g·peakHeight)),
574
- // then the mass-coupling dial (jumpV0Scale ∝ 1/√mass, modulated by
575
- // massCouplingStrength) scales it for the actual body. Identical result
576
- // to the prior `derived.initialVelocity · jumpV0Scale`, now routed
577
- // through the force so jump power has a single physical source.
578
- runtime.jumpInitialVy =
579
- launchVelocityForForce(runtime.jumpForce, bodyCfg.referenceMass, bodyCfg.jumpPushoffDistance)
580
- * runtime.massRatios.jumpV0Scale;
581
-
582
- // Seed yaw from the starting body rotation. `toEulerAnglesYXZ`
583
- // returns (pitch, yaw, roll) — we only care about y.
584
- SCRATCH_Q_EULER.set(
585
- bodyTransform.rotation_x,
586
- bodyTransform.rotation_y,
587
- bodyTransform.rotation_z,
588
- bodyTransform.rotation_w
589
- );
590
- SCRATCH_Q_EULER.toEulerAnglesYXZ(SCRATCH_V3_A);
591
- runtime.bodyYaw = SCRATCH_V3_A.y;
592
- runtime.eyePitch = 0;
593
-
594
- // Seed render-rate-look + position-interpolation state to the spawn pose.
595
- runtime.prevFixedYaw = runtime.bodyYaw;
596
- runtime.renderPrevPos.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
597
- runtime.renderCurPos.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
598
-
599
- // Initialize springs to standing-eye-height baseline
600
- runtime.eyeHeightSpring.settle(controller.config.body.height);
601
- runtime.fovSpring.settle(controller.config.fov.base);
602
- controller.state.eyeHeight = controller.config.body.height;
603
-
604
- // Create eye entity if one wasn't supplied
605
- if (controller.eyeEntity === -1 || !ecd.entityExists(controller.eyeEntity)) {
606
- const eye = new Entity();
607
-
608
- const eyeTransform = new Transform64();
609
- const baseEyePos = SCRATCH_V3_A.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
610
- baseEyePos.y += controller.config.body.height;
611
- eyeTransform.setTranslation(baseEyePos.x, baseEyePos.y, baseEyePos.z);
612
-
613
- const camera = new Camera();
614
- camera.active.set(true);
615
- camera.fov.set(controller.config.fov.base);
616
- camera.clip_near = 0.05;
617
- camera.clip_far = 1000;
618
- camera.autoClip = true;
619
-
620
- eye.add(eyeTransform);
621
- eye.add(camera);
622
- eye.add(SerializationMetadata.Transient);
623
-
624
- eye.build(ecd);
625
-
626
- controller.eyeEntity = eye.id;
627
- }
628
-
629
- runtime.eyeEntity = controller.eyeEntity;
630
- }
631
-
632
- /**
633
- * @param {FirstPersonPlayerController} controller
634
- * @param {Transform64} bodyTransform
635
- * @param {number} entity
636
- */
637
- unlink(controller, bodyTransform, entity) {
638
- const ecd = this.entityManager.dataset;
639
-
640
- if (controller.eyeEntity !== -1 && ecd.entityExists(controller.eyeEntity)) {
641
- ecd.removeEntity(controller.eyeEntity);
642
- controller.eyeEntity = -1;
643
- }
644
-
645
- this.runtime.delete(entity);
646
- }
647
-
648
- /**
649
- * Look up the per-entity runtime for an entity that has this
650
- * controller. Used by cross-system code (sensors system, future
651
- * ability-driven systems) to reach internal state without leaking
652
- * it onto the controller component itself.
653
- *
654
- * @param {number} entity
655
- * @returns {PerEntityRuntime|undefined} undefined if entity is not linked
656
- */
657
- getRuntime(entity) {
658
- return this.runtime.get(entity);
659
- }
660
-
661
- /**
662
- * Deterministic simulation step — L1 + L2 + L4.
663
- * @param {number} dt
664
- */
665
- fixedUpdate(dt) {
666
- const ecd = this.entityManager.dataset;
667
- if (ecd === null) return;
668
-
669
- this._currentDt = dt;
670
- ecd.traverseComponents(FirstPersonPlayerController, this._tickEntity, this);
671
- }
672
-
673
- /**
674
- * Variable-rate camera composition — L3.
675
- * @param {number} dt
676
- */
677
- update(dt) {
678
- const ecd = this.entityManager.dataset;
679
- if (ecd === null) return;
680
-
681
- this._currentRenderDt = dt;
682
- ecd.traverseComponents(FirstPersonPlayerController, this._composeEye, this);
683
- }
684
-
685
- /**
686
- * @private
687
- * @param {FirstPersonPlayerController} controller
688
- * @param {number} entity
689
- */
690
- _tickEntity(controller, entity) {
691
- const ecd = this.entityManager.dataset;
692
- const runtime = this.runtime.get(entity);
693
- if (runtime === undefined) return;
694
-
695
- const dt = this._currentDt;
696
- const cfg = controller.config;
697
- const intent = controller.intent;
698
- const state = controller.state;
699
- const sig = controller.signals;
700
-
701
- const bodyTransform = ecd.getComponent(entity, Transform64);
702
- if (bodyTransform === undefined) return;
703
-
704
- // Decay the mastery score's EMA. Doing this once per tick keeps the
705
- // score's time-window characteristic stable regardless of how many
706
- // evaluators fire (they each *record* a sample, the decay
707
- // independently ages all samples).
708
- controller.mastery.tick(dt);
709
-
710
- // -- L1.a: Derive the per-step turn rate ------------------------
711
- // Look (yaw/pitch) is consumed at RENDER rate in {@link _consumeLook} so
712
- // the camera tracks the mouse without 60Hz aliasing — consuming the
713
- // accumulated mouse delta once per fixed step beats against an unaligned
714
- // mouse report rate and makes a slow turn lurch (the reported snap).
715
- // fixedUpdate reads the latest yaw and derives the per-fixed-step turn
716
- // RATE for look-lean + the turn evaluators, from the yaw change since the
717
- // previous step (shortest-arc across the ±π wrap) — smooth and decoupled
718
- // from per-frame input jitter.
719
- let yawStep = runtime.bodyYaw - runtime.prevFixedYaw;
720
- if (yawStep > Math.PI) yawStep -= TWO_PI;
721
- else if (yawStep < -Math.PI) yawStep += TWO_PI;
722
- runtime.yawRateRadPerSec = yawStep / Math.max(dt, 1e-4);
723
- runtime.prevFixedYaw = runtime.bodyYaw;
724
-
725
- // Write body yaw back to the transform (pure yaw, no pitch on body) —
726
- // the collision sweep + sensors use it. The camera uses runtime.bodyYaw
727
- // directly, which _consumeLook keeps current at render rate.
728
- SCRATCH_Q_EULER.fromAxisAngle(Vector3.up, runtime.bodyYaw);
729
- bodyTransform.setRotation(
730
- SCRATCH_Q_EULER.x, SCRATCH_Q_EULER.y, SCRATCH_Q_EULER.z, SCRATCH_Q_EULER.w
731
- );
732
- bodyTransform.updateMatrix();
733
-
734
- // -- Shared flags. Computed BEFORE the ability tick so abilities
735
- // can read them. `isCrouchActive` is deliberately computed
736
- // AFTER the ability tick because `_resolveCrouchHeld` mutates
737
- // `runtime.prevCrouchHeld` — abilities like Slide need to see
738
- // the previous-tick value to detect a rising edge on the
739
- // crouch press.
740
- const isSprintIntent = intent.sprint && intent.move.y > 0.5 && state.grounded;
741
- const isBackwardIntent = intent.move.y < 0;
742
- runtime.sinYaw = Math.sin(runtime.bodyYaw);
743
- runtime.cosYaw = Math.cos(runtime.bodyYaw);
744
- // L2 observers read sinYaw/cosYaw as locals — destructure once.
745
- const { sinYaw, cosYaw } = runtime;
746
-
747
- // Age the ledge re-grab refractory window (dt-driven). Decremented
748
- // BEFORE the ability layer so LedgeGrab.canActivate sees the current
749
- // value; it gates the auto-catch for a beat after any dismount.
750
- if (runtime.ledgeRegrabCooldown > 0) {
751
- runtime.ledgeRegrabCooldown = Math.max(0, runtime.ledgeRegrabCooldown - dt);
752
- }
753
-
754
- // -- Ability layer: at most one active ability owns motion. The
755
- // set returns true when no ability owned the tick, in which
756
- // case base L1.b-h runs below; false means an ability fully
757
- // handled this tick (it called the system's helpers for any
758
- // standard work it wanted to keep, e.g. gravity).
759
- const runBaseLocomotion = controller.abilities.tick(
760
- controller, runtime, bodyTransform, runtime.sensors, dt, this,
761
- );
762
-
763
- // The ledge-pull-up request is set and consumed WITHIN abilities.tick
764
- // (LedgeGrab release → Mantle activation, same call). Clear it here as
765
- // a safety net so a pull-up that didn't activate a mantle can't leak
766
- // the flag into a later auto-mantle (which would let it vault a thin
767
- // wall it should have refused).
768
- runtime.ledgePullUpRequest = false;
769
-
770
- // Now resolve crouch (updates prevCrouchHeld) — used by base and L2.
771
- // Headroom-aware: force-keeps the player crouched under an overhang.
772
- const isCrouchActive = this._resolveCrouchHeld(controller, runtime, bodyTransform);
773
-
774
- if (runBaseLocomotion) {
775
- this._runBaseLocomotion(
776
- controller, runtime, bodyTransform, dt,
777
- isCrouchActive, isSprintIntent, isBackwardIntent,
778
- );
779
- }
780
-
781
- // (everything below this line runs every tick — L2 observers don't
782
- // care who owned motion)
783
-
784
- // -- L2.a: speed / moveMode ------------------------------------
785
- // -- L2.a: speed / moveMode ------------------------------------
786
- const horizSpeed = Math.hypot(runtime.velocityX, runtime.velocityZ);
787
- runtime.horizSpeed = horizSpeed;
788
- state.speed = horizSpeed;
789
- state.speedNormalized = clamp(horizSpeed / Math.max(cfg.motion.sprintSpeed, 1e-3), 0, 1);
790
-
791
- // Backwardness: 0 = moving forward (or sideways), 1 = moving directly
792
- // backward at the back-pedal speed ceiling. Derived from the actual
793
- // velocity (not the intent) so external knockback or stuck states
794
- // also register as "moving backward" and the gait wobble reflects it.
795
- //
796
- // Reference speed is the *achievable* backward max — walkSpeed ×
797
- // backwardSpeedFactor — NOT the sprint speed. Backward can never
798
- // reach sprint, so normalizing against sprint would cap backwardness
799
- // at ~0.3 and the wobble multipliers below would barely apply.
800
- const screenFwdVel = runtime.velocityX * sinYaw + runtime.velocityZ * cosYaw;
801
- const maxBackwardSpeed = Math.max(cfg.motion.walkSpeed * cfg.motion.backwardSpeedFactor, 1e-3);
802
- runtime.backwardness = clamp(-screenFwdVel / maxBackwardSpeed, 0, 1);
803
-
804
- // Locomotion mode is the *intent-driven* horizontal mode. Airborne
805
- // state is tracked separately on pose.actionState — they're
806
- // orthogonal facets (you can be Sprint+Airborne after a jump).
807
- const prevLocomotionMode = state.locomotionMode;
808
- if (isCrouchActive) {
809
- state.locomotionMode = FirstPersonLocomotionMode.Crouch;
810
- } else if (isSprintIntent && horizSpeed > 0.1) {
811
- state.locomotionMode = FirstPersonLocomotionMode.Sprint;
812
- } else if (horizSpeed > 0.1) {
813
- state.locomotionMode = FirstPersonLocomotionMode.Walk;
814
- } else {
815
- state.locomotionMode = FirstPersonLocomotionMode.Idle;
816
- }
817
-
818
- if (state.locomotionMode === FirstPersonLocomotionMode.Sprint
819
- && prevLocomotionMode !== FirstPersonLocomotionMode.Sprint) {
820
- sig.onSprintStart.send0();
821
- } else if (prevLocomotionMode === FirstPersonLocomotionMode.Sprint
822
- && state.locomotionMode !== FirstPersonLocomotionMode.Sprint) {
823
- sig.onSprintStop.send0();
824
- }
825
-
826
- // -- L2.b: Exertion --------------------------------------------
827
- // Heavier bodies tire faster — sprint rise scales with massRatios.exertionRiseScale.
828
- const exertionRise = isSprintIntent
829
- ? cfg.exertion.sprintRiseRate * runtime.massRatios.exertionRiseScale
830
- : 0;
831
- const exertionFall = exertionRise > 0 ? 0 : cfg.exertion.idleDecayRate;
832
- state.exertion = clamp(state.exertion + (exertionRise - exertionFall) * dt, 0, 1);
833
-
834
- // -- L2.c: Breath ----------------------------------------------
835
- // breathRate and breathAmplitude lag exertion through separate
836
- // exponential decays. Rate hangs around longer than amplitude.
837
- const metabolicRate = lerp(cfg.breath.rateRestHz, cfg.breath.rateMaxHz, state.exertion);
838
- const targetAmp = lerp(cfg.breath.amplitudeRestM, cfg.breath.amplitudeMaxM, state.exertion);
839
-
840
- // Locomotor-respiratory coupling — see math/computeLRCBreathRate.
841
- // The pure function is unit-tested; this site just provides inputs.
842
- //
843
- // Gait is gated on a "feet strike the ground" posture (Stand /
844
- // Crouch). Prone (slide) and Hang (ledge-grab) have no stride —
845
- // the body's feet are not making contact in a walking pattern,
846
- // so stride frequency drops to zero and downstream gait
847
- // signals (footsteps, bob intensity) go quiet.
848
- const feetStriking = state.posture === FirstPersonPosture.Stand
849
- || state.posture === FirstPersonPosture.Crouch;
850
- const strideFreqHz = feetStriking && state.grounded && horizSpeed > cfg.bob.minStepSpeed
851
- ? cfg.bob.stepFreqAtWalk * Math.pow(
852
- Math.max(horizSpeed, 1e-3) / Math.max(cfg.motion.walkSpeed, 1e-3),
853
- cfg.bob.stepFreqExp,
854
- )
855
- : 0;
856
- const targetRate = computeLRCBreathRate(
857
- metabolicRate,
858
- strideFreqHz,
859
- state.exertion,
860
- cfg.breath.locomotorCouplingMax,
861
- cfg.breath.couplingMinStrideFreqHz,
862
- );
863
- state.breathRateHz = exponentialApproach(state.breathRateHz, targetRate, cfg.exertion.rateDecayHalfLife, dt);
864
- state.breathAmplitudeM = exponentialApproach(state.breathAmplitudeM, targetAmp, cfg.exertion.ampDecayHalfLife, dt);
865
-
866
- runtime.prevBreathPhase = state.breathPhase;
867
- state.breathPhase += state.breathRateHz * dt;
868
- state.breathPhase -= Math.floor(state.breathPhase); // wrap [0,1)
869
-
870
- // Breath edge detection — inhale at 0.25, exhale at 0.75
871
- if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.25)) {
872
- sig.onBreathIn.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
873
- }
874
- if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.75)) {
875
- sig.onBreathOut.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
876
- }
877
-
878
- // -- L2.d: Stride ----------------------------------------------
879
- // strideFreqHz computed above in the breath block; reused here.
880
- runtime.prevStridePhase = state.stridePhase;
881
- if (strideFreqHz > 0) {
882
- // 1 full stride cycle = 2 footfalls; phase advances at freq/2 of cycle
883
- state.stridePhase += (strideFreqHz * 0.5) * dt;
884
- state.stridePhase -= Math.floor(state.stridePhase);
885
- }
886
- // Footstep on phase wraparound past 0 (R) or past 0.5 (L). Same
887
- // posture gate as stride advance — feet must be striking.
888
- if (feetStriking && state.grounded && horizSpeed > cfg.bob.minStepSpeed) {
889
- const fireFootstep = () => {
890
- state.stepCount++;
891
- const side = runtime.nextFootSide;
892
- runtime.nextFootSide = side === "R" ? "L" : "R";
893
- // The foot that just fired is now the one bearing weight
894
- // through the upcoming half-stride. Drives lateral-bob sign.
895
- runtime.standingFoot = side;
896
- sig.onFootStep.send1({ side, speed: horizSpeed, surfaceTag: state.surfaceTag });
897
- // Kick the vertical impact spring DOWNWARD. The kick magnitude
898
- // is the per-step desired peak dip × impactKickMultiplier; the
899
- // multiplier is empirical (depends on impact spring params) so
900
- // that "verticalAmpAtWalk" still corresponds approximately to
901
- // the visible peak dip depth. Scaled by bobIntensity so a
902
- // mid-deceleration footstep doesn't deliver a full-strength
903
- // impulse.
904
- const massBoost = (cfg.body.mass - 80) * cfg.bob.ampMassScale;
905
- const ampVMult = 1 + (cfg.bob.backwardVerticalAmpFactor - 1) * runtime.backwardness;
906
- const peakDip = (cfg.bob.verticalAmpAtWalk + massBoost) * runtime.bobIntensitySpring.value * ampVMult;
907
- runtime.verticalImpactSpring.kick(-peakDip * cfg.bob.impactKickMultiplier);
908
- };
909
- if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0)) {
910
- fireFootstep();
911
- }
912
- if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0.5)) {
913
- fireFootstep();
914
- }
915
- }
916
-
917
- // -- L2.d.bob-intensity & impact -------------------------------
918
- // Smoothed bob amplitude envelope: when the player starts/stops
919
- // moving the visible bob fades in/out rather than cutting on/off.
920
- // Target = the "natural" amp scale (max of speed and backwardness)
921
- // while grounded, zero while airborne so the bob disappears mid-jump.
922
- const naturalBobIntensity = Math.max(state.speedNormalized, runtime.backwardness);
923
- // Bob fades to zero whenever feet aren't striking (airborne, or
924
- // Prone/Hang posture). The verticalImpactSpring (separate
925
- // channel) still carries any entry/landing kicks through to the
926
- // camera, but no recurring step bob.
927
- const targetBobIntensity = (state.grounded && feetStriking) ? naturalBobIntensity : 0;
928
- runtime.bobIntensitySpring.stepTo(targetBobIntensity, cfg.bob.intensityHalfLife, 1.0, dt);
929
-
930
- // Vertical impact spring — damped decay toward 0, with the under-
931
- // damped overshoot that produces the recovery + leg-push curve.
932
- runtime.verticalImpactSpring.stepTo(0, cfg.bob.impactSpringHalfLife, cfg.bob.impactSpringZeta, dt);
933
-
934
- // Sprint posture — head pitches forward as commitment to sprint
935
- // builds. Driven by "sprintness" — how much of the gap between
936
- // walk and sprint speed the player is *currently* in (0..1). The
937
- // pitch target is multiplied by sprintness, then critically damped.
938
- // Only applies while grounded — pitching into airborne motion looks weird.
939
- const sprintness = clamp(
940
- (state.speed - cfg.motion.walkSpeed)
941
- / Math.max(cfg.motion.sprintSpeed - cfg.motion.walkSpeed, 1e-3),
942
- 0, 1,
943
- );
944
- const targetSprintPitch = state.grounded
945
- ? cfg.posture.sprintForwardPitchDeg * DEG_TO_RAD * sprintness
946
- : 0;
947
- runtime.sprintPostureSpring.stepTo(
948
- targetSprintPitch,
949
- cfg.posture.sprintForwardPitchHalfLife,
950
- 1.0, dt,
951
- );
952
- runtime.sprintness = sprintness;
953
-
954
- // Head droop — exertion drives a subtle additional forward pitch.
955
- // Combines with sprintPostureSpring (sprint = head down to commit)
956
- // so a fatigued sprinter has BOTH effects layered.
957
- const targetDroopRad = cfg.exertion.headDroopAtMaxDeg * DEG_TO_RAD * state.exertion;
958
- runtime.headDroopSpring.stepTo(targetDroopRad, cfg.exertion.headDroopHalfLife, 1.0, dt);
959
-
960
- // -- L2.e: Posture → eye height --------------------------------
961
- // Posture is set by whichever layer owned motion this tick: base
962
- // writes Stand / Crouch from isCrouchActive (see end of
963
- // _runBaseLocomotion); active abilities write Prone (Slide) or
964
- // Hang (LedgeGrab) in their tick. Mapping is one switch — adding
965
- // a new posture is one enum value + one case.
966
- let targetEyeH;
967
- switch (state.posture) {
968
- case FirstPersonPosture.Prone: targetEyeH = cfg.body.proneHeight; break;
969
- case FirstPersonPosture.Crouch: targetEyeH = cfg.body.crouchHeight; break;
970
- case FirstPersonPosture.Hang: targetEyeH = cfg.body.height; break;
971
- case FirstPersonPosture.Stand:
972
- default: targetEyeH = cfg.body.height; break;
973
- }
974
- const crouchHalfLife = cfg.crouch.transitionTime / 4; // halfLife is ~quarter of full transition
975
- runtime.eyeHeightSpring.stepTo(targetEyeH, crouchHalfLife, 1.0, dt);
976
- state.eyeHeight = runtime.eyeHeightSpring.value;
977
-
978
- if (isCrouchActive !== state.crouchActive) {
979
- state.crouchActive = isCrouchActive;
980
- if (isCrouchActive) {
981
- sig.onCrouchEnter.send0();
982
- // Impulse: dropping into a crouch grips the knees. Small
983
- // bump — we don't want crouch-spamming to instantly tire.
984
- state.exertion = clamp(
985
- state.exertion + cfg.exertion.crouchEnterRise * runtime.massRatios.exertionRiseScale,
986
- 0, 1,
987
- );
988
- } else {
989
- sig.onCrouchExit.send0();
990
- }
991
- }
992
-
993
- // -- L2.f: Lean spring → camera roll ---------------------------
994
- // The TARGET for this tick was written by whichever layer owned
995
- // motion: base writes the lat-accel + look-lean derived value at
996
- // the end of _runBaseLocomotion; abilities override (WallRun
997
- // tilts toward the wall; Slide / LedgeGrab / Mantle force zero).
998
- // L2.f is now a flat spring-step + commit — no branching, no
999
- // null sentinel.
1000
- runtime.prevVelocityX = runtime.velocityX;
1001
- runtime.prevVelocityZ = runtime.velocityZ;
1002
- runtime.leanSpring.stepTo(runtime.leanTargetRad, cfg.lean.spring.halfLife, cfg.lean.spring.zeta, dt);
1003
- state.leanRollRad = runtime.leanSpring.value;
1004
-
1005
- // -- L2.g: Land spring decay (drives the landing recovery dip) -
1006
- // Target is 0; under-damped (cfg zeta < 1) so it rings.
1007
- runtime.landSpring.stepTo(0, cfg.landing.recovery.spring.halfLife, cfg.landing.recovery.spring.zeta, dt);
1008
-
1009
- // -- L2.h: Publish pose channels --------------------------------
1010
- this._publishPose(controller, runtime, bodyTransform);
1011
-
1012
- // -- L2.h.1: Say that the body moved ----------------------------
1013
- // The yaw write above and the mover's write into the translation both land straight on the
1014
- // component, and a Transform64 announces nothing on its own. One event here, at the point
1015
- // where the pose is final, covers every writer in the tick and covers them once - which is
1016
- // what wakes the mesh placement, the audio emitter and anything else that follows a player.
1017
- t64_announce_change(ecd, entity);
1018
-
1019
- // -- L2.i: Sync collider shape to posture -----------------------
1020
- // All posture-writers (base locomotion + any active ability)
1021
- // have run for this tick. Swap the collider's shape to the
1022
- // pre-built capsule matching the final posture so downstream
1023
- // physics queries (move-and-slide cast, sensors, overlap from
1024
- // outside) see the right volume. No-op when posture is
1025
- // unchanged.
1026
- this._syncColliderShape(runtime, state.posture);
1027
- }
1028
-
1029
- /**
1030
- * @private
1031
- * @param {FirstPersonPlayerController} controller
1032
- * @param {PerEntityRuntime} runtime
1033
- * @returns {boolean}
1034
- */
1035
- /**
1036
- * Swap {@link Collider.shape} to the pre-built capsule that matches
1037
- * the player's current posture. Cheap — just a reference swap when
1038
- * the posture changed, no-op otherwise. The pre-built shapes live
1039
- * on the runtime (see {@link PerEntityRuntime.colliderShapeStand}
1040
- * etc.) so this method allocates nothing per tick.
1041
- *
1042
- * Hang posture reuses Stand: the player's body is full-extent,
1043
- * hanging below the ledge — the rig handles the arms-up animation,
1044
- * but the collision volume is unchanged. If a game ever wants a
1045
- * narrower hang silhouette (e.g. wedging into a chimney) it can
1046
- * add a `colliderShapeHang` and route here.
1047
- *
1048
- * @private
1049
- */
1050
- _syncColliderShape(runtime, posture) {
1051
- if (posture === runtime.lastPosture) return;
1052
- let next;
1053
- if (posture === FirstPersonPosture.Crouch) {
1054
- next = runtime.colliderShapeCrouch;
1055
- } else if (posture === FirstPersonPosture.Prone) {
1056
- next = runtime.colliderShapeProne;
1057
- } else {
1058
- // Stand and Hang share the full-extent capsule.
1059
- next = runtime.colliderShapeStand;
1060
- }
1061
- runtime.collider.shape = next;
1062
- runtime.lastPosture = posture;
1063
- }
1064
-
1065
- _resolveCrouchHeld(controller, runtime, bodyTransform) {
1066
- const cfg = controller.config;
1067
- const intent = controller.intent;
1068
- const state = controller.state;
1069
-
1070
- let wantsCrouch;
1071
- if (cfg.crouch.mode === "toggle") {
1072
- // Edge: rising press flips the latch
1073
- if (intent.crouch && !runtime.prevCrouchHeld) {
1074
- runtime.crouchLatched = !runtime.crouchLatched;
1075
- }
1076
- runtime.prevCrouchHeld = intent.crouch;
1077
- wantsCrouch = runtime.crouchLatched;
1078
- } else {
1079
- // "hold" mode
1080
- runtime.prevCrouchHeld = intent.crouch;
1081
- wantsCrouch = intent.crouch;
1082
- }
1083
-
1084
- // Headroom override — block UN-crouching under an overhang. A player who
1085
- // is ALREADY low (crouched or prone) and wants to stand is force-kept
1086
- // crouched while the Stand collider won't fit: letting it grow into the
1087
- // overhang would have the next tick's depenetration shove the player out
1088
- // the bottom, through the floor. `crouchBlocked` records this for HUD /
1089
- // camera.
1090
- //
1091
- // Crucially this fires ONLY when already low. A STANDING player who walks
1092
- // under a low ceiling is NEVER auto-pushed down into a crouch — their
1093
- // full-height capsule simply can't fit, so the mover blocks their forward
1094
- // motion instead. "If I can't clear it, I don't move forward." Dropping
1095
- // posture is the player's choice (press crouch), not the ceiling's.
1096
- const alreadyLow = runtime.lastPosture === FirstPersonPosture.Crouch
1097
- || runtime.lastPosture === FirstPersonPosture.Prone;
1098
- if (!wantsCrouch && alreadyLow) {
1099
- const growDelta = runtime.standTop - runtime.crouchTop;
1100
- if (!this._hasHeadroomToGrow(runtime, bodyTransform, runtime.colliderShapeCrouch, growDelta)) {
1101
- state.crouchBlocked = true;
1102
- return true;
1103
- }
1104
- }
1105
- state.crouchBlocked = false;
1106
- return wantsCrouch;
1107
- }
1108
-
1109
- /**
1110
- * Headroom probe for a posture change that GROWS the collider.
1111
- * Sweeps `fromShape` (the shorter posture's capsule) straight up by
1112
- * `growDelta` (the height the collider would gain). A hit means an
1113
- * overhang occupies the space the taller posture needs, so the grow
1114
- * is blocked. Returns true (clear) when no physics is wired or the
1115
- * grow is non-positive.
1116
- *
1117
- * Why an up-cast rather than an overlap-test of the taller shape:
1118
- * overlap would false-positive on a wall *beside* the player. The
1119
- * up-cast sweeps parallel to (and, per the mover's skin clearance,
1120
- * clear of) lateral walls so they aren't hit; the floor is below
1121
- * the rising probe; the player's own body is filtered out. Only
1122
- * genuine overhead geometry blocks the grow. Relies on the player
1123
- * resting at `floor+skin` (the mover's stick-to-ground), so the
1124
- * probe's bottom starts clear of the floor.
1125
- *
1126
- * @private
1127
- */
1128
- _hasHeadroomToGrow(runtime, bodyTransform, fromShape, growDelta) {
1129
- if (this.physicsSystem === null || growDelta <= 1e-4) return true;
1130
- const ownCollider = runtime.collider;
1131
- const filter = (_e, c) => c !== ownCollider;
1132
- const ray = this._postureRay;
1133
- ray.setOrigin(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
1134
- ray.setDirection(0, 1, 0);
1135
- ray.tMax = growDelta;
1136
- return !this.physicsSystem.shapeCast(ray, fromShape, bodyTransform.rotation, this._postureHit, filter);
1137
- }
1138
-
1139
- /**
1140
- * Jump finite-state-machine: button-edge detection, buffer + coyote
1141
- * grace, anticipation timer, impulse on completion. Variable-height
1142
- * cut is captured here as a `state.isVariableJumpCut` flag that the
1143
- * gravity step in `_integrateVerticalAndResolveGround` consumes.
1144
- *
1145
- * @private
1146
- * @param {FirstPersonPlayerController} controller
1147
- * @param {PerEntityRuntime} runtime
1148
- * @param {Transform64} bodyTransform
1149
- * @param {number} dt
1150
- */
1151
- _advanceJumpFsm(controller, runtime, bodyTransform, dt) {
1152
- const cfg = controller.config;
1153
- const intent = controller.intent;
1154
- const state = controller.state;
1155
- const sig = controller.signals;
1156
-
1157
- const jumpPressedEdge = intent.jump && !runtime.prevJumpHeld;
1158
- const jumpReleasedEdge = !intent.jump && runtime.prevJumpHeld;
1159
- runtime.prevJumpHeld = intent.jump;
1160
-
1161
- if (jumpPressedEdge) {
1162
- state.jumpBufferRemaining = cfg.jump.bufferTime;
1163
- }
1164
- state.jumpBufferRemaining = Math.max(0, state.jumpBufferRemaining - dt);
1165
-
1166
- const canJumpNow =
1167
- (state.grounded || state.timeSinceGrounded < cfg.jump.coyoteTime)
1168
- && state.jumpBufferRemaining > 0
1169
- && !state.inJumpAnticipation
1170
- && !runtime.midJump;
1171
-
1172
- if (canJumpNow) {
1173
- // Begin anticipation — squash; impulse fires after duration elapses
1174
- state.inJumpAnticipation = true;
1175
- runtime.anticipationRemaining = cfg.jump.anticipation.duration;
1176
- state.jumpBufferRemaining = 0; // claimed
1177
- }
1178
-
1179
- // Variable-height cut: only valid during ascent, post-launch.
1180
- if (jumpReleasedEdge && runtime.midJump && runtime.velocityY > 0) {
1181
- state.isVariableJumpCut = true;
1182
- }
1183
-
1184
- // Anticipation timer; impulse on completion.
1185
- //
1186
- // Anticipation completes regardless of grounded state. The reason
1187
- // we DON'T cancel on `!grounded`: the canonical coyote-jump path
1188
- // depends on it. The player walks off a ledge (grounded → false),
1189
- // presses jump within the coyote window, canJumpNow accepts on
1190
- // the coyote branch and starts anticipation. If we cancelled
1191
- // anticipation here on !grounded, the impulse would never fire
1192
- // and "coyote time" would be silently dead — the FSM's own next-
1193
- // statement contradicting the canJumpNow gate three lines up.
1194
- //
1195
- // The same logic handles the rug-pull case (player on a moving
1196
- // platform that slides out mid-anticipation): the player
1197
- // committed to the jump, they get the jump. A future
1198
- // knockback / stagger system can explicitly clear
1199
- // inJumpAnticipation if it wants to override that commitment.
1200
- if (state.inJumpAnticipation) {
1201
- runtime.anticipationRemaining -= dt;
1202
- if (runtime.anticipationRemaining <= 0) {
1203
- // Mastery: gather a multiplier from all evaluators
1204
- // registered for JumpImpulse. Default (no evaluators)
1205
- // returns 1.0 → unchanged behaviour.
1206
- const masteryMul = controller.mastery.evaluate(
1207
- DecisionPoint.JumpImpulse, controller, runtime,
1208
- );
1209
- runtime.velocityY = runtime.jumpInitialVy * masteryMul;
1210
- runtime.midJump = true;
1211
- runtime.apexFired = false;
1212
- runtime.peakAltitude = bodyTransform.translation_y;
1213
- state.inJumpAnticipation = false;
1214
- state.isVariableJumpCut = false;
1215
- state.isAscending = true;
1216
- state.exertion = clamp(
1217
- state.exertion + cfg.exertion.jumpRise * runtime.massRatios.exertionRiseScale,
1218
- 0, 1,
1219
- );
1220
-
1221
- sig.onJumpStart.send1({ peakHeight: cfg.jump.peakHeight });
1222
- // NOTE: onLeaveGround is fired by the motor (_onLeaveGround)
1223
- // when the move actually breaks ground contact this same tick —
1224
- // with reason "jump" since midJump is now true. Firing it here
1225
- // too double-sent the signal on every grounded jump (and a
1226
- // spurious second "jump" leave on a coyote jump, which already
1227
- // left ground on the walk-off). Let the motor own it.
1228
- }
1229
- }
1230
- }
1231
-
1232
- /**
1233
- * The vertical + collision phase of one fixed step: apply gravity
1234
- * (the motor), resolve the move against the world, then detect the
1235
- * jump apex. Collision resolution is the {@link KinematicMover} when
1236
- * a physics layer is present; otherwise a lightweight flat-ground
1237
- * integrator for headless / unit-test scenes with no collidable
1238
- * geometry. Land / leave-ground events flow identically from either.
1239
- *
1240
- * @private
1241
- * @param {FirstPersonPlayerController} controller
1242
- * @param {PerEntityRuntime} runtime
1243
- * @param {Transform64} bodyTransform
1244
- * @param {number} dt
1245
- */
1246
- _integrateVerticalAndResolveGround(controller, runtime, bodyTransform, dt) {
1247
- this._applyGravity(controller, runtime, dt);
1248
- this._resolveMotion(controller, runtime, bodyTransform, dt);
1249
- this._detectJumpApex(controller, runtime, bodyTransform);
1250
- }
1251
-
1252
- /**
1253
- * Resolve the current `runtime.velocity*` against the world — the
1254
- * physics-backed {@link KinematicMover} (recover + sweep-and-slide +
1255
- * ground-categorize) when a PhysicsSystem is present, else the no-physics
1256
- * flat-ground fallback. Gravity is NOT applied here: the standard path
1257
- * applies it in {@link _applyGravity} just before, and abilities with a
1258
- * non-standard vertical model (WallRun's reduced gravity) apply their own
1259
- * and then call this directly — so motion routes through one motor for
1260
- * everyone (sweep-and-slide, anti-tunnel, land/leave events) rather than
1261
- * a bespoke `position._add` + hand-rolled ground-catch.
1262
- * @private
1263
- */
1264
- _resolveMotion(controller, runtime, bodyTransform, dt) {
1265
- if (this.physicsSystem !== null) {
1266
- this._moveViaMover(controller, runtime, bodyTransform, dt);
1267
- } else {
1268
- this._moveFlatGround(controller, runtime, bodyTransform, dt);
1269
- }
1270
- }
1271
-
1272
- /**
1273
- * Motor: integrate gravity into `velocityY` with the fall / variable-
1274
- * cut multipliers. The mover never invents motion, so gravity lives
1275
- * here (callable by abilities that want standard gravity too).
1276
- * @private
1277
- */
1278
- _applyGravity(controller, runtime, dt) {
1279
- const cfg = controller.config;
1280
- const state = controller.state;
1281
- let gMag = runtime.gravity;
1282
- if (runtime.velocityY <= 0) {
1283
- gMag *= cfg.jump.fallGravityMult;
1284
- state.isAscending = false;
1285
- } else if (state.isVariableJumpCut) {
1286
- gMag *= cfg.jump.cutGravityMult;
1287
- }
1288
- runtime.velocityY -= gMag * dt;
1289
- }
1290
-
1291
- /**
1292
- * Resolve the move against the world via {@link KinematicMover}
1293
- * (recover + unified sweep-and-slide + ground-categorize), then map
1294
- * the result back to controller state and land / leave events.
1295
- * @private
1296
- */
1297
- _moveViaMover(controller, runtime, bodyTransform, dt) {
1298
- const state = controller.state;
1299
-
1300
- if (this._mover === null) {
1301
- this._mover = new KinematicMover(this.physicsSystem, this.entityManager.dataset);
1302
- }
1303
-
1304
- // Fall speed captured BEFORE the move — the mover clips velocityY
1305
- // to ~0 on contact, so the land event needs the pre-move value.
1306
- const impactVy = -runtime.velocityY;
1307
-
1308
- const v = this._moverVelocity;
1309
- v.set(runtime.velocityX, runtime.velocityY, runtime.velocityZ);
1310
-
1311
- const ownCollider = runtime.collider;
1312
- const filter = (_e, c) => c !== ownCollider;
1313
-
1314
- const result = this._mover.move(
1315
- bodyTransform.translation,
1316
- bodyTransform.rotation,
1317
- runtime.collider.shape,
1318
- v, dt, filter,
1319
- );
1320
-
1321
- // Read back corrected horizontal velocity. Vertical follows the
1322
- // control policy: zero on ground (the slope-climb the mover
1323
- // produced is a position effect, not accumulated speed), keep the
1324
- // mover-clipped value in the air (captures ceiling bonks).
1325
- runtime.velocityX = v.x;
1326
- runtime.velocityZ = v.z;
1327
- runtime.velocityY = result.grounded ? 0 : v.y;
1328
-
1329
- const wasGrounded = state.grounded;
1330
- if (result.grounded) {
1331
- if (!wasGrounded) this._onLand(controller, runtime, impactVy);
1332
- state.grounded = true;
1333
- state.groundNormal.copy(result.groundNormal);
1334
- state.verticalSpeed = 0;
1335
- state.airborneTime = 0;
1336
- state.timeSinceGrounded = 0;
1337
- } else {
1338
- if (wasGrounded) this._onLeaveGround(controller, runtime, bodyTransform);
1339
- state.grounded = false;
1340
- state.verticalSpeed = runtime.velocityY;
1341
- state.airborneTime += dt;
1342
- state.timeSinceGrounded += dt;
1343
- state.fallDistance += Math.max(0, -runtime.velocityY * dt);
1344
- }
1345
- }
1346
-
1347
- /**
1348
- * No-physics fallback: integrate the velocity directly (there's no
1349
- * collidable geometry to sweep against) and resolve the floor from
1350
- * the built-in flat ground and/or the host `groundResolver`. For
1351
- * headless scenes and control-layer unit tests. With both ground
1352
- * sources off, an external physics layer is expected to own
1353
- * `state.grounded`; we only advance timers.
1354
- * @private
1355
- */
1356
- _moveFlatGround(controller, runtime, bodyTransform, dt) {
1357
- const state = controller.state;
1358
-
1359
- // translation lands in the matrix as it is written, so no updateMatrix is owed here
1360
- bodyTransform.setTranslation(
1361
- bodyTransform.translation_x + runtime.velocityX * dt,
1362
- bodyTransform.translation_y + runtime.velocityY * dt,
1363
- bodyTransform.translation_z + runtime.velocityZ * dt
1364
- );
1365
-
1366
- if (this.useBuiltInFlatGround || this.groundResolver !== null) {
1367
- // Effective ground = max(built-in flat ground, resolver).
1368
- let testY = this.useBuiltInFlatGround ? this.groundY : Number.NEGATIVE_INFINITY;
1369
- if (this.groundResolver !== null) {
1370
- const resolved = this.groundResolver(
1371
- bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z,
1372
- );
1373
- if (resolved !== null && resolved > testY) testY = resolved;
1374
- }
1375
- const haveGround = testY !== Number.NEGATIVE_INFINITY;
1376
- if (haveGround && bodyTransform.translation_y <= testY) {
1377
- bodyTransform.setTranslation(bodyTransform.translation_x, testY, bodyTransform.translation_z);
1378
- if (!state.grounded) this._onLand(controller, runtime, -runtime.velocityY);
1379
- state.grounded = true;
1380
- state.groundNormal.set(0, 1, 0);
1381
- state.verticalSpeed = 0;
1382
- runtime.velocityY = 0;
1383
- state.airborneTime = 0;
1384
- state.timeSinceGrounded = 0;
1385
- } else {
1386
- if (state.grounded) this._onLeaveGround(controller, runtime, bodyTransform);
1387
- state.grounded = false;
1388
- state.verticalSpeed = runtime.velocityY;
1389
- state.airborneTime += dt;
1390
- state.timeSinceGrounded += dt;
1391
- state.fallDistance += Math.max(0, -runtime.velocityY * dt);
1392
- }
1393
- } else {
1394
- // External physics maintains state.grounded; just track timers.
1395
- if (state.grounded) {
1396
- state.timeSinceGrounded = 0;
1397
- state.airborneTime = 0;
1398
- } else {
1399
- state.timeSinceGrounded += dt;
1400
- state.airborneTime += dt;
1401
- }
1402
- }
1403
- }
1404
-
1405
- /**
1406
- * Landing transition: dip / impact springs, exertion impulse, flag
1407
- * resets, and the `onLand` signal (fired LAST so handlers see fully-
1408
- * reacted state). Shared by both move paths. `impactVy` is the
1409
- * downward speed at touchdown (positive).
1410
- * @private
1411
- */
1412
- _onLand(controller, runtime, impactVy) {
1413
- const cfg = controller.config;
1414
- const state = controller.state;
1415
- const kind = impactVy >= cfg.landing.hardThreshold ? "hard" : "soft";
1416
-
1417
- const massScaledDip = impactVy * cfg.landing.recovery.dipPerVy
1418
- * runtime.massRatios.landingDipScale;
1419
- const dip = clamp(massScaledDip, 0, cfg.landing.recovery.dipMax);
1420
- runtime.landSpring.settle(-dip);
1421
-
1422
- const landImpulse = clamp(
1423
- impactVy * cfg.exertion.landImpulsePerVy * runtime.massRatios.exertionRiseScale,
1424
- 0, cfg.exertion.landImpulseMax,
1425
- );
1426
- state.exertion = clamp(state.exertion + landImpulse, 0, 1);
1427
-
1428
- runtime.midJump = false;
1429
- state.isAscending = false;
1430
- state.isVariableJumpCut = false;
1431
- state.fallDistance = 0;
1432
-
1433
- controller.signals.onLand.send1({ verticalSpeed: impactVy, kind });
1434
- }
1435
-
1436
- /**
1437
- * Leave-ground transition: `onLeaveGround` signal + takeoff bookkeeping.
1438
- * Shared by both move paths.
1439
- * @private
1440
- */
1441
- _onLeaveGround(controller, runtime, bodyTransform) {
1442
- controller.signals.onLeaveGround.send1({ reason: runtime.midJump ? "jump" : "fall" });
1443
- runtime.takeoffVy = runtime.velocityY;
1444
- runtime.peakAltitude = bodyTransform.translation_y;
1445
- }
1446
-
1447
- /**
1448
- * Fire `onJumpApex` once, when an in-progress jump stops rising.
1449
- * @private
1450
- */
1451
- _detectJumpApex(controller, runtime, bodyTransform) {
1452
- if (runtime.midJump && !runtime.apexFired) {
1453
- if (bodyTransform.translation_y > runtime.peakAltitude) {
1454
- runtime.peakAltitude = bodyTransform.translation_y;
1455
- } else if (runtime.velocityY <= 0) {
1456
- controller.signals.onJumpApex.send0();
1457
- runtime.apexFired = true;
1458
- }
1459
- }
1460
- }
1461
-
1462
- /**
1463
- * Run the base (no-ability) L1 locomotion phases: speed selection,
1464
- * desired-velocity computation, accel/decel, jump FSM, gravity, body
1465
- * integration, ground resolution. Only invoked when no ability owns
1466
- * the tick (see {@link AbilitySet.tick}).
1467
- *
1468
- * @private
1469
- * @param {FirstPersonPlayerController} controller
1470
- * @param {PerEntityRuntime} runtime
1471
- * @param {Transform64} bodyTransform
1472
- * @param {number} dt
1473
- * @param {boolean} isCrouchActive
1474
- * @param {boolean} isSprintIntent
1475
- * @param {boolean} isBackwardIntent
1476
- */
1477
- _runBaseLocomotion(controller, runtime, bodyTransform, dt,
1478
- isCrouchActive, isSprintIntent, isBackwardIntent) {
1479
- const cfg = controller.config;
1480
- const intent = controller.intent;
1481
- const state = controller.state;
1482
-
1483
- // -- L1.b: Speed selection ------------------------------------
1484
- let targetSpeed;
1485
- if (isCrouchActive) {
1486
- targetSpeed = cfg.motion.crouchSpeed;
1487
- } else if (isSprintIntent) {
1488
- targetSpeed = cfg.motion.sprintSpeed;
1489
- } else {
1490
- targetSpeed = cfg.motion.walkSpeed;
1491
- }
1492
- if (isBackwardIntent) {
1493
- targetSpeed *= cfg.motion.backwardSpeedFactor;
1494
- }
1495
-
1496
- // Airborne momentum floor — preserve whatever horizontal speed
1497
- // the player carried into the jump. Without this, a sprint
1498
- // jump (9 m/s) decays toward walkSpeed (4.5 m/s) at
1499
- // airAccel = 14 m/s², losing all sprint momentum in ~0.32 s —
1500
- // well before the apex of a `peakHeight = 1.8 m` jump arc. The
1501
- // air-control band (Mirror's Edge, Titanfall, modern CoD) and
1502
- // the long-jump biomechanics literature both say the same
1503
- // thing: there's no thrust source in flight, so horizontal
1504
- // velocity is conserved across the arc and air "control" is
1505
- // for steering (direction) — not for changing speed magnitude.
1506
- // Raising the target to the current speed makes `stepTowards`
1507
- // a no-op when the player keeps pressing forward, while
1508
- // releasing the stick still lets `airAccel` decelerate to
1509
- // `walkSpeed` (the user CAN bleed off speed, just not have it
1510
- // bled off for them).
1511
- if (!state.grounded) {
1512
- const horizSpeed = Math.hypot(runtime.velocityX, runtime.velocityZ);
1513
- if (horizSpeed > targetSpeed) targetSpeed = horizSpeed;
1514
- }
1515
-
1516
- // -- L1.c: Move intent → desired horizontal velocity ----------
1517
- // screen_forward(θ) = ( sin θ, 0, cos θ )
1518
- // screen_right (θ) = (-cos θ, 0, sin θ )
1519
- const { sinYaw, cosYaw } = runtime;
1520
- const mvX = intent.move.x;
1521
- const mvY = intent.move.y;
1522
- const mvMag = Math.hypot(mvX, mvY);
1523
- const nmvX = mvMag > 1 ? mvX / mvMag : mvX;
1524
- const nmvY = mvMag > 1 ? mvY / mvMag : mvY;
1525
- const desiredVx = sinYaw * nmvY + -cosYaw * nmvX;
1526
- const desiredVz = cosYaw * nmvY + sinYaw * nmvX;
1527
- const desiredHorizontalVx = desiredVx * targetSpeed;
1528
- const desiredHorizontalVz = desiredVz * targetSpeed;
1529
-
1530
- // -- L1.d: Accel/decel toward desired velocity ----------------
1531
- //
1532
- // Three regimes — air control, grounded decel-to-stop, grounded
1533
- // accel-to-target — each with its own model:
1534
- //
1535
- // • Air control: constant-rate `stepTowards`. No ground
1536
- // reaction force in flight; air control is a steering
1537
- // budget, not a thrust curve. Constant accel matches the
1538
- // player mental model of "fixed mid-air authority".
1539
- //
1540
- // • Grounded decel (no intent): constant-rate `stepTowards`
1541
- // toward zero. Friction is approximately constant for a
1542
- // biped on level ground — Coulomb friction. Faster than
1543
- // accel because the body's own resistance + active
1544
- // decel-foot-plants combine into a sharper deceleration.
1545
- //
1546
- // • Grounded accel (intent active): mono-exponential
1547
- // approach (Hill 1927; Furusawa-Hill 1928). dv/dt is
1548
- // proportional to (v_target − v), so accel is highest at
1549
- // low speed and tapers as v approaches v_target. Matches
1550
- // human sprint biomechanics — modern sprint-profiling
1551
- // work (Morin & Samozino 2016) fits this same mono-exp
1552
- // curve to empirical force-plate data.
1553
- //
1554
- // The mass + mastery + backward scalars compose multiplicatively
1555
- // on the EFFECTIVE half-life (heavier ⇒ longer half-life ⇒
1556
- // slower ramp; mastery accel-bonus ⇒ shorter half-life ⇒
1557
- // faster ramp). See FirstPersonPlayerControllerConfig.js's
1558
- // `groundAccelHalfLife` doc for the literature and the
1559
- // SprintAcceleration.spec.js for the model assertions.
1560
- const intentLen = Math.hypot(nmvX, nmvY);
1561
- if (!state.grounded) {
1562
- const maxStep = cfg.motion.airAccel * dt;
1563
- runtime.velocityX = stepTowards(runtime.velocityX, desiredHorizontalVx, maxStep);
1564
- runtime.velocityZ = stepTowards(runtime.velocityZ, desiredHorizontalVz, maxStep);
1565
- } else if (intentLen < 1e-4) {
1566
- let decel = cfg.motion.groundDecel * runtime.massRatios.groundAccelScale;
1567
- decel *= controller.mastery.evaluate(DecisionPoint.GroundAccel, controller, runtime);
1568
- const maxStep = decel * dt;
1569
- runtime.velocityX = stepTowards(runtime.velocityX, 0, maxStep);
1570
- runtime.velocityZ = stepTowards(runtime.velocityZ, 0, maxStep);
1571
- } else {
1572
- // Mono-exponential approach. Scale half-life by the
1573
- // inverse of the accel scalars so that "more accel" (large
1574
- // groundAccelScale, mastery > 1.0) translates to a shorter
1575
- // half-life (faster ramp). Backward intent slows things
1576
- // down — backwardAccelFactor < 1 ⇒ longer half-life.
1577
- let halfLife = cfg.motion.groundAccelHalfLife
1578
- / runtime.massRatios.groundAccelScale
1579
- / controller.mastery.evaluate(DecisionPoint.GroundAccel, controller, runtime);
1580
- if (isBackwardIntent) halfLife /= cfg.motion.backwardAccelFactor;
1581
- runtime.velocityX = exponentialApproach(runtime.velocityX, desiredHorizontalVx, halfLife, dt);
1582
- runtime.velocityZ = exponentialApproach(runtime.velocityZ, desiredHorizontalVz, halfLife, dt);
1583
- }
1584
-
1585
- // -- L1.e/f/g/h: jump FSM + vertical integration --------------
1586
- this._advanceJumpFsm(controller, runtime, bodyTransform, dt);
1587
- this._integrateVerticalAndResolveGround(controller, runtime, bodyTransform, dt);
1588
-
1589
- // -- Publish posture for L2 consumers (eye height, gait gating).
1590
- // Base owns posture when no ability is active: Crouch if the
1591
- // crouch intent is resolved active, otherwise Stand. Abilities
1592
- // that need a different posture (slide → Prone, ledge-grab →
1593
- // Hang) set state.posture themselves in their tick.
1594
- let posture = isCrouchActive
1595
- ? FirstPersonPosture.Crouch
1596
- : FirstPersonPosture.Stand;
1597
- // Headroom floor — block UN-proning under a very low overhang. Only an
1598
- // ALREADY-prone body (e.g. one that slid into a crawl-height tunnel) is
1599
- // force-kept prone while the Crouch collider can't fit above it. A player
1600
- // merely crouching by intent is NOT auto-dropped to prone when they meet
1601
- // a lower overhang — same rule as above: the mover blocks their forward
1602
- // motion instead of the ceiling shoving them down. Uses the post-move
1603
- // position, so it reflects where the body actually ended up this tick.
1604
- if (posture === FirstPersonPosture.Crouch
1605
- && runtime.lastPosture === FirstPersonPosture.Prone) {
1606
- const proneGrow = runtime.crouchTop - runtime.proneTop;
1607
- if (!this._hasHeadroomToGrow(runtime, bodyTransform, runtime.colliderShapeProne, proneGrow)) {
1608
- posture = FirstPersonPosture.Prone;
1609
- }
1610
- }
1611
- controller.state.posture = posture;
1612
-
1613
- // -- Publish lean target for L2.f. Base writes the natural
1614
- // (lat-accel + look-lean) value; abilities override in their
1615
- // own tick. L2.f spring-steps toward whatever's here.
1616
- runtime.leanTargetRad = this._computeNaturalLeanTarget(controller, runtime, dt);
1617
- }
1618
-
1619
- /**
1620
- * Compute the natural camera lean for this tick: lat-accel-driven
1621
- * roll into a turn, plus a yaw-rate look-lean contribution, both
1622
- * clamped. The result is the target the lean spring chases each
1623
- * tick when no ability has opinions.
1624
- *
1625
- * Pure-ish helper — reads `controller`, `runtime`, `dt`; returns a
1626
- * number. Extracted so both base and any future ability that wants
1627
- * to compose its lean on top of the natural value can call it.
1628
- *
1629
- * @private
1630
- * @param {FirstPersonPlayerController} controller
1631
- * @param {PerEntityRuntime} runtime
1632
- * @param {number} dt
1633
- * @returns {number} target roll in radians
1634
- */
1635
- _computeNaturalLeanTarget(controller, runtime, dt) {
1636
- const cfg = controller.config;
1637
- const state = controller.state;
1638
- if (!cfg.lean.enabled) return 0;
1639
-
1640
- const sinYaw = runtime.sinYaw;
1641
- const cosYaw = runtime.cosYaw;
1642
-
1643
- // Lateral acceleration projected onto screen-right.
1644
- // accel_world = (vel - prevVel) / dt; screen_right = (-cos θ, 0, sin θ).
1645
- const accWorldX = (runtime.velocityX - runtime.prevVelocityX) / Math.max(dt, 1e-4);
1646
- const accWorldZ = (runtime.velocityZ - runtime.prevVelocityZ) / Math.max(dt, 1e-4);
1647
- const latAccel = accWorldX * (-cosYaw) + accWorldZ * sinYaw;
1648
- const normalized = clamp(latAccel / 9.81, -2, 2);
1649
- //
1650
- // Sign convention for the roll (the eye composes the rotation
1651
- // as qYaw * qPitch * qRoll, where qRoll is around (0,0,1)).
1652
- // After the engine's camera-invert pipeline:
1653
- // φ > 0 → camera-up tilts toward screen-right (−X) → HEAD TILTS RIGHT
1654
- // φ < 0 → camera-up tilts toward screen-left (+X) → HEAD TILTS LEFT
1655
- //
1656
- // For the "bank into the turn" feel (Apex / Titanfall / Mirror's
1657
- // Edge): accelerating right (latAccel > 0) should tilt the head
1658
- // RIGHT, i.e. positive φ. So leanTargetRad has the SAME sign
1659
- // as latAccel.
1660
- let leanTargetRad = normalized * cfg.lean.maxRollDeg * DEG_TO_RAD;
1661
-
1662
- // Look-lean: yaw-rate-driven banking. runtime.yawRateRadPerSec
1663
- // was cached at L1.a — negative is the "turn right" convention.
1664
- // For "bank into the turn": turning right → head tilts right →
1665
- // positive engine roll. So lookLean = -yawRate * scale matches
1666
- // sign.
1667
- //
1668
- // Crouched players are in a low, stable, low-momentum stance —
1669
- // banking the head from a mouse turn reads as unmotivated. We
1670
- // scale the contribution down (default to 0) while crouched.
1671
- // Lat-accel lean is left alone: its magnitude naturally tracks
1672
- // the (lower) crouch acceleration, so it stays motivated.
1673
- if (cfg.lean.lookLeanEnabled) {
1674
- const yawRate = clamp(
1675
- runtime.yawRateRadPerSec,
1676
- -cfg.lean.lookLeanYawRateClamp,
1677
- cfg.lean.lookLeanYawRateClamp,
1678
- );
1679
- const crouchFactor = state.crouchActive ? cfg.lean.crouchLookLeanFactor : 1.0;
1680
- leanTargetRad += -yawRate * cfg.lean.lookLeanDegPerRadPerSec * DEG_TO_RAD * crouchFactor;
1681
- }
1682
-
1683
- // Final clamp on the sum: cap the combined target to ±2 ×
1684
- // maxRollDeg (matches the latAccel normalized clamp range) so
1685
- // even simultaneous max-strafe-accel + max-yaw-flick produces a
1686
- // sane upper bound.
1687
- const maxTotal = cfg.lean.maxRollDeg * DEG_TO_RAD * 2;
1688
- return clamp(leanTargetRad, -maxTotal, maxTotal);
1689
- }
1690
-
1691
- /**
1692
- * Snapshot the per-tick "what is the body doing" information into the
1693
- * pose channels for downstream consumption (skeleton, sound, AI).
1694
- * Read-only with respect to controller state — this is purely a publish
1695
- * step.
1696
- *
1697
- * @private
1698
- * @param {FirstPersonPlayerController} controller
1699
- * @param {PerEntityRuntime} runtime
1700
- * @param {Transform64} bodyTransform
1701
- */
1702
- _publishPose(controller, runtime, bodyTransform) {
1703
- const cfg = controller.config;
1704
- const state = controller.state;
1705
- const pose = controller.pose;
1706
-
1707
- pose.rootPosition.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
1708
- pose.rootYawRad = runtime.bodyYaw;
1709
- pose.headYawRad = runtime.bodyYaw;
1710
- pose.headPitchRad = runtime.eyePitch;
1711
- pose.headRollRad = state.leanRollRad;
1712
- pose.locomotionPhase = state.stridePhase;
1713
- pose.locomotionSpeed = runtime.horizSpeed;
1714
- // Strafe component: project velocity onto screen-right (-cos θ, 0, sin θ).
1715
- // Positive = moving to the player's right.
1716
- pose.locomotionStrafe = (runtime.velocityX * (-runtime.cosYaw) + runtime.velocityZ * runtime.sinYaw)
1717
- / Math.max(cfg.motion.sprintSpeed, 1e-3);
1718
- pose.actionState =
1719
- state.inJumpAnticipation ? FirstPersonActionState.Anticipating
1720
- : !state.grounded ? FirstPersonActionState.Airborne
1721
- : (Math.abs(runtime.landSpring.value) > 0.01 ? FirstPersonActionState.Landing
1722
- : FirstPersonActionState.Grounded);
1723
- pose.locomotionMode = state.locomotionMode;
1724
- const crouchSpan = Math.max(cfg.body.height - cfg.body.crouchHeight, 1e-3);
1725
- pose.crouchAmount = clamp((cfg.body.height - state.eyeHeight) / crouchSpan, 0, 1);
1726
-
1727
- // Posture channel for downstream animation: which body shape +
1728
- // how far the body is into it from the standing neutral.
1729
- //
1730
- // `posture` is the enum (Stand / Crouch / Prone / Hang) — picks
1731
- // the animation track. `postureAmount` is the [0..1] blend
1732
- // weight from standing toward that posture, derived from the
1733
- // eye-height spring so the value transitions smoothly across
1734
- // changes (matches the visible camera motion).
1735
- pose.posture = state.posture;
1736
- let postureTargetH;
1737
- switch (state.posture) {
1738
- case FirstPersonPosture.Prone: postureTargetH = cfg.body.proneHeight; break;
1739
- case FirstPersonPosture.Crouch: postureTargetH = cfg.body.crouchHeight; break;
1740
- case FirstPersonPosture.Hang: postureTargetH = cfg.body.height; break;
1741
- case FirstPersonPosture.Stand:
1742
- default: postureTargetH = cfg.body.height; break;
1743
- }
1744
- const postureSpan = Math.max(cfg.body.height - postureTargetH, 1e-3);
1745
- pose.postureAmount = clamp((cfg.body.height - state.eyeHeight) / postureSpan, 0, 1);
1746
-
1747
- pose.aimPitch = runtime.eyePitch;
1748
-
1749
- this._assertTickInvariants(controller, runtime, bodyTransform);
1750
- this._snapshotRenderPose(runtime, bodyTransform);
1751
- }
1752
-
1753
- /**
1754
- * Record this fixed step's authoritative body position as the latest
1755
- * render-interpolation endpoint, shifting the old latest to "previous".
1756
- * {@link _composeEye} blends prev→cur by the sub-step alpha so the camera
1757
- * position is smooth at any refresh rate. (Yaw/pitch aren't snapshotted —
1758
- * they're render-rate.) On a teleport (`renderSnap`), prev and cur collapse
1759
- * to the new position so the camera jumps rather than gliding across it.
1760
- * @private
1761
- */
1762
- _snapshotRenderPose(runtime, bodyTransform) {
1763
- const p = bodyTransform;
1764
- if (runtime.renderSnap) {
1765
- runtime.renderSnap = false;
1766
- runtime.renderPrevPos.set(p.translation_x, p.translation_y, p.translation_z);
1767
- runtime.renderCurPos.set(p.translation_x, p.translation_y, p.translation_z);
1768
- return;
1769
- }
1770
- runtime.renderPrevPos.copy(runtime.renderCurPos);
1771
- runtime.renderCurPos.set(p.translation_x, p.translation_y, p.translation_z);
1772
- }
1773
-
1774
- /**
1775
- * Dev-only invariant guard (meep asserts compile out of prod, so this is
1776
- * free there). Catches the class of corruption that surfaces as an
1777
- * unexplained camera "snap": a NaN/Infinity leaking into the yaw/pitch the
1778
- * eye composes from, or into the body's velocity / position. Runs once at
1779
- * the END of every fixed step, so a bad value is caught the tick it appears
1780
- * — naming the layer that produced it — instead of manifesting as a camera
1781
- * jump frames (or seconds) later when something finally reads it.
1782
- * @private
1783
- */
1784
- _assertTickInvariants(controller, runtime, bodyTransform) {
1785
- const p = bodyTransform;
1786
- assert.ok(Number.isFinite(runtime.bodyYaw),
1787
- "FP invariant: runtime.bodyYaw is non-finite (camera yaw corrupted)");
1788
- assert.ok(Number.isFinite(runtime.eyePitch),
1789
- "FP invariant: runtime.eyePitch is non-finite (camera pitch corrupted)");
1790
- assert.ok(Number.isFinite(controller.state.leanRollRad),
1791
- "FP invariant: state.leanRollRad is non-finite (camera roll corrupted)");
1792
- assert.ok(
1793
- Number.isFinite(runtime.velocityX)
1794
- && Number.isFinite(runtime.velocityY)
1795
- && Number.isFinite(runtime.velocityZ),
1796
- "FP invariant: runtime.velocity is non-finite",
1797
- );
1798
- assert.ok(
1799
- Number.isFinite(p.translation_x) && Number.isFinite(p.translation_y) && Number.isFinite(p.translation_z),
1800
- "FP invariant: body position is non-finite",
1801
- );
1802
- }
1803
-
1804
- /**
1805
- * Consume the accumulated mouse-look delta into body yaw + eye pitch at
1806
- * RENDER rate (once per render frame, from {@link _composeEye}). This is the
1807
- * fix for the slow-turn camera snap: sampling `intent.look` at the 60Hz
1808
- * fixed step aliases an unaligned mouse report rate into an uneven turn,
1809
- * whereas consuming it every render frame tracks the mouse as smoothly as it
1810
- * arrives. `intent.look` is zeroed after consuming so the same delta isn't
1811
- * re-applied next frame.
1812
- *
1813
- * Conventions (raw mouse delta source — movementX/Y positive moving
1814
- * right/down): look.x > 0 ("mouse right") → turn right (the yaw sign is
1815
- * negated because the engine is left-handed with +Z forward, so a +Y
1816
- * rotation reads as a LEFT turn through the camera); look.y > 0 → look down,
1817
- * flipped by invertY.
1818
- * @private
1819
- */
1820
- _consumeLook(controller, runtime) {
1821
- const intent = controller.intent;
1822
- const cfg = controller.config;
1823
- let yawDelta = -intent.look.x;
1824
- const pitchSign = cfg.look.invertY ? -1 : 1;
1825
- let pitchDelta = intent.look.y * pitchSign;
1826
- intent.look.set(0, 0);
1827
-
1828
- // Reject input SPIKES. A single render frame's look delta beyond a sane
1829
- // bound is a corrupt browser/driver `movementX` (a high-res mouse under
1830
- // OS pointer acceleration, or a pointer-lock burst), not a real turn —
1831
- // applying it snaps the camera 20–100°. Drop the spiking axis; legit
1832
- // turns (even fast flicks) stay well under the bound at render rate.
1833
- const maxDelta = cfg.look.maxFrameDeltaRad;
1834
- if (Math.abs(yawDelta) > maxDelta) yawDelta = 0;
1835
- if (Math.abs(pitchDelta) > maxDelta) pitchDelta = 0;
1836
-
1837
- runtime.bodyYaw += yawDelta;
1838
- if (runtime.bodyYaw > Math.PI) runtime.bodyYaw -= TWO_PI;
1839
- else if (runtime.bodyYaw < -Math.PI) runtime.bodyYaw += TWO_PI;
1840
-
1841
- runtime.eyePitch = clamp(
1842
- runtime.eyePitch + pitchDelta,
1843
- cfg.look.pitchMinDeg * DEG_TO_RAD,
1844
- cfg.look.pitchMaxDeg * DEG_TO_RAD,
1845
- );
1846
- }
1847
-
1848
- /**
1849
- * Compose the eye transform from body + state-driven offsets.
1850
- * @private
1851
- * @param {FirstPersonPlayerController} controller
1852
- * @param {number} entity
1853
- */
1854
- _composeEye(controller, entity) {
1855
- const ecd = this.entityManager.dataset;
1856
- const runtime = this.runtime.get(entity);
1857
- if (runtime === undefined) return;
1858
-
1859
- // Consume mouse-look at RENDER rate so the camera direction tracks the
1860
- // mouse smoothly, decoupled from the aliased 60Hz sim sampling.
1861
- this._consumeLook(controller, runtime);
1862
-
1863
- const dt = this._currentRenderDt;
1864
- const cfg = controller.config;
1865
- const state = controller.state;
1866
-
1867
- const bodyTransform = ecd.getComponent(entity, Transform64);
1868
- if (bodyTransform === undefined) return;
1869
-
1870
- if (controller.eyeEntity === -1) return;
1871
- const eyeTransform = ecd.getComponent(controller.eyeEntity, Transform64);
1872
- const camera = ecd.getComponent(controller.eyeEntity, Camera);
1873
- if (eyeTransform === undefined || camera === undefined) return;
1874
-
1875
- // -- Body-local eye offset, composed via the additive stack ----
1876
- // The base (0, eyeHeight, 0) is the standing/crouched neutral; each
1877
- // additional contribution (bob, breath, landing, anticipation,
1878
- // sprint posture) goes through the stack so external systems can
1879
- // push their own contributions on the same channel.
1880
- const stack = runtime.eyeOffsetStack;
1881
- stack.clear();
1882
- stack.push("eyeHeight", 0, state.eyeHeight, 0);
1883
-
1884
- // Bob — gated on grounded only (the impact spring decays naturally
1885
- // even at rest, so the bob fade-out is smooth; lateral amp uses the
1886
- // bob-intensity envelope which spring-decays after stopping).
1887
- if (state.grounded) {
1888
- const phase = state.stridePhase * TWO_PI;
1889
- const massBoost = (cfg.body.mass - 80) * cfg.bob.ampMassScale;
1890
- const intensity = runtime.bobIntensitySpring.value;
1891
-
1892
- // Back-pedal amp boost — lateral grows more than vertical because
1893
- // backward gait has worse side-to-side balance than vertical compression.
1894
- // Exertion adds a smaller boost on top: tired = wobbly gait.
1895
- const ampLMult = 1 + (cfg.bob.backwardLateralAmpFactor - 1) * runtime.backwardness;
1896
- const exertionBoost = 1 + cfg.exertion.bobLateralBoostAtMax * state.exertion;
1897
- const ampL = (cfg.bob.lateralAmpAtWalk + massBoost) * intensity * ampLMult * exertionBoost;
1898
-
1899
- // Vertical: read directly from the impact spring (footfall kicks,
1900
- // under-damped recovery → trough + leg-push overshoot).
1901
- stack.push("bob.impact", 0, runtime.verticalImpactSpring.value, 0);
1902
-
1903
- // Lateral: head shifts toward the foot bearing weight. Polarity
1904
- // sourced from runtime.standingFoot — the same signal the
1905
- // footstep emits — so bob direction and footstep side agree.
1906
- // |sin(phase)| is the non-negative "midstance envelope".
1907
- const lateralPolarity = runtime.standingFoot === "R" ? -1 : 1;
1908
- stack.push("bob.lateral", ampL * lateralPolarity * Math.abs(Math.sin(phase)), 0, 0);
1909
- }
1910
-
1911
- // Breath — sine + tiny noise riding the rate spring.
1912
- const breathOffset = -state.breathAmplitudeM
1913
- * Math.sin(state.breathPhase * TWO_PI)
1914
- * (1 + cfg.breath.noiseAmount * (Math.sin(state.breathPhase * 13.7) * 0.5));
1915
- stack.push("breath", 0, breathOffset, 0);
1916
-
1917
- // Landing spring dip (under-damped — overshoots once on recovery).
1918
- stack.push("landing", 0, runtime.landSpring.value, 0);
1919
-
1920
- // Jump anticipation dip (eased ramp during the squash window).
1921
- if (state.inJumpAnticipation) {
1922
- const t = 1 - clamp(runtime.anticipationRemaining / Math.max(cfg.jump.anticipation.duration, 1e-3), 0, 1);
1923
- const eased = t * (2 - t); // ease-out quad
1924
- stack.push("anticipation", 0, -cfg.jump.anticipation.dipAmount * eased, 0);
1925
- }
1926
-
1927
- // Sprint posture: head leans slightly forward as commitment builds.
1928
- // Pitch part is in the rotation block below; the +Z position shift
1929
- // sells "head leading the hips" (Mirror's Edge), tied to the same
1930
- // spring envelope so they move together.
1931
- const sprintPitch = runtime.sprintPostureSpring.value;
1932
- const sprintShiftFraction =
1933
- cfg.posture.sprintForwardPitchDeg > 0
1934
- ? sprintPitch / (cfg.posture.sprintForwardPitchDeg * DEG_TO_RAD)
1935
- : 0;
1936
- stack.push("posture.sprintShift", 0, 0, cfg.posture.sprintForwardShiftM * sprintShiftFraction);
1937
-
1938
- // Body POSITION advances only at the fixed step → blend the last two
1939
- // steps by the engine's sub-step alpha (the same fraction the
1940
- // InterpolationSystem uses for physics bodies) so it's smooth on a high-
1941
- // refresh display. Yaw + pitch are already render-rate (consumed in
1942
- // _consumeLook), so they're used directly — no blend, no aliasing.
1943
- const alpha = this.entityManager.getFixedStepAlpha();
1944
- const iPosX = lerp(runtime.renderPrevPos.x, runtime.renderCurPos.x, alpha);
1945
- const iPosY = lerp(runtime.renderPrevPos.y, runtime.renderCurPos.y, alpha);
1946
- const iPosZ = lerp(runtime.renderPrevPos.z, runtime.renderCurPos.z, alpha);
1947
- const qYaw0 = SCRATCH_Q_A.fromAxisAngle(Vector3.up, runtime.bodyYaw);
1948
-
1949
- // Transform body-local accumulated offset into world space (about the
1950
- // render-rate yaw, to match the interpolated base position).
1951
- const worldOffset = SCRATCH_V3_B.copy(stack.offset);
1952
- worldOffset.applyQuaternion(qYaw0);
1953
-
1954
- eyeTransform.setTranslation(
1955
- iPosX + worldOffset.x,
1956
- iPosY + worldOffset.y,
1957
- iPosZ + worldOffset.z
1958
- );
1959
-
1960
- // -- Eye rotation: body yaw × eye pitch × roll -------------------
1961
- // Bob roll mixes in for a subtle head sway (in phase with lateral bob).
1962
- // Breath pitch is a small extra nod 90° out of phase with vertical
1963
- // breath; merged into the main pitch so we don't pay an extra quat
1964
- // multiply and the composition stays trivially correct.
1965
- let rollTotal = state.leanRollRad;
1966
- if (state.grounded) {
1967
- // Roll: head tilts toward the standing foot, in phase with the
1968
- // lateral sway. Polarity sourced from runtime.standingFoot for
1969
- // consistency with the lateral bob. Positive engine roll = head
1970
- // tilts RIGHT (camera-invert convention), so R-foot midstance =
1971
- // positive roll, L-foot midstance = negative roll.
1972
- const phase = state.stridePhase * TWO_PI;
1973
- const rollBackMult = 1 + (cfg.bob.backwardRollFactor - 1) * runtime.backwardness;
1974
- const ampRoll = cfg.bob.rollAtWalkDeg * DEG_TO_RAD * runtime.bobIntensitySpring.value * rollBackMult;
1975
- const rollPolarity = runtime.standingFoot === "R" ? 1 : -1;
1976
- const rollEnvelope = Math.abs(Math.sin(phase));
1977
- const bobRollSigned = ampRoll * rollPolarity * rollEnvelope;
1978
-
1979
- // Lean × bob coupling: excursions in the lean direction get
1980
- // amplified, opposite excursions attenuated. Lean is normalized
1981
- // against maxRollDeg so the coupling magnitude stays bounded
1982
- // regardless of how aggressively lean is configured.
1983
- const maxLeanRad = Math.max(cfg.lean.maxRollDeg * DEG_TO_RAD, 1e-6);
1984
- const leanFraction = clamp(state.leanRollRad / maxLeanRad, -1, 1);
1985
- // sign(bobRollSigned) matches lean? amplify; else attenuate.
1986
- const sameSign = (bobRollSigned * leanFraction) >= 0;
1987
- const couplingMag = cfg.bob.leanCouplingFactor * Math.abs(leanFraction);
1988
- const couplingScale = sameSign ? (1 + couplingMag) : (1 - couplingMag);
1989
- rollTotal += bobRollSigned * couplingScale;
1990
- }
1991
-
1992
- const breathPitch = lerp(cfg.breath.pitchAmpRestDeg, cfg.breath.pitchAmpMaxDeg, state.exertion)
1993
- * DEG_TO_RAD
1994
- * Math.cos(state.breathPhase * TWO_PI);
1995
- // Combined pitch contributions: player input + breath nod + sprint
1996
- // commitment + fatigue droop. All in the same "positive = look-down"
1997
- // convention so they sum cleanly.
1998
- const pitchTotal = runtime.eyePitch
1999
- + breathPitch
2000
- + runtime.sprintPostureSpring.value
2001
- + runtime.headDroopSpring.value;
2002
-
2003
- // composition: yaw * pitch * roll
2004
- // pitch around world X — yaw applied after, so effective axis is camera-local right
2005
- // roll around world Z — yaw and pitch applied after, so effective axis is camera-local forward
2006
- const qYaw = qYaw0; // SCRATCH_Q_A, already the render-rate yaw
2007
- const qPitch = SCRATCH_Q_B.fromAxisAngle(Vector3.right, pitchTotal);
2008
- const qRoll = SCRATCH_Q_C.fromAxisAngle(Vector3.forward, rollTotal);
2009
-
2010
- // composed in a scratch: a transform keeps its rotation as four numbers and has no
2011
- // quaternion product of its own
2012
- SCRATCH_Q_EULER.multiplyQuaternions(qYaw, qPitch);
2013
- SCRATCH_Q_EULER.multiply(qRoll);
2014
-
2015
- eyeTransform.setRotation(
2016
- SCRATCH_Q_EULER.x, SCRATCH_Q_EULER.y, SCRATCH_Q_EULER.z, SCRATCH_Q_EULER.w
2017
- );
2018
-
2019
- eyeTransform.updateMatrix();
2020
-
2021
- // Dev-only camera-output guard (compiled out of prod). The composed eye
2022
- // direction is exactly what the player sees; if a spring (head-droop,
2023
- // breath nod) or lean ever drove pitch/roll to NaN, the camera would
2024
- // snap to garbage. Assert it here, at the source of the visible jump.
2025
- assert.ok(
2026
- Number.isFinite(eyeTransform.rotation_x) && Number.isFinite(eyeTransform.rotation_y) && Number.isFinite(eyeTransform.rotation_z) && Number.isFinite(eyeTransform.rotation_w),
2027
- "FP invariant: composed eye rotation is non-finite (camera direction corrupted)",
2028
- );
2029
-
2030
- // -- FOV ---------------------------------------------------------
2031
- let fovTarget = cfg.fov.base;
2032
- if (cfg.fov.sprintAdd !== 0) {
2033
- fovTarget += cfg.fov.sprintAdd * runtime.sprintness;
2034
- }
2035
- if (state.crouchActive) fovTarget += cfg.fov.crouchAdd;
2036
-
2037
- runtime.fovSpring.stepTo(fovTarget, cfg.fov.smoothHalfLife, 1.0, dt);
2038
- camera.fov.set(runtime.fovSpring.value);
2039
- }
2040
- }
2041
-
2042
- // ---------------------------------------------------------------------------
2043
- // helpers
2044
- // ---------------------------------------------------------------------------
2045
-
2046
- /**
2047
- * Exponential approach with half-life parameterization.
2048
- * @param {number} current
2049
- * @param {number} target
2050
- * @param {number} halfLife
2051
- * @param {number} dt
2052
- * @returns {number}
2053
- */
2054
- function exponentialApproach(current, target, halfLife, dt) {
2055
- if (halfLife <= 0) return target;
2056
- const alpha = 1 - Math.exp(-LN2 * dt / halfLife);
2057
- return current + (target - current) * alpha;
2058
- }
2059
-
2060
- /**
2061
- * Detect that phase value crossed a boundary in [0,1) between two ticks.
2062
- * Handles the wraparound case where phase jumps from e.g. 0.95 to 0.05.
2063
- *
2064
- * @param {number} prev previous phase in [0,1)
2065
- * @param {number} next current phase in [0,1)
2066
- * @param {number} boundary in [0,1)
2067
- * @returns {boolean}
2068
- */
2069
- function phaseCrossed(prev, next, boundary) {
2070
- if (next >= prev) {
2071
- // no wrap
2072
- return prev < boundary && next >= boundary;
2073
- } else {
2074
- // wrapped past 1.0
2075
- return prev < boundary || next >= boundary;
2076
- }
2077
- }
2078
-
1
+ import { assert } from "../../../core/assert.js";
2
+ import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
3
+ import { CapsuleShape3D } from "../../../core/geom/3d/shape/CapsuleShape3D.js";
4
+ import { TransformedShape3D } from "../../../core/geom/3d/shape/TransformedShape3D.js";
5
+ import Quaternion from "../../../core/geom/Quaternion.js";
6
+ import Vector3 from "../../../core/geom/Vector3.js";
7
+ import { clamp } from "../../../core/math/clamp.js";
8
+ import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
9
+ import { lerp } from "../../../core/math/lerp.js";
10
+ import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
11
+ import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
12
+ import { SerializationMetadata } from "../../ecs/components/SerializationMetadata.js";
13
+ import Entity from "../../ecs/Entity.js";
14
+ import { System } from "../../ecs/System.js";
15
+ import { Transform64 } from "../../ecs/transform/Transform64.js";
16
+ import { Camera } from "../../graphics/ecs/camera/Camera.js";
17
+ import { BodyKind } from "../../physics/ecs/BodyKind.js";
18
+ import { Collider } from "../../physics/ecs/Collider.js";
19
+ import { PhysicsSystem } from "../../physics/ecs/PhysicsSystem.js";
20
+ import { RigidBody } from "../../physics/ecs/RigidBody.js";
21
+ import { PhysicsSurfacePoint } from "../../physics/queries/PhysicsSurfacePoint.js";
22
+ import { KinematicMover } from "./collision/KinematicMover.js";
23
+ import { EyeOffsetStack } from "./composer/EyeOffsetStack.js";
24
+ import { FirstPersonPlayerController } from "./FirstPersonPlayerController.js";
25
+ import { DecisionPoint } from "./mastery/DecisionPoint.js";
26
+ import { computeJumpFromApex } from "./math/computeJumpFromApex.js";
27
+ import { computeLRCBreathRate } from "./math/computeLRCBreathRate.js";
28
+ import { computeMassRatios } from "./math/computeMassRatios.js";
29
+ import { jumpForceForHeight, launchVelocityForForce } from "./math/jumpDynamics.js";
30
+ import { Spring } from "./math/Spring.js";
31
+ import { stepTowards } from "./math/stepTowards.js";
32
+ import { FirstPersonActionState, FirstPersonLocomotionMode } from "./pose/FirstPersonPose.js";
33
+ import { FirstPersonPosture } from "./pose/FirstPersonPosture.js";
34
+ import { FirstPersonSensors } from "./sensors/FirstPersonSensors.js";
35
+ import { t64_announce_change } from "../../ecs/transform/t64_announce_change.js";
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Scratch allocations — reused per frame to avoid GC pressure
39
+ // ---------------------------------------------------------------------------
40
+ const SCRATCH_V3_A = new Vector3();
41
+ const SCRATCH_V3_B = new Vector3();
42
+ const SCRATCH_V3_C = new Vector3();
43
+ const SCRATCH_Q_A = new Quaternion();
44
+ const SCRATCH_Q_B = new Quaternion();
45
+ const SCRATCH_Q_C = new Quaternion();
46
+ /** the transform holds its rotation as four numbers, so euler and axis-angle work goes through here */
47
+ const SCRATCH_Q_EULER = new Quaternion();
48
+
49
+ const TWO_PI = Math.PI * 2;
50
+ const LN2 = Math.log(2);
51
+
52
+ /**
53
+ * Build a posture-sized player capsule: a {@link CapsuleShape3D} of
54
+ * `radius` and the appropriate cylinder height, wrapped in a
55
+ * {@link TransformedShape3D} whose Y offset puts the capsule's bottom
56
+ * exactly at the wrapped shape's local origin. The entity's
57
+ * `transform.position` then represents the player's feet — and a
58
+ * posture-driven shrink doesn't yank the feet up the way a centred
59
+ * capsule would, nor dip them below the floor.
60
+ *
61
+ * The capsule's lowest point in its own local frame is at
62
+ * `-(cylinderHeight/2 + radius) = -max(totalHeight/2, radius)`.
63
+ * Offsetting the wrapper by the magnitude of that puts the bottom at
64
+ * Y = 0:
65
+ * - Stand (`H = 1.8`, `r = 0.34`): cylHeight = 1.12, offset = 0.9.
66
+ * Bottom = -0.9 + 0.9 = 0. Top = +0.9 + 0.9 = 1.8.
67
+ * - Crouch (`H = 0.8`, `r = 0.34`): cylHeight = 0.12, offset = 0.4.
68
+ * Bottom = -0.4 + 0.4 = 0. Top = +0.4 + 0.4 = 0.8.
69
+ * - Prone (`H = 0.4`, `r = 0.34`): cylHeight = 0 (capsule collapses
70
+ * to a sphere of radius), offset = max(0.2, 0.34) = 0.34.
71
+ * Bottom = -0.34 + 0.34 = 0. Top = +0.34 + 0.34 = 0.68. The
72
+ * `totalHeight = 0.4` value is honoured for the offset budget
73
+ * but the actual Y extent floors at `2·radius`.
74
+ *
75
+ * Picking `totalHeight/2` blindly (the obvious choice) would put the
76
+ * Prone capsule's bottom at `0.2 - 0.34 = -0.14` — dipping below the
77
+ * feet, and into any physics floor that's flush with feet level. On
78
+ * a physics ground slab, every horizontal shape_cast from inside the
79
+ * floor returns t = 0, `advance = max(0, t - SKIN) = 0`, and the
80
+ * slide freezes in place — see SlideMotion.spec.js for the
81
+ * regression test that pins this.
82
+ *
83
+ * @param {number} radius — capsule radius in metres
84
+ * @param {number} totalHeight — desired full Y extent; ignored below
85
+ * `2·radius` (the capsule's intrinsic minimum extent)
86
+ * @returns {TransformedShape3D}
87
+ */
88
+ function makePostureCapsule(radius, totalHeight) {
89
+ const cylinderHeight = Math.max(0, totalHeight - 2 * radius);
90
+ const yOffset = Math.max(totalHeight / 2, radius);
91
+ return TransformedShape3D.from_translation(
92
+ CapsuleShape3D.from(radius, cylinderHeight),
93
+ [0, yOffset, 0],
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Per-entity runtime state the system maintains internally — too transient
99
+ * even for {@link FirstPersonPlayerController}'s `state` member, because it
100
+ * encodes input-edge bookkeeping and timer values the public surface should
101
+ * never see directly.
102
+ */
103
+ class PerEntityRuntime {
104
+ /**
105
+ * Co-attached kinematic body. Set by {@link FirstPersonPlayerControllerSystem.link}
106
+ * after asserting it's present. The controller writes Transform64.position
107
+ * directly (existing motion logic); physics derives the body's velocity
108
+ * from the per-step delta. Other physics systems (raycasts, contact
109
+ * events) see the player through this body.
110
+ * @type {RigidBody|null}
111
+ */
112
+ rigidBody = null;
113
+
114
+ /**
115
+ * Co-attached collider, cached at link. Same source the physics
116
+ * narrowphase uses, so move-and-slide casts the player's
117
+ * actual collision shape against the world.
118
+ * @type {Collider|null}
119
+ */
120
+ collider = null;
121
+
122
+ /**
123
+ * This character's own mover.
124
+ *
125
+ * The mover carries per-character state - `_wasGrounded`, which is the sole input to the
126
+ * `landed`/`landingSpeed` fields its docblock says exist so a consumer does not have to diff
127
+ * `grounded` itself, and a reused `_result` object. One mover per SYSTEM made that a single
128
+ * slot shared by every first-person character in the dataset: with one airborne and one
129
+ * standing, the standing one reported `landed` on every tick with the other's descent speed,
130
+ * and the airborne one's real landing reported `landed === false`.
131
+ *
132
+ * Lazily built on first use, like the system-wide one was.
133
+ *
134
+ * @type {KinematicMover|null}
135
+ */
136
+ mover = null;
137
+
138
+ /**
139
+ * Pre-built capsule colliders, one per posture. Cached at link
140
+ * from `config.body.{height, crouchHeight, proneHeight, radius}`
141
+ * so {@link _syncColliderShape} can swap the collider's shape on
142
+ * a posture change with zero per-tick allocation. Hang reuses
143
+ * Stand (the player's body is full-extent, just hanging below
144
+ * the ledge — the rig animates the arms-up pose). Sentinel
145
+ * `lastPosture = -1` forces a sync on the first tick after
146
+ * link, so the initial shape always matches Stand.
147
+ * @private
148
+ * @type {TransformedShape3D|null}
149
+ */
150
+ colliderShapeStand = null;
151
+
152
+ /** @private @type {TransformedShape3D|null} */
153
+ colliderShapeCrouch = null;
154
+
155
+ /** @private @type {TransformedShape3D|null} */
156
+ colliderShapeProne = null;
157
+
158
+ /** @private */
159
+ lastPosture = -1;
160
+
161
+ /** Eye pitch in radians, clamped to config.look limits. */
162
+ eyePitch = 0;
163
+
164
+ /** Body yaw in radians (around world up). */
165
+ bodyYaw = 0;
166
+
167
+ /** Yaw rate (rad/s) computed in look consumption — for evaluators. */
168
+ yawRateRadPerSec = 0;
169
+
170
+ // -- Render-rate look + position interpolation ---------------------
171
+ // The camera DIRECTION (yaw/pitch) is consumed at RENDER rate (see the
172
+ // controller's _consumeLook), so it tracks the mouse smoothly instead of
173
+ // being aliased by the 60Hz sim sampling — consuming the accumulated
174
+ // mouse delta once per fixed step beats against an unaligned mouse
175
+ // report rate and makes a slow turn lurch (the reported snap). The body
176
+ // POSITION still advances only at the fixed step, so it's blended
177
+ // between the last two steps by the sub-step alpha to stay smooth on a
178
+ // high-refresh display.
179
+ /** Body yaw at the end of the previous fixed step — used to derive the
180
+ * per-step turn RATE that drives look-lean, decoupled from per-frame
181
+ * input jitter. */
182
+ prevFixedYaw = 0;
183
+
184
+ renderPrevPos = new Vector3();
185
+ renderCurPos = new Vector3();
186
+
187
+ /** When true, the next position snapshot collapses prev=cur (no blend)
188
+ * — set on a body teleport (e.g. the ledge-grab hang snap) so the
189
+ * camera doesn't glide across the jump. */
190
+ renderSnap = false;
191
+
192
+ /** Horizontal+vertical velocity. We integrate these inside the system
193
+ * when no external physics layer is attached. */
194
+ velocityX = 0;
195
+
196
+ velocityY = 0;
197
+ velocityZ = 0;
198
+
199
+ /** Previous-tick jump intent — for rising/falling edge detection. */
200
+ prevJumpHeld = false;
201
+
202
+ /** Previous-tick crouch intent — for toggle-mode edge detection. */
203
+ prevCrouchHeld = false;
204
+
205
+ /** True while crouch toggle is latched on (used only in toggle mode). */
206
+ crouchLatched = false;
207
+
208
+ /** Remaining time in jump anticipation, or <= 0 if not anticipating. */
209
+ anticipationRemaining = 0;
210
+
211
+ /** Cached derived gravity (m/s^2) from peakHeight + timeToApex. */
212
+ gravity = 9.81;
213
+
214
+ /**
215
+ * Cached jump PUSH-OFF FORCE (newtons) — the physical leg force that
216
+ * reaches `peakHeight` at the reference mass, the quantity jump power is
217
+ * expressed in. Derived at link via {@link jumpForceForHeight}; abilities
218
+ * (e.g. the scramble wall-kick) launch off this same force.
219
+ */
220
+ jumpForce = 0;
221
+
222
+ /** Cached derived jump impulse (m/s upward), post-mass-scaling. */
223
+ jumpInitialVy = 5.0;
224
+
225
+ /**
226
+ * Cached mass scaling factors — computed once at link. See
227
+ * {@link computeMassRatios}. Heavier ⇒ lower jumpV0Scale, lower
228
+ * groundAccelScale, higher landingDipScale + exertionRiseScale.
229
+ */
230
+ massRatios = null;
231
+
232
+ /** Spring for landing dip (under-damped → rings after impact). */
233
+ landSpring = new Spring();
234
+
235
+ /** Spring for FOV (critically damped). */
236
+ fovSpring = new Spring(70);
237
+
238
+ /** Spring for eye height (crouch transition). */
239
+ eyeHeightSpring = new Spring(1.80);
240
+
241
+ /** Spring for lean roll (radians) — banks into lateral acceleration. */
242
+ leanSpring = new Spring();
243
+
244
+ /**
245
+ * Lean target this tick (radians). Always set; L2.f spring-steps
246
+ * toward this value. Whoever owned motion this tick wrote it:
247
+ * base writes the lat-accel + look-lean derived value at the end
248
+ * of {@link _runBaseLocomotion}; abilities that want to override
249
+ * (WallRun → tilt-into-wall, Slide/Mantle/LedgeGrab → zero) write
250
+ * their own value in tick. Uniform channel — no null sentinel.
251
+ */
252
+ leanTargetRad = 0;
253
+
254
+ /** Previous horizontal velocity — for lateral acceleration → lean. */
255
+ prevVelocityX = 0;
256
+
257
+ prevVelocityZ = 0;
258
+
259
+ /** Previous-tick grounded for edge detection. */
260
+ prevGrounded = true;
261
+
262
+ /** Vertical speed at moment of last "leave ground". */
263
+ takeoffVy = 0;
264
+
265
+ /** Max vertical position since last takeoff — for jump apex detection. */
266
+ peakAltitude = 0;
267
+
268
+ /** Set true once a jump has been launched; cleared on land. */
269
+ midJump = false;
270
+
271
+ /** Apex already fired for this airborne segment? */
272
+ apexFired = false;
273
+
274
+ /** Stride phase from previous fixed step — for footstep edge detection. */
275
+ prevStridePhase = 0;
276
+
277
+ /** Breath phase from previous fixed step — for inhale/exhale edge detection. */
278
+ prevBreathPhase = 0;
279
+
280
+ /** Which foot fires next — flipped on each footstep signal. */
281
+ nextFootSide = "R";
282
+
283
+ /**
284
+ * Which foot is currently bearing the body's weight (the foot that
285
+ * most recently landed). Drives the lateral-bob direction: at R
286
+ * midstance the COM is over the right foot, so the head shifts
287
+ * laterally toward screen-right; at L midstance the opposite.
288
+ * Coupled to the same signal the footstep emits, so anything that
289
+ * listens to onFootStep.side will see the bob agree.
290
+ * Initialized "L" so the very first footstep fires "R" and the
291
+ * standingFoot updates to "R" — putting the head laterally right
292
+ * during the first half-stride, as expected.
293
+ */
294
+ standingFoot = "L";
295
+
296
+ /**
297
+ * [0..1] How "backward" the player is currently moving. Derived in
298
+ * fixedUpdate from velocity · screen-forward, normalized to sprint
299
+ * speed. Drives the gait wobble amplifier on the L3 camera-composition
300
+ * pass. Stored on runtime (rather than state) because it's a render-
301
+ * side input — downstream observers should look at velocity directly.
302
+ */
303
+ backwardness = 0;
304
+
305
+ /**
306
+ * Smoothed bob amplitude envelope. Target = max(speedNormalized,
307
+ * backwardness) when grounded, 0 airborne. Spring decay prevents
308
+ * the whiplash where stopping motion would snap the bob to neutral.
309
+ */
310
+ bobIntensitySpring = new Spring();
311
+
312
+ /**
313
+ * Vertical impact spring — kicked downward at each footfall, decays
314
+ * with a slight under-damped overshoot. Produces the impact-arrest +
315
+ * leg-push curve. value units: meters (added directly to eyeLocal.y).
316
+ */
317
+ verticalImpactSpring = new Spring();
318
+
319
+ /**
320
+ * Sprint-posture spring — eye pitches forward as the player commits
321
+ * to a sprint, returns to neutral when they slow. Value is in
322
+ * radians; slower half-life than other springs so it feels like
323
+ * a posture change rather than an input twitch. See cfg.posture.
324
+ */
325
+ sprintPostureSpring = new Spring();
326
+
327
+ /**
328
+ * Head-droop spring — additional forward pitch as exertion rises.
329
+ * Sells fatigue subtly. Target tracks exertion-driven max droop
330
+ * angle; spring lag keeps the transition slow and physical.
331
+ */
332
+ headDroopSpring = new Spring();
333
+
334
+ /**
335
+ * [0..1] sprintness — how much of the walk→sprint speed range the
336
+ * body is currently in. Computed in fixedUpdate, read by L3 for FOV
337
+ * and the sprint-posture pitch / forward-shift offset.
338
+ */
339
+ sprintness = 0;
340
+
341
+ /**
342
+ * Cached sin/cos of current body yaw — written once per fixedUpdate
343
+ * after look intent is consumed, read by every downstream step
344
+ * (locomotion, backwardness, lean look-rate, pose channels). Avoids
345
+ * recomputing the trig 3+ times per tick.
346
+ */
347
+ sinYaw = 0;
348
+
349
+ cosYaw = 1;
350
+
351
+ /** Cached horizontal speed (m/s) for this tick — written in derived-state. */
352
+ horizSpeed = 0;
353
+
354
+ /** Cached stride frequency (Hz) for this tick — written in breath block, read by stride. */
355
+ strideFreqHz = 0;
356
+
357
+ /**
358
+ * Additive accumulator for body-local eye-position offsets. The
359
+ * system pushes its own contributions (bob, breath, landing,
360
+ * sprint posture) each render frame; external systems can push
361
+ * recoil/shake/knockback contributions via the same interface.
362
+ */
363
+ eyeOffsetStack = new EyeOffsetStack();
364
+
365
+ /**
366
+ * Spatial-query results populated by {@link FirstPersonSensorsSystem}
367
+ * (when present). Abilities and the locomotion FSM read this.
368
+ * Lives on runtime so other systems can populate it without
369
+ * touching the controller component's public surface.
370
+ */
371
+ sensors = new FirstPersonSensors();
372
+
373
+ /** Cached eye entity ID. -1 until link assigns it. */
374
+ eyeEntity = -1;
375
+
376
+ /**
377
+ * One-shot hand-off flag: LedgeGrab sets it on a pull-up release so
378
+ * Mantle (next tick) knows this is a ledge pull-up — it then allows a
379
+ * too-thin top (auto-mantle still requires a standable one) and vaults
380
+ * OVER instead of onto. Mantle consumes it in onActivate.
381
+ */
382
+ ledgePullUpRequest = false;
383
+
384
+ /**
385
+ * Seconds remaining in the post-release ledge re-grab refractory
386
+ * window. Set by {@link LedgeGrab} on deactivation, aged down by dt
387
+ * each fixed step, and read by LedgeGrab.canActivate — so a deliberate
388
+ * dismount can fall clear of the grab window without the auto-catch
389
+ * snapping the body straight back onto the same lip.
390
+ */
391
+ ledgeRegrabCooldown = 0;
392
+ }
393
+
394
+ /**
395
+ * Drives a first-person camera + body from intent fields. See sibling
396
+ * DESIGN.md for goals, architecture, and the five processing layers (L0..L4).
397
+ *
398
+ * - fixedUpdate runs L1 (locomotion), L2 (pose state), and L4 (events) so
399
+ * the simulation remains deterministic.
400
+ * - update runs L3 (camera composition) at render rate so the eye is never
401
+ * smoother than the screen.
402
+ *
403
+ * The system itself integrates a simple flat-floor at y = `config.gravity.magnitude > 0
404
+ * ? state.groundY : -Infinity` for the prototype. A real physics layer should
405
+ * write `state.grounded`/`state.groundNormal` from outside instead; the
406
+ * built-in resolver is just a convenience to keep the controller usable
407
+ * without dependencies.
408
+ *
409
+ * @author Alex Goldring
410
+ * @copyright Company Named Limited (c) 2026
411
+ */
412
+ export class FirstPersonPlayerControllerSystem extends System {
413
+ // Dependencies kept to (controller, transform) so we can ASSERT on
414
+ // RigidBody at link time and emit a clear error if missing. If
415
+ // RigidBody were a hard dep, entities lacking one would silently
416
+ // never link — the controller would appear inert with no
417
+ // diagnostic. The assert below catches the missing-body case
418
+ // explicitly.
419
+ dependencies = [FirstPersonPlayerController, Transform64];
420
+
421
+ components_used = [
422
+ ResourceAccessSpecification.from(Transform64, ResourceAccessKind.Write),
423
+ ResourceAccessSpecification.from(Camera, ResourceAccessKind.Write),
424
+ ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Write),
425
+ ];
426
+
427
+ /**
428
+ * Per-entity runtime, keyed by entity id.
429
+ * @type {Map<number, PerEntityRuntime>}
430
+ */
431
+ runtime = new Map();
432
+
433
+ /**
434
+ * If true, the system clamps body y >= groundY and writes
435
+ * state.grounded itself. Turn off when wiring a real physics layer.
436
+ * @type {boolean}
437
+ */
438
+ useBuiltInFlatGround = true;
439
+
440
+ /**
441
+ * The flat-ground y for the built-in resolver. Ignored when
442
+ * useBuiltInFlatGround is false.
443
+ * @type {number}
444
+ */
445
+ groundY = 0;
446
+
447
+ /**
448
+ * Optional callback that returns the surface Y under the player
449
+ * for ground resolution. Called each tick with the player's
450
+ * current (x, y, z); returns the world-Y of the ground below,
451
+ * or null if no ground is below (gap / void).
452
+ *
453
+ * Combines with `useBuiltInFlatGround`: the effective ground for
454
+ * the tick is `max(this.groundY when enabled, resolver(...))`.
455
+ * Set both off (`useBuiltInFlatGround=false`, `groundResolver=null`)
456
+ * to defer to external physics entirely.
457
+ *
458
+ * Designed for prototypes / gyms that need elevated platforms
459
+ * without a full physics layer. Production should wire a real
460
+ * physics system instead.
461
+ *
462
+ * @type {((x:number, y:number, z:number) => number|null) | null}
463
+ */
464
+ groundResolver = null;
465
+
466
+ /**
467
+ * PhysicsSystem reference. Auto-acquired at startup; can be
468
+ * overridden by the caller. When present, collision is resolved
469
+ * by the {@link KinematicMover}; when null, the controller uses
470
+ * the flat-ground integrator ({@link _moveFlatGround}) — useful
471
+ * for spec setups that don't wire physics.
472
+ * @type {PhysicsSystem|null}
473
+ */
474
+ physicsSystem = null;
475
+
476
+ /**
477
+ * The collision solver — the authoritative move resolver whenever
478
+ * a {@link PhysicsSystem} is present (recover + unified
479
+ * sweep-and-slide + ground-categorize; see DESIGN_COLLISION.md).
480
+ *
481
+ * NOTE: unused, and deliberately left here as `null`. The mover lives on
482
+ * {@link PerEntityRuntime#mover} now, because it carries per-character state a system-wide
483
+ * instance shared across every first-person character; see the note there. Kept so an
484
+ * external consumer reading `system._mover` sees a null rather than a stale solver.
485
+ *
486
+ * @deprecated read {@link PerEntityRuntime#mover} instead
487
+ * @private
488
+ * @type {KinematicMover|null}
489
+ */
490
+ _mover = null;
491
+
492
+ /** @private Scratch velocity handed to the mover each tick. */
493
+ _moverVelocity = new Vector3();
494
+
495
+ /** @private Scratch for the posture-headroom up-cast. */
496
+ _postureRay = new Ray3();
497
+
498
+ /** @private @type {PhysicsSurfacePoint} */
499
+ _postureHit = new PhysicsSurfacePoint();
500
+
501
+ async startup(entityManager) {
502
+ this.entityManager = entityManager;
503
+ if (this.physicsSystem === null) {
504
+ const ps = entityManager.getSystem(PhysicsSystem);
505
+ if (ps !== null) this.physicsSystem = ps;
506
+ }
507
+ }
508
+
509
+ /**
510
+ * @param {FirstPersonPlayerController} controller
511
+ * @param {Transform64} bodyTransform
512
+ * @param {number} entity
513
+ */
514
+ link(controller, bodyTransform, entity) {
515
+ const ecd = this.entityManager.dataset;
516
+
517
+ // The controller assumes a kinematic-position RigidBody is co-
518
+ // attached on this entity. The body is the spatial proxy used
519
+ // for sensor raycasts and physics-side observers (other entities
520
+ // raycasting against the player, dynamic bodies colliding with
521
+ // the capsule, etc.). The controller writes Transform directly,
522
+ // physics derives velocity from the per-step delta. If a body is
523
+ // missing the controller could still drive the camera, but the
524
+ // physics integration silently breaks — assert here so the
525
+ // misconfiguration is caught at link time.
526
+ const rigidBody = ecd.getComponent(entity, RigidBody);
527
+ assert.ok(rigidBody !== undefined,
528
+ "FirstPersonPlayerController entity must have a co-attached RigidBody "
529
+ + "(kinematic capsule). See prototype_first_person_controller.js for setup.");
530
+ assert.equal(rigidBody.kind, BodyKind.KinematicPosition,
531
+ "FirstPersonPlayerController RigidBody must be BodyKind.KinematicPosition; "
532
+ + "the controller owns the Transform and physics derives velocity.");
533
+ // Collider is also required — the KinematicMover casts this shape
534
+ // against the world (recover / sweep / ground probe). Asserted
535
+ // here so a missing collider surfaces at link rather than
536
+ // producing a null-deref at the first cast attempt.
537
+ const collider = ecd.getComponent(entity, Collider);
538
+ assert.ok(collider !== undefined,
539
+ "FirstPersonPlayerController entity must have a co-attached Collider. "
540
+ + "The controller's move-and-slide casts this shape to detect blockers.");
541
+
542
+ const runtime = new PerEntityRuntime();
543
+ runtime.rigidBody = rigidBody;
544
+ runtime.collider = collider;
545
+
546
+ // Pre-build one capsule per posture from cfg.body. Eye-height
547
+ // doubles as collider-top by convention here — the prototype's
548
+ // `buildPlayerEntity` uses the same approximation (`totalHeight =
549
+ // bodyCfg.height`). The +Y offset puts the capsule bottom at
550
+ // transform.position so the player's "feet" stay anchored across
551
+ // posture changes; only the head drops/rises.
552
+ const radius = controller.config.body.radius;
553
+ runtime.colliderShapeStand = makePostureCapsule(radius, controller.config.body.height);
554
+ runtime.colliderShapeCrouch = makePostureCapsule(radius, controller.config.body.crouchHeight);
555
+ runtime.colliderShapeProne = makePostureCapsule(radius, controller.config.body.proneHeight);
556
+ // Cache each posture capsule's ACTUAL top (bbox maxY) — not the
557
+ // nominal config height. A capsule's height floors at 2·radius,
558
+ // so e.g. a 0.4 m "prone" height actually yields a 0.68 m
559
+ // capsule. The headroom up-cast sweeps by the difference of real
560
+ // tops; using config heights would over-/under-estimate the grow
561
+ // and clamp to the wrong posture (Prone instead of Crouch).
562
+ const bbox = new Float64Array(6);
563
+ runtime.colliderShapeStand.compute_bounding_box(bbox);
564
+ runtime.standTop = bbox[4];
565
+ runtime.colliderShapeCrouch.compute_bounding_box(bbox);
566
+ runtime.crouchTop = bbox[4];
567
+ runtime.colliderShapeProne.compute_bounding_box(bbox);
568
+ runtime.proneTop = bbox[4];
569
+ // Force a shape sync on the first tick: even though the caller
570
+ // built a Stand-sized collider, we rebuild it from cfg here so a
571
+ // post-link config tweak (e.g. crouchHeight changed for a unit
572
+ // test) is reflected on the live collider without a relink.
573
+ runtime.lastPosture = -1;
574
+
575
+ this.runtime.set(entity, runtime);
576
+
577
+ // Derive gravity + jump impulse from designer-friendly params, then
578
+ // mass-scale the initial velocity (heavier ⇒ lower jump).
579
+ runtime.massRatios = computeMassRatios(
580
+ controller.config.body.mass,
581
+ controller.config.body.referenceMass,
582
+ controller.config.body.massCouplingStrength,
583
+ );
584
+ const derived = { gravity: 0, initialVelocity: 0 };
585
+ computeJumpFromApex(controller.config.jump.peakHeight, controller.config.jump.timeToApex, derived);
586
+ runtime.gravity = derived.gravity;
587
+ // Express jump power as a physical push-off FORCE (newtons): the force the
588
+ // legs deliver to reach peakHeight at the REFERENCE mass. Stored so other
589
+ // moves (the scramble wall-kick) can launch off the SAME force.
590
+ const bodyCfg = controller.config.body;
591
+ runtime.jumpForce = jumpForceForHeight(
592
+ bodyCfg.referenceMass, runtime.gravity, bodyCfg.jumpPushoffDistance, controller.config.jump.peakHeight,
593
+ );
594
+ // Force → launch velocity for the reference body (== √(2·g·peakHeight)),
595
+ // then the mass-coupling dial (jumpV0Scale ∝ 1/√mass, modulated by
596
+ // massCouplingStrength) scales it for the actual body. Identical result
597
+ // to the prior `derived.initialVelocity · jumpV0Scale`, now routed
598
+ // through the force so jump power has a single physical source.
599
+ runtime.jumpInitialVy =
600
+ launchVelocityForForce(runtime.jumpForce, bodyCfg.referenceMass, bodyCfg.jumpPushoffDistance)
601
+ * runtime.massRatios.jumpV0Scale;
602
+
603
+ // Seed yaw from the starting body rotation. `toEulerAnglesYXZ`
604
+ // returns (pitch, yaw, roll) — we only care about y.
605
+ SCRATCH_Q_EULER.set(
606
+ bodyTransform.rotation_x,
607
+ bodyTransform.rotation_y,
608
+ bodyTransform.rotation_z,
609
+ bodyTransform.rotation_w
610
+ );
611
+ SCRATCH_Q_EULER.toEulerAnglesYXZ(SCRATCH_V3_A);
612
+ runtime.bodyYaw = SCRATCH_V3_A.y;
613
+ runtime.eyePitch = 0;
614
+
615
+ // Seed render-rate-look + position-interpolation state to the spawn pose.
616
+ runtime.prevFixedYaw = runtime.bodyYaw;
617
+ runtime.renderPrevPos.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
618
+ runtime.renderCurPos.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
619
+
620
+ // Initialize springs to standing-eye-height baseline
621
+ runtime.eyeHeightSpring.settle(controller.config.body.height);
622
+ runtime.fovSpring.settle(controller.config.fov.base);
623
+ controller.state.eyeHeight = controller.config.body.height;
624
+
625
+ // Create eye entity if one wasn't supplied
626
+ if (controller.eyeEntity === -1 || !ecd.entityExists(controller.eyeEntity)) {
627
+ const eye = new Entity();
628
+
629
+ const eyeTransform = new Transform64();
630
+ const baseEyePos = SCRATCH_V3_A.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
631
+ baseEyePos.y += controller.config.body.height;
632
+ eyeTransform.setTranslation(baseEyePos.x, baseEyePos.y, baseEyePos.z);
633
+
634
+ const camera = new Camera();
635
+ camera.active.set(true);
636
+ camera.fov.set(controller.config.fov.base);
637
+ camera.clip_near = 0.05;
638
+ camera.clip_far = 1000;
639
+ camera.autoClip = true;
640
+
641
+ eye.add(eyeTransform);
642
+ eye.add(camera);
643
+ eye.add(SerializationMetadata.Transient);
644
+
645
+ eye.build(ecd);
646
+
647
+ controller.eyeEntity = eye.id;
648
+ }
649
+
650
+ runtime.eyeEntity = controller.eyeEntity;
651
+ }
652
+
653
+ /**
654
+ * @param {FirstPersonPlayerController} controller
655
+ * @param {Transform64} bodyTransform
656
+ * @param {number} entity
657
+ */
658
+ unlink(controller, bodyTransform, entity) {
659
+ const ecd = this.entityManager.dataset;
660
+
661
+ if (controller.eyeEntity !== -1 && ecd.entityExists(controller.eyeEntity)) {
662
+ ecd.removeEntity(controller.eyeEntity);
663
+ controller.eyeEntity = -1;
664
+ }
665
+
666
+ this.runtime.delete(entity);
667
+ }
668
+
669
+ /**
670
+ * Look up the per-entity runtime for an entity that has this
671
+ * controller. Used by cross-system code (sensors system, future
672
+ * ability-driven systems) to reach internal state without leaking
673
+ * it onto the controller component itself.
674
+ *
675
+ * @param {number} entity
676
+ * @returns {PerEntityRuntime|undefined} undefined if entity is not linked
677
+ */
678
+ getRuntime(entity) {
679
+ return this.runtime.get(entity);
680
+ }
681
+
682
+ /**
683
+ * Deterministic simulation step — L1 + L2 + L4.
684
+ * @param {number} dt
685
+ */
686
+ fixedUpdate(dt) {
687
+ const ecd = this.entityManager.dataset;
688
+ if (ecd === null) return;
689
+
690
+ this._currentDt = dt;
691
+ ecd.traverseComponents(FirstPersonPlayerController, this._tickEntity, this);
692
+ }
693
+
694
+ /**
695
+ * Variable-rate camera composition — L3.
696
+ * @param {number} dt
697
+ */
698
+ update(dt) {
699
+ const ecd = this.entityManager.dataset;
700
+ if (ecd === null) return;
701
+
702
+ this._currentRenderDt = dt;
703
+ ecd.traverseComponents(FirstPersonPlayerController, this._composeEye, this);
704
+ }
705
+
706
+ /**
707
+ * @private
708
+ * @param {FirstPersonPlayerController} controller
709
+ * @param {number} entity
710
+ */
711
+ _tickEntity(controller, entity) {
712
+ const ecd = this.entityManager.dataset;
713
+ const runtime = this.runtime.get(entity);
714
+ if (runtime === undefined) return;
715
+
716
+ const dt = this._currentDt;
717
+ const cfg = controller.config;
718
+ const intent = controller.intent;
719
+ const state = controller.state;
720
+ const sig = controller.signals;
721
+
722
+ const bodyTransform = ecd.getComponent(entity, Transform64);
723
+ if (bodyTransform === undefined) return;
724
+
725
+ // Decay the mastery score's EMA. Doing this once per tick keeps the
726
+ // score's time-window characteristic stable regardless of how many
727
+ // evaluators fire (they each *record* a sample, the decay
728
+ // independently ages all samples).
729
+ controller.mastery.tick(dt);
730
+
731
+ // -- L1.a: Derive the per-step turn rate ------------------------
732
+ // Look (yaw/pitch) is consumed at RENDER rate in {@link _consumeLook} so
733
+ // the camera tracks the mouse without 60Hz aliasing — consuming the
734
+ // accumulated mouse delta once per fixed step beats against an unaligned
735
+ // mouse report rate and makes a slow turn lurch (the reported snap).
736
+ // fixedUpdate reads the latest yaw and derives the per-fixed-step turn
737
+ // RATE for look-lean + the turn evaluators, from the yaw change since the
738
+ // previous step (shortest-arc across the ±π wrap) — smooth and decoupled
739
+ // from per-frame input jitter.
740
+ let yawStep = runtime.bodyYaw - runtime.prevFixedYaw;
741
+ if (yawStep > Math.PI) yawStep -= TWO_PI;
742
+ else if (yawStep < -Math.PI) yawStep += TWO_PI;
743
+ runtime.yawRateRadPerSec = yawStep / Math.max(dt, 1e-4);
744
+ runtime.prevFixedYaw = runtime.bodyYaw;
745
+
746
+ // Write body yaw back to the transform (pure yaw, no pitch on body) —
747
+ // the collision sweep + sensors use it. The camera uses runtime.bodyYaw
748
+ // directly, which _consumeLook keeps current at render rate.
749
+ SCRATCH_Q_EULER.fromAxisAngle(Vector3.up, runtime.bodyYaw);
750
+ bodyTransform.setRotation(
751
+ SCRATCH_Q_EULER.x, SCRATCH_Q_EULER.y, SCRATCH_Q_EULER.z, SCRATCH_Q_EULER.w
752
+ );
753
+ bodyTransform.updateMatrix();
754
+
755
+ // -- Shared flags. Computed BEFORE the ability tick so abilities
756
+ // can read them. `isCrouchActive` is deliberately computed
757
+ // AFTER the ability tick because `_resolveCrouchHeld` mutates
758
+ // `runtime.prevCrouchHeld` — abilities like Slide need to see
759
+ // the previous-tick value to detect a rising edge on the
760
+ // crouch press.
761
+ const isSprintIntent = intent.sprint && intent.move.y > 0.5 && state.grounded;
762
+ const isBackwardIntent = intent.move.y < 0;
763
+ runtime.sinYaw = Math.sin(runtime.bodyYaw);
764
+ runtime.cosYaw = Math.cos(runtime.bodyYaw);
765
+ // L2 observers read sinYaw/cosYaw as locals — destructure once.
766
+ const { sinYaw, cosYaw } = runtime;
767
+
768
+ // Age the ledge re-grab refractory window (dt-driven). Decremented
769
+ // BEFORE the ability layer so LedgeGrab.canActivate sees the current
770
+ // value; it gates the auto-catch for a beat after any dismount.
771
+ if (runtime.ledgeRegrabCooldown > 0) {
772
+ runtime.ledgeRegrabCooldown = Math.max(0, runtime.ledgeRegrabCooldown - dt);
773
+ }
774
+
775
+ // -- Ability layer: at most one active ability owns motion. The
776
+ // set returns true when no ability owned the tick, in which
777
+ // case base L1.b-h runs below; false means an ability fully
778
+ // handled this tick (it called the system's helpers for any
779
+ // standard work it wanted to keep, e.g. gravity).
780
+ const runBaseLocomotion = controller.abilities.tick(
781
+ controller, runtime, bodyTransform, runtime.sensors, dt, this,
782
+ );
783
+
784
+ // The ledge-pull-up request is set and consumed WITHIN abilities.tick
785
+ // (LedgeGrab release → Mantle activation, same call). Clear it here as
786
+ // a safety net so a pull-up that didn't activate a mantle can't leak
787
+ // the flag into a later auto-mantle (which would let it vault a thin
788
+ // wall it should have refused).
789
+ runtime.ledgePullUpRequest = false;
790
+
791
+ // Now resolve crouch (updates prevCrouchHeld) — used by base and L2.
792
+ // Headroom-aware: force-keeps the player crouched under an overhang.
793
+ const isCrouchActive = this._resolveCrouchHeld(controller, runtime, bodyTransform);
794
+
795
+ if (runBaseLocomotion) {
796
+ this._runBaseLocomotion(
797
+ controller, runtime, bodyTransform, dt,
798
+ isCrouchActive, isSprintIntent, isBackwardIntent,
799
+ );
800
+ }
801
+
802
+ // (everything below this line runs every tick — L2 observers don't
803
+ // care who owned motion)
804
+
805
+ // -- L2.a: speed / moveMode ------------------------------------
806
+ // -- L2.a: speed / moveMode ------------------------------------
807
+ const horizSpeed = Math.hypot(runtime.velocityX, runtime.velocityZ);
808
+ runtime.horizSpeed = horizSpeed;
809
+ state.speed = horizSpeed;
810
+ state.speedNormalized = clamp(horizSpeed / Math.max(cfg.motion.sprintSpeed, 1e-3), 0, 1);
811
+
812
+ // Backwardness: 0 = moving forward (or sideways), 1 = moving directly
813
+ // backward at the back-pedal speed ceiling. Derived from the actual
814
+ // velocity (not the intent) so external knockback or stuck states
815
+ // also register as "moving backward" and the gait wobble reflects it.
816
+ //
817
+ // Reference speed is the *achievable* backward max — walkSpeed ×
818
+ // backwardSpeedFactor — NOT the sprint speed. Backward can never
819
+ // reach sprint, so normalizing against sprint would cap backwardness
820
+ // at ~0.3 and the wobble multipliers below would barely apply.
821
+ const screenFwdVel = runtime.velocityX * sinYaw + runtime.velocityZ * cosYaw;
822
+ const maxBackwardSpeed = Math.max(cfg.motion.walkSpeed * cfg.motion.backwardSpeedFactor, 1e-3);
823
+ runtime.backwardness = clamp(-screenFwdVel / maxBackwardSpeed, 0, 1);
824
+
825
+ // Locomotion mode is the *intent-driven* horizontal mode. Airborne
826
+ // state is tracked separately on pose.actionState — they're
827
+ // orthogonal facets (you can be Sprint+Airborne after a jump).
828
+ const prevLocomotionMode = state.locomotionMode;
829
+ if (isCrouchActive) {
830
+ state.locomotionMode = FirstPersonLocomotionMode.Crouch;
831
+ } else if (isSprintIntent && horizSpeed > 0.1) {
832
+ state.locomotionMode = FirstPersonLocomotionMode.Sprint;
833
+ } else if (horizSpeed > 0.1) {
834
+ state.locomotionMode = FirstPersonLocomotionMode.Walk;
835
+ } else {
836
+ state.locomotionMode = FirstPersonLocomotionMode.Idle;
837
+ }
838
+
839
+ if (state.locomotionMode === FirstPersonLocomotionMode.Sprint
840
+ && prevLocomotionMode !== FirstPersonLocomotionMode.Sprint) {
841
+ sig.onSprintStart.send0();
842
+ } else if (prevLocomotionMode === FirstPersonLocomotionMode.Sprint
843
+ && state.locomotionMode !== FirstPersonLocomotionMode.Sprint) {
844
+ sig.onSprintStop.send0();
845
+ }
846
+
847
+ // -- L2.b: Exertion --------------------------------------------
848
+ // Heavier bodies tire faster — sprint rise scales with massRatios.exertionRiseScale.
849
+ const exertionRise = isSprintIntent
850
+ ? cfg.exertion.sprintRiseRate * runtime.massRatios.exertionRiseScale
851
+ : 0;
852
+ const exertionFall = exertionRise > 0 ? 0 : cfg.exertion.idleDecayRate;
853
+ state.exertion = clamp(state.exertion + (exertionRise - exertionFall) * dt, 0, 1);
854
+
855
+ // -- L2.c: Breath ----------------------------------------------
856
+ // breathRate and breathAmplitude lag exertion through separate
857
+ // exponential decays. Rate hangs around longer than amplitude.
858
+ const metabolicRate = lerp(cfg.breath.rateRestHz, cfg.breath.rateMaxHz, state.exertion);
859
+ const targetAmp = lerp(cfg.breath.amplitudeRestM, cfg.breath.amplitudeMaxM, state.exertion);
860
+
861
+ // Locomotor-respiratory coupling — see math/computeLRCBreathRate.
862
+ // The pure function is unit-tested; this site just provides inputs.
863
+ //
864
+ // Gait is gated on a "feet strike the ground" posture (Stand /
865
+ // Crouch). Prone (slide) and Hang (ledge-grab) have no stride —
866
+ // the body's feet are not making contact in a walking pattern,
867
+ // so stride frequency drops to zero and downstream gait
868
+ // signals (footsteps, bob intensity) go quiet.
869
+ const feetStriking = state.posture === FirstPersonPosture.Stand
870
+ || state.posture === FirstPersonPosture.Crouch;
871
+ const strideFreqHz = feetStriking && state.grounded && horizSpeed > cfg.bob.minStepSpeed
872
+ ? cfg.bob.stepFreqAtWalk * Math.pow(
873
+ Math.max(horizSpeed, 1e-3) / Math.max(cfg.motion.walkSpeed, 1e-3),
874
+ cfg.bob.stepFreqExp,
875
+ )
876
+ : 0;
877
+ const targetRate = computeLRCBreathRate(
878
+ metabolicRate,
879
+ strideFreqHz,
880
+ state.exertion,
881
+ cfg.breath.locomotorCouplingMax,
882
+ cfg.breath.couplingMinStrideFreqHz,
883
+ );
884
+ state.breathRateHz = exponentialApproach(state.breathRateHz, targetRate, cfg.exertion.rateDecayHalfLife, dt);
885
+ state.breathAmplitudeM = exponentialApproach(state.breathAmplitudeM, targetAmp, cfg.exertion.ampDecayHalfLife, dt);
886
+
887
+ runtime.prevBreathPhase = state.breathPhase;
888
+ state.breathPhase += state.breathRateHz * dt;
889
+ state.breathPhase -= Math.floor(state.breathPhase); // wrap [0,1)
890
+
891
+ // Breath edge detection — inhale at 0.25, exhale at 0.75
892
+ if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.25)) {
893
+ sig.onBreathIn.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
894
+ }
895
+ if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.75)) {
896
+ sig.onBreathOut.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
897
+ }
898
+
899
+ // -- L2.d: Stride ----------------------------------------------
900
+ // strideFreqHz computed above in the breath block; reused here.
901
+ runtime.prevStridePhase = state.stridePhase;
902
+ if (strideFreqHz > 0) {
903
+ // 1 full stride cycle = 2 footfalls; phase advances at freq/2 of cycle
904
+ state.stridePhase += (strideFreqHz * 0.5) * dt;
905
+ state.stridePhase -= Math.floor(state.stridePhase);
906
+ }
907
+ // Footstep on phase wraparound past 0 (R) or past 0.5 (L). Same
908
+ // posture gate as stride advance — feet must be striking.
909
+ if (feetStriking && state.grounded && horizSpeed > cfg.bob.minStepSpeed) {
910
+ const fireFootstep = () => {
911
+ state.stepCount++;
912
+ const side = runtime.nextFootSide;
913
+ runtime.nextFootSide = side === "R" ? "L" : "R";
914
+ // The foot that just fired is now the one bearing weight
915
+ // through the upcoming half-stride. Drives lateral-bob sign.
916
+ runtime.standingFoot = side;
917
+ sig.onFootStep.send1({ side, speed: horizSpeed, surfaceTag: state.surfaceTag });
918
+ // Kick the vertical impact spring DOWNWARD. The kick magnitude
919
+ // is the per-step desired peak dip × impactKickMultiplier; the
920
+ // multiplier is empirical (depends on impact spring params) so
921
+ // that "verticalAmpAtWalk" still corresponds approximately to
922
+ // the visible peak dip depth. Scaled by bobIntensity so a
923
+ // mid-deceleration footstep doesn't deliver a full-strength
924
+ // impulse.
925
+ const massBoost = (cfg.body.mass - 80) * cfg.bob.ampMassScale;
926
+ const ampVMult = 1 + (cfg.bob.backwardVerticalAmpFactor - 1) * runtime.backwardness;
927
+ const peakDip = (cfg.bob.verticalAmpAtWalk + massBoost) * runtime.bobIntensitySpring.value * ampVMult;
928
+ runtime.verticalImpactSpring.kick(-peakDip * cfg.bob.impactKickMultiplier);
929
+ };
930
+ if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0)) {
931
+ fireFootstep();
932
+ }
933
+ if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0.5)) {
934
+ fireFootstep();
935
+ }
936
+ }
937
+
938
+ // -- L2.d.bob-intensity & impact -------------------------------
939
+ // Smoothed bob amplitude envelope: when the player starts/stops
940
+ // moving the visible bob fades in/out rather than cutting on/off.
941
+ // Target = the "natural" amp scale (max of speed and backwardness)
942
+ // while grounded, zero while airborne so the bob disappears mid-jump.
943
+ const naturalBobIntensity = Math.max(state.speedNormalized, runtime.backwardness);
944
+ // Bob fades to zero whenever feet aren't striking (airborne, or
945
+ // Prone/Hang posture). The verticalImpactSpring (separate
946
+ // channel) still carries any entry/landing kicks through to the
947
+ // camera, but no recurring step bob.
948
+ const targetBobIntensity = (state.grounded && feetStriking) ? naturalBobIntensity : 0;
949
+ runtime.bobIntensitySpring.stepTo(targetBobIntensity, cfg.bob.intensityHalfLife, 1.0, dt);
950
+
951
+ // Vertical impact spring — damped decay toward 0, with the under-
952
+ // damped overshoot that produces the recovery + leg-push curve.
953
+ runtime.verticalImpactSpring.stepTo(0, cfg.bob.impactSpringHalfLife, cfg.bob.impactSpringZeta, dt);
954
+
955
+ // Sprint posture — head pitches forward as commitment to sprint
956
+ // builds. Driven by "sprintness" — how much of the gap between
957
+ // walk and sprint speed the player is *currently* in (0..1). The
958
+ // pitch target is multiplied by sprintness, then critically damped.
959
+ // Only applies while grounded — pitching into airborne motion looks weird.
960
+ const sprintness = clamp(
961
+ (state.speed - cfg.motion.walkSpeed)
962
+ / Math.max(cfg.motion.sprintSpeed - cfg.motion.walkSpeed, 1e-3),
963
+ 0, 1,
964
+ );
965
+ const targetSprintPitch = state.grounded
966
+ ? cfg.posture.sprintForwardPitchDeg * DEG_TO_RAD * sprintness
967
+ : 0;
968
+ runtime.sprintPostureSpring.stepTo(
969
+ targetSprintPitch,
970
+ cfg.posture.sprintForwardPitchHalfLife,
971
+ 1.0, dt,
972
+ );
973
+ runtime.sprintness = sprintness;
974
+
975
+ // Head droop — exertion drives a subtle additional forward pitch.
976
+ // Combines with sprintPostureSpring (sprint = head down to commit)
977
+ // so a fatigued sprinter has BOTH effects layered.
978
+ const targetDroopRad = cfg.exertion.headDroopAtMaxDeg * DEG_TO_RAD * state.exertion;
979
+ runtime.headDroopSpring.stepTo(targetDroopRad, cfg.exertion.headDroopHalfLife, 1.0, dt);
980
+
981
+ // -- L2.e: Posture → eye height --------------------------------
982
+ // Posture is set by whichever layer owned motion this tick: base
983
+ // writes Stand / Crouch from isCrouchActive (see end of
984
+ // _runBaseLocomotion); active abilities write Prone (Slide) or
985
+ // Hang (LedgeGrab) in their tick. Mapping is one switch — adding
986
+ // a new posture is one enum value + one case.
987
+ let targetEyeH;
988
+ switch (state.posture) {
989
+ case FirstPersonPosture.Prone: targetEyeH = cfg.body.proneHeight; break;
990
+ case FirstPersonPosture.Crouch: targetEyeH = cfg.body.crouchHeight; break;
991
+ case FirstPersonPosture.Hang: targetEyeH = cfg.body.height; break;
992
+ case FirstPersonPosture.Stand:
993
+ default: targetEyeH = cfg.body.height; break;
994
+ }
995
+ const crouchHalfLife = cfg.crouch.transitionTime / 4; // halfLife is ~quarter of full transition
996
+ runtime.eyeHeightSpring.stepTo(targetEyeH, crouchHalfLife, 1.0, dt);
997
+ state.eyeHeight = runtime.eyeHeightSpring.value;
998
+
999
+ if (isCrouchActive !== state.crouchActive) {
1000
+ state.crouchActive = isCrouchActive;
1001
+ if (isCrouchActive) {
1002
+ sig.onCrouchEnter.send0();
1003
+ // Impulse: dropping into a crouch grips the knees. Small
1004
+ // bump — we don't want crouch-spamming to instantly tire.
1005
+ state.exertion = clamp(
1006
+ state.exertion + cfg.exertion.crouchEnterRise * runtime.massRatios.exertionRiseScale,
1007
+ 0, 1,
1008
+ );
1009
+ } else {
1010
+ sig.onCrouchExit.send0();
1011
+ }
1012
+ }
1013
+
1014
+ // -- L2.f: Lean spring → camera roll ---------------------------
1015
+ // The TARGET for this tick was written by whichever layer owned
1016
+ // motion: base writes the lat-accel + look-lean derived value at
1017
+ // the end of _runBaseLocomotion; abilities override (WallRun
1018
+ // tilts toward the wall; Slide / LedgeGrab / Mantle force zero).
1019
+ // L2.f is now a flat spring-step + commit — no branching, no
1020
+ // null sentinel.
1021
+ runtime.prevVelocityX = runtime.velocityX;
1022
+ runtime.prevVelocityZ = runtime.velocityZ;
1023
+ runtime.leanSpring.stepTo(runtime.leanTargetRad, cfg.lean.spring.halfLife, cfg.lean.spring.zeta, dt);
1024
+ state.leanRollRad = runtime.leanSpring.value;
1025
+
1026
+ // -- L2.g: Land spring decay (drives the landing recovery dip) -
1027
+ // Target is 0; under-damped (cfg zeta < 1) so it rings.
1028
+ runtime.landSpring.stepTo(0, cfg.landing.recovery.spring.halfLife, cfg.landing.recovery.spring.zeta, dt);
1029
+
1030
+ // -- L2.h: Publish pose channels --------------------------------
1031
+ this._publishPose(controller, runtime, bodyTransform);
1032
+
1033
+ // -- L2.h.1: Say that the body moved ----------------------------
1034
+ // The yaw write above and the mover's write into the translation both land straight on the
1035
+ // component, and a Transform64 announces nothing on its own. One event here, at the point
1036
+ // where the pose is final, covers every writer in the tick and covers them once - which is
1037
+ // what wakes the mesh placement, the audio emitter and anything else that follows a player.
1038
+ t64_announce_change(ecd, entity);
1039
+
1040
+ // -- L2.i: Sync collider shape to posture -----------------------
1041
+ // All posture-writers (base locomotion + any active ability)
1042
+ // have run for this tick. Swap the collider's shape to the
1043
+ // pre-built capsule matching the final posture so downstream
1044
+ // physics queries (move-and-slide cast, sensors, overlap from
1045
+ // outside) see the right volume. No-op when posture is
1046
+ // unchanged.
1047
+ this._syncColliderShape(runtime, state.posture);
1048
+ }
1049
+
1050
+ /**
1051
+ * @private
1052
+ * @param {FirstPersonPlayerController} controller
1053
+ * @param {PerEntityRuntime} runtime
1054
+ * @returns {boolean}
1055
+ */
1056
+ /**
1057
+ * Swap {@link Collider.shape} to the pre-built capsule that matches
1058
+ * the player's current posture. Cheap — just a reference swap when
1059
+ * the posture changed, no-op otherwise. The pre-built shapes live
1060
+ * on the runtime (see {@link PerEntityRuntime.colliderShapeStand}
1061
+ * etc.) so this method allocates nothing per tick.
1062
+ *
1063
+ * Hang posture reuses Stand: the player's body is full-extent,
1064
+ * hanging below the ledge — the rig handles the arms-up animation,
1065
+ * but the collision volume is unchanged. If a game ever wants a
1066
+ * narrower hang silhouette (e.g. wedging into a chimney) it can
1067
+ * add a `colliderShapeHang` and route here.
1068
+ *
1069
+ * @private
1070
+ */
1071
+ _syncColliderShape(runtime, posture) {
1072
+ if (posture === runtime.lastPosture) return;
1073
+ let next;
1074
+ if (posture === FirstPersonPosture.Crouch) {
1075
+ next = runtime.colliderShapeCrouch;
1076
+ } else if (posture === FirstPersonPosture.Prone) {
1077
+ next = runtime.colliderShapeProne;
1078
+ } else {
1079
+ // Stand and Hang share the full-extent capsule.
1080
+ next = runtime.colliderShapeStand;
1081
+ }
1082
+ runtime.collider.shape = next;
1083
+ runtime.lastPosture = posture;
1084
+ }
1085
+
1086
+ _resolveCrouchHeld(controller, runtime, bodyTransform) {
1087
+ const cfg = controller.config;
1088
+ const intent = controller.intent;
1089
+ const state = controller.state;
1090
+
1091
+ let wantsCrouch;
1092
+ if (cfg.crouch.mode === "toggle") {
1093
+ // Edge: rising press flips the latch
1094
+ if (intent.crouch && !runtime.prevCrouchHeld) {
1095
+ runtime.crouchLatched = !runtime.crouchLatched;
1096
+ }
1097
+ runtime.prevCrouchHeld = intent.crouch;
1098
+ wantsCrouch = runtime.crouchLatched;
1099
+ } else {
1100
+ // "hold" mode
1101
+ runtime.prevCrouchHeld = intent.crouch;
1102
+ wantsCrouch = intent.crouch;
1103
+ }
1104
+
1105
+ // Headroom override — block UN-crouching under an overhang. A player who
1106
+ // is ALREADY low (crouched or prone) and wants to stand is force-kept
1107
+ // crouched while the Stand collider won't fit: letting it grow into the
1108
+ // overhang would have the next tick's depenetration shove the player out
1109
+ // the bottom, through the floor. `crouchBlocked` records this for HUD /
1110
+ // camera.
1111
+ //
1112
+ // Crucially this fires ONLY when already low. A STANDING player who walks
1113
+ // under a low ceiling is NEVER auto-pushed down into a crouch — their
1114
+ // full-height capsule simply can't fit, so the mover blocks their forward
1115
+ // motion instead. "If I can't clear it, I don't move forward." Dropping
1116
+ // posture is the player's choice (press crouch), not the ceiling's.
1117
+ const alreadyLow = runtime.lastPosture === FirstPersonPosture.Crouch
1118
+ || runtime.lastPosture === FirstPersonPosture.Prone;
1119
+ if (!wantsCrouch && alreadyLow) {
1120
+ const growDelta = runtime.standTop - runtime.crouchTop;
1121
+ if (!this._hasHeadroomToGrow(runtime, bodyTransform, runtime.colliderShapeCrouch, growDelta)) {
1122
+ state.crouchBlocked = true;
1123
+ return true;
1124
+ }
1125
+ }
1126
+ state.crouchBlocked = false;
1127
+ return wantsCrouch;
1128
+ }
1129
+
1130
+ /**
1131
+ * Headroom probe for a posture change that GROWS the collider.
1132
+ * Sweeps `fromShape` (the shorter posture's capsule) straight up by
1133
+ * `growDelta` (the height the collider would gain). A hit means an
1134
+ * overhang occupies the space the taller posture needs, so the grow
1135
+ * is blocked. Returns true (clear) when no physics is wired or the
1136
+ * grow is non-positive.
1137
+ *
1138
+ * Why an up-cast rather than an overlap-test of the taller shape:
1139
+ * overlap would false-positive on a wall *beside* the player. The
1140
+ * up-cast sweeps parallel to (and, per the mover's skin clearance,
1141
+ * clear of) lateral walls so they aren't hit; the floor is below
1142
+ * the rising probe; the player's own body is filtered out. Only
1143
+ * genuine overhead geometry blocks the grow. Relies on the player
1144
+ * resting at `floor+skin` (the mover's stick-to-ground), so the
1145
+ * probe's bottom starts clear of the floor.
1146
+ *
1147
+ * @private
1148
+ */
1149
+ _hasHeadroomToGrow(runtime, bodyTransform, fromShape, growDelta) {
1150
+ if (this.physicsSystem === null || growDelta <= 1e-4) return true;
1151
+ const ownCollider = runtime.collider;
1152
+ const filter = (_e, c) => c !== ownCollider;
1153
+ const ray = this._postureRay;
1154
+ ray.setOrigin(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
1155
+ ray.setDirection(0, 1, 0);
1156
+ ray.tMax = growDelta;
1157
+ return !this.physicsSystem.shapeCast(ray, fromShape, bodyTransform.rotation, this._postureHit, filter);
1158
+ }
1159
+
1160
+ /**
1161
+ * Jump finite-state-machine: button-edge detection, buffer + coyote
1162
+ * grace, anticipation timer, impulse on completion. Variable-height
1163
+ * cut is captured here as a `state.isVariableJumpCut` flag that the
1164
+ * gravity step in `_integrateVerticalAndResolveGround` consumes.
1165
+ *
1166
+ * @private
1167
+ * @param {FirstPersonPlayerController} controller
1168
+ * @param {PerEntityRuntime} runtime
1169
+ * @param {Transform64} bodyTransform
1170
+ * @param {number} dt
1171
+ */
1172
+ _advanceJumpFsm(controller, runtime, bodyTransform, dt) {
1173
+ const cfg = controller.config;
1174
+ const intent = controller.intent;
1175
+ const state = controller.state;
1176
+ const sig = controller.signals;
1177
+
1178
+ const jumpPressedEdge = intent.jump && !runtime.prevJumpHeld;
1179
+ const jumpReleasedEdge = !intent.jump && runtime.prevJumpHeld;
1180
+ runtime.prevJumpHeld = intent.jump;
1181
+
1182
+ if (jumpPressedEdge) {
1183
+ state.jumpBufferRemaining = cfg.jump.bufferTime;
1184
+ }
1185
+ state.jumpBufferRemaining = Math.max(0, state.jumpBufferRemaining - dt);
1186
+
1187
+ const canJumpNow =
1188
+ (state.grounded || state.timeSinceGrounded < cfg.jump.coyoteTime)
1189
+ && state.jumpBufferRemaining > 0
1190
+ && !state.inJumpAnticipation
1191
+ && !runtime.midJump;
1192
+
1193
+ if (canJumpNow) {
1194
+ // Begin anticipation — squash; impulse fires after duration elapses
1195
+ state.inJumpAnticipation = true;
1196
+ runtime.anticipationRemaining = cfg.jump.anticipation.duration;
1197
+ state.jumpBufferRemaining = 0; // claimed
1198
+ }
1199
+
1200
+ // Variable-height cut: only valid during ascent, post-launch.
1201
+ if (jumpReleasedEdge && runtime.midJump && runtime.velocityY > 0) {
1202
+ state.isVariableJumpCut = true;
1203
+ }
1204
+
1205
+ // Anticipation timer; impulse on completion.
1206
+ //
1207
+ // Anticipation completes regardless of grounded state. The reason
1208
+ // we DON'T cancel on `!grounded`: the canonical coyote-jump path
1209
+ // depends on it. The player walks off a ledge (grounded → false),
1210
+ // presses jump within the coyote window, canJumpNow accepts on
1211
+ // the coyote branch and starts anticipation. If we cancelled
1212
+ // anticipation here on !grounded, the impulse would never fire
1213
+ // and "coyote time" would be silently dead — the FSM's own next-
1214
+ // statement contradicting the canJumpNow gate three lines up.
1215
+ //
1216
+ // The same logic handles the rug-pull case (player on a moving
1217
+ // platform that slides out mid-anticipation): the player
1218
+ // committed to the jump, they get the jump. A future
1219
+ // knockback / stagger system can explicitly clear
1220
+ // inJumpAnticipation if it wants to override that commitment.
1221
+ if (state.inJumpAnticipation) {
1222
+ runtime.anticipationRemaining -= dt;
1223
+ if (runtime.anticipationRemaining <= 0) {
1224
+ // Mastery: gather a multiplier from all evaluators
1225
+ // registered for JumpImpulse. Default (no evaluators)
1226
+ // returns 1.0 → unchanged behaviour.
1227
+ const masteryMul = controller.mastery.evaluate(
1228
+ DecisionPoint.JumpImpulse, controller, runtime,
1229
+ );
1230
+ runtime.velocityY = runtime.jumpInitialVy * masteryMul;
1231
+ runtime.midJump = true;
1232
+ runtime.apexFired = false;
1233
+ runtime.peakAltitude = bodyTransform.translation_y;
1234
+ state.inJumpAnticipation = false;
1235
+ state.isVariableJumpCut = false;
1236
+ state.isAscending = true;
1237
+ state.exertion = clamp(
1238
+ state.exertion + cfg.exertion.jumpRise * runtime.massRatios.exertionRiseScale,
1239
+ 0, 1,
1240
+ );
1241
+
1242
+ sig.onJumpStart.send1({ peakHeight: cfg.jump.peakHeight });
1243
+ // NOTE: onLeaveGround is fired by the motor (_onLeaveGround)
1244
+ // when the move actually breaks ground contact this same tick —
1245
+ // with reason "jump" since midJump is now true. Firing it here
1246
+ // too double-sent the signal on every grounded jump (and a
1247
+ // spurious second "jump" leave on a coyote jump, which already
1248
+ // left ground on the walk-off). Let the motor own it.
1249
+ }
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * The vertical + collision phase of one fixed step: apply gravity
1255
+ * (the motor), resolve the move against the world, then detect the
1256
+ * jump apex. Collision resolution is the {@link KinematicMover} when
1257
+ * a physics layer is present; otherwise a lightweight flat-ground
1258
+ * integrator for headless / unit-test scenes with no collidable
1259
+ * geometry. Land / leave-ground events flow identically from either.
1260
+ *
1261
+ * @private
1262
+ * @param {FirstPersonPlayerController} controller
1263
+ * @param {PerEntityRuntime} runtime
1264
+ * @param {Transform64} bodyTransform
1265
+ * @param {number} dt
1266
+ */
1267
+ _integrateVerticalAndResolveGround(controller, runtime, bodyTransform, dt) {
1268
+ this._applyGravity(controller, runtime, dt);
1269
+ this._resolveMotion(controller, runtime, bodyTransform, dt);
1270
+ this._detectJumpApex(controller, runtime, bodyTransform);
1271
+ }
1272
+
1273
+ /**
1274
+ * Resolve the current `runtime.velocity*` against the world — the
1275
+ * physics-backed {@link KinematicMover} (recover + sweep-and-slide +
1276
+ * ground-categorize) when a PhysicsSystem is present, else the no-physics
1277
+ * flat-ground fallback. Gravity is NOT applied here: the standard path
1278
+ * applies it in {@link _applyGravity} just before, and abilities with a
1279
+ * non-standard vertical model (WallRun's reduced gravity) apply their own
1280
+ * and then call this directly — so motion routes through one motor for
1281
+ * everyone (sweep-and-slide, anti-tunnel, land/leave events) rather than
1282
+ * a bespoke `position._add` + hand-rolled ground-catch.
1283
+ * @private
1284
+ */
1285
+ _resolveMotion(controller, runtime, bodyTransform, dt) {
1286
+ if (this.physicsSystem !== null) {
1287
+ this._moveViaMover(controller, runtime, bodyTransform, dt);
1288
+ } else {
1289
+ this._moveFlatGround(controller, runtime, bodyTransform, dt);
1290
+ }
1291
+ }
1292
+
1293
+ /**
1294
+ * Motor: integrate gravity into `velocityY` with the fall / variable-
1295
+ * cut multipliers. The mover never invents motion, so gravity lives
1296
+ * here (callable by abilities that want standard gravity too).
1297
+ * @private
1298
+ */
1299
+ _applyGravity(controller, runtime, dt) {
1300
+ const cfg = controller.config;
1301
+ const state = controller.state;
1302
+ let gMag = runtime.gravity;
1303
+ if (runtime.velocityY <= 0) {
1304
+ gMag *= cfg.jump.fallGravityMult;
1305
+ state.isAscending = false;
1306
+ } else if (state.isVariableJumpCut) {
1307
+ gMag *= cfg.jump.cutGravityMult;
1308
+ }
1309
+ runtime.velocityY -= gMag * dt;
1310
+ }
1311
+
1312
+ /**
1313
+ * Resolve the move against the world via {@link KinematicMover}
1314
+ * (recover + unified sweep-and-slide + ground-categorize), then map
1315
+ * the result back to controller state and land / leave events.
1316
+ * @private
1317
+ */
1318
+ _moveViaMover(controller, runtime, bodyTransform, dt) {
1319
+ const state = controller.state;
1320
+
1321
+ // per character, not per system - see the note on PerEntityRuntime#mover
1322
+ if (runtime.mover === null) {
1323
+ runtime.mover = new KinematicMover(this.physicsSystem, this.entityManager.dataset);
1324
+ }
1325
+
1326
+ // Fall speed captured BEFORE the move — the mover clips velocityY
1327
+ // to ~0 on contact, so the land event needs the pre-move value.
1328
+ const impactVy = -runtime.velocityY;
1329
+
1330
+ const v = this._moverVelocity;
1331
+ v.set(runtime.velocityX, runtime.velocityY, runtime.velocityZ);
1332
+
1333
+ const ownCollider = runtime.collider;
1334
+ const filter = (_e, c) => c !== ownCollider;
1335
+
1336
+ const result = runtime.mover.move(
1337
+ bodyTransform.translation,
1338
+ bodyTransform.rotation,
1339
+ runtime.collider.shape,
1340
+ v, dt, filter,
1341
+ );
1342
+
1343
+ // Read back corrected horizontal velocity. Vertical follows the
1344
+ // control policy: zero on ground (the slope-climb the mover
1345
+ // produced is a position effect, not accumulated speed), keep the
1346
+ // mover-clipped value in the air (captures ceiling bonks).
1347
+ runtime.velocityX = v.x;
1348
+ runtime.velocityZ = v.z;
1349
+ runtime.velocityY = result.grounded ? 0 : v.y;
1350
+
1351
+ const wasGrounded = state.grounded;
1352
+ if (result.grounded) {
1353
+ if (!wasGrounded) this._onLand(controller, runtime, impactVy);
1354
+ state.grounded = true;
1355
+ state.groundNormal.copy(result.groundNormal);
1356
+ state.verticalSpeed = 0;
1357
+ state.airborneTime = 0;
1358
+ state.timeSinceGrounded = 0;
1359
+ } else {
1360
+ if (wasGrounded) this._onLeaveGround(controller, runtime, bodyTransform);
1361
+ state.grounded = false;
1362
+ state.verticalSpeed = runtime.velocityY;
1363
+ state.airborneTime += dt;
1364
+ state.timeSinceGrounded += dt;
1365
+ state.fallDistance += Math.max(0, -runtime.velocityY * dt);
1366
+ }
1367
+ }
1368
+
1369
+ /**
1370
+ * No-physics fallback: integrate the velocity directly (there's no
1371
+ * collidable geometry to sweep against) and resolve the floor from
1372
+ * the built-in flat ground and/or the host `groundResolver`. For
1373
+ * headless scenes and control-layer unit tests. With both ground
1374
+ * sources off, an external physics layer is expected to own
1375
+ * `state.grounded`; we only advance timers.
1376
+ * @private
1377
+ */
1378
+ _moveFlatGround(controller, runtime, bodyTransform, dt) {
1379
+ const state = controller.state;
1380
+
1381
+ // translation lands in the matrix as it is written, so no updateMatrix is owed here
1382
+ bodyTransform.setTranslation(
1383
+ bodyTransform.translation_x + runtime.velocityX * dt,
1384
+ bodyTransform.translation_y + runtime.velocityY * dt,
1385
+ bodyTransform.translation_z + runtime.velocityZ * dt
1386
+ );
1387
+
1388
+ if (this.useBuiltInFlatGround || this.groundResolver !== null) {
1389
+ // Effective ground = max(built-in flat ground, resolver).
1390
+ let testY = this.useBuiltInFlatGround ? this.groundY : Number.NEGATIVE_INFINITY;
1391
+ if (this.groundResolver !== null) {
1392
+ const resolved = this.groundResolver(
1393
+ bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z,
1394
+ );
1395
+ if (resolved !== null && resolved > testY) testY = resolved;
1396
+ }
1397
+ const haveGround = testY !== Number.NEGATIVE_INFINITY;
1398
+ if (haveGround && bodyTransform.translation_y <= testY) {
1399
+ bodyTransform.setTranslation(bodyTransform.translation_x, testY, bodyTransform.translation_z);
1400
+ if (!state.grounded) this._onLand(controller, runtime, -runtime.velocityY);
1401
+ state.grounded = true;
1402
+ state.groundNormal.set(0, 1, 0);
1403
+ state.verticalSpeed = 0;
1404
+ runtime.velocityY = 0;
1405
+ state.airborneTime = 0;
1406
+ state.timeSinceGrounded = 0;
1407
+ } else {
1408
+ if (state.grounded) this._onLeaveGround(controller, runtime, bodyTransform);
1409
+ state.grounded = false;
1410
+ state.verticalSpeed = runtime.velocityY;
1411
+ state.airborneTime += dt;
1412
+ state.timeSinceGrounded += dt;
1413
+ state.fallDistance += Math.max(0, -runtime.velocityY * dt);
1414
+ }
1415
+ } else {
1416
+ // External physics maintains state.grounded; just track timers.
1417
+ if (state.grounded) {
1418
+ state.timeSinceGrounded = 0;
1419
+ state.airborneTime = 0;
1420
+ } else {
1421
+ state.timeSinceGrounded += dt;
1422
+ state.airborneTime += dt;
1423
+ }
1424
+ }
1425
+ }
1426
+
1427
+ /**
1428
+ * Landing transition: dip / impact springs, exertion impulse, flag
1429
+ * resets, and the `onLand` signal (fired LAST so handlers see fully-
1430
+ * reacted state). Shared by both move paths. `impactVy` is the
1431
+ * downward speed at touchdown (positive).
1432
+ * @private
1433
+ */
1434
+ _onLand(controller, runtime, impactVy) {
1435
+ const cfg = controller.config;
1436
+ const state = controller.state;
1437
+ const kind = impactVy >= cfg.landing.hardThreshold ? "hard" : "soft";
1438
+
1439
+ const massScaledDip = impactVy * cfg.landing.recovery.dipPerVy
1440
+ * runtime.massRatios.landingDipScale;
1441
+ const dip = clamp(massScaledDip, 0, cfg.landing.recovery.dipMax);
1442
+ runtime.landSpring.settle(-dip);
1443
+
1444
+ const landImpulse = clamp(
1445
+ impactVy * cfg.exertion.landImpulsePerVy * runtime.massRatios.exertionRiseScale,
1446
+ 0, cfg.exertion.landImpulseMax,
1447
+ );
1448
+ state.exertion = clamp(state.exertion + landImpulse, 0, 1);
1449
+
1450
+ runtime.midJump = false;
1451
+ state.isAscending = false;
1452
+ state.isVariableJumpCut = false;
1453
+ state.fallDistance = 0;
1454
+
1455
+ controller.signals.onLand.send1({ verticalSpeed: impactVy, kind });
1456
+ }
1457
+
1458
+ /**
1459
+ * Leave-ground transition: `onLeaveGround` signal + takeoff bookkeeping.
1460
+ * Shared by both move paths.
1461
+ * @private
1462
+ */
1463
+ _onLeaveGround(controller, runtime, bodyTransform) {
1464
+ controller.signals.onLeaveGround.send1({ reason: runtime.midJump ? "jump" : "fall" });
1465
+ runtime.takeoffVy = runtime.velocityY;
1466
+ runtime.peakAltitude = bodyTransform.translation_y;
1467
+ }
1468
+
1469
+ /**
1470
+ * Fire `onJumpApex` once, when an in-progress jump stops rising.
1471
+ * @private
1472
+ */
1473
+ _detectJumpApex(controller, runtime, bodyTransform) {
1474
+ if (runtime.midJump && !runtime.apexFired) {
1475
+ if (bodyTransform.translation_y > runtime.peakAltitude) {
1476
+ runtime.peakAltitude = bodyTransform.translation_y;
1477
+ } else if (runtime.velocityY <= 0) {
1478
+ controller.signals.onJumpApex.send0();
1479
+ runtime.apexFired = true;
1480
+ }
1481
+ }
1482
+ }
1483
+
1484
+ /**
1485
+ * Run the base (no-ability) L1 locomotion phases: speed selection,
1486
+ * desired-velocity computation, accel/decel, jump FSM, gravity, body
1487
+ * integration, ground resolution. Only invoked when no ability owns
1488
+ * the tick (see {@link AbilitySet.tick}).
1489
+ *
1490
+ * @private
1491
+ * @param {FirstPersonPlayerController} controller
1492
+ * @param {PerEntityRuntime} runtime
1493
+ * @param {Transform64} bodyTransform
1494
+ * @param {number} dt
1495
+ * @param {boolean} isCrouchActive
1496
+ * @param {boolean} isSprintIntent
1497
+ * @param {boolean} isBackwardIntent
1498
+ */
1499
+ _runBaseLocomotion(controller, runtime, bodyTransform, dt,
1500
+ isCrouchActive, isSprintIntent, isBackwardIntent) {
1501
+ const cfg = controller.config;
1502
+ const intent = controller.intent;
1503
+ const state = controller.state;
1504
+
1505
+ // -- L1.b: Speed selection ------------------------------------
1506
+ let targetSpeed;
1507
+ if (isCrouchActive) {
1508
+ targetSpeed = cfg.motion.crouchSpeed;
1509
+ } else if (isSprintIntent) {
1510
+ targetSpeed = cfg.motion.sprintSpeed;
1511
+ } else {
1512
+ targetSpeed = cfg.motion.walkSpeed;
1513
+ }
1514
+ if (isBackwardIntent) {
1515
+ targetSpeed *= cfg.motion.backwardSpeedFactor;
1516
+ }
1517
+
1518
+ // Airborne momentum floor — preserve whatever horizontal speed
1519
+ // the player carried into the jump. Without this, a sprint
1520
+ // jump (9 m/s) decays toward walkSpeed (4.5 m/s) at
1521
+ // airAccel = 14 m/s², losing all sprint momentum in ~0.32 s —
1522
+ // well before the apex of a `peakHeight = 1.8 m` jump arc. The
1523
+ // air-control band (Mirror's Edge, Titanfall, modern CoD) and
1524
+ // the long-jump biomechanics literature both say the same
1525
+ // thing: there's no thrust source in flight, so horizontal
1526
+ // velocity is conserved across the arc and air "control" is
1527
+ // for steering (direction) — not for changing speed magnitude.
1528
+ // Raising the target to the current speed makes `stepTowards`
1529
+ // a no-op when the player keeps pressing forward, while
1530
+ // releasing the stick still lets `airAccel` decelerate to
1531
+ // `walkSpeed` (the user CAN bleed off speed, just not have it
1532
+ // bled off for them).
1533
+ if (!state.grounded) {
1534
+ const horizSpeed = Math.hypot(runtime.velocityX, runtime.velocityZ);
1535
+ if (horizSpeed > targetSpeed) targetSpeed = horizSpeed;
1536
+ }
1537
+
1538
+ // -- L1.c: Move intent → desired horizontal velocity ----------
1539
+ // screen_forward(θ) = ( sin θ, 0, cos θ )
1540
+ // screen_right (θ) = (-cos θ, 0, sin θ )
1541
+ const { sinYaw, cosYaw } = runtime;
1542
+ const mvX = intent.move.x;
1543
+ const mvY = intent.move.y;
1544
+ const mvMag = Math.hypot(mvX, mvY);
1545
+ const nmvX = mvMag > 1 ? mvX / mvMag : mvX;
1546
+ const nmvY = mvMag > 1 ? mvY / mvMag : mvY;
1547
+ const desiredVx = sinYaw * nmvY + -cosYaw * nmvX;
1548
+ const desiredVz = cosYaw * nmvY + sinYaw * nmvX;
1549
+ const desiredHorizontalVx = desiredVx * targetSpeed;
1550
+ const desiredHorizontalVz = desiredVz * targetSpeed;
1551
+
1552
+ // -- L1.d: Accel/decel toward desired velocity ----------------
1553
+ //
1554
+ // Three regimes — air control, grounded decel-to-stop, grounded
1555
+ // accel-to-target — each with its own model:
1556
+ //
1557
+ // • Air control: constant-rate `stepTowards`. No ground
1558
+ // reaction force in flight; air control is a steering
1559
+ // budget, not a thrust curve. Constant accel matches the
1560
+ // player mental model of "fixed mid-air authority".
1561
+ //
1562
+ // • Grounded decel (no intent): constant-rate `stepTowards`
1563
+ // toward zero. Friction is approximately constant for a
1564
+ // biped on level ground — Coulomb friction. Faster than
1565
+ // accel because the body's own resistance + active
1566
+ // decel-foot-plants combine into a sharper deceleration.
1567
+ //
1568
+ // • Grounded accel (intent active): mono-exponential
1569
+ // approach (Hill 1927; Furusawa-Hill 1928). dv/dt is
1570
+ // proportional to (v_target − v), so accel is highest at
1571
+ // low speed and tapers as v approaches v_target. Matches
1572
+ // human sprint biomechanics — modern sprint-profiling
1573
+ // work (Morin & Samozino 2016) fits this same mono-exp
1574
+ // curve to empirical force-plate data.
1575
+ //
1576
+ // The mass + mastery + backward scalars compose multiplicatively
1577
+ // on the EFFECTIVE half-life (heavier ⇒ longer half-life ⇒
1578
+ // slower ramp; mastery accel-bonus ⇒ shorter half-life ⇒
1579
+ // faster ramp). See FirstPersonPlayerControllerConfig.js's
1580
+ // `groundAccelHalfLife` doc for the literature and the
1581
+ // SprintAcceleration.spec.js for the model assertions.
1582
+ const intentLen = Math.hypot(nmvX, nmvY);
1583
+ if (!state.grounded) {
1584
+ const maxStep = cfg.motion.airAccel * dt;
1585
+ runtime.velocityX = stepTowards(runtime.velocityX, desiredHorizontalVx, maxStep);
1586
+ runtime.velocityZ = stepTowards(runtime.velocityZ, desiredHorizontalVz, maxStep);
1587
+ } else if (intentLen < 1e-4) {
1588
+ let decel = cfg.motion.groundDecel * runtime.massRatios.groundAccelScale;
1589
+ decel *= controller.mastery.evaluate(DecisionPoint.GroundAccel, controller, runtime);
1590
+ const maxStep = decel * dt;
1591
+ runtime.velocityX = stepTowards(runtime.velocityX, 0, maxStep);
1592
+ runtime.velocityZ = stepTowards(runtime.velocityZ, 0, maxStep);
1593
+ } else {
1594
+ // Mono-exponential approach. Scale half-life by the
1595
+ // inverse of the accel scalars so that "more accel" (large
1596
+ // groundAccelScale, mastery > 1.0) translates to a shorter
1597
+ // half-life (faster ramp). Backward intent slows things
1598
+ // down — backwardAccelFactor < 1 ⇒ longer half-life.
1599
+ let halfLife = cfg.motion.groundAccelHalfLife
1600
+ / runtime.massRatios.groundAccelScale
1601
+ / controller.mastery.evaluate(DecisionPoint.GroundAccel, controller, runtime);
1602
+ if (isBackwardIntent) halfLife /= cfg.motion.backwardAccelFactor;
1603
+ runtime.velocityX = exponentialApproach(runtime.velocityX, desiredHorizontalVx, halfLife, dt);
1604
+ runtime.velocityZ = exponentialApproach(runtime.velocityZ, desiredHorizontalVz, halfLife, dt);
1605
+ }
1606
+
1607
+ // -- L1.e/f/g/h: jump FSM + vertical integration --------------
1608
+ this._advanceJumpFsm(controller, runtime, bodyTransform, dt);
1609
+ this._integrateVerticalAndResolveGround(controller, runtime, bodyTransform, dt);
1610
+
1611
+ // -- Publish posture for L2 consumers (eye height, gait gating).
1612
+ // Base owns posture when no ability is active: Crouch if the
1613
+ // crouch intent is resolved active, otherwise Stand. Abilities
1614
+ // that need a different posture (slide → Prone, ledge-grab →
1615
+ // Hang) set state.posture themselves in their tick.
1616
+ let posture = isCrouchActive
1617
+ ? FirstPersonPosture.Crouch
1618
+ : FirstPersonPosture.Stand;
1619
+ // Headroom floor — block UN-proning under a very low overhang. Only an
1620
+ // ALREADY-prone body (e.g. one that slid into a crawl-height tunnel) is
1621
+ // force-kept prone while the Crouch collider can't fit above it. A player
1622
+ // merely crouching by intent is NOT auto-dropped to prone when they meet
1623
+ // a lower overhang — same rule as above: the mover blocks their forward
1624
+ // motion instead of the ceiling shoving them down. Uses the post-move
1625
+ // position, so it reflects where the body actually ended up this tick.
1626
+ if (posture === FirstPersonPosture.Crouch
1627
+ && runtime.lastPosture === FirstPersonPosture.Prone) {
1628
+ const proneGrow = runtime.crouchTop - runtime.proneTop;
1629
+ if (!this._hasHeadroomToGrow(runtime, bodyTransform, runtime.colliderShapeProne, proneGrow)) {
1630
+ posture = FirstPersonPosture.Prone;
1631
+ }
1632
+ }
1633
+ controller.state.posture = posture;
1634
+
1635
+ // -- Publish lean target for L2.f. Base writes the natural
1636
+ // (lat-accel + look-lean) value; abilities override in their
1637
+ // own tick. L2.f spring-steps toward whatever's here.
1638
+ runtime.leanTargetRad = this._computeNaturalLeanTarget(controller, runtime, dt);
1639
+ }
1640
+
1641
+ /**
1642
+ * Compute the natural camera lean for this tick: lat-accel-driven
1643
+ * roll into a turn, plus a yaw-rate look-lean contribution, both
1644
+ * clamped. The result is the target the lean spring chases each
1645
+ * tick when no ability has opinions.
1646
+ *
1647
+ * Pure-ish helper — reads `controller`, `runtime`, `dt`; returns a
1648
+ * number. Extracted so both base and any future ability that wants
1649
+ * to compose its lean on top of the natural value can call it.
1650
+ *
1651
+ * @private
1652
+ * @param {FirstPersonPlayerController} controller
1653
+ * @param {PerEntityRuntime} runtime
1654
+ * @param {number} dt
1655
+ * @returns {number} target roll in radians
1656
+ */
1657
+ _computeNaturalLeanTarget(controller, runtime, dt) {
1658
+ const cfg = controller.config;
1659
+ const state = controller.state;
1660
+ if (!cfg.lean.enabled) return 0;
1661
+
1662
+ const sinYaw = runtime.sinYaw;
1663
+ const cosYaw = runtime.cosYaw;
1664
+
1665
+ // Lateral acceleration projected onto screen-right.
1666
+ // accel_world = (vel - prevVel) / dt; screen_right = (-cos θ, 0, sin θ).
1667
+ const accWorldX = (runtime.velocityX - runtime.prevVelocityX) / Math.max(dt, 1e-4);
1668
+ const accWorldZ = (runtime.velocityZ - runtime.prevVelocityZ) / Math.max(dt, 1e-4);
1669
+ const latAccel = accWorldX * (-cosYaw) + accWorldZ * sinYaw;
1670
+ const normalized = clamp(latAccel / 9.81, -2, 2);
1671
+ //
1672
+ // Sign convention for the roll (the eye composes the rotation
1673
+ // as qYaw * qPitch * qRoll, where qRoll is around (0,0,1)).
1674
+ // After the engine's camera-invert pipeline:
1675
+ // φ > 0 → camera-up tilts toward screen-right (−X) → HEAD TILTS RIGHT
1676
+ // φ < 0 → camera-up tilts toward screen-left (+X) → HEAD TILTS LEFT
1677
+ //
1678
+ // For the "bank into the turn" feel (Apex / Titanfall / Mirror's
1679
+ // Edge): accelerating right (latAccel > 0) should tilt the head
1680
+ // RIGHT, i.e. positive φ. So leanTargetRad has the SAME sign
1681
+ // as latAccel.
1682
+ let leanTargetRad = normalized * cfg.lean.maxRollDeg * DEG_TO_RAD;
1683
+
1684
+ // Look-lean: yaw-rate-driven banking. runtime.yawRateRadPerSec
1685
+ // was cached at L1.a — negative is the "turn right" convention.
1686
+ // For "bank into the turn": turning right → head tilts right →
1687
+ // positive engine roll. So lookLean = -yawRate * scale matches
1688
+ // sign.
1689
+ //
1690
+ // Crouched players are in a low, stable, low-momentum stance —
1691
+ // banking the head from a mouse turn reads as unmotivated. We
1692
+ // scale the contribution down (default to 0) while crouched.
1693
+ // Lat-accel lean is left alone: its magnitude naturally tracks
1694
+ // the (lower) crouch acceleration, so it stays motivated.
1695
+ if (cfg.lean.lookLeanEnabled) {
1696
+ const yawRate = clamp(
1697
+ runtime.yawRateRadPerSec,
1698
+ -cfg.lean.lookLeanYawRateClamp,
1699
+ cfg.lean.lookLeanYawRateClamp,
1700
+ );
1701
+ const crouchFactor = state.crouchActive ? cfg.lean.crouchLookLeanFactor : 1.0;
1702
+ leanTargetRad += -yawRate * cfg.lean.lookLeanDegPerRadPerSec * DEG_TO_RAD * crouchFactor;
1703
+ }
1704
+
1705
+ // Final clamp on the sum: cap the combined target to ±2 ×
1706
+ // maxRollDeg (matches the latAccel normalized clamp range) so
1707
+ // even simultaneous max-strafe-accel + max-yaw-flick produces a
1708
+ // sane upper bound.
1709
+ const maxTotal = cfg.lean.maxRollDeg * DEG_TO_RAD * 2;
1710
+ return clamp(leanTargetRad, -maxTotal, maxTotal);
1711
+ }
1712
+
1713
+ /**
1714
+ * Snapshot the per-tick "what is the body doing" information into the
1715
+ * pose channels for downstream consumption (skeleton, sound, AI).
1716
+ * Read-only with respect to controller state — this is purely a publish
1717
+ * step.
1718
+ *
1719
+ * @private
1720
+ * @param {FirstPersonPlayerController} controller
1721
+ * @param {PerEntityRuntime} runtime
1722
+ * @param {Transform64} bodyTransform
1723
+ */
1724
+ _publishPose(controller, runtime, bodyTransform) {
1725
+ const cfg = controller.config;
1726
+ const state = controller.state;
1727
+ const pose = controller.pose;
1728
+
1729
+ pose.rootPosition.set(bodyTransform.translation_x, bodyTransform.translation_y, bodyTransform.translation_z);
1730
+ pose.rootYawRad = runtime.bodyYaw;
1731
+ pose.headYawRad = runtime.bodyYaw;
1732
+ pose.headPitchRad = runtime.eyePitch;
1733
+ pose.headRollRad = state.leanRollRad;
1734
+ pose.locomotionPhase = state.stridePhase;
1735
+ pose.locomotionSpeed = runtime.horizSpeed;
1736
+ // Strafe component: project velocity onto screen-right (-cos θ, 0, sin θ).
1737
+ // Positive = moving to the player's right.
1738
+ pose.locomotionStrafe = (runtime.velocityX * (-runtime.cosYaw) + runtime.velocityZ * runtime.sinYaw)
1739
+ / Math.max(cfg.motion.sprintSpeed, 1e-3);
1740
+ pose.actionState =
1741
+ state.inJumpAnticipation ? FirstPersonActionState.Anticipating
1742
+ : !state.grounded ? FirstPersonActionState.Airborne
1743
+ : (Math.abs(runtime.landSpring.value) > 0.01 ? FirstPersonActionState.Landing
1744
+ : FirstPersonActionState.Grounded);
1745
+ pose.locomotionMode = state.locomotionMode;
1746
+ const crouchSpan = Math.max(cfg.body.height - cfg.body.crouchHeight, 1e-3);
1747
+ pose.crouchAmount = clamp((cfg.body.height - state.eyeHeight) / crouchSpan, 0, 1);
1748
+
1749
+ // Posture channel for downstream animation: which body shape +
1750
+ // how far the body is into it from the standing neutral.
1751
+ //
1752
+ // `posture` is the enum (Stand / Crouch / Prone / Hang) — picks
1753
+ // the animation track. `postureAmount` is the [0..1] blend
1754
+ // weight from standing toward that posture, derived from the
1755
+ // eye-height spring so the value transitions smoothly across
1756
+ // changes (matches the visible camera motion).
1757
+ pose.posture = state.posture;
1758
+ let postureTargetH;
1759
+ switch (state.posture) {
1760
+ case FirstPersonPosture.Prone: postureTargetH = cfg.body.proneHeight; break;
1761
+ case FirstPersonPosture.Crouch: postureTargetH = cfg.body.crouchHeight; break;
1762
+ case FirstPersonPosture.Hang: postureTargetH = cfg.body.height; break;
1763
+ case FirstPersonPosture.Stand:
1764
+ default: postureTargetH = cfg.body.height; break;
1765
+ }
1766
+ const postureSpan = Math.max(cfg.body.height - postureTargetH, 1e-3);
1767
+ pose.postureAmount = clamp((cfg.body.height - state.eyeHeight) / postureSpan, 0, 1);
1768
+
1769
+ pose.aimPitch = runtime.eyePitch;
1770
+
1771
+ this._assertTickInvariants(controller, runtime, bodyTransform);
1772
+ this._snapshotRenderPose(runtime, bodyTransform);
1773
+ }
1774
+
1775
+ /**
1776
+ * Record this fixed step's authoritative body position as the latest
1777
+ * render-interpolation endpoint, shifting the old latest to "previous".
1778
+ * {@link _composeEye} blends prev→cur by the sub-step alpha so the camera
1779
+ * position is smooth at any refresh rate. (Yaw/pitch aren't snapshotted —
1780
+ * they're render-rate.) On a teleport (`renderSnap`), prev and cur collapse
1781
+ * to the new position so the camera jumps rather than gliding across it.
1782
+ * @private
1783
+ */
1784
+ _snapshotRenderPose(runtime, bodyTransform) {
1785
+ const p = bodyTransform;
1786
+ if (runtime.renderSnap) {
1787
+ runtime.renderSnap = false;
1788
+ runtime.renderPrevPos.set(p.translation_x, p.translation_y, p.translation_z);
1789
+ runtime.renderCurPos.set(p.translation_x, p.translation_y, p.translation_z);
1790
+ return;
1791
+ }
1792
+ runtime.renderPrevPos.copy(runtime.renderCurPos);
1793
+ runtime.renderCurPos.set(p.translation_x, p.translation_y, p.translation_z);
1794
+ }
1795
+
1796
+ /**
1797
+ * Dev-only invariant guard (meep asserts compile out of prod, so this is
1798
+ * free there). Catches the class of corruption that surfaces as an
1799
+ * unexplained camera "snap": a NaN/Infinity leaking into the yaw/pitch the
1800
+ * eye composes from, or into the body's velocity / position. Runs once at
1801
+ * the END of every fixed step, so a bad value is caught the tick it appears
1802
+ * — naming the layer that produced it — instead of manifesting as a camera
1803
+ * jump frames (or seconds) later when something finally reads it.
1804
+ * @private
1805
+ */
1806
+ _assertTickInvariants(controller, runtime, bodyTransform) {
1807
+ const p = bodyTransform;
1808
+ assert.ok(Number.isFinite(runtime.bodyYaw),
1809
+ "FP invariant: runtime.bodyYaw is non-finite (camera yaw corrupted)");
1810
+ assert.ok(Number.isFinite(runtime.eyePitch),
1811
+ "FP invariant: runtime.eyePitch is non-finite (camera pitch corrupted)");
1812
+ assert.ok(Number.isFinite(controller.state.leanRollRad),
1813
+ "FP invariant: state.leanRollRad is non-finite (camera roll corrupted)");
1814
+ assert.ok(
1815
+ Number.isFinite(runtime.velocityX)
1816
+ && Number.isFinite(runtime.velocityY)
1817
+ && Number.isFinite(runtime.velocityZ),
1818
+ "FP invariant: runtime.velocity is non-finite",
1819
+ );
1820
+ assert.ok(
1821
+ Number.isFinite(p.translation_x) && Number.isFinite(p.translation_y) && Number.isFinite(p.translation_z),
1822
+ "FP invariant: body position is non-finite",
1823
+ );
1824
+ }
1825
+
1826
+ /**
1827
+ * Consume the accumulated mouse-look delta into body yaw + eye pitch at
1828
+ * RENDER rate (once per render frame, from {@link _composeEye}). This is the
1829
+ * fix for the slow-turn camera snap: sampling `intent.look` at the 60Hz
1830
+ * fixed step aliases an unaligned mouse report rate into an uneven turn,
1831
+ * whereas consuming it every render frame tracks the mouse as smoothly as it
1832
+ * arrives. `intent.look` is zeroed after consuming so the same delta isn't
1833
+ * re-applied next frame.
1834
+ *
1835
+ * Conventions (raw mouse delta source — movementX/Y positive moving
1836
+ * right/down): look.x > 0 ("mouse right") → turn right (the yaw sign is
1837
+ * negated because the engine is left-handed with +Z forward, so a +Y
1838
+ * rotation reads as a LEFT turn through the camera); look.y > 0 → look down,
1839
+ * flipped by invertY.
1840
+ * @private
1841
+ */
1842
+ _consumeLook(controller, runtime, dt) {
1843
+ const intent = controller.intent;
1844
+ const cfg = controller.config;
1845
+ let yawDelta = -intent.look.x;
1846
+ const pitchSign = cfg.look.invertY ? -1 : 1;
1847
+ let pitchDelta = intent.look.y * pitchSign;
1848
+ intent.look.set(0, 0);
1849
+
1850
+ // Reject input SPIKES. A corrupt browser/driver `movementX` (a high-res
1851
+ // mouse under OS pointer acceleration, or a pointer-lock burst) snaps the
1852
+ // camera 20–100° in one frame.
1853
+ //
1854
+ // That is a RATE problem, not an amount problem: `intent.look` accumulates
1855
+ // between consumptions, so its magnitude is proportional to frame time by
1856
+ // construction. Comparing it against a constant per-frame bound made the
1857
+ // effective ceiling `bound / frameTime` - ~43 rad/s at 144 Hz, 9 rad/s at
1858
+ // 30 fps, 1.5 rad/s across a 200 ms hitch - so a 180° flick over 0.15 s
1859
+ // (~21 rad/s) passed on a fast display and was discarded on a slow one.
1860
+ //
1861
+ // And CLAMPED, not dropped: a turn that overshoots the ceiling now slows
1862
+ // down instead of not happening at all. The accumulator was already zeroed
1863
+ // above, so a dropped delta was unrecoverable.
1864
+ const maxDelta = (dt > 0)
1865
+ ? cfg.look.maxLookRateRadPerSecond * dt
1866
+ : cfg.look.maxFrameDeltaRad;
1867
+
1868
+ if (yawDelta > maxDelta) {
1869
+ yawDelta = maxDelta;
1870
+ } else if (yawDelta < -maxDelta) {
1871
+ yawDelta = -maxDelta;
1872
+ }
1873
+
1874
+ if (pitchDelta > maxDelta) {
1875
+ pitchDelta = maxDelta;
1876
+ } else if (pitchDelta < -maxDelta) {
1877
+ pitchDelta = -maxDelta;
1878
+ }
1879
+
1880
+ runtime.bodyYaw += yawDelta;
1881
+ if (runtime.bodyYaw > Math.PI) runtime.bodyYaw -= TWO_PI;
1882
+ else if (runtime.bodyYaw < -Math.PI) runtime.bodyYaw += TWO_PI;
1883
+
1884
+ runtime.eyePitch = clamp(
1885
+ runtime.eyePitch + pitchDelta,
1886
+ cfg.look.pitchMinDeg * DEG_TO_RAD,
1887
+ cfg.look.pitchMaxDeg * DEG_TO_RAD,
1888
+ );
1889
+ }
1890
+
1891
+ /**
1892
+ * Compose the eye transform from body + state-driven offsets.
1893
+ * @private
1894
+ * @param {FirstPersonPlayerController} controller
1895
+ * @param {number} entity
1896
+ */
1897
+ _composeEye(controller, entity) {
1898
+ const ecd = this.entityManager.dataset;
1899
+ const runtime = this.runtime.get(entity);
1900
+ if (runtime === undefined) return;
1901
+
1902
+ const dt = this._currentRenderDt;
1903
+
1904
+ // Consume mouse-look at RENDER rate so the camera direction tracks the
1905
+ // mouse smoothly, decoupled from the aliased 60Hz sim sampling. `dt` goes
1906
+ // in because the spike ceiling is an angular rate, not a per-frame amount.
1907
+ this._consumeLook(controller, runtime, dt);
1908
+ const cfg = controller.config;
1909
+ const state = controller.state;
1910
+
1911
+ const bodyTransform = ecd.getComponent(entity, Transform64);
1912
+ if (bodyTransform === undefined) return;
1913
+
1914
+ if (controller.eyeEntity === -1) return;
1915
+ const eyeTransform = ecd.getComponent(controller.eyeEntity, Transform64);
1916
+ const camera = ecd.getComponent(controller.eyeEntity, Camera);
1917
+ if (eyeTransform === undefined || camera === undefined) return;
1918
+
1919
+ // -- Body-local eye offset, composed via the additive stack ----
1920
+ // The base (0, eyeHeight, 0) is the standing/crouched neutral; each
1921
+ // additional contribution (bob, breath, landing, anticipation,
1922
+ // sprint posture) goes through the stack so external systems can
1923
+ // push their own contributions on the same channel.
1924
+ const stack = runtime.eyeOffsetStack;
1925
+ stack.clear();
1926
+ stack.push("eyeHeight", 0, state.eyeHeight, 0);
1927
+
1928
+ // Bob — gated on grounded only (the impact spring decays naturally
1929
+ // even at rest, so the bob fade-out is smooth; lateral amp uses the
1930
+ // bob-intensity envelope which spring-decays after stopping).
1931
+ if (state.grounded) {
1932
+ const phase = state.stridePhase * TWO_PI;
1933
+ const massBoost = (cfg.body.mass - 80) * cfg.bob.ampMassScale;
1934
+ const intensity = runtime.bobIntensitySpring.value;
1935
+
1936
+ // Back-pedal amp boost — lateral grows more than vertical because
1937
+ // backward gait has worse side-to-side balance than vertical compression.
1938
+ // Exertion adds a smaller boost on top: tired = wobbly gait.
1939
+ const ampLMult = 1 + (cfg.bob.backwardLateralAmpFactor - 1) * runtime.backwardness;
1940
+ const exertionBoost = 1 + cfg.exertion.bobLateralBoostAtMax * state.exertion;
1941
+ const ampL = (cfg.bob.lateralAmpAtWalk + massBoost) * intensity * ampLMult * exertionBoost;
1942
+
1943
+ // Vertical: read directly from the impact spring (footfall kicks,
1944
+ // under-damped recovery → trough + leg-push overshoot).
1945
+ stack.push("bob.impact", 0, runtime.verticalImpactSpring.value, 0);
1946
+
1947
+ // Lateral: head shifts toward the foot bearing weight. Polarity
1948
+ // sourced from runtime.standingFoot — the same signal the
1949
+ // footstep emits — so bob direction and footstep side agree.
1950
+ // |sin(phase)| is the non-negative "midstance envelope".
1951
+ const lateralPolarity = runtime.standingFoot === "R" ? -1 : 1;
1952
+ stack.push("bob.lateral", ampL * lateralPolarity * Math.abs(Math.sin(phase)), 0, 0);
1953
+ }
1954
+
1955
+ // Breath — sine + tiny noise riding the rate spring.
1956
+ const breathOffset = -state.breathAmplitudeM
1957
+ * Math.sin(state.breathPhase * TWO_PI)
1958
+ * (1 + cfg.breath.noiseAmount * (Math.sin(state.breathPhase * 13.7) * 0.5));
1959
+ stack.push("breath", 0, breathOffset, 0);
1960
+
1961
+ // Landing spring dip (under-damped — overshoots once on recovery).
1962
+ stack.push("landing", 0, runtime.landSpring.value, 0);
1963
+
1964
+ // Jump anticipation dip (eased ramp during the squash window).
1965
+ if (state.inJumpAnticipation) {
1966
+ const t = 1 - clamp(runtime.anticipationRemaining / Math.max(cfg.jump.anticipation.duration, 1e-3), 0, 1);
1967
+ const eased = t * (2 - t); // ease-out quad
1968
+ stack.push("anticipation", 0, -cfg.jump.anticipation.dipAmount * eased, 0);
1969
+ }
1970
+
1971
+ // Sprint posture: head leans slightly forward as commitment builds.
1972
+ // Pitch part is in the rotation block below; the +Z position shift
1973
+ // sells "head leading the hips" (Mirror's Edge), tied to the same
1974
+ // spring envelope so they move together.
1975
+ const sprintPitch = runtime.sprintPostureSpring.value;
1976
+ const sprintShiftFraction =
1977
+ cfg.posture.sprintForwardPitchDeg > 0
1978
+ ? sprintPitch / (cfg.posture.sprintForwardPitchDeg * DEG_TO_RAD)
1979
+ : 0;
1980
+ stack.push("posture.sprintShift", 0, 0, cfg.posture.sprintForwardShiftM * sprintShiftFraction);
1981
+
1982
+ // Body POSITION advances only at the fixed step → blend the last two
1983
+ // steps by the engine's sub-step alpha (the same fraction the
1984
+ // InterpolationSystem uses for physics bodies) so it's smooth on a high-
1985
+ // refresh display. Yaw + pitch are already render-rate (consumed in
1986
+ // _consumeLook), so they're used directly — no blend, no aliasing.
1987
+ const alpha = this.entityManager.getFixedStepAlpha();
1988
+ const iPosX = lerp(runtime.renderPrevPos.x, runtime.renderCurPos.x, alpha);
1989
+ const iPosY = lerp(runtime.renderPrevPos.y, runtime.renderCurPos.y, alpha);
1990
+ const iPosZ = lerp(runtime.renderPrevPos.z, runtime.renderCurPos.z, alpha);
1991
+ const qYaw0 = SCRATCH_Q_A.fromAxisAngle(Vector3.up, runtime.bodyYaw);
1992
+
1993
+ // Transform body-local accumulated offset into world space (about the
1994
+ // render-rate yaw, to match the interpolated base position).
1995
+ const worldOffset = SCRATCH_V3_B.copy(stack.offset);
1996
+ worldOffset.applyQuaternion(qYaw0);
1997
+
1998
+ eyeTransform.setTranslation(
1999
+ iPosX + worldOffset.x,
2000
+ iPosY + worldOffset.y,
2001
+ iPosZ + worldOffset.z
2002
+ );
2003
+
2004
+ // -- Eye rotation: body yaw × eye pitch × roll -------------------
2005
+ // Bob roll mixes in for a subtle head sway (in phase with lateral bob).
2006
+ // Breath pitch is a small extra nod 90° out of phase with vertical
2007
+ // breath; merged into the main pitch so we don't pay an extra quat
2008
+ // multiply and the composition stays trivially correct.
2009
+ let rollTotal = state.leanRollRad;
2010
+ if (state.grounded) {
2011
+ // Roll: head tilts toward the standing foot, in phase with the
2012
+ // lateral sway. Polarity sourced from runtime.standingFoot for
2013
+ // consistency with the lateral bob. Positive engine roll = head
2014
+ // tilts RIGHT (camera-invert convention), so R-foot midstance =
2015
+ // positive roll, L-foot midstance = negative roll.
2016
+ const phase = state.stridePhase * TWO_PI;
2017
+ const rollBackMult = 1 + (cfg.bob.backwardRollFactor - 1) * runtime.backwardness;
2018
+ const ampRoll = cfg.bob.rollAtWalkDeg * DEG_TO_RAD * runtime.bobIntensitySpring.value * rollBackMult;
2019
+ const rollPolarity = runtime.standingFoot === "R" ? 1 : -1;
2020
+ const rollEnvelope = Math.abs(Math.sin(phase));
2021
+ const bobRollSigned = ampRoll * rollPolarity * rollEnvelope;
2022
+
2023
+ // Lean × bob coupling: excursions in the lean direction get
2024
+ // amplified, opposite excursions attenuated. Lean is normalized
2025
+ // against maxRollDeg so the coupling magnitude stays bounded
2026
+ // regardless of how aggressively lean is configured.
2027
+ const maxLeanRad = Math.max(cfg.lean.maxRollDeg * DEG_TO_RAD, 1e-6);
2028
+ const leanFraction = clamp(state.leanRollRad / maxLeanRad, -1, 1);
2029
+ // sign(bobRollSigned) matches lean? amplify; else attenuate.
2030
+ const sameSign = (bobRollSigned * leanFraction) >= 0;
2031
+ const couplingMag = cfg.bob.leanCouplingFactor * Math.abs(leanFraction);
2032
+ const couplingScale = sameSign ? (1 + couplingMag) : (1 - couplingMag);
2033
+ rollTotal += bobRollSigned * couplingScale;
2034
+ }
2035
+
2036
+ const breathPitch = lerp(cfg.breath.pitchAmpRestDeg, cfg.breath.pitchAmpMaxDeg, state.exertion)
2037
+ * DEG_TO_RAD
2038
+ * Math.cos(state.breathPhase * TWO_PI);
2039
+ // Combined pitch contributions: player input + breath nod + sprint
2040
+ // commitment + fatigue droop. All in the same "positive = look-down"
2041
+ // convention so they sum cleanly.
2042
+ const pitchTotal = runtime.eyePitch
2043
+ + breathPitch
2044
+ + runtime.sprintPostureSpring.value
2045
+ + runtime.headDroopSpring.value;
2046
+
2047
+ // composition: yaw * pitch * roll
2048
+ // pitch around world X — yaw applied after, so effective axis is camera-local right
2049
+ // roll around world Z — yaw and pitch applied after, so effective axis is camera-local forward
2050
+ const qYaw = qYaw0; // SCRATCH_Q_A, already the render-rate yaw
2051
+ const qPitch = SCRATCH_Q_B.fromAxisAngle(Vector3.right, pitchTotal);
2052
+ const qRoll = SCRATCH_Q_C.fromAxisAngle(Vector3.forward, rollTotal);
2053
+
2054
+ // composed in a scratch: a transform keeps its rotation as four numbers and has no
2055
+ // quaternion product of its own
2056
+ SCRATCH_Q_EULER.multiplyQuaternions(qYaw, qPitch);
2057
+ SCRATCH_Q_EULER.multiply(qRoll);
2058
+
2059
+ eyeTransform.setRotation(
2060
+ SCRATCH_Q_EULER.x, SCRATCH_Q_EULER.y, SCRATCH_Q_EULER.z, SCRATCH_Q_EULER.w
2061
+ );
2062
+
2063
+ eyeTransform.updateMatrix();
2064
+
2065
+ // Dev-only camera-output guard (compiled out of prod). The composed eye
2066
+ // direction is exactly what the player sees; if a spring (head-droop,
2067
+ // breath nod) or lean ever drove pitch/roll to NaN, the camera would
2068
+ // snap to garbage. Assert it here, at the source of the visible jump.
2069
+ assert.ok(
2070
+ Number.isFinite(eyeTransform.rotation_x) && Number.isFinite(eyeTransform.rotation_y) && Number.isFinite(eyeTransform.rotation_z) && Number.isFinite(eyeTransform.rotation_w),
2071
+ "FP invariant: composed eye rotation is non-finite (camera direction corrupted)",
2072
+ );
2073
+
2074
+ // -- FOV ---------------------------------------------------------
2075
+ let fovTarget = cfg.fov.base;
2076
+ if (cfg.fov.sprintAdd !== 0) {
2077
+ fovTarget += cfg.fov.sprintAdd * runtime.sprintness;
2078
+ }
2079
+ if (state.crouchActive) fovTarget += cfg.fov.crouchAdd;
2080
+
2081
+ runtime.fovSpring.stepTo(fovTarget, cfg.fov.smoothHalfLife, 1.0, dt);
2082
+ camera.fov.set(runtime.fovSpring.value);
2083
+ }
2084
+ }
2085
+
2086
+ // ---------------------------------------------------------------------------
2087
+ // helpers
2088
+ // ---------------------------------------------------------------------------
2089
+
2090
+ /**
2091
+ * Exponential approach with half-life parameterization.
2092
+ * @param {number} current
2093
+ * @param {number} target
2094
+ * @param {number} halfLife
2095
+ * @param {number} dt
2096
+ * @returns {number}
2097
+ */
2098
+ function exponentialApproach(current, target, halfLife, dt) {
2099
+ if (halfLife <= 0) return target;
2100
+ const alpha = 1 - Math.exp(-LN2 * dt / halfLife);
2101
+ return current + (target - current) * alpha;
2102
+ }
2103
+
2104
+ /**
2105
+ * Detect that phase value crossed a boundary in [0,1) between two ticks.
2106
+ * Handles the wraparound case where phase jumps from e.g. 0.95 to 0.05.
2107
+ *
2108
+ * @param {number} prev previous phase in [0,1)
2109
+ * @param {number} next current phase in [0,1)
2110
+ * @param {number} boundary in [0,1)
2111
+ * @returns {boolean}
2112
+ */
2113
+ function phaseCrossed(prev, next, boundary) {
2114
+ if (next >= prev) {
2115
+ // no wrap
2116
+ return prev < boundary && next >= boundary;
2117
+ } else {
2118
+ // wrapped past 1.0
2119
+ return prev < boundary || next >= boundary;
2120
+ }
2121
+ }
2122
+