@waica/engine 0.19.0 → 0.19.2
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/dist/animation/sheet.js +9 -3
- package/dist/assets/asset-loader.js +3 -2
- package/dist/audio/web-audio-backend.js +15 -6
- package/dist/authoring-defaults.js +4 -4
- package/dist/camera-effects.js +7 -3
- package/dist/collision-shape.js +45 -20
- package/dist/component-registry.d.ts +11 -1
- package/dist/component-registry.js +26 -3
- package/dist/component-update-schedule.d.ts +2 -0
- package/dist/component-update-schedule.js +124 -68
- package/dist/components/animated-sprite.js +3 -2
- package/dist/components/sprite.js +3 -2
- package/dist/components/tilemap.d.ts +6 -0
- package/dist/components/tilemap.js +49 -38
- package/dist/entity.js +6 -4
- package/dist/game.js +23 -9
- package/dist/pointer.js +27 -10
- package/dist/render-sort.js +11 -5
- package/dist/report-rejection.d.ts +7 -0
- package/dist/report-rejection.js +12 -0
- package/dist/runtime-bridge.d.ts +4 -0
- package/dist/runtime-bridge.js +35 -27
- package/dist/runtime-inspection.js +106 -86
- package/dist/scene.d.ts +0 -1
- package/dist/scene.js +34 -1
- package/dist/spatial-broadphase.js +60 -34
- package/dist/spatial-query-geometry.js +48 -35
- package/package.json +3 -3
|
@@ -5,6 +5,7 @@ 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
|
+
import { reportRejection } from '../report-rejection.js';
|
|
8
9
|
/** One authorable cell map rendered as a single merged geometry. */
|
|
9
10
|
export class Tilemap extends Component {
|
|
10
11
|
static componentName = 'Tilemap';
|
|
@@ -218,14 +219,14 @@ export class Tilemap extends Component {
|
|
|
218
219
|
// happens if the texture was replaced or the component destroyed
|
|
219
220
|
// meanwhile.
|
|
220
221
|
const { texture, settled } = this.game.assets.texture(requested);
|
|
221
|
-
|
|
222
|
+
reportRejection(settled.then((outcome) => {
|
|
222
223
|
if (this.loadedTexture !== texture || this.texture !== requested)
|
|
223
224
|
return;
|
|
224
225
|
if (outcome === 'loaded')
|
|
225
226
|
this.rebuildGeometry();
|
|
226
227
|
else
|
|
227
228
|
this.dropFailedTexture();
|
|
228
|
-
});
|
|
229
|
+
}), 'tilemap texture settle');
|
|
229
230
|
if (this.pixelArt) {
|
|
230
231
|
texture.magFilter = THREE.NearestFilter;
|
|
231
232
|
texture.minFilter = THREE.NearestFilter;
|
|
@@ -260,56 +261,66 @@ export class Tilemap extends Component {
|
|
|
260
261
|
const mesh = this.mesh;
|
|
261
262
|
if (!mesh)
|
|
262
263
|
return;
|
|
263
|
-
const
|
|
264
|
-
const uvs = [];
|
|
265
|
-
const indices = [];
|
|
264
|
+
const buffers = { positions: [], uvs: [], indices: [] };
|
|
266
265
|
const width = Math.max(0, Math.floor(this.mapWidth));
|
|
267
266
|
const height = Math.max(0, Math.floor(this.mapHeight));
|
|
268
267
|
const size = this.cellSize;
|
|
269
268
|
if (Number.isFinite(size) && size > 0) {
|
|
270
|
-
const
|
|
271
|
-
const imageWidth = image?.width && image.width > 0 ? image.width : Math.max(1, this.cols);
|
|
272
|
-
const imageHeight = image?.height && image.height > 0 ? image.height : Math.max(1, this.rows);
|
|
273
|
-
const frameParams = {
|
|
274
|
-
gridOffsetX: this.gridOffsetX,
|
|
275
|
-
gridOffsetY: this.gridOffsetY,
|
|
276
|
-
spacingX: this.spacingX,
|
|
277
|
-
spacingY: this.spacingY,
|
|
278
|
-
cellWidth: this.cellWidth,
|
|
279
|
-
cellHeight: this.cellHeight,
|
|
280
|
-
};
|
|
269
|
+
const sheet = this.sheetSize();
|
|
281
270
|
for (let index = 0; index < width * height; index++) {
|
|
282
271
|
const tile = this.cells[index] ?? -1;
|
|
283
272
|
if (!Number.isFinite(tile) || tile < 0)
|
|
284
273
|
continue;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
const center = this.game.projection === 'isometric'
|
|
290
|
-
? projectIsometric(logicalCenterX, logicalCenterY)
|
|
291
|
-
: { x: logicalCenterX, y: logicalCenterY };
|
|
292
|
-
const halfWidth = this.game.projection === 'isometric' ? size : size / 2;
|
|
293
|
-
const halfHeight = size / 2;
|
|
294
|
-
const z = this.layer * 0.01;
|
|
295
|
-
positions.push(center.x - halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y + halfHeight, z, center.x - halfWidth, center.y + halfHeight, z);
|
|
296
|
-
const frame = sheetCell(imageWidth, imageHeight, this.cols, this.rows, tile, frameParams);
|
|
297
|
-
const left = frame.x / imageWidth;
|
|
298
|
-
const right = (frame.x + frame.width) / imageWidth;
|
|
299
|
-
const bottom = 1 - (frame.y + frame.height) / imageHeight;
|
|
300
|
-
const top = 1 - frame.y / imageHeight;
|
|
301
|
-
uvs.push(left, bottom, right, bottom, right, top, left, top);
|
|
302
|
-
const vertex = positions.length / 3 - 4;
|
|
303
|
-
indices.push(vertex, vertex + 1, vertex + 2, vertex, vertex + 2, vertex + 3);
|
|
274
|
+
this.pushTileQuad(buffers.positions, index % width, Math.floor(index / width));
|
|
275
|
+
buffers.uvs.push(...this.tileUvs(tile, sheet));
|
|
276
|
+
const vertex = buffers.positions.length / 3 - 4;
|
|
277
|
+
buffers.indices.push(vertex, vertex + 1, vertex + 2, vertex, vertex + 2, vertex + 3);
|
|
304
278
|
}
|
|
305
279
|
}
|
|
306
280
|
const geometry = new THREE.BufferGeometry();
|
|
307
|
-
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
308
|
-
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
309
|
-
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
281
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(buffers.positions, 3));
|
|
282
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(buffers.uvs, 2));
|
|
283
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(buffers.indices), 1));
|
|
310
284
|
mesh.geometry.dispose();
|
|
311
285
|
mesh.geometry = geometry;
|
|
312
286
|
}
|
|
287
|
+
/** The loaded sheet image's size, or one pixel per sheet cell until it arrives. */
|
|
288
|
+
sheetSize() {
|
|
289
|
+
const image = this.loadedTexture?.image;
|
|
290
|
+
return {
|
|
291
|
+
width: image?.width && image.width > 0 ? image.width : Math.max(1, this.cols),
|
|
292
|
+
height: image?.height && image.height > 0 ? image.height : Math.max(1, this.rows),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** The four corners of a cell's quad, projected for the Game, on this layer's depth. */
|
|
296
|
+
pushTileQuad(positions, column, row) {
|
|
297
|
+
const size = this.cellSize;
|
|
298
|
+
const logicalCenterX = (column + 0.5) * size;
|
|
299
|
+
const logicalCenterY = (row + 0.5) * size;
|
|
300
|
+
const center = this.game.projection === 'isometric'
|
|
301
|
+
? projectIsometric(logicalCenterX, logicalCenterY)
|
|
302
|
+
: { x: logicalCenterX, y: logicalCenterY };
|
|
303
|
+
const halfWidth = this.game.projection === 'isometric' ? size : size / 2;
|
|
304
|
+
const halfHeight = size / 2;
|
|
305
|
+
const z = this.layer * 0.01;
|
|
306
|
+
positions.push(center.x - halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y - halfHeight, z, center.x + halfWidth, center.y + halfHeight, z, center.x - halfWidth, center.y + halfHeight, z);
|
|
307
|
+
}
|
|
308
|
+
/** The UVs of a tile's sheet cell, in the quad's corner order. */
|
|
309
|
+
tileUvs(tile, sheet) {
|
|
310
|
+
const frame = sheetCell(sheet.width, sheet.height, this.cols, this.rows, tile, {
|
|
311
|
+
gridOffsetX: this.gridOffsetX,
|
|
312
|
+
gridOffsetY: this.gridOffsetY,
|
|
313
|
+
spacingX: this.spacingX,
|
|
314
|
+
spacingY: this.spacingY,
|
|
315
|
+
cellWidth: this.cellWidth,
|
|
316
|
+
cellHeight: this.cellHeight,
|
|
317
|
+
});
|
|
318
|
+
const left = frame.x / sheet.width;
|
|
319
|
+
const right = (frame.x + frame.width) / sheet.width;
|
|
320
|
+
const bottom = 1 - (frame.y + frame.height) / sheet.height;
|
|
321
|
+
const top = 1 - frame.y / sheet.height;
|
|
322
|
+
return [left, bottom, right, bottom, right, top, left, top];
|
|
323
|
+
}
|
|
313
324
|
rebuildSolids() {
|
|
314
325
|
if (!this.entity || !this.game)
|
|
315
326
|
return;
|
package/dist/entity.js
CHANGED
|
@@ -22,13 +22,15 @@ export class Entity {
|
|
|
22
22
|
}
|
|
23
23
|
/** Keeps identity scenes zero-copy while projected scenes own a logical transform. */
|
|
24
24
|
setProjected(projected) {
|
|
25
|
-
|
|
26
|
-
return;
|
|
25
|
+
const logical = this.logicalPosition;
|
|
27
26
|
if (projected) {
|
|
28
|
-
|
|
27
|
+
if (logical === null)
|
|
28
|
+
this.logicalPosition = this.node.position.clone();
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
if (logical === null)
|
|
32
|
+
return;
|
|
33
|
+
this.node.position.copy(logical);
|
|
32
34
|
this.logicalPosition = null;
|
|
33
35
|
}
|
|
34
36
|
get scale() {
|
package/dist/game.js
CHANGED
|
@@ -5,7 +5,7 @@ import { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
|
5
5
|
import { dispatchCollisions as dispatchHitboxCollisions } from './collision-dispatch.js';
|
|
6
6
|
import { isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera, } from './camera.js';
|
|
7
7
|
import { advanceCameraEffects, CameraEffects, layoutCameraEffects } from './camera-effects.js';
|
|
8
|
-
import { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
8
|
+
import { implementsOnUpdate, resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
9
9
|
import { Entity } from './entity.js';
|
|
10
10
|
import { Emitter } from './events.js';
|
|
11
11
|
import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedToStep, } from './fixed-step.js';
|
|
@@ -20,6 +20,16 @@ import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
|
|
|
20
20
|
import { createSpatialQuery } from './spatial-query.js';
|
|
21
21
|
import { Stats } from './stats.js';
|
|
22
22
|
import { anchoredPiecesOf, GameUi } from './ui.js';
|
|
23
|
+
import { componentClassOf } from './component-registry.js';
|
|
24
|
+
/** Maps a resolved update order back to the entity's components; the order only names components it was built from. */
|
|
25
|
+
function componentsInOrder(order, byName) {
|
|
26
|
+
return order.map((name) => {
|
|
27
|
+
const component = byName.get(name);
|
|
28
|
+
if (!component)
|
|
29
|
+
throw new Error(`Component update schedule names "${name}", which is not on the entity`);
|
|
30
|
+
return component;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
23
33
|
/**
|
|
24
34
|
* Engine core: loop, unified 2D/3D three scene, orthographic camera,
|
|
25
35
|
* entities with components, and input. See DESIGN.md.
|
|
@@ -265,7 +275,7 @@ export class Game {
|
|
|
265
275
|
}
|
|
266
276
|
/** Applies persisted overrides to a freshly added component. */
|
|
267
277
|
applyParamOverrides(entity, component) {
|
|
268
|
-
const Class = component
|
|
278
|
+
const Class = componentClassOf(component);
|
|
269
279
|
const override = this.paramOverrides[entity.name]?.[Class.componentName];
|
|
270
280
|
if (override)
|
|
271
281
|
Object.assign(component, override);
|
|
@@ -535,8 +545,12 @@ export class Game {
|
|
|
535
545
|
}
|
|
536
546
|
}
|
|
537
547
|
const z = ySortZ(entries);
|
|
538
|
-
for (const [index, participant] of participants.entries())
|
|
539
|
-
|
|
548
|
+
for (const [index, participant] of participants.entries()) {
|
|
549
|
+
const sortZ = z[index];
|
|
550
|
+
if (sortZ === undefined)
|
|
551
|
+
throw new Error(`ySortZ returned no z for y-sort participant ${index}`);
|
|
552
|
+
participant.setSortZ(sortZ);
|
|
553
|
+
}
|
|
540
554
|
}
|
|
541
555
|
renderSurface() {
|
|
542
556
|
if (this.sceneProjection === 'isometric') {
|
|
@@ -578,11 +592,11 @@ export class Game {
|
|
|
578
592
|
const names = [];
|
|
579
593
|
const signatureParts = [];
|
|
580
594
|
for (const component of components) {
|
|
581
|
-
const Class = component
|
|
595
|
+
const Class = componentClassOf(component);
|
|
582
596
|
const name = Class.componentName;
|
|
583
597
|
names.push(name);
|
|
584
598
|
byName.set(name, component);
|
|
585
|
-
signatureParts.push(`${name}:${
|
|
599
|
+
signatureParts.push(`${name}:${implementsOnUpdate(Class) ? 'updates' : 'passive'}:` +
|
|
586
600
|
[...new Set(Class.updateAfter ?? [])].sort().join(','));
|
|
587
601
|
}
|
|
588
602
|
const signature = signatureParts.sort().join('|');
|
|
@@ -592,13 +606,13 @@ export class Game {
|
|
|
592
606
|
// when nothing about this entity's components changed since last time.
|
|
593
607
|
const cached = this.updateScheduleCache.get(entity);
|
|
594
608
|
if (cached && cached.signature === signature) {
|
|
595
|
-
return cached.order
|
|
609
|
+
return componentsInOrder(cached.order, byName);
|
|
596
610
|
}
|
|
597
611
|
const registry = {
|
|
598
612
|
...(this.registry?.components ?? {}),
|
|
599
613
|
};
|
|
600
614
|
for (const component of components) {
|
|
601
|
-
const Class = component
|
|
615
|
+
const Class = componentClassOf(component);
|
|
602
616
|
registry[Class.componentName] = Class;
|
|
603
617
|
}
|
|
604
618
|
const result = resolveComponentUpdateSchedule(names, registry);
|
|
@@ -613,7 +627,7 @@ export class Game {
|
|
|
613
627
|
}
|
|
614
628
|
this.invalidUpdateCompositions.delete(entity);
|
|
615
629
|
this.updateScheduleCache.set(entity, { signature, order: result.order });
|
|
616
|
-
return result.order
|
|
630
|
+
return componentsInOrder(result.order, byName);
|
|
617
631
|
}
|
|
618
632
|
updateSceneCamera(dt) {
|
|
619
633
|
const cam = this.sceneCamera;
|
package/dist/pointer.js
CHANGED
|
@@ -148,15 +148,32 @@ export class Pointer {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
if (hits.length === 1)
|
|
154
|
-
return hits[0].entity;
|
|
155
|
-
const z = ySortZ(hits.map((h) => ({ layer: h.layer, y: h.y })));
|
|
156
|
-
let bestIndex = 0;
|
|
157
|
-
for (let i = 1; i < hits.length; i += 1)
|
|
158
|
-
if (z[i] > z[bestIndex])
|
|
159
|
-
bestIndex = i;
|
|
160
|
-
return hits[bestIndex].entity;
|
|
151
|
+
return frontMostHit(hits);
|
|
161
152
|
}
|
|
162
153
|
}
|
|
154
|
+
/** The front-most hit under y-sort; on an exact z tie the earlier hit wins. */
|
|
155
|
+
function frontMostHit(hits) {
|
|
156
|
+
const [first] = hits;
|
|
157
|
+
if (!first)
|
|
158
|
+
return null;
|
|
159
|
+
if (hits.length === 1)
|
|
160
|
+
return first.entity;
|
|
161
|
+
const z = ySortZ(hits.map((h) => ({ layer: h.layer, y: h.y })));
|
|
162
|
+
let best = first;
|
|
163
|
+
let bestZ = zOfHit(z, 0);
|
|
164
|
+
for (const [index, hit] of hits.entries()) {
|
|
165
|
+
const hitZ = zOfHit(z, index);
|
|
166
|
+
if (hitZ > bestZ) {
|
|
167
|
+
best = hit;
|
|
168
|
+
bestZ = hitZ;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return best.entity;
|
|
172
|
+
}
|
|
173
|
+
/** `ySortZ` returns one z per entry, so every hit index has one. */
|
|
174
|
+
function zOfHit(z, index) {
|
|
175
|
+
const value = z[index];
|
|
176
|
+
if (value === undefined)
|
|
177
|
+
throw new Error(`ySortZ returned no z for pointer hit ${index}`);
|
|
178
|
+
return value;
|
|
179
|
+
}
|
package/dist/render-sort.js
CHANGED
|
@@ -4,6 +4,13 @@ export function isYSortParticipant(value) {
|
|
|
4
4
|
typeof value.layer === 'number' &&
|
|
5
5
|
typeof value.setSortZ === 'function');
|
|
6
6
|
}
|
|
7
|
+
/** The Y of the entry at `index`, which `ySortZ` only reads for indices it grouped from `entries`. */
|
|
8
|
+
function entryY(entries, index) {
|
|
9
|
+
const entry = entries[index];
|
|
10
|
+
if (!entry)
|
|
11
|
+
throw new Error(`ySortZ has no entry at index ${index}`);
|
|
12
|
+
return entry.y;
|
|
13
|
+
}
|
|
7
14
|
/**
|
|
8
15
|
* Z per entry under y-sort. Each layer keeps its 0.01 band; within a band,
|
|
9
16
|
* lower Y gets a higher z (renders in front), and exact Y ties keep input
|
|
@@ -22,14 +29,13 @@ export function ySortZ(entries) {
|
|
|
22
29
|
else
|
|
23
30
|
byLayer.set(entry.layer, [index]);
|
|
24
31
|
}
|
|
25
|
-
const layers = [...byLayer
|
|
32
|
+
const layers = [...byLayer].sort(([a], [b]) => a - b);
|
|
26
33
|
const z = new Array(entries.length);
|
|
27
|
-
for (const [i, layer] of layers.entries()) {
|
|
28
|
-
const
|
|
29
|
-
const next = layers[i + 1];
|
|
34
|
+
for (const [i, [layer, indices]] of layers.entries()) {
|
|
35
|
+
const next = layers[i + 1]?.[0];
|
|
30
36
|
const width = next === undefined ? 0.01 : Math.min(0.01, (next - layer) * 0.01);
|
|
31
37
|
// Stable sort: back-to-front is descending Y, ties keep input order.
|
|
32
|
-
const ordered = [...indices].sort((a, b) => entries
|
|
38
|
+
const ordered = [...indices].sort((a, b) => entryY(entries, b) - entryY(entries, a));
|
|
33
39
|
const step = width / (ordered.length + 1);
|
|
34
40
|
for (const [rank, index] of ordered.entries()) {
|
|
35
41
|
z[index] = layer * 0.01 + (rank + 1) * step;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observes a task the engine starts without awaiting (a texture settling, an
|
|
3
|
+
* AudioContext state change): its rejection is logged once as a `[waica]`
|
|
4
|
+
* error instead of surfacing as an unhandled rejection. Internal; not part of
|
|
5
|
+
* the package entry.
|
|
6
|
+
*/
|
|
7
|
+
export declare function reportRejection(task: Promise<unknown>, context: string): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observes a task the engine starts without awaiting (a texture settling, an
|
|
3
|
+
* AudioContext state change): its rejection is logged once as a `[waica]`
|
|
4
|
+
* error instead of surfacing as an unhandled rejection. Internal; not part of
|
|
5
|
+
* the package entry.
|
|
6
|
+
*/
|
|
7
|
+
export function reportRejection(task, context) {
|
|
8
|
+
task.catch((error) => {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
10
|
+
console.error(`[waica] ${context} failed: ${message}`);
|
|
11
|
+
});
|
|
12
|
+
}
|
package/dist/runtime-bridge.d.ts
CHANGED
|
@@ -106,6 +106,10 @@ export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
|
106
106
|
metadata(): RuntimeMetadata;
|
|
107
107
|
inspect(filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
108
108
|
control(request: RuntimeControlRequest): RuntimeControlResult;
|
|
109
|
+
private injectAction;
|
|
110
|
+
/** Advances whole Simulation Steps while paused. */
|
|
111
|
+
private step;
|
|
112
|
+
private loadScene;
|
|
109
113
|
/** Counts one Simulation Step, whoever ran it (paused stepping or real-time playback). */
|
|
110
114
|
private advance;
|
|
111
115
|
unregister(): void;
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -96,29 +96,11 @@ export class EngineRuntimeBridge {
|
|
|
96
96
|
case 'press':
|
|
97
97
|
case 'hold':
|
|
98
98
|
case 'release':
|
|
99
|
-
|
|
100
|
-
const available = this.host.availableActions();
|
|
101
|
-
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown action "${request.action}". Available actions: ${available.join(', ') || '(none)'}.`, available);
|
|
102
|
-
}
|
|
99
|
+
this.injectAction(request);
|
|
103
100
|
break;
|
|
104
|
-
case 'step':
|
|
105
|
-
|
|
106
|
-
throw new RuntimeBridgeOperationError('runtime-invalid-state', 'step is only available while the Runtime Bridge is paused.');
|
|
107
|
-
}
|
|
108
|
-
// A caller-chosen dt is rejected outright, never ignored: a pre-ADR-0014
|
|
109
|
-
// client that still sends one would otherwise believe it stepped by it.
|
|
110
|
-
if ('dt' in request) {
|
|
111
|
-
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'step takes no dt: it advances whole Simulation Steps of 1/60 s each; pass frames (1 through 600) instead.');
|
|
112
|
-
}
|
|
113
|
-
const frames = request.frames ?? 1;
|
|
114
|
-
if (!Number.isInteger(frames) || frames < 1 || frames > 600) {
|
|
115
|
-
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'frames must be an integer from 1 through 600.');
|
|
116
|
-
}
|
|
117
|
-
for (let index = 0; index < frames; index += 1) {
|
|
118
|
-
this.host.step(() => this.advance());
|
|
119
|
-
}
|
|
101
|
+
case 'step':
|
|
102
|
+
this.step(request);
|
|
120
103
|
break;
|
|
121
|
-
}
|
|
122
104
|
case 'click': {
|
|
123
105
|
if (!Number.isFinite(request.x) || !Number.isFinite(request.y)) {
|
|
124
106
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'x and y must be finite numbers.');
|
|
@@ -126,13 +108,9 @@ export class EngineRuntimeBridge {
|
|
|
126
108
|
this.host.click(request.x, request.y);
|
|
127
109
|
break;
|
|
128
110
|
}
|
|
129
|
-
case 'scene':
|
|
130
|
-
|
|
131
|
-
const available = this.host.availableScenes();
|
|
132
|
-
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown scene "${request.scene}". Available scenes: ${available.join(', ') || '(none)'}.`, undefined, available);
|
|
133
|
-
}
|
|
111
|
+
case 'scene':
|
|
112
|
+
this.loadScene(request.scene);
|
|
134
113
|
break;
|
|
135
|
-
}
|
|
136
114
|
default: {
|
|
137
115
|
const unsupported = request;
|
|
138
116
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unsupported runtime control operation "${unsupported.operation}".`);
|
|
@@ -140,6 +118,36 @@ export class EngineRuntimeBridge {
|
|
|
140
118
|
}
|
|
141
119
|
return { ...this.metadata(), heldActions: this.host.heldActions() };
|
|
142
120
|
}
|
|
121
|
+
injectAction(request) {
|
|
122
|
+
if (this.host.injectAction(request.action, request.operation))
|
|
123
|
+
return;
|
|
124
|
+
const available = this.host.availableActions();
|
|
125
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown action "${request.action}". Available actions: ${available.join(', ') || '(none)'}.`, available);
|
|
126
|
+
}
|
|
127
|
+
/** Advances whole Simulation Steps while paused. */
|
|
128
|
+
step(request) {
|
|
129
|
+
if (this.mode !== 'paused') {
|
|
130
|
+
throw new RuntimeBridgeOperationError('runtime-invalid-state', 'step is only available while the Runtime Bridge is paused.');
|
|
131
|
+
}
|
|
132
|
+
// A caller-chosen dt is rejected outright, never ignored: a pre-ADR-0014
|
|
133
|
+
// client that still sends one would otherwise believe it stepped by it.
|
|
134
|
+
if ('dt' in request) {
|
|
135
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'step takes no dt: it advances whole Simulation Steps of 1/60 s each; pass frames (1 through 600) instead.');
|
|
136
|
+
}
|
|
137
|
+
const frames = request.frames ?? 1;
|
|
138
|
+
if (!Number.isInteger(frames) || frames < 1 || frames > 600) {
|
|
139
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'frames must be an integer from 1 through 600.');
|
|
140
|
+
}
|
|
141
|
+
for (let index = 0; index < frames; index += 1) {
|
|
142
|
+
this.host.step(() => this.advance());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
loadScene(scene) {
|
|
146
|
+
if (this.host.loadScene(scene))
|
|
147
|
+
return;
|
|
148
|
+
const available = this.host.availableScenes();
|
|
149
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown scene "${scene}". Available scenes: ${available.join(', ') || '(none)'}.`, undefined, available);
|
|
150
|
+
}
|
|
143
151
|
/** Counts one Simulation Step, whoever ran it (paused stepping or real-time playback). */
|
|
144
152
|
advance() {
|
|
145
153
|
this.frame += 1;
|