@waica/engine 0.22.0 → 0.24.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 (47) hide show
  1. package/README.md +33 -0
  2. package/dist/assets/asset-loader.d.ts +1 -1
  3. package/dist/assets/asset-loader.js +1 -1
  4. package/dist/assets/texture-backend.d.ts +1 -1
  5. package/dist/assets/texture-backend.js +1 -1
  6. package/dist/camera.js +1 -1
  7. package/dist/collision-category.js +5 -2
  8. package/dist/collision-dispatch.d.ts +7 -1
  9. package/dist/collision-dispatch.js +18 -11
  10. package/dist/components/animated-sprite.d.ts +2 -0
  11. package/dist/components/animated-sprite.js +42 -7
  12. package/dist/components/sprite.d.ts +6 -0
  13. package/dist/components/sprite.js +53 -5
  14. package/dist/components/tilemap.js +3 -1
  15. package/dist/entity.d.ts +1 -1
  16. package/dist/entity.js +1 -1
  17. package/dist/game.d.ts +22 -3
  18. package/dist/game.js +68 -45
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.js +3 -1
  21. package/dist/particle-batch.d.ts +1 -1
  22. package/dist/particle-batch.js +4 -1
  23. package/dist/particle-sort-bounds.d.ts +1 -1
  24. package/dist/particle-sort-bounds.js +1 -1
  25. package/dist/pointer.js +1 -1
  26. package/dist/render-output.d.ts +27 -0
  27. package/dist/render-output.js +44 -0
  28. package/dist/render-readiness.d.ts +37 -0
  29. package/dist/render-readiness.js +61 -0
  30. package/dist/render-sort.d.ts +14 -0
  31. package/dist/render-sort.js +37 -0
  32. package/dist/runtime-bridge.d.ts +18 -2
  33. package/dist/runtime-bridge.js +7 -1
  34. package/dist/scene.d.ts +10 -0
  35. package/dist/spatial-broadphase.d.ts +7 -1
  36. package/dist/spatial-broadphase.js +16 -45
  37. package/dist/spatial-pairs.d.ts +17 -0
  38. package/dist/spatial-pairs.js +79 -0
  39. package/dist/sprite-batch.d.ts +93 -0
  40. package/dist/sprite-batch.js +223 -0
  41. package/dist/sprite-batches.d.ts +107 -0
  42. package/dist/sprite-batches.js +260 -0
  43. package/dist/sprite-draw-collector.d.ts +42 -0
  44. package/dist/sprite-draw-collector.js +107 -0
  45. package/dist/sprite-runs.d.ts +40 -0
  46. package/dist/sprite-runs.js +49 -0
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,25 @@ Waica's public engine core: entities and components, the game loop, scene and pr
6
6
  import { Component, Game, loadScene } from '@waica/engine'
7
7
  ```
8
8
 
9
+ ## Migrating to 0.24.0: WebGPURenderer and `game.ready()`
10
+
11
+ A `Game` draws through three's `WebGPURenderer` (ADR 0025): WebGPU when the browser offers it, otherwise the renderer's own WebGL2 fallback. The browser decides — there is no option to force either one — and `game.backend` reports which one it got (`'webgpu'` or `'webgl2'`; `null` until the renderer is ready).
12
+
13
+ - **Await `game.ready()` before the first frame.** `new Game(...)` stays synchronous, but the renderer initializes asynchronously. Frames before it finishes still simulate (steps, `onUpdate`) but draw nothing, so a host waits for it before `game.start()`:
14
+
15
+ ```ts
16
+ const game = new Game({ canvas })
17
+ loadScene(game, scene, registry)
18
+ await game.assets.ready()
19
+ await game.ready()
20
+ game.start()
21
+ ```
22
+
23
+ Every call returns the same promise. It rejects with an error naming both `webgpu` and `webgl2` when neither initializes; nothing is ever drawn then. A `Game` disposed while `ready()` is pending never starts, and its renderer is released once initialization settles.
24
+ - **`THREE` is the `three/webgpu` build.** `import { THREE } from '@waica/engine'` now re-exports `three/webgpu`, the same copy of three the engine draws with: `THREE.WebGPURenderer` exists and `THREE.WebGLRenderer` does not. Custom GLSL does not render under `WebGPURenderer` — `ShaderMaterial`, `RawShaderMaterial` and `material.onBeforeCompile` are not supported there — so write custom shading as node materials with TSL (`three/tsl`). Built-in materials such as `MeshBasicMaterial` keep working unchanged.
25
+ - **Tests under happy-dom.** happy-dom has no GPU: a project test that builds a `Game` replaces `WebGPURenderer` from `three/webgpu` with a fake (it previously replaced `WebGLRenderer` from `three`), whose `init()` resolves.
26
+ - **Run Sessions.** A Run Session waits for the Render Backend before it reports ready, steps or screenshots, and its snapshots carry `backend`. A renderer that cannot initialize ends `start_project` with a runtime error that names the failure.
27
+
9
28
  ## Hitbox Collision Layers and Masks
10
29
 
11
30
  Every `Hitbox` belongs to one named Collision Layer and declares the other layers in which it is interested through a Collision Mask:
@@ -150,6 +169,20 @@ The cues that hook a `StateMachine` must come after it and after the `ParticleEm
150
169
 
151
170
  `active`, `capacity`, and `emitting` are the only fields exposed by Runtime Snapshot. Particle arrays remain private. This first CPU implementation is intended for bounded 2D effects, not collision, per-particle scripting, GPU simulation, trails, rotation, or sub-emitters.
152
171
 
172
+ ## Sprite Batches
173
+
174
+ `Sprite` and `AnimatedSprite` are drawn through **Sprite Batches** by default (ADR 0024). Every frame, after y-sort, the engine orders everything it draws exactly as three would without batching — layer bands, then y-sort, then spawn order, with particle batches, tilemaps and any other mesh in between — and each run of consecutive sprites that share a batch key becomes one instanced draw call. The key is the art and how it is sampled: the texture (an `AnimatedSprite`'s current sheet), `pixelArt` and `shape`. Each sprite is one instance carrying its own placement, color, frame and flip, so tinting, resizing, re-anchoring or animating a sprite never creates a material, and what is drawn in front of what never changes.
175
+
176
+ - **On by default.** A scene without a `render` block, or with `render.batch: true`, batches. Nothing to configure: sprites of one texture in a row draw in one call, a sprite of other art (or a particle batch, a tilemap) in between starts a new run.
177
+ - **Culling and Group order kept.** A sprite outside the camera is skipped for the frame, as three culls a sprite's own mesh, and an off-camera renderable never splits a run. A run hangs under a Group with its sprites' Group `renderOrder`, so `entity.node.renderOrder` orders batched sprites exactly as it orders their meshes without batching.
178
+ - **Shared art.** All live sprites of one key share one material and one texture clone, scene-scoped: released with the scene at `unloadScene()` and `game.dispose()`. A sprite whose texture fails moves alone to the untextured entry of its shape and keeps showing its `color`.
179
+ - **Reusable slots and buffers.** A destroyed sprite's slot goes to the next spawn of its key. Each run draws through a pooled instance buffer sized by that run: 16 instances at first, doubling when a longer run needs it, never shrinking during the scene. A key drawn as several runs keeps one buffer per run, so instance memory stays proportional to the sprites drawn. Once the buffers have grown to a scene's longest runs (and as many runs as it ever splits into), spawning, destroying and reordering sprites allocates no GPU objects.
180
+ - **Turning it off.** Scene JSON `"render": { "batch": false }` (the editor's **Sprite batching** toggle in the scene inspector) restores the per-sprite path: one mesh, one material and one draw call per sprite. Use it to rule batching out when a scene looks different than you expect, or while debugging draw order; the cost is one draw call per sprite, which is what dominates frame time with thousands of sprites.
181
+
182
+ Under batching, the object a sprite adds to its entity is a hidden placement anchor that is never drawn. Hide or show a batched sprite through its entity (`entity.node.visible`, as `Health` blinking does); the anchor's own `visible` is not read for the batched draw, and setting it to true makes three also draw the anchor as a plain quad (without the sprite's tint or frame) — leave it alone, or use `render.batch: false` when code needs one real mesh per sprite.
183
+
184
+ How much batching saves depends on how interleaved a scene is: one texture over the whole scene draws in one call, while a top-down scene that alternates many textures by y gains little. Runtime Snapshots are unchanged — batches are not entities, and sprites report the same state either way.
185
+
153
186
  ## Component lifecycle
154
187
 
155
188
  Waica keeps the lifecycle boundaries distinct:
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  import { type TextureBackend } from './texture-backend.js';
3
3
  /** `game.assets.status`: a fresh snapshot of the cache's counters, cumulative for the Game. */
4
4
  export interface AssetStatus {
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  import { ThreeTextureBackend } from './texture-backend.js';
3
3
  import { reportRejection } from '../report-rejection.js';
4
4
  /**
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  /**
3
3
  * The seam between `game.assets` and the browser (ADR 0013's move, applied
4
4
  * to textures — ADR 0019). `load` resolves once the image behind `url` is
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  /** The real implementation: one `THREE.TextureLoader` per Game, wrapped in a promise. */
3
3
  export class ThreeTextureBackend {
4
4
  loader = new THREE.TextureLoader();
package/dist/camera.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  export const CAMERA_DEFAULTS = {
3
3
  position: [0, 0],
4
4
  zoom: 12,
@@ -5,7 +5,10 @@ export function validCollisionLayer(value) {
5
5
  }
6
6
  /** Package-internal exact directional interest check. */
7
7
  export function collisionMaskTargets(mask, targetLayer) {
8
- if (!validCollisionLayer(targetLayer) || !Array.isArray(mask))
8
+ if (!Array.isArray(mask))
9
9
  return false;
10
- return mask.some((entry) => typeof entry === 'string' && (entry === '*' || entry === targetLayer));
10
+ // Membership first: with explicit masks it rejects most pairs of a dense
11
+ // scene before the layer's syntax check runs. A '*' mask still checks it.
12
+ const named = mask.some((entry) => typeof entry === 'string' && (entry === '*' || entry === targetLayer));
13
+ return named && validCollisionLayer(targetLayer);
11
14
  }
@@ -4,5 +4,11 @@ export interface CollisionDispatchStats {
4
4
  readonly candidatePairs: number;
5
5
  readonly narrowphaseCalls: number;
6
6
  }
7
- /** Game's package-internal trigger dispatch implementation. */
7
+ /**
8
+ * Game's package-internal trigger dispatch implementation. Pairs come from a
9
+ * frozen grid; each pair is then checked live, most selective checks first
10
+ * (liveness, masks, then the snapshotted Hitbox still mounted). The checks
11
+ * have no side effects, so their order only changes how fast a pair is
12
+ * rejected.
13
+ */
8
14
  export declare function dispatchCollisions(game: Game): CollisionDispatchStats;
@@ -3,34 +3,41 @@ import { collisionMaskTargets } from './collision-category.js';
3
3
  import { collisionOverlap } from './collision-shape.js';
4
4
  import { Hitbox } from './components/hitbox.js';
5
5
  import { createHitboxBroadphase } from './spatial-broadphase.js';
6
- /** Game's package-internal trigger dispatch implementation. */
6
+ /**
7
+ * Game's package-internal trigger dispatch implementation. Pairs come from a
8
+ * frozen grid; each pair is then checked live, most selective checks first
9
+ * (liveness, masks, then the snapshotted Hitbox still mounted). The checks
10
+ * have no side effects, so their order only changes how fast a pair is
11
+ * rejected.
12
+ */
7
13
  export function dispatchCollisions(game) {
8
- const pairs = createHitboxBroadphase(game).pairs();
14
+ let candidatePairs = 0;
9
15
  let narrowphaseCalls = 0;
10
- for (const [first, second] of pairs) {
16
+ createHitboxBroadphase(game).forEachPair((first, second) => {
17
+ candidatePairs += 1;
11
18
  const a = first.entity;
12
19
  const b = second.entity;
13
20
  if (!a.alive || !b.alive)
14
- continue;
15
- if (a.get(Hitbox) !== first.hitbox || b.get(Hitbox) !== second.hitbox)
16
- continue;
21
+ return;
17
22
  const notifyA = collisionMaskTargets(first.hitbox.collidesWith, second.hitbox.layer);
18
23
  const notifyB = collisionMaskTargets(second.hitbox.collidesWith, first.hitbox.layer);
19
24
  if (!notifyA && !notifyB)
20
- continue;
25
+ return;
26
+ if (a.get(Hitbox) !== first.hitbox || b.get(Hitbox) !== second.hitbox)
27
+ return;
21
28
  narrowphaseCalls += 1;
22
29
  if (!collisionOverlap(collisionBody(first.hitbox), collisionBody(second.hitbox)))
23
- continue;
30
+ return;
24
31
  if (notifyA) {
25
32
  for (const component of [...a.components])
26
33
  component.onCollide?.(b);
27
34
  }
28
35
  if (!a.alive || !b.alive)
29
- continue;
36
+ return;
30
37
  if (notifyB) {
31
38
  for (const component of [...b.components])
32
39
  component.onCollide?.(a);
33
40
  }
34
- }
35
- return { candidatePairs: pairs.length, narrowphaseCalls };
41
+ });
42
+ return { candidatePairs, narrowphaseCalls };
36
43
  }
@@ -113,6 +113,8 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
113
113
  * touches game.assets, so it counts nowhere and warns about nothing.
114
114
  */
115
115
  private sheetTexture;
116
+ /** The batch key of a sheet: its art, this sprite's filtering, a rectangle. */
117
+ private sheetKey;
116
118
  private showFrame;
117
119
  /** Repositions/rescales the displayed frame inside its anchored full-size box. */
118
120
  private syncQuad;
@@ -1,9 +1,12 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  import { Component } from '../component.js';
3
3
  import { ClipPlayer } from '../animation/clip-player.js';
4
4
  import { locateFrame, sheetCell } from '../animation/sheet.js';
5
5
  import { spritePlacement } from '../sprite-placement.js';
6
6
  import { reportRejection } from '../report-rejection.js';
7
+ import { spriteBatchesOf, spriteInstanceOf } from '../sprite-batches.js';
8
+ /** Where a frame on a failed sheet draws: the flat (white) untextured quad. */
9
+ const UNTEXTURED = { texture: null, pixelArt: false, shape: 'rectangle' };
7
10
  const clampAnchor = (value) => Math.min(1, Math.max(0, value));
8
11
  /**
9
12
  * Sprite animated from one or more spritesheets. The main sheet is the
@@ -139,6 +142,8 @@ export class AnimatedSprite extends Component {
139
142
  texs = [];
140
143
  // Clones whose sheet failed to load: applyFrame never installs them (CA-4).
141
144
  failedSheets = new Set();
145
+ // The drawn quad, or under Sprite Batches (ADR 0024) the hidden anchor of
146
+ // this sprite's instance, whose frame UV is read from the sheet clone.
142
147
  mesh;
143
148
  frame = 0;
144
149
  // Current frame's quad scale relative to the sheet's largest cell: always
@@ -169,8 +174,17 @@ export class AnimatedSprite extends Component {
169
174
  tex.repeat.set(1 / Math.max(1, sheet.cols), 1 / Math.max(1, sheet.rows));
170
175
  return tex;
171
176
  });
172
- const material = new THREE.MeshBasicMaterial({ map: this.texs[0], transparent: true });
173
- this.mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
177
+ const batches = spriteBatchesOf(this.game);
178
+ if (batches?.enabled) {
179
+ const instance = batches.attach(this.sheetKey(0));
180
+ instance.shownSheet = 0;
181
+ instance.uvSource = this.texs[0] ?? instance.uvSource;
182
+ this.mesh = instance.anchor;
183
+ }
184
+ else {
185
+ const material = new THREE.MeshBasicMaterial({ map: this.texs[0], transparent: true });
186
+ this.mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
187
+ }
174
188
  this.mesh.position.z = this.layer * 0.01;
175
189
  this.syncQuad();
176
190
  this.entity.node.add(this.mesh);
@@ -193,9 +207,16 @@ export class AnimatedSprite extends Component {
193
207
  this.showFrame(this.player.advance(dt));
194
208
  }
195
209
  onDestroy() {
196
- this.mesh?.removeFromParent();
197
- this.mesh?.geometry.dispose();
198
- this.mesh?.material.dispose();
210
+ const instance = spriteInstanceOf(this.mesh);
211
+ if (instance) {
212
+ instance.release();
213
+ }
214
+ else {
215
+ this.mesh?.removeFromParent();
216
+ this.mesh?.geometry.dispose();
217
+ this.mesh?.material.dispose();
218
+ }
219
+ this.mesh = undefined;
199
220
  // Only this sprite's clones: the cached bases live with the Game.
200
221
  for (const tex of this.texs)
201
222
  tex.dispose();
@@ -225,6 +246,10 @@ export class AnimatedSprite extends Component {
225
246
  }), 'sheet texture settle');
226
247
  return texture;
227
248
  }
249
+ /** The batch key of a sheet: its art, this sprite's filtering, a rectangle. */
250
+ sheetKey(index) {
251
+ return { texture: this.sheets[index]?.texture ?? '', pixelArt: this.pixelArt, shape: 'rectangle' };
252
+ }
228
253
  showFrame(index) {
229
254
  this.frame = index;
230
255
  this.applyFrame();
@@ -254,7 +279,17 @@ export class AnimatedSprite extends Component {
254
279
  const tex = this.texs[located.sheet];
255
280
  if (!sheet || !tex)
256
281
  return;
257
- if (this.mesh) {
282
+ const instance = spriteInstanceOf(this.mesh);
283
+ if (instance) {
284
+ const failed = this.failedSheets.has(tex);
285
+ if (instance.shownSheet !== located.sheet || instance.shownSheetFailed !== failed) {
286
+ instance.moveTo(failed ? UNTEXTURED : this.sheetKey(located.sheet));
287
+ instance.shownSheet = located.sheet;
288
+ instance.shownSheetFailed = failed;
289
+ }
290
+ instance.uvSource = tex;
291
+ }
292
+ else if (this.mesh) {
258
293
  const map = this.failedSheets.has(tex) ? null : tex;
259
294
  if (this.mesh.material.map !== map) {
260
295
  this.mesh.material.map = map;
@@ -68,6 +68,12 @@ export declare class Sprite extends Component implements YSortParticipant {
68
68
  set shape(value: SpriteShape);
69
69
  private mesh?;
70
70
  onReady(): void;
71
+ private buildMesh;
72
+ /**
73
+ * One instance in the batch of this sprite's key, tinted like the material
74
+ * the per-sprite path would build: white over a texture, `color` without.
75
+ */
76
+ private joinBatch;
71
77
  onDestroy(): void;
72
78
  /**
73
79
  * CA-4's failure rule: an image that never arrives leaves the flat
@@ -1,7 +1,8 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  import { Component } from '../component.js';
3
3
  import { spritePlacement } from '../sprite-placement.js';
4
4
  import { reportRejection } from '../report-rejection.js';
5
+ import { spriteBatchesOf, spriteInstanceOf } from '../sprite-batches.js';
5
6
  const clampAnchor = (value) => Math.min(1, Math.max(0, value));
6
7
  /**
7
8
  * Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
@@ -41,7 +42,11 @@ export class Sprite extends Component {
41
42
  }
42
43
  set color(value) {
43
44
  this._color = value;
44
- this.mesh?.material.color.setHex(value);
45
+ const instance = spriteInstanceOf(this.mesh);
46
+ if (instance)
47
+ instance.color.setHex(value);
48
+ else
49
+ this.mesh?.material.color.setHex(value);
45
50
  }
46
51
  _offsetX = 0;
47
52
  _offsetY = 0;
@@ -102,11 +107,30 @@ export class Sprite extends Component {
102
107
  this._shape = value === 'circle' ? 'circle' : 'rectangle';
103
108
  if (!this.mesh)
104
109
  return;
110
+ const instance = spriteInstanceOf(this.mesh);
111
+ if (instance) {
112
+ instance.moveTo({ ...instance.key, shape: this._shape });
113
+ return;
114
+ }
105
115
  this.mesh.geometry.dispose();
106
116
  this.mesh.geometry = this.createGeometry();
107
117
  }
118
+ // The drawn quad, or under Sprite Batches (ADR 0024) the hidden anchor of
119
+ // this sprite's instance: the same placement either way.
108
120
  mesh;
109
121
  onReady() {
122
+ const batches = spriteBatchesOf(this.game);
123
+ if (batches?.enabled)
124
+ this.joinBatch(batches);
125
+ else
126
+ this.buildMesh();
127
+ if (!this.mesh)
128
+ return;
129
+ this.mesh.position.z = this.layer * 0.01;
130
+ this.syncQuad();
131
+ this.entity.node.add(this.mesh);
132
+ }
133
+ buildMesh() {
110
134
  const material = new THREE.MeshBasicMaterial({ color: this.color, transparent: true });
111
135
  if (this.texture) {
112
136
  // Its own clone of the cached base (game.assets, ADR 0019): filters
@@ -125,11 +149,35 @@ export class Sprite extends Component {
125
149
  }), 'sprite texture settle');
126
150
  }
127
151
  this.mesh = new THREE.Mesh(this.createGeometry(), material);
128
- this.mesh.position.z = this.layer * 0.01;
129
- this.syncQuad();
130
- this.entity.node.add(this.mesh);
152
+ }
153
+ /**
154
+ * One instance in the batch of this sprite's key, tinted like the material
155
+ * the per-sprite path would build: white over a texture, `color` without.
156
+ */
157
+ joinBatch(batches) {
158
+ const key = { texture: this.texture || null, pixelArt: this.pixelArt, shape: this._shape };
159
+ const instance = batches.attach(key);
160
+ instance.color.setHex(this.texture ? 0xffffff : this.color);
161
+ this.mesh = instance.anchor;
162
+ const settled = batches.settled(key);
163
+ if (!settled)
164
+ return;
165
+ reportRejection(settled.then((outcome) => {
166
+ // CA-4's failure rule, copy-on-write: only this sprite leaves the
167
+ // shared entry, for the untextured one of its shape, with its color.
168
+ if (outcome !== 'failed' || spriteInstanceOf(this.mesh) !== instance)
169
+ return;
170
+ instance.moveTo({ ...instance.key, texture: null });
171
+ instance.color.setHex(this.color);
172
+ }), 'sprite texture settle');
131
173
  }
132
174
  onDestroy() {
175
+ const instance = spriteInstanceOf(this.mesh);
176
+ if (instance) {
177
+ instance.release();
178
+ this.mesh = undefined;
179
+ return;
180
+ }
133
181
  this.mesh?.removeFromParent();
134
182
  this.mesh?.geometry.dispose();
135
183
  // Only this sprite's clone: the cached base lives with the Game.
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  import { sheetCell } from '../animation/sheet.js';
3
3
  import { Component } from '../component.js';
4
4
  import { projectIsometric } from '../projection.js';
@@ -283,6 +283,8 @@ export class Tilemap extends Component {
283
283
  geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(buffers.indices), 1));
284
284
  mesh.geometry.dispose();
285
285
  mesh.geometry = geometry;
286
+ // WebGPURenderer issues a zero-count draw (WebGLRenderer skipped it): no tile, no draw.
287
+ mesh.visible = buffers.indices.length > 0;
286
288
  }
287
289
  /** The loaded sheet image's size, or one pixel per sheet cell until it arrives. */
288
290
  sheetSize() {
package/dist/entity.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  import type { Component, ComponentClass } from './component.js';
3
3
  import type { Game } from './game.js';
4
4
  /**
package/dist/entity.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  /**
3
3
  * A live scene node: a transform (three Group) + components.
4
4
  * Created with `game.spawn(name)`.
package/dist/game.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as THREE from 'three';
1
+ import * as THREE from 'three/webgpu';
2
2
  import { AssetLoader } from './assets/asset-loader.js';
3
3
  import type { TextureBackend } from './assets/texture-backend.js';
4
4
  import type { AudioBackend } from './audio/backend.js';
@@ -11,6 +11,7 @@ import { Emitter } from './events.js';
11
11
  import { GameTime } from './game-time.js';
12
12
  import { Input, type InputBindings } from './input.js';
13
13
  import { Pointer } from './pointer.js';
14
+ import { type RenderBackend } from './render-readiness.js';
14
15
  import { type SceneJson, type SceneRegistry, type SceneRenderJson } from './scene.js';
15
16
  import { type SpatialQuery } from './spatial-query.js';
16
17
  import { Stats, type StatValue } from './stats.js';
@@ -101,8 +102,12 @@ export declare class Game {
101
102
  * or collisions — the editor's edit mode.
102
103
  */
103
104
  simulate: boolean;
105
+ /** WebGPU when the browser offers it, else three's WebGL2 fallback (ADR 0025). */
104
106
  private readonly renderer;
107
+ private readonly readiness;
105
108
  private readonly resizeObserver;
109
+ /** Scene-scoped Sprite Batches (ADR 0024); `render.batch: false` opts a scene out. */
110
+ private readonly spriteBatches;
106
111
  private readonly updateFns;
107
112
  private readonly invalidUpdateCompositions;
108
113
  /**
@@ -133,9 +138,20 @@ export declare class Game {
133
138
  private liveSceneName;
134
139
  /** True for the whole extent of a runFrame() call, incl. its tail. */
135
140
  private insideFrame;
141
+ /** Set by dispose(): a disposed Game never starts again nor reports its renderer (review #3). */
142
+ private disposed;
136
143
  /** A loadSceneByName() enqueued while insideFrame; applied at the next runFrame's start. */
137
144
  private pendingSceneLoad;
138
145
  constructor(options: GameOptions);
146
+ /**
147
+ * Resolves once the renderer can draw; rejects, naming both Render
148
+ * Backends, when neither initializes. Every call returns the same promise.
149
+ * Frames before it simulate but draw nothing, so a host awaits it before
150
+ * its first frame (ADR 0025).
151
+ */
152
+ ready(): Promise<void>;
153
+ /** The Render Backend the renderer settled on; null until ready() resolved. */
154
+ get backend(): RenderBackend | null;
139
155
  /** Creates a live entity in the scene. */
140
156
  spawn(name: string): Entity;
141
157
  /** Instantiates a registered prefab after a scene has supplied the registry. */
@@ -203,6 +219,11 @@ export declare class Game {
203
219
  setViewHeight(value: number): void;
204
220
  /** Shuts the game down completely (loop, input, GPU). */
205
221
  dispose(): void;
222
+ /**
223
+ * three installs the loop once its init settled; a failed init is
224
+ * reported by ready(), not by every loop change made before it.
225
+ */
226
+ private setAnimationLoop;
206
227
  /**
207
228
  * Real-time playback for the Runtime Bridge: the same clock-driven loop
208
229
  * as start(), sharing the accumulator, with `onStep` told after every
@@ -246,8 +267,6 @@ export declare class Game {
246
267
  private runHostUpdates;
247
268
  private flushPendingSceneLoad;
248
269
  private unregisterRuntimeBridge;
249
- /** Under y-sort, re-derives every participant's z from layer band + entity Y. */
250
- private applyYSort;
251
270
  private renderSurface;
252
271
  private componentUpdateSchedule;
253
272
  private updateSceneCamera;