@woosh/meep-engine 2.145.0 → 2.147.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 -14
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +20 -8
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -546
- package/src/engine/control/first-person/TODO.md +13 -11
- package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
- package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
- package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/Mantle.js +195 -188
- 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 +183 -163
- 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/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
- package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
- package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
- package/src/engine/ecs/EntityManager.d.ts +34 -11
- package/src/engine/ecs/EntityManager.d.ts.map +1 -1
- package/src/engine/ecs/EntityManager.js +71 -42
- package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
- package/src/engine/interpolation/Interpoland.d.ts +48 -0
- package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
- package/src/engine/interpolation/Interpoland.js +49 -0
- package/src/engine/interpolation/Interpolated.d.ts +101 -0
- package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
- package/src/engine/interpolation/Interpolated.js +149 -0
- package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
- package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
- package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
- package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
- package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
- package/src/engine/interpolation/InterpolationSystem.js +233 -0
- package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
- package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
- package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
- package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
- package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
- package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
- package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
- package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
- package/src/engine/interpolation/pose_interpoland.js +27 -0
- package/src/engine/network/NetworkSession.d.ts +2 -2
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +2 -2
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
- package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
- package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
- package/src/engine/physics/PLAN.md +944 -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 +82 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +227 -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 +814 -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 +97 -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
- package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
- package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
- /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
- /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
|
@@ -51,6 +51,15 @@ export class FirstPersonSensors {
|
|
|
51
51
|
* obstacle's top finds empty space, indicating a grabbable edge.
|
|
52
52
|
*/
|
|
53
53
|
ledgeAhead: SensorHit;
|
|
54
|
+
/**
|
|
55
|
+
* True when `ledgeAhead`'s top surface is wide enough to STAND on
|
|
56
|
+
* (a standing capsule fits forward of the edge), false when it's a
|
|
57
|
+
* knife-edge / thin wall you could only hang from. Mantle reads this
|
|
58
|
+
* to refuse vaulting onto a top it would immediately fall off; ledge
|
|
59
|
+
* grab ignores it (you can hang from a thin edge). Default false so a
|
|
60
|
+
* mantle never fires on stale data.
|
|
61
|
+
*/
|
|
62
|
+
ledgeStandable: boolean;
|
|
54
63
|
/** Clear all sensor slots — call at the start of each tick. */
|
|
55
64
|
clearAll(): void;
|
|
56
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FirstPersonSensors.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensors.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IAEQ,aAAgB;IAChB,4DAA4D;IAC5D,iBAAiB;IACjB,iCAAiC;IACjC,eAA0B;IAC1B,yEAAyE;IACzE,gBAAkC;IAClC,6DAA6D;IAC7D,gBAAsB;IAG1B,mEAAmE;IACnE,cAMC;CACJ;AAED;;;;;;;;;;;;;;GAcG;AACH;IAEQ,2DAA2D;IAC3D,oBAA+B;IAC/B,4DAA4D;IAC5D,qBAAgC;IAChC,0CAA0C;IAC1C,qBAAgC;IAChC;;;;OAIG;IACH,yBAAoC;IACpC;;;;OAIG;IACH,sBAAiC;
|
|
1
|
+
{"version":3,"file":"FirstPersonSensors.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensors.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IAEQ,aAAgB;IAChB,4DAA4D;IAC5D,iBAAiB;IACjB,iCAAiC;IACjC,eAA0B;IAC1B,yEAAyE;IACzE,gBAAkC;IAClC,6DAA6D;IAC7D,gBAAsB;IAG1B,mEAAmE;IACnE,cAMC;CACJ;AAED;;;;;;;;;;;;;;GAcG;AACH;IAEQ,2DAA2D;IAC3D,oBAA+B;IAC/B,4DAA4D;IAC5D,qBAAgC;IAChC,0CAA0C;IAC1C,qBAAgC;IAChC;;;;OAIG;IACH,yBAAoC;IACpC;;;;OAIG;IACH,sBAAiC;IACjC;;;;;;;OAOG;IACH,wBAA2B;IAG/B,+DAA+D;IAC/D,iBAOC;CACJ;oBAtFmB,kCAAkC"}
|
|
@@ -1,77 +1,87 @@
|
|
|
1
|
-
import Vector3 from "../../../../core/geom/Vector3.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Single spatial-query result. Set fields when `hit` is true; ignore them
|
|
5
|
-
* otherwise. `surfaceTag` is optional — populated by future surface-tag
|
|
6
|
-
* systems (grass/wood/metal/etc.) so abilities and audio can react to the
|
|
7
|
-
* material.
|
|
8
|
-
*/
|
|
9
|
-
export class SensorHit {
|
|
10
|
-
constructor() {
|
|
11
|
-
this.hit = false;
|
|
12
|
-
/** Distance from query origin to contact point (meters). */
|
|
13
|
-
this.distance = 0;
|
|
14
|
-
/** World-space contact point. */
|
|
15
|
-
this.point = new Vector3();
|
|
16
|
-
/** Surface normal at contact (unit vector, points away from surface). */
|
|
17
|
-
this.normal = new Vector3(0, 1, 0);
|
|
18
|
-
/** Optional material/surface tag — e.g. "grass", "metal". */
|
|
19
|
-
this.surfaceTag = null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Reset to "no hit" — call before re-using the slot each tick. */
|
|
23
|
-
clear() {
|
|
24
|
-
this.hit = false;
|
|
25
|
-
this.distance = 0;
|
|
26
|
-
this.point.set(0, 0, 0);
|
|
27
|
-
this.normal.set(0, 1, 0);
|
|
28
|
-
this.surfaceTag = null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Cached spatial-query results for one first-person controller. Populated
|
|
34
|
-
* by {@link FirstPersonSensorsSystem} once per fixed step, read by
|
|
35
|
-
* abilities and (where useful) by the base controller.
|
|
36
|
-
*
|
|
37
|
-
* The point of cacheing is dedup: mantle and ledge-grab both want a
|
|
38
|
-
* "forward + up obstacle probe". Compute once, both read the cached hit.
|
|
39
|
-
*
|
|
40
|
-
* Sensor slots cleared each tick before population. Abilities should
|
|
41
|
-
* treat any `hit === false` slot as "absent" — never read distance/point
|
|
42
|
-
* when hit is false (they're stale or zero, depending on clear-mode).
|
|
43
|
-
*
|
|
44
|
-
* @author Alex Goldring
|
|
45
|
-
* @copyright Company Named Limited (c) 2026
|
|
46
|
-
*/
|
|
47
|
-
export class FirstPersonSensors {
|
|
48
|
-
constructor() {
|
|
49
|
-
/** Wall to the body-local left, probed at chest height. */
|
|
50
|
-
this.wallLeft = new SensorHit();
|
|
51
|
-
/** Wall to the body-local right, probed at chest height. */
|
|
52
|
-
this.wallRight = new SensorHit();
|
|
53
|
-
/** Wall directly in front of the body. */
|
|
54
|
-
this.wallFront = new SensorHit();
|
|
55
|
-
/**
|
|
56
|
-
* Obstacle ahead at chest/waist height — used by mantle/vault.
|
|
57
|
-
* Distance is along the body's forward direction; normal is the
|
|
58
|
-
* face of the obstacle the player is approaching.
|
|
59
|
-
*/
|
|
60
|
-
this.obstacleAhead = new SensorHit();
|
|
61
|
-
/**
|
|
62
|
-
* Ledge edge ahead — for ledge grab. Populated when the forward
|
|
63
|
-
* obstacle probe hits and a downward probe just past the
|
|
64
|
-
* obstacle's top finds empty space, indicating a grabbable edge.
|
|
65
|
-
*/
|
|
66
|
-
this.ledgeAhead = new SensorHit();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
}
|
|
77
|
-
|
|
1
|
+
import Vector3 from "../../../../core/geom/Vector3.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Single spatial-query result. Set fields when `hit` is true; ignore them
|
|
5
|
+
* otherwise. `surfaceTag` is optional — populated by future surface-tag
|
|
6
|
+
* systems (grass/wood/metal/etc.) so abilities and audio can react to the
|
|
7
|
+
* material.
|
|
8
|
+
*/
|
|
9
|
+
export class SensorHit {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.hit = false;
|
|
12
|
+
/** Distance from query origin to contact point (meters). */
|
|
13
|
+
this.distance = 0;
|
|
14
|
+
/** World-space contact point. */
|
|
15
|
+
this.point = new Vector3();
|
|
16
|
+
/** Surface normal at contact (unit vector, points away from surface). */
|
|
17
|
+
this.normal = new Vector3(0, 1, 0);
|
|
18
|
+
/** Optional material/surface tag — e.g. "grass", "metal". */
|
|
19
|
+
this.surfaceTag = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Reset to "no hit" — call before re-using the slot each tick. */
|
|
23
|
+
clear() {
|
|
24
|
+
this.hit = false;
|
|
25
|
+
this.distance = 0;
|
|
26
|
+
this.point.set(0, 0, 0);
|
|
27
|
+
this.normal.set(0, 1, 0);
|
|
28
|
+
this.surfaceTag = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Cached spatial-query results for one first-person controller. Populated
|
|
34
|
+
* by {@link FirstPersonSensorsSystem} once per fixed step, read by
|
|
35
|
+
* abilities and (where useful) by the base controller.
|
|
36
|
+
*
|
|
37
|
+
* The point of cacheing is dedup: mantle and ledge-grab both want a
|
|
38
|
+
* "forward + up obstacle probe". Compute once, both read the cached hit.
|
|
39
|
+
*
|
|
40
|
+
* Sensor slots cleared each tick before population. Abilities should
|
|
41
|
+
* treat any `hit === false` slot as "absent" — never read distance/point
|
|
42
|
+
* when hit is false (they're stale or zero, depending on clear-mode).
|
|
43
|
+
*
|
|
44
|
+
* @author Alex Goldring
|
|
45
|
+
* @copyright Company Named Limited (c) 2026
|
|
46
|
+
*/
|
|
47
|
+
export class FirstPersonSensors {
|
|
48
|
+
constructor() {
|
|
49
|
+
/** Wall to the body-local left, probed at chest height. */
|
|
50
|
+
this.wallLeft = new SensorHit();
|
|
51
|
+
/** Wall to the body-local right, probed at chest height. */
|
|
52
|
+
this.wallRight = new SensorHit();
|
|
53
|
+
/** Wall directly in front of the body. */
|
|
54
|
+
this.wallFront = new SensorHit();
|
|
55
|
+
/**
|
|
56
|
+
* Obstacle ahead at chest/waist height — used by mantle/vault.
|
|
57
|
+
* Distance is along the body's forward direction; normal is the
|
|
58
|
+
* face of the obstacle the player is approaching.
|
|
59
|
+
*/
|
|
60
|
+
this.obstacleAhead = new SensorHit();
|
|
61
|
+
/**
|
|
62
|
+
* Ledge edge ahead — for ledge grab. Populated when the forward
|
|
63
|
+
* obstacle probe hits and a downward probe just past the
|
|
64
|
+
* obstacle's top finds empty space, indicating a grabbable edge.
|
|
65
|
+
*/
|
|
66
|
+
this.ledgeAhead = new SensorHit();
|
|
67
|
+
/**
|
|
68
|
+
* True when `ledgeAhead`'s top surface is wide enough to STAND on
|
|
69
|
+
* (a standing capsule fits forward of the edge), false when it's a
|
|
70
|
+
* knife-edge / thin wall you could only hang from. Mantle reads this
|
|
71
|
+
* to refuse vaulting onto a top it would immediately fall off; ledge
|
|
72
|
+
* grab ignores it (you can hang from a thin edge). Default false so a
|
|
73
|
+
* mantle never fires on stale data.
|
|
74
|
+
*/
|
|
75
|
+
this.ledgeStandable = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Clear all sensor slots — call at the start of each tick. */
|
|
79
|
+
clearAll() {
|
|
80
|
+
this.wallLeft.clear();
|
|
81
|
+
this.wallRight.clear();
|
|
82
|
+
this.wallFront.clear();
|
|
83
|
+
this.obstacleAhead.clear();
|
|
84
|
+
this.ledgeAhead.clear();
|
|
85
|
+
this.ledgeStandable = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -58,6 +58,14 @@ export class FirstPersonSensorsSystem extends System<any, any, any, any, any> {
|
|
|
58
58
|
* @private
|
|
59
59
|
*/
|
|
60
60
|
private _populateForEntity;
|
|
61
|
+
/**
|
|
62
|
+
* Down-probe a point on (or just past) a ledge top: returns true if a
|
|
63
|
+
* surface is still there near `edgeY` — i.e. the top is wide enough for a
|
|
64
|
+
* standing capsule. A miss (or a much-lower hit) means the surface dropped
|
|
65
|
+
* away: a thin wall / lip you can only hang from, not stand on.
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
private _probeStandable;
|
|
61
69
|
/**
|
|
62
70
|
* Raycast through {@link PhysicsSystem.raycast}, populating the slot
|
|
63
71
|
* if anything is hit. Direction is assumed unit (the controller's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FirstPersonSensorsSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;IACI,cAsCC;IAnCG,wEAA4D;IAE5D;;;;;OAKG;IACH,kBAFU,iCAAiC,GAAC,IAAI,CAEpB;IAE5B;;;;;OAKG;IACH,eAFU,aAAa,GAAC,IAAI,CAEH;IAEzB;;;;;;OAMG;IACH,mBAA4B;IAE5B;;;;;;OAMG;IACH,mBAA2C;IAG/C,2CAaC;IAED,2BAIC;IAED;;OAEG;IACH,
|
|
1
|
+
{"version":3,"file":"FirstPersonSensorsSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;IACI,cAsCC;IAnCG,wEAA4D;IAE5D;;;;;OAKG;IACH,kBAFU,iCAAiC,GAAC,IAAI,CAEpB;IAE5B;;;;;OAKG;IACH,eAFU,aAAa,GAAC,IAAI,CAEH;IAEzB;;;;;;OAMG;IACH,mBAA4B;IAE5B;;;;;;OAMG;IACH,mBAA2C;IAG/C,2CAaC;IAED,2BAIC;IAED;;OAEG;IACH,2BAmFC;IAED;;;;;;OAMG;IACH,wBASC;IAED;;;;;;;;;;;OAWG;IACH,kBAiBC;CACJ;uBAhOsB,wBAAwB;0BACrB,qCAAqC;4CACnB,mCAAmC;kDAC7B,yCAAyC;8BAL7D,uCAAuC"}
|
|
@@ -1,196 +1,229 @@
|
|
|
1
|
-
import { assert } from "../../../../core/assert.js";
|
|
2
|
-
import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
|
|
3
|
-
import { PhysicsSystem } from "../../../physics/ecs/PhysicsSystem.js";
|
|
4
|
-
import { PhysicsSurfacePoint } from "../../../physics/queries/PhysicsSurfacePoint.js";
|
|
5
|
-
import { System } from "../../../ecs/System.js";
|
|
6
|
-
import { Transform } from "../../../ecs/transform/Transform.js";
|
|
7
|
-
import { FirstPersonPlayerController } from "../FirstPersonPlayerController.js";
|
|
8
|
-
import { FirstPersonPlayerControllerSystem } from "../FirstPersonPlayerControllerSystem.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Populates the {@link FirstPersonSensors} struct on each controller
|
|
12
|
-
* entity, once per fixed step. Designed to run BEFORE the main
|
|
13
|
-
* {@link FirstPersonPlayerControllerSystem} `fixedUpdate` so abilities
|
|
14
|
-
* see fresh sensor data on the same tick.
|
|
15
|
-
*
|
|
16
|
-
* Raycasts go through {@link PhysicsSystem.raycast} — BVH-backed,
|
|
17
|
-
* static + dynamic broadphase. The player's own body is excluded via a
|
|
18
|
-
* per-call filter so downward probes don't collide with the player's
|
|
19
|
-
* own capsule.
|
|
20
|
-
*
|
|
21
|
-
* BVH raycasts are cheap (~1000/frame at 60Hz is comfortable), so this
|
|
22
|
-
* system doesn't aggressively optimize. NPC controllers that don't
|
|
23
|
-
* need sensors can simply not have this system attached.
|
|
24
|
-
*
|
|
25
|
-
* Sensor data lives at `runtime.sensors` on the
|
|
26
|
-
* {@link FirstPersonPlayerControllerSystem}'s per-entity runtime.
|
|
27
|
-
*
|
|
28
|
-
* @author Alex Goldring
|
|
29
|
-
* @copyright Company Named Limited (c) 2026
|
|
30
|
-
*/
|
|
31
|
-
export class FirstPersonSensorsSystem extends System {
|
|
32
|
-
constructor() {
|
|
33
|
-
super();
|
|
34
|
-
|
|
35
|
-
this.dependencies = [FirstPersonPlayerController, Transform];
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Reference to the controller system — needed to look up each
|
|
39
|
-
* entity's per-controller runtime (which owns the sensors slot).
|
|
40
|
-
* Auto-acquired in startup; can be overridden by the caller.
|
|
41
|
-
* @type {FirstPersonPlayerControllerSystem|null}
|
|
42
|
-
*/
|
|
43
|
-
this.controllerSystem = null;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Physics system used as the spatial-query backend. Auto-acquired
|
|
47
|
-
* in startup; can be overridden. Required — sensors are unusable
|
|
48
|
-
* without it.
|
|
49
|
-
* @type {PhysicsSystem|null}
|
|
50
|
-
*/
|
|
51
|
-
this.physicsSystem = null;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Pre-allocated raycast query primitive. Refilled in place per
|
|
55
|
-
* {@link _probeRay} call so the system doesn't allocate a new
|
|
56
|
-
* `Ray3` every probe.
|
|
57
|
-
* @private
|
|
58
|
-
* @type {Ray3}
|
|
59
|
-
*/
|
|
60
|
-
this._probe_ray = new Ray3();
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Pre-allocated raycast result. Refilled in place per probe;
|
|
64
|
-
* gameplay-visible sensor fields are copied out before the next
|
|
65
|
-
* probe overwrites it.
|
|
66
|
-
* @private
|
|
67
|
-
* @type {PhysicsSurfacePoint}
|
|
68
|
-
*/
|
|
69
|
-
this._probe_hit = new PhysicsSurfacePoint();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async startup(entityManager) {
|
|
73
|
-
this.entityManager = entityManager;
|
|
74
|
-
|
|
75
|
-
// Auto-acquire collaborators if they're already in the world.
|
|
76
|
-
// Caller can still override either field before fixedUpdate runs.
|
|
77
|
-
if (this.controllerSystem === null) {
|
|
78
|
-
const cs = entityManager.getSystem(FirstPersonPlayerControllerSystem);
|
|
79
|
-
if (cs !== null) this.controllerSystem = cs;
|
|
80
|
-
}
|
|
81
|
-
if (this.physicsSystem === null) {
|
|
82
|
-
const ps = entityManager.getSystem(PhysicsSystem);
|
|
83
|
-
if (ps !== null) this.physicsSystem = ps;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
fixedUpdate(dt) {
|
|
88
|
-
const ecd = this.entityManager.dataset;
|
|
89
|
-
if (ecd === null) return;
|
|
90
|
-
ecd.traverseComponents(FirstPersonPlayerController, this._populateForEntity, this);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @private
|
|
95
|
-
*/
|
|
96
|
-
_populateForEntity(controller, entity) {
|
|
97
|
-
// Sensors live on the per-entity runtime owned by the
|
|
98
|
-
// controller's own system. Look it up by entity — no leakage
|
|
99
|
-
// through the controller component itself.
|
|
100
|
-
if (this.controllerSystem === null) return; // not wired up
|
|
101
|
-
const runtime = this.controllerSystem.getRuntime(entity);
|
|
102
|
-
if (runtime === undefined) return;
|
|
103
|
-
const sensors = runtime.sensors;
|
|
104
|
-
if (sensors === undefined || sensors === null) return;
|
|
105
|
-
|
|
106
|
-
// Sensors are useless without physics. Assert at the first
|
|
107
|
-
// populate; the misconfiguration would otherwise show up as
|
|
108
|
-
// mysteriously inert abilities.
|
|
109
|
-
assert.notEqual(this.physicsSystem, null,
|
|
110
|
-
"FirstPersonSensorsSystem requires a PhysicsSystem to be in the world. "
|
|
111
|
-
+ "Add it before this system, or set `sensorsSystem.physicsSystem` "
|
|
112
|
-
+ "explicitly.");
|
|
113
|
-
|
|
114
|
-
const ecd = this.entityManager.dataset;
|
|
115
|
-
const transform = ecd.getComponent(entity, Transform);
|
|
116
|
-
if (transform === undefined) return;
|
|
117
|
-
|
|
118
|
-
sensors.clearAll();
|
|
119
|
-
|
|
120
|
-
// Probe origin: at chest height (approx body height × 0.65).
|
|
121
|
-
const cfg = controller.config;
|
|
122
|
-
const chestY = cfg.body.height * 0.65;
|
|
123
|
-
const ox = transform.position.x;
|
|
124
|
-
const oy = transform.position.y + chestY;
|
|
125
|
-
const oz = transform.position.z;
|
|
126
|
-
|
|
127
|
-
// Body-local forward + right (engine-local axes — see DESIGN.md
|
|
128
|
-
// §5.3 for the conventions). bodyYaw lives on the runtime.
|
|
129
|
-
const yaw = runtime.bodyYaw;
|
|
130
|
-
const fx = Math.sin(yaw);
|
|
131
|
-
const fz = Math.cos(yaw);
|
|
132
|
-
const rx = Math.cos(yaw);
|
|
133
|
-
const rz = -Math.sin(yaw);
|
|
134
|
-
|
|
135
|
-
const wallProbeDist = cfg.body.radius * 2 + 0.05; // just past the capsule
|
|
136
|
-
const obstacleProbeDist = 1.2; // ~one stride
|
|
137
|
-
|
|
138
|
-
// Wall left: probe along -right.
|
|
139
|
-
this._probeRay(sensors.wallLeft, ox, oy, oz, -rx, 0, -rz, wallProbeDist, entity);
|
|
140
|
-
// Wall right: probe along +right.
|
|
141
|
-
this._probeRay(sensors.wallRight, ox, oy, oz, rx, 0, rz, wallProbeDist, entity);
|
|
142
|
-
// Wall front: probe along +forward.
|
|
143
|
-
this._probeRay(sensors.wallFront, ox, oy, oz, fx, 0, fz, wallProbeDist, entity);
|
|
144
|
-
// Obstacle ahead: same direction as wallFront but longer reach.
|
|
145
|
-
// Used by mantle/vault; the consumer compares the hit altitude to
|
|
146
|
-
// the body height to decide vault vs. mantle vs. neither.
|
|
147
|
-
this._probeRay(sensors.obstacleAhead, ox, oy, oz, fx, 0, fz, obstacleProbeDist, entity);
|
|
148
|
-
|
|
149
|
-
// Ledge ahead — only meaningful if obstacleAhead hit something
|
|
150
|
-
// around chest height: probe down from just past the top of the
|
|
151
|
-
// obstacle, look for ground.
|
|
152
|
-
if (sensors.obstacleAhead.hit) {
|
|
153
|
-
const topY = transform.position.y + cfg.body.height + 0.1;
|
|
154
|
-
const px = sensors.obstacleAhead.point.x + fx * 0.3;
|
|
155
|
-
const pz = sensors.obstacleAhead.point.z + fz * 0.3;
|
|
156
|
-
this._probeRay(
|
|
157
|
-
sensors.ledgeAhead,
|
|
158
|
-
px, topY, pz,
|
|
159
|
-
0, -1, 0,
|
|
160
|
-
cfg.body.height * 0.5,
|
|
161
|
-
entity,
|
|
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
|
-
|
|
1
|
+
import { assert } from "../../../../core/assert.js";
|
|
2
|
+
import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
|
|
3
|
+
import { PhysicsSystem } from "../../../physics/ecs/PhysicsSystem.js";
|
|
4
|
+
import { PhysicsSurfacePoint } from "../../../physics/queries/PhysicsSurfacePoint.js";
|
|
5
|
+
import { System } from "../../../ecs/System.js";
|
|
6
|
+
import { Transform } from "../../../ecs/transform/Transform.js";
|
|
7
|
+
import { FirstPersonPlayerController } from "../FirstPersonPlayerController.js";
|
|
8
|
+
import { FirstPersonPlayerControllerSystem } from "../FirstPersonPlayerControllerSystem.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Populates the {@link FirstPersonSensors} struct on each controller
|
|
12
|
+
* entity, once per fixed step. Designed to run BEFORE the main
|
|
13
|
+
* {@link FirstPersonPlayerControllerSystem} `fixedUpdate` so abilities
|
|
14
|
+
* see fresh sensor data on the same tick.
|
|
15
|
+
*
|
|
16
|
+
* Raycasts go through {@link PhysicsSystem.raycast} — BVH-backed,
|
|
17
|
+
* static + dynamic broadphase. The player's own body is excluded via a
|
|
18
|
+
* per-call filter so downward probes don't collide with the player's
|
|
19
|
+
* own capsule.
|
|
20
|
+
*
|
|
21
|
+
* BVH raycasts are cheap (~1000/frame at 60Hz is comfortable), so this
|
|
22
|
+
* system doesn't aggressively optimize. NPC controllers that don't
|
|
23
|
+
* need sensors can simply not have this system attached.
|
|
24
|
+
*
|
|
25
|
+
* Sensor data lives at `runtime.sensors` on the
|
|
26
|
+
* {@link FirstPersonPlayerControllerSystem}'s per-entity runtime.
|
|
27
|
+
*
|
|
28
|
+
* @author Alex Goldring
|
|
29
|
+
* @copyright Company Named Limited (c) 2026
|
|
30
|
+
*/
|
|
31
|
+
export class FirstPersonSensorsSystem extends System {
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
|
|
35
|
+
this.dependencies = [FirstPersonPlayerController, Transform];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reference to the controller system — needed to look up each
|
|
39
|
+
* entity's per-controller runtime (which owns the sensors slot).
|
|
40
|
+
* Auto-acquired in startup; can be overridden by the caller.
|
|
41
|
+
* @type {FirstPersonPlayerControllerSystem|null}
|
|
42
|
+
*/
|
|
43
|
+
this.controllerSystem = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Physics system used as the spatial-query backend. Auto-acquired
|
|
47
|
+
* in startup; can be overridden. Required — sensors are unusable
|
|
48
|
+
* without it.
|
|
49
|
+
* @type {PhysicsSystem|null}
|
|
50
|
+
*/
|
|
51
|
+
this.physicsSystem = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pre-allocated raycast query primitive. Refilled in place per
|
|
55
|
+
* {@link _probeRay} call so the system doesn't allocate a new
|
|
56
|
+
* `Ray3` every probe.
|
|
57
|
+
* @private
|
|
58
|
+
* @type {Ray3}
|
|
59
|
+
*/
|
|
60
|
+
this._probe_ray = new Ray3();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pre-allocated raycast result. Refilled in place per probe;
|
|
64
|
+
* gameplay-visible sensor fields are copied out before the next
|
|
65
|
+
* probe overwrites it.
|
|
66
|
+
* @private
|
|
67
|
+
* @type {PhysicsSurfacePoint}
|
|
68
|
+
*/
|
|
69
|
+
this._probe_hit = new PhysicsSurfacePoint();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async startup(entityManager) {
|
|
73
|
+
this.entityManager = entityManager;
|
|
74
|
+
|
|
75
|
+
// Auto-acquire collaborators if they're already in the world.
|
|
76
|
+
// Caller can still override either field before fixedUpdate runs.
|
|
77
|
+
if (this.controllerSystem === null) {
|
|
78
|
+
const cs = entityManager.getSystem(FirstPersonPlayerControllerSystem);
|
|
79
|
+
if (cs !== null) this.controllerSystem = cs;
|
|
80
|
+
}
|
|
81
|
+
if (this.physicsSystem === null) {
|
|
82
|
+
const ps = entityManager.getSystem(PhysicsSystem);
|
|
83
|
+
if (ps !== null) this.physicsSystem = ps;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fixedUpdate(dt) {
|
|
88
|
+
const ecd = this.entityManager.dataset;
|
|
89
|
+
if (ecd === null) return;
|
|
90
|
+
ecd.traverseComponents(FirstPersonPlayerController, this._populateForEntity, this);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
_populateForEntity(controller, entity) {
|
|
97
|
+
// Sensors live on the per-entity runtime owned by the
|
|
98
|
+
// controller's own system. Look it up by entity — no leakage
|
|
99
|
+
// through the controller component itself.
|
|
100
|
+
if (this.controllerSystem === null) return; // not wired up
|
|
101
|
+
const runtime = this.controllerSystem.getRuntime(entity);
|
|
102
|
+
if (runtime === undefined) return;
|
|
103
|
+
const sensors = runtime.sensors;
|
|
104
|
+
if (sensors === undefined || sensors === null) return;
|
|
105
|
+
|
|
106
|
+
// Sensors are useless without physics. Assert at the first
|
|
107
|
+
// populate; the misconfiguration would otherwise show up as
|
|
108
|
+
// mysteriously inert abilities.
|
|
109
|
+
assert.notEqual(this.physicsSystem, null,
|
|
110
|
+
"FirstPersonSensorsSystem requires a PhysicsSystem to be in the world. "
|
|
111
|
+
+ "Add it before this system, or set `sensorsSystem.physicsSystem` "
|
|
112
|
+
+ "explicitly.");
|
|
113
|
+
|
|
114
|
+
const ecd = this.entityManager.dataset;
|
|
115
|
+
const transform = ecd.getComponent(entity, Transform);
|
|
116
|
+
if (transform === undefined) return;
|
|
117
|
+
|
|
118
|
+
sensors.clearAll();
|
|
119
|
+
|
|
120
|
+
// Probe origin: at chest height (approx body height × 0.65).
|
|
121
|
+
const cfg = controller.config;
|
|
122
|
+
const chestY = cfg.body.height * 0.65;
|
|
123
|
+
const ox = transform.position.x;
|
|
124
|
+
const oy = transform.position.y + chestY;
|
|
125
|
+
const oz = transform.position.z;
|
|
126
|
+
|
|
127
|
+
// Body-local forward + right (engine-local axes — see DESIGN.md
|
|
128
|
+
// §5.3 for the conventions). bodyYaw lives on the runtime.
|
|
129
|
+
const yaw = runtime.bodyYaw;
|
|
130
|
+
const fx = Math.sin(yaw);
|
|
131
|
+
const fz = Math.cos(yaw);
|
|
132
|
+
const rx = Math.cos(yaw);
|
|
133
|
+
const rz = -Math.sin(yaw);
|
|
134
|
+
|
|
135
|
+
const wallProbeDist = cfg.body.radius * 2 + 0.05; // just past the capsule
|
|
136
|
+
const obstacleProbeDist = 1.2; // ~one stride
|
|
137
|
+
|
|
138
|
+
// Wall left: probe along -right.
|
|
139
|
+
this._probeRay(sensors.wallLeft, ox, oy, oz, -rx, 0, -rz, wallProbeDist, entity);
|
|
140
|
+
// Wall right: probe along +right.
|
|
141
|
+
this._probeRay(sensors.wallRight, ox, oy, oz, rx, 0, rz, wallProbeDist, entity);
|
|
142
|
+
// Wall front: probe along +forward.
|
|
143
|
+
this._probeRay(sensors.wallFront, ox, oy, oz, fx, 0, fz, wallProbeDist, entity);
|
|
144
|
+
// Obstacle ahead: same direction as wallFront but longer reach.
|
|
145
|
+
// Used by mantle/vault; the consumer compares the hit altitude to
|
|
146
|
+
// the body height to decide vault vs. mantle vs. neither.
|
|
147
|
+
this._probeRay(sensors.obstacleAhead, ox, oy, oz, fx, 0, fz, obstacleProbeDist, entity);
|
|
148
|
+
|
|
149
|
+
// Ledge ahead — only meaningful if obstacleAhead hit something
|
|
150
|
+
// around chest height: probe down from just past the top of the
|
|
151
|
+
// obstacle, look for ground.
|
|
152
|
+
if (sensors.obstacleAhead.hit) {
|
|
153
|
+
const topY = transform.position.y + cfg.body.height + 0.1;
|
|
154
|
+
const px = sensors.obstacleAhead.point.x + fx * 0.3;
|
|
155
|
+
const pz = sensors.obstacleAhead.point.z + fz * 0.3;
|
|
156
|
+
this._probeRay(
|
|
157
|
+
sensors.ledgeAhead,
|
|
158
|
+
px, topY, pz,
|
|
159
|
+
0, -1, 0,
|
|
160
|
+
cfg.body.height * 0.5,
|
|
161
|
+
entity,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Can the player STAND on that ledge, or only HANG from it? Probe
|
|
166
|
+
// straight down a capsule-width forward of the grab edge: if the top
|
|
167
|
+
// surface is still there a standing capsule fits; if it has dropped
|
|
168
|
+
// away it's a thin wall / knife-edge you could only hang from. Mantle
|
|
169
|
+
// reads this to refuse vaulting onto a top it would fall right off.
|
|
170
|
+
if (sensors.ledgeAhead.hit) {
|
|
171
|
+
const reach = cfg.body.radius * 2;
|
|
172
|
+
sensors.ledgeStandable = this._probeStandable(
|
|
173
|
+
sensors.ledgeAhead.point.x + fx * reach,
|
|
174
|
+
sensors.ledgeAhead.point.y,
|
|
175
|
+
sensors.ledgeAhead.point.z + fz * reach,
|
|
176
|
+
entity,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Down-probe a point on (or just past) a ledge top: returns true if a
|
|
183
|
+
* surface is still there near `edgeY` — i.e. the top is wide enough for a
|
|
184
|
+
* standing capsule. A miss (or a much-lower hit) means the surface dropped
|
|
185
|
+
* away: a thin wall / lip you can only hang from, not stand on.
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
_probeStandable(px, edgeY, pz, excludeEntity) {
|
|
189
|
+
const ray = this._probe_ray;
|
|
190
|
+
ray.setOrigin(px, edgeY + 0.2, pz);
|
|
191
|
+
ray.setDirection(0, -1, 0);
|
|
192
|
+
ray.tMax = 0.4;
|
|
193
|
+
const filter = (e) => e !== excludeEntity;
|
|
194
|
+
const out = this._probe_hit;
|
|
195
|
+
if (!this.physicsSystem.raycast(ray, out, filter)) return false;
|
|
196
|
+
return out.position.y > edgeY - 0.15; // surface still up near the edge height
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Raycast through {@link PhysicsSystem.raycast}, populating the slot
|
|
201
|
+
* if anything is hit. Direction is assumed unit (the controller's
|
|
202
|
+
* callers pass body-local unit axes).
|
|
203
|
+
*
|
|
204
|
+
* The raycast now writes the AABB face normal into `_probe_hit.normal`,
|
|
205
|
+
* which is exact for AABB-shaped colliders (the common case in our gym)
|
|
206
|
+
* and a stable approximation elsewhere. Future narrowphase refinement
|
|
207
|
+
* will replace this with the true shape normal at the same call site.
|
|
208
|
+
*
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
_probeRay(slot, ox, oy, oz, dx, dy, dz, maxDist, excludeEntity) {
|
|
212
|
+
// Exclude the player's own body from the result — downward
|
|
213
|
+
// probes from chest start inside the capsule, would otherwise
|
|
214
|
+
// hit the capsule's bottom hemisphere.
|
|
215
|
+
const ray = this._probe_ray;
|
|
216
|
+
ray.setOrigin(ox, oy, oz);
|
|
217
|
+
ray.setDirection(dx, dy, dz);
|
|
218
|
+
ray.tMax = maxDist;
|
|
219
|
+
const filter = (e) => e !== excludeEntity;
|
|
220
|
+
const out = this._probe_hit;
|
|
221
|
+
if (!this.physicsSystem.raycast(ray, out, filter)) return;
|
|
222
|
+
|
|
223
|
+
slot.hit = true;
|
|
224
|
+
slot.distance = out.t;
|
|
225
|
+
slot.point.set(out.position.x, out.position.y, out.position.z);
|
|
226
|
+
slot.normal.set(out.normal.x, out.normal.y, out.normal.z);
|
|
227
|
+
slot.surfaceTag = null; // future: surface-tag lookup
|
|
228
|
+
}
|
|
229
|
+
}
|