@waica/engine 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/assets/asset-loader.d.ts +76 -0
- package/dist/assets/asset-loader.js +138 -0
- package/dist/assets/texture-backend.d.ts +17 -0
- package/dist/assets/texture-backend.js +15 -0
- package/dist/components/animated-sprite.d.ts +17 -4
- package/dist/components/animated-sprite.js +41 -12
- package/dist/components/sprite.d.ts +6 -0
- package/dist/components/sprite.js +28 -6
- package/dist/components/tilemap.d.ts +6 -0
- package/dist/components/tilemap.js +28 -4
- package/dist/game.d.ts +15 -0
- package/dist/game.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/runtime-bridge.d.ts +11 -2
- package/dist/runtime-bridge.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -224,3 +224,25 @@ bar.remove()
|
|
|
224
224
|
- **Runtime Snapshot.** Every snapshot carries `ui: { shown, anchored }`: the visible screen pieces by name, and each live instance in creation order as `{ piece, entity, x, y, clipped, values }`, with the pixel coordinates of its last placement; `values` are bounded like component state (a string over 4 KiB becomes a `$waica: 'truncated'` marker), and once every entity is cut the 1 MiB snapshot cap drops instances from the end.
|
|
225
225
|
|
|
226
226
|
The trade-off is ADR 0018's: Anchored Pieces are HTML drawn over the game view, not text rendered in the three scene. They always draw above the world — a label behind a tree draws over it — hide with the rest of the UI overlay while the Game is not simulating (including the editor's edit mode), and do not align to a pixel-art grid.
|
|
227
|
+
|
|
228
|
+
## game.assets: textures, preload and Assets Ready
|
|
229
|
+
|
|
230
|
+
Every `Game` owns an `assets: AssetLoader` — the engine's texture cache. `Sprite`, `AnimatedSprite` and `Tilemap` load through it: one fetch and one base texture per URL for the whole life of the Game, and every component gets its own clone of that base (three shares the GPU upload between clones with equal sampler parameters and reference-counts their disposal), so eight crates cost one image. A scene load never waits for images — `loadScene` stays synchronous — and **Assets Ready** is a promise beside it (ADR 0019):
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
game.registerSceneCatalog({ scenes, registry })
|
|
234
|
+
game.loadSceneByName('main')
|
|
235
|
+
await game.assets.ready() // every texture requested so far has loaded, or failed and been recorded
|
|
236
|
+
game.start()
|
|
237
|
+
|
|
238
|
+
await game.assets.preload(['waica:iso-crate', 'src/art/tree.png']) // ahead of a later scene
|
|
239
|
+
game.assets.status // { pending, loaded, failed } — a fresh object on every read
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- **Cache rule.** Keyed by the URL as received: components pass what `resolveProps` resolved, `preload` resolves each uri through the registered scene catalog first (identity before `registerSceneCatalog`, and for a uri the resolver leaves unchanged). N requests of one URL call the backend once; `status.loaded` counts URLs, not requests. Keep-all: `unloadScene()`, `loadScene` and `loadSceneByName` never dispose, evict or reset an entry — `main → cave → main` re-downloads nothing — and only `game.dispose()` disposes every cached base, exactly once, leaving `status` at zeros. Components dispose only their own clone.
|
|
243
|
+
- **`ready()`.** Resolves the first time `pending` is 0 after the call: at once when nothing is pending, otherwise once every URL requested before or while waiting has settled. Readiness is relative to the moment it is awaited — a later spawn that requests new art reopens it until that art settles — and concurrent callers all resolve. It never rejects, and it has no timeout: a stalled image with no error event holds a host that awaits it (a Run Session is bounded by the MCP's session timeout instead).
|
|
244
|
+
- **`preload(uris)`.** Requests every uri and resolves once all of them settled, failures included. A component that later asks for the same resolved URL is a cache hit.
|
|
245
|
+
- **`status`.** `pending` is current; `loaded` and `failed` are cumulative for the Game and only grow until `dispose()`. A host that wants a loading bar reads it from `game.onUpdate`.
|
|
246
|
+
- **Failure rule.** A texture that fails to load is recorded, not thrown: `console.warn('[waica] assets: failed to load "<url>"', error)` once per Game per URL, `failed` counts it, `ready()` resolves, and every consumer falls back to its flat material — `Sprite` and `Tilemap` drop the failed map, dispose their clone and render their `color` again; `AnimatedSprite` never installs the failed sheet's clone, so that sheet's frames show a plain white quad. Asking for that URL again on the same Game is a cached failure — no retry, no second warning.
|
|
247
|
+
- **`GameOptions.textures`.** Replaces the real `THREE.TextureLoader` backend for that Game (ADR 0013's seam, applied to textures): `happy-dom` decodes no images, so a project's own tests inject a backend that resolves at once. The Runtime Bridge reports `assets` in its metadata under the `'assets'` capability, and a Run Session waits for `pending === 0` at readiness, after a `scene` operation and before every screenshot.
|
|
248
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { type TextureBackend } from './texture-backend.js';
|
|
3
|
+
/** `game.assets.status`: a fresh snapshot of the cache's counters, cumulative for the Game. */
|
|
4
|
+
export interface AssetStatus {
|
|
5
|
+
/** Requested URLs that have not settled yet. */
|
|
6
|
+
pending: number;
|
|
7
|
+
/** Settled URLs whose image arrived. */
|
|
8
|
+
loaded: number;
|
|
9
|
+
/** Settled URLs whose load failed — recorded, warned once, never retried. */
|
|
10
|
+
failed: number;
|
|
11
|
+
}
|
|
12
|
+
export type TextureOutcome = 'loaded' | 'failed';
|
|
13
|
+
/** What a consumer gets from `game.assets.texture(url)`: its own clone, and when the image settled. */
|
|
14
|
+
export interface TextureHandle {
|
|
15
|
+
texture: THREE.Texture;
|
|
16
|
+
settled: Promise<TextureOutcome>;
|
|
17
|
+
}
|
|
18
|
+
export interface AssetLoaderOptions {
|
|
19
|
+
/** Replaces the real `THREE.TextureLoader` implementation (ADR 0013); defaults to it. */
|
|
20
|
+
backend?: TextureBackend;
|
|
21
|
+
/**
|
|
22
|
+
* Resolves a uri for `preload()` the way the scene loader resolves every
|
|
23
|
+
* prefab's string prop (`resolveProps` in scene.ts). Looked up on every
|
|
24
|
+
* call: `Game` wires this to its registered scene catalog, which survives
|
|
25
|
+
* `unloadScene()`. Defaults to identity, so an already-resolved or
|
|
26
|
+
* unresolvable uri passes through unchanged either way.
|
|
27
|
+
*/
|
|
28
|
+
resolveAsset?: (uri: string) => string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The engine's texture cache (`game.assets`, ADR 0019): keep-all for the
|
|
32
|
+
* Game's whole life, keyed by the URL as received, one backend load and one
|
|
33
|
+
* base texture per URL. `ready()` is the **Assets Ready** signal — a
|
|
34
|
+
* promise beside the synchronous scene load — and a failed image is
|
|
35
|
+
* recorded, not thrown, so it never breaks a host or a Run Session that
|
|
36
|
+
* awaits it. `unloadScene()` never touches this cache; `dispose()` empties it.
|
|
37
|
+
*/
|
|
38
|
+
export declare class AssetLoader {
|
|
39
|
+
private readonly backend;
|
|
40
|
+
private readonly resolveAsset;
|
|
41
|
+
private readonly entries;
|
|
42
|
+
private pendingCount;
|
|
43
|
+
private loadedCount;
|
|
44
|
+
private failedCount;
|
|
45
|
+
constructor(options?: AssetLoaderOptions);
|
|
46
|
+
/** A fresh object on every read: `pending` is current, `loaded` and `failed` only grow until `dispose()`. */
|
|
47
|
+
get status(): AssetStatus;
|
|
48
|
+
/**
|
|
49
|
+
* The consumer entry (Sprite, AnimatedSprite, Tilemap): requests `url`
|
|
50
|
+
* — already resolved by `resolveProps` — and returns a clone of its base
|
|
51
|
+
* right away, so a mesh exists before the image arrives, plus the
|
|
52
|
+
* settlement of that URL, which also fires on a cache hit.
|
|
53
|
+
*/
|
|
54
|
+
texture(url: string): TextureHandle;
|
|
55
|
+
/**
|
|
56
|
+
* Requests every uri ahead of time, each resolved through the registered
|
|
57
|
+
* scene catalog. Resolves once all of them settled, failures included —
|
|
58
|
+
* a failure still only warns once, the same as a component's request.
|
|
59
|
+
*/
|
|
60
|
+
preload(uris: string[]): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Assets Ready: resolves the first time `pending` is 0 after the call —
|
|
63
|
+
* at once when nothing is pending, otherwise once every URL requested
|
|
64
|
+
* before or while waiting has settled. Never rejects. Resolves after the
|
|
65
|
+
* `settled` continuations consumers chained before the call, so a host
|
|
66
|
+
* that does `await game.assets.ready()` then `game.start()` renders its
|
|
67
|
+
* first frame with the textures in place.
|
|
68
|
+
*/
|
|
69
|
+
ready(): Promise<void>;
|
|
70
|
+
/** Disposes every cached base exactly once and forgets everything; called by `Game.dispose()`. */
|
|
71
|
+
dispose(): void;
|
|
72
|
+
private request;
|
|
73
|
+
private load;
|
|
74
|
+
/** One backend load as an outcome, never a rejection — a backend that throws synchronously included. */
|
|
75
|
+
private attempt;
|
|
76
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { ThreeTextureBackend } from './texture-backend.js';
|
|
3
|
+
/**
|
|
4
|
+
* Not `base.clone()`: `Texture.copy` sets `needsUpdate`, which bumps the
|
|
5
|
+
* shared Source's version — a GPU re-upload of the same image for every
|
|
6
|
+
* clone — and, while the image is still missing, makes the renderer warn
|
|
7
|
+
* on every frame. Sharing the Source and mirroring `version` instead marks
|
|
8
|
+
* the clone uploadable exactly when the base is, and three then keeps one
|
|
9
|
+
* GPU texture per (Source, sampler parameters), reference-counted across
|
|
10
|
+
* clones (WebGLTextures). Colour space is the base's; filters, repeat and
|
|
11
|
+
* offset are the clone's own.
|
|
12
|
+
*/
|
|
13
|
+
function cloneOf(base) {
|
|
14
|
+
const clone = new THREE.Texture();
|
|
15
|
+
clone.source = base.source;
|
|
16
|
+
clone.colorSpace = base.colorSpace;
|
|
17
|
+
clone.version = base.version;
|
|
18
|
+
return clone;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The engine's texture cache (`game.assets`, ADR 0019): keep-all for the
|
|
22
|
+
* Game's whole life, keyed by the URL as received, one backend load and one
|
|
23
|
+
* base texture per URL. `ready()` is the **Assets Ready** signal — a
|
|
24
|
+
* promise beside the synchronous scene load — and a failed image is
|
|
25
|
+
* recorded, not thrown, so it never breaks a host or a Run Session that
|
|
26
|
+
* awaits it. `unloadScene()` never touches this cache; `dispose()` empties it.
|
|
27
|
+
*/
|
|
28
|
+
export class AssetLoader {
|
|
29
|
+
backend;
|
|
30
|
+
resolveAsset;
|
|
31
|
+
entries = new Map();
|
|
32
|
+
pendingCount = 0;
|
|
33
|
+
loadedCount = 0;
|
|
34
|
+
failedCount = 0;
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.backend = options.backend ?? new ThreeTextureBackend();
|
|
37
|
+
this.resolveAsset = options.resolveAsset ?? ((uri) => uri);
|
|
38
|
+
}
|
|
39
|
+
/** A fresh object on every read: `pending` is current, `loaded` and `failed` only grow until `dispose()`. */
|
|
40
|
+
get status() {
|
|
41
|
+
return { pending: this.pendingCount, loaded: this.loadedCount, failed: this.failedCount };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The consumer entry (Sprite, AnimatedSprite, Tilemap): requests `url`
|
|
45
|
+
* — already resolved by `resolveProps` — and returns a clone of its base
|
|
46
|
+
* right away, so a mesh exists before the image arrives, plus the
|
|
47
|
+
* settlement of that URL, which also fires on a cache hit.
|
|
48
|
+
*/
|
|
49
|
+
texture(url) {
|
|
50
|
+
const entry = this.request(url);
|
|
51
|
+
const texture = cloneOf(entry.base);
|
|
52
|
+
if (entry.outcome === null) {
|
|
53
|
+
// Chained here, before the consumer chains its own continuation on
|
|
54
|
+
// `settled`, so the clone is uploadable by the time that one runs.
|
|
55
|
+
void entry.settled.then((outcome) => {
|
|
56
|
+
if (outcome === 'loaded')
|
|
57
|
+
texture.version = entry.base.version;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return { texture, settled: entry.settled };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Requests every uri ahead of time, each resolved through the registered
|
|
64
|
+
* scene catalog. Resolves once all of them settled, failures included —
|
|
65
|
+
* a failure still only warns once, the same as a component's request.
|
|
66
|
+
*/
|
|
67
|
+
async preload(uris) {
|
|
68
|
+
await Promise.all(uris.map((uri) => this.request(this.resolveAsset(uri)).settled));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Assets Ready: resolves the first time `pending` is 0 after the call —
|
|
72
|
+
* at once when nothing is pending, otherwise once every URL requested
|
|
73
|
+
* before or while waiting has settled. Never rejects. Resolves after the
|
|
74
|
+
* `settled` continuations consumers chained before the call, so a host
|
|
75
|
+
* that does `await game.assets.ready()` then `game.start()` renders its
|
|
76
|
+
* first frame with the textures in place.
|
|
77
|
+
*/
|
|
78
|
+
async ready() {
|
|
79
|
+
while (this.pendingCount > 0) {
|
|
80
|
+
const inFlight = [...this.entries.values()].filter((entry) => entry.outcome === null);
|
|
81
|
+
await Promise.all(inFlight.map((entry) => entry.settled));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Disposes every cached base exactly once and forgets everything; called by `Game.dispose()`. */
|
|
85
|
+
dispose() {
|
|
86
|
+
for (const entry of this.entries.values())
|
|
87
|
+
entry.base.dispose();
|
|
88
|
+
this.entries.clear();
|
|
89
|
+
this.pendingCount = 0;
|
|
90
|
+
this.loadedCount = 0;
|
|
91
|
+
this.failedCount = 0;
|
|
92
|
+
}
|
|
93
|
+
request(url) {
|
|
94
|
+
const existing = this.entries.get(url);
|
|
95
|
+
if (existing)
|
|
96
|
+
return existing;
|
|
97
|
+
const base = new THREE.Texture();
|
|
98
|
+
base.colorSpace = THREE.SRGBColorSpace;
|
|
99
|
+
const entry = { base, outcome: null, settled: Promise.resolve('failed') };
|
|
100
|
+
// Registered and counted before the backend runs, so a backend that
|
|
101
|
+
// throws synchronously still settles this very entry.
|
|
102
|
+
this.entries.set(url, entry);
|
|
103
|
+
this.pendingCount += 1;
|
|
104
|
+
entry.settled = this.load(url, entry);
|
|
105
|
+
return entry;
|
|
106
|
+
}
|
|
107
|
+
async load(url, entry) {
|
|
108
|
+
const attempt = await this.attempt(url);
|
|
109
|
+
// A dispose() in flight already forgot this entry and disposed its base:
|
|
110
|
+
// nothing to write, warn about or count. Only the settlement is still
|
|
111
|
+
// owed, so a consumer awaiting it never hangs; it reports what the
|
|
112
|
+
// backend did.
|
|
113
|
+
if (this.entries.get(url) !== entry)
|
|
114
|
+
return attempt.outcome;
|
|
115
|
+
if (attempt.outcome === 'loaded') {
|
|
116
|
+
// The image lands on the base's Source, which every clone shares.
|
|
117
|
+
entry.base.image = attempt.texture.image;
|
|
118
|
+
entry.base.needsUpdate = true;
|
|
119
|
+
this.loadedCount += 1;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.warn(`[waica] assets: failed to load "${url}"`, attempt.error);
|
|
123
|
+
this.failedCount += 1;
|
|
124
|
+
}
|
|
125
|
+
entry.outcome = attempt.outcome;
|
|
126
|
+
this.pendingCount -= 1;
|
|
127
|
+
return attempt.outcome;
|
|
128
|
+
}
|
|
129
|
+
/** One backend load as an outcome, never a rejection — a backend that throws synchronously included. */
|
|
130
|
+
async attempt(url) {
|
|
131
|
+
try {
|
|
132
|
+
return { outcome: 'loaded', texture: await this.backend.load(url) };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return { outcome: 'failed', error };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* The seam between `game.assets` and the browser (ADR 0013's move, applied
|
|
4
|
+
* to textures — ADR 0019). `load` resolves once the image behind `url` is
|
|
5
|
+
* decoded, with a texture whose `image` the loader adopts into its own
|
|
6
|
+
* cached base; it rejects on a load error and never throws synchronously.
|
|
7
|
+
* `happy-dom` decodes no images, so a test injects `FakeTextureBackend`
|
|
8
|
+
* (assets/test-helpers.ts) through `GameOptions.textures`.
|
|
9
|
+
*/
|
|
10
|
+
export interface TextureBackend {
|
|
11
|
+
load(url: string): Promise<THREE.Texture>;
|
|
12
|
+
}
|
|
13
|
+
/** The real implementation: one `THREE.TextureLoader` per Game, wrapped in a promise. */
|
|
14
|
+
export declare class ThreeTextureBackend implements TextureBackend {
|
|
15
|
+
private readonly loader;
|
|
16
|
+
load(url: string): Promise<THREE.Texture>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/** The real implementation: one `THREE.TextureLoader` per Game, wrapped in a promise. */
|
|
3
|
+
export class ThreeTextureBackend {
|
|
4
|
+
loader = new THREE.TextureLoader();
|
|
5
|
+
load(url) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
try {
|
|
8
|
+
this.loader.load(url, resolve, undefined, reject);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
reject(error);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -6,10 +6,11 @@ import type { YSortParticipant } from '../render-sort.js';
|
|
|
6
6
|
* Sprite animated from one or more spritesheets. The main sheet is the
|
|
7
7
|
* top-level texture/cols/rows (or explicit cells); extraSheets append after
|
|
8
8
|
* it, and clip frames index the sheets consecutively (sheet 0 owns 0..n0-1,
|
|
9
|
-
* sheet 1 the next n1, …). Each instance
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* sheet 1 the next n1, …). Each instance owns its own clones of the cached
|
|
10
|
+
* sheets (game.assets, ADR 0019) to animate UVs independently. On sheets
|
|
11
|
+
* with explicit cells the frames vary in pixel size, so the quad rescales
|
|
12
|
+
* per frame, anchored bottom-center — width/height size the sheet's largest
|
|
13
|
+
* frame and smaller ones keep their feet planted.
|
|
13
14
|
*/
|
|
14
15
|
export declare class AnimatedSprite extends Component implements YSortParticipant {
|
|
15
16
|
static componentName: string;
|
|
@@ -91,6 +92,7 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
|
|
|
91
92
|
private readonly player;
|
|
92
93
|
private sheets;
|
|
93
94
|
private texs;
|
|
95
|
+
private readonly failedSheets;
|
|
94
96
|
private mesh?;
|
|
95
97
|
private frame;
|
|
96
98
|
private frameScaleX;
|
|
@@ -100,6 +102,17 @@ export declare class AnimatedSprite extends Component implements YSortParticipan
|
|
|
100
102
|
play(name: string): void;
|
|
101
103
|
onUpdate(dt: number): void;
|
|
102
104
|
onDestroy(): void;
|
|
105
|
+
/**
|
|
106
|
+
* The sheet's own clone of the cached base. Non-uniform slicing needs
|
|
107
|
+
* the image's pixel size, so the current frame is re-applied once the
|
|
108
|
+
* sheet settles — on a cache hit too, whose settlement is already
|
|
109
|
+
* resolved — unless this sprite was destroyed meanwhile. A sheet that
|
|
110
|
+
* fails is remembered so the quad shows its flat material on that
|
|
111
|
+
* sheet's frames instead of sampling an image that never arrived (CA-4).
|
|
112
|
+
* An empty url (a sheet not authored yet) owns a bare texture and never
|
|
113
|
+
* touches game.assets, so it counts nowhere and warns about nothing.
|
|
114
|
+
*/
|
|
115
|
+
private sheetTexture;
|
|
103
116
|
private showFrame;
|
|
104
117
|
/** Repositions/rescales the displayed frame inside its anchored full-size box. */
|
|
105
118
|
private syncQuad;
|
|
@@ -3,16 +3,16 @@ 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
|
-
const loader = new THREE.TextureLoader();
|
|
7
6
|
const clampAnchor = (value) => Math.min(1, Math.max(0, value));
|
|
8
7
|
/**
|
|
9
8
|
* Sprite animated from one or more spritesheets. The main sheet is the
|
|
10
9
|
* top-level texture/cols/rows (or explicit cells); extraSheets append after
|
|
11
10
|
* it, and clip frames index the sheets consecutively (sheet 0 owns 0..n0-1,
|
|
12
|
-
* sheet 1 the next n1, …). Each instance
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* sheet 1 the next n1, …). Each instance owns its own clones of the cached
|
|
12
|
+
* sheets (game.assets, ADR 0019) to animate UVs independently. On sheets
|
|
13
|
+
* with explicit cells the frames vary in pixel size, so the quad rescales
|
|
14
|
+
* per frame, anchored bottom-center — width/height size the sheet's largest
|
|
15
|
+
* frame and smaller ones keep their feet planted.
|
|
16
16
|
*/
|
|
17
17
|
export class AnimatedSprite extends Component {
|
|
18
18
|
static componentName = 'AnimatedSprite';
|
|
@@ -30,6 +30,7 @@ export class AnimatedSprite extends Component {
|
|
|
30
30
|
'player',
|
|
31
31
|
'sheets',
|
|
32
32
|
'texs',
|
|
33
|
+
'failedSheets',
|
|
33
34
|
'mesh',
|
|
34
35
|
'frame',
|
|
35
36
|
'frameScaleX',
|
|
@@ -135,6 +136,8 @@ export class AnimatedSprite extends Component {
|
|
|
135
136
|
player = new ClipPlayer();
|
|
136
137
|
sheets = [];
|
|
137
138
|
texs = [];
|
|
139
|
+
// Clones whose sheet failed to load: applyFrame never installs them (CA-4).
|
|
140
|
+
failedSheets = new Set();
|
|
138
141
|
mesh;
|
|
139
142
|
frame = 0;
|
|
140
143
|
// Current frame's quad scale relative to the sheet's largest cell: always
|
|
@@ -157,10 +160,7 @@ export class AnimatedSprite extends Component {
|
|
|
157
160
|
main.cells = this.cells;
|
|
158
161
|
this.sheets = [main, ...this.extraSheets];
|
|
159
162
|
this.texs = this.sheets.map((sheet) => {
|
|
160
|
-
|
|
161
|
-
// current frame once each texture is in.
|
|
162
|
-
const tex = loader.load(sheet.texture, () => this.applyFrame());
|
|
163
|
-
tex.colorSpace = THREE.SRGBColorSpace;
|
|
163
|
+
const tex = this.sheetTexture(sheet.texture);
|
|
164
164
|
if (this.pixelArt) {
|
|
165
165
|
tex.magFilter = THREE.NearestFilter;
|
|
166
166
|
tex.minFilter = THREE.NearestFilter;
|
|
@@ -195,8 +195,34 @@ export class AnimatedSprite extends Component {
|
|
|
195
195
|
this.mesh?.removeFromParent();
|
|
196
196
|
this.mesh?.geometry.dispose();
|
|
197
197
|
this.mesh?.material.dispose();
|
|
198
|
+
// Only this sprite's clones: the cached bases live with the Game.
|
|
198
199
|
for (const tex of this.texs)
|
|
199
200
|
tex.dispose();
|
|
201
|
+
this.texs = [];
|
|
202
|
+
this.failedSheets.clear();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The sheet's own clone of the cached base. Non-uniform slicing needs
|
|
206
|
+
* the image's pixel size, so the current frame is re-applied once the
|
|
207
|
+
* sheet settles — on a cache hit too, whose settlement is already
|
|
208
|
+
* resolved — unless this sprite was destroyed meanwhile. A sheet that
|
|
209
|
+
* fails is remembered so the quad shows its flat material on that
|
|
210
|
+
* sheet's frames instead of sampling an image that never arrived (CA-4).
|
|
211
|
+
* An empty url (a sheet not authored yet) owns a bare texture and never
|
|
212
|
+
* touches game.assets, so it counts nowhere and warns about nothing.
|
|
213
|
+
*/
|
|
214
|
+
sheetTexture(url) {
|
|
215
|
+
if (!url)
|
|
216
|
+
return new THREE.Texture();
|
|
217
|
+
const { texture, settled } = this.game.assets.texture(url);
|
|
218
|
+
void settled.then((outcome) => {
|
|
219
|
+
if (!this.texs.includes(texture))
|
|
220
|
+
return;
|
|
221
|
+
if (outcome === 'failed')
|
|
222
|
+
this.failedSheets.add(texture);
|
|
223
|
+
this.applyFrame();
|
|
224
|
+
});
|
|
225
|
+
return texture;
|
|
200
226
|
}
|
|
201
227
|
showFrame(index) {
|
|
202
228
|
this.frame = index;
|
|
@@ -227,9 +253,12 @@ export class AnimatedSprite extends Component {
|
|
|
227
253
|
const tex = this.texs[located.sheet];
|
|
228
254
|
if (!sheet || !tex)
|
|
229
255
|
return;
|
|
230
|
-
if (this.mesh
|
|
231
|
-
this.
|
|
232
|
-
this.mesh.material.
|
|
256
|
+
if (this.mesh) {
|
|
257
|
+
const map = this.failedSheets.has(tex) ? null : tex;
|
|
258
|
+
if (this.mesh.material.map !== map) {
|
|
259
|
+
this.mesh.material.map = map;
|
|
260
|
+
this.mesh.material.needsUpdate = true;
|
|
261
|
+
}
|
|
233
262
|
}
|
|
234
263
|
const cells = sheet.cells;
|
|
235
264
|
if (cells?.length) {
|
|
@@ -69,6 +69,12 @@ export declare class Sprite extends Component implements YSortParticipant {
|
|
|
69
69
|
private mesh?;
|
|
70
70
|
onReady(): void;
|
|
71
71
|
onDestroy(): void;
|
|
72
|
+
/**
|
|
73
|
+
* CA-4's failure rule: an image that never arrives leaves the flat
|
|
74
|
+
* `color`, not a white quad over an empty map. Skipped once the sprite
|
|
75
|
+
* was destroyed or the clone is no longer this material's map.
|
|
76
|
+
*/
|
|
77
|
+
private dropFailedTexture;
|
|
72
78
|
private syncQuad;
|
|
73
79
|
private createGeometry;
|
|
74
80
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { Component } from '../component.js';
|
|
3
3
|
import { spritePlacement } from '../sprite-placement.js';
|
|
4
|
-
const loader = new THREE.TextureLoader();
|
|
5
4
|
const clampAnchor = (value) => Math.min(1, Math.max(0, value));
|
|
6
5
|
/**
|
|
7
6
|
* Textured or flat-color quad. In the unified pipeline, a 2D sprite is a
|
|
@@ -109,14 +108,20 @@ export class Sprite extends Component {
|
|
|
109
108
|
onReady() {
|
|
110
109
|
const material = new THREE.MeshBasicMaterial({ color: this.color, transparent: true });
|
|
111
110
|
if (this.texture) {
|
|
112
|
-
|
|
111
|
+
// Its own clone of the cached base (game.assets, ADR 0019): filters
|
|
112
|
+
// are per clone, colour space comes with the base, and the image lands
|
|
113
|
+
// on the Source every clone of this URL shares.
|
|
114
|
+
const { texture, settled } = this.game.assets.texture(this.texture);
|
|
113
115
|
if (this.pixelArt) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
texture.magFilter = THREE.NearestFilter;
|
|
117
|
+
texture.minFilter = THREE.NearestFilter;
|
|
116
118
|
}
|
|
117
|
-
|
|
118
|
-
material.map = tex;
|
|
119
|
+
material.map = texture;
|
|
119
120
|
material.color.set(0xffffff);
|
|
121
|
+
void settled.then((outcome) => {
|
|
122
|
+
if (outcome === 'failed')
|
|
123
|
+
this.dropFailedTexture(texture);
|
|
124
|
+
});
|
|
120
125
|
}
|
|
121
126
|
this.mesh = new THREE.Mesh(this.createGeometry(), material);
|
|
122
127
|
this.mesh.position.z = this.layer * 0.01;
|
|
@@ -126,7 +131,24 @@ export class Sprite extends Component {
|
|
|
126
131
|
onDestroy() {
|
|
127
132
|
this.mesh?.removeFromParent();
|
|
128
133
|
this.mesh?.geometry.dispose();
|
|
134
|
+
// Only this sprite's clone: the cached base lives with the Game.
|
|
135
|
+
this.mesh?.material.map?.dispose();
|
|
129
136
|
this.mesh?.material.dispose();
|
|
137
|
+
this.mesh = undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* CA-4's failure rule: an image that never arrives leaves the flat
|
|
141
|
+
* `color`, not a white quad over an empty map. Skipped once the sprite
|
|
142
|
+
* was destroyed or the clone is no longer this material's map.
|
|
143
|
+
*/
|
|
144
|
+
dropFailedTexture(texture) {
|
|
145
|
+
const material = this.mesh?.material;
|
|
146
|
+
if (!material || material.map !== texture)
|
|
147
|
+
return;
|
|
148
|
+
material.map = null;
|
|
149
|
+
texture.dispose();
|
|
150
|
+
material.color.setHex(this.color);
|
|
151
|
+
material.needsUpdate = true;
|
|
130
152
|
}
|
|
131
153
|
syncQuad() {
|
|
132
154
|
if (!this.mesh)
|
|
@@ -107,6 +107,12 @@ export declare class Tilemap extends Component implements SolidSource {
|
|
|
107
107
|
private gridSpec;
|
|
108
108
|
private rebuildMap;
|
|
109
109
|
private rebuildMaterial;
|
|
110
|
+
/**
|
|
111
|
+
* CA-4's failure rule: an image that never arrives leaves the flat
|
|
112
|
+
* `color`, not a white map over an empty texture. Only reached while the
|
|
113
|
+
* failed clone is still the current one (see rebuildMaterial).
|
|
114
|
+
*/
|
|
115
|
+
private dropFailedTexture;
|
|
110
116
|
private makeMaterial;
|
|
111
117
|
private rebuildGeometry;
|
|
112
118
|
private rebuildSolids;
|
|
@@ -5,7 +5,6 @@ import { projectIsometric } from '../projection.js';
|
|
|
5
5
|
import { SOLID_SOURCE_SYMBOL } from '../scene-solids.js';
|
|
6
6
|
import { cellAt as gridCellAt, cellBounds as gridCellBounds, cellIndex as gridCellIndex, } from '../tilemap-grid.js';
|
|
7
7
|
import { Solid } from './solid.js';
|
|
8
|
-
const loader = new THREE.TextureLoader();
|
|
9
8
|
/** One authorable cell map rendered as a single merged geometry. */
|
|
10
9
|
export class Tilemap extends Component {
|
|
11
10
|
static componentName = 'Tilemap';
|
|
@@ -212,20 +211,45 @@ export class Tilemap extends Component {
|
|
|
212
211
|
if (!this.texture)
|
|
213
212
|
return;
|
|
214
213
|
const requested = this.texture;
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
// Its own clone of the cached base (game.assets, ADR 0019). The image's
|
|
215
|
+
// pixel size drives the UVs, so the geometry is rebuilt once the texture
|
|
216
|
+
// settles — on a cache hit too, whose settlement is already resolved —
|
|
217
|
+
// and a texture that fails is dropped for the flat colour; neither
|
|
218
|
+
// happens if the texture was replaced or the component destroyed
|
|
219
|
+
// meanwhile.
|
|
220
|
+
const { texture, settled } = this.game.assets.texture(requested);
|
|
221
|
+
void settled.then((outcome) => {
|
|
222
|
+
if (this.loadedTexture !== texture || this.texture !== requested)
|
|
223
|
+
return;
|
|
224
|
+
if (outcome === 'loaded')
|
|
217
225
|
this.rebuildGeometry();
|
|
226
|
+
else
|
|
227
|
+
this.dropFailedTexture();
|
|
218
228
|
});
|
|
219
229
|
if (this.pixelArt) {
|
|
220
230
|
texture.magFilter = THREE.NearestFilter;
|
|
221
231
|
texture.minFilter = THREE.NearestFilter;
|
|
222
232
|
}
|
|
223
|
-
texture.colorSpace = THREE.SRGBColorSpace;
|
|
224
233
|
this.loadedTexture = texture;
|
|
225
234
|
mesh.material.map = texture;
|
|
226
235
|
mesh.material.color.set(0xffffff);
|
|
227
236
|
mesh.material.needsUpdate = true;
|
|
228
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* CA-4's failure rule: an image that never arrives leaves the flat
|
|
240
|
+
* `color`, not a white map over an empty texture. Only reached while the
|
|
241
|
+
* failed clone is still the current one (see rebuildMaterial).
|
|
242
|
+
*/
|
|
243
|
+
dropFailedTexture() {
|
|
244
|
+
const mesh = this.mesh;
|
|
245
|
+
if (!mesh)
|
|
246
|
+
return;
|
|
247
|
+
this.loadedTexture?.dispose();
|
|
248
|
+
this.loadedTexture = undefined;
|
|
249
|
+
mesh.material.map = null;
|
|
250
|
+
mesh.material.color.setHex(this.color);
|
|
251
|
+
mesh.material.needsUpdate = true;
|
|
252
|
+
}
|
|
229
253
|
makeMaterial() {
|
|
230
254
|
return new THREE.MeshBasicMaterial({
|
|
231
255
|
color: this.color,
|
package/dist/game.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import { AssetLoader } from './assets/asset-loader.js';
|
|
3
|
+
import type { TextureBackend } from './assets/texture-backend.js';
|
|
2
4
|
import type { AudioBackend } from './audio/backend.js';
|
|
3
5
|
import { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
4
6
|
import { type SceneCameraJson } from './camera.js';
|
|
@@ -38,6 +40,13 @@ export interface GameOptions {
|
|
|
38
40
|
* first unlock (CA-6), never eagerly here.
|
|
39
41
|
*/
|
|
40
42
|
audio?: AudioBackend;
|
|
43
|
+
/**
|
|
44
|
+
* Replaces the real `THREE.TextureLoader` implementation behind
|
|
45
|
+
* `game.assets` (ADR 0013's seam, applied to textures — ADR 0019), mainly
|
|
46
|
+
* for a project's own tests: `happy-dom` decodes no images. Defaults to
|
|
47
|
+
* the real backend either way; `game.assets` always exists.
|
|
48
|
+
*/
|
|
49
|
+
textures?: TextureBackend;
|
|
41
50
|
}
|
|
42
51
|
export type UpdateFn = (dt: number) => void;
|
|
43
52
|
export interface SpawnPrefabOptions {
|
|
@@ -68,6 +77,12 @@ export declare class Game {
|
|
|
68
77
|
readonly ui: GameUi;
|
|
69
78
|
/** The audio mixer: channels, master, playback. See ADR 0012, ADR 0013. */
|
|
70
79
|
readonly audio: AudioSubsystem;
|
|
80
|
+
/**
|
|
81
|
+
* The texture cache: keep-all for the Game's life, `preload`, `status`
|
|
82
|
+
* and the Assets Ready promise `ready()`. Session-scoped by construction
|
|
83
|
+
* (ADR 0011): `unloadScene()` never touches it. See ADR 0019.
|
|
84
|
+
*/
|
|
85
|
+
readonly assets: AssetLoader;
|
|
71
86
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
72
87
|
readonly time: GameTime;
|
|
73
88
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
package/dist/game.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
2
|
import { gameViewport } from './anchored-pieces.js';
|
|
3
|
+
import { AssetLoader } from './assets/asset-loader.js';
|
|
3
4
|
import { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
4
5
|
import { dispatchCollisions as dispatchHitboxCollisions } from './collision-dispatch.js';
|
|
5
6
|
import { isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera, } from './camera.js';
|
|
@@ -35,6 +36,12 @@ export class Game {
|
|
|
35
36
|
ui;
|
|
36
37
|
/** The audio mixer: channels, master, playback. See ADR 0012, ADR 0013. */
|
|
37
38
|
audio;
|
|
39
|
+
/**
|
|
40
|
+
* The texture cache: keep-all for the Game's life, `preload`, `status`
|
|
41
|
+
* and the Assets Ready promise `ready()`. Session-scoped by construction
|
|
42
|
+
* (ADR 0011): `unloadScene()` never touches it. See ADR 0019.
|
|
43
|
+
*/
|
|
44
|
+
assets;
|
|
38
45
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
39
46
|
time = new GameTime();
|
|
40
47
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
@@ -103,6 +110,12 @@ export class Game {
|
|
|
103
110
|
// music bed needs across a scene swap.
|
|
104
111
|
resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
|
|
105
112
|
});
|
|
113
|
+
this.assets = new AssetLoader({
|
|
114
|
+
backend: options.textures,
|
|
115
|
+
// Same late lookup as audio's: preload() resolves through whatever
|
|
116
|
+
// catalog is registered at call time, which outlives every scene.
|
|
117
|
+
resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
|
|
118
|
+
});
|
|
106
119
|
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
107
120
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
108
121
|
this.scene.background = new THREE.Color(background);
|
|
@@ -310,6 +323,7 @@ export class Game {
|
|
|
310
323
|
},
|
|
311
324
|
loadScene: (name) => this.loadSceneByName(name),
|
|
312
325
|
availableScenes: () => this.availableScenes,
|
|
326
|
+
assets: () => this.assets.status,
|
|
313
327
|
});
|
|
314
328
|
activation.register(this.runtimeBridge);
|
|
315
329
|
this.audio.setSilenced(true);
|
|
@@ -357,6 +371,9 @@ export class Game {
|
|
|
357
371
|
this.time.cancelAll();
|
|
358
372
|
for (const entity of [...this.entities])
|
|
359
373
|
entity.destroy();
|
|
374
|
+
// After the entities: their clones go with the cascade above, the
|
|
375
|
+
// cached bases go here, exactly once (ADR 0019).
|
|
376
|
+
this.assets.dispose();
|
|
360
377
|
this.renderer.dispose();
|
|
361
378
|
}
|
|
362
379
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
2
|
export type { GameOptions, GameResolution, SceneCatalog, SpawnPrefabOptions, UpdateFn, ParamOverrides, } from './game.js';
|
|
3
|
+
export { AssetLoader } from './assets/asset-loader.js';
|
|
4
|
+
export type { AssetStatus } from './assets/asset-loader.js';
|
|
5
|
+
export type { TextureBackend } from './assets/texture-backend.js';
|
|
3
6
|
export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
4
7
|
export type { AudioSubsystemOptions } from './audio/audio-subsystem.js';
|
|
5
8
|
export type { AudioBackend, AudioResource, BackendPlayHandle, BackendPlayOptions } from './audio/backend.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Game } from './game.js';
|
|
2
|
+
export { AssetLoader } from './assets/asset-loader.js';
|
|
2
3
|
export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
3
4
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
4
5
|
export { isYSortParticipant, ySortZ } from './render-sort.js';
|
package/dist/runtime-bridge.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssetStatus } from './assets/asset-loader.js';
|
|
1
2
|
import type { RuntimeSnapshot, RuntimeSnapshotFilters } from './runtime-inspection.js';
|
|
2
3
|
export declare const RUNTIME_BRIDGE_PROTOCOL_VERSION: 1;
|
|
3
4
|
export declare const RUNTIME_BRIDGE_SYMBOL: unique symbol;
|
|
@@ -10,9 +11,13 @@ export type RuntimeMode = 'paused' | 'real-time';
|
|
|
10
11
|
* gating on capabilities check for (review finding #4) — protocol 1 alone
|
|
11
12
|
* doesn't distinguish an engine that silently no-ops an unknown operation
|
|
12
13
|
* from one that runs it. `fixed-step` (ADR 0014) announces that `step`
|
|
13
|
-
* advances whole 1/60 s Simulation Steps and takes no `dt`.
|
|
14
|
+
* advances whole 1/60 s Simulation Steps and takes no `dt`. `assets`
|
|
15
|
+
* (ADR 0019) announces that every metadata carries `assets: { pending,
|
|
16
|
+
* loaded, failed }` from `game.assets.status`, so a Run Session can wait
|
|
17
|
+
* for Assets Ready (`pending === 0`) at readiness, after a `scene`
|
|
18
|
+
* operation and before a screenshot.
|
|
14
19
|
*/
|
|
15
|
-
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step'];
|
|
20
|
+
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets'];
|
|
16
21
|
export interface RuntimeMetadata {
|
|
17
22
|
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
18
23
|
engineVersion: string;
|
|
@@ -20,6 +25,8 @@ export interface RuntimeMetadata {
|
|
|
20
25
|
frame: number;
|
|
21
26
|
simulationTime: number;
|
|
22
27
|
capabilities: readonly string[];
|
|
28
|
+
/** `game.assets.status` at the moment of the read — reading it never advances `frame`. */
|
|
29
|
+
assets: AssetStatus;
|
|
23
30
|
}
|
|
24
31
|
export type RuntimeControlRequest = {
|
|
25
32
|
operation: 'press' | 'hold' | 'release';
|
|
@@ -81,6 +88,8 @@ export interface RuntimeBridgeHost {
|
|
|
81
88
|
/** Resolves `name` through the registered catalog and loads it. */
|
|
82
89
|
loadScene(name: string): boolean;
|
|
83
90
|
availableScenes(): string[];
|
|
91
|
+
/** A fresh `game.assets.status`. */
|
|
92
|
+
assets(): AssetStatus;
|
|
84
93
|
}
|
|
85
94
|
export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
86
95
|
readonly surface: HTMLCanvasElement;
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -10,9 +10,13 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
|
10
10
|
* gating on capabilities check for (review finding #4) — protocol 1 alone
|
|
11
11
|
* doesn't distinguish an engine that silently no-ops an unknown operation
|
|
12
12
|
* from one that runs it. `fixed-step` (ADR 0014) announces that `step`
|
|
13
|
-
* advances whole 1/60 s Simulation Steps and takes no `dt`.
|
|
13
|
+
* advances whole 1/60 s Simulation Steps and takes no `dt`. `assets`
|
|
14
|
+
* (ADR 0019) announces that every metadata carries `assets: { pending,
|
|
15
|
+
* loaded, failed }` from `game.assets.status`, so a Run Session can wait
|
|
16
|
+
* for Assets Ready (`pending === 0`) at readiness, after a `scene`
|
|
17
|
+
* operation and before a screenshot.
|
|
14
18
|
*/
|
|
15
|
-
export const RUNTIME_BRIDGE_CAPABILITIES = ['click', 'scene', 'fixed-step'];
|
|
19
|
+
export const RUNTIME_BRIDGE_CAPABILITIES = ['click', 'scene', 'fixed-step', 'assets'];
|
|
16
20
|
export class RuntimeBridgeOperationError extends Error {
|
|
17
21
|
code;
|
|
18
22
|
availableActions;
|
|
@@ -61,6 +65,7 @@ export class EngineRuntimeBridge {
|
|
|
61
65
|
// Derived, never summed: 60 steps are exactly 1 s, with no float drift.
|
|
62
66
|
simulationTime: this.frame * SIMULATION_STEP,
|
|
63
67
|
capabilities: RUNTIME_BRIDGE_CAPABILITIES,
|
|
68
|
+
assets: this.host.assets(),
|
|
64
69
|
};
|
|
65
70
|
}
|
|
66
71
|
inspect(filters = {}) {
|