@woosh/meep-engine 2.139.0 → 2.141.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.
- package/package.json +1 -1
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts +3 -3
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js +4 -4
- package/src/{engine/physics/broadphase/aabb_transform_oriented.d.ts → core/geom/3d/aabb/aabb3_transform_oriented.d.ts} +2 -2
- package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
- package/src/{engine/physics/broadphase/aabb_transform_oriented.js → core/geom/3d/aabb/aabb3_transform_oriented.js} +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
- package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
- package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
- package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
- package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
- package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
- package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/Triangle3D.js +318 -0
- package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
- package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
- package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
- package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
- package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
- package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
- package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
- package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_negate_array.js +2 -2
- package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
- package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
- package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
- package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
- package/src/engine/control/first-person/TODO.md +173 -127
- package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Slide.js +9 -1
- package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
- package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
- package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
- package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
- package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
- package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
- package/src/engine/physics/BULLET_REVIEW.md +945 -0
- package/src/engine/physics/CANNON_REVIEW.md +1300 -0
- package/src/engine/physics/JOLT_REVIEW.md +913 -0
- package/src/engine/physics/PLAN.md +578 -236
- package/src/engine/physics/RAPIER_REVIEW.md +934 -0
- package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
- package/src/engine/physics/REVIEW_002.md +151 -0
- package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
- package/src/engine/physics/constraint/DofMode.d.ts +28 -0
- package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
- package/src/engine/physics/constraint/DofMode.js +35 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
- package/src/engine/physics/constraint/solve_constraints.js +436 -0
- package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +608 -499
- package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
- package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.d.ts +179 -0
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
- package/src/engine/physics/ecs/Joint.js +234 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +180 -20
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +1423 -1159
- package/src/engine/physics/fluid/FluidField.d.ts +14 -10
- package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
- package/src/engine/physics/fluid/FluidField.js +14 -10
- package/src/engine/physics/fluid/FluidSimulator.js +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +30 -6
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
- package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +44 -18
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
- package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
- package/src/engine/physics/gjk/gjk.d.ts +28 -2
- package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
- package/src/engine/physics/gjk/gjk.js +421 -378
- package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
- package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
- package/src/engine/physics/gjk/minkowski_support.js +75 -0
- package/src/engine/physics/gjk/mpr.d.ts +56 -0
- package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
- package/src/engine/physics/gjk/mpr.js +344 -0
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
- package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
- package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
- package/src/engine/physics/integration/integrate_position.d.ts +25 -7
- package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
- package/src/engine/physics/integration/integrate_position.js +43 -12
- package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
- package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
- package/src/engine/physics/integration/integrate_velocity.js +82 -1
- package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
- package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
- package/src/engine/physics/island/IslandBuilder.js +33 -16
- package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
- package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/PosedShape.js +28 -30
- package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_box_manifold.js +140 -18
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
- package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
- package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
- package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
- package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts +41 -2
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +1497 -382
- package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
- package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
- package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
- package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
- package/src/engine/physics/queries/overlap_shape.js +183 -0
- package/src/engine/physics/queries/shape_cast.d.ts +56 -0
- package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
- package/src/engine/physics/queries/shape_cast.js +387 -0
- package/src/engine/physics/solver/solve_contacts.d.ts +146 -32
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +809 -223
- package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
- package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
|
@@ -1,1416 +1,1789 @@
|
|
|
1
|
-
import { assert } from "../../../core/assert.js";
|
|
2
|
-
import Quaternion from "../../../core/geom/Quaternion.js";
|
|
3
|
-
import Vector3 from "../../../core/geom/Vector3.js";
|
|
4
|
-
import { clamp } from "../../../core/math/clamp.js";
|
|
5
|
-
import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
|
|
6
|
-
import { lerp } from "../../../core/math/lerp.js";
|
|
7
|
-
import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
|
|
8
|
-
import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
|
|
9
|
-
import { SerializationMetadata } from "../../ecs/components/SerializationMetadata.js";
|
|
10
|
-
import Entity from "../../ecs/Entity.js";
|
|
11
|
-
import { System } from "../../ecs/System.js";
|
|
12
|
-
import { Transform } from "../../ecs/transform/Transform.js";
|
|
13
|
-
import { Camera } from "../../graphics/ecs/camera/Camera.js";
|
|
14
|
-
import { EyeOffsetStack } from "./composer/EyeOffsetStack.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*/
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*/
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*/
|
|
180
|
-
this.
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
*
|
|
412
|
-
* @param {number}
|
|
413
|
-
*/
|
|
414
|
-
|
|
415
|
-
const ecd = this.entityManager.dataset;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
this.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
controller.
|
|
458
|
-
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
runtime.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
runtime.
|
|
751
|
-
state.
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
//
|
|
755
|
-
runtime.
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
//
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
if (
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
);
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
//
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
//
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
//
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
//
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import Quaternion from "../../../core/geom/Quaternion.js";
|
|
3
|
+
import Vector3 from "../../../core/geom/Vector3.js";
|
|
4
|
+
import { clamp } from "../../../core/math/clamp.js";
|
|
5
|
+
import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
|
|
6
|
+
import { lerp } from "../../../core/math/lerp.js";
|
|
7
|
+
import { ResourceAccessKind } from "../../../core/model/ResourceAccessKind.js";
|
|
8
|
+
import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
|
|
9
|
+
import { SerializationMetadata } from "../../ecs/components/SerializationMetadata.js";
|
|
10
|
+
import Entity from "../../ecs/Entity.js";
|
|
11
|
+
import { System } from "../../ecs/System.js";
|
|
12
|
+
import { Transform } from "../../ecs/transform/Transform.js";
|
|
13
|
+
import { Camera } from "../../graphics/ecs/camera/Camera.js";
|
|
14
|
+
import { EyeOffsetStack } from "./composer/EyeOffsetStack.js";
|
|
15
|
+
import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
|
|
16
|
+
import { CapsuleShape3D } from "../../../core/geom/3d/shape/CapsuleShape3D.js";
|
|
17
|
+
import { TransformedShape3D } from "../../../core/geom/3d/shape/TransformedShape3D.js";
|
|
18
|
+
import { BodyKind } from "../../physics/ecs/BodyKind.js";
|
|
19
|
+
import { Collider } from "../../physics/ecs/Collider.js";
|
|
20
|
+
import { PhysicsSystem } from "../../physics/ecs/PhysicsSystem.js";
|
|
21
|
+
import { RigidBody } from "../../physics/ecs/RigidBody.js";
|
|
22
|
+
import { PhysicsSurfacePoint } from "../../physics/queries/PhysicsSurfacePoint.js";
|
|
23
|
+
import { FirstPersonPlayerController } from "./FirstPersonPlayerController.js";
|
|
24
|
+
import { DecisionPoint } from "./mastery/DecisionPoint.js";
|
|
25
|
+
import { computeJumpFromApex } from "./math/computeJumpFromApex.js";
|
|
26
|
+
import { computeLRCBreathRate } from "./math/computeLRCBreathRate.js";
|
|
27
|
+
import { computeMassRatios } from "./math/computeMassRatios.js";
|
|
28
|
+
import { Spring } from "./math/Spring.js";
|
|
29
|
+
import { stepTowards } from "./math/stepTowards.js";
|
|
30
|
+
import { FirstPersonActionState, FirstPersonLocomotionMode } from "./pose/FirstPersonPose.js";
|
|
31
|
+
import { FirstPersonPosture } from "./pose/FirstPersonPosture.js";
|
|
32
|
+
import { FirstPersonSensors } from "./sensors/FirstPersonSensors.js";
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Scratch allocations — reused per frame to avoid GC pressure
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
const SCRATCH_V3_A = new Vector3();
|
|
38
|
+
const SCRATCH_V3_B = new Vector3();
|
|
39
|
+
const SCRATCH_V3_C = new Vector3();
|
|
40
|
+
const SCRATCH_Q_A = new Quaternion();
|
|
41
|
+
const SCRATCH_Q_B = new Quaternion();
|
|
42
|
+
const SCRATCH_Q_C = new Quaternion();
|
|
43
|
+
|
|
44
|
+
const TWO_PI = Math.PI * 2;
|
|
45
|
+
const LN2 = Math.log(2);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a posture-sized player capsule: a {@link CapsuleShape3D} of
|
|
49
|
+
* `radius` and the appropriate cylinder height, wrapped in a
|
|
50
|
+
* {@link TransformedShape3D} whose Y offset puts the capsule's bottom
|
|
51
|
+
* exactly at the wrapped shape's local origin. The entity's
|
|
52
|
+
* `transform.position` then represents the player's feet — and a
|
|
53
|
+
* posture-driven shrink doesn't yank the feet up the way a centred
|
|
54
|
+
* capsule would, nor dip them below the floor.
|
|
55
|
+
*
|
|
56
|
+
* The capsule's lowest point in its own local frame is at
|
|
57
|
+
* `-(cylinderHeight/2 + radius) = -max(totalHeight/2, radius)`.
|
|
58
|
+
* Offsetting the wrapper by the magnitude of that puts the bottom at
|
|
59
|
+
* Y = 0:
|
|
60
|
+
* - Stand (`H = 1.8`, `r = 0.34`): cylHeight = 1.12, offset = 0.9.
|
|
61
|
+
* Bottom = -0.9 + 0.9 = 0. Top = +0.9 + 0.9 = 1.8.
|
|
62
|
+
* - Crouch (`H = 0.8`, `r = 0.34`): cylHeight = 0.12, offset = 0.4.
|
|
63
|
+
* Bottom = -0.4 + 0.4 = 0. Top = +0.4 + 0.4 = 0.8.
|
|
64
|
+
* - Prone (`H = 0.4`, `r = 0.34`): cylHeight = 0 (capsule collapses
|
|
65
|
+
* to a sphere of radius), offset = max(0.2, 0.34) = 0.34.
|
|
66
|
+
* Bottom = -0.34 + 0.34 = 0. Top = +0.34 + 0.34 = 0.68. The
|
|
67
|
+
* `totalHeight = 0.4` value is honoured for the offset budget
|
|
68
|
+
* but the actual Y extent floors at `2·radius`.
|
|
69
|
+
*
|
|
70
|
+
* Picking `totalHeight/2` blindly (the obvious choice) would put the
|
|
71
|
+
* Prone capsule's bottom at `0.2 - 0.34 = -0.14` — dipping below the
|
|
72
|
+
* feet, and into any physics floor that's flush with feet level. On
|
|
73
|
+
* a physics ground slab, every horizontal shape_cast from inside the
|
|
74
|
+
* floor returns t = 0, `advance = max(0, t - SKIN) = 0`, and the
|
|
75
|
+
* slide freezes in place — see SlideMotion.spec.js for the
|
|
76
|
+
* regression test that pins this.
|
|
77
|
+
*
|
|
78
|
+
* @param {number} radius — capsule radius in metres
|
|
79
|
+
* @param {number} totalHeight — desired full Y extent; ignored below
|
|
80
|
+
* `2·radius` (the capsule's intrinsic minimum extent)
|
|
81
|
+
* @returns {TransformedShape3D}
|
|
82
|
+
*/
|
|
83
|
+
function makePostureCapsule(radius, totalHeight) {
|
|
84
|
+
const cylinderHeight = Math.max(0, totalHeight - 2 * radius);
|
|
85
|
+
const yOffset = Math.max(totalHeight / 2, radius);
|
|
86
|
+
return TransformedShape3D.from_translation(
|
|
87
|
+
CapsuleShape3D.from(radius, cylinderHeight),
|
|
88
|
+
[0, yOffset, 0],
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Per-entity runtime state the system maintains internally — too transient
|
|
94
|
+
* even for {@link FirstPersonPlayerController}'s `state` member, because it
|
|
95
|
+
* encodes input-edge bookkeeping and timer values the public surface should
|
|
96
|
+
* never see directly.
|
|
97
|
+
*/
|
|
98
|
+
class PerEntityRuntime {
|
|
99
|
+
constructor() {
|
|
100
|
+
/**
|
|
101
|
+
* Co-attached kinematic body. Set by {@link FirstPersonPlayerControllerSystem.link}
|
|
102
|
+
* after asserting it's present. The controller writes Transform.position
|
|
103
|
+
* directly (existing motion logic); physics derives the body's velocity
|
|
104
|
+
* from the per-step delta. Other physics systems (raycasts, contact
|
|
105
|
+
* events) see the player through this body.
|
|
106
|
+
* @type {RigidBody|null}
|
|
107
|
+
*/
|
|
108
|
+
this.rigidBody = null;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Co-attached collider, cached at link. Same source the physics
|
|
112
|
+
* narrowphase uses, so move-and-slide casts the player's
|
|
113
|
+
* actual collision shape against the world.
|
|
114
|
+
* @type {Collider|null}
|
|
115
|
+
*/
|
|
116
|
+
this.collider = null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pre-allocated move-and-slide scratch — Ray3 and PhysicsSurfacePoint
|
|
120
|
+
* reused per cast so the controller doesn't churn the allocator
|
|
121
|
+
* each fixed step. Lazily filled by {@link _moveAndSlide}.
|
|
122
|
+
* @private
|
|
123
|
+
* @type {Ray3|null}
|
|
124
|
+
*/
|
|
125
|
+
this.slideRay = null;
|
|
126
|
+
/** @private @type {PhysicsSurfacePoint|null} */
|
|
127
|
+
this.slideHit = null;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Pre-built capsule colliders, one per posture. Cached at link
|
|
131
|
+
* from `config.body.{height, crouchHeight, proneHeight, radius}`
|
|
132
|
+
* so {@link _syncColliderShape} can swap the collider's shape on
|
|
133
|
+
* a posture change with zero per-tick allocation. Hang reuses
|
|
134
|
+
* Stand (the player's body is full-extent, just hanging below
|
|
135
|
+
* the ledge — the rig animates the arms-up pose). Sentinel
|
|
136
|
+
* `lastPosture = -1` forces a sync on the first tick after
|
|
137
|
+
* link, so the initial shape always matches Stand.
|
|
138
|
+
* @private
|
|
139
|
+
* @type {TransformedShape3D|null}
|
|
140
|
+
*/
|
|
141
|
+
this.colliderShapeStand = null;
|
|
142
|
+
/** @private @type {TransformedShape3D|null} */
|
|
143
|
+
this.colliderShapeCrouch = null;
|
|
144
|
+
/** @private @type {TransformedShape3D|null} */
|
|
145
|
+
this.colliderShapeProne = null;
|
|
146
|
+
/** @private */
|
|
147
|
+
this.lastPosture = -1;
|
|
148
|
+
|
|
149
|
+
/** Eye pitch in radians, clamped to config.look limits. */
|
|
150
|
+
this.eyePitch = 0;
|
|
151
|
+
/** Body yaw in radians (around world up). */
|
|
152
|
+
this.bodyYaw = 0;
|
|
153
|
+
/** Yaw rate (rad/s) computed in look consumption — for evaluators. */
|
|
154
|
+
this.yawRateRadPerSec = 0;
|
|
155
|
+
|
|
156
|
+
/** Horizontal+vertical velocity. We integrate these inside the system
|
|
157
|
+
* when no external physics layer is attached. */
|
|
158
|
+
this.velocityX = 0;
|
|
159
|
+
this.velocityY = 0;
|
|
160
|
+
this.velocityZ = 0;
|
|
161
|
+
|
|
162
|
+
/** Previous-tick jump intent — for rising/falling edge detection. */
|
|
163
|
+
this.prevJumpHeld = false;
|
|
164
|
+
/** Previous-tick crouch intent — for toggle-mode edge detection. */
|
|
165
|
+
this.prevCrouchHeld = false;
|
|
166
|
+
/** True while crouch toggle is latched on (used only in toggle mode). */
|
|
167
|
+
this.crouchLatched = false;
|
|
168
|
+
|
|
169
|
+
/** Remaining time in jump anticipation, or <= 0 if not anticipating. */
|
|
170
|
+
this.anticipationRemaining = 0;
|
|
171
|
+
/** Cached derived gravity (m/s^2) from peakHeight + timeToApex. */
|
|
172
|
+
this.gravity = 9.81;
|
|
173
|
+
/** Cached derived jump impulse (m/s upward), post-mass-scaling. */
|
|
174
|
+
this.jumpInitialVy = 5.0;
|
|
175
|
+
/**
|
|
176
|
+
* Cached mass scaling factors — computed once at link. See
|
|
177
|
+
* {@link computeMassRatios}. Heavier ⇒ lower jumpV0Scale, lower
|
|
178
|
+
* groundAccelScale, higher landingDipScale + exertionRiseScale.
|
|
179
|
+
*/
|
|
180
|
+
this.massRatios = null;
|
|
181
|
+
|
|
182
|
+
/** Spring for landing dip (under-damped → rings after impact). */
|
|
183
|
+
this.landSpring = new Spring();
|
|
184
|
+
/** Spring for FOV (critically damped). */
|
|
185
|
+
this.fovSpring = new Spring(70);
|
|
186
|
+
/** Spring for eye height (crouch transition). */
|
|
187
|
+
this.eyeHeightSpring = new Spring(1.80);
|
|
188
|
+
/** Spring for lean roll (radians) — banks into lateral acceleration. */
|
|
189
|
+
this.leanSpring = new Spring();
|
|
190
|
+
/**
|
|
191
|
+
* Lean target this tick (radians). Always set; L2.f spring-steps
|
|
192
|
+
* toward this value. Whoever owned motion this tick wrote it:
|
|
193
|
+
* base writes the lat-accel + look-lean derived value at the end
|
|
194
|
+
* of {@link _runBaseLocomotion}; abilities that want to override
|
|
195
|
+
* (WallRun → tilt-into-wall, Slide/Mantle/LedgeGrab → zero) write
|
|
196
|
+
* their own value in tick. Uniform channel — no null sentinel.
|
|
197
|
+
*/
|
|
198
|
+
this.leanTargetRad = 0;
|
|
199
|
+
|
|
200
|
+
/** Previous horizontal velocity — for lateral acceleration → lean. */
|
|
201
|
+
this.prevVelocityX = 0;
|
|
202
|
+
this.prevVelocityZ = 0;
|
|
203
|
+
|
|
204
|
+
/** Previous-tick grounded for edge detection. */
|
|
205
|
+
this.prevGrounded = true;
|
|
206
|
+
/** Vertical speed at moment of last "leave ground". */
|
|
207
|
+
this.takeoffVy = 0;
|
|
208
|
+
/** Max vertical position since last takeoff — for jump apex detection. */
|
|
209
|
+
this.peakAltitude = 0;
|
|
210
|
+
/** Set true once a jump has been launched; cleared on land. */
|
|
211
|
+
this.midJump = false;
|
|
212
|
+
/** Apex already fired for this airborne segment? */
|
|
213
|
+
this.apexFired = false;
|
|
214
|
+
|
|
215
|
+
/** Stride phase from previous fixed step — for footstep edge detection. */
|
|
216
|
+
this.prevStridePhase = 0;
|
|
217
|
+
/** Breath phase from previous fixed step — for inhale/exhale edge detection. */
|
|
218
|
+
this.prevBreathPhase = 0;
|
|
219
|
+
/** Which foot fires next — flipped on each footstep signal. */
|
|
220
|
+
this.nextFootSide = "R";
|
|
221
|
+
/**
|
|
222
|
+
* Which foot is currently bearing the body's weight (the foot that
|
|
223
|
+
* most recently landed). Drives the lateral-bob direction: at R
|
|
224
|
+
* midstance the COM is over the right foot, so the head shifts
|
|
225
|
+
* laterally toward screen-right; at L midstance the opposite.
|
|
226
|
+
* Coupled to the same signal the footstep emits, so anything that
|
|
227
|
+
* listens to onFootStep.side will see the bob agree.
|
|
228
|
+
* Initialized "L" so the very first footstep fires "R" and the
|
|
229
|
+
* standingFoot updates to "R" — putting the head laterally right
|
|
230
|
+
* during the first half-stride, as expected.
|
|
231
|
+
*/
|
|
232
|
+
this.standingFoot = "L";
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* [0..1] How "backward" the player is currently moving. Derived in
|
|
236
|
+
* fixedUpdate from velocity · screen-forward, normalized to sprint
|
|
237
|
+
* speed. Drives the gait wobble amplifier on the L3 camera-composition
|
|
238
|
+
* pass. Stored on runtime (rather than state) because it's a render-
|
|
239
|
+
* side input — downstream observers should look at velocity directly.
|
|
240
|
+
*/
|
|
241
|
+
this.backwardness = 0;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Smoothed bob amplitude envelope. Target = max(speedNormalized,
|
|
245
|
+
* backwardness) when grounded, 0 airborne. Spring decay prevents
|
|
246
|
+
* the whiplash where stopping motion would snap the bob to neutral.
|
|
247
|
+
*/
|
|
248
|
+
this.bobIntensitySpring = new Spring();
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Vertical impact spring — kicked downward at each footfall, decays
|
|
252
|
+
* with a slight under-damped overshoot. Produces the impact-arrest +
|
|
253
|
+
* leg-push curve. value units: meters (added directly to eyeLocal.y).
|
|
254
|
+
*/
|
|
255
|
+
this.verticalImpactSpring = new Spring();
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Sprint-posture spring — eye pitches forward as the player commits
|
|
259
|
+
* to a sprint, returns to neutral when they slow. Value is in
|
|
260
|
+
* radians; slower half-life than other springs so it feels like
|
|
261
|
+
* a posture change rather than an input twitch. See cfg.posture.
|
|
262
|
+
*/
|
|
263
|
+
this.sprintPostureSpring = new Spring();
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Head-droop spring — additional forward pitch as exertion rises.
|
|
267
|
+
* Sells fatigue subtly. Target tracks exertion-driven max droop
|
|
268
|
+
* angle; spring lag keeps the transition slow and physical.
|
|
269
|
+
*/
|
|
270
|
+
this.headDroopSpring = new Spring();
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* [0..1] sprintness — how much of the walk→sprint speed range the
|
|
274
|
+
* body is currently in. Computed in fixedUpdate, read by L3 for FOV
|
|
275
|
+
* and the sprint-posture pitch / forward-shift offset.
|
|
276
|
+
*/
|
|
277
|
+
this.sprintness = 0;
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Cached sin/cos of current body yaw — written once per fixedUpdate
|
|
281
|
+
* after look intent is consumed, read by every downstream step
|
|
282
|
+
* (locomotion, backwardness, lean look-rate, pose channels). Avoids
|
|
283
|
+
* recomputing the trig 3+ times per tick.
|
|
284
|
+
*/
|
|
285
|
+
this.sinYaw = 0;
|
|
286
|
+
this.cosYaw = 1;
|
|
287
|
+
|
|
288
|
+
/** Cached horizontal speed (m/s) for this tick — written in derived-state. */
|
|
289
|
+
this.horizSpeed = 0;
|
|
290
|
+
|
|
291
|
+
/** Cached stride frequency (Hz) for this tick — written in breath block, read by stride. */
|
|
292
|
+
this.strideFreqHz = 0;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Additive accumulator for body-local eye-position offsets. The
|
|
296
|
+
* system pushes its own contributions (bob, breath, landing,
|
|
297
|
+
* sprint posture) each render frame; external systems can push
|
|
298
|
+
* recoil/shake/knockback contributions via the same interface.
|
|
299
|
+
*/
|
|
300
|
+
this.eyeOffsetStack = new EyeOffsetStack();
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Spatial-query results populated by {@link FirstPersonSensorsSystem}
|
|
304
|
+
* (when present). Abilities and the locomotion FSM read this.
|
|
305
|
+
* Lives on runtime so other systems can populate it without
|
|
306
|
+
* touching the controller component's public surface.
|
|
307
|
+
*/
|
|
308
|
+
this.sensors = new FirstPersonSensors();
|
|
309
|
+
|
|
310
|
+
/** Cached eye entity ID. -1 until link assigns it. */
|
|
311
|
+
this.eyeEntity = -1;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Drives a first-person camera + body from intent fields. See sibling
|
|
317
|
+
* DESIGN.md for goals, architecture, and the five processing layers (L0..L4).
|
|
318
|
+
*
|
|
319
|
+
* - fixedUpdate runs L1 (locomotion), L2 (pose state), and L4 (events) so
|
|
320
|
+
* the simulation remains deterministic.
|
|
321
|
+
* - update runs L3 (camera composition) at render rate so the eye is never
|
|
322
|
+
* smoother than the screen.
|
|
323
|
+
*
|
|
324
|
+
* The system itself integrates a simple flat-floor at y = `config.gravity.magnitude > 0
|
|
325
|
+
* ? state.groundY : -Infinity` for the prototype. A real physics layer should
|
|
326
|
+
* write `state.grounded`/`state.groundNormal` from outside instead; the
|
|
327
|
+
* built-in resolver is just a convenience to keep the controller usable
|
|
328
|
+
* without dependencies.
|
|
329
|
+
*
|
|
330
|
+
* @author Alex Goldring
|
|
331
|
+
* @copyright Company Named Limited (c) 2026
|
|
332
|
+
*/
|
|
333
|
+
export class FirstPersonPlayerControllerSystem extends System {
|
|
334
|
+
constructor() {
|
|
335
|
+
super();
|
|
336
|
+
|
|
337
|
+
// Dependencies kept to (controller, transform) so we can ASSERT on
|
|
338
|
+
// RigidBody at link time and emit a clear error if missing. If
|
|
339
|
+
// RigidBody were a hard dep, entities lacking one would silently
|
|
340
|
+
// never link — the controller would appear inert with no
|
|
341
|
+
// diagnostic. The assert below catches the missing-body case
|
|
342
|
+
// explicitly.
|
|
343
|
+
this.dependencies = [FirstPersonPlayerController, Transform];
|
|
344
|
+
|
|
345
|
+
this.components_used = [
|
|
346
|
+
ResourceAccessSpecification.from(Transform, ResourceAccessKind.Write),
|
|
347
|
+
ResourceAccessSpecification.from(Camera, ResourceAccessKind.Write),
|
|
348
|
+
ResourceAccessSpecification.from(RigidBody, ResourceAccessKind.Write),
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Per-entity runtime, keyed by entity id.
|
|
353
|
+
* @type {Map<number, PerEntityRuntime>}
|
|
354
|
+
*/
|
|
355
|
+
this.runtime = new Map();
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* If true, the system clamps body y >= groundY and writes
|
|
359
|
+
* state.grounded itself. Turn off when wiring a real physics layer.
|
|
360
|
+
* @type {boolean}
|
|
361
|
+
*/
|
|
362
|
+
this.useBuiltInFlatGround = true;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The flat-ground y for the built-in resolver. Ignored when
|
|
366
|
+
* useBuiltInFlatGround is false.
|
|
367
|
+
* @type {number}
|
|
368
|
+
*/
|
|
369
|
+
this.groundY = 0;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Optional callback that returns the surface Y under the player
|
|
373
|
+
* for ground resolution. Called each tick with the player's
|
|
374
|
+
* current (x, y, z); returns the world-Y of the ground below,
|
|
375
|
+
* or null if no ground is below (gap / void).
|
|
376
|
+
*
|
|
377
|
+
* Combines with `useBuiltInFlatGround`: the effective ground for
|
|
378
|
+
* the tick is `max(this.groundY when enabled, resolver(...))`.
|
|
379
|
+
* Set both off (`useBuiltInFlatGround=false`, `groundResolver=null`)
|
|
380
|
+
* to defer to external physics entirely.
|
|
381
|
+
*
|
|
382
|
+
* Designed for prototypes / gyms that need elevated platforms
|
|
383
|
+
* without a full physics layer. Production should wire a real
|
|
384
|
+
* physics system instead.
|
|
385
|
+
*
|
|
386
|
+
* @type {((x:number, y:number, z:number) => number|null) | null}
|
|
387
|
+
*/
|
|
388
|
+
this.groundResolver = null;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* PhysicsSystem reference used by {@link _moveAndSlide}. Auto-
|
|
392
|
+
* acquired at startup; can be overridden by the caller. When
|
|
393
|
+
* null (no physics in the world), move-and-slide degrades to a
|
|
394
|
+
* direct position add — useful for spec setups that don't wire
|
|
395
|
+
* physics.
|
|
396
|
+
* @type {PhysicsSystem|null}
|
|
397
|
+
*/
|
|
398
|
+
this.physicsSystem = null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async startup(entityManager) {
|
|
402
|
+
this.entityManager = entityManager;
|
|
403
|
+
if (this.physicsSystem === null) {
|
|
404
|
+
const ps = entityManager.getSystem(PhysicsSystem);
|
|
405
|
+
if (ps !== null) this.physicsSystem = ps;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @param {FirstPersonPlayerController} controller
|
|
411
|
+
* @param {Transform} bodyTransform
|
|
412
|
+
* @param {number} entity
|
|
413
|
+
*/
|
|
414
|
+
link(controller, bodyTransform, entity) {
|
|
415
|
+
const ecd = this.entityManager.dataset;
|
|
416
|
+
|
|
417
|
+
// The controller assumes a kinematic-position RigidBody is co-
|
|
418
|
+
// attached on this entity. The body is the spatial proxy used
|
|
419
|
+
// for sensor raycasts and physics-side observers (other entities
|
|
420
|
+
// raycasting against the player, dynamic bodies colliding with
|
|
421
|
+
// the capsule, etc.). The controller writes Transform directly,
|
|
422
|
+
// physics derives velocity from the per-step delta. If a body is
|
|
423
|
+
// missing the controller could still drive the camera, but the
|
|
424
|
+
// physics integration silently breaks — assert here so the
|
|
425
|
+
// misconfiguration is caught at link time.
|
|
426
|
+
const rigidBody = ecd.getComponent(entity, RigidBody);
|
|
427
|
+
assert.ok(rigidBody !== undefined,
|
|
428
|
+
"FirstPersonPlayerController entity must have a co-attached RigidBody "
|
|
429
|
+
+ "(kinematic capsule). See prototype_first_person_controller.js for setup.");
|
|
430
|
+
assert.equal(rigidBody.kind, BodyKind.KinematicPosition,
|
|
431
|
+
"FirstPersonPlayerController RigidBody must be BodyKind.KinematicPosition; "
|
|
432
|
+
+ "the controller owns the Transform and physics derives velocity.");
|
|
433
|
+
// Collider is also required — _moveAndSlide casts its shape
|
|
434
|
+
// against the world to prevent tunneling. Asserted here so a
|
|
435
|
+
// missing collider surfaces at link rather than producing a
|
|
436
|
+
// null-deref at the first cast attempt.
|
|
437
|
+
const collider = ecd.getComponent(entity, Collider);
|
|
438
|
+
assert.ok(collider !== undefined,
|
|
439
|
+
"FirstPersonPlayerController entity must have a co-attached Collider. "
|
|
440
|
+
+ "The controller's move-and-slide casts this shape to detect blockers.");
|
|
441
|
+
|
|
442
|
+
const runtime = new PerEntityRuntime();
|
|
443
|
+
runtime.rigidBody = rigidBody;
|
|
444
|
+
runtime.collider = collider;
|
|
445
|
+
runtime.slideRay = new Ray3();
|
|
446
|
+
runtime.slideHit = new PhysicsSurfacePoint();
|
|
447
|
+
|
|
448
|
+
// Pre-build one capsule per posture from cfg.body. Eye-height
|
|
449
|
+
// doubles as collider-top by convention here — the prototype's
|
|
450
|
+
// `buildPlayerEntity` uses the same approximation (`totalHeight =
|
|
451
|
+
// bodyCfg.height`). The +Y offset puts the capsule bottom at
|
|
452
|
+
// transform.position so the player's "feet" stay anchored across
|
|
453
|
+
// posture changes; only the head drops/rises.
|
|
454
|
+
const radius = controller.config.body.radius;
|
|
455
|
+
runtime.colliderShapeStand = makePostureCapsule(radius, controller.config.body.height);
|
|
456
|
+
runtime.colliderShapeCrouch = makePostureCapsule(radius, controller.config.body.crouchHeight);
|
|
457
|
+
runtime.colliderShapeProne = makePostureCapsule(radius, controller.config.body.proneHeight);
|
|
458
|
+
// Force a shape sync on the first tick: even though the caller
|
|
459
|
+
// built a Stand-sized collider, we rebuild it from cfg here so a
|
|
460
|
+
// post-link config tweak (e.g. crouchHeight changed for a unit
|
|
461
|
+
// test) is reflected on the live collider without a relink.
|
|
462
|
+
runtime.lastPosture = -1;
|
|
463
|
+
|
|
464
|
+
this.runtime.set(entity, runtime);
|
|
465
|
+
|
|
466
|
+
// Derive gravity + jump impulse from designer-friendly params, then
|
|
467
|
+
// mass-scale the initial velocity (heavier ⇒ lower jump).
|
|
468
|
+
runtime.massRatios = computeMassRatios(
|
|
469
|
+
controller.config.body.mass,
|
|
470
|
+
controller.config.body.referenceMass,
|
|
471
|
+
controller.config.body.massCouplingStrength,
|
|
472
|
+
);
|
|
473
|
+
const derived = { gravity: 0, initialVelocity: 0 };
|
|
474
|
+
computeJumpFromApex(controller.config.jump.peakHeight, controller.config.jump.timeToApex, derived);
|
|
475
|
+
runtime.gravity = derived.gravity;
|
|
476
|
+
runtime.jumpInitialVy = derived.initialVelocity * runtime.massRatios.jumpV0Scale;
|
|
477
|
+
|
|
478
|
+
// Seed yaw from the starting body rotation. `toEulerAnglesYXZ`
|
|
479
|
+
// returns (pitch, yaw, roll) — we only care about y.
|
|
480
|
+
bodyTransform.rotation.toEulerAnglesYXZ(SCRATCH_V3_A);
|
|
481
|
+
runtime.bodyYaw = SCRATCH_V3_A.y;
|
|
482
|
+
runtime.eyePitch = 0;
|
|
483
|
+
|
|
484
|
+
// Initialize springs to standing-eye-height baseline
|
|
485
|
+
runtime.eyeHeightSpring.settle(controller.config.body.height);
|
|
486
|
+
runtime.fovSpring.settle(controller.config.fov.base);
|
|
487
|
+
controller.state.eyeHeight = controller.config.body.height;
|
|
488
|
+
|
|
489
|
+
// Create eye entity if one wasn't supplied
|
|
490
|
+
if (controller.eyeEntity === -1 || !ecd.entityExists(controller.eyeEntity)) {
|
|
491
|
+
const eye = new Entity();
|
|
492
|
+
|
|
493
|
+
const eyeTransform = new Transform();
|
|
494
|
+
const baseEyePos = SCRATCH_V3_A.copy(bodyTransform.position);
|
|
495
|
+
baseEyePos.y += controller.config.body.height;
|
|
496
|
+
eyeTransform.position.copy(baseEyePos);
|
|
497
|
+
|
|
498
|
+
const camera = new Camera();
|
|
499
|
+
camera.active.set(true);
|
|
500
|
+
camera.fov.set(controller.config.fov.base);
|
|
501
|
+
camera.clip_near = 0.05;
|
|
502
|
+
camera.clip_far = 1000;
|
|
503
|
+
camera.autoClip = false;
|
|
504
|
+
|
|
505
|
+
eye.add(eyeTransform);
|
|
506
|
+
eye.add(camera);
|
|
507
|
+
eye.add(SerializationMetadata.Transient);
|
|
508
|
+
|
|
509
|
+
eye.build(ecd);
|
|
510
|
+
|
|
511
|
+
controller.eyeEntity = eye.id;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
runtime.eyeEntity = controller.eyeEntity;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* @param {FirstPersonPlayerController} controller
|
|
519
|
+
* @param {Transform} bodyTransform
|
|
520
|
+
* @param {number} entity
|
|
521
|
+
*/
|
|
522
|
+
unlink(controller, bodyTransform, entity) {
|
|
523
|
+
const ecd = this.entityManager.dataset;
|
|
524
|
+
|
|
525
|
+
if (controller.eyeEntity !== -1 && ecd.entityExists(controller.eyeEntity)) {
|
|
526
|
+
ecd.removeEntity(controller.eyeEntity);
|
|
527
|
+
controller.eyeEntity = -1;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.runtime.delete(entity);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Look up the per-entity runtime for an entity that has this
|
|
535
|
+
* controller. Used by cross-system code (sensors system, future
|
|
536
|
+
* ability-driven systems) to reach internal state without leaking
|
|
537
|
+
* it onto the controller component itself.
|
|
538
|
+
*
|
|
539
|
+
* @param {number} entity
|
|
540
|
+
* @returns {PerEntityRuntime|undefined} undefined if entity is not linked
|
|
541
|
+
*/
|
|
542
|
+
getRuntime(entity) {
|
|
543
|
+
return this.runtime.get(entity);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Deterministic simulation step — L1 + L2 + L4.
|
|
548
|
+
* @param {number} dt
|
|
549
|
+
*/
|
|
550
|
+
fixedUpdate(dt) {
|
|
551
|
+
const ecd = this.entityManager.dataset;
|
|
552
|
+
if (ecd === null) return;
|
|
553
|
+
|
|
554
|
+
this._currentDt = dt;
|
|
555
|
+
ecd.traverseComponents(FirstPersonPlayerController, this._tickEntity, this);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Variable-rate camera composition — L3.
|
|
560
|
+
* @param {number} dt
|
|
561
|
+
*/
|
|
562
|
+
update(dt) {
|
|
563
|
+
const ecd = this.entityManager.dataset;
|
|
564
|
+
if (ecd === null) return;
|
|
565
|
+
|
|
566
|
+
this._currentRenderDt = dt;
|
|
567
|
+
ecd.traverseComponents(FirstPersonPlayerController, this._composeEye, this);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @private
|
|
572
|
+
* @param {FirstPersonPlayerController} controller
|
|
573
|
+
* @param {number} entity
|
|
574
|
+
*/
|
|
575
|
+
_tickEntity(controller, entity) {
|
|
576
|
+
const ecd = this.entityManager.dataset;
|
|
577
|
+
const runtime = this.runtime.get(entity);
|
|
578
|
+
if (runtime === undefined) return;
|
|
579
|
+
|
|
580
|
+
const dt = this._currentDt;
|
|
581
|
+
const cfg = controller.config;
|
|
582
|
+
const intent = controller.intent;
|
|
583
|
+
const state = controller.state;
|
|
584
|
+
const sig = controller.signals;
|
|
585
|
+
|
|
586
|
+
const bodyTransform = ecd.getComponent(entity, Transform);
|
|
587
|
+
if (bodyTransform === undefined) return;
|
|
588
|
+
|
|
589
|
+
// Decay the mastery score's EMA. Doing this once per tick keeps the
|
|
590
|
+
// score's time-window characteristic stable regardless of how many
|
|
591
|
+
// evaluators fire (they each *record* a sample, the decay
|
|
592
|
+
// independently ages all samples).
|
|
593
|
+
controller.mastery.tick(dt);
|
|
594
|
+
|
|
595
|
+
// -- L1.a: Consume look delta -----------------------------------
|
|
596
|
+
// intent.look is zeroed after consume so accumulated input doesn't
|
|
597
|
+
// re-apply on the next fixed step.
|
|
598
|
+
//
|
|
599
|
+
// Conventions (with raw mouse delta as the source — movementX/Y both
|
|
600
|
+
// positive when moving right/down):
|
|
601
|
+
// look.x > 0 ("mouse right") → turn right
|
|
602
|
+
// look.y > 0 ("mouse down") → look down (flipped by invertY)
|
|
603
|
+
//
|
|
604
|
+
// The yaw sign is negated because the engine uses left-handed
|
|
605
|
+
// coordinates with +Z as forward; a positive Y-axis rotation takes
|
|
606
|
+
// +Z toward +X, which presents to the player as a LEFT turn through
|
|
607
|
+
// the Three.js camera (`quaternion_invert_orientation`). Negating
|
|
608
|
+
// here gives the player-intuitive "mouse right → turn right".
|
|
609
|
+
const yawDelta = -intent.look.x;
|
|
610
|
+
const pitchSign = cfg.look.invertY ? -1 : 1;
|
|
611
|
+
const pitchDelta = intent.look.y * pitchSign;
|
|
612
|
+
intent.look.set(0, 0);
|
|
613
|
+
|
|
614
|
+
// Cache yaw rate for mastery evaluators (look-lean, foot-asymmetry-
|
|
615
|
+
// turn, etc.). Rad/s, signed (negative = turning right in our
|
|
616
|
+
// convention — matches yawDelta).
|
|
617
|
+
runtime.yawRateRadPerSec = yawDelta / Math.max(dt, 1e-4);
|
|
618
|
+
|
|
619
|
+
runtime.bodyYaw += yawDelta;
|
|
620
|
+
// keep yaw bounded (purely cosmetic — sin/cos handle wraparound fine)
|
|
621
|
+
if (runtime.bodyYaw > Math.PI) runtime.bodyYaw -= TWO_PI;
|
|
622
|
+
else if (runtime.bodyYaw < -Math.PI) runtime.bodyYaw += TWO_PI;
|
|
623
|
+
|
|
624
|
+
runtime.eyePitch = clamp(
|
|
625
|
+
runtime.eyePitch + pitchDelta,
|
|
626
|
+
cfg.look.pitchMinDeg * DEG_TO_RAD,
|
|
627
|
+
cfg.look.pitchMaxDeg * DEG_TO_RAD,
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// Write body yaw back to transform (pure yaw, no pitch on body)
|
|
631
|
+
bodyTransform.rotation.fromAxisAngle(Vector3.up, runtime.bodyYaw);
|
|
632
|
+
|
|
633
|
+
// -- Shared flags. Computed BEFORE the ability tick so abilities
|
|
634
|
+
// can read them. `isCrouchActive` is deliberately computed
|
|
635
|
+
// AFTER the ability tick because `_resolveCrouchHeld` mutates
|
|
636
|
+
// `runtime.prevCrouchHeld` — abilities like Slide need to see
|
|
637
|
+
// the previous-tick value to detect a rising edge on the
|
|
638
|
+
// crouch press.
|
|
639
|
+
const isSprintIntent = intent.sprint && intent.move.y > 0.5 && state.grounded;
|
|
640
|
+
const isBackwardIntent = intent.move.y < 0;
|
|
641
|
+
runtime.sinYaw = Math.sin(runtime.bodyYaw);
|
|
642
|
+
runtime.cosYaw = Math.cos(runtime.bodyYaw);
|
|
643
|
+
// L2 observers read sinYaw/cosYaw as locals — destructure once.
|
|
644
|
+
const { sinYaw, cosYaw } = runtime;
|
|
645
|
+
|
|
646
|
+
// -- Ability layer: at most one active ability owns motion. The
|
|
647
|
+
// set returns true when no ability owned the tick, in which
|
|
648
|
+
// case base L1.b-h runs below; false means an ability fully
|
|
649
|
+
// handled this tick (it called the system's helpers for any
|
|
650
|
+
// standard work it wanted to keep, e.g. gravity).
|
|
651
|
+
const runBaseLocomotion = controller.abilities.tick(
|
|
652
|
+
controller, runtime, bodyTransform, runtime.sensors, dt, this,
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// Now resolve crouch (updates prevCrouchHeld) — used by base and L2.
|
|
656
|
+
const isCrouchActive = this._resolveCrouchHeld(controller, runtime);
|
|
657
|
+
|
|
658
|
+
if (runBaseLocomotion) {
|
|
659
|
+
this._runBaseLocomotion(
|
|
660
|
+
controller, runtime, bodyTransform, dt,
|
|
661
|
+
isCrouchActive, isSprintIntent, isBackwardIntent,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// (everything below this line runs every tick — L2 observers don't
|
|
666
|
+
// care who owned motion)
|
|
667
|
+
|
|
668
|
+
// -- L2.a: speed / moveMode ------------------------------------
|
|
669
|
+
// -- L2.a: speed / moveMode ------------------------------------
|
|
670
|
+
const horizSpeed = Math.hypot(runtime.velocityX, runtime.velocityZ);
|
|
671
|
+
runtime.horizSpeed = horizSpeed;
|
|
672
|
+
state.speed = horizSpeed;
|
|
673
|
+
state.speedNormalized = clamp(horizSpeed / Math.max(cfg.motion.sprintSpeed, 1e-3), 0, 1);
|
|
674
|
+
|
|
675
|
+
// Backwardness: 0 = moving forward (or sideways), 1 = moving directly
|
|
676
|
+
// backward at the back-pedal speed ceiling. Derived from the actual
|
|
677
|
+
// velocity (not the intent) so external knockback or stuck states
|
|
678
|
+
// also register as "moving backward" and the gait wobble reflects it.
|
|
679
|
+
//
|
|
680
|
+
// Reference speed is the *achievable* backward max — walkSpeed ×
|
|
681
|
+
// backwardSpeedFactor — NOT the sprint speed. Backward can never
|
|
682
|
+
// reach sprint, so normalizing against sprint would cap backwardness
|
|
683
|
+
// at ~0.3 and the wobble multipliers below would barely apply.
|
|
684
|
+
const screenFwdVel = runtime.velocityX * sinYaw + runtime.velocityZ * cosYaw;
|
|
685
|
+
const maxBackwardSpeed = Math.max(cfg.motion.walkSpeed * cfg.motion.backwardSpeedFactor, 1e-3);
|
|
686
|
+
runtime.backwardness = clamp(-screenFwdVel / maxBackwardSpeed, 0, 1);
|
|
687
|
+
|
|
688
|
+
// Locomotion mode is the *intent-driven* horizontal mode. Airborne
|
|
689
|
+
// state is tracked separately on pose.actionState — they're
|
|
690
|
+
// orthogonal facets (you can be Sprint+Airborne after a jump).
|
|
691
|
+
const prevLocomotionMode = state.locomotionMode;
|
|
692
|
+
if (isCrouchActive) {
|
|
693
|
+
state.locomotionMode = FirstPersonLocomotionMode.Crouch;
|
|
694
|
+
} else if (isSprintIntent && horizSpeed > 0.1) {
|
|
695
|
+
state.locomotionMode = FirstPersonLocomotionMode.Sprint;
|
|
696
|
+
} else if (horizSpeed > 0.1) {
|
|
697
|
+
state.locomotionMode = FirstPersonLocomotionMode.Walk;
|
|
698
|
+
} else {
|
|
699
|
+
state.locomotionMode = FirstPersonLocomotionMode.Idle;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (state.locomotionMode === FirstPersonLocomotionMode.Sprint
|
|
703
|
+
&& prevLocomotionMode !== FirstPersonLocomotionMode.Sprint) {
|
|
704
|
+
sig.onSprintStart.send0();
|
|
705
|
+
} else if (prevLocomotionMode === FirstPersonLocomotionMode.Sprint
|
|
706
|
+
&& state.locomotionMode !== FirstPersonLocomotionMode.Sprint) {
|
|
707
|
+
sig.onSprintStop.send0();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// -- L2.b: Exertion --------------------------------------------
|
|
711
|
+
// Heavier bodies tire faster — sprint rise scales with massRatios.exertionRiseScale.
|
|
712
|
+
const exertionRise = isSprintIntent
|
|
713
|
+
? cfg.exertion.sprintRiseRate * runtime.massRatios.exertionRiseScale
|
|
714
|
+
: 0;
|
|
715
|
+
const exertionFall = exertionRise > 0 ? 0 : cfg.exertion.idleDecayRate;
|
|
716
|
+
state.exertion = clamp(state.exertion + (exertionRise - exertionFall) * dt, 0, 1);
|
|
717
|
+
|
|
718
|
+
// -- L2.c: Breath ----------------------------------------------
|
|
719
|
+
// breathRate and breathAmplitude lag exertion through separate
|
|
720
|
+
// exponential decays. Rate hangs around longer than amplitude.
|
|
721
|
+
const metabolicRate = lerp(cfg.breath.rateRestHz, cfg.breath.rateMaxHz, state.exertion);
|
|
722
|
+
const targetAmp = lerp(cfg.breath.amplitudeRestM, cfg.breath.amplitudeMaxM, state.exertion);
|
|
723
|
+
|
|
724
|
+
// Locomotor-respiratory coupling — see math/computeLRCBreathRate.
|
|
725
|
+
// The pure function is unit-tested; this site just provides inputs.
|
|
726
|
+
//
|
|
727
|
+
// Gait is gated on a "feet strike the ground" posture (Stand /
|
|
728
|
+
// Crouch). Prone (slide) and Hang (ledge-grab) have no stride —
|
|
729
|
+
// the body's feet are not making contact in a walking pattern,
|
|
730
|
+
// so stride frequency drops to zero and downstream gait
|
|
731
|
+
// signals (footsteps, bob intensity) go quiet.
|
|
732
|
+
const feetStriking = state.posture === FirstPersonPosture.Stand
|
|
733
|
+
|| state.posture === FirstPersonPosture.Crouch;
|
|
734
|
+
const strideFreqHz = feetStriking && state.grounded && horizSpeed > cfg.bob.minStepSpeed
|
|
735
|
+
? cfg.bob.stepFreqAtWalk * Math.pow(
|
|
736
|
+
Math.max(horizSpeed, 1e-3) / Math.max(cfg.motion.walkSpeed, 1e-3),
|
|
737
|
+
cfg.bob.stepFreqExp,
|
|
738
|
+
)
|
|
739
|
+
: 0;
|
|
740
|
+
const targetRate = computeLRCBreathRate(
|
|
741
|
+
metabolicRate,
|
|
742
|
+
strideFreqHz,
|
|
743
|
+
state.exertion,
|
|
744
|
+
cfg.breath.locomotorCouplingMax,
|
|
745
|
+
cfg.breath.couplingMinStrideFreqHz,
|
|
746
|
+
);
|
|
747
|
+
state.breathRateHz = exponentialApproach(state.breathRateHz, targetRate, cfg.exertion.rateDecayHalfLife, dt);
|
|
748
|
+
state.breathAmplitudeM = exponentialApproach(state.breathAmplitudeM, targetAmp, cfg.exertion.ampDecayHalfLife, dt);
|
|
749
|
+
|
|
750
|
+
runtime.prevBreathPhase = state.breathPhase;
|
|
751
|
+
state.breathPhase += state.breathRateHz * dt;
|
|
752
|
+
state.breathPhase -= Math.floor(state.breathPhase); // wrap [0,1)
|
|
753
|
+
|
|
754
|
+
// Breath edge detection — inhale at 0.25, exhale at 0.75
|
|
755
|
+
if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.25)) {
|
|
756
|
+
sig.onBreathIn.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
|
|
757
|
+
}
|
|
758
|
+
if (phaseCrossed(runtime.prevBreathPhase, state.breathPhase, 0.75)) {
|
|
759
|
+
sig.onBreathOut.send1({ amplitude: state.breathAmplitudeM, rateHz: state.breathRateHz });
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// -- L2.d: Stride ----------------------------------------------
|
|
763
|
+
// strideFreqHz computed above in the breath block; reused here.
|
|
764
|
+
runtime.prevStridePhase = state.stridePhase;
|
|
765
|
+
if (strideFreqHz > 0) {
|
|
766
|
+
// 1 full stride cycle = 2 footfalls; phase advances at freq/2 of cycle
|
|
767
|
+
state.stridePhase += (strideFreqHz * 0.5) * dt;
|
|
768
|
+
state.stridePhase -= Math.floor(state.stridePhase);
|
|
769
|
+
}
|
|
770
|
+
// Footstep on phase wraparound past 0 (R) or past 0.5 (L). Same
|
|
771
|
+
// posture gate as stride advance — feet must be striking.
|
|
772
|
+
if (feetStriking && state.grounded && horizSpeed > cfg.bob.minStepSpeed) {
|
|
773
|
+
const fireFootstep = () => {
|
|
774
|
+
state.stepCount++;
|
|
775
|
+
const side = runtime.nextFootSide;
|
|
776
|
+
runtime.nextFootSide = side === "R" ? "L" : "R";
|
|
777
|
+
// The foot that just fired is now the one bearing weight
|
|
778
|
+
// through the upcoming half-stride. Drives lateral-bob sign.
|
|
779
|
+
runtime.standingFoot = side;
|
|
780
|
+
sig.onFootStep.send1({ side, speed: horizSpeed, surfaceTag: state.surfaceTag });
|
|
781
|
+
// Kick the vertical impact spring DOWNWARD. The kick magnitude
|
|
782
|
+
// is the per-step desired peak dip × impactKickMultiplier; the
|
|
783
|
+
// multiplier is empirical (depends on impact spring params) so
|
|
784
|
+
// that "verticalAmpAtWalk" still corresponds approximately to
|
|
785
|
+
// the visible peak dip depth. Scaled by bobIntensity so a
|
|
786
|
+
// mid-deceleration footstep doesn't deliver a full-strength
|
|
787
|
+
// impulse.
|
|
788
|
+
const massBoost = (cfg.body.mass - 80) * cfg.bob.ampMassScale;
|
|
789
|
+
const ampVMult = 1 + (cfg.bob.backwardVerticalAmpFactor - 1) * runtime.backwardness;
|
|
790
|
+
const peakDip = (cfg.bob.verticalAmpAtWalk + massBoost) * runtime.bobIntensitySpring.value * ampVMult;
|
|
791
|
+
runtime.verticalImpactSpring.kick(-peakDip * cfg.bob.impactKickMultiplier);
|
|
792
|
+
};
|
|
793
|
+
if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0)) {
|
|
794
|
+
fireFootstep();
|
|
795
|
+
}
|
|
796
|
+
if (phaseCrossed(runtime.prevStridePhase, state.stridePhase, 0.5)) {
|
|
797
|
+
fireFootstep();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// -- L2.d.bob-intensity & impact -------------------------------
|
|
802
|
+
// Smoothed bob amplitude envelope: when the player starts/stops
|
|
803
|
+
// moving the visible bob fades in/out rather than cutting on/off.
|
|
804
|
+
// Target = the "natural" amp scale (max of speed and backwardness)
|
|
805
|
+
// while grounded, zero while airborne so the bob disappears mid-jump.
|
|
806
|
+
const naturalBobIntensity = Math.max(state.speedNormalized, runtime.backwardness);
|
|
807
|
+
// Bob fades to zero whenever feet aren't striking (airborne, or
|
|
808
|
+
// Prone/Hang posture). The verticalImpactSpring (separate
|
|
809
|
+
// channel) still carries any entry/landing kicks through to the
|
|
810
|
+
// camera, but no recurring step bob.
|
|
811
|
+
const targetBobIntensity = (state.grounded && feetStriking) ? naturalBobIntensity : 0;
|
|
812
|
+
runtime.bobIntensitySpring.stepTo(targetBobIntensity, cfg.bob.intensityHalfLife, 1.0, dt);
|
|
813
|
+
|
|
814
|
+
// Vertical impact spring — damped decay toward 0, with the under-
|
|
815
|
+
// damped overshoot that produces the recovery + leg-push curve.
|
|
816
|
+
runtime.verticalImpactSpring.stepTo(0, cfg.bob.impactSpringHalfLife, cfg.bob.impactSpringZeta, dt);
|
|
817
|
+
|
|
818
|
+
// Sprint posture — head pitches forward as commitment to sprint
|
|
819
|
+
// builds. Driven by "sprintness" — how much of the gap between
|
|
820
|
+
// walk and sprint speed the player is *currently* in (0..1). The
|
|
821
|
+
// pitch target is multiplied by sprintness, then critically damped.
|
|
822
|
+
// Only applies while grounded — pitching into airborne motion looks weird.
|
|
823
|
+
const sprintness = clamp(
|
|
824
|
+
(state.speed - cfg.motion.walkSpeed)
|
|
825
|
+
/ Math.max(cfg.motion.sprintSpeed - cfg.motion.walkSpeed, 1e-3),
|
|
826
|
+
0, 1,
|
|
827
|
+
);
|
|
828
|
+
const targetSprintPitch = state.grounded
|
|
829
|
+
? cfg.posture.sprintForwardPitchDeg * DEG_TO_RAD * sprintness
|
|
830
|
+
: 0;
|
|
831
|
+
runtime.sprintPostureSpring.stepTo(
|
|
832
|
+
targetSprintPitch,
|
|
833
|
+
cfg.posture.sprintForwardPitchHalfLife,
|
|
834
|
+
1.0, dt,
|
|
835
|
+
);
|
|
836
|
+
runtime.sprintness = sprintness;
|
|
837
|
+
|
|
838
|
+
// Head droop — exertion drives a subtle additional forward pitch.
|
|
839
|
+
// Combines with sprintPostureSpring (sprint = head down to commit)
|
|
840
|
+
// so a fatigued sprinter has BOTH effects layered.
|
|
841
|
+
const targetDroopRad = cfg.exertion.headDroopAtMaxDeg * DEG_TO_RAD * state.exertion;
|
|
842
|
+
runtime.headDroopSpring.stepTo(targetDroopRad, cfg.exertion.headDroopHalfLife, 1.0, dt);
|
|
843
|
+
|
|
844
|
+
// -- L2.e: Posture → eye height --------------------------------
|
|
845
|
+
// Posture is set by whichever layer owned motion this tick: base
|
|
846
|
+
// writes Stand / Crouch from isCrouchActive (see end of
|
|
847
|
+
// _runBaseLocomotion); active abilities write Prone (Slide) or
|
|
848
|
+
// Hang (LedgeGrab) in their tick. Mapping is one switch — adding
|
|
849
|
+
// a new posture is one enum value + one case.
|
|
850
|
+
let targetEyeH;
|
|
851
|
+
switch (state.posture) {
|
|
852
|
+
case FirstPersonPosture.Prone: targetEyeH = cfg.body.proneHeight; break;
|
|
853
|
+
case FirstPersonPosture.Crouch: targetEyeH = cfg.body.crouchHeight; break;
|
|
854
|
+
case FirstPersonPosture.Hang: targetEyeH = cfg.body.height; break;
|
|
855
|
+
case FirstPersonPosture.Stand:
|
|
856
|
+
default: targetEyeH = cfg.body.height; break;
|
|
857
|
+
}
|
|
858
|
+
const crouchHalfLife = cfg.crouch.transitionTime / 4; // halfLife is ~quarter of full transition
|
|
859
|
+
runtime.eyeHeightSpring.stepTo(targetEyeH, crouchHalfLife, 1.0, dt);
|
|
860
|
+
state.eyeHeight = runtime.eyeHeightSpring.value;
|
|
861
|
+
|
|
862
|
+
if (isCrouchActive !== state.crouchActive) {
|
|
863
|
+
state.crouchActive = isCrouchActive;
|
|
864
|
+
if (isCrouchActive) {
|
|
865
|
+
sig.onCrouchEnter.send0();
|
|
866
|
+
// Impulse: dropping into a crouch grips the knees. Small
|
|
867
|
+
// bump — we don't want crouch-spamming to instantly tire.
|
|
868
|
+
state.exertion = clamp(
|
|
869
|
+
state.exertion + cfg.exertion.crouchEnterRise * runtime.massRatios.exertionRiseScale,
|
|
870
|
+
0, 1,
|
|
871
|
+
);
|
|
872
|
+
} else {
|
|
873
|
+
sig.onCrouchExit.send0();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// -- L2.f: Lean spring → camera roll ---------------------------
|
|
878
|
+
// The TARGET for this tick was written by whichever layer owned
|
|
879
|
+
// motion: base writes the lat-accel + look-lean derived value at
|
|
880
|
+
// the end of _runBaseLocomotion; abilities override (WallRun
|
|
881
|
+
// tilts toward the wall; Slide / LedgeGrab / Mantle force zero).
|
|
882
|
+
// L2.f is now a flat spring-step + commit — no branching, no
|
|
883
|
+
// null sentinel.
|
|
884
|
+
runtime.prevVelocityX = runtime.velocityX;
|
|
885
|
+
runtime.prevVelocityZ = runtime.velocityZ;
|
|
886
|
+
runtime.leanSpring.stepTo(runtime.leanTargetRad, cfg.lean.spring.halfLife, cfg.lean.spring.zeta, dt);
|
|
887
|
+
state.leanRollRad = runtime.leanSpring.value;
|
|
888
|
+
|
|
889
|
+
// -- L2.g: Land spring decay (drives the landing recovery dip) -
|
|
890
|
+
// Target is 0; under-damped (cfg zeta < 1) so it rings.
|
|
891
|
+
runtime.landSpring.stepTo(0, cfg.landing.recovery.spring.halfLife, cfg.landing.recovery.spring.zeta, dt);
|
|
892
|
+
|
|
893
|
+
// -- L2.h: Publish pose channels --------------------------------
|
|
894
|
+
this._publishPose(controller, runtime, bodyTransform);
|
|
895
|
+
|
|
896
|
+
// -- L2.i: Sync collider shape to posture -----------------------
|
|
897
|
+
// All posture-writers (base locomotion + any active ability)
|
|
898
|
+
// have run for this tick. Swap the collider's shape to the
|
|
899
|
+
// pre-built capsule matching the final posture so downstream
|
|
900
|
+
// physics queries (move-and-slide cast, sensors, overlap from
|
|
901
|
+
// outside) see the right volume. No-op when posture is
|
|
902
|
+
// unchanged.
|
|
903
|
+
this._syncColliderShape(runtime, state.posture);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* @private
|
|
908
|
+
* @param {FirstPersonPlayerController} controller
|
|
909
|
+
* @param {PerEntityRuntime} runtime
|
|
910
|
+
* @returns {boolean}
|
|
911
|
+
*/
|
|
912
|
+
/**
|
|
913
|
+
* Swap {@link Collider.shape} to the pre-built capsule that matches
|
|
914
|
+
* the player's current posture. Cheap — just a reference swap when
|
|
915
|
+
* the posture changed, no-op otherwise. The pre-built shapes live
|
|
916
|
+
* on the runtime (see {@link PerEntityRuntime.colliderShapeStand}
|
|
917
|
+
* etc.) so this method allocates nothing per tick.
|
|
918
|
+
*
|
|
919
|
+
* Hang posture reuses Stand: the player's body is full-extent,
|
|
920
|
+
* hanging below the ledge — the rig handles the arms-up animation,
|
|
921
|
+
* but the collision volume is unchanged. If a game ever wants a
|
|
922
|
+
* narrower hang silhouette (e.g. wedging into a chimney) it can
|
|
923
|
+
* add a `colliderShapeHang` and route here.
|
|
924
|
+
*
|
|
925
|
+
* @private
|
|
926
|
+
*/
|
|
927
|
+
_syncColliderShape(runtime, posture) {
|
|
928
|
+
if (posture === runtime.lastPosture) return;
|
|
929
|
+
let next;
|
|
930
|
+
if (posture === FirstPersonPosture.Crouch) {
|
|
931
|
+
next = runtime.colliderShapeCrouch;
|
|
932
|
+
} else if (posture === FirstPersonPosture.Prone) {
|
|
933
|
+
next = runtime.colliderShapeProne;
|
|
934
|
+
} else {
|
|
935
|
+
// Stand and Hang share the full-extent capsule.
|
|
936
|
+
next = runtime.colliderShapeStand;
|
|
937
|
+
}
|
|
938
|
+
runtime.collider.shape = next;
|
|
939
|
+
runtime.lastPosture = posture;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
_resolveCrouchHeld(controller, runtime) {
|
|
943
|
+
const cfg = controller.config;
|
|
944
|
+
const intent = controller.intent;
|
|
945
|
+
|
|
946
|
+
if (cfg.crouch.mode === "toggle") {
|
|
947
|
+
// Edge: rising press flips the latch
|
|
948
|
+
if (intent.crouch && !runtime.prevCrouchHeld) {
|
|
949
|
+
runtime.crouchLatched = !runtime.crouchLatched;
|
|
950
|
+
}
|
|
951
|
+
runtime.prevCrouchHeld = intent.crouch;
|
|
952
|
+
return runtime.crouchLatched;
|
|
953
|
+
}
|
|
954
|
+
// "hold" mode
|
|
955
|
+
runtime.prevCrouchHeld = intent.crouch;
|
|
956
|
+
return intent.crouch;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Jump finite-state-machine: button-edge detection, buffer + coyote
|
|
961
|
+
* grace, anticipation timer, impulse on completion. Variable-height
|
|
962
|
+
* cut is captured here as a `state.isVariableJumpCut` flag that the
|
|
963
|
+
* gravity step in `_integrateVerticalAndResolveGround` consumes.
|
|
964
|
+
*
|
|
965
|
+
* @private
|
|
966
|
+
* @param {FirstPersonPlayerController} controller
|
|
967
|
+
* @param {PerEntityRuntime} runtime
|
|
968
|
+
* @param {Transform} bodyTransform
|
|
969
|
+
* @param {number} dt
|
|
970
|
+
*/
|
|
971
|
+
_advanceJumpFsm(controller, runtime, bodyTransform, dt) {
|
|
972
|
+
const cfg = controller.config;
|
|
973
|
+
const intent = controller.intent;
|
|
974
|
+
const state = controller.state;
|
|
975
|
+
const sig = controller.signals;
|
|
976
|
+
|
|
977
|
+
const jumpPressedEdge = intent.jump && !runtime.prevJumpHeld;
|
|
978
|
+
const jumpReleasedEdge = !intent.jump && runtime.prevJumpHeld;
|
|
979
|
+
runtime.prevJumpHeld = intent.jump;
|
|
980
|
+
|
|
981
|
+
if (jumpPressedEdge) {
|
|
982
|
+
state.jumpBufferRemaining = cfg.jump.bufferTime;
|
|
983
|
+
}
|
|
984
|
+
state.jumpBufferRemaining = Math.max(0, state.jumpBufferRemaining - dt);
|
|
985
|
+
|
|
986
|
+
const canJumpNow =
|
|
987
|
+
(state.grounded || state.timeSinceGrounded < cfg.jump.coyoteTime)
|
|
988
|
+
&& state.jumpBufferRemaining > 0
|
|
989
|
+
&& !state.inJumpAnticipation
|
|
990
|
+
&& !runtime.midJump;
|
|
991
|
+
|
|
992
|
+
if (canJumpNow) {
|
|
993
|
+
// Begin anticipation — squash; impulse fires after duration elapses
|
|
994
|
+
state.inJumpAnticipation = true;
|
|
995
|
+
runtime.anticipationRemaining = cfg.jump.anticipation.duration;
|
|
996
|
+
state.jumpBufferRemaining = 0; // claimed
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Variable-height cut: only valid during ascent, post-launch.
|
|
1000
|
+
if (jumpReleasedEdge && runtime.midJump && runtime.velocityY > 0) {
|
|
1001
|
+
state.isVariableJumpCut = true;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Anticipation timer; impulse on completion.
|
|
1005
|
+
//
|
|
1006
|
+
// Anticipation completes regardless of grounded state. The reason
|
|
1007
|
+
// we DON'T cancel on `!grounded`: the canonical coyote-jump path
|
|
1008
|
+
// depends on it. The player walks off a ledge (grounded → false),
|
|
1009
|
+
// presses jump within the coyote window, canJumpNow accepts on
|
|
1010
|
+
// the coyote branch and starts anticipation. If we cancelled
|
|
1011
|
+
// anticipation here on !grounded, the impulse would never fire
|
|
1012
|
+
// and "coyote time" would be silently dead — the FSM's own next-
|
|
1013
|
+
// statement contradicting the canJumpNow gate three lines up.
|
|
1014
|
+
//
|
|
1015
|
+
// The same logic handles the rug-pull case (player on a moving
|
|
1016
|
+
// platform that slides out mid-anticipation): the player
|
|
1017
|
+
// committed to the jump, they get the jump. A future
|
|
1018
|
+
// knockback / stagger system can explicitly clear
|
|
1019
|
+
// inJumpAnticipation if it wants to override that commitment.
|
|
1020
|
+
if (state.inJumpAnticipation) {
|
|
1021
|
+
runtime.anticipationRemaining -= dt;
|
|
1022
|
+
if (runtime.anticipationRemaining <= 0) {
|
|
1023
|
+
// Mastery: gather a multiplier from all evaluators
|
|
1024
|
+
// registered for JumpImpulse. Default (no evaluators)
|
|
1025
|
+
// returns 1.0 → unchanged behaviour.
|
|
1026
|
+
const masteryMul = controller.mastery.evaluate(
|
|
1027
|
+
DecisionPoint.JumpImpulse, controller, runtime,
|
|
1028
|
+
);
|
|
1029
|
+
runtime.velocityY = runtime.jumpInitialVy * masteryMul;
|
|
1030
|
+
runtime.midJump = true;
|
|
1031
|
+
runtime.apexFired = false;
|
|
1032
|
+
runtime.peakAltitude = bodyTransform.position.y;
|
|
1033
|
+
state.inJumpAnticipation = false;
|
|
1034
|
+
state.isVariableJumpCut = false;
|
|
1035
|
+
state.isAscending = true;
|
|
1036
|
+
state.exertion = clamp(
|
|
1037
|
+
state.exertion + cfg.exertion.jumpRise * runtime.massRatios.exertionRiseScale,
|
|
1038
|
+
0, 1,
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
sig.onJumpStart.send1({ peakHeight: cfg.jump.peakHeight });
|
|
1042
|
+
sig.onLeaveGround.send1({ reason: "jump" });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Sweep the player's collider along (dx, dy, dz) via
|
|
1049
|
+
* {@link PhysicsSystem.shapeCast} and translate the Transform up to
|
|
1050
|
+
* (but not past) the first contact. Prevents tunneling through
|
|
1051
|
+
* static geometry and creep-penetration over many ticks.
|
|
1052
|
+
*
|
|
1053
|
+
* v1 limitations:
|
|
1054
|
+
* - The broadphase shape-cast returns the back-along-the-sweep
|
|
1055
|
+
* normal (`−direction`), not the true surface normal. With
|
|
1056
|
+
* that, the principled "slide along the surface" residual is
|
|
1057
|
+
* `delta -= dot(delta, n)·n = 0` — i.e. the player stops at
|
|
1058
|
+
* contact instead of sliding tangent to the wall. Once
|
|
1059
|
+
* narrowphase refinement lands and `result.normal` becomes the
|
|
1060
|
+
* true surface normal, the same residual computation will
|
|
1061
|
+
* naturally produce sliding without an API change.
|
|
1062
|
+
* - SKIN clearance (5 mm) keeps the player just shy of the wall
|
|
1063
|
+
* so the next cast doesn't start with the capsule already in
|
|
1064
|
+
* contact. Picking this too small risks GJK reporting `t = 0`
|
|
1065
|
+
* and the player getting stuck; too large is visible as a gap.
|
|
1066
|
+
*
|
|
1067
|
+
* Falls through to a direct position add when the host hasn't
|
|
1068
|
+
* wired a {@link PhysicsSystem} — useful for spec setups that
|
|
1069
|
+
* don't bring physics up.
|
|
1070
|
+
*
|
|
1071
|
+
* @private
|
|
1072
|
+
* @param {PerEntityRuntime} runtime
|
|
1073
|
+
* @param {Transform} bodyTransform
|
|
1074
|
+
* @param {number} deltaX
|
|
1075
|
+
* @param {number} deltaY
|
|
1076
|
+
* @param {number} deltaZ
|
|
1077
|
+
* @returns {boolean} true if a contact occurred (and the sweep was
|
|
1078
|
+
* truncated); false on a clean full advance.
|
|
1079
|
+
*/
|
|
1080
|
+
_moveAndSlide(runtime, bodyTransform, deltaX, deltaY, deltaZ) {
|
|
1081
|
+
if (this.physicsSystem === null) {
|
|
1082
|
+
// No physics in this world — treat the cast as a free path
|
|
1083
|
+
// and just advance.
|
|
1084
|
+
if (deltaX !== 0 || deltaY !== 0 || deltaZ !== 0) {
|
|
1085
|
+
bodyTransform.position._add(deltaX, deltaY, deltaZ);
|
|
1086
|
+
}
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Sweep + slide along the contact tangent, iterating to handle
|
|
1091
|
+
// multi-contact corners. PhysicsSystem.shapeCast returns the true
|
|
1092
|
+
// surface normal (narrowphase-refined), so the canonical
|
|
1093
|
+
// projection `residual -= dot(residual, n)·n` lands cleanly.
|
|
1094
|
+
//
|
|
1095
|
+
// Up to MAX_ITERS iterations: first contact stops at the wall and
|
|
1096
|
+
// projects the leftover motion onto the wall's tangent; the
|
|
1097
|
+
// second iteration sweeps that tangent through any second wall
|
|
1098
|
+
// (corner case) and projects again; etc. With axis-aligned
|
|
1099
|
+
// walls a corner needs ≤2 iterations. The cap defends against
|
|
1100
|
+
// pathological geometry (a player in a cone of inward-pointing
|
|
1101
|
+
// walls).
|
|
1102
|
+
const ownCollider = runtime.collider;
|
|
1103
|
+
const filter = (_e, c) => c !== ownCollider;
|
|
1104
|
+
const CAST_STEP_HEIGHT = 0.05;
|
|
1105
|
+
const SKIN = 0.005;
|
|
1106
|
+
const MAX_ITERS = 4;
|
|
1107
|
+
|
|
1108
|
+
let remX = deltaX, remY = deltaY, remZ = deltaZ;
|
|
1109
|
+
let didHit = false;
|
|
1110
|
+
|
|
1111
|
+
for (let iter = 0; iter < MAX_ITERS; iter++) {
|
|
1112
|
+
const len = Math.hypot(remX, remY, remZ);
|
|
1113
|
+
if (len < 1e-6) break;
|
|
1114
|
+
|
|
1115
|
+
const inv = 1 / len;
|
|
1116
|
+
const ndx = remX * inv;
|
|
1117
|
+
const ndy = remY * inv;
|
|
1118
|
+
const ndz = remZ * inv;
|
|
1119
|
+
|
|
1120
|
+
const ray = runtime.slideRay;
|
|
1121
|
+
ray.setOrigin(
|
|
1122
|
+
bodyTransform.position.x,
|
|
1123
|
+
bodyTransform.position.y + CAST_STEP_HEIGHT,
|
|
1124
|
+
bodyTransform.position.z,
|
|
1125
|
+
);
|
|
1126
|
+
ray.setDirection(ndx, ndy, ndz);
|
|
1127
|
+
ray.tMax = len;
|
|
1128
|
+
|
|
1129
|
+
const hit = this.physicsSystem.shapeCast(
|
|
1130
|
+
ray,
|
|
1131
|
+
runtime.collider.shape,
|
|
1132
|
+
bodyTransform.rotation,
|
|
1133
|
+
runtime.slideHit,
|
|
1134
|
+
filter,
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
if (!hit) {
|
|
1138
|
+
bodyTransform.position._add(remX, remY, remZ);
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
didHit = true;
|
|
1143
|
+
const advance = Math.max(0, runtime.slideHit.t - SKIN);
|
|
1144
|
+
if (advance > 0) {
|
|
1145
|
+
bodyTransform.position._add(ndx * advance, ndy * advance, ndz * advance);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Project the residual onto the contact tangent. `len - t`
|
|
1149
|
+
// is what we still wanted to travel; the SKIN slice (the
|
|
1150
|
+
// gap between (t - SKIN) and t) is lost as clearance.
|
|
1151
|
+
const leftoverLen = len - runtime.slideHit.t;
|
|
1152
|
+
if (leftoverLen <= 0) break;
|
|
1153
|
+
|
|
1154
|
+
const nx = runtime.slideHit.normal.x;
|
|
1155
|
+
const ny = runtime.slideHit.normal.y;
|
|
1156
|
+
const nz = runtime.slideHit.normal.z;
|
|
1157
|
+
const dotD = ndx * nx + ndy * ny + ndz * nz;
|
|
1158
|
+
const tx = ndx - dotD * nx;
|
|
1159
|
+
const ty = ndy - dotD * ny;
|
|
1160
|
+
const tz = ndz - dotD * nz;
|
|
1161
|
+
remX = tx * leftoverLen;
|
|
1162
|
+
remY = ty * leftoverLen;
|
|
1163
|
+
remZ = tz * leftoverLen;
|
|
1164
|
+
|
|
1165
|
+
// Project velocity too, but only the into-wall component.
|
|
1166
|
+
// Moving away from the wall (dotV > 0 with the outward
|
|
1167
|
+
// normal) is left alone.
|
|
1168
|
+
const dotV = runtime.velocityX * nx + runtime.velocityY * ny + runtime.velocityZ * nz;
|
|
1169
|
+
if (dotV < 0) {
|
|
1170
|
+
runtime.velocityX -= dotV * nx;
|
|
1171
|
+
runtime.velocityY -= dotV * ny;
|
|
1172
|
+
runtime.velocityZ -= dotV * nz;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return didHit;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Gravity (with fall and cut multipliers), vertical integration,
|
|
1180
|
+
* built-in flat-floor resolution (land event + impulse), and jump-apex
|
|
1181
|
+
* detection. The full vertical phase of one fixed step.
|
|
1182
|
+
*
|
|
1183
|
+
* The built-in flat-floor branch only runs when `useBuiltInFlatGround`
|
|
1184
|
+
* is true (the prototype's standalone mode); with an external physics
|
|
1185
|
+
* layer attached the system relies on the layer to set `state.grounded`
|
|
1186
|
+
* and only maintains airborne/grounded timers here.
|
|
1187
|
+
*
|
|
1188
|
+
* @private
|
|
1189
|
+
* @param {FirstPersonPlayerController} controller
|
|
1190
|
+
* @param {PerEntityRuntime} runtime
|
|
1191
|
+
* @param {Transform} bodyTransform
|
|
1192
|
+
* @param {number} dt
|
|
1193
|
+
*/
|
|
1194
|
+
_integrateVerticalAndResolveGround(controller, runtime, bodyTransform, dt) {
|
|
1195
|
+
const cfg = controller.config;
|
|
1196
|
+
const state = controller.state;
|
|
1197
|
+
const sig = controller.signals;
|
|
1198
|
+
|
|
1199
|
+
// Gravity with fall/cut multipliers.
|
|
1200
|
+
let gMag = runtime.gravity;
|
|
1201
|
+
if (runtime.velocityY <= 0) {
|
|
1202
|
+
gMag *= cfg.jump.fallGravityMult;
|
|
1203
|
+
state.isAscending = false;
|
|
1204
|
+
} else if (state.isVariableJumpCut) {
|
|
1205
|
+
gMag *= cfg.jump.cutGravityMult;
|
|
1206
|
+
}
|
|
1207
|
+
runtime.velocityY -= gMag * dt;
|
|
1208
|
+
|
|
1209
|
+
// Horizontal sweep — `_moveAndSlide` casts the player's capsule
|
|
1210
|
+
// along (vx, 0, vz) * dt and stops at first contact, so the
|
|
1211
|
+
// player can't tunnel into walls. Vertical is integrated as a
|
|
1212
|
+
// direct add below; the ground resolver handles floor contact
|
|
1213
|
+
// and the move-and-slide is intentionally NOT 3D to avoid the
|
|
1214
|
+
// SKIN-clearance-vs-floor-snap conflict (a small SKIN backoff
|
|
1215
|
+
// would land the player a few mm above the floor, which the
|
|
1216
|
+
// resolver would then re-flag as airborne).
|
|
1217
|
+
this._moveAndSlide(
|
|
1218
|
+
runtime, bodyTransform,
|
|
1219
|
+
runtime.velocityX * dt, 0, runtime.velocityZ * dt,
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
// Vertical integration — direct add; ground resolution below
|
|
1223
|
+
// does the snap on contact.
|
|
1224
|
+
bodyTransform.position._add(0, runtime.velocityY * dt, 0);
|
|
1225
|
+
|
|
1226
|
+
// Ground resolution.
|
|
1227
|
+
// Effective ground = max(built-in flat ground, optional resolver).
|
|
1228
|
+
// - useBuiltInFlatGround=true gives a baseline floor at groundY.
|
|
1229
|
+
// - groundResolver lets the host scene raise the floor under
|
|
1230
|
+
// platforms / terrain. Returns the surface Y under the player,
|
|
1231
|
+
// or null when no ground is below (gap / void).
|
|
1232
|
+
// If both are off, the original "external physics" branch
|
|
1233
|
+
// (else-block below) just tracks timers and leaves grounded
|
|
1234
|
+
// alone — the host's physics layer is expected to set it.
|
|
1235
|
+
if (this.useBuiltInFlatGround || this.groundResolver !== null) {
|
|
1236
|
+
let testY = this.useBuiltInFlatGround ? this.groundY : Number.NEGATIVE_INFINITY;
|
|
1237
|
+
if (this.groundResolver !== null) {
|
|
1238
|
+
const resolved = this.groundResolver(
|
|
1239
|
+
bodyTransform.position.x,
|
|
1240
|
+
bodyTransform.position.y,
|
|
1241
|
+
bodyTransform.position.z,
|
|
1242
|
+
);
|
|
1243
|
+
if (resolved !== null && resolved > testY) testY = resolved;
|
|
1244
|
+
}
|
|
1245
|
+
const haveGround = testY !== Number.NEGATIVE_INFINITY;
|
|
1246
|
+
if (haveGround && bodyTransform.position.y <= testY) {
|
|
1247
|
+
bodyTransform.position.setY(testY);
|
|
1248
|
+
|
|
1249
|
+
if (!state.grounded) {
|
|
1250
|
+
// Land — apply all state changes first, then fire the
|
|
1251
|
+
// signal LAST so handlers see the fully-reacted state.
|
|
1252
|
+
const impactVy = -runtime.velocityY;
|
|
1253
|
+
const kind = impactVy >= cfg.landing.hardThreshold ? "hard"
|
|
1254
|
+
: (impactVy >= cfg.landing.softThreshold ? "soft" : "soft");
|
|
1255
|
+
|
|
1256
|
+
const massScaledDip = impactVy * cfg.landing.recovery.dipPerVy
|
|
1257
|
+
* runtime.massRatios.landingDipScale;
|
|
1258
|
+
const dip = clamp(massScaledDip, 0, cfg.landing.recovery.dipMax);
|
|
1259
|
+
runtime.landSpring.settle(-dip);
|
|
1260
|
+
|
|
1261
|
+
const landImpulse = clamp(
|
|
1262
|
+
impactVy * cfg.exertion.landImpulsePerVy * runtime.massRatios.exertionRiseScale,
|
|
1263
|
+
0,
|
|
1264
|
+
cfg.exertion.landImpulseMax,
|
|
1265
|
+
);
|
|
1266
|
+
state.exertion = clamp(state.exertion + landImpulse, 0, 1);
|
|
1267
|
+
|
|
1268
|
+
runtime.midJump = false;
|
|
1269
|
+
state.isAscending = false;
|
|
1270
|
+
state.isVariableJumpCut = false;
|
|
1271
|
+
state.fallDistance = 0;
|
|
1272
|
+
|
|
1273
|
+
sig.onLand.send1({ verticalSpeed: impactVy, kind });
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
state.grounded = true;
|
|
1277
|
+
state.verticalSpeed = 0;
|
|
1278
|
+
runtime.velocityY = 0;
|
|
1279
|
+
state.airborneTime = 0;
|
|
1280
|
+
state.timeSinceGrounded = 0;
|
|
1281
|
+
} else {
|
|
1282
|
+
if (state.grounded) {
|
|
1283
|
+
sig.onLeaveGround.send1({ reason: runtime.midJump ? "jump" : "fall" });
|
|
1284
|
+
runtime.takeoffVy = runtime.velocityY;
|
|
1285
|
+
runtime.peakAltitude = bodyTransform.position.y;
|
|
1286
|
+
}
|
|
1287
|
+
state.grounded = false;
|
|
1288
|
+
state.verticalSpeed = runtime.velocityY;
|
|
1289
|
+
state.airborneTime += dt;
|
|
1290
|
+
state.timeSinceGrounded += dt;
|
|
1291
|
+
state.fallDistance += Math.max(0, -runtime.velocityY * dt);
|
|
1292
|
+
}
|
|
1293
|
+
} else {
|
|
1294
|
+
// External physics maintains state.grounded; just track timers.
|
|
1295
|
+
if (state.grounded) {
|
|
1296
|
+
state.timeSinceGrounded = 0;
|
|
1297
|
+
state.airborneTime = 0;
|
|
1298
|
+
} else {
|
|
1299
|
+
state.timeSinceGrounded += dt;
|
|
1300
|
+
state.airborneTime += dt;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Jump apex detection.
|
|
1305
|
+
if (runtime.midJump && !runtime.apexFired) {
|
|
1306
|
+
if (bodyTransform.position.y > runtime.peakAltitude) {
|
|
1307
|
+
runtime.peakAltitude = bodyTransform.position.y;
|
|
1308
|
+
} else if (runtime.velocityY <= 0) {
|
|
1309
|
+
sig.onJumpApex.send0();
|
|
1310
|
+
runtime.apexFired = true;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Run the base (no-ability) L1 locomotion phases: speed selection,
|
|
1317
|
+
* desired-velocity computation, accel/decel, jump FSM, gravity, body
|
|
1318
|
+
* integration, ground resolution. Only invoked when no ability owns
|
|
1319
|
+
* the tick (see {@link AbilitySet.tick}).
|
|
1320
|
+
*
|
|
1321
|
+
* @private
|
|
1322
|
+
* @param {FirstPersonPlayerController} controller
|
|
1323
|
+
* @param {PerEntityRuntime} runtime
|
|
1324
|
+
* @param {Transform} bodyTransform
|
|
1325
|
+
* @param {number} dt
|
|
1326
|
+
* @param {boolean} isCrouchActive
|
|
1327
|
+
* @param {boolean} isSprintIntent
|
|
1328
|
+
* @param {boolean} isBackwardIntent
|
|
1329
|
+
*/
|
|
1330
|
+
_runBaseLocomotion(controller, runtime, bodyTransform, dt,
|
|
1331
|
+
isCrouchActive, isSprintIntent, isBackwardIntent) {
|
|
1332
|
+
const cfg = controller.config;
|
|
1333
|
+
const intent = controller.intent;
|
|
1334
|
+
const state = controller.state;
|
|
1335
|
+
|
|
1336
|
+
// -- L1.b: Speed selection ------------------------------------
|
|
1337
|
+
let targetSpeed;
|
|
1338
|
+
if (isCrouchActive) {
|
|
1339
|
+
targetSpeed = cfg.motion.crouchSpeed;
|
|
1340
|
+
} else if (isSprintIntent) {
|
|
1341
|
+
targetSpeed = cfg.motion.sprintSpeed;
|
|
1342
|
+
} else {
|
|
1343
|
+
targetSpeed = cfg.motion.walkSpeed;
|
|
1344
|
+
}
|
|
1345
|
+
if (isBackwardIntent) {
|
|
1346
|
+
targetSpeed *= cfg.motion.backwardSpeedFactor;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Airborne momentum floor — preserve whatever horizontal speed
|
|
1350
|
+
// the player carried into the jump. Without this, a sprint
|
|
1351
|
+
// jump (9 m/s) decays toward walkSpeed (4.5 m/s) at
|
|
1352
|
+
// airAccel = 14 m/s², losing all sprint momentum in ~0.32 s —
|
|
1353
|
+
// well before the apex of a `peakHeight = 1.8 m` jump arc. The
|
|
1354
|
+
// air-control band (Mirror's Edge, Titanfall, modern CoD) and
|
|
1355
|
+
// the long-jump biomechanics literature both say the same
|
|
1356
|
+
// thing: there's no thrust source in flight, so horizontal
|
|
1357
|
+
// velocity is conserved across the arc and air "control" is
|
|
1358
|
+
// for steering (direction) — not for changing speed magnitude.
|
|
1359
|
+
// Raising the target to the current speed makes `stepTowards`
|
|
1360
|
+
// a no-op when the player keeps pressing forward, while
|
|
1361
|
+
// releasing the stick still lets `airAccel` decelerate to
|
|
1362
|
+
// `walkSpeed` (the user CAN bleed off speed, just not have it
|
|
1363
|
+
// bled off for them).
|
|
1364
|
+
if (!state.grounded) {
|
|
1365
|
+
const horizSpeed = Math.hypot(runtime.velocityX, runtime.velocityZ);
|
|
1366
|
+
if (horizSpeed > targetSpeed) targetSpeed = horizSpeed;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// -- L1.c: Move intent → desired horizontal velocity ----------
|
|
1370
|
+
// screen_forward(θ) = ( sin θ, 0, cos θ )
|
|
1371
|
+
// screen_right (θ) = (-cos θ, 0, sin θ )
|
|
1372
|
+
const { sinYaw, cosYaw } = runtime;
|
|
1373
|
+
const mvX = intent.move.x;
|
|
1374
|
+
const mvY = intent.move.y;
|
|
1375
|
+
const mvMag = Math.hypot(mvX, mvY);
|
|
1376
|
+
const nmvX = mvMag > 1 ? mvX / mvMag : mvX;
|
|
1377
|
+
const nmvY = mvMag > 1 ? mvY / mvMag : mvY;
|
|
1378
|
+
const desiredVx = sinYaw * nmvY + -cosYaw * nmvX;
|
|
1379
|
+
const desiredVz = cosYaw * nmvY + sinYaw * nmvX;
|
|
1380
|
+
const desiredHorizontalVx = desiredVx * targetSpeed;
|
|
1381
|
+
const desiredHorizontalVz = desiredVz * targetSpeed;
|
|
1382
|
+
|
|
1383
|
+
// -- L1.d: Accel/decel toward desired velocity ----------------
|
|
1384
|
+
//
|
|
1385
|
+
// Three regimes — air control, grounded decel-to-stop, grounded
|
|
1386
|
+
// accel-to-target — each with its own model:
|
|
1387
|
+
//
|
|
1388
|
+
// • Air control: constant-rate `stepTowards`. No ground
|
|
1389
|
+
// reaction force in flight; air control is a steering
|
|
1390
|
+
// budget, not a thrust curve. Constant accel matches the
|
|
1391
|
+
// player mental model of "fixed mid-air authority".
|
|
1392
|
+
//
|
|
1393
|
+
// • Grounded decel (no intent): constant-rate `stepTowards`
|
|
1394
|
+
// toward zero. Friction is approximately constant for a
|
|
1395
|
+
// biped on level ground — Coulomb friction. Faster than
|
|
1396
|
+
// accel because the body's own resistance + active
|
|
1397
|
+
// decel-foot-plants combine into a sharper deceleration.
|
|
1398
|
+
//
|
|
1399
|
+
// • Grounded accel (intent active): mono-exponential
|
|
1400
|
+
// approach (Hill 1927; Furusawa-Hill 1928). dv/dt is
|
|
1401
|
+
// proportional to (v_target − v), so accel is highest at
|
|
1402
|
+
// low speed and tapers as v approaches v_target. Matches
|
|
1403
|
+
// human sprint biomechanics — modern sprint-profiling
|
|
1404
|
+
// work (Morin & Samozino 2016) fits this same mono-exp
|
|
1405
|
+
// curve to empirical force-plate data.
|
|
1406
|
+
//
|
|
1407
|
+
// The mass + mastery + backward scalars compose multiplicatively
|
|
1408
|
+
// on the EFFECTIVE half-life (heavier ⇒ longer half-life ⇒
|
|
1409
|
+
// slower ramp; mastery accel-bonus ⇒ shorter half-life ⇒
|
|
1410
|
+
// faster ramp). See FirstPersonPlayerControllerConfig.js's
|
|
1411
|
+
// `groundAccelHalfLife` doc for the literature and the
|
|
1412
|
+
// SprintAcceleration.spec.js for the model assertions.
|
|
1413
|
+
const intentLen = Math.hypot(nmvX, nmvY);
|
|
1414
|
+
if (!state.grounded) {
|
|
1415
|
+
const maxStep = cfg.motion.airAccel * dt;
|
|
1416
|
+
runtime.velocityX = stepTowards(runtime.velocityX, desiredHorizontalVx, maxStep);
|
|
1417
|
+
runtime.velocityZ = stepTowards(runtime.velocityZ, desiredHorizontalVz, maxStep);
|
|
1418
|
+
} else if (intentLen < 1e-4) {
|
|
1419
|
+
let decel = cfg.motion.groundDecel * runtime.massRatios.groundAccelScale;
|
|
1420
|
+
decel *= controller.mastery.evaluate(DecisionPoint.GroundAccel, controller, runtime);
|
|
1421
|
+
const maxStep = decel * dt;
|
|
1422
|
+
runtime.velocityX = stepTowards(runtime.velocityX, 0, maxStep);
|
|
1423
|
+
runtime.velocityZ = stepTowards(runtime.velocityZ, 0, maxStep);
|
|
1424
|
+
} else {
|
|
1425
|
+
// Mono-exponential approach. Scale half-life by the
|
|
1426
|
+
// inverse of the accel scalars so that "more accel" (large
|
|
1427
|
+
// groundAccelScale, mastery > 1.0) translates to a shorter
|
|
1428
|
+
// half-life (faster ramp). Backward intent slows things
|
|
1429
|
+
// down — backwardAccelFactor < 1 ⇒ longer half-life.
|
|
1430
|
+
let halfLife = cfg.motion.groundAccelHalfLife
|
|
1431
|
+
/ runtime.massRatios.groundAccelScale
|
|
1432
|
+
/ controller.mastery.evaluate(DecisionPoint.GroundAccel, controller, runtime);
|
|
1433
|
+
if (isBackwardIntent) halfLife /= cfg.motion.backwardAccelFactor;
|
|
1434
|
+
runtime.velocityX = exponentialApproach(runtime.velocityX, desiredHorizontalVx, halfLife, dt);
|
|
1435
|
+
runtime.velocityZ = exponentialApproach(runtime.velocityZ, desiredHorizontalVz, halfLife, dt);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// -- L1.e/f/g/h: jump FSM + vertical integration --------------
|
|
1439
|
+
this._advanceJumpFsm(controller, runtime, bodyTransform, dt);
|
|
1440
|
+
this._integrateVerticalAndResolveGround(controller, runtime, bodyTransform, dt);
|
|
1441
|
+
|
|
1442
|
+
// -- Publish posture for L2 consumers (eye height, gait gating).
|
|
1443
|
+
// Base owns posture when no ability is active: Crouch if the
|
|
1444
|
+
// crouch intent is resolved active, otherwise Stand. Abilities
|
|
1445
|
+
// that need a different posture (slide → Prone, ledge-grab →
|
|
1446
|
+
// Hang) set state.posture themselves in their tick.
|
|
1447
|
+
controller.state.posture = isCrouchActive
|
|
1448
|
+
? FirstPersonPosture.Crouch
|
|
1449
|
+
: FirstPersonPosture.Stand;
|
|
1450
|
+
|
|
1451
|
+
// -- Publish lean target for L2.f. Base writes the natural
|
|
1452
|
+
// (lat-accel + look-lean) value; abilities override in their
|
|
1453
|
+
// own tick. L2.f spring-steps toward whatever's here.
|
|
1454
|
+
runtime.leanTargetRad = this._computeNaturalLeanTarget(controller, runtime, dt);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Compute the natural camera lean for this tick: lat-accel-driven
|
|
1459
|
+
* roll into a turn, plus a yaw-rate look-lean contribution, both
|
|
1460
|
+
* clamped. The result is the target the lean spring chases each
|
|
1461
|
+
* tick when no ability has opinions.
|
|
1462
|
+
*
|
|
1463
|
+
* Pure-ish helper — reads `controller`, `runtime`, `dt`; returns a
|
|
1464
|
+
* number. Extracted so both base and any future ability that wants
|
|
1465
|
+
* to compose its lean on top of the natural value can call it.
|
|
1466
|
+
*
|
|
1467
|
+
* @private
|
|
1468
|
+
* @param {FirstPersonPlayerController} controller
|
|
1469
|
+
* @param {PerEntityRuntime} runtime
|
|
1470
|
+
* @param {number} dt
|
|
1471
|
+
* @returns {number} target roll in radians
|
|
1472
|
+
*/
|
|
1473
|
+
_computeNaturalLeanTarget(controller, runtime, dt) {
|
|
1474
|
+
const cfg = controller.config;
|
|
1475
|
+
const state = controller.state;
|
|
1476
|
+
if (!cfg.lean.enabled) return 0;
|
|
1477
|
+
|
|
1478
|
+
const sinYaw = runtime.sinYaw;
|
|
1479
|
+
const cosYaw = runtime.cosYaw;
|
|
1480
|
+
|
|
1481
|
+
// Lateral acceleration projected onto screen-right.
|
|
1482
|
+
// accel_world = (vel - prevVel) / dt; screen_right = (-cos θ, 0, sin θ).
|
|
1483
|
+
const accWorldX = (runtime.velocityX - runtime.prevVelocityX) / Math.max(dt, 1e-4);
|
|
1484
|
+
const accWorldZ = (runtime.velocityZ - runtime.prevVelocityZ) / Math.max(dt, 1e-4);
|
|
1485
|
+
const latAccel = accWorldX * (-cosYaw) + accWorldZ * sinYaw;
|
|
1486
|
+
const normalized = clamp(latAccel / 9.81, -2, 2);
|
|
1487
|
+
//
|
|
1488
|
+
// Sign convention for the roll (the eye composes the rotation
|
|
1489
|
+
// as qYaw * qPitch * qRoll, where qRoll is around (0,0,1)).
|
|
1490
|
+
// After the engine's camera-invert pipeline:
|
|
1491
|
+
// φ > 0 → camera-up tilts toward screen-right (−X) → HEAD TILTS RIGHT
|
|
1492
|
+
// φ < 0 → camera-up tilts toward screen-left (+X) → HEAD TILTS LEFT
|
|
1493
|
+
//
|
|
1494
|
+
// For the "bank into the turn" feel (Apex / Titanfall / Mirror's
|
|
1495
|
+
// Edge): accelerating right (latAccel > 0) should tilt the head
|
|
1496
|
+
// RIGHT, i.e. positive φ. So leanTargetRad has the SAME sign
|
|
1497
|
+
// as latAccel.
|
|
1498
|
+
let leanTargetRad = normalized * cfg.lean.maxRollDeg * DEG_TO_RAD;
|
|
1499
|
+
|
|
1500
|
+
// Look-lean: yaw-rate-driven banking. runtime.yawRateRadPerSec
|
|
1501
|
+
// was cached at L1.a — negative is the "turn right" convention.
|
|
1502
|
+
// For "bank into the turn": turning right → head tilts right →
|
|
1503
|
+
// positive engine roll. So lookLean = -yawRate * scale matches
|
|
1504
|
+
// sign.
|
|
1505
|
+
//
|
|
1506
|
+
// Crouched players are in a low, stable, low-momentum stance —
|
|
1507
|
+
// banking the head from a mouse turn reads as unmotivated. We
|
|
1508
|
+
// scale the contribution down (default to 0) while crouched.
|
|
1509
|
+
// Lat-accel lean is left alone: its magnitude naturally tracks
|
|
1510
|
+
// the (lower) crouch acceleration, so it stays motivated.
|
|
1511
|
+
if (cfg.lean.lookLeanEnabled) {
|
|
1512
|
+
const yawRate = clamp(
|
|
1513
|
+
runtime.yawRateRadPerSec,
|
|
1514
|
+
-cfg.lean.lookLeanYawRateClamp,
|
|
1515
|
+
cfg.lean.lookLeanYawRateClamp,
|
|
1516
|
+
);
|
|
1517
|
+
const crouchFactor = state.crouchActive ? cfg.lean.crouchLookLeanFactor : 1.0;
|
|
1518
|
+
leanTargetRad += -yawRate * cfg.lean.lookLeanDegPerRadPerSec * DEG_TO_RAD * crouchFactor;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Final clamp on the sum: cap the combined target to ±2 ×
|
|
1522
|
+
// maxRollDeg (matches the latAccel normalized clamp range) so
|
|
1523
|
+
// even simultaneous max-strafe-accel + max-yaw-flick produces a
|
|
1524
|
+
// sane upper bound.
|
|
1525
|
+
const maxTotal = cfg.lean.maxRollDeg * DEG_TO_RAD * 2;
|
|
1526
|
+
return clamp(leanTargetRad, -maxTotal, maxTotal);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Snapshot the per-tick "what is the body doing" information into the
|
|
1531
|
+
* pose channels for downstream consumption (skeleton, sound, AI).
|
|
1532
|
+
* Read-only with respect to controller state — this is purely a publish
|
|
1533
|
+
* step.
|
|
1534
|
+
*
|
|
1535
|
+
* @private
|
|
1536
|
+
* @param {FirstPersonPlayerController} controller
|
|
1537
|
+
* @param {PerEntityRuntime} runtime
|
|
1538
|
+
* @param {Transform} bodyTransform
|
|
1539
|
+
*/
|
|
1540
|
+
_publishPose(controller, runtime, bodyTransform) {
|
|
1541
|
+
const cfg = controller.config;
|
|
1542
|
+
const state = controller.state;
|
|
1543
|
+
const pose = controller.pose;
|
|
1544
|
+
|
|
1545
|
+
pose.rootPosition.copy(bodyTransform.position);
|
|
1546
|
+
pose.rootYawRad = runtime.bodyYaw;
|
|
1547
|
+
pose.headYawRad = runtime.bodyYaw;
|
|
1548
|
+
pose.headPitchRad = runtime.eyePitch;
|
|
1549
|
+
pose.headRollRad = state.leanRollRad;
|
|
1550
|
+
pose.locomotionPhase = state.stridePhase;
|
|
1551
|
+
pose.locomotionSpeed = runtime.horizSpeed;
|
|
1552
|
+
// Strafe component: project velocity onto screen-right (-cos θ, 0, sin θ).
|
|
1553
|
+
// Positive = moving to the player's right.
|
|
1554
|
+
pose.locomotionStrafe = (runtime.velocityX * (-runtime.cosYaw) + runtime.velocityZ * runtime.sinYaw)
|
|
1555
|
+
/ Math.max(cfg.motion.sprintSpeed, 1e-3);
|
|
1556
|
+
pose.actionState =
|
|
1557
|
+
state.inJumpAnticipation ? FirstPersonActionState.Anticipating
|
|
1558
|
+
: !state.grounded ? FirstPersonActionState.Airborne
|
|
1559
|
+
: (Math.abs(runtime.landSpring.value) > 0.01 ? FirstPersonActionState.Landing
|
|
1560
|
+
: FirstPersonActionState.Grounded);
|
|
1561
|
+
pose.locomotionMode = state.locomotionMode;
|
|
1562
|
+
const crouchSpan = Math.max(cfg.body.height - cfg.body.crouchHeight, 1e-3);
|
|
1563
|
+
pose.crouchAmount = clamp((cfg.body.height - state.eyeHeight) / crouchSpan, 0, 1);
|
|
1564
|
+
|
|
1565
|
+
// Posture channel for downstream animation: which body shape +
|
|
1566
|
+
// how far the body is into it from the standing neutral.
|
|
1567
|
+
//
|
|
1568
|
+
// `posture` is the enum (Stand / Crouch / Prone / Hang) — picks
|
|
1569
|
+
// the animation track. `postureAmount` is the [0..1] blend
|
|
1570
|
+
// weight from standing toward that posture, derived from the
|
|
1571
|
+
// eye-height spring so the value transitions smoothly across
|
|
1572
|
+
// changes (matches the visible camera motion).
|
|
1573
|
+
pose.posture = state.posture;
|
|
1574
|
+
let postureTargetH;
|
|
1575
|
+
switch (state.posture) {
|
|
1576
|
+
case FirstPersonPosture.Prone: postureTargetH = cfg.body.proneHeight; break;
|
|
1577
|
+
case FirstPersonPosture.Crouch: postureTargetH = cfg.body.crouchHeight; break;
|
|
1578
|
+
case FirstPersonPosture.Hang: postureTargetH = cfg.body.height; break;
|
|
1579
|
+
case FirstPersonPosture.Stand:
|
|
1580
|
+
default: postureTargetH = cfg.body.height; break;
|
|
1581
|
+
}
|
|
1582
|
+
const postureSpan = Math.max(cfg.body.height - postureTargetH, 1e-3);
|
|
1583
|
+
pose.postureAmount = clamp((cfg.body.height - state.eyeHeight) / postureSpan, 0, 1);
|
|
1584
|
+
|
|
1585
|
+
pose.aimPitch = runtime.eyePitch;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Compose the eye transform from body + state-driven offsets.
|
|
1590
|
+
* @private
|
|
1591
|
+
* @param {FirstPersonPlayerController} controller
|
|
1592
|
+
* @param {number} entity
|
|
1593
|
+
*/
|
|
1594
|
+
_composeEye(controller, entity) {
|
|
1595
|
+
const ecd = this.entityManager.dataset;
|
|
1596
|
+
const runtime = this.runtime.get(entity);
|
|
1597
|
+
if (runtime === undefined) return;
|
|
1598
|
+
|
|
1599
|
+
const dt = this._currentRenderDt;
|
|
1600
|
+
const cfg = controller.config;
|
|
1601
|
+
const state = controller.state;
|
|
1602
|
+
|
|
1603
|
+
const bodyTransform = ecd.getComponent(entity, Transform);
|
|
1604
|
+
if (bodyTransform === undefined) return;
|
|
1605
|
+
|
|
1606
|
+
if (controller.eyeEntity === -1) return;
|
|
1607
|
+
const eyeTransform = ecd.getComponent(controller.eyeEntity, Transform);
|
|
1608
|
+
const camera = ecd.getComponent(controller.eyeEntity, Camera);
|
|
1609
|
+
if (eyeTransform === undefined || camera === undefined) return;
|
|
1610
|
+
|
|
1611
|
+
// -- Body-local eye offset, composed via the additive stack ----
|
|
1612
|
+
// The base (0, eyeHeight, 0) is the standing/crouched neutral; each
|
|
1613
|
+
// additional contribution (bob, breath, landing, anticipation,
|
|
1614
|
+
// sprint posture) goes through the stack so external systems can
|
|
1615
|
+
// push their own contributions on the same channel.
|
|
1616
|
+
const stack = runtime.eyeOffsetStack;
|
|
1617
|
+
stack.clear();
|
|
1618
|
+
stack.push("eyeHeight", 0, state.eyeHeight, 0);
|
|
1619
|
+
|
|
1620
|
+
// Bob — gated on grounded only (the impact spring decays naturally
|
|
1621
|
+
// even at rest, so the bob fade-out is smooth; lateral amp uses the
|
|
1622
|
+
// bob-intensity envelope which spring-decays after stopping).
|
|
1623
|
+
if (state.grounded) {
|
|
1624
|
+
const phase = state.stridePhase * TWO_PI;
|
|
1625
|
+
const massBoost = (cfg.body.mass - 80) * cfg.bob.ampMassScale;
|
|
1626
|
+
const intensity = runtime.bobIntensitySpring.value;
|
|
1627
|
+
|
|
1628
|
+
// Back-pedal amp boost — lateral grows more than vertical because
|
|
1629
|
+
// backward gait has worse side-to-side balance than vertical compression.
|
|
1630
|
+
// Exertion adds a smaller boost on top: tired = wobbly gait.
|
|
1631
|
+
const ampLMult = 1 + (cfg.bob.backwardLateralAmpFactor - 1) * runtime.backwardness;
|
|
1632
|
+
const exertionBoost = 1 + cfg.exertion.bobLateralBoostAtMax * state.exertion;
|
|
1633
|
+
const ampL = (cfg.bob.lateralAmpAtWalk + massBoost) * intensity * ampLMult * exertionBoost;
|
|
1634
|
+
|
|
1635
|
+
// Vertical: read directly from the impact spring (footfall kicks,
|
|
1636
|
+
// under-damped recovery → trough + leg-push overshoot).
|
|
1637
|
+
stack.push("bob.impact", 0, runtime.verticalImpactSpring.value, 0);
|
|
1638
|
+
|
|
1639
|
+
// Lateral: head shifts toward the foot bearing weight. Polarity
|
|
1640
|
+
// sourced from runtime.standingFoot — the same signal the
|
|
1641
|
+
// footstep emits — so bob direction and footstep side agree.
|
|
1642
|
+
// |sin(phase)| is the non-negative "midstance envelope".
|
|
1643
|
+
const lateralPolarity = runtime.standingFoot === "R" ? -1 : 1;
|
|
1644
|
+
stack.push("bob.lateral", ampL * lateralPolarity * Math.abs(Math.sin(phase)), 0, 0);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Breath — sine + tiny noise riding the rate spring.
|
|
1648
|
+
const breathOffset = -state.breathAmplitudeM
|
|
1649
|
+
* Math.sin(state.breathPhase * TWO_PI)
|
|
1650
|
+
* (1 + cfg.breath.noiseAmount * (Math.sin(state.breathPhase * 13.7) * 0.5));
|
|
1651
|
+
stack.push("breath", 0, breathOffset, 0);
|
|
1652
|
+
|
|
1653
|
+
// Landing spring dip (under-damped — overshoots once on recovery).
|
|
1654
|
+
stack.push("landing", 0, runtime.landSpring.value, 0);
|
|
1655
|
+
|
|
1656
|
+
// Jump anticipation dip (eased ramp during the squash window).
|
|
1657
|
+
if (state.inJumpAnticipation) {
|
|
1658
|
+
const t = 1 - clamp(runtime.anticipationRemaining / Math.max(cfg.jump.anticipation.duration, 1e-3), 0, 1);
|
|
1659
|
+
const eased = t * (2 - t); // ease-out quad
|
|
1660
|
+
stack.push("anticipation", 0, -cfg.jump.anticipation.dipAmount * eased, 0);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Sprint posture: head leans slightly forward as commitment builds.
|
|
1664
|
+
// Pitch part is in the rotation block below; the +Z position shift
|
|
1665
|
+
// sells "head leading the hips" (Mirror's Edge), tied to the same
|
|
1666
|
+
// spring envelope so they move together.
|
|
1667
|
+
const sprintPitch = runtime.sprintPostureSpring.value;
|
|
1668
|
+
const sprintShiftFraction =
|
|
1669
|
+
cfg.posture.sprintForwardPitchDeg > 0
|
|
1670
|
+
? sprintPitch / (cfg.posture.sprintForwardPitchDeg * DEG_TO_RAD)
|
|
1671
|
+
: 0;
|
|
1672
|
+
stack.push("posture.sprintShift", 0, 0, cfg.posture.sprintForwardShiftM * sprintShiftFraction);
|
|
1673
|
+
|
|
1674
|
+
// Transform body-local accumulated offset into world space.
|
|
1675
|
+
const worldOffset = SCRATCH_V3_B.copy(stack.offset);
|
|
1676
|
+
worldOffset.applyQuaternion(bodyTransform.rotation);
|
|
1677
|
+
|
|
1678
|
+
eyeTransform.position.copy(bodyTransform.position);
|
|
1679
|
+
eyeTransform.position._add(worldOffset.x, worldOffset.y, worldOffset.z);
|
|
1680
|
+
|
|
1681
|
+
// -- Eye rotation: body yaw × eye pitch × roll -------------------
|
|
1682
|
+
// Bob roll mixes in for a subtle head sway (in phase with lateral bob).
|
|
1683
|
+
// Breath pitch is a small extra nod 90° out of phase with vertical
|
|
1684
|
+
// breath; merged into the main pitch so we don't pay an extra quat
|
|
1685
|
+
// multiply and the composition stays trivially correct.
|
|
1686
|
+
let rollTotal = state.leanRollRad;
|
|
1687
|
+
if (state.grounded) {
|
|
1688
|
+
// Roll: head tilts toward the standing foot, in phase with the
|
|
1689
|
+
// lateral sway. Polarity sourced from runtime.standingFoot for
|
|
1690
|
+
// consistency with the lateral bob. Positive engine roll = head
|
|
1691
|
+
// tilts RIGHT (camera-invert convention), so R-foot midstance =
|
|
1692
|
+
// positive roll, L-foot midstance = negative roll.
|
|
1693
|
+
const phase = state.stridePhase * TWO_PI;
|
|
1694
|
+
const rollBackMult = 1 + (cfg.bob.backwardRollFactor - 1) * runtime.backwardness;
|
|
1695
|
+
const ampRoll = cfg.bob.rollAtWalkDeg * DEG_TO_RAD * runtime.bobIntensitySpring.value * rollBackMult;
|
|
1696
|
+
const rollPolarity = runtime.standingFoot === "R" ? 1 : -1;
|
|
1697
|
+
const rollEnvelope = Math.abs(Math.sin(phase));
|
|
1698
|
+
const bobRollSigned = ampRoll * rollPolarity * rollEnvelope;
|
|
1699
|
+
|
|
1700
|
+
// Lean × bob coupling: excursions in the lean direction get
|
|
1701
|
+
// amplified, opposite excursions attenuated. Lean is normalized
|
|
1702
|
+
// against maxRollDeg so the coupling magnitude stays bounded
|
|
1703
|
+
// regardless of how aggressively lean is configured.
|
|
1704
|
+
const maxLeanRad = Math.max(cfg.lean.maxRollDeg * DEG_TO_RAD, 1e-6);
|
|
1705
|
+
const leanFraction = clamp(state.leanRollRad / maxLeanRad, -1, 1);
|
|
1706
|
+
// sign(bobRollSigned) matches lean? amplify; else attenuate.
|
|
1707
|
+
const sameSign = (bobRollSigned * leanFraction) >= 0;
|
|
1708
|
+
const couplingMag = cfg.bob.leanCouplingFactor * Math.abs(leanFraction);
|
|
1709
|
+
const couplingScale = sameSign ? (1 + couplingMag) : (1 - couplingMag);
|
|
1710
|
+
rollTotal += bobRollSigned * couplingScale;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const breathPitch = lerp(cfg.breath.pitchAmpRestDeg, cfg.breath.pitchAmpMaxDeg, state.exertion)
|
|
1714
|
+
* DEG_TO_RAD
|
|
1715
|
+
* Math.cos(state.breathPhase * TWO_PI);
|
|
1716
|
+
// Combined pitch contributions: player input + breath nod + sprint
|
|
1717
|
+
// commitment + fatigue droop. All in the same "positive = look-down"
|
|
1718
|
+
// convention so they sum cleanly.
|
|
1719
|
+
const pitchTotal = runtime.eyePitch
|
|
1720
|
+
+ breathPitch
|
|
1721
|
+
+ runtime.sprintPostureSpring.value
|
|
1722
|
+
+ runtime.headDroopSpring.value;
|
|
1723
|
+
|
|
1724
|
+
// composition: yaw * pitch * roll
|
|
1725
|
+
// pitch around world X — yaw applied after, so effective axis is camera-local right
|
|
1726
|
+
// roll around world Z — yaw and pitch applied after, so effective axis is camera-local forward
|
|
1727
|
+
const qYaw = SCRATCH_Q_A.fromAxisAngle(Vector3.up, runtime.bodyYaw);
|
|
1728
|
+
const qPitch = SCRATCH_Q_B.fromAxisAngle(Vector3.right, pitchTotal);
|
|
1729
|
+
const qRoll = SCRATCH_Q_C.fromAxisAngle(Vector3.forward, rollTotal);
|
|
1730
|
+
|
|
1731
|
+
eyeTransform.rotation.multiplyQuaternions(qYaw, qPitch);
|
|
1732
|
+
eyeTransform.rotation.multiply(qRoll);
|
|
1733
|
+
|
|
1734
|
+
// -- FOV ---------------------------------------------------------
|
|
1735
|
+
let fovTarget = cfg.fov.base;
|
|
1736
|
+
if (cfg.fov.sprintAdd !== 0) {
|
|
1737
|
+
fovTarget += cfg.fov.sprintAdd * runtime.sprintness;
|
|
1738
|
+
}
|
|
1739
|
+
if (state.crouchActive) fovTarget += cfg.fov.crouchAdd;
|
|
1740
|
+
|
|
1741
|
+
runtime.fovSpring.stepTo(fovTarget, cfg.fov.smoothHalfLife, 1.0, dt);
|
|
1742
|
+
// Write directly to the underlying Three.js camera. Going through
|
|
1743
|
+
// camera.fov.set() fires onChanged which triggers a full camera
|
|
1744
|
+
// rebuild in CameraSystem — far too expensive to do per frame.
|
|
1745
|
+
// The CameraSystem's visibility-construction hook calls
|
|
1746
|
+
// updateProjectionMatrix() each frame anyway.
|
|
1747
|
+
if (camera.object !== null) {
|
|
1748
|
+
camera.object.fov = runtime.fovSpring.value;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// ---------------------------------------------------------------------------
|
|
1754
|
+
// helpers
|
|
1755
|
+
// ---------------------------------------------------------------------------
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Exponential approach with half-life parameterization.
|
|
1759
|
+
* @param {number} current
|
|
1760
|
+
* @param {number} target
|
|
1761
|
+
* @param {number} halfLife
|
|
1762
|
+
* @param {number} dt
|
|
1763
|
+
* @returns {number}
|
|
1764
|
+
*/
|
|
1765
|
+
function exponentialApproach(current, target, halfLife, dt) {
|
|
1766
|
+
if (halfLife <= 0) return target;
|
|
1767
|
+
const alpha = 1 - Math.exp(-LN2 * dt / halfLife);
|
|
1768
|
+
return current + (target - current) * alpha;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Detect that phase value crossed a boundary in [0,1) between two ticks.
|
|
1773
|
+
* Handles the wraparound case where phase jumps from e.g. 0.95 to 0.05.
|
|
1774
|
+
*
|
|
1775
|
+
* @param {number} prev previous phase in [0,1)
|
|
1776
|
+
* @param {number} next current phase in [0,1)
|
|
1777
|
+
* @param {number} boundary in [0,1)
|
|
1778
|
+
* @returns {boolean}
|
|
1779
|
+
*/
|
|
1780
|
+
function phaseCrossed(prev, next, boundary) {
|
|
1781
|
+
if (next >= prev) {
|
|
1782
|
+
// no wrap
|
|
1783
|
+
return prev < boundary && next >= boundary;
|
|
1784
|
+
} else {
|
|
1785
|
+
// wrapped past 1.0
|
|
1786
|
+
return prev < boundary || next >= boundary;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|