@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
@@ -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
+ }
@@ -0,0 +1,158 @@
1
+ import { Collider } from '../components/collider.js';
2
+ import { RigidBody } from '../components/rigid-body.js';
3
+ import { SIMULATION_STEP } from '../fixed-step.js';
4
+ import { colliderProblem, createBody, rigidBodyProblem, syncBodyPose } from './body-sync.js';
5
+ import { CharacterMotion } from './character-controller.js';
6
+ /**
7
+ * One Rapier world and the bodies of the live 3D scene (ADR 0028): created
8
+ * with the scene, stepped once per Simulation Step, freed with the scene. It
9
+ * owns every Rapier handle; components only hold plain params and ask the
10
+ * Game's `physics` for velocity, impulses and the like by entity.
11
+ */
12
+ export class PhysicsWorld {
13
+ R;
14
+ /** The Rapier world itself, for the modules that query it; nothing outside `physics-3d/` and its tests should touch it. */
15
+ raw;
16
+ records = new Map();
17
+ byCollider = new Map();
18
+ /** Entities with a RigidBody and, so far, no Collider: warned about at the first step if it stays so. */
19
+ orphans = new Set();
20
+ /** The last warning each entity got, so the same problem seen from both of its components is said once. */
21
+ warned = new WeakMap();
22
+ sensors = 0;
23
+ /** Bodies came or went since Rapier last updated the structure its queries read. */
24
+ queriesStale = false;
25
+ characters;
26
+ constructor(
27
+ /** The Rapier module the world was made with, for the shapes and rays its queries build. */
28
+ R, gravity) {
29
+ this.R = R;
30
+ this.raw = new R.World({ x: gravity[0], y: gravity[1], z: gravity[2] });
31
+ this.raw.timestep = SIMULATION_STEP;
32
+ this.characters = new CharacterMotion(R, this.raw);
33
+ }
34
+ /** Whether any body has a sensor Collider, so the sensor dispatch can be skipped when none does. */
35
+ get hasSensors() {
36
+ return this.sensors > 0;
37
+ }
38
+ get gravity() {
39
+ const { x, y, z } = this.raw.gravity;
40
+ return [x, y, z];
41
+ }
42
+ set gravity(value) {
43
+ this.raw.gravity = { x: value[0], y: value[1], z: value[2] };
44
+ }
45
+ /** The record of an entity's body, if it has one. */
46
+ recordOf(entity) {
47
+ return this.records.get(entity);
48
+ }
49
+ /** The record that owns a Rapier collider handle. */
50
+ recordOfCollider(handle) {
51
+ return this.byCollider.get(handle);
52
+ }
53
+ /** The bodies of the given entities, in their order. */
54
+ bodiesOf(entities) {
55
+ return entities.flatMap((entity) => {
56
+ const record = this.records.get(entity);
57
+ return record ? [record] : [];
58
+ });
59
+ }
60
+ /**
61
+ * Builds (or rebuilds) the entity's body from the Collider and RigidBody it
62
+ * has now. Called from each component's `onReady`, so either order of the
63
+ * two works: the later one replaces the earlier one's body.
64
+ */
65
+ attach(entity) {
66
+ const collider = entity.get(Collider);
67
+ const rigid = entity.get(RigidBody) ?? null;
68
+ if (!collider) {
69
+ if (rigid)
70
+ this.orphans.add(entity);
71
+ return;
72
+ }
73
+ this.detach(entity);
74
+ const problem = colliderProblem(collider);
75
+ const bodyProblem = problem === null && rigid ? rigidBodyProblem(rigid) : null;
76
+ if (problem !== null)
77
+ return this.warnOnce(entity, `Collider on "${entity.name}" creates no body: ${problem}.`);
78
+ if (bodyProblem !== null)
79
+ return this.warnOnce(entity, `RigidBody on "${entity.name}" creates no body: ${bodyProblem}.`);
80
+ const record = createBody(this.R, this.raw, { entity, collider, rigid });
81
+ this.records.set(entity, record);
82
+ this.queriesStale = true;
83
+ this.byCollider.set(record.shape.handle, record);
84
+ if (record.sensor)
85
+ this.sensors += 1;
86
+ }
87
+ /** Removes the entity's body and collider; a no-op for an entity without one. */
88
+ detach(entity) {
89
+ this.orphans.delete(entity);
90
+ const record = this.records.get(entity);
91
+ if (!record)
92
+ return;
93
+ this.records.delete(entity);
94
+ this.byCollider.delete(record.shape.handle);
95
+ if (record.sensor)
96
+ this.sensors -= 1;
97
+ this.raw.removeRigidBody(record.body);
98
+ this.queriesStale = true;
99
+ }
100
+ /**
101
+ * One Simulation Step of physics: every kinematic body is moved through the
102
+ * character controller, Rapier steps, then moving bodies write their pose to
103
+ * their entities.
104
+ */
105
+ step() {
106
+ this.warnOrphans();
107
+ this.characters.follow(this.records.values());
108
+ for (const record of this.records.values()) {
109
+ if (record.kind !== 'kinematic')
110
+ continue;
111
+ this.refreshQueries();
112
+ this.characters.move(record);
113
+ }
114
+ this.raw.step();
115
+ this.queriesStale = false;
116
+ for (const record of this.records.values())
117
+ syncBodyPose(record);
118
+ }
119
+ /**
120
+ * Makes ray casts, shape queries and the character controller see the bodies
121
+ * created or removed since the last step: Rapier only updates the structure
122
+ * they read inside `step()`, so a body spawned this frame would be invisible
123
+ * (a character would fall through a floor spawned with it). A step of zero
124
+ * seconds updates it and moves nothing.
125
+ */
126
+ refreshQueries() {
127
+ if (!this.queriesStale)
128
+ return;
129
+ this.queriesStale = false;
130
+ const timestep = this.raw.timestep;
131
+ this.raw.timestep = 0;
132
+ this.raw.step();
133
+ this.raw.timestep = timestep;
134
+ }
135
+ /** Frees the Rapier world and forgets every body. The entities are destroyed (and detached) before this. */
136
+ free() {
137
+ this.records.clear();
138
+ this.byCollider.clear();
139
+ this.orphans.clear();
140
+ this.sensors = 0;
141
+ this.characters.free();
142
+ this.raw.free();
143
+ }
144
+ warnOrphans() {
145
+ for (const entity of this.orphans) {
146
+ this.orphans.delete(entity);
147
+ if (!entity.alive || entity.has(Collider))
148
+ continue;
149
+ this.warnOnce(entity, `RigidBody on "${entity.name}" needs a Collider on the same entity; no body was created.`);
150
+ }
151
+ }
152
+ warnOnce(entity, message) {
153
+ if (this.warned.get(entity) === message)
154
+ return;
155
+ this.warned.set(entity, message);
156
+ console.warn(`[waica] ${message}`);
157
+ }
158
+ }
@@ -0,0 +1,17 @@
1
+ import type * as Rapier from '@dimforge/rapier3d-deterministic-compat';
2
+ /** The initialized Rapier module: the `RAPIER` namespace the package documents. */
3
+ export type RapierModule = typeof Rapier;
4
+ export type { Collider as RapierCollider, ColliderDesc as RapierColliderDesc, KinematicCharacterController as RapierCharacterController, RigidBody as RapierBody, TempContactManifold as RapierManifold, World as RapierWorld, } from '@dimforge/rapier3d-deterministic-compat';
5
+ /** The package name, for the messages that name it. */
6
+ export declare const RAPIER_PACKAGE = "@dimforge/rapier3d-deterministic-compat";
7
+ /**
8
+ * Where a Game gets Rapier (ADR 0013's seam, applied to physics): resolves to
9
+ * the initialized module. Defaults to `loadRapier`; a project's own tests pass
10
+ * the real module they initialized once, or a rejecting stub for the failure path.
11
+ */
12
+ export type PhysicsBackend = () => Promise<RapierModule>;
13
+ /**
14
+ * Imports and initializes Rapier, once per page: every Game that asks shares
15
+ * the same module. A failed load is forgotten, so a later Game may retry.
16
+ */
17
+ export declare function loadRapier(): Promise<RapierModule>;
@@ -0,0 +1,19 @@
1
+ /** The package name, for the messages that name it. */
2
+ export const RAPIER_PACKAGE = '@dimforge/rapier3d-deterministic-compat';
3
+ let shared = null;
4
+ /**
5
+ * Imports and initializes Rapier, once per page: every Game that asks shares
6
+ * the same module. A failed load is forgotten, so a later Game may retry.
7
+ */
8
+ export function loadRapier() {
9
+ shared ??= import('@dimforge/rapier3d-deterministic-compat')
10
+ .then(async (module) => {
11
+ await module.init();
12
+ return module;
13
+ })
14
+ .catch((error) => {
15
+ shared = null;
16
+ throw error;
17
+ });
18
+ return shared;
19
+ }
@@ -0,0 +1,10 @@
1
+ import type { Game } from '../game.js';
2
+ import type { PhysicsWorld } from './physics-world.js';
3
+ /**
4
+ * The 3D counterpart of the Hitbox trigger dispatch (CA-16): after the physics
5
+ * step, every overlap involving a sensor Collider fires `onCollide(other)` on
6
+ * the components of each side whose interest names the other's layer, every
7
+ * step while they overlap. Pairs are frozen first and each is re-checked live,
8
+ * like the 2D dispatch.
9
+ */
10
+ export declare function dispatchSensorCollisions(game: Game, world: PhysicsWorld): void;
@@ -0,0 +1,77 @@
1
+ import { collisionMaskTargets } from '../collision-category.js';
2
+ import { Collider } from '../components/collider.js';
3
+ /** The mask a Collider has when nothing was declared: every layer. */
4
+ function isDefaultMask(mask) {
5
+ return mask.length === 1 && mask[0] === '*';
6
+ }
7
+ /**
8
+ * Whether `self` is notified about `target` (ADR 0016's directional
9
+ * interest). A sensor always has an interest, its `collidesWith` (default
10
+ * every layer), except that the default mask leaves out the level's fixed
11
+ * solids: a 2D Hitbox never sees a Solid, so a pickup resting on the floor is
12
+ * not told about the floor every step; naming their layer still works. A
13
+ * solid collider has no interest until it declares a mask of its own
14
+ * (inference 14, issue #159): the default `['*']` on a solid means "no
15
+ * interest", so a wall or a character is not woken by every trigger it touches.
16
+ */
17
+ function interestedIn(self, target) {
18
+ const { collider } = self;
19
+ const defaultMask = isDefaultMask(collider.collidesWith);
20
+ if (!self.sensor && defaultMask)
21
+ return false;
22
+ if (defaultMask && !target.sensor && target.kind === 'fixed')
23
+ return false;
24
+ return collisionMaskTargets(collider.collidesWith, target.collider.layer);
25
+ }
26
+ /** Every sensor overlap of the world as a frozen snapshot: each pair once, the earlier-spawned sensor first. */
27
+ function sensorPairs(game, world) {
28
+ const records = world.bodiesOf(game.entities);
29
+ const spawnIndex = new Map(records.map((record, index) => [record, index]));
30
+ const spawnedBefore = (a, b) => (spawnIndex.get(a) ?? 0) < (spawnIndex.get(b) ?? 0);
31
+ const pairs = [];
32
+ for (const sensor of records.filter((record) => record.sensor)) {
33
+ world.raw.intersectionPairsWith(sensor.shape, (shape) => {
34
+ const other = world.recordOfCollider(shape.handle);
35
+ if (!other || other === sensor)
36
+ return;
37
+ // Two sensors see each other: the earlier one reports the pair.
38
+ if (other.sensor && spawnedBefore(other, sensor))
39
+ return;
40
+ pairs.push([sensor, other]);
41
+ });
42
+ }
43
+ return pairs;
44
+ }
45
+ /** Both entities of a snapshotted pair are alive and still carry the Collider it was taken from. */
46
+ function stillOverlapping([first, second]) {
47
+ return (first.entity.alive &&
48
+ second.entity.alive &&
49
+ first.entity.get(Collider) === first.collider &&
50
+ second.entity.get(Collider) === second.collider);
51
+ }
52
+ /** `onCollide(other)` on every component of `self`'s entity, when its interest names `other`'s layer. */
53
+ function notify(self, other) {
54
+ if (!interestedIn(self, other))
55
+ return;
56
+ for (const component of [...self.entity.components])
57
+ component.onCollide?.(other.entity);
58
+ }
59
+ /**
60
+ * The 3D counterpart of the Hitbox trigger dispatch (CA-16): after the physics
61
+ * step, every overlap involving a sensor Collider fires `onCollide(other)` on
62
+ * the components of each side whose interest names the other's layer, every
63
+ * step while they overlap. Pairs are frozen first and each is re-checked live,
64
+ * like the 2D dispatch.
65
+ */
66
+ export function dispatchSensorCollisions(game, world) {
67
+ if (!world.hasSensors)
68
+ return;
69
+ for (const pair of sensorPairs(game, world)) {
70
+ if (!stillOverlapping(pair))
71
+ continue;
72
+ const [first, second] = pair;
73
+ notify(first, second);
74
+ if (stillOverlapping(pair))
75
+ notify(second, first);
76
+ }
77
+ }
@@ -0,0 +1,39 @@
1
+ import type { BodyRecord } from './body-sync.js';
2
+ import type { PhysicsWorld } from './physics-world.js';
3
+ /** A point or a direction in world space, as `game.query` takes and returns it. */
4
+ export interface Point3d {
5
+ x: number;
6
+ y: number;
7
+ z: number;
8
+ }
9
+ /** A volume `game.query.area` tests sensor colliders against. */
10
+ export type QueryVolume3d = {
11
+ shape: 'box';
12
+ center: Point3d;
13
+ size: Point3d;
14
+ } | {
15
+ shape: 'sphere';
16
+ center: Point3d;
17
+ radius: number;
18
+ };
19
+ /** One collider a ray reaches: whose it is, how far, where and which way its surface faces. */
20
+ export interface WorldRayHit {
21
+ record: BodyRecord;
22
+ distance: number;
23
+ point: Point3d;
24
+ normal: Point3d;
25
+ }
26
+ /**
27
+ * Every collider along a ray whose record `accepts`, nearest first.
28
+ * `direction` is a unit vector, so the distance is in world units; an origin
29
+ * inside a collider hits it at distance 0.
30
+ */
31
+ export declare function worldRayHits(world: PhysicsWorld, ray: {
32
+ origin: Point3d;
33
+ direction: Point3d;
34
+ maxDistance: number;
35
+ }, accepts: (record: BodyRecord) => boolean): WorldRayHit[];
36
+ /** Every record whose collider `accepts` and that intersects the volume. */
37
+ export declare function recordsInVolume(world: PhysicsWorld, volume: QueryVolume3d, accepts: (record: BodyRecord) => boolean): BodyRecord[];
38
+ /** Every record whose collider `accepts` and that contains the point. */
39
+ export declare function recordsAtPoint(world: PhysicsWorld, at: Point3d, accepts: (record: BodyRecord) => boolean): BodyRecord[];