@woosh/meep-engine 2.145.0 → 2.146.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/HeightMapShape3D.d.ts +33 -3
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
- package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -352
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
- package/src/engine/control/first-person/TODO.md +13 -11
- package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallJump.js +11 -3
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +12 -0
- package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
- package/src/engine/control/first-person/collision/KinematicMover.js +634 -592
- package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
- package/src/engine/physics/PLAN.md +943 -809
- package/src/engine/physics/body/BodyStorage.d.ts +9 -0
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +23 -0
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +7 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
- package/src/engine/physics/ccd/linear_sweep.js +238 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +185 -183
- package/src/engine/simulation/Ticker.d.ts +14 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +136 -1
|
@@ -1,901 +1,1003 @@
|
|
|
1
|
-
import { BoxGeometry, MeshStandardMaterial, TorusKnotGeometry } from "three";
|
|
2
|
-
import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
|
|
3
|
-
import { BoxShape3D } from "../../../core/geom/3d/shape/BoxShape3D.js";
|
|
4
|
-
import { CapsuleShape3D } from "../../../core/geom/3d/shape/CapsuleShape3D.js";
|
|
5
|
-
import { TransformedShape3D } from "../../../core/geom/3d/shape/TransformedShape3D.js";
|
|
6
|
-
import { shape_mesh_from_geometry } from "../../../core/geom/3d/shape/shape_mesh_from_geometry.js";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* -
|
|
51
|
-
* -
|
|
52
|
-
* -
|
|
53
|
-
* -
|
|
54
|
-
* -
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* @
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
const
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
//
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
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
|
-
* and the
|
|
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
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
sg.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
collider
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.add(
|
|
305
|
-
.add(
|
|
306
|
-
.add(
|
|
307
|
-
.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* -
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*/
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*/
|
|
689
|
-
function
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
* @param {EntityComponentDataset} ecd
|
|
717
|
-
* @
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1
|
+
import { BoxGeometry, MeshStandardMaterial, TorusKnotGeometry } from "three";
|
|
2
|
+
import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
|
|
3
|
+
import { BoxShape3D } from "../../../core/geom/3d/shape/BoxShape3D.js";
|
|
4
|
+
import { CapsuleShape3D } from "../../../core/geom/3d/shape/CapsuleShape3D.js";
|
|
5
|
+
import { TransformedShape3D } from "../../../core/geom/3d/shape/TransformedShape3D.js";
|
|
6
|
+
import { shape_mesh_from_geometry } from "../../../core/geom/3d/shape/shape_mesh_from_geometry.js";
|
|
7
|
+
import Quaternion from "../../../core/geom/Quaternion.js";
|
|
8
|
+
import Vector2 from "../../../core/geom/Vector2.js";
|
|
9
|
+
import Vector3 from "../../../core/geom/Vector3.js";
|
|
10
|
+
import { SerializationMetadata } from "../../ecs/components/SerializationMetadata.js";
|
|
11
|
+
import { Tag } from "../../ecs/components/Tag.js";
|
|
12
|
+
import Entity from "../../ecs/Entity.js";
|
|
13
|
+
import { obtainTerrain } from "../../ecs/terrain/util/obtainTerrain.js";
|
|
14
|
+
import { Transform } from "../../ecs/transform/Transform.js";
|
|
15
|
+
import { EngineHarness } from "../../EngineHarness.js";
|
|
16
|
+
import { CameraSystem } from "../../graphics/ecs/camera/CameraSystem.js";
|
|
17
|
+
import { ShadedGeometry } from "../../graphics/ecs/mesh-v2/ShadedGeometry.js";
|
|
18
|
+
import { ShadedGeometryFlags } from "../../graphics/ecs/mesh-v2/ShadedGeometryFlags.js";
|
|
19
|
+
import { ShadedGeometrySystem } from "../../graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
|
|
20
|
+
import {
|
|
21
|
+
AmbientOcclusionPostProcessEffect
|
|
22
|
+
} from "../../graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js";
|
|
23
|
+
import InputController from "../../input/ecs/components/InputController.js";
|
|
24
|
+
import InputControllerSystem from "../../input/ecs/systems/InputControllerSystem.js";
|
|
25
|
+
import { BodyKind } from "../../physics/ecs/BodyKind.js";
|
|
26
|
+
import { Collider } from "../../physics/ecs/Collider.js";
|
|
27
|
+
import { ColliderObserverSystem } from "../../physics/ecs/ColliderObserverSystem.js";
|
|
28
|
+
import { PhysicsSystem } from "../../physics/ecs/PhysicsSystem.js";
|
|
29
|
+
import { RigidBody } from "../../physics/ecs/RigidBody.js";
|
|
30
|
+
import { PhysicsSurfacePoint } from "../../physics/queries/PhysicsSurfacePoint.js";
|
|
31
|
+
import { LedgeGrab } from "./abilities/LedgeGrab.js";
|
|
32
|
+
import { Mantle } from "./abilities/Mantle.js";
|
|
33
|
+
import { Slide } from "./abilities/Slide.js";
|
|
34
|
+
import { WallJump } from "./abilities/WallJump.js";
|
|
35
|
+
import { WallRun } from "./abilities/WallRun.js";
|
|
36
|
+
import { FirstPersonPlayerController } from "./FirstPersonPlayerController.js";
|
|
37
|
+
import { FirstPersonPlayerControllerSystem } from "./FirstPersonPlayerControllerSystem.js";
|
|
38
|
+
import { BreathRhythmEvaluator } from "./mastery/BreathRhythmEvaluator.js";
|
|
39
|
+
import { DecisionPoint } from "./mastery/DecisionPoint.js";
|
|
40
|
+
import { FootAsymmetryTurnEvaluator } from "./mastery/FootAsymmetryTurnEvaluator.js";
|
|
41
|
+
import { SlideInitiationTimingEvaluator } from "./mastery/SlideInitiationTimingEvaluator.js";
|
|
42
|
+
import { StrideTimingJumpEvaluator } from "./mastery/StrideTimingJumpEvaluator.js";
|
|
43
|
+
import { FirstPersonSensorsSystem } from "./sensors/FirstPersonSensorsSystem.js";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Prototype harness for {@link FirstPersonPlayerController} — a parkour
|
|
47
|
+
* "gym" laid out as discrete test stations around the spawn point.
|
|
48
|
+
*
|
|
49
|
+
* Controls:
|
|
50
|
+
* - Click to capture mouse (pointer lock); ESC to release
|
|
51
|
+
* - WASD — move
|
|
52
|
+
* - Mouse — look
|
|
53
|
+
* - Space — jump (tap; holding does not auto-repeat)
|
|
54
|
+
* - Shift — sprint (hold while moving forward)
|
|
55
|
+
* - C — crouch (hold; sprint+crouch press → slide)
|
|
56
|
+
*
|
|
57
|
+
* Stations (compass directions from spawn):
|
|
58
|
+
*
|
|
59
|
+
* N Mantle row four obstacles from too-low to too-tall
|
|
60
|
+
* NE Slide tunnel sprint run-up + low ceiling
|
|
61
|
+
* E Wall-run wall one long wall to run alongside
|
|
62
|
+
* SE Gap jump raised platforms with a gap to clear
|
|
63
|
+
* S Ledge-grab wall too tall to mantle; jump + catch the lip
|
|
64
|
+
* SW Stairs a climbable flight + a too-tall blocking riser
|
|
65
|
+
* W Wall-jump chimney two parallel walls for back-and-forth jumps
|
|
66
|
+
* NW Ramp row inclines from gentle (walkable) to too-steep (slide)
|
|
67
|
+
*
|
|
68
|
+
* Each station is colour-coded at the entrance. Look around: the
|
|
69
|
+
* coloured pad on the ground tells you what's there.
|
|
70
|
+
*
|
|
71
|
+
* @author Alex Goldring
|
|
72
|
+
* @copyright Company Named Limited (c) 2026
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
const SPAWN_X = 100;
|
|
76
|
+
const SPAWN_Z = 100;
|
|
77
|
+
|
|
78
|
+
// Station palette — used for entry pads + matching obstacle colour.
|
|
79
|
+
const COLOR_MANTLE = 0x55ff55;
|
|
80
|
+
const COLOR_WALLRUN = 0xff5555;
|
|
81
|
+
const COLOR_WALLJUMP = 0xaa55ff;
|
|
82
|
+
const COLOR_LEDGE = 0xffaa44;
|
|
83
|
+
const COLOR_SLIDE = 0x44ddee;
|
|
84
|
+
const COLOR_GAP = 0xff66cc;
|
|
85
|
+
const COLOR_STAIRS = 0xeedd44;
|
|
86
|
+
const COLOR_RAMP = 0x3377dd;
|
|
87
|
+
const COLOR_NEUTRAL = 0xaaaaaa;
|
|
88
|
+
|
|
89
|
+
const eh = new EngineHarness();
|
|
90
|
+
|
|
91
|
+
async function main(engine) {
|
|
92
|
+
const em = engine.entityManager;
|
|
93
|
+
const ecd = engine.entityManager.dataset;
|
|
94
|
+
|
|
95
|
+
// -- Systems ---------------------------------------------------------
|
|
96
|
+
EngineHarness.addFpsCounter(engine);
|
|
97
|
+
|
|
98
|
+
await EngineHarness.buildLights({
|
|
99
|
+
engine,
|
|
100
|
+
sunIntensity: 1.0,
|
|
101
|
+
ambientIntensity: 0.25,
|
|
102
|
+
shadowmapResolution: 2048,
|
|
103
|
+
sunShadowDistance: 30,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
engine.plugins.acquire(AmbientOcclusionPostProcessEffect);
|
|
107
|
+
|
|
108
|
+
if (em.getSystem(CameraSystem) === null) {
|
|
109
|
+
await em.addSystem(new CameraSystem(engine.graphics));
|
|
110
|
+
}
|
|
111
|
+
if (em.getSystem(InputControllerSystem) === null) {
|
|
112
|
+
await em.addSystem(new InputControllerSystem(engine.devices));
|
|
113
|
+
}
|
|
114
|
+
if (em.getSystem(ShadedGeometrySystem) === null) {
|
|
115
|
+
await em.addSystem(new ShadedGeometrySystem(engine));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Physics — owns RigidBody+Collider lifecycle, fires contact events,
|
|
119
|
+
// and serves spatial queries (raycast). Sensors and the ground
|
|
120
|
+
// resolver both go through it. Added BEFORE the controller so the
|
|
121
|
+
// sensors and ground resolver find it in their startup auto-acquire.
|
|
122
|
+
const physicsSystem = new PhysicsSystem();
|
|
123
|
+
await em.addSystem(physicsSystem);
|
|
124
|
+
// ColliderObserver wires Collider components into PhysicsSystem
|
|
125
|
+
// bodies as entities are built. Without it, every collider would
|
|
126
|
+
// need an explicit attach_collider call.
|
|
127
|
+
await em.addSystem(new ColliderObserverSystem(physicsSystem));
|
|
128
|
+
|
|
129
|
+
const fpsSystem = new FirstPersonPlayerControllerSystem();
|
|
130
|
+
// Built-in flat-ground OFF — the gym uses the physics-backed ground
|
|
131
|
+
// resolver below (which finds the highest static surface under the
|
|
132
|
+
// player from BVH raycasts). Leaving the flat baseline on would
|
|
133
|
+
// pin the player to y=0 even when standing on a 2 m platform.
|
|
134
|
+
fpsSystem.useBuiltInFlatGround = false;
|
|
135
|
+
// Collision is resolved by the KinematicMover (recover + unified 3D
|
|
136
|
+
// sweep-and-slide + ground categorize/stick/slope/stairs) whenever a
|
|
137
|
+
// PhysicsSystem is present — which it is here. The mover probes the
|
|
138
|
+
// physics world for ground itself, so no `groundResolver` is wired.
|
|
139
|
+
// See DESIGN_COLLISION.md.
|
|
140
|
+
await em.addSystem(fpsSystem);
|
|
141
|
+
|
|
142
|
+
// Sensors system — populates wall/obstacle/ledge probes via
|
|
143
|
+
// PhysicsSystem.raycast. Auto-acquires the physics system in startup.
|
|
144
|
+
const sensorsSystem = new FirstPersonSensorsSystem();
|
|
145
|
+
await em.addSystem(sensorsSystem);
|
|
146
|
+
|
|
147
|
+
if (!ecd.isComponentTypeRegistered(Tag)) {
|
|
148
|
+
ecd.registerComponentType(Tag);
|
|
149
|
+
}
|
|
150
|
+
if (!ecd.isComponentTypeRegistered(SerializationMetadata)) {
|
|
151
|
+
ecd.registerComponentType(SerializationMetadata);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -- World -----------------------------------------------------------
|
|
155
|
+
await EngineHarness.buildTerrain({
|
|
156
|
+
engine,
|
|
157
|
+
size: new Vector2(200, 200),
|
|
158
|
+
resolution: 8,
|
|
159
|
+
enableWater: false,
|
|
160
|
+
diffuse0: "data/textures/materials/terrain_township_set/512/Grass_3.png",
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
obtainTerrain(ecd); // no-op call to ensure terrain is built
|
|
164
|
+
|
|
165
|
+
buildGym(ecd);
|
|
166
|
+
|
|
167
|
+
// -- Player ----------------------------------------------------------
|
|
168
|
+
const player = buildPlayerEntity(ecd);
|
|
169
|
+
|
|
170
|
+
// Ground resolver: downward raycast through PhysicsSystem, filtered
|
|
171
|
+
// to exclude the player's own capsule. Wired AFTER the player is
|
|
172
|
+
// built so the filter has the right entity id. The controller's
|
|
173
|
+
// vertical integrator falls through when the resolver returns null
|
|
174
|
+
// (no platform below) — set up with `useBuiltInFlatGround = false`
|
|
175
|
+
// above so that means actual airborne / void, not a hidden y=0
|
|
176
|
+
// safety floor.
|
|
177
|
+
fpsSystem.groundResolver = makePhysicsGroundResolver(physicsSystem, player.entity);
|
|
178
|
+
|
|
179
|
+
// -- Inputs ----------------------------------------------------------
|
|
180
|
+
buildInputBindings(ecd, player);
|
|
181
|
+
|
|
182
|
+
setupPointerLock(engine);
|
|
183
|
+
|
|
184
|
+
logHelp();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Downward-raycast ground resolver, backed by {@link PhysicsSystem.raycast}.
|
|
189
|
+
* Each call: from just above the player's feet, cast straight down
|
|
190
|
+
* through static + dynamic broadphase; filter out the player's own
|
|
191
|
+
* body; return the hit y, or null if no surface below.
|
|
192
|
+
*
|
|
193
|
+
* The controller's vertical integrator picks max(useBuiltInFlatGround,
|
|
194
|
+
* resolver). In the gym we leave useBuiltInFlatGround off so the
|
|
195
|
+
* resolver is the sole source of truth — gaps in the floor are real
|
|
196
|
+
* gaps (the player falls into them).
|
|
197
|
+
*
|
|
198
|
+
* @param {PhysicsSystem} physicsSystem
|
|
199
|
+
* @param {number} playerEntity excluded from raycasts to skip the player capsule
|
|
200
|
+
* @returns {(x:number, y:number, z:number) => number|null}
|
|
201
|
+
*/
|
|
202
|
+
function makePhysicsGroundResolver(physicsSystem, playerEntity) {
|
|
203
|
+
// Probe starts slightly above the player feet so a grounded player
|
|
204
|
+
// standing right on a platform still raycasts cleanly. Length is
|
|
205
|
+
// longer than any reasonable gym platform.
|
|
206
|
+
const PROBE_LIFT = 0.1;
|
|
207
|
+
const PROBE_DOWN = 50;
|
|
208
|
+
const filter = (entity) => entity !== playerEntity;
|
|
209
|
+
// Reuse a single ray and surface-point across calls — the resolver
|
|
210
|
+
// can fire many times per frame as the controller hunts for foothold.
|
|
211
|
+
const ray = Ray3.from(0, 0, 0, 0, -1, 0, PROBE_DOWN);
|
|
212
|
+
const hit = new PhysicsSurfacePoint();
|
|
213
|
+
return function physicsGroundResolver(x, y, z) {
|
|
214
|
+
ray.setOrigin(x, y + PROBE_LIFT, z);
|
|
215
|
+
if (!physicsSystem.raycast(ray, hit, filter)) return null;
|
|
216
|
+
// Direction is unit (0,-1,0); t is the distance to the surface
|
|
217
|
+
// along that direction. Surface Y = origin Y minus t.
|
|
218
|
+
return (y + PROBE_LIFT) - hit.t;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =====================================================================
|
|
223
|
+
// Gym construction
|
|
224
|
+
// =====================================================================
|
|
225
|
+
|
|
226
|
+
function buildGym(ecd) {
|
|
227
|
+
buildGroundPlane(ecd);
|
|
228
|
+
buildSpawnPlaza(ecd);
|
|
229
|
+
buildMantleStation(ecd);
|
|
230
|
+
buildWallRunStation(ecd);
|
|
231
|
+
buildWallJumpChimney(ecd);
|
|
232
|
+
buildLedgeGrabStation(ecd);
|
|
233
|
+
buildSlideTunnel(ecd);
|
|
234
|
+
buildGapJumpStation(ecd);
|
|
235
|
+
buildStairsStation(ecd);
|
|
236
|
+
buildRampStation(ecd);
|
|
237
|
+
buildMeshShapeShowcase(ecd);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Drop a torus-knot prop off to the side as a smoke test for
|
|
242
|
+
* {@link MeshShape3D}: arbitrary indexed triangle mesh → tetrahedralised
|
|
243
|
+
* collider via {@link shape_mesh_from_geometry}.
|
|
244
|
+
*
|
|
245
|
+
* Dynamic body, so the player capsule (kinematic) can bump it around.
|
|
246
|
+
* KinematicPosition bodies don't receive impulses themselves (the
|
|
247
|
+
* controller writes the Transform), so a static knot vs. kinematic
|
|
248
|
+
* capsule produces zero solver response and the capsule clips through.
|
|
249
|
+
* Making the knot dynamic flips the relationship: when the capsule
|
|
250
|
+
* penetrates, the solver applies impulse to the knot — it bounces away
|
|
251
|
+
* and the player gets a visible reaction.
|
|
252
|
+
*
|
|
253
|
+
* Placed off the spawn plaza's main travel path (radius 18 north-east of
|
|
254
|
+
* spawn) so it's visible but not in the way of the parkour drills.
|
|
255
|
+
*/
|
|
256
|
+
function buildMeshShapeShowcase(ecd) {
|
|
257
|
+
// TorusKnotGeometry (radius, tube, tubularSegments, radialSegments).
|
|
258
|
+
// 64 × 8 = 512 segments → ~512 verts / ~1024 tris — plenty of detail
|
|
259
|
+
// for visual interest, light enough that the per-vertex `support()`
|
|
260
|
+
// scan stays fast.
|
|
261
|
+
const geom = new TorusKnotGeometry(0.5, 0.15, 64, 8);
|
|
262
|
+
geom.computeVertexNormals();
|
|
263
|
+
|
|
264
|
+
// Three packs positions as Float32Array (interleaved x, y, z) and
|
|
265
|
+
// indices as Uint16Array by default. The MeshShape3D factory wants
|
|
266
|
+
// a Uint32Array index buffer — convert once at construction.
|
|
267
|
+
const positions = geom.attributes.position.array;
|
|
268
|
+
const src_indices = geom.index.array;
|
|
269
|
+
const indices = src_indices instanceof Uint32Array
|
|
270
|
+
? src_indices
|
|
271
|
+
: Uint32Array.from(src_indices);
|
|
272
|
+
|
|
273
|
+
const shape = shape_mesh_from_geometry(positions, indices);
|
|
274
|
+
|
|
275
|
+
const material = new MeshStandardMaterial({ color: 0xffaa66, roughness: 0.5 });
|
|
276
|
+
const sg = ShadedGeometry.from(geom, material);
|
|
277
|
+
sg.setFlag(ShadedGeometryFlags.CastShadow);
|
|
278
|
+
sg.setFlag(ShadedGeometryFlags.ReceiveShadow);
|
|
279
|
+
sg.setFlag(ShadedGeometryFlags.Visible);
|
|
280
|
+
|
|
281
|
+
const transform = new Transform();
|
|
282
|
+
// North-east of spawn, lifted ~2m off the ground so the knot drops
|
|
283
|
+
// onto the floor when the sim starts and the player can walk over,
|
|
284
|
+
// bump it, and watch it tumble.
|
|
285
|
+
transform.position.set(SPAWN_X + 18, 2, SPAWN_Z - 18);
|
|
286
|
+
|
|
287
|
+
const rigidBody = new RigidBody();
|
|
288
|
+
rigidBody.kind = BodyKind.Dynamic;
|
|
289
|
+
rigidBody.mass = 1;
|
|
290
|
+
// Bounding radius ≈ main_radius + tube_radius = 0.65. For a chunky
|
|
291
|
+
// bumpable body matching the existing 1m cube reference, use the
|
|
292
|
+
// same `(6, 6, 6)` inverse inertia — it's slightly under-rotational
|
|
293
|
+
// for the torus knot's mass distribution but reads as solid.
|
|
294
|
+
rigidBody.inverseInertiaLocal.set(6, 6, 6);
|
|
295
|
+
rigidBody.linearDamping = 0.5;
|
|
296
|
+
rigidBody.angularDamping = 0.5;
|
|
297
|
+
|
|
298
|
+
const collider = new Collider();
|
|
299
|
+
collider.shape = shape;
|
|
300
|
+
collider.friction = 0.5;
|
|
301
|
+
collider.restitution = 0.2;
|
|
302
|
+
|
|
303
|
+
new Entity()
|
|
304
|
+
.add(sg)
|
|
305
|
+
.add(rigidBody)
|
|
306
|
+
.add(collider)
|
|
307
|
+
.add(transform)
|
|
308
|
+
.add(Tag.fromJSON(["GymObstacle"]))
|
|
309
|
+
.add(SerializationMetadata.Transient)
|
|
310
|
+
.build(ecd);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Reference markers around spawn so motion is visually obvious — a ring
|
|
315
|
+
* of coloured cubes (circle-strafe targets) and a few tall pillars at
|
|
316
|
+
* radius 22 for parallax-at-distance.
|
|
317
|
+
*/
|
|
318
|
+
function buildSpawnPlaza(ecd) {
|
|
319
|
+
const unitCubeGeom = new BoxGeometry(1, 1, 1);
|
|
320
|
+
const palette = [
|
|
321
|
+
0xff5555, 0x55ff55, 0x5577ff, 0xffaa44,
|
|
322
|
+
0xaa55ff, 0x44ddee, 0xff66cc, 0x66ffcc,
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
// Inner ring at radius 6 — close enough to circle-strafe around.
|
|
326
|
+
const RING_N = 8;
|
|
327
|
+
for (let i = 0; i < RING_N; i++) {
|
|
328
|
+
const a = (i / RING_N) * Math.PI * 2;
|
|
329
|
+
spawnBox(ecd, {
|
|
330
|
+
center: new Vector3(SPAWN_X + Math.cos(a) * 6, 0.5, SPAWN_Z + Math.sin(a) * 6),
|
|
331
|
+
size: new Vector3(1, 1, 1),
|
|
332
|
+
geometry: unitCubeGeom,
|
|
333
|
+
color: palette[i % palette.length],
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Tall pillars further out — parallax depth perception when running.
|
|
338
|
+
const pillarGeom = new BoxGeometry(1, 6, 1);
|
|
339
|
+
const PILLAR_N = 6;
|
|
340
|
+
for (let i = 0; i < PILLAR_N; i++) {
|
|
341
|
+
const a = (i / PILLAR_N) * Math.PI * 2 + 0.3;
|
|
342
|
+
const r = 22;
|
|
343
|
+
spawnBox(ecd, {
|
|
344
|
+
center: new Vector3(SPAWN_X + Math.cos(a) * r, 3, SPAWN_Z + Math.sin(a) * r),
|
|
345
|
+
size: new Vector3(1, 6, 1),
|
|
346
|
+
geometry: pillarGeom,
|
|
347
|
+
color: 0x888888,
|
|
348
|
+
roughness: 0.9,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Mantle row — directly north of spawn. Four obstacles in line, heights
|
|
355
|
+
* stepping past the mantle range:
|
|
356
|
+
*
|
|
357
|
+
* - 0.6m: well within mantle range; lowest "step-up" test
|
|
358
|
+
* - 1.0m: mid mantle range
|
|
359
|
+
* - 1.3m: near top of mantle range
|
|
360
|
+
* - 2.0m: too tall to mantle from the ground (heightDiff > maxHeight)
|
|
361
|
+
*
|
|
362
|
+
* The 2.0m block is also the canonical "LedgeGrab if you jump at it"
|
|
363
|
+
* candidate, but the dedicated LedgeGrab station to the south has a
|
|
364
|
+
* cleaner approach path.
|
|
365
|
+
*/
|
|
366
|
+
function buildMantleStation(ecd) {
|
|
367
|
+
const baseZ = SPAWN_Z + 15;
|
|
368
|
+
spawnPad(ecd, SPAWN_X, baseZ - 3, COLOR_MANTLE);
|
|
369
|
+
|
|
370
|
+
const heights = [0.6, 1.0, 1.3, 2.0];
|
|
371
|
+
for (let i = 0; i < heights.length; i++) {
|
|
372
|
+
const h = heights[i];
|
|
373
|
+
spawnBox(ecd, {
|
|
374
|
+
center: new Vector3(SPAWN_X, h / 2, baseZ + i * 5),
|
|
375
|
+
size: new Vector3(3, h, 1),
|
|
376
|
+
color: COLOR_MANTLE,
|
|
377
|
+
roughness: 0.6,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Wall-run course — a dedicated traversal east of spawn. Two raised
|
|
384
|
+
* platforms separated by a gap too wide to plain-jump, with a tall wall
|
|
385
|
+
* down ONE side that spans the gap. Stairs climb onto each platform from
|
|
386
|
+
* its outer end. The drill:
|
|
387
|
+
*
|
|
388
|
+
* 1. climb the west stairs onto platform A
|
|
389
|
+
* 2. SPRINT east along the wall (a run, not a walk — wallRun.minSpeed 5.5)
|
|
390
|
+
* 3. jump at the platform edge → the wall-run engages and carries you
|
|
391
|
+
* across the gap along the wall
|
|
392
|
+
* 4. land on platform B and descend the east stairs
|
|
393
|
+
*
|
|
394
|
+
* The wall is on the −Z side, its north face flush with the platforms' south
|
|
395
|
+
* faces, rising from the gap floor; it overlaps ~2 m onto each platform so
|
|
396
|
+
* the run can engage before the gap and dismount onto the far side.
|
|
397
|
+
*/
|
|
398
|
+
function buildWallRunStation(ecd) {
|
|
399
|
+
const z = SPAWN_Z; // course centreline along Z
|
|
400
|
+
const gapCenterX = SPAWN_X + 22; // east of spawn (clear of the inner ring)
|
|
401
|
+
const H = 1.5; // platform top height
|
|
402
|
+
const GAP = 7; // wider than a running jump (~5 m) → must wall-run
|
|
403
|
+
const platX = 5, platZ = 4; // platform footprint (X × Z)
|
|
404
|
+
const halfX = platX / 2;
|
|
405
|
+
const RISE = 0.25, DEPTH = 0.4, STEPS = Math.round(H / RISE); // 6 steps to y=H
|
|
406
|
+
|
|
407
|
+
const aCenterX = gapCenterX - GAP / 2 - halfX; // platform A (west, launch)
|
|
408
|
+
const bCenterX = gapCenterX + GAP / 2 + halfX; // platform B (east, land)
|
|
409
|
+
const aWestEdge = aCenterX - halfX;
|
|
410
|
+
const bEastEdge = bCenterX + halfX;
|
|
411
|
+
|
|
412
|
+
// Entry pad on open ground, just west of the foot of the west stairs.
|
|
413
|
+
spawnPad(ecd, aWestEdge - STEPS * DEPTH - 2.5, z, COLOR_WALLRUN);
|
|
414
|
+
|
|
415
|
+
// Two platforms, solid from the ground (top at y=H).
|
|
416
|
+
spawnBox(ecd, { center: new Vector3(aCenterX, H / 2, z), size: new Vector3(platX, H, platZ), color: COLOR_WALLRUN, roughness: 0.6 });
|
|
417
|
+
spawnBox(ecd, { center: new Vector3(bCenterX, H / 2, z), size: new Vector3(platX, H, platZ), color: COLOR_WALLRUN, roughness: 0.6 });
|
|
418
|
+
|
|
419
|
+
// The wall to run on — south edge, north face flush with the platforms'
|
|
420
|
+
// −Z faces, spanning the gap plus 2 m onto each platform, rising from the
|
|
421
|
+
// gap floor well above head height through the run arc.
|
|
422
|
+
const wallH = 5;
|
|
423
|
+
const wallZ = z - platZ / 2 - 0.25; // 0.5-thick wall; north face at the platform south edge
|
|
424
|
+
spawnBox(ecd, {
|
|
425
|
+
center: new Vector3(gapCenterX, wallH / 2, wallZ),
|
|
426
|
+
size: new Vector3(GAP + 4, wallH, 0.5),
|
|
427
|
+
color: COLOR_WALLRUN,
|
|
428
|
+
roughness: 0.6,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Stairs onto each platform from its outer end, each flight rising toward
|
|
432
|
+
// the platform it serves.
|
|
433
|
+
spawnStairFlight(ecd, aWestEdge, z, +1, STEPS, RISE, DEPTH, platZ, COLOR_WALLRUN); // west: ascend +X onto A
|
|
434
|
+
spawnStairFlight(ecd, bEastEdge, z, -1, STEPS, RISE, DEPTH, platZ, COLOR_WALLRUN); // east: ascend −X onto B
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A flight of solid-pillar steps (the buildStairsStation profile) reaching
|
|
439
|
+
* height `steps*rise`, flush at `topEdgeX`, descending to the ground away
|
|
440
|
+
* from it. `ascendDir` is the climb direction along X (+1 toward +X, −1
|
|
441
|
+
* toward −X); steps march the other way, edge-to-edge, each a pillar from the
|
|
442
|
+
* ground up to its tread height.
|
|
443
|
+
*/
|
|
444
|
+
function spawnStairFlight(ecd, topEdgeX, z, ascendDir, steps, rise, depth, width, color) {
|
|
445
|
+
for (let i = 1; i <= steps; i++) {
|
|
446
|
+
const topY = i * rise;
|
|
447
|
+
const cx = topEdgeX - ascendDir * (steps - i + 0.5) * depth;
|
|
448
|
+
spawnBox(ecd, {
|
|
449
|
+
center: new Vector3(cx, topY / 2, z),
|
|
450
|
+
size: new Vector3(depth, topY, width),
|
|
451
|
+
color,
|
|
452
|
+
roughness: 0.6,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Wall-jump chimney — two parallel walls on the west side, ~3m apart,
|
|
459
|
+
* 6m tall. Player walks into the gap, jumps, hits one wall, wall-jumps
|
|
460
|
+
* to the other side. Repeat to climb.
|
|
461
|
+
*
|
|
462
|
+
* The walls are wide enough that wall-jumping straight back to the
|
|
463
|
+
* other side stays inside the chimney.
|
|
464
|
+
*/
|
|
465
|
+
function buildWallJumpChimney(ecd) {
|
|
466
|
+
const baseX = SPAWN_X - 15;
|
|
467
|
+
spawnPad(ecd, baseX + 3, SPAWN_Z, COLOR_WALLJUMP);
|
|
468
|
+
|
|
469
|
+
// North wall of the chimney (pushes south).
|
|
470
|
+
spawnBox(ecd, {
|
|
471
|
+
center: new Vector3(baseX - 2, 3, SPAWN_Z + 2),
|
|
472
|
+
size: new Vector3(4, 6, 0.5),
|
|
473
|
+
color: COLOR_WALLJUMP,
|
|
474
|
+
roughness: 0.6,
|
|
475
|
+
});
|
|
476
|
+
// South wall of the chimney (pushes north). 3m gap to the north wall.
|
|
477
|
+
spawnBox(ecd, {
|
|
478
|
+
center: new Vector3(baseX - 2, 3, SPAWN_Z - 1.25),
|
|
479
|
+
size: new Vector3(4, 6, 0.5),
|
|
480
|
+
color: COLOR_WALLJUMP,
|
|
481
|
+
roughness: 0.6,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Ledge-grab station — a single 2m-tall wall just south of spawn.
|
|
487
|
+
* Too tall to mantle from the ground (heightDiff = 2 > maxHeight 1.4),
|
|
488
|
+
* but the player can run-jump at it and catch the lip on descent:
|
|
489
|
+
*
|
|
490
|
+
* 1. sprint south
|
|
491
|
+
* 2. jump just before the wall
|
|
492
|
+
* 3. on the way down, chest probe hits the wall face;
|
|
493
|
+
* ledgeAhead probe finds the top → LedgeGrab fires
|
|
494
|
+
* 4. press jump to chain into Mantle (mantle-up release)
|
|
495
|
+
*
|
|
496
|
+
* The wall is 6m wide so the player doesn't have to aim perfectly.
|
|
497
|
+
*/
|
|
498
|
+
function buildLedgeGrabStation(ecd) {
|
|
499
|
+
const baseZ = SPAWN_Z - 15;
|
|
500
|
+
spawnPad(ecd, SPAWN_X, baseZ + 3, COLOR_LEDGE);
|
|
501
|
+
|
|
502
|
+
spawnBox(ecd, {
|
|
503
|
+
center: new Vector3(SPAWN_X, 1.0, baseZ - 1),
|
|
504
|
+
size: new Vector3(6, 2.0, 2),
|
|
505
|
+
color: COLOR_LEDGE,
|
|
506
|
+
roughness: 0.6,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// A larger top-of-wall platform behind the lip, so once the player
|
|
510
|
+
// climbs up they have somewhere to stand and look around.
|
|
511
|
+
spawnBox(ecd, {
|
|
512
|
+
center: new Vector3(SPAWN_X, 1.0, baseZ - 4.5),
|
|
513
|
+
size: new Vector3(6, 2.0, 5),
|
|
514
|
+
color: COLOR_LEDGE,
|
|
515
|
+
roughness: 0.6,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Slide tunnel — a sprint run-up followed by a low ceiling that forces
|
|
521
|
+
* the player to slide under. NE of spawn.
|
|
522
|
+
*
|
|
523
|
+
* Layout: a long flat lane (just an entry pad / marker, ground is
|
|
524
|
+
* already flat), with an overhead box at 1m height. Approaching at
|
|
525
|
+
* sprint + crouch press → slide → fits under the gap.
|
|
526
|
+
*/
|
|
527
|
+
function buildSlideTunnel(ecd) {
|
|
528
|
+
const baseX = SPAWN_X + 11;
|
|
529
|
+
const baseZ = SPAWN_Z + 11;
|
|
530
|
+
spawnPad(ecd, baseX, baseZ, COLOR_SLIDE);
|
|
531
|
+
|
|
532
|
+
// The "ceiling" — a slab suspended 1.0m above the ground. Sliding
|
|
533
|
+
// brings the eye height down to ~0.4m; standing-eye-height would
|
|
534
|
+
// collide.
|
|
535
|
+
spawnBox(ecd, {
|
|
536
|
+
center: new Vector3(baseX + 4, 1.5, baseZ + 4),
|
|
537
|
+
size: new Vector3(4, 1.0, 4),
|
|
538
|
+
color: COLOR_SLIDE,
|
|
539
|
+
roughness: 0.6,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Side pillars to make the tunnel feel like a tunnel.
|
|
543
|
+
spawnBox(ecd, {
|
|
544
|
+
center: new Vector3(baseX + 4, 1.0, baseZ + 2.0),
|
|
545
|
+
size: new Vector3(4, 2.0, 0.5),
|
|
546
|
+
color: COLOR_SLIDE,
|
|
547
|
+
roughness: 0.7,
|
|
548
|
+
});
|
|
549
|
+
spawnBox(ecd, {
|
|
550
|
+
center: new Vector3(baseX + 4, 1.0, baseZ + 6.0),
|
|
551
|
+
size: new Vector3(4, 2.0, 0.5),
|
|
552
|
+
color: COLOR_SLIDE,
|
|
553
|
+
roughness: 0.7,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Gap jump — two elevated platforms with a 3m gap between them. Player
|
|
559
|
+
* approaches via a mantle stair, runs across the first platform,
|
|
560
|
+
* sprints + jumps the gap.
|
|
561
|
+
*
|
|
562
|
+
* The platforms sit 1.5m above the spawn ground (mantleable). The gap
|
|
563
|
+
* doesn't actually break the floor — landing flat would still catch
|
|
564
|
+
* the player on the y=0 floor below. That's fine for testing the
|
|
565
|
+
* mid-flight feel of a jump; survival of misses is a feature here.
|
|
566
|
+
*/
|
|
567
|
+
function buildGapJumpStation(ecd) {
|
|
568
|
+
const baseX = SPAWN_X + 11;
|
|
569
|
+
const baseZ = SPAWN_Z - 11;
|
|
570
|
+
spawnPad(ecd, baseX, baseZ, COLOR_GAP);
|
|
571
|
+
|
|
572
|
+
// Approach mantle (step up to the first platform).
|
|
573
|
+
spawnBox(ecd, {
|
|
574
|
+
center: new Vector3(baseX + 2, 0.5, baseZ - 2),
|
|
575
|
+
size: new Vector3(2, 1.0, 2),
|
|
576
|
+
color: COLOR_GAP,
|
|
577
|
+
roughness: 0.7,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// First platform — 4m long along Z.
|
|
581
|
+
spawnBox(ecd, {
|
|
582
|
+
center: new Vector3(baseX + 5, 0.75, baseZ - 4),
|
|
583
|
+
size: new Vector3(4, 1.5, 4),
|
|
584
|
+
color: COLOR_GAP,
|
|
585
|
+
roughness: 0.6,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Second platform — same height, 3m gap.
|
|
589
|
+
spawnBox(ecd, {
|
|
590
|
+
center: new Vector3(baseX + 5, 0.75, baseZ - 11),
|
|
591
|
+
size: new Vector3(4, 1.5, 4),
|
|
592
|
+
color: COLOR_GAP,
|
|
593
|
+
roughness: 0.6,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Stairs station — a walkable staircase up to a landing, SW of spawn.
|
|
599
|
+
* Exercises the KinematicMover's stair handling (DESIGN_COLLISION.md
|
|
600
|
+
* Phase 3): the player walks up the steps staying grounded (no launch
|
|
601
|
+
* off each lip) and walks back down staying grounded (stick-to-ground
|
|
602
|
+
* snaps onto each lower step rather than going airborne).
|
|
603
|
+
*
|
|
604
|
+
* Each step rises `RISE` (0.2 m) — comfortably under the controller's
|
|
605
|
+
* `stepHeight` (0.3 m), so the mover climbs it. A blocking reference
|
|
606
|
+
* sits beside the stairs: a single 0.5 m riser (> stepHeight) the
|
|
607
|
+
* player canNOT walk up, for contrast.
|
|
608
|
+
*
|
|
609
|
+
* Steps are solid pillars from the ground to each tread height, placed
|
|
610
|
+
* edge-to-edge so they form a clean staircase profile (no floating
|
|
611
|
+
* treads, no overlap). The player approaches from the +X (spawn) side
|
|
612
|
+
* and walks −X up the flight.
|
|
613
|
+
*/
|
|
614
|
+
function buildStairsStation(ecd) {
|
|
615
|
+
const baseX = SPAWN_X - 11;
|
|
616
|
+
const baseZ = SPAWN_Z - 11;
|
|
617
|
+
spawnPad(ecd, baseX, baseZ, COLOR_STAIRS);
|
|
618
|
+
|
|
619
|
+
const RISE = 0.2; // per-step rise — under stepHeight (0.3) → climbable
|
|
620
|
+
const DEPTH = 0.45; // tread depth
|
|
621
|
+
const WIDTH = 4; // generous width so aiming isn't required
|
|
622
|
+
const STEPS = 7; // → 1.4 m top
|
|
623
|
+
const firstFrontX = baseX - 2; // near (east) edge of the first step
|
|
624
|
+
|
|
625
|
+
for (let i = 1; i <= STEPS; i++) {
|
|
626
|
+
const topY = i * RISE;
|
|
627
|
+
spawnBox(ecd, {
|
|
628
|
+
center: new Vector3(firstFrontX - (i - 0.5) * DEPTH, topY / 2, baseZ),
|
|
629
|
+
size: new Vector3(DEPTH, topY, WIDTH),
|
|
630
|
+
color: COLOR_STAIRS,
|
|
631
|
+
roughness: 0.6,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Landing platform at the top, flush with the last tread.
|
|
636
|
+
const topY = STEPS * RISE;
|
|
637
|
+
spawnBox(ecd, {
|
|
638
|
+
center: new Vector3(firstFrontX - STEPS * DEPTH - 1.5, topY / 2, baseZ),
|
|
639
|
+
size: new Vector3(3, topY, WIDTH),
|
|
640
|
+
color: COLOR_STAIRS,
|
|
641
|
+
roughness: 0.6,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// Blocking reference — a single 0.5 m riser (> stepHeight) beside the
|
|
645
|
+
// flight (offset +Z). Walk into it: the mover blocks rather than
|
|
646
|
+
// climbing, the contrast to the stairs.
|
|
647
|
+
spawnBox(ecd, {
|
|
648
|
+
center: new Vector3(firstFrontX - 1, 0.25, baseZ + WIDTH / 2 + 2),
|
|
649
|
+
size: new Vector3(3, 0.5, 3),
|
|
650
|
+
color: COLOR_NEUTRAL,
|
|
651
|
+
roughness: 0.8,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* NW — Ramp row. A line of inclined slabs at increasing angle. The mover's
|
|
657
|
+
* walkability gate is `minWalkNormal` 0.7 (≈ 45.6°), so the gentle ramps can
|
|
658
|
+
* be walked / run up, while the steep ones can't be stood on — step onto one
|
|
659
|
+
* and you slide back down. Approach from the south (the pad) and head +Z up
|
|
660
|
+
* each.
|
|
661
|
+
*
|
|
662
|
+
* Each ramp is a thin slab rotated about X so its top face rises in +Z. The
|
|
663
|
+
* lower half is buried in the ground plane (the realistic "ramp base sunk in
|
|
664
|
+
* terrain" pattern), so the surface emerges flush — no lip to catch on.
|
|
665
|
+
*/
|
|
666
|
+
function buildRampStation(ecd) {
|
|
667
|
+
const baseX = SPAWN_X - 11;
|
|
668
|
+
const baseZ = SPAWN_Z + 11; // emergence line — ramps rise +Z (north) from here
|
|
669
|
+
spawnPad(ecd, baseX, baseZ - 3, COLOR_RAMP);
|
|
670
|
+
|
|
671
|
+
const angles = [20, 30, 40, 50, 60]; // ° — 20/30/40 walkable, 50/60 too steep (slide)
|
|
672
|
+
const LENGTH = 7; // slab length (half buried) → ~3.5·cosθ of usable run
|
|
673
|
+
const WIDTH = 2.5; // generous enough to aim up without precision
|
|
674
|
+
const SPACING = 3; // between ramp centres, along X
|
|
675
|
+
const x0 = baseX - ((angles.length - 1) * SPACING) / 2; // centre the row on baseX
|
|
676
|
+
|
|
677
|
+
angles.forEach((deg, i) => {
|
|
678
|
+
spawnRamp(ecd, x0 + i * SPACING, baseZ, deg, LENGTH, WIDTH, COLOR_RAMP);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Spawn one inclined ramp slab whose top face emerges from the ground at
|
|
684
|
+
* (ex, 0, ez) and rises in +Z at `angleDeg`. Built as a `length`×`THICK`×
|
|
685
|
+
* `width`... actually width×THICK×length box (X=width, Y=thickness, Z=length)
|
|
686
|
+
* rotated −angle about X, then offset so the top-face centre lands on the
|
|
687
|
+
* emergence point (lower half buried, upper half the walkable incline).
|
|
688
|
+
*/
|
|
689
|
+
function spawnRamp(ecd, ex, ez, angleDeg, length, width, color) {
|
|
690
|
+
const theta = (angleDeg * Math.PI) / 180;
|
|
691
|
+
const THICK = 0.5;
|
|
692
|
+
const h = THICK / 2;
|
|
693
|
+
const rot = new Quaternion();
|
|
694
|
+
rot.fromAxisAngle(new Vector3(1, 0, 0), -theta); // top face tilts up toward +Z
|
|
695
|
+
spawnBox(ecd, {
|
|
696
|
+
center: new Vector3(ex, -h * Math.cos(theta), ez + h * Math.sin(theta)),
|
|
697
|
+
size: new Vector3(width, THICK, length),
|
|
698
|
+
color,
|
|
699
|
+
roughness: 0.6,
|
|
700
|
+
rotation: rot,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// =====================================================================
|
|
705
|
+
// Spawn helpers
|
|
706
|
+
// =====================================================================
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Add a box-shaped obstacle to the world. Position is the box centre.
|
|
710
|
+
*
|
|
711
|
+
* Single entity carrying both visual (ShadedGeometry) and physical
|
|
712
|
+
* (RigidBody + Collider) representations so the player can stand on it,
|
|
713
|
+
* sensors can raycast against it, and dynamic crates (when added) bump
|
|
714
|
+
* off it. Bodies are {@link BodyKind.Static} — gym geometry never moves.
|
|
715
|
+
*
|
|
716
|
+
* @param {EntityComponentDataset} ecd
|
|
717
|
+
* @param {object} p
|
|
718
|
+
* @param {Vector3} p.center
|
|
719
|
+
* @param {Vector3} p.size w, h, d (box dimensions)
|
|
720
|
+
* @param {number} p.color
|
|
721
|
+
* @param {number} [p.roughness]
|
|
722
|
+
* @param {THREE.BufferGeometry} [p.geometry] reuse to avoid new geometry per spawn
|
|
723
|
+
* @param {string} [p.tag]
|
|
724
|
+
* @param {boolean} [p.collide] default true; set false for decorative pads
|
|
725
|
+
*/
|
|
726
|
+
function spawnBox(ecd, { center, size, color, roughness = 0.7, geometry, tag = "GymObstacle", collide = true, rotation }) {
|
|
727
|
+
const geom = geometry !== undefined ? geometry : new BoxGeometry(size.x, size.y, size.z);
|
|
728
|
+
const material = new MeshStandardMaterial({ color, roughness });
|
|
729
|
+
const sg = ShadedGeometry.from(geom, material);
|
|
730
|
+
sg.setFlag(ShadedGeometryFlags.CastShadow);
|
|
731
|
+
sg.setFlag(ShadedGeometryFlags.ReceiveShadow);
|
|
732
|
+
sg.setFlag(ShadedGeometryFlags.Visible);
|
|
733
|
+
|
|
734
|
+
const transform = new Transform();
|
|
735
|
+
transform.position.copy(center);
|
|
736
|
+
// One Transform drives both the visual mesh and the collider, so a
|
|
737
|
+
// rotation here tilts a box into a ramp consistently for render + physics.
|
|
738
|
+
if (rotation !== undefined) transform.rotation.copy(rotation);
|
|
739
|
+
|
|
740
|
+
const eb = new Entity()
|
|
741
|
+
.add(sg)
|
|
742
|
+
.add(Tag.fromJSON([tag]))
|
|
743
|
+
.add(SerializationMetadata.Transient);
|
|
744
|
+
|
|
745
|
+
if (collide) {
|
|
746
|
+
const rigidBody = new RigidBody();
|
|
747
|
+
rigidBody.kind = BodyKind.Static;
|
|
748
|
+
const collider = new Collider();
|
|
749
|
+
// BoxShape3D with explicit half-extents — routes through the
|
|
750
|
+
// closed-form sphere-box / box-box / capsule-box narrowphase paths
|
|
751
|
+
// instead of degenerating into GJK+EPA, which is unreliable on
|
|
752
|
+
// smooth-surface pairs like capsule-vs-box.
|
|
753
|
+
collider.shape = BoxShape3D.from_size(size.x, size.y, size.z);
|
|
754
|
+
// Transform is added LAST so the (RigidBody, Transform) tuple
|
|
755
|
+
// completes only after the body is present — PhysicsSystem.link
|
|
756
|
+
// is the trigger, not the rigidBody add.
|
|
757
|
+
eb.add(rigidBody).add(collider).add(transform).build(ecd);
|
|
758
|
+
} else {
|
|
759
|
+
eb.add(transform).build(ecd);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Lay a thin coloured "entry pad" on the ground at (x, z). Visual
|
|
765
|
+
* landmark only — flush with the floor, no collision (a 5 cm-thick
|
|
766
|
+
* static body in the player's walking path would bump the camera).
|
|
767
|
+
*/
|
|
768
|
+
function spawnPad(ecd, x, z, color) {
|
|
769
|
+
spawnBox(ecd, {
|
|
770
|
+
center: new Vector3(x, 0.025, z),
|
|
771
|
+
size: new Vector3(4, 0.05, 4),
|
|
772
|
+
color,
|
|
773
|
+
roughness: 0.95,
|
|
774
|
+
tag: "GymEntryPad",
|
|
775
|
+
collide: false,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Lay a physics-only ground body underneath the entire gym so the
|
|
781
|
+
* player has something to stand on. The terrain mesh provides the
|
|
782
|
+
* grass visual at y=0; this is its physics counterpart (the player
|
|
783
|
+
* controller's vertical integrator and sensors only look at physics
|
|
784
|
+
* bodies, not the terrain heightfield).
|
|
785
|
+
*
|
|
786
|
+
* Sized to comfortably encompass all stations (the gym spans ~30 m
|
|
787
|
+
* radius around spawn). The slab top sits at y=0 — same as the
|
|
788
|
+
* terrain visual — so standing on it puts the player's feet on the
|
|
789
|
+
* grass.
|
|
790
|
+
*/
|
|
791
|
+
function buildGroundPlane(ecd) {
|
|
792
|
+
const SIZE = 120;
|
|
793
|
+
const THICKNESS = 1.0;
|
|
794
|
+
|
|
795
|
+
const rigidBody = new RigidBody();
|
|
796
|
+
rigidBody.kind = BodyKind.Static;
|
|
797
|
+
const collider = new Collider();
|
|
798
|
+
collider.shape = BoxShape3D.from_size(SIZE, THICKNESS, SIZE);
|
|
799
|
+
|
|
800
|
+
const transform = new Transform();
|
|
801
|
+
// Top face at y=0 → centre at y = -thickness/2.
|
|
802
|
+
transform.position.set(SPAWN_X, -THICKNESS / 2, SPAWN_Z);
|
|
803
|
+
|
|
804
|
+
new Entity()
|
|
805
|
+
.add(rigidBody)
|
|
806
|
+
.add(collider)
|
|
807
|
+
.add(Tag.fromJSON(["GymGroundPlane"]))
|
|
808
|
+
.add(SerializationMetadata.Transient)
|
|
809
|
+
.add(transform)
|
|
810
|
+
.build(ecd);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// =====================================================================
|
|
814
|
+
// Player + input
|
|
815
|
+
// =====================================================================
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* @param {EntityComponentDataset} ecd
|
|
819
|
+
* @returns {{entity:number, controller:FirstPersonPlayerController}}
|
|
820
|
+
*/
|
|
821
|
+
function buildPlayerEntity(ecd) {
|
|
822
|
+
const transform = new Transform();
|
|
823
|
+
// Spawn standing on the ground at the centre of the plaza.
|
|
824
|
+
transform.position.set(SPAWN_X, 0, SPAWN_Z);
|
|
825
|
+
|
|
826
|
+
const controller = new FirstPersonPlayerController();
|
|
827
|
+
|
|
828
|
+
// Movement abilities (priority order — see DESIGN_EXTENSIONS §2.2):
|
|
829
|
+
// Slide(10) < Mantle(30) < LedgeGrab(40) < WallRun(50) < WallJump(60).
|
|
830
|
+
controller.abilities.add(new Slide());
|
|
831
|
+
controller.abilities.add(new Mantle());
|
|
832
|
+
controller.abilities.add(new LedgeGrab());
|
|
833
|
+
controller.abilities.add(new WallRun());
|
|
834
|
+
controller.abilities.add(new WallJump());
|
|
835
|
+
|
|
836
|
+
// Mastery evaluators — small bonuses/penalties tied to gait phase,
|
|
837
|
+
// foot asymmetry, and breath rhythm. Each fires on one (or a few)
|
|
838
|
+
// DecisionPoint(s); contributions compose multiplicatively.
|
|
839
|
+
controller.mastery.add(new StrideTimingJumpEvaluator());
|
|
840
|
+
controller.mastery.add(new SlideInitiationTimingEvaluator());
|
|
841
|
+
controller.mastery.add(new FootAsymmetryTurnEvaluator());
|
|
842
|
+
controller.mastery.add(new BreathRhythmEvaluator());
|
|
843
|
+
controller.mastery.add(new BreathRhythmEvaluator({
|
|
844
|
+
decisionPoints: [DecisionPoint.GroundAccel],
|
|
845
|
+
peakBonus: 0.015,
|
|
846
|
+
troughPenalty: 0.005,
|
|
847
|
+
requireExertionAbove: 0.3,
|
|
848
|
+
}));
|
|
849
|
+
|
|
850
|
+
// Kinematic capsule body: the controller writes Transform directly,
|
|
851
|
+
// physics derives velocity from the per-step delta. Capsule centred
|
|
852
|
+
// on the Transform — bottom hemisphere extends below the feet (this
|
|
853
|
+
// is fine for spatial queries since sensor probes start at chest
|
|
854
|
+
// height and the ground resolver filters out the player's own body).
|
|
855
|
+
const bodyCfg = controller.config.body;
|
|
856
|
+
const radius = bodyCfg.radius;
|
|
857
|
+
const totalHeight = bodyCfg.height; // standing eye height ≈ body extent
|
|
858
|
+
const cylinderHeight = Math.max(0, totalHeight - 2 * radius);
|
|
859
|
+
const rigidBody = new RigidBody();
|
|
860
|
+
rigidBody.kind = BodyKind.KinematicPosition;
|
|
861
|
+
rigidBody.mass = bodyCfg.mass;
|
|
862
|
+
const collider = new Collider();
|
|
863
|
+
// Capsule lives in [feet .. head] in world space. CapsuleShape3D is
|
|
864
|
+
// centred at its local origin, so wrap it in a TransformedShape3D
|
|
865
|
+
// with a +Y translation of totalHeight/2 — that puts the capsule
|
|
866
|
+
// bottom at the player's feet (Transform.position) and the top at
|
|
867
|
+
// the head. Without this, shape_cast queries against the ground
|
|
868
|
+
// would stop the capsule centre at floor + half-extent, leaving
|
|
869
|
+
// the player visibly floating half a body height above the floor.
|
|
870
|
+
collider.shape = TransformedShape3D.from_translation(
|
|
871
|
+
CapsuleShape3D.from(radius, cylinderHeight),
|
|
872
|
+
[0, totalHeight / 2, 0],
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
// Build order matters: Transform is added LAST so the FirstPerson
|
|
876
|
+
// controller system's link() sees the RigidBody already attached.
|
|
877
|
+
// (Same pattern as the spec helpers.)
|
|
878
|
+
const eb = new Entity()
|
|
879
|
+
.add(controller)
|
|
880
|
+
.add(rigidBody)
|
|
881
|
+
.add(collider)
|
|
882
|
+
.add(Tag.fromJSON(["Player"]))
|
|
883
|
+
.add(SerializationMetadata.Transient)
|
|
884
|
+
.add(transform);
|
|
885
|
+
|
|
886
|
+
eb.build(ecd);
|
|
887
|
+
|
|
888
|
+
return { entity: eb.id, controller };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Wire keyboard + mouse to the controller's intent surface.
|
|
893
|
+
*
|
|
894
|
+
* @param {EntityComponentDataset} ecd
|
|
895
|
+
* @param {{entity:number, controller:FirstPersonPlayerController}} player
|
|
896
|
+
*/
|
|
897
|
+
function buildInputBindings(ecd, player) {
|
|
898
|
+
const ic = new InputController();
|
|
899
|
+
const intent = player.controller.intent;
|
|
900
|
+
|
|
901
|
+
// Mouse sensitivity — radians of view rotation per pixel of mouse delta.
|
|
902
|
+
const SENSITIVITY = (Math.PI * 2) / 1200;
|
|
903
|
+
|
|
904
|
+
// -- WASD movement --------------------------------------------------
|
|
905
|
+
const held = { w: false, a: false, s: false, d: false };
|
|
906
|
+
|
|
907
|
+
function recomputeMove() {
|
|
908
|
+
let mx = 0, my = 0;
|
|
909
|
+
if (held.d) mx += 1;
|
|
910
|
+
if (held.a) mx -= 1;
|
|
911
|
+
if (held.w) my += 1;
|
|
912
|
+
if (held.s) my -= 1;
|
|
913
|
+
intent.move.set(mx, my);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function bindMoveKey(key, prop) {
|
|
917
|
+
ic.bind(`keyboard/keys/${key}/down`, () => {
|
|
918
|
+
held[prop] = true;
|
|
919
|
+
recomputeMove();
|
|
920
|
+
});
|
|
921
|
+
ic.bind(`keyboard/keys/${key}/up`, () => {
|
|
922
|
+
held[prop] = false;
|
|
923
|
+
recomputeMove();
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
bindMoveKey("w", "w");
|
|
928
|
+
bindMoveKey("a", "a");
|
|
929
|
+
bindMoveKey("s", "s");
|
|
930
|
+
bindMoveKey("d", "d");
|
|
931
|
+
|
|
932
|
+
// -- Jump ------------------------------------------------------------
|
|
933
|
+
ic.bind("keyboard/keys/space/down", () => { intent.jump = true; });
|
|
934
|
+
ic.bind("keyboard/keys/space/up", () => { intent.jump = false; });
|
|
935
|
+
|
|
936
|
+
// -- Sprint ----------------------------------------------------------
|
|
937
|
+
ic.bind("keyboard/keys/shift/down", () => { intent.sprint = true; });
|
|
938
|
+
ic.bind("keyboard/keys/shift/up", () => { intent.sprint = false; });
|
|
939
|
+
|
|
940
|
+
// -- Crouch ----------------------------------------------------------
|
|
941
|
+
ic.bind("keyboard/keys/c/down", () => { intent.crouch = true; });
|
|
942
|
+
ic.bind("keyboard/keys/c/up", () => { intent.crouch = false; });
|
|
943
|
+
|
|
944
|
+
// -- Mouse look ------------------------------------------------------
|
|
945
|
+
ic.bind("pointer/on/move", (position, event, delta) => {
|
|
946
|
+
if (document.pointerLockElement === null) return;
|
|
947
|
+
intent.look._add(delta.x * SENSITIVITY, delta.y * SENSITIVITY);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
ic.on.unlinked.add(() => {
|
|
951
|
+
intent.move.set(0, 0);
|
|
952
|
+
intent.look.set(0, 0);
|
|
953
|
+
intent.jump = false;
|
|
954
|
+
intent.crouch = false;
|
|
955
|
+
intent.sprint = false;
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
new Entity()
|
|
959
|
+
.add(ic)
|
|
960
|
+
.add(Tag.fromJSON(["FirstPersonInputBindings"]))
|
|
961
|
+
.add(SerializationMetadata.Transient)
|
|
962
|
+
.build(ecd);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Click to capture; ESC (or other browser-default) to release.
|
|
967
|
+
* @param {Engine} engine
|
|
968
|
+
*/
|
|
969
|
+
function setupPointerLock(engine) {
|
|
970
|
+
const el = engine.graphics.domElement;
|
|
971
|
+
const captureEl = engine.viewStack.el;
|
|
972
|
+
|
|
973
|
+
captureEl.addEventListener("click", () => {
|
|
974
|
+
if (document.pointerLockElement !== el && el.requestPointerLock) {
|
|
975
|
+
el.requestPointerLock();
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
captureEl.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function logHelp() {
|
|
983
|
+
/* eslint-disable no-console */
|
|
984
|
+
console.log("%cFirstPersonPlayerController — Parkour Gym",
|
|
985
|
+
"color:#88ddff;font-weight:bold;font-size:14px");
|
|
986
|
+
console.log("Click the canvas to capture mouse. ESC to release.");
|
|
987
|
+
console.log("WASD = move | Mouse = look | Space = jump | Shift = sprint | C = crouch");
|
|
988
|
+
console.log("");
|
|
989
|
+
console.log("%cStations:", "font-weight:bold");
|
|
990
|
+
console.log(" N Mantle row — four obstacles, last one is too tall");
|
|
991
|
+
console.log(" NE Slide tunnel — sprint + C to slide under the low ceiling");
|
|
992
|
+
console.log(" E Wall-run — sprint along, jump near the wall");
|
|
993
|
+
console.log(" SE Gap jump — mantle up the stair, sprint, jump the gap");
|
|
994
|
+
console.log(" S Ledge-grab — too tall to mantle; jump at it, catch the lip");
|
|
995
|
+
console.log(" W Wall-jump chimney — alternate walls to climb");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async function init() {
|
|
999
|
+
await eh.initialize();
|
|
1000
|
+
await main(eh.engine);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
init();
|