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