@waica/engine 0.24.0 → 0.26.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/README.md +79 -1
- package/dist/anchored-pieces.d.ts +3 -2
- package/dist/anchored-pieces.js +12 -8
- package/dist/archetype.d.ts +2 -2
- package/dist/assets/asset-loader.d.ts +28 -5
- package/dist/assets/asset-loader.js +63 -7
- package/dist/assets/model-backend.d.ts +21 -0
- package/dist/assets/model-backend.js +21 -0
- package/dist/assets/model-cache.d.ts +47 -0
- package/dist/assets/model-cache.js +117 -0
- package/dist/audio/audio-subsystem.d.ts +6 -1
- package/dist/audio/audio-subsystem.js +8 -15
- package/dist/audio/placement.d.ts +41 -0
- package/dist/audio/placement.js +21 -0
- package/dist/audio/spatial.d.ts +20 -0
- package/dist/audio/spatial.js +26 -0
- package/dist/audio/types.d.ts +7 -1
- package/dist/camera-projection.d.ts +38 -0
- package/dist/camera-projection.js +44 -0
- package/dist/camera-rig.d.ts +48 -0
- package/dist/camera-rig.js +98 -0
- package/dist/camera.d.ts +20 -3
- package/dist/camera.js +3 -1
- package/dist/component.d.ts +38 -2
- package/dist/component.js +3 -0
- package/dist/components/animated-sprite.d.ts +8 -1
- package/dist/components/animated-sprite.js +16 -1
- package/dist/components/collider.d.ts +89 -0
- package/dist/components/collider.js +57 -0
- package/dist/components/dynamic-body.d.ts +2 -1
- package/dist/components/dynamic-body.js +1 -0
- package/dist/components/hitbox.d.ts +2 -1
- package/dist/components/hitbox.js +1 -0
- package/dist/components/light.d.ts +69 -0
- package/dist/components/light.js +60 -0
- package/dist/components/model.d.ts +63 -0
- package/dist/components/model.js +117 -0
- package/dist/components/particle-emitter.d.ts +8 -1
- package/dist/components/particle-emitter.js +14 -0
- package/dist/components/point-light.d.ts +75 -0
- package/dist/components/point-light.js +102 -0
- package/dist/components/rigid-body.d.ts +114 -0
- package/dist/components/rigid-body.js +83 -0
- package/dist/components/solid.d.ts +2 -1
- package/dist/components/solid.js +1 -0
- package/dist/components/sprite.d.ts +8 -1
- package/dist/components/sprite.js +16 -1
- package/dist/components/sun.d.ts +46 -0
- package/dist/components/sun.js +74 -0
- package/dist/components/tilemap.d.ts +2 -1
- package/dist/components/tilemap.js +7 -0
- package/dist/entity-transform.d.ts +18 -0
- package/dist/entity-transform.js +17 -0
- package/dist/frame-quad.d.ts +21 -0
- package/dist/frame-quad.js +50 -0
- package/dist/game.d.ts +59 -15
- package/dist/game.js +100 -75
- package/dist/index.d.ts +39 -4
- package/dist/index.js +18 -1
- package/dist/light-field.d.ts +78 -0
- package/dist/light-field.js +145 -0
- package/dist/light-map-material.d.ts +40 -0
- package/dist/light-map-material.js +203 -0
- package/dist/light-map.d.ts +46 -0
- package/dist/light-map.js +132 -0
- package/dist/lit-frame.d.ts +48 -0
- package/dist/lit-frame.js +174 -0
- package/dist/occluder-grid.d.ts +19 -0
- package/dist/occluder-grid.js +50 -0
- package/dist/particle-batch.js +3 -2
- package/dist/physics-3d/body-contacts.d.ts +10 -0
- package/dist/physics-3d/body-contacts.js +59 -0
- package/dist/physics-3d/body-sync.d.ts +52 -0
- package/dist/physics-3d/body-sync.js +153 -0
- package/dist/physics-3d/character-controller.d.ts +44 -0
- package/dist/physics-3d/character-controller.js +126 -0
- package/dist/physics-3d/physics-host.d.ts +86 -0
- package/dist/physics-3d/physics-host.js +174 -0
- package/dist/physics-3d/physics-world.d.ts +65 -0
- package/dist/physics-3d/physics-world.js +158 -0
- package/dist/physics-3d/rapier-module.d.ts +17 -0
- package/dist/physics-3d/rapier-module.js +19 -0
- package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
- package/dist/physics-3d/sensor-dispatch.js +77 -0
- package/dist/physics-3d/world-queries.d.ts +39 -0
- package/dist/physics-3d/world-queries.js +63 -0
- package/dist/pointer-raycast.d.ts +17 -0
- package/dist/pointer-raycast.js +48 -0
- package/dist/pointer.d.ts +27 -6
- package/dist/pointer.js +43 -17
- package/dist/post-effects.d.ts +33 -0
- package/dist/post-effects.js +48 -0
- package/dist/post-pass.d.ts +23 -0
- package/dist/post-pass.js +93 -0
- package/dist/render-layers.d.ts +18 -0
- package/dist/render-layers.js +19 -0
- package/dist/render-output.d.ts +7 -0
- package/dist/render-output.js +17 -1
- package/dist/runtime-bridge.d.ts +1 -1
- package/dist/runtime-inspection.d.ts +14 -0
- package/dist/runtime-inspection.js +12 -2
- package/dist/runtime-lighting-snapshot.d.ts +67 -0
- package/dist/runtime-lighting-snapshot.js +77 -0
- package/dist/runtime-physics-snapshot.d.ts +41 -0
- package/dist/runtime-physics-snapshot.js +45 -0
- package/dist/runtime-view-snapshot.d.ts +25 -0
- package/dist/runtime-view-snapshot.js +30 -0
- package/dist/scene-camera-3d.d.ts +66 -0
- package/dist/scene-camera-3d.js +117 -0
- package/dist/scene-lighting.d.ts +52 -0
- package/dist/scene-lighting.js +107 -0
- package/dist/scene-lights-3d.d.ts +17 -0
- package/dist/scene-lights-3d.js +27 -0
- package/dist/scene-render-options.d.ts +51 -0
- package/dist/scene-render-options.js +126 -0
- package/dist/scene-simulation.d.ts +22 -0
- package/dist/scene-simulation.js +29 -0
- package/dist/scene-space.d.ts +50 -0
- package/dist/scene-space.js +103 -0
- package/dist/scene.d.ts +41 -2
- package/dist/scene.js +11 -4
- package/dist/simulation-phases.d.ts +9 -0
- package/dist/simulation-phases.js +19 -0
- package/dist/spatial-query-3d.d.ts +48 -0
- package/dist/spatial-query-3d.js +96 -0
- package/dist/spatial-query.d.ts +18 -4
- package/dist/spatial-query.js +57 -1
- package/dist/sprite-batch.js +5 -1
- package/package.json +2 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { RIGID_BODY_TYPES, RigidBody } from '../components/rigid-body.js';
|
|
2
|
+
/** The dimensions a collider needs are positive finite numbers (Rapier does not defend itself against anything else). */
|
|
3
|
+
function positive(...values) {
|
|
4
|
+
return values.every((value) => Number.isFinite(value) && value > 0);
|
|
5
|
+
}
|
|
6
|
+
function shapeProblem(collider) {
|
|
7
|
+
switch (collider.shape) {
|
|
8
|
+
case 'box':
|
|
9
|
+
return Array.isArray(collider.size) && collider.size.length === 3 && positive(...collider.size)
|
|
10
|
+
? null
|
|
11
|
+
: 'a box needs a size of three positive numbers';
|
|
12
|
+
case 'sphere':
|
|
13
|
+
return positive(collider.radius) ? null : 'a sphere needs a positive radius';
|
|
14
|
+
case 'capsule':
|
|
15
|
+
return positive(collider.radius, collider.height) ? null : 'a capsule needs a positive radius and height';
|
|
16
|
+
default:
|
|
17
|
+
return `unknown shape "${String(collider.shape)}"`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const isVec3 = (value) => Array.isArray(value) && value.length === 3 && value.every(Number.isFinite);
|
|
21
|
+
const isUnitInterval = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
22
|
+
/** Why this collider cannot become a Rapier shape, or null when it can. */
|
|
23
|
+
export function colliderProblem(collider) {
|
|
24
|
+
if (!isVec3(collider.offset))
|
|
25
|
+
return 'the offset needs three finite numbers';
|
|
26
|
+
if (!isUnitInterval(collider.friction) || !isUnitInterval(collider.restitution))
|
|
27
|
+
return 'friction and restitution need numbers from 0 to 1';
|
|
28
|
+
const problem = shapeProblem(collider);
|
|
29
|
+
if (problem !== null)
|
|
30
|
+
return problem;
|
|
31
|
+
// A shorter capsule would silently become a sphere of diameter 2 * radius, taller than the height declared.
|
|
32
|
+
if (collider.shape === 'capsule' && collider.height < 2 * collider.radius)
|
|
33
|
+
return 'a capsule needs a height of at least twice its radius';
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const MIN_MASS = RigidBody.params.mass.min;
|
|
37
|
+
/** Why this RigidBody cannot become a Rapier body, or null when it can (the mirror of `colliderProblem`). */
|
|
38
|
+
export function rigidBodyProblem(rigid) {
|
|
39
|
+
if (!RIGID_BODY_TYPES.includes(rigid.type))
|
|
40
|
+
return `type must be ${RIGID_BODY_TYPES.join(' or ')}; got ${JSON.stringify(rigid.type)}`;
|
|
41
|
+
if (!Number.isFinite(rigid.mass) || rigid.mass < MIN_MASS)
|
|
42
|
+
return `mass must be at least ${MIN_MASS}`;
|
|
43
|
+
if (!Number.isFinite(rigid.gravityScale))
|
|
44
|
+
return 'gravityScale must be a finite number';
|
|
45
|
+
for (const [name, value] of [['linearDamping', rigid.linearDamping], ['angularDamping', rigid.angularDamping]]) {
|
|
46
|
+
if (!Number.isFinite(value) || value < 0)
|
|
47
|
+
return `${name} must be at least 0`;
|
|
48
|
+
}
|
|
49
|
+
if (!isVec3(rigid.velocity))
|
|
50
|
+
return 'velocity must be three finite numbers';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The Rapier shape of a collider at the entity's current scale (inference 9,
|
|
55
|
+
* issue #159): a box's size and a capsule's height scale per axis, a sphere's
|
|
56
|
+
* radius by the largest axis, a capsule's radius by the larger of x and z.
|
|
57
|
+
* A capsule's `height` is end to end, so its straight part is the rest.
|
|
58
|
+
*/
|
|
59
|
+
function shapeDesc(R, collider, scale) {
|
|
60
|
+
const [sx, sy, sz] = [Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z)];
|
|
61
|
+
switch (collider.shape) {
|
|
62
|
+
case 'sphere':
|
|
63
|
+
return R.ColliderDesc.ball(collider.radius * Math.max(sx, sy, sz));
|
|
64
|
+
case 'capsule': {
|
|
65
|
+
const radius = collider.radius * Math.max(sx, sz);
|
|
66
|
+
return R.ColliderDesc.capsule(Math.max(0, (collider.height * sy) / 2 - radius), radius);
|
|
67
|
+
}
|
|
68
|
+
case 'box': {
|
|
69
|
+
const [x, y, z] = collider.size;
|
|
70
|
+
return R.ColliderDesc.cuboid((x * sx) / 2, (y * sy) / 2, (z * sz) / 2);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function bodyKind(rigid) {
|
|
75
|
+
if (!rigid)
|
|
76
|
+
return 'fixed';
|
|
77
|
+
return rigid.type === 'kinematic' ? 'kinematic' : 'dynamic';
|
|
78
|
+
}
|
|
79
|
+
function bodyDesc(R, entity, rigid) {
|
|
80
|
+
const kind = bodyKind(rigid);
|
|
81
|
+
const desc = kind === 'dynamic' ? R.RigidBodyDesc.dynamic() : kind === 'kinematic' ? R.RigidBodyDesc.kinematicPositionBased() : R.RigidBodyDesc.fixed();
|
|
82
|
+
const { x, y, z } = entity.position;
|
|
83
|
+
const q = entity.node.quaternion;
|
|
84
|
+
desc.setTranslation(x, y, z).setRotation({ x: q.x, y: q.y, z: q.z, w: q.w });
|
|
85
|
+
if (kind === 'dynamic' && rigid) {
|
|
86
|
+
const [vx, vy, vz] = rigid.velocity;
|
|
87
|
+
desc
|
|
88
|
+
.setGravityScale(rigid.gravityScale)
|
|
89
|
+
.setLinearDamping(rigid.linearDamping)
|
|
90
|
+
.setAngularDamping(rigid.angularDamping)
|
|
91
|
+
.setLinvel(vx, vy, vz);
|
|
92
|
+
if (rigid.lockRotations)
|
|
93
|
+
desc.lockRotations();
|
|
94
|
+
}
|
|
95
|
+
if (kind === 'kinematic')
|
|
96
|
+
desc.lockRotations();
|
|
97
|
+
return desc;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Which pairs Rapier computes contacts for. Its default leaves out every pair
|
|
101
|
+
* with a kinematic body and no dynamic one, so a character standing on the
|
|
102
|
+
* floor or against a wall would never hear `onBodyContact`: solid colliders
|
|
103
|
+
* add the kinematic-fixed and kinematic-kinematic pairs on both sides. A
|
|
104
|
+
* sensor on a fixed or kinematic body must still see the others (ALL leaves
|
|
105
|
+
* fixed-fixed out).
|
|
106
|
+
*/
|
|
107
|
+
function activeCollisionTypes(R, sensor) {
|
|
108
|
+
const { ActiveCollisionTypes: types } = R;
|
|
109
|
+
return sensor ? types.ALL | types.FIXED_FIXED : types.DEFAULT | types.KINEMATIC_FIXED | types.KINEMATIC_KINEMATIC;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Builds the entity's body and collider from its components and puts them in
|
|
113
|
+
* the world. Static bodies are created once and never follow the entity; a
|
|
114
|
+
* dynamic body is Rapier's from here on. The caller has checked the parts
|
|
115
|
+
* with `colliderProblem` and `rigidBodyProblem`.
|
|
116
|
+
*/
|
|
117
|
+
export function createBody(R, world, parts) {
|
|
118
|
+
const { entity, collider, rigid } = parts;
|
|
119
|
+
const scale = entity.scale;
|
|
120
|
+
const body = world.createRigidBody(bodyDesc(R, entity, rigid));
|
|
121
|
+
const [ox, oy, oz] = collider.offset;
|
|
122
|
+
const desc = shapeDesc(R, collider, scale)
|
|
123
|
+
.setTranslation(ox * scale.x, oy * scale.y, oz * scale.z)
|
|
124
|
+
.setSensor(collider.sensor)
|
|
125
|
+
.setFriction(collider.friction)
|
|
126
|
+
.setRestitution(collider.restitution)
|
|
127
|
+
.setActiveCollisionTypes(activeCollisionTypes(R, collider.sensor));
|
|
128
|
+
if (rigid)
|
|
129
|
+
desc.setMass(rigid.mass);
|
|
130
|
+
const shape = world.createCollider(desc, body);
|
|
131
|
+
return { entity, collider, rigid, kind: bodyKind(rigid), sensor: collider.sensor, body, shape, grounded: false, vy: 0, velocity: { x: 0, y: 0, z: 0 } };
|
|
132
|
+
}
|
|
133
|
+
/** A body's linear velocity: Rapier's for a dynamic one, the measured movement of a kinematic one, none for a fixed one. */
|
|
134
|
+
export function bodyVelocity(record) {
|
|
135
|
+
if (record.kind === 'kinematic')
|
|
136
|
+
return { ...record.velocity };
|
|
137
|
+
if (record.kind === 'dynamic') {
|
|
138
|
+
const { x, y, z } = record.body.linvel();
|
|
139
|
+
return { x, y, z };
|
|
140
|
+
}
|
|
141
|
+
return { x: 0, y: 0, z: 0 };
|
|
142
|
+
}
|
|
143
|
+
/** After a step: a moving body's pose becomes its entity's (a sleeping dynamic body has not moved). */
|
|
144
|
+
export function syncBodyPose(record) {
|
|
145
|
+
if (record.kind === 'fixed' || (record.kind === 'dynamic' && record.body.isSleeping()))
|
|
146
|
+
return;
|
|
147
|
+
const t = record.body.translation();
|
|
148
|
+
record.entity.position.set(t.x, t.y, t.z);
|
|
149
|
+
if (record.kind === 'kinematic')
|
|
150
|
+
return;
|
|
151
|
+
const q = record.body.rotation();
|
|
152
|
+
record.entity.node.quaternion.set(q.x, q.y, q.z, q.w);
|
|
153
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BodyRecord } from './body-sync.js';
|
|
2
|
+
import type { RapierModule, RapierWorld } from './rapier-module.js';
|
|
3
|
+
/**
|
|
4
|
+
* Moves the kinematic bodies of a world through Rapier's character controller
|
|
5
|
+
* (ADR 0028): one controller per world, tuned for each body from its
|
|
6
|
+
* RigidBody before it moves. The controller applies no gravity, so the engine
|
|
7
|
+
* integrates each body's vertical velocity here (inference 19, issue #159).
|
|
8
|
+
*/
|
|
9
|
+
export declare class CharacterMotion {
|
|
10
|
+
private readonly R;
|
|
11
|
+
private readonly world;
|
|
12
|
+
private controller;
|
|
13
|
+
private profile;
|
|
14
|
+
constructor(R: RapierModule, world: RapierWorld);
|
|
15
|
+
/**
|
|
16
|
+
* Puts every kinematic body where game code moved its entity since the last
|
|
17
|
+
* step (a teleport), before any of them moves: the controller reads the
|
|
18
|
+
* colliders' poses, which Rapier updates only inside a step, so they are
|
|
19
|
+
* propagated here, and a teleported body starts over (no fall speed, not
|
|
20
|
+
* grounded).
|
|
21
|
+
*/
|
|
22
|
+
follow(records: Iterable<BodyRecord>): void;
|
|
23
|
+
/** Moves one kinematic body for this Simulation Step; Rapier applies the new position at `world.step()`. */
|
|
24
|
+
move(record: BodyRecord): void;
|
|
25
|
+
/** Releases the controller; the world is freed right after. */
|
|
26
|
+
free(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Whether what holds the body up, where this step leaves it, is ground it
|
|
29
|
+
* may stand on: Rapier's `computedGrounded` counts any contact short of a
|
|
30
|
+
* wall, `maxSlope` included or not, so a body would stand on, and jump off,
|
|
31
|
+
* a slope it cannot climb. A short cast of its own collider straight down
|
|
32
|
+
* finds the surface under it and its outward normal; ground is one within
|
|
33
|
+
* `maxSlope` of up. On a steeper slope gravity keeps pulling the body down
|
|
34
|
+
* it. (The controller records no collision for a resting contact, so the
|
|
35
|
+
* movement's own collisions cannot tell the slope.)
|
|
36
|
+
*/
|
|
37
|
+
private standsWithin;
|
|
38
|
+
/** The entity is where the body put it, unless game code moved it since: then the body goes there (a teleport). */
|
|
39
|
+
private followEntity;
|
|
40
|
+
/** After the move: standing ends a fall, and a ceiling ends a rise. */
|
|
41
|
+
private settleVertical;
|
|
42
|
+
private tuned;
|
|
43
|
+
private created;
|
|
44
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { SIMULATION_STEP } from '../fixed-step.js';
|
|
2
|
+
/** The gap Rapier's controller keeps between the character and what it touches. */
|
|
3
|
+
const SKIN = 0.01;
|
|
4
|
+
/** The narrowest ledge an autostep may land on. */
|
|
5
|
+
const MIN_STEP_WIDTH = 0.2;
|
|
6
|
+
/** How far below the body the probe for the ground it stands on looks: past the skin, with room for a slope's slant. */
|
|
7
|
+
const GROUND_PROBE = SKIN * 4;
|
|
8
|
+
const DOWN = { x: 0, y: -1, z: 0 };
|
|
9
|
+
const DEGREES = Math.PI / 180;
|
|
10
|
+
const profileOf = (rigid) => `${rigid.stepHeight}/${rigid.maxSlope}/${rigid.snapDistance}`;
|
|
11
|
+
/**
|
|
12
|
+
* Moves the kinematic bodies of a world through Rapier's character controller
|
|
13
|
+
* (ADR 0028): one controller per world, tuned for each body from its
|
|
14
|
+
* RigidBody before it moves. The controller applies no gravity, so the engine
|
|
15
|
+
* integrates each body's vertical velocity here (inference 19, issue #159).
|
|
16
|
+
*/
|
|
17
|
+
export class CharacterMotion {
|
|
18
|
+
R;
|
|
19
|
+
world;
|
|
20
|
+
controller = null;
|
|
21
|
+
profile = null;
|
|
22
|
+
constructor(R, world) {
|
|
23
|
+
this.R = R;
|
|
24
|
+
this.world = world;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Puts every kinematic body where game code moved its entity since the last
|
|
28
|
+
* step (a teleport), before any of them moves: the controller reads the
|
|
29
|
+
* colliders' poses, which Rapier updates only inside a step, so they are
|
|
30
|
+
* propagated here, and a teleported body starts over (no fall speed, not
|
|
31
|
+
* grounded).
|
|
32
|
+
*/
|
|
33
|
+
follow(records) {
|
|
34
|
+
let teleported = false;
|
|
35
|
+
for (const record of records) {
|
|
36
|
+
if (record.kind !== 'kinematic' || !this.followEntity(record))
|
|
37
|
+
continue;
|
|
38
|
+
teleported = true;
|
|
39
|
+
record.vy = 0;
|
|
40
|
+
record.grounded = false;
|
|
41
|
+
}
|
|
42
|
+
if (teleported)
|
|
43
|
+
this.world.propagateModifiedBodyPositionsToColliders();
|
|
44
|
+
}
|
|
45
|
+
/** Moves one kinematic body for this Simulation Step; Rapier applies the new position at `world.step()`. */
|
|
46
|
+
move(record) {
|
|
47
|
+
const { rigid, body, shape } = record;
|
|
48
|
+
if (!rigid)
|
|
49
|
+
return;
|
|
50
|
+
// A body standing on the ground feels no gravity: pulling it into the floor every step makes it sink.
|
|
51
|
+
if (!(record.grounded && record.vy <= 0))
|
|
52
|
+
record.vy += this.world.gravity.y * rigid.gravityScale * SIMULATION_STEP;
|
|
53
|
+
const desired = { x: rigid.desiredVelocity.x * SIMULATION_STEP, y: record.vy * SIMULATION_STEP, z: rigid.desiredVelocity.z * SIMULATION_STEP };
|
|
54
|
+
const controller = this.tuned(rigid);
|
|
55
|
+
controller.computeColliderMovement(shape, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS);
|
|
56
|
+
const moved = controller.computedMovement();
|
|
57
|
+
const at = body.translation();
|
|
58
|
+
body.setNextKinematicTranslation({ x: at.x + moved.x, y: at.y + moved.y, z: at.z + moved.z });
|
|
59
|
+
record.grounded = controller.computedGrounded() && this.standsWithin(record, moved, rigid.maxSlope);
|
|
60
|
+
record.velocity = { x: moved.x / SIMULATION_STEP, y: moved.y / SIMULATION_STEP, z: moved.z / SIMULATION_STEP };
|
|
61
|
+
this.settleVertical(record, desired.y, moved.y);
|
|
62
|
+
}
|
|
63
|
+
/** Releases the controller; the world is freed right after. */
|
|
64
|
+
free() {
|
|
65
|
+
if (this.controller)
|
|
66
|
+
this.world.removeCharacterController(this.controller);
|
|
67
|
+
this.controller = null;
|
|
68
|
+
this.profile = null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether what holds the body up, where this step leaves it, is ground it
|
|
72
|
+
* may stand on: Rapier's `computedGrounded` counts any contact short of a
|
|
73
|
+
* wall, `maxSlope` included or not, so a body would stand on, and jump off,
|
|
74
|
+
* a slope it cannot climb. A short cast of its own collider straight down
|
|
75
|
+
* finds the surface under it and its outward normal; ground is one within
|
|
76
|
+
* `maxSlope` of up. On a steeper slope gravity keeps pulling the body down
|
|
77
|
+
* it. (The controller records no collision for a resting contact, so the
|
|
78
|
+
* movement's own collisions cannot tell the slope.)
|
|
79
|
+
*/
|
|
80
|
+
standsWithin({ shape }, moved, maxSlope) {
|
|
81
|
+
const at = shape.translation();
|
|
82
|
+
const next = { x: at.x + moved.x, y: at.y + moved.y, z: at.z + moved.z };
|
|
83
|
+
const flags = this.R.QueryFilterFlags.EXCLUDE_SENSORS;
|
|
84
|
+
const hit = this.world.castShape(next, shape.rotation(), DOWN, shape.shape, 0, GROUND_PROBE, true, flags, undefined, shape);
|
|
85
|
+
return hit !== null && hit.normal1.y >= Math.cos(maxSlope * DEGREES);
|
|
86
|
+
}
|
|
87
|
+
/** The entity is where the body put it, unless game code moved it since: then the body goes there (a teleport). */
|
|
88
|
+
followEntity({ entity, body }) {
|
|
89
|
+
const at = body.translation();
|
|
90
|
+
const { x, y, z } = entity.position;
|
|
91
|
+
if (x === at.x && y === at.y && z === at.z)
|
|
92
|
+
return false;
|
|
93
|
+
body.setTranslation({ x, y, z }, true);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** After the move: standing ends a fall, and a ceiling ends a rise. */
|
|
97
|
+
settleVertical(record, wanted, got) {
|
|
98
|
+
if (record.grounded && record.vy <= 0)
|
|
99
|
+
record.vy = 0;
|
|
100
|
+
else if (record.vy > 0 && got < wanted / 2)
|
|
101
|
+
record.vy = 0;
|
|
102
|
+
}
|
|
103
|
+
tuned(rigid) {
|
|
104
|
+
const controller = (this.controller ??= this.created());
|
|
105
|
+
const profile = profileOf(rigid);
|
|
106
|
+
if (profile === this.profile)
|
|
107
|
+
return controller;
|
|
108
|
+
this.profile = profile;
|
|
109
|
+
if (rigid.stepHeight > 0)
|
|
110
|
+
controller.enableAutostep(rigid.stepHeight, MIN_STEP_WIDTH, true);
|
|
111
|
+
else
|
|
112
|
+
controller.disableAutostep();
|
|
113
|
+
controller.setMaxSlopeClimbAngle(rigid.maxSlope * DEGREES);
|
|
114
|
+
if (rigid.snapDistance > 0)
|
|
115
|
+
controller.enableSnapToGround(rigid.snapDistance);
|
|
116
|
+
else
|
|
117
|
+
controller.disableSnapToGround();
|
|
118
|
+
return controller;
|
|
119
|
+
}
|
|
120
|
+
created() {
|
|
121
|
+
const controller = this.world.createCharacterController(SKIN);
|
|
122
|
+
controller.setUp({ x: 0, y: 1, z: 0 });
|
|
123
|
+
controller.setApplyImpulsesToDynamicBodies(true);
|
|
124
|
+
return controller;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Entity } from '../entity.js';
|
|
2
|
+
import type { Game } from '../game.js';
|
|
3
|
+
import type { Vec3Json } from '../scene-camera-3d.js';
|
|
4
|
+
import { type ResolvedSceneSimulation } from '../scene-simulation.js';
|
|
5
|
+
import type { SceneSpace } from '../scene-space.js';
|
|
6
|
+
import { type BodyRecord } from './body-sync.js';
|
|
7
|
+
import { type PhysicsBackend } from './rapier-module.js';
|
|
8
|
+
import { PhysicsWorld } from './physics-world.js';
|
|
9
|
+
/** `loading` until the Rapier module arrives, `ready` once the live 3D scene has its world, `failed` for good if the load failed. */
|
|
10
|
+
export type PhysicsState = 'loading' | 'ready' | 'failed';
|
|
11
|
+
/**
|
|
12
|
+
* The Game's physics (ADR 0028): loads Rapier the first time a 3D scene loads
|
|
13
|
+
* and counts that load as a pending asset, so Assets Ready, `game.ready()` and
|
|
14
|
+
* the Run Session's wait all cover it; keeps one `PhysicsWorld` per live 3D
|
|
15
|
+
* scene; and answers the components' questions by entity. A 2D scene never
|
|
16
|
+
* triggers the load.
|
|
17
|
+
*/
|
|
18
|
+
export declare class PhysicsHost {
|
|
19
|
+
private readonly game;
|
|
20
|
+
private readonly backend?;
|
|
21
|
+
private module;
|
|
22
|
+
private loading;
|
|
23
|
+
private failure;
|
|
24
|
+
private gravity;
|
|
25
|
+
private live;
|
|
26
|
+
private disposed;
|
|
27
|
+
constructor(game: Game, backend?: PhysicsBackend | undefined);
|
|
28
|
+
/** The live 3D scene's world; null for a 2D scene, while the module loads and after it failed. */
|
|
29
|
+
get world(): PhysicsWorld | null;
|
|
30
|
+
/** The state a 3D scene reports; for a 2D scene it is meaningless and callers do not ask. */
|
|
31
|
+
get state(): PhysicsState;
|
|
32
|
+
/** The gravity the live scene runs with: its world's, or the declared one while the world does not exist yet. */
|
|
33
|
+
get sceneGravity(): Vec3Json;
|
|
34
|
+
/**
|
|
35
|
+
* Adopts a scene's space (called by `setSceneRender`): a 3D scene starts the
|
|
36
|
+
* load, gets its world at once when the module is here, or hears about the
|
|
37
|
+
* load that already failed.
|
|
38
|
+
*/
|
|
39
|
+
enterScene(space: SceneSpace): void;
|
|
40
|
+
/** Adopts a scene's gravity (called by `setSceneSimulation`). */
|
|
41
|
+
setSimulation(simulation: ResolvedSceneSimulation): void;
|
|
42
|
+
/** Frees the world after the scene's entities are destroyed; the module stays. */
|
|
43
|
+
unloadScene(): void;
|
|
44
|
+
dispose(): void;
|
|
45
|
+
/**
|
|
46
|
+
* `game.ready()`: the renderer's promise, and for a Game whose live scene is
|
|
47
|
+
* 3D also the Rapier load (which rejects, naming the package, when it fails).
|
|
48
|
+
*/
|
|
49
|
+
whenReady(rendering: Promise<void>): Promise<void>;
|
|
50
|
+
/** One Simulation Step of the world, when there is one. */
|
|
51
|
+
step(): void;
|
|
52
|
+
attach(entity: Entity): void;
|
|
53
|
+
detach(entity: Entity): void;
|
|
54
|
+
recordOf(entity: Entity): BodyRecord | undefined;
|
|
55
|
+
velocityOf(entity: Entity): {
|
|
56
|
+
x: number;
|
|
57
|
+
y: number;
|
|
58
|
+
z: number;
|
|
59
|
+
} | null;
|
|
60
|
+
/** Sets a dynamic body's velocity; for a kinematic one, x and z become its desired velocity and y its vertical velocity. */
|
|
61
|
+
setVelocityOf(entity: Entity, velocity: {
|
|
62
|
+
x: number;
|
|
63
|
+
y: number;
|
|
64
|
+
z: number;
|
|
65
|
+
}): void;
|
|
66
|
+
groundedOf(entity: Entity): boolean;
|
|
67
|
+
/** Launches a kinematic body upward when it stands on something; whether it did. */
|
|
68
|
+
jumpOf(entity: Entity, speed: number): boolean;
|
|
69
|
+
applyImpulseTo(entity: Entity, impulse: {
|
|
70
|
+
x: number;
|
|
71
|
+
y: number;
|
|
72
|
+
z: number;
|
|
73
|
+
}): void;
|
|
74
|
+
private begin;
|
|
75
|
+
private arrive;
|
|
76
|
+
private fail;
|
|
77
|
+
/** Tells the Runtime Bridge the scene it is driving cannot simulate (the mirror of `render-backend-failed`). */
|
|
78
|
+
private report;
|
|
79
|
+
/**
|
|
80
|
+
* The world of the live 3D scene, with a body for every Collider already
|
|
81
|
+
* spawned (a load that finished late). A body that cannot be built costs
|
|
82
|
+
* that entity its body, never the rest of the scene or the load itself
|
|
83
|
+
* (spawnFromJson's rule for a throwing component).
|
|
84
|
+
*/
|
|
85
|
+
private createWorld;
|
|
86
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { activeRuntimeBridgeHook } from '../runtime-bridge.js';
|
|
2
|
+
import { DEFAULT_GRAVITY } from '../scene-simulation.js';
|
|
3
|
+
import { bodyVelocity } from './body-sync.js';
|
|
4
|
+
import { loadRapier, RAPIER_PACKAGE } from './rapier-module.js';
|
|
5
|
+
import { PhysicsWorld } from './physics-world.js';
|
|
6
|
+
/**
|
|
7
|
+
* The Game's physics (ADR 0028): loads Rapier the first time a 3D scene loads
|
|
8
|
+
* and counts that load as a pending asset, so Assets Ready, `game.ready()` and
|
|
9
|
+
* the Run Session's wait all cover it; keeps one `PhysicsWorld` per live 3D
|
|
10
|
+
* scene; and answers the components' questions by entity. A 2D scene never
|
|
11
|
+
* triggers the load.
|
|
12
|
+
*/
|
|
13
|
+
export class PhysicsHost {
|
|
14
|
+
game;
|
|
15
|
+
backend;
|
|
16
|
+
module = null;
|
|
17
|
+
loading = null;
|
|
18
|
+
failure = null;
|
|
19
|
+
gravity = [...DEFAULT_GRAVITY];
|
|
20
|
+
live = null;
|
|
21
|
+
disposed = false;
|
|
22
|
+
constructor(game, backend) {
|
|
23
|
+
this.game = game;
|
|
24
|
+
this.backend = backend;
|
|
25
|
+
}
|
|
26
|
+
/** The live 3D scene's world; null for a 2D scene, while the module loads and after it failed. */
|
|
27
|
+
get world() {
|
|
28
|
+
return this.live;
|
|
29
|
+
}
|
|
30
|
+
/** The state a 3D scene reports; for a 2D scene it is meaningless and callers do not ask. */
|
|
31
|
+
get state() {
|
|
32
|
+
if (this.failure)
|
|
33
|
+
return 'failed';
|
|
34
|
+
return this.live ? 'ready' : 'loading';
|
|
35
|
+
}
|
|
36
|
+
/** The gravity the live scene runs with: its world's, or the declared one while the world does not exist yet. */
|
|
37
|
+
get sceneGravity() {
|
|
38
|
+
return this.live ? this.live.gravity : [...this.gravity];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Adopts a scene's space (called by `setSceneRender`): a 3D scene starts the
|
|
42
|
+
* load, gets its world at once when the module is here, or hears about the
|
|
43
|
+
* load that already failed.
|
|
44
|
+
*/
|
|
45
|
+
enterScene(space) {
|
|
46
|
+
if (space !== '3d' || this.disposed)
|
|
47
|
+
return;
|
|
48
|
+
if (this.failure)
|
|
49
|
+
this.report(this.failure);
|
|
50
|
+
else if (this.module)
|
|
51
|
+
this.createWorld(this.module);
|
|
52
|
+
else
|
|
53
|
+
this.begin();
|
|
54
|
+
}
|
|
55
|
+
/** Adopts a scene's gravity (called by `setSceneSimulation`). */
|
|
56
|
+
setSimulation(simulation) {
|
|
57
|
+
this.gravity = simulation.gravity;
|
|
58
|
+
if (this.live)
|
|
59
|
+
this.live.gravity = simulation.gravity;
|
|
60
|
+
}
|
|
61
|
+
/** Frees the world after the scene's entities are destroyed; the module stays. */
|
|
62
|
+
unloadScene() {
|
|
63
|
+
this.live?.free();
|
|
64
|
+
this.live = null;
|
|
65
|
+
this.gravity = [...DEFAULT_GRAVITY];
|
|
66
|
+
}
|
|
67
|
+
dispose() {
|
|
68
|
+
this.disposed = true;
|
|
69
|
+
this.unloadScene();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `game.ready()`: the renderer's promise, and for a Game whose live scene is
|
|
73
|
+
* 3D also the Rapier load (which rejects, naming the package, when it fails).
|
|
74
|
+
*/
|
|
75
|
+
whenReady(rendering) {
|
|
76
|
+
const loading = this.game.space === '3d' ? this.loading : null;
|
|
77
|
+
return loading ? Promise.all([rendering, loading]).then(() => undefined) : rendering;
|
|
78
|
+
}
|
|
79
|
+
/** One Simulation Step of the world, when there is one. */
|
|
80
|
+
step() {
|
|
81
|
+
this.live?.step();
|
|
82
|
+
}
|
|
83
|
+
attach(entity) {
|
|
84
|
+
this.live?.attach(entity);
|
|
85
|
+
}
|
|
86
|
+
detach(entity) {
|
|
87
|
+
this.live?.detach(entity);
|
|
88
|
+
}
|
|
89
|
+
recordOf(entity) {
|
|
90
|
+
return this.live?.recordOf(entity);
|
|
91
|
+
}
|
|
92
|
+
velocityOf(entity) {
|
|
93
|
+
const record = this.recordOf(entity);
|
|
94
|
+
return record ? bodyVelocity(record) : null;
|
|
95
|
+
}
|
|
96
|
+
/** Sets a dynamic body's velocity; for a kinematic one, x and z become its desired velocity and y its vertical velocity. */
|
|
97
|
+
setVelocityOf(entity, velocity) {
|
|
98
|
+
const record = this.recordOf(entity);
|
|
99
|
+
if (record?.kind === 'dynamic')
|
|
100
|
+
record.body.setLinvel(velocity, true);
|
|
101
|
+
if (record?.kind === 'kinematic' && record.rigid) {
|
|
102
|
+
record.rigid.desiredVelocity = { x: velocity.x, z: velocity.z };
|
|
103
|
+
record.vy = velocity.y;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
groundedOf(entity) {
|
|
107
|
+
return this.recordOf(entity)?.grounded ?? false;
|
|
108
|
+
}
|
|
109
|
+
/** Launches a kinematic body upward when it stands on something; whether it did. */
|
|
110
|
+
jumpOf(entity, speed) {
|
|
111
|
+
const record = this.recordOf(entity);
|
|
112
|
+
if (record?.kind !== 'kinematic' || !record.grounded)
|
|
113
|
+
return false;
|
|
114
|
+
record.vy = speed;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
applyImpulseTo(entity, impulse) {
|
|
118
|
+
const record = this.recordOf(entity);
|
|
119
|
+
if (record?.kind === 'dynamic')
|
|
120
|
+
record.body.applyImpulse(impulse, true);
|
|
121
|
+
}
|
|
122
|
+
begin() {
|
|
123
|
+
if (this.loading)
|
|
124
|
+
return;
|
|
125
|
+
const work = (this.backend ?? loadRapier)().then((module) => this.arrive(module), (cause) => this.fail(cause));
|
|
126
|
+
this.loading = work;
|
|
127
|
+
// The asset loader counts it: pending now, loaded or failed when it settles, and Assets Ready waits for it.
|
|
128
|
+
this.game.assets.track(work, RAPIER_PACKAGE);
|
|
129
|
+
// Observed here: `whenReady` hands every caller its own derived promise.
|
|
130
|
+
work.catch(() => undefined);
|
|
131
|
+
}
|
|
132
|
+
arrive(module) {
|
|
133
|
+
this.module = module;
|
|
134
|
+
if (this.disposed || this.game.space !== '3d')
|
|
135
|
+
return;
|
|
136
|
+
this.createWorld(module);
|
|
137
|
+
}
|
|
138
|
+
fail(cause) {
|
|
139
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
140
|
+
const error = new Error(`Physics failed to load: could not import ${RAPIER_PACKAGE} (${reason})`, { cause });
|
|
141
|
+
this.failure = error;
|
|
142
|
+
// A Game whose live scene is 2D by now needs no physics: the failure waits for the next 3D scene.
|
|
143
|
+
if (this.game.space === '3d')
|
|
144
|
+
this.report(error);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
/** Tells the Runtime Bridge the scene it is driving cannot simulate (the mirror of `render-backend-failed`). */
|
|
148
|
+
report(error) {
|
|
149
|
+
if (this.disposed)
|
|
150
|
+
return;
|
|
151
|
+
activeRuntimeBridgeHook()?.fail?.({ code: 'physics-backend-failed', message: error.message });
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The world of the live 3D scene, with a body for every Collider already
|
|
155
|
+
* spawned (a load that finished late). A body that cannot be built costs
|
|
156
|
+
* that entity its body, never the rest of the scene or the load itself
|
|
157
|
+
* (spawnFromJson's rule for a throwing component).
|
|
158
|
+
*/
|
|
159
|
+
createWorld(module) {
|
|
160
|
+
if (this.live || this.disposed)
|
|
161
|
+
return;
|
|
162
|
+
const world = new PhysicsWorld(module, this.gravity);
|
|
163
|
+
this.live = world;
|
|
164
|
+
for (const entity of this.game.entities) {
|
|
165
|
+
try {
|
|
166
|
+
world.attach(entity);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
console.warn(`[waica] "${entity.name}": no physics body was created (${message})`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Entity } from '../entity.js';
|
|
2
|
+
import type { Vec3Json } from '../scene-camera-3d.js';
|
|
3
|
+
import { type BodyRecord } from './body-sync.js';
|
|
4
|
+
import type { RapierModule, RapierWorld } from './rapier-module.js';
|
|
5
|
+
/**
|
|
6
|
+
* One Rapier world and the bodies of the live 3D scene (ADR 0028): created
|
|
7
|
+
* with the scene, stepped once per Simulation Step, freed with the scene. It
|
|
8
|
+
* owns every Rapier handle; components only hold plain params and ask the
|
|
9
|
+
* Game's `physics` for velocity, impulses and the like by entity.
|
|
10
|
+
*/
|
|
11
|
+
export declare class PhysicsWorld {
|
|
12
|
+
/** The Rapier module the world was made with, for the shapes and rays its queries build. */
|
|
13
|
+
readonly R: RapierModule;
|
|
14
|
+
/** The Rapier world itself, for the modules that query it; nothing outside `physics-3d/` and its tests should touch it. */
|
|
15
|
+
readonly raw: RapierWorld;
|
|
16
|
+
private readonly records;
|
|
17
|
+
private readonly byCollider;
|
|
18
|
+
/** Entities with a RigidBody and, so far, no Collider: warned about at the first step if it stays so. */
|
|
19
|
+
private readonly orphans;
|
|
20
|
+
/** The last warning each entity got, so the same problem seen from both of its components is said once. */
|
|
21
|
+
private readonly warned;
|
|
22
|
+
private sensors;
|
|
23
|
+
/** Bodies came or went since Rapier last updated the structure its queries read. */
|
|
24
|
+
private queriesStale;
|
|
25
|
+
private readonly characters;
|
|
26
|
+
constructor(
|
|
27
|
+
/** The Rapier module the world was made with, for the shapes and rays its queries build. */
|
|
28
|
+
R: RapierModule, gravity: Vec3Json);
|
|
29
|
+
/** Whether any body has a sensor Collider, so the sensor dispatch can be skipped when none does. */
|
|
30
|
+
get hasSensors(): boolean;
|
|
31
|
+
get gravity(): Vec3Json;
|
|
32
|
+
set gravity(value: Vec3Json);
|
|
33
|
+
/** The record of an entity's body, if it has one. */
|
|
34
|
+
recordOf(entity: Entity): BodyRecord | undefined;
|
|
35
|
+
/** The record that owns a Rapier collider handle. */
|
|
36
|
+
recordOfCollider(handle: number): BodyRecord | undefined;
|
|
37
|
+
/** The bodies of the given entities, in their order. */
|
|
38
|
+
bodiesOf(entities: readonly Entity[]): BodyRecord[];
|
|
39
|
+
/**
|
|
40
|
+
* Builds (or rebuilds) the entity's body from the Collider and RigidBody it
|
|
41
|
+
* has now. Called from each component's `onReady`, so either order of the
|
|
42
|
+
* two works: the later one replaces the earlier one's body.
|
|
43
|
+
*/
|
|
44
|
+
attach(entity: Entity): void;
|
|
45
|
+
/** Removes the entity's body and collider; a no-op for an entity without one. */
|
|
46
|
+
detach(entity: Entity): void;
|
|
47
|
+
/**
|
|
48
|
+
* One Simulation Step of physics: every kinematic body is moved through the
|
|
49
|
+
* character controller, Rapier steps, then moving bodies write their pose to
|
|
50
|
+
* their entities.
|
|
51
|
+
*/
|
|
52
|
+
step(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Makes ray casts, shape queries and the character controller see the bodies
|
|
55
|
+
* created or removed since the last step: Rapier only updates the structure
|
|
56
|
+
* they read inside `step()`, so a body spawned this frame would be invisible
|
|
57
|
+
* (a character would fall through a floor spawned with it). A step of zero
|
|
58
|
+
* seconds updates it and moves nothing.
|
|
59
|
+
*/
|
|
60
|
+
refreshQueries(): void;
|
|
61
|
+
/** Frees the Rapier world and forgets every body. The entities are destroyed (and detached) before this. */
|
|
62
|
+
free(): void;
|
|
63
|
+
private warnOrphans;
|
|
64
|
+
private warnOnce;
|
|
65
|
+
}
|