@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.
Files changed (114) hide show
  1. package/README.md +77 -1
  2. package/dist/anchored-pieces.d.ts +3 -2
  3. package/dist/anchored-pieces.js +12 -8
  4. package/dist/animation/clip-player.d.ts +7 -0
  5. package/dist/animation/clip-player.js +9 -0
  6. package/dist/animation/clip-target.d.ts +38 -0
  7. package/dist/animation/clip-target.js +9 -0
  8. package/dist/animation/pose-hash.d.ts +10 -0
  9. package/dist/animation/pose-hash.js +43 -0
  10. package/dist/archetype.d.ts +2 -2
  11. package/dist/assets/asset-loader.d.ts +28 -5
  12. package/dist/assets/asset-loader.js +63 -7
  13. package/dist/assets/model-backend.d.ts +21 -0
  14. package/dist/assets/model-backend.js +21 -0
  15. package/dist/assets/model-cache.d.ts +47 -0
  16. package/dist/assets/model-cache.js +117 -0
  17. package/dist/audio/audio-subsystem.d.ts +6 -1
  18. package/dist/audio/audio-subsystem.js +8 -15
  19. package/dist/audio/placement.d.ts +41 -0
  20. package/dist/audio/placement.js +21 -0
  21. package/dist/audio/spatial.d.ts +20 -0
  22. package/dist/audio/spatial.js +26 -0
  23. package/dist/audio/types.d.ts +7 -1
  24. package/dist/camera-projection.d.ts +38 -0
  25. package/dist/camera-projection.js +44 -0
  26. package/dist/camera-rig.d.ts +48 -0
  27. package/dist/camera-rig.js +98 -0
  28. package/dist/camera.d.ts +20 -3
  29. package/dist/camera.js +3 -1
  30. package/dist/component.d.ts +38 -2
  31. package/dist/component.js +3 -0
  32. package/dist/components/animated-sprite.d.ts +14 -3
  33. package/dist/components/animated-sprite.js +16 -2
  34. package/dist/components/animator.d.ts +98 -0
  35. package/dist/components/animator.js +197 -0
  36. package/dist/components/collider.d.ts +89 -0
  37. package/dist/components/collider.js +57 -0
  38. package/dist/components/dynamic-body.d.ts +2 -1
  39. package/dist/components/dynamic-body.js +1 -0
  40. package/dist/components/hitbox.d.ts +2 -1
  41. package/dist/components/hitbox.js +1 -0
  42. package/dist/components/light.d.ts +2 -1
  43. package/dist/components/light.js +1 -0
  44. package/dist/components/model.d.ts +71 -0
  45. package/dist/components/model.js +128 -0
  46. package/dist/components/particle-emitter.d.ts +2 -1
  47. package/dist/components/particle-emitter.js +1 -0
  48. package/dist/components/point-light.d.ts +75 -0
  49. package/dist/components/point-light.js +102 -0
  50. package/dist/components/rigid-body.d.ts +114 -0
  51. package/dist/components/rigid-body.js +83 -0
  52. package/dist/components/solid.d.ts +2 -1
  53. package/dist/components/solid.js +1 -0
  54. package/dist/components/sprite.d.ts +2 -1
  55. package/dist/components/sprite.js +1 -0
  56. package/dist/components/sun.d.ts +46 -0
  57. package/dist/components/sun.js +74 -0
  58. package/dist/components/tilemap.d.ts +2 -1
  59. package/dist/components/tilemap.js +1 -0
  60. package/dist/entity-transform.d.ts +18 -0
  61. package/dist/entity-transform.js +17 -0
  62. package/dist/game.d.ts +51 -15
  63. package/dist/game.js +83 -70
  64. package/dist/index.d.ts +34 -4
  65. package/dist/index.js +15 -1
  66. package/dist/lit-frame.js +10 -3
  67. package/dist/physics-3d/body-contacts.d.ts +10 -0
  68. package/dist/physics-3d/body-contacts.js +59 -0
  69. package/dist/physics-3d/body-sync.d.ts +52 -0
  70. package/dist/physics-3d/body-sync.js +153 -0
  71. package/dist/physics-3d/character-controller.d.ts +44 -0
  72. package/dist/physics-3d/character-controller.js +126 -0
  73. package/dist/physics-3d/physics-host.d.ts +86 -0
  74. package/dist/physics-3d/physics-host.js +174 -0
  75. package/dist/physics-3d/physics-world.d.ts +65 -0
  76. package/dist/physics-3d/physics-world.js +158 -0
  77. package/dist/physics-3d/rapier-module.d.ts +17 -0
  78. package/dist/physics-3d/rapier-module.js +19 -0
  79. package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
  80. package/dist/physics-3d/sensor-dispatch.js +77 -0
  81. package/dist/physics-3d/world-queries.d.ts +39 -0
  82. package/dist/physics-3d/world-queries.js +63 -0
  83. package/dist/pointer-raycast.d.ts +17 -0
  84. package/dist/pointer-raycast.js +48 -0
  85. package/dist/pointer.d.ts +27 -6
  86. package/dist/pointer.js +43 -17
  87. package/dist/runtime-bridge.d.ts +1 -1
  88. package/dist/runtime-inspection.d.ts +9 -0
  89. package/dist/runtime-inspection.js +8 -2
  90. package/dist/runtime-lighting-snapshot.d.ts +27 -1
  91. package/dist/runtime-lighting-snapshot.js +31 -0
  92. package/dist/runtime-physics-snapshot.d.ts +41 -0
  93. package/dist/runtime-physics-snapshot.js +45 -0
  94. package/dist/runtime-view-snapshot.d.ts +25 -0
  95. package/dist/runtime-view-snapshot.js +30 -0
  96. package/dist/scene-camera-3d.d.ts +66 -0
  97. package/dist/scene-camera-3d.js +117 -0
  98. package/dist/scene-lights-3d.d.ts +17 -0
  99. package/dist/scene-lights-3d.js +27 -0
  100. package/dist/scene-simulation.d.ts +22 -0
  101. package/dist/scene-simulation.js +29 -0
  102. package/dist/scene-space.d.ts +50 -0
  103. package/dist/scene-space.js +103 -0
  104. package/dist/scene.d.ts +12 -2
  105. package/dist/scene.js +11 -4
  106. package/dist/simulation-phases.d.ts +9 -0
  107. package/dist/simulation-phases.js +19 -0
  108. package/dist/spatial-query-3d.d.ts +48 -0
  109. package/dist/spatial-query-3d.js +96 -0
  110. package/dist/spatial-query.d.ts +18 -4
  111. package/dist/spatial-query.js +57 -1
  112. package/dist/state/state-machine.d.ts +32 -8
  113. package/dist/state/state-machine.js +88 -27
  114. 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
- const lit = this.surface.game.lighting.active && !ambientIsFull(this.surface.game.lighting);
60
- const post = this.surface.game.post.active;
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
+ }