@waica/engine 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +55 -1
  2. package/dist/anchored-pieces.d.ts +3 -2
  3. package/dist/anchored-pieces.js +12 -8
  4. package/dist/archetype.d.ts +2 -2
  5. package/dist/assets/asset-loader.d.ts +28 -5
  6. package/dist/assets/asset-loader.js +63 -7
  7. package/dist/assets/model-backend.d.ts +21 -0
  8. package/dist/assets/model-backend.js +21 -0
  9. package/dist/assets/model-cache.d.ts +47 -0
  10. package/dist/assets/model-cache.js +117 -0
  11. package/dist/audio/audio-subsystem.d.ts +6 -1
  12. package/dist/audio/audio-subsystem.js +8 -15
  13. package/dist/audio/placement.d.ts +41 -0
  14. package/dist/audio/placement.js +21 -0
  15. package/dist/audio/spatial.d.ts +20 -0
  16. package/dist/audio/spatial.js +26 -0
  17. package/dist/audio/types.d.ts +7 -1
  18. package/dist/camera-projection.d.ts +38 -0
  19. package/dist/camera-projection.js +44 -0
  20. package/dist/camera-rig.d.ts +48 -0
  21. package/dist/camera-rig.js +98 -0
  22. package/dist/camera.d.ts +20 -3
  23. package/dist/camera.js +3 -1
  24. package/dist/component.d.ts +38 -2
  25. package/dist/component.js +3 -0
  26. package/dist/components/animated-sprite.d.ts +2 -1
  27. package/dist/components/animated-sprite.js +1 -0
  28. package/dist/components/collider.d.ts +89 -0
  29. package/dist/components/collider.js +57 -0
  30. package/dist/components/dynamic-body.d.ts +2 -1
  31. package/dist/components/dynamic-body.js +1 -0
  32. package/dist/components/hitbox.d.ts +2 -1
  33. package/dist/components/hitbox.js +1 -0
  34. package/dist/components/light.d.ts +2 -1
  35. package/dist/components/light.js +1 -0
  36. package/dist/components/model.d.ts +63 -0
  37. package/dist/components/model.js +117 -0
  38. package/dist/components/particle-emitter.d.ts +2 -1
  39. package/dist/components/particle-emitter.js +1 -0
  40. package/dist/components/point-light.d.ts +75 -0
  41. package/dist/components/point-light.js +102 -0
  42. package/dist/components/rigid-body.d.ts +114 -0
  43. package/dist/components/rigid-body.js +83 -0
  44. package/dist/components/solid.d.ts +2 -1
  45. package/dist/components/solid.js +1 -0
  46. package/dist/components/sprite.d.ts +2 -1
  47. package/dist/components/sprite.js +1 -0
  48. package/dist/components/sun.d.ts +46 -0
  49. package/dist/components/sun.js +74 -0
  50. package/dist/components/tilemap.d.ts +2 -1
  51. package/dist/components/tilemap.js +1 -0
  52. package/dist/entity-transform.d.ts +18 -0
  53. package/dist/entity-transform.js +17 -0
  54. package/dist/game.d.ts +51 -15
  55. package/dist/game.js +83 -70
  56. package/dist/index.d.ts +29 -4
  57. package/dist/index.js +12 -1
  58. package/dist/lit-frame.js +10 -3
  59. package/dist/physics-3d/body-contacts.d.ts +10 -0
  60. package/dist/physics-3d/body-contacts.js +59 -0
  61. package/dist/physics-3d/body-sync.d.ts +52 -0
  62. package/dist/physics-3d/body-sync.js +153 -0
  63. package/dist/physics-3d/character-controller.d.ts +44 -0
  64. package/dist/physics-3d/character-controller.js +126 -0
  65. package/dist/physics-3d/physics-host.d.ts +86 -0
  66. package/dist/physics-3d/physics-host.js +174 -0
  67. package/dist/physics-3d/physics-world.d.ts +65 -0
  68. package/dist/physics-3d/physics-world.js +158 -0
  69. package/dist/physics-3d/rapier-module.d.ts +17 -0
  70. package/dist/physics-3d/rapier-module.js +19 -0
  71. package/dist/physics-3d/sensor-dispatch.d.ts +10 -0
  72. package/dist/physics-3d/sensor-dispatch.js +77 -0
  73. package/dist/physics-3d/world-queries.d.ts +39 -0
  74. package/dist/physics-3d/world-queries.js +63 -0
  75. package/dist/pointer-raycast.d.ts +17 -0
  76. package/dist/pointer-raycast.js +48 -0
  77. package/dist/pointer.d.ts +27 -6
  78. package/dist/pointer.js +43 -17
  79. package/dist/runtime-bridge.d.ts +1 -1
  80. package/dist/runtime-inspection.d.ts +9 -0
  81. package/dist/runtime-inspection.js +8 -2
  82. package/dist/runtime-lighting-snapshot.d.ts +27 -1
  83. package/dist/runtime-lighting-snapshot.js +31 -0
  84. package/dist/runtime-physics-snapshot.d.ts +41 -0
  85. package/dist/runtime-physics-snapshot.js +45 -0
  86. package/dist/runtime-view-snapshot.d.ts +25 -0
  87. package/dist/runtime-view-snapshot.js +30 -0
  88. package/dist/scene-camera-3d.d.ts +66 -0
  89. package/dist/scene-camera-3d.js +117 -0
  90. package/dist/scene-lights-3d.d.ts +17 -0
  91. package/dist/scene-lights-3d.js +27 -0
  92. package/dist/scene-simulation.d.ts +22 -0
  93. package/dist/scene-simulation.js +29 -0
  94. package/dist/scene-space.d.ts +50 -0
  95. package/dist/scene-space.js +103 -0
  96. package/dist/scene.d.ts +12 -2
  97. package/dist/scene.js +11 -4
  98. package/dist/simulation-phases.d.ts +9 -0
  99. package/dist/simulation-phases.js +19 -0
  100. package/dist/spatial-query-3d.d.ts +48 -0
  101. package/dist/spatial-query-3d.js +96 -0
  102. package/dist/spatial-query.d.ts +18 -4
  103. package/dist/spatial-query.js +57 -1
  104. package/package.json +2 -1
package/dist/game.js CHANGED
@@ -2,8 +2,9 @@ import * as THREE from 'three/webgpu';
2
2
  import { gameViewport } from './anchored-pieces.js';
3
3
  import { AssetLoader } from './assets/asset-loader.js';
4
4
  import { AudioSubsystem } from './audio/audio-subsystem.js';
5
- import { dispatchCollisions as dispatchHitboxCollisions } from './collision-dispatch.js';
6
- import { isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera, } from './camera.js';
5
+ import { listenerPosition, screenPanOf } from './audio/spatial.js';
6
+ import { worldToNormalized } from './camera-projection.js';
7
+ import { CameraRig } from './camera-rig.js';
7
8
  import { advanceCameraEffects, CameraEffects, layoutCameraEffects } from './camera-effects.js';
8
9
  import { implementsOnUpdate, resolveComponentUpdateSchedule } from './component-update-schedule.js';
9
10
  import { Entity } from './entity.js';
@@ -11,19 +12,23 @@ import { Emitter } from './events.js';
11
12
  import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedToStep, } from './fixed-step.js';
12
13
  import { advanceGameTime, GameTime } from './game-time.js';
13
14
  import { Input } from './input.js';
15
+ import { PhysicsHost } from './physics-3d/physics-host.js';
14
16
  import { Pointer } from './pointer.js';
15
17
  import { GamePost } from './post-effects.js';
16
18
  import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
17
19
  import { RuntimeInspector } from './runtime-inspection.js';
18
20
  import { FrameComposer } from './lit-frame.js';
19
- import { EMISSIVE_LAYER } from './render-layers.js';
20
- import { projectIsometric, unprojectIsometric } from './projection.js';
21
+ import { projectIsometric } from './projection.js';
21
22
  import { canvasBackground, drawStraightToCanvas, mapUvPerVertex } from './render-output.js';
22
23
  import { RenderReadiness } from './render-readiness.js';
23
24
  import { applyYSort } from './render-sort.js';
24
25
  import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
25
26
  import { sceneDrainsOf } from './scene-drains.js';
27
+ import { resolveSceneSimulation } from './scene-simulation.js';
28
+ import { SceneAmbientLight3d } from './scene-lights-3d.js';
29
+ import { resolveRenderPolicy } from './scene-space.js';
26
30
  import { GameLighting } from './scene-lighting.js';
31
+ import { runSimulationPhases } from './simulation-phases.js';
27
32
  import { createSpriteBatches } from './sprite-batches.js';
28
33
  import { createSpatialQuery } from './spatial-query.js';
29
34
  import { Stats } from './stats.js';
@@ -44,7 +49,6 @@ function componentsInOrder(order, byName) {
44
49
  */
45
50
  export class Game {
46
51
  scene = new THREE.Scene();
47
- camera;
48
52
  input;
49
53
  pointer;
50
54
  query;
@@ -72,6 +76,8 @@ export class Game {
72
76
  lighting = new GameLighting();
73
77
  /** The live scene's Post Effects (vignette, color grade); they die with their scene. */
74
78
  post = new GamePost();
79
+ /** The live 3D scene's Rapier world and bodies (ADR 0028); package-internal, read by the physics components and the Runtime Snapshot. */
80
+ physics;
75
81
  /** Registry retained by loadScene for runtime prefab spawning. */
76
82
  registry = null;
77
83
  paramOverrides = {};
@@ -103,8 +109,21 @@ export class Game {
103
109
  baseViewHeight;
104
110
  viewHeight;
105
111
  sceneCamera = null;
112
+ /** The orthographic camera of 2D scenes and the perspective one of 3D scenes (ADR 0027); read by the Runtime Snapshot, not part of the game-facing API. */
113
+ cameraRig = new CameraRig();
114
+ sceneSpace = '2d';
115
+ /** A 3D scene's stereo pan, reading the live camera at every call (built once, not per frame). */
116
+ screenPan = screenPanOf(() => this.camera);
117
+ /** What following an entity reads from the Game, built once rather than per Simulation Step. */
118
+ followHost = {
119
+ find: (name) => this.find(name),
120
+ renderPoint: (x, y) => this.renderPoint(x, y),
121
+ viewHeight: () => this.viewHeight,
122
+ };
106
123
  renderSort = null;
107
124
  sceneProjection = null;
125
+ /** The Ambient Light of a 3D scene, fed by `lighting.ambient` (ADR 0027). */
126
+ ambientLight3d = new SceneAmbientLight3d(this.scene);
108
127
  /** Timestamp of the last animation frame; null until the loop's first frame seeds it. */
109
128
  lastTime = null;
110
129
  /** Seconds of elapsed time not yet worth a whole Simulation Step (ADR 0014). */
@@ -147,11 +166,13 @@ export class Game {
147
166
  });
148
167
  this.assets = new AssetLoader({
149
168
  backend: options.textures,
169
+ models: options.models,
150
170
  // Same late lookup as audio's: preload() resolves through whatever
151
171
  // catalog is registered at call time, which outlives every scene.
152
172
  resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
153
173
  });
154
174
  this.spriteBatches = createSpriteBatches(this, this.assets);
175
+ this.physics = new PhysicsHost(this, options.physics);
155
176
  this.cameraEffects = new CameraEffects({
156
177
  host: () => canvas.parentElement ?? document.body,
157
178
  // One screen pixel in world units: the shake snaps to it (never the base).
@@ -170,14 +191,10 @@ export class Game {
170
191
  activeRuntimeBridgeHook()?.fail?.({ code: 'render-backend-failed', message: error.message });
171
192
  });
172
193
  this.scene.background = canvasBackground(background);
173
- this.camera = new THREE.OrthographicCamera();
174
- this.camera.position.z = 10;
175
- // Emissive drawables sit on their own layer; an unlit frame draws it in place.
176
- this.camera.layers.enable(EMISSIVE_LAYER);
177
194
  const { renderer, spriteBatches, resolution } = this;
178
195
  this.frame = new FrameComposer({ game: this, renderer, spriteBatches, resolution });
179
196
  this.pointer = new Pointer(canvas, {
180
- camera: this.camera,
197
+ camera: () => this.camera,
181
198
  resolution: this.resolution,
182
199
  projection: () => this.sceneProjection,
183
200
  entities: this.entities,
@@ -188,12 +205,14 @@ export class Game {
188
205
  }
189
206
  /**
190
207
  * Resolves once the renderer can draw; rejects, naming both Render
191
- * Backends, when neither initializes. Every call returns the same promise.
208
+ * Backends, when neither initializes. While the live scene is 3D it also
209
+ * waits for the physics module, and rejects naming its package if that
210
+ * fails to load (ADR 0028).
192
211
  * Frames before it simulate but draw nothing, so a host awaits it before
193
212
  * its first frame (ADR 0025).
194
213
  */
195
214
  ready() {
196
- return this.readiness.promise;
215
+ return this.physics.whenReady(this.readiness.promise);
197
216
  }
198
217
  /** The Render Backend the renderer settled on; null until ready() resolved. */
199
218
  get backend() {
@@ -251,6 +270,8 @@ export class Game {
251
270
  // Pointer holds that array by reference, so it must never be reassigned.
252
271
  for (const entity of [...this.entities])
253
272
  entity.destroy();
273
+ // After the cascade removed every body: the world goes with its scene.
274
+ this.physics.unloadScene();
254
275
  // Drains transferred by the destruction cascade above are scene-scoped too.
255
276
  sceneDrainsOf(this).clear();
256
277
  // After the cascade freed every slot: the batches go with their scene.
@@ -259,6 +280,8 @@ export class Game {
259
280
  this.renderSort = null;
260
281
  this.sceneProjection = null;
261
282
  this.sceneCamera = null;
283
+ this.sceneSpace = '2d';
284
+ this.cameraRig.reset();
262
285
  this.liveSceneName = null;
263
286
  this.setViewHeight(this.baseViewHeight);
264
287
  }
@@ -348,11 +371,13 @@ export class Game {
348
371
  * draw order stays layer-banded with spawn-order ties.
349
372
  */
350
373
  setSceneRender(json) {
351
- this.renderSort = json?.sort === 'y' ? 'y' : null;
374
+ const { space, sort, projection } = resolveRenderPolicy(json);
375
+ this.sceneSpace = space;
376
+ this.renderSort = sort;
352
377
  this.spriteBatches.enabled = json?.batch !== false;
353
378
  this.lighting.loadScene(json);
354
379
  this.post.loadScene(json);
355
- const projection = json?.projection === 'isometric' ? 'isometric' : null;
380
+ this.physics.enterScene(space);
356
381
  if (projection === this.sceneProjection)
357
382
  return;
358
383
  this.sceneProjection = projection;
@@ -362,26 +387,28 @@ export class Game {
362
387
  component.onProjectionChange?.(projection);
363
388
  }
364
389
  }
390
+ /** Adopts a scene's simulation block (gravity); called by loadScene beside setSceneRender. */
391
+ setSceneSimulation(json) {
392
+ this.physics.setSimulation(resolveSceneSimulation(json));
393
+ }
365
394
  /**
366
395
  * Adopts a scene's camera block: jumps to its framing and, while
367
396
  * simulating, follows/clamps per its settings. Called by loadScene.
368
397
  */
369
398
  setSceneCamera(json) {
370
- // No camera block: leave the camera to the host (constructor viewHeight).
371
- if (!json) {
372
- this.sceneCamera = null;
399
+ // No orthographic block (none, or a 3D scene): leave the camera to the host.
400
+ const cam = (this.sceneCamera = this.cameraRig.adopt(json, this.sceneSpace));
401
+ if (!cam)
373
402
  return;
374
- }
375
- this.sceneCamera = resolveSceneCamera(json);
376
403
  // With a follow target the declared position is moot: start centered on
377
404
  // the target so play begins framed like the editor shows it.
378
- const followed = this.sceneCamera.follow ? this.find(this.sceneCamera.follow) : undefined;
405
+ const followed = cam.follow ? this.find(cam.follow) : undefined;
379
406
  const center = followed
380
407
  ? this.renderPoint(followed.position.x, followed.position.y)
381
- : { x: this.sceneCamera.position[0], y: this.sceneCamera.position[1] };
408
+ : { x: cam.position[0], y: cam.position[1] };
382
409
  this.camera.position.x = center.x;
383
410
  this.camera.position.y = center.y;
384
- this.setViewHeight(this.sceneCamera.zoom);
411
+ this.setViewHeight(cam.zoom);
385
412
  }
386
413
  start() {
387
414
  if (this.disposed)
@@ -438,11 +465,35 @@ export class Game {
438
465
  get projection() {
439
466
  return this.sceneProjection;
440
467
  }
441
- /** Visible world height (2D camera zoom). */
468
+ /** The live scene's space (`render.space`); `'2d'` with no scene (ADR 0027). */
469
+ get space() {
470
+ return this.sceneSpace;
471
+ }
472
+ /** The camera the live scene draws with: orthographic in 2D scenes, perspective in 3D ones. */
473
+ get camera() {
474
+ return this.cameraRig.camera;
475
+ }
476
+ /**
477
+ * Where a logical point falls on the canvas, in CSS px from its top-left
478
+ * corner inside the letterbox; null when a perspective camera cannot see it
479
+ * (behind its near plane). The one projection Anchored Pieces, spatial
480
+ * audio and the editor share (CA-6).
481
+ */
482
+ worldToScreen(point) {
483
+ const { clientWidth: w, clientHeight: h } = this.renderer.domElement;
484
+ const view = gameViewport(w, h, this.resolution);
485
+ const render = this.renderPoint(point.x, point.y);
486
+ const at = worldToNormalized(this.camera, { x: render.x, y: render.y, z: point.z });
487
+ return at && { x: view.x + at.nx * view.width, y: view.y + at.ny * view.height };
488
+ }
489
+ /** Visible world height (2D camera zoom); a perspective camera does not use it. */
442
490
  get view() {
443
491
  return this.viewHeight;
444
492
  }
445
493
  setViewHeight(value) {
494
+ // A perspective camera frames by its lens, not by a visible height.
495
+ if (this.sceneSpace === '3d')
496
+ return;
446
497
  this.viewHeight = Math.min(Math.max(value, 2), 80);
447
498
  this.resize();
448
499
  }
@@ -462,6 +513,7 @@ export class Game {
462
513
  this.time.cancelAll();
463
514
  for (const entity of [...this.entities])
464
515
  entity.destroy();
516
+ this.physics.dispose();
465
517
  sceneDrainsOf(this).clear();
466
518
  this.spriteBatches.unload();
467
519
  // After the entities: their clones go with the cascade above, the
@@ -555,7 +607,7 @@ export class Game {
555
607
  // Positional audio (CA-8): recomputed every frame, on this same pass —
556
608
  // never a second walk of `this.entities`, since `this.audio` already
557
609
  // holds direct references to whichever entities are tracked.
558
- this.audio.updatePlacements(this.audioListenerPosition(), (x, y) => this.renderPoint(x, y));
610
+ this.audio.updatePlacements(listenerPosition(this.sceneSpace, this.camera, this.sceneProjection), (x, y) => this.renderPoint(x, y), this.sceneSpace === '3d' ? this.screenPan : undefined);
559
611
  this.renderSurface();
560
612
  }
561
613
  finally {
@@ -581,7 +633,7 @@ export class Game {
581
633
  for (const component of schedule)
582
634
  component.onUpdate?.(SIMULATION_STEP);
583
635
  }
584
- this.dispatchCollisions();
636
+ runSimulationPhases(this);
585
637
  this.updateSceneCamera(SIMULATION_STEP);
586
638
  advanceCameraEffects(this.cameraEffects);
587
639
  this.finishStep();
@@ -625,11 +677,13 @@ export class Game {
625
677
  if (this.renderSort === 'y')
626
678
  applyYSort(this.entities, sceneDrainsOf(this).participants());
627
679
  this.ui.setActive(this.simulate);
680
+ this.ambientLight3d.sync(this.sceneSpace, this.lighting.ambient);
628
681
  // The shake offset exists only while drawing (ADR 0020): Anchored Pieces
629
682
  // and the render see the drawn center, then the base comes back exactly,
630
683
  // so follow smoothing never starts from its own jitter.
631
684
  const { x, y } = this.camera.position;
632
- const { shake } = this.cameraEffects.state;
685
+ // A perspective camera does not shake: the offset is in orthographic world units.
686
+ const shake = this.sceneSpace === '3d' ? { x: 0, y: 0 } : this.cameraEffects.state.shake;
633
687
  this.camera.position.x = x + shake.x;
634
688
  this.camera.position.y = y + shake.y;
635
689
  try {
@@ -688,48 +742,13 @@ export class Game {
688
742
  return componentsInOrder(result.order, byName);
689
743
  }
690
744
  updateSceneCamera(dt) {
691
- const cam = this.sceneCamera;
692
- if (!cam)
745
+ if (!this.sceneCamera)
693
746
  return;
694
- const followed = cam.follow ? this.find(cam.follow) : undefined;
695
- const provider = followed?.components.find((c) => isCameraVelocityProvider(c));
696
- const velocity = provider?.getCameraVelocity();
697
- const target = followed
698
- ? this.renderPoint(followed.position.x, followed.position.y)
699
- : null;
700
- const renderVelocity = velocity
701
- ? this.renderPoint(velocity.vx, velocity.vy)
702
- : { x: 0, y: 0 };
703
- const next = stepSceneCamera(cam, {
704
- x: this.camera.position.x,
705
- y: this.camera.position.y,
706
- halfW: (this.camera.right - this.camera.left) / 2,
707
- halfH: this.viewHeight / 2,
708
- target,
709
- vx: renderVelocity.x,
710
- vy: renderVelocity.y,
711
- dt,
712
- });
713
- this.camera.position.x = next.x;
714
- this.camera.position.y = next.y;
747
+ this.cameraRig.follow(this.sceneCamera, this.followHost, dt);
715
748
  }
716
749
  renderPoint(x, y) {
717
750
  return this.sceneProjection === 'isometric' ? projectIsometric(x, y) : { x, y };
718
751
  }
719
- /**
720
- * The audio listener's position (CA-8) in logical coordinates. The camera
721
- * itself only ever holds render-space coordinates (see `updateSceneCamera`,
722
- * `setSceneCamera`), so under `projection: 'isometric'` this is the exact
723
- * inverse of `renderPoint` — without it, distance-based attenuation would
724
- * measure render-space distance instead of real game distance.
725
- */
726
- audioListenerPosition() {
727
- const { x, y } = this.camera.position;
728
- return this.sceneProjection === 'isometric' ? unprojectIsometric(x, y) : { x, y };
729
- }
730
- dispatchCollisions() {
731
- dispatchHitboxCollisions(this);
732
- }
733
752
  resize() {
734
753
  const canvas = this.renderer.domElement;
735
754
  const { clientWidth: w, clientHeight: h } = canvas;
@@ -747,13 +766,7 @@ export class Game {
747
766
  this.renderer.setViewport(vx, vy, vw, vh);
748
767
  this.renderer.setScissor(vx, vy, vw, vh);
749
768
  }
750
- const halfH = this.viewHeight / 2;
751
- const halfW = halfH * aspect;
752
- this.camera.left = -halfW;
753
- this.camera.right = halfW;
754
- this.camera.top = halfH;
755
- this.camera.bottom = -halfH;
756
- this.camera.updateProjectionMatrix();
769
+ this.cameraRig.fit(aspect, this.viewHeight);
757
770
  layoutCameraEffects(this.cameraEffects, gameViewport(w, h, this.resolution));
758
771
  }
759
772
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,9 @@ 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';
@@ -21,11 +24,20 @@ export { CameraEffects } from './camera-effects.js';
21
24
  export type { CameraEffectColor, CameraEffectHandle, CameraEffectsOptions, CameraEffectsState, FadeOptions, FlashOptions, ShakeOptions, } from './camera-effects.js';
22
25
  export { GameTime, advanceGameTime } from './game-time.js';
23
26
  export type { EasingName, TimerHandle, TimerOptions, TweenOptions } from './game-time.js';
24
- export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
27
+ export type { SceneCameraJson, OrthographicSceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
28
+ export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
29
+ export type { PerspectiveSceneCameraJson, ResolvedPerspectiveCamera, SceneFieldIssue, Vec3Json, } from './scene-camera-3d.js';
30
+ export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
31
+ export type { GameCamera, NormalizedPoint, ViewCamera, WorldPoint } from './camera-projection.js';
32
+ export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
33
+ export type { SceneSpace, SpaceMarked } from './scene-space.js';
34
+ export type { RuntimeSnapshotView } from './runtime-view-snapshot.js';
35
+ export { applyTransformJson } from './entity-transform.js';
36
+ export type { PositionJson, TransformJson } from './entity-transform.js';
25
37
  export { Entity } from './entity.js';
26
- export { Component } from './component.js';
38
+ export { COMPONENT_SPACES, Component } from './component.js';
27
39
  export { authoringDefaults } from './authoring-defaults.js';
28
- export type { ComponentClass, ContactNormal, ParamSpec, SolidContact, } from './component.js';
40
+ export type { BodyContact, ComponentClass, ComponentSpace, ContactNormal, ParamSpec, SolidContact, } from './component.js';
29
41
  export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
30
42
  export type { ComponentModule } from './component-registry.js';
31
43
  export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
@@ -40,7 +52,7 @@ export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_B
40
52
  export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeBridgeFailure, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
41
53
  export type { RenderBackend } from './render-readiness.js';
42
54
  export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
43
- export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
55
+ export type { RuntimeSnapshotLight, RuntimeSnapshotLighting, RuntimeSnapshotPointLight, RuntimeSnapshotSun, RuntimeSnapshotPost, } from './runtime-lighting-snapshot.js';
44
56
  export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
45
57
  export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
46
58
  export { Stats } from './stats.js';
@@ -49,6 +61,18 @@ export { GameUi } from './ui.js';
49
61
  export type { AnchoredPieceHandle, AttachOptions } from './anchored-pieces.js';
50
62
  export { Sprite } from './components/sprite.js';
51
63
  export { Light } from './components/light.js';
64
+ export { Model } from './components/model.js';
65
+ export type { ModelShape } from './components/model.js';
66
+ export { Sun } from './components/sun.js';
67
+ export { Collider, COLLIDER_SHAPES } from './components/collider.js';
68
+ export type { ColliderShape } from './components/collider.js';
69
+ export { RigidBody, RIGID_BODY_TYPES } from './components/rigid-body.js';
70
+ export type { BodyVector, RigidBodyType } from './components/rigid-body.js';
71
+ export { DEFAULT_GRAVITY, resolveSceneSimulation, sceneSimulationIssues } from './scene-simulation.js';
72
+ export type { ResolvedSceneSimulation, SceneSimulationJson } from './scene-simulation.js';
73
+ export type { PhysicsBackend, RapierModule } from './physics-3d/rapier-module.js';
74
+ export type { RuntimeSnapshotBody, RuntimeSnapshotPhysics } from './runtime-physics-snapshot.js';
75
+ export { PointLight } from './components/point-light.js';
52
76
  export { GameLighting } from './scene-lighting.js';
53
77
  export type { AmbientLightInput } from './scene-lighting.js';
54
78
  export { GamePost } from './post-effects.js';
@@ -74,6 +98,7 @@ export type { TilemapCell, TilemapCellBounds, TilemapGridSpec, } from './tilemap
74
98
  export { COLLISION_SHAPES, DEFAULT_COLLISION_POLYGON, collisionBounds, collisionOverlap, collisionVertices, resolveCollisionPoints, } from './collision-shape.js';
75
99
  export type { CollisionBody, CollisionBounds, CollisionPoint, CollisionShape, } from './collision-shape.js';
76
100
  export type { EntityWith, NearestQueryContext, NearestSpatialQueryFilter, RayHit, SpatialQuery, SpatialQueryFilter, } from './spatial-query.js';
101
+ export type { Point3d, QueryVolume3d, RayHit3d } from './spatial-query-3d.js';
77
102
  export { Emitter } from './events.js';
78
103
  export { loadScene, spawnFromJson, resolveEntityComponents, resolveProps } from './scene.js';
79
104
  export type { SceneJson, SceneEntityJson, SceneComponentJson, SceneRegistry, SceneRenderJson, SceneLightingJson, ScenePostJson, PrefabJson, } from './scene.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
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';
5
6
  export { isYSortBatchParticipant, isYSortParticipant, ySortZ } from './render-sort.js';
@@ -9,8 +10,12 @@ export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepScen
9
10
  export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
10
11
  export { CameraEffects } from './camera-effects.js';
11
12
  export { GameTime, advanceGameTime } from './game-time.js';
13
+ export { isPerspectiveCameraJson, PERSPECTIVE_DEFAULTS, perspectiveCameraIssues, placePerspectiveCamera, resolvePerspectiveCamera, } from './scene-camera-3d.js';
14
+ export { isPerspectiveCamera, worldToNormalized } from './camera-projection.js';
15
+ export { componentSpaceMismatch, componentSpaceOf, entityTransformIssues, resolveSceneSpace, SCENE_SPACES, sceneSpaceIssues, } from './scene-space.js';
16
+ export { applyTransformJson } from './entity-transform.js';
12
17
  export { Entity } from './entity.js';
13
- export { Component } from './component.js';
18
+ export { COMPONENT_SPACES, Component } from './component.js';
14
19
  export { authoringDefaults } from './authoring-defaults.js';
15
20
  export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
16
21
  export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
@@ -23,6 +28,12 @@ export { Stats } from './stats.js';
23
28
  export { GameUi } from './ui.js';
24
29
  export { Sprite } from './components/sprite.js';
25
30
  export { Light } from './components/light.js';
31
+ export { Model } from './components/model.js';
32
+ export { Sun } from './components/sun.js';
33
+ export { Collider, COLLIDER_SHAPES } from './components/collider.js';
34
+ export { RigidBody, RIGID_BODY_TYPES } from './components/rigid-body.js';
35
+ export { DEFAULT_GRAVITY, resolveSceneSimulation, sceneSimulationIssues } from './scene-simulation.js';
36
+ export { PointLight } from './components/point-light.js';
26
37
  export { GameLighting } from './scene-lighting.js';
27
38
  export { GamePost } from './post-effects.js';
28
39
  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;