@waica/engine 0.25.0 → 0.27.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 +77 -1
- package/dist/anchored-pieces.d.ts +3 -2
- package/dist/anchored-pieces.js +12 -8
- package/dist/animation/clip-player.d.ts +7 -0
- package/dist/animation/clip-player.js +9 -0
- package/dist/animation/clip-target.d.ts +38 -0
- package/dist/animation/clip-target.js +9 -0
- package/dist/animation/pose-hash.d.ts +10 -0
- package/dist/animation/pose-hash.js +43 -0
- 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 +14 -3
- package/dist/components/animated-sprite.js +16 -2
- package/dist/components/animator.d.ts +98 -0
- package/dist/components/animator.js +197 -0
- 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 +2 -1
- package/dist/components/light.js +1 -0
- package/dist/components/model.d.ts +71 -0
- package/dist/components/model.js +128 -0
- package/dist/components/particle-emitter.d.ts +2 -1
- package/dist/components/particle-emitter.js +1 -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 +2 -1
- package/dist/components/sprite.js +1 -0
- 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 +1 -0
- package/dist/entity-transform.d.ts +18 -0
- package/dist/entity-transform.js +17 -0
- package/dist/game.d.ts +51 -15
- package/dist/game.js +83 -70
- package/dist/index.d.ts +34 -4
- package/dist/index.js +15 -1
- package/dist/lit-frame.js +10 -3
- 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/runtime-bridge.d.ts +1 -1
- package/dist/runtime-inspection.d.ts +9 -0
- package/dist/runtime-inspection.js +8 -2
- package/dist/runtime-lighting-snapshot.d.ts +27 -1
- package/dist/runtime-lighting-snapshot.js +31 -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-lights-3d.d.ts +17 -0
- package/dist/scene-lights-3d.js +27 -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 +12 -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/state/state-machine.d.ts +32 -8
- package/dist/state/state-machine.js +88 -27
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,17 @@ export type { GameOptions, GameResolution, SceneCatalog, SpawnPrefabOptions, Upd
|
|
|
3
3
|
export { AssetLoader } from './assets/asset-loader.js';
|
|
4
4
|
export type { AssetStatus } from './assets/asset-loader.js';
|
|
5
5
|
export type { TextureBackend } from './assets/texture-backend.js';
|
|
6
|
+
export { GltfModelBackend } from './assets/model-backend.js';
|
|
7
|
+
export type { LoadedModel, ModelBackend } from './assets/model-backend.js';
|
|
8
|
+
export type { ModelHandle, ModelOutcome } from './assets/model-cache.js';
|
|
6
9
|
export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
7
10
|
export type { AudioSubsystemOptions } from './audio/audio-subsystem.js';
|
|
8
11
|
export type { AudioBackend, AudioResource, BackendPlayHandle, BackendPlayOptions } from './audio/backend.js';
|
|
9
12
|
export type { AudioChannelState, AudioPlayOptions, LiveSoundInfo, SoundHandle } from './audio/types.js';
|
|
10
13
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
11
14
|
export type { AnimationFacingProvider, DirectionalAnimation, DirectionalFallback, ResolvedDirectionalClip, } from './animation/directional.js';
|
|
15
|
+
export { isClipTarget } from './animation/clip-target.js';
|
|
16
|
+
export type { ClipTarget } from './animation/clip-target.js';
|
|
12
17
|
export { isYSortBatchParticipant, isYSortParticipant, ySortZ } from './render-sort.js';
|
|
13
18
|
export type { YSortBatchParticipant, YSortEntry, YSortParticipant } from './render-sort.js';
|
|
14
19
|
export { projectIsometric, screenInputToLogical, unprojectIsometric } from './projection.js';
|
|
@@ -21,11 +26,20 @@ export { CameraEffects } from './camera-effects.js';
|
|
|
21
26
|
export type { CameraEffectColor, CameraEffectHandle, CameraEffectsOptions, CameraEffectsState, FadeOptions, FlashOptions, ShakeOptions, } from './camera-effects.js';
|
|
22
27
|
export { GameTime, advanceGameTime } from './game-time.js';
|
|
23
28
|
export type { EasingName, TimerHandle, TimerOptions, TweenOptions } from './game-time.js';
|
|
24
|
-
export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
|
|
29
|
+
export type { SceneCameraJson, OrthographicSceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
|
|
30
|
+
export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
|
|
31
|
+
export type { PerspectiveSceneCameraJson, ResolvedPerspectiveCamera, SceneFieldIssue, Vec3Json, } from './scene-camera-3d.js';
|
|
32
|
+
export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
|
|
33
|
+
export type { GameCamera, NormalizedPoint, ViewCamera, WorldPoint } from './camera-projection.js';
|
|
34
|
+
export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
|
|
35
|
+
export type { SceneSpace, SpaceMarked } from './scene-space.js';
|
|
36
|
+
export type { RuntimeSnapshotView } from './runtime-view-snapshot.js';
|
|
37
|
+
export { applyTransformJson } from './entity-transform.js';
|
|
38
|
+
export type { PositionJson, TransformJson } from './entity-transform.js';
|
|
25
39
|
export { Entity } from './entity.js';
|
|
26
|
-
export { Component } from './component.js';
|
|
40
|
+
export { COMPONENT_SPACES, Component } from './component.js';
|
|
27
41
|
export { authoringDefaults } from './authoring-defaults.js';
|
|
28
|
-
export type { ComponentClass, ContactNormal, ParamSpec, SolidContact, } from './component.js';
|
|
42
|
+
export type { BodyContact, ComponentClass, ComponentSpace, ContactNormal, ParamSpec, SolidContact, } from './component.js';
|
|
29
43
|
export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
|
|
30
44
|
export type { ComponentModule } from './component-registry.js';
|
|
31
45
|
export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
@@ -40,7 +54,7 @@ export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_B
|
|
|
40
54
|
export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeBridgeFailure, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
|
|
41
55
|
export type { RenderBackend } from './render-readiness.js';
|
|
42
56
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
43
|
-
export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
|
|
57
|
+
export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPointLight, RuntimeSnapshotSun, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
|
|
44
58
|
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
45
59
|
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
|
|
46
60
|
export { Stats } from './stats.js';
|
|
@@ -49,6 +63,21 @@ export { GameUi } from './ui.js';
|
|
|
49
63
|
export type { AnchoredPieceHandle, AttachOptions } from './anchored-pieces.js';
|
|
50
64
|
export { Sprite } from './components/sprite.js';
|
|
51
65
|
export { Light } from './components/light.js';
|
|
66
|
+
export { Model } from './components/model.js';
|
|
67
|
+
export type { ModelShape } from './components/model.js';
|
|
68
|
+
export { Animator } from './components/animator.js';
|
|
69
|
+
export type { AnimatorClipOverride, AnimatorState } from './components/animator.js';
|
|
70
|
+
export { poseHash } from './animation/pose-hash.js';
|
|
71
|
+
export { Sun } from './components/sun.js';
|
|
72
|
+
export { Collider, COLLIDER_SHAPES } from './components/collider.js';
|
|
73
|
+
export type { ColliderShape } from './components/collider.js';
|
|
74
|
+
export { RigidBody, RIGID_BODY_TYPES } from './components/rigid-body.js';
|
|
75
|
+
export type { BodyVector, RigidBodyType } from './components/rigid-body.js';
|
|
76
|
+
export { DEFAULT_GRAVITY, resolveSceneSimulation, sceneSimulationIssues } from './scene-simulation.js';
|
|
77
|
+
export type { ResolvedSceneSimulation, SceneSimulationJson } from './scene-simulation.js';
|
|
78
|
+
export type { PhysicsBackend, RapierModule } from './physics-3d/rapier-module.js';
|
|
79
|
+
export type { RuntimeSnapshotBody, RuntimeSnapshotPhysics } from './runtime-physics-snapshot.js';
|
|
80
|
+
export { PointLight } from './components/point-light.js';
|
|
52
81
|
export { GameLighting } from './scene-lighting.js';
|
|
53
82
|
export type { AmbientLightInput } from './scene-lighting.js';
|
|
54
83
|
export { GamePost } from './post-effects.js';
|
|
@@ -74,6 +103,7 @@ export type { TilemapCell, TilemapCellBounds, TilemapGridSpec, } from './tilemap
|
|
|
74
103
|
export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
|
|
75
104
|
export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, } from './collision-shape.js';
|
|
76
105
|
export type { EntityWith, NearestQueryContext, NearestSpatialQueryFilter, RayHit, SpatialQuery, SpatialQueryFilter, } from './spatial-query.js';
|
|
106
|
+
export type { Point3d, QueryVolume3d, RayHit3d } from './spatial-query-3d.js';
|
|
77
107
|
export { Emitter } from './events.js';
|
|
78
108
|
export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
|
|
79
109
|
export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, SceneLightingJson, ScenePostJson, PrefabJson, } from './scene.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
2
|
export { AssetLoader } from './assets/asset-loader.js';
|
|
3
|
+
export { GltfModelBackend } from './assets/model-backend.js';
|
|
3
4
|
export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
4
5
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
6
|
+
export { isClipTarget } from './animation/clip-target.js';
|
|
5
7
|
export { isYSortBatchParticipant, isYSortParticipant, ySortZ } from './render-sort.js';
|
|
6
8
|
export { projectIsometric, screenInputToLogical, unprojectIsometric } from './projection.js';
|
|
7
9
|
export { spritePlacement } from './sprite-placement.js';
|
|
@@ -9,8 +11,12 @@ export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepScen
|
|
|
9
11
|
export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
|
|
10
12
|
export { CameraEffects } from './camera-effects.js';
|
|
11
13
|
export { GameTime, advanceGameTime } from './game-time.js';
|
|
14
|
+
export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
|
|
15
|
+
export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
|
|
16
|
+
export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
|
|
17
|
+
export { applyTransformJson } from './entity-transform.js';
|
|
12
18
|
export { Entity } from './entity.js';
|
|
13
|
-
export { Component } from './component.js';
|
|
19
|
+
export { COMPONENT_SPACES, Component } from './component.js';
|
|
14
20
|
export { authoringDefaults } from './authoring-defaults.js';
|
|
15
21
|
export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
|
|
16
22
|
export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
@@ -23,6 +29,14 @@ export { Stats } from './stats.js';
|
|
|
23
29
|
export { GameUi } from './ui.js';
|
|
24
30
|
export { Sprite } from './components/sprite.js';
|
|
25
31
|
export { Light } from './components/light.js';
|
|
32
|
+
export { Model } from './components/model.js';
|
|
33
|
+
export { Animator } from './components/animator.js';
|
|
34
|
+
export { poseHash } from './animation/pose-hash.js';
|
|
35
|
+
export { Sun } from './components/sun.js';
|
|
36
|
+
export { Collider, COLLIDER_SHAPES } from './components/collider.js';
|
|
37
|
+
export { RigidBody, RIGID_BODY_TYPES } from './components/rigid-body.js';
|
|
38
|
+
export { DEFAULT_GRAVITY, resolveSceneSimulation, sceneSimulationIssues } from './scene-simulation.js';
|
|
39
|
+
export { PointLight } from './components/point-light.js';
|
|
26
40
|
export { GameLighting } from './scene-lighting.js';
|
|
27
41
|
export { GamePost } from './post-effects.js';
|
|
28
42
|
export { EMISSIVE_LAYER, setEmissive } from './render-layers.js';
|
package/dist/lit-frame.js
CHANGED
|
@@ -56,8 +56,10 @@ export class FrameComposer {
|
|
|
56
56
|
}
|
|
57
57
|
draw() {
|
|
58
58
|
// Full Ambient Light: the light-map would be 1 everywhere, so nothing to multiply.
|
|
59
|
-
|
|
60
|
-
const
|
|
59
|
+
// A 3D scene never builds one: its lights are three's own (ADR 0027).
|
|
60
|
+
const { game } = this.surface;
|
|
61
|
+
const lit = game.space === '2d' && game.lighting.active && !ambientIsFull(game.lighting);
|
|
62
|
+
const post = game.post.active;
|
|
61
63
|
if (!lit && !post) {
|
|
62
64
|
this.clearLetterbox();
|
|
63
65
|
this.drawScene();
|
|
@@ -98,7 +100,12 @@ export class FrameComposer {
|
|
|
98
100
|
/** The scene through its Sprite Batches (ADR 0024), for the camera's current layers. */
|
|
99
101
|
drawScene() {
|
|
100
102
|
const { spriteBatches, renderer } = this.surface;
|
|
101
|
-
const { scene, camera } = this.surface.game;
|
|
103
|
+
const { scene, camera, space } = this.surface.game;
|
|
104
|
+
// A 3D scene has no sprites to batch: it draws straight, the depth buffer ordering it.
|
|
105
|
+
if (space === '3d') {
|
|
106
|
+
renderer.render(scene, camera);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
102
109
|
spriteBatches.drawFrame(scene, camera, () => renderer.render(scene, camera));
|
|
103
110
|
}
|
|
104
111
|
/** The scene for one layer mask, restoring the camera's own mask after. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Game } from '../game.js';
|
|
2
|
+
import type { PhysicsWorld } from './physics-world.js';
|
|
3
|
+
/**
|
|
4
|
+
* After the physics step (CA-15): every component on an entity with a solid
|
|
5
|
+
* Collider that implements `onBodyContact` hears about each other entity it
|
|
6
|
+
* touches, once per step, with the normal pointing from it to the other. The
|
|
7
|
+
* common scene, where no component listens, allocates nothing here: the
|
|
8
|
+
* narrow phase is read only for an entity that has a receiver.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dispatchBodyContacts(game: Game, world: PhysicsWorld): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Adds zero so a negated zero reads as 0, not -0. */
|
|
2
|
+
const clean = (value) => value + 0;
|
|
3
|
+
/** The contact one Rapier manifold makes for `record` against `other`, or null when it holds no solver contact point. */
|
|
4
|
+
function manifoldContact(sides, manifold, flipped) {
|
|
5
|
+
const [record, other] = sides;
|
|
6
|
+
const point = manifold.numSolverContacts() > 0 ? manifold.solverContactPoint(0) : null;
|
|
7
|
+
if (!point)
|
|
8
|
+
return null;
|
|
9
|
+
// The manifold's normal points from its first shape to its second; `flipped` says that first shape is the other one, so it is negated to point from ours.
|
|
10
|
+
const n = manifold.normal();
|
|
11
|
+
const sign = flipped ? -1 : 1;
|
|
12
|
+
return {
|
|
13
|
+
entity: record.entity,
|
|
14
|
+
other: other.entity,
|
|
15
|
+
normal: { x: clean(sign * n.x), y: clean(sign * n.y), z: clean(sign * n.z) },
|
|
16
|
+
point: { x: point.x, y: point.y, z: point.z },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** The solid contacts of one body this step, one per other entity, read from Rapier's narrow phase. */
|
|
20
|
+
function contactsOf(world, record) {
|
|
21
|
+
const contacts = new Map();
|
|
22
|
+
world.raw.contactPairsWith(record.shape, (shape) => {
|
|
23
|
+
const other = world.recordOfCollider(shape.handle);
|
|
24
|
+
if (!other || other === record || contacts.has(other))
|
|
25
|
+
return;
|
|
26
|
+
world.raw.contactPair(record.shape, shape, (manifold, flipped) => {
|
|
27
|
+
const contact = contacts.has(other) ? null : manifoldContact([record, other], manifold, flipped);
|
|
28
|
+
if (contact)
|
|
29
|
+
contacts.set(other, contact);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return [...contacts.values()];
|
|
33
|
+
}
|
|
34
|
+
const listens = (component) => typeof component.onBodyContact === 'function';
|
|
35
|
+
function deliver(contacts, receivers) {
|
|
36
|
+
for (const contact of contacts) {
|
|
37
|
+
if (!contact.entity.alive || !contact.other.alive)
|
|
38
|
+
continue;
|
|
39
|
+
for (const component of receivers)
|
|
40
|
+
component.onBodyContact?.(contact);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* After the physics step (CA-15): every component on an entity with a solid
|
|
45
|
+
* Collider that implements `onBodyContact` hears about each other entity it
|
|
46
|
+
* touches, once per step, with the normal pointing from it to the other. The
|
|
47
|
+
* common scene, where no component listens, allocates nothing here: the
|
|
48
|
+
* narrow phase is read only for an entity that has a receiver.
|
|
49
|
+
*/
|
|
50
|
+
export function dispatchBodyContacts(game, world) {
|
|
51
|
+
for (const entity of game.entities) {
|
|
52
|
+
if (game.physics.world !== world)
|
|
53
|
+
return;
|
|
54
|
+
const record = world.recordOf(entity);
|
|
55
|
+
if (!record || record.sensor || !entity.alive || !entity.components.some(listens))
|
|
56
|
+
continue;
|
|
57
|
+
deliver(contactsOf(world, record), entity.components.filter(listens));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Collider } from '../components/collider.js';
|
|
2
|
+
import { RigidBody } from '../components/rigid-body.js';
|
|
3
|
+
import type { Entity } from '../entity.js';
|
|
4
|
+
import type { RapierBody, RapierCollider, RapierModule, RapierWorld } from './rapier-module.js';
|
|
5
|
+
/** Rapier's three body types, as the Runtime Snapshot names them. */
|
|
6
|
+
export type BodyKind = 'fixed' | 'dynamic' | 'kinematic';
|
|
7
|
+
/** What a body is built from: the entity and the Collider and RigidBody components it has. */
|
|
8
|
+
export interface BodyParts {
|
|
9
|
+
readonly entity: Entity;
|
|
10
|
+
readonly collider: Collider;
|
|
11
|
+
readonly rigid: RigidBody | null;
|
|
12
|
+
}
|
|
13
|
+
/** One entity's body in the world: its components and the Rapier handles built from them. */
|
|
14
|
+
export interface BodyRecord {
|
|
15
|
+
readonly entity: Entity;
|
|
16
|
+
readonly collider: Collider;
|
|
17
|
+
readonly rigid: RigidBody | null;
|
|
18
|
+
readonly kind: BodyKind;
|
|
19
|
+
/** Whether the Rapier collider was built as a sensor; frozen here because `Collider.sensor` is a plain field a script may flip later. */
|
|
20
|
+
readonly sensor: boolean;
|
|
21
|
+
readonly body: RapierBody;
|
|
22
|
+
readonly shape: RapierCollider;
|
|
23
|
+
/** Whether a kinematic body stands on something after the last step; written by the character controller. */
|
|
24
|
+
grounded: boolean;
|
|
25
|
+
/** A kinematic body's vertical velocity (units per second), integrated by the character controller. */
|
|
26
|
+
vy: number;
|
|
27
|
+
/** A kinematic body's movement over the last step, per second. */
|
|
28
|
+
velocity: {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
z: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Why this collider cannot become a Rapier shape, or null when it can. */
|
|
35
|
+
export declare function colliderProblem(collider: Collider): string | null;
|
|
36
|
+
/** Why this RigidBody cannot become a Rapier body, or null when it can (the mirror of `colliderProblem`). */
|
|
37
|
+
export declare function rigidBodyProblem(rigid: RigidBody): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Builds the entity's body and collider from its components and puts them in
|
|
40
|
+
* the world. Static bodies are created once and never follow the entity; a
|
|
41
|
+
* dynamic body is Rapier's from here on. The caller has checked the parts
|
|
42
|
+
* with `colliderProblem` and `rigidBodyProblem`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createBody(R: RapierModule, world: RapierWorld, parts: BodyParts): BodyRecord;
|
|
45
|
+
/** A body's linear velocity: Rapier's for a dynamic one, the measured movement of a kinematic one, none for a fixed one. */
|
|
46
|
+
export declare function bodyVelocity(record: BodyRecord): {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
z: number;
|
|
50
|
+
};
|
|
51
|
+
/** After a step: a moving body's pose becomes its entity's (a sleeping dynamic body has not moved). */
|
|
52
|
+
export declare function syncBodyPose(record: BodyRecord): void;
|
|
@@ -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
|
+
}
|