@waica/engine 0.18.0 → 0.19.1
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/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.d.ts +142 -0
- package/dist/camera-effects.js +338 -0
- 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-time.d.ts +4 -0
- package/dist/game-time.js +2 -1
- package/dist/game.d.ts +6 -0
- package/dist/game.js +59 -17
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- 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 +8 -2
- package/dist/runtime-bridge.js +45 -29
- package/dist/runtime-inspection.d.ts +9 -0
- package/dist/runtime-inspection.js +107 -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 +1 -1
package/dist/game.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { TextureBackend } from './assets/texture-backend.js';
|
|
|
4
4
|
import type { AudioBackend } from './audio/backend.js';
|
|
5
5
|
import { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
6
6
|
import { type SceneCameraJson } from './camera.js';
|
|
7
|
+
import { CameraEffects } from './camera-effects.js';
|
|
7
8
|
import type { Component } from './component.js';
|
|
8
9
|
import { Entity } from './entity.js';
|
|
9
10
|
import { Emitter } from './events.js';
|
|
@@ -83,6 +84,11 @@ export declare class Game {
|
|
|
83
84
|
* (ADR 0011): `unloadScene()` never touches it. See ADR 0019.
|
|
84
85
|
*/
|
|
85
86
|
readonly assets: AssetLoader;
|
|
87
|
+
/**
|
|
88
|
+
* Shake, Fade and Flash, beside `camera` and never written into it
|
|
89
|
+
* (ADR 0020). A Fade survives `unloadScene()`; a Shake or Flash does not.
|
|
90
|
+
*/
|
|
91
|
+
readonly cameraEffects: CameraEffects;
|
|
86
92
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
87
93
|
readonly time: GameTime;
|
|
88
94
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
package/dist/game.js
CHANGED
|
@@ -4,7 +4,8 @@ import { AssetLoader } from './assets/asset-loader.js';
|
|
|
4
4
|
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
|
-
import {
|
|
7
|
+
import { advanceCameraEffects, CameraEffects, layoutCameraEffects } from './camera-effects.js';
|
|
8
|
+
import { implementsOnUpdate, resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
8
9
|
import { Entity } from './entity.js';
|
|
9
10
|
import { Emitter } from './events.js';
|
|
10
11
|
import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedToStep, } from './fixed-step.js';
|
|
@@ -19,6 +20,16 @@ import { loadScene, registryEntry, spawnFromJson, } from './scene.js';
|
|
|
19
20
|
import { createSpatialQuery } from './spatial-query.js';
|
|
20
21
|
import { Stats } from './stats.js';
|
|
21
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
|
+
}
|
|
22
33
|
/**
|
|
23
34
|
* Engine core: loop, unified 2D/3D three scene, orthographic camera,
|
|
24
35
|
* entities with components, and input. See DESIGN.md.
|
|
@@ -42,6 +53,11 @@ export class Game {
|
|
|
42
53
|
* (ADR 0011): `unloadScene()` never touches it. See ADR 0019.
|
|
43
54
|
*/
|
|
44
55
|
assets;
|
|
56
|
+
/**
|
|
57
|
+
* Shake, Fade and Flash, beside `camera` and never written into it
|
|
58
|
+
* (ADR 0020). A Fade survives `unloadScene()`; a Shake or Flash does not.
|
|
59
|
+
*/
|
|
60
|
+
cameraEffects;
|
|
45
61
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
46
62
|
time = new GameTime();
|
|
47
63
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
@@ -116,6 +132,11 @@ export class Game {
|
|
|
116
132
|
// catalog is registered at call time, which outlives every scene.
|
|
117
133
|
resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
|
|
118
134
|
});
|
|
135
|
+
this.cameraEffects = new CameraEffects({
|
|
136
|
+
host: () => canvas.parentElement ?? document.body,
|
|
137
|
+
// One screen pixel in world units: the shake snaps to it (never the base).
|
|
138
|
+
pixel: () => (this.resolution ? this.viewHeight / this.resolution.height : null),
|
|
139
|
+
});
|
|
119
140
|
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
120
141
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
121
142
|
this.scene.background = new THREE.Color(background);
|
|
@@ -173,6 +194,7 @@ export class Game {
|
|
|
173
194
|
// Scene-scoped timers/tweens die here too (ADR 0017): before entities are
|
|
174
195
|
// destroyed below, so an owner's own destroy() cancellation is a no-op.
|
|
175
196
|
this.time.cancelSceneScoped();
|
|
197
|
+
this.cameraEffects.unloadScene();
|
|
176
198
|
// An explicit unload means "no scene": a swap queued earlier this frame
|
|
177
199
|
// would otherwise flush next frame and resurrect one.
|
|
178
200
|
this.pendingSceneLoad = null;
|
|
@@ -253,7 +275,7 @@ export class Game {
|
|
|
253
275
|
}
|
|
254
276
|
/** Applies persisted overrides to a freshly added component. */
|
|
255
277
|
applyParamOverrides(entity, component) {
|
|
256
|
-
const Class = component
|
|
278
|
+
const Class = componentClassOf(component);
|
|
257
279
|
const override = this.paramOverrides[entity.name]?.[Class.componentName];
|
|
258
280
|
if (override)
|
|
259
281
|
Object.assign(component, override);
|
|
@@ -366,6 +388,7 @@ export class Game {
|
|
|
366
388
|
this.resizeObserver.disconnect();
|
|
367
389
|
this.ui.dispose();
|
|
368
390
|
this.audio.dispose();
|
|
391
|
+
this.cameraEffects.dispose();
|
|
369
392
|
// Cancels both scopes, including session-scoped work no entity owns
|
|
370
393
|
// (entity.destroy() below only ever reaches owned work) — ADR 0017.
|
|
371
394
|
this.time.cancelAll();
|
|
@@ -478,6 +501,7 @@ export class Game {
|
|
|
478
501
|
}
|
|
479
502
|
this.dispatchCollisions();
|
|
480
503
|
this.updateSceneCamera(SIMULATION_STEP);
|
|
504
|
+
advanceCameraEffects(this.cameraEffects);
|
|
481
505
|
this.finishStep();
|
|
482
506
|
}
|
|
483
507
|
/** Closes a step (real or the non-simulating stand-in): host callbacks, then the input frame. */
|
|
@@ -521,8 +545,12 @@ export class Game {
|
|
|
521
545
|
}
|
|
522
546
|
}
|
|
523
547
|
const z = ySortZ(entries);
|
|
524
|
-
for (const [index, participant] of participants.entries())
|
|
525
|
-
|
|
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
|
+
}
|
|
526
554
|
}
|
|
527
555
|
renderSurface() {
|
|
528
556
|
if (this.sceneProjection === 'isometric') {
|
|
@@ -535,15 +563,28 @@ export class Game {
|
|
|
535
563
|
if (this.renderSort === 'y')
|
|
536
564
|
this.applyYSort();
|
|
537
565
|
this.ui.setActive(this.simulate);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
566
|
+
// The shake offset exists only while drawing (ADR 0020): Anchored Pieces
|
|
567
|
+
// and the render see the drawn center, then the base comes back exactly,
|
|
568
|
+
// so follow smoothing never starts from its own jitter.
|
|
569
|
+
const { x, y } = this.camera.position;
|
|
570
|
+
const { shake } = this.cameraEffects.state;
|
|
571
|
+
this.camera.position.x = x + shake.x;
|
|
572
|
+
this.camera.position.y = y + shake.y;
|
|
573
|
+
try {
|
|
574
|
+
anchoredPiecesOf(this.ui).place();
|
|
575
|
+
if (this.resolution) {
|
|
576
|
+
// Letterbox bars: clear the whole canvas, then render inside the scissor.
|
|
577
|
+
this.renderer.setScissorTest(false);
|
|
578
|
+
this.renderer.setClearColor(0x000000, 1);
|
|
579
|
+
this.renderer.clear(true, false, false);
|
|
580
|
+
this.renderer.setScissorTest(true);
|
|
581
|
+
}
|
|
582
|
+
this.renderer.render(this.scene, this.camera);
|
|
583
|
+
}
|
|
584
|
+
finally {
|
|
585
|
+
this.camera.position.x = x;
|
|
586
|
+
this.camera.position.y = y;
|
|
545
587
|
}
|
|
546
|
-
this.renderer.render(this.scene, this.camera);
|
|
547
588
|
}
|
|
548
589
|
componentUpdateSchedule(entity) {
|
|
549
590
|
const components = [...entity.components];
|
|
@@ -551,11 +592,11 @@ export class Game {
|
|
|
551
592
|
const names = [];
|
|
552
593
|
const signatureParts = [];
|
|
553
594
|
for (const component of components) {
|
|
554
|
-
const Class = component
|
|
595
|
+
const Class = componentClassOf(component);
|
|
555
596
|
const name = Class.componentName;
|
|
556
597
|
names.push(name);
|
|
557
598
|
byName.set(name, component);
|
|
558
|
-
signatureParts.push(`${name}:${
|
|
599
|
+
signatureParts.push(`${name}:${implementsOnUpdate(Class) ? 'updates' : 'passive'}:` +
|
|
559
600
|
[...new Set(Class.updateAfter ?? [])].sort().join(','));
|
|
560
601
|
}
|
|
561
602
|
const signature = signatureParts.sort().join('|');
|
|
@@ -565,13 +606,13 @@ export class Game {
|
|
|
565
606
|
// when nothing about this entity's components changed since last time.
|
|
566
607
|
const cached = this.updateScheduleCache.get(entity);
|
|
567
608
|
if (cached && cached.signature === signature) {
|
|
568
|
-
return cached.order
|
|
609
|
+
return componentsInOrder(cached.order, byName);
|
|
569
610
|
}
|
|
570
611
|
const registry = {
|
|
571
612
|
...(this.registry?.components ?? {}),
|
|
572
613
|
};
|
|
573
614
|
for (const component of components) {
|
|
574
|
-
const Class = component
|
|
615
|
+
const Class = componentClassOf(component);
|
|
575
616
|
registry[Class.componentName] = Class;
|
|
576
617
|
}
|
|
577
618
|
const result = resolveComponentUpdateSchedule(names, registry);
|
|
@@ -586,7 +627,7 @@ export class Game {
|
|
|
586
627
|
}
|
|
587
628
|
this.invalidUpdateCompositions.delete(entity);
|
|
588
629
|
this.updateScheduleCache.set(entity, { signature, order: result.order });
|
|
589
|
-
return result.order
|
|
630
|
+
return componentsInOrder(result.order, byName);
|
|
590
631
|
}
|
|
591
632
|
updateSceneCamera(dt) {
|
|
592
633
|
const cam = this.sceneCamera;
|
|
@@ -655,5 +696,6 @@ export class Game {
|
|
|
655
696
|
this.camera.top = halfH;
|
|
656
697
|
this.camera.bottom = -halfH;
|
|
657
698
|
this.camera.updateProjectionMatrix();
|
|
699
|
+
layoutCameraEffects(this.cameraEffects, gameViewport(w, h, this.resolution));
|
|
658
700
|
}
|
|
659
701
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export { spritePlacement } from './sprite-placement.js';
|
|
|
17
17
|
export type { SpritePlacement, SpritePlacementInput } from './sprite-placement.js';
|
|
18
18
|
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
19
19
|
export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
|
|
20
|
+
export { CameraEffects } from './camera-effects.js';
|
|
21
|
+
export type { CameraEffectColor, CameraEffectHandle, CameraEffectsOptions, CameraEffectsState, FadeOptions, FlashOptions, ShakeOptions, } from './camera-effects.js';
|
|
20
22
|
export { GameTime, advanceGameTime } from './game-time.js';
|
|
21
23
|
export type { EasingName, TimerHandle, TimerOptions, TweenOptions } from './game-time.js';
|
|
22
24
|
export type { SceneCameraJson, CameraLimitsJson, CameraVelocity, CameraVelocityProvider, ResolvedSceneCamera, } from './camera.js';
|
|
@@ -35,7 +37,7 @@ export type { PointerCamera, PointerDeps, PointerPick, PointerResolution } from
|
|
|
35
37
|
export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
36
38
|
export type { RuntimeBridge, RuntimeBridgeActivation, RuntimeControlRequest, RuntimeControlResult, RuntimeMetadata, RuntimeMode, } from './runtime-bridge.js';
|
|
37
39
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
|
38
|
-
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
40
|
+
export type { ProjectedValue, ProjectionIssue, ProjectionMarker, ProjectionMarkerKind, RuntimeComponentSnapshot, RuntimeEntitySnapshot, RuntimeSnapshot, RuntimeSnapshotAudio, RuntimeSnapshotCamera, RuntimeSnapshotFilters, RuntimeSnapshotTime, RuntimeSnapshotUi, RuntimeTransformSnapshot, } from './runtime-inspection.js';
|
|
39
41
|
export type { ArchetypeArt, ArchetypeManifest, BrowserArchetypeManifest, EntityTemplate, } from './archetype.js';
|
|
40
42
|
export { Stats } from './stats.js';
|
|
41
43
|
export type { StatValue } from './stats.js';
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { projectIsometric, screenInputToLogical, unprojectIsometric } from './pr
|
|
|
7
7
|
export { spritePlacement } from './sprite-placement.js';
|
|
8
8
|
export { CAMERA_DEFAULTS, isCameraVelocityProvider, resolveSceneCamera, stepSceneCamera } from './camera.js';
|
|
9
9
|
export { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
|
|
10
|
+
export { CameraEffects } from './camera-effects.js';
|
|
10
11
|
export { GameTime, advanceGameTime } from './game-time.js';
|
|
11
12
|
export { Entity } from './entity.js';
|
|
12
13
|
export { Component } from './component.js';
|
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
|
@@ -15,9 +15,11 @@ export type RuntimeMode = 'paused' | 'real-time';
|
|
|
15
15
|
* (ADR 0019) announces that every metadata carries `assets: { pending,
|
|
16
16
|
* loaded, failed }` from `game.assets.status`, so a Run Session can wait
|
|
17
17
|
* for Assets Ready (`pending === 0`) at readiness, after a `scene`
|
|
18
|
-
* operation and before a screenshot.
|
|
18
|
+
* operation and before a screenshot. `camera-effects` (issue #74, ADR 0020)
|
|
19
|
+
* announces that every snapshot carries `camera: { shake, fade, flash }`
|
|
20
|
+
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
19
21
|
*/
|
|
20
|
-
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets'];
|
|
22
|
+
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects'];
|
|
21
23
|
export interface RuntimeMetadata {
|
|
22
24
|
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
23
25
|
engineVersion: string;
|
|
@@ -104,6 +106,10 @@ export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
|
104
106
|
metadata(): RuntimeMetadata;
|
|
105
107
|
inspect(filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
106
108
|
control(request: RuntimeControlRequest): RuntimeControlResult;
|
|
109
|
+
private injectAction;
|
|
110
|
+
/** Advances whole Simulation Steps while paused. */
|
|
111
|
+
private step;
|
|
112
|
+
private loadScene;
|
|
107
113
|
/** Counts one Simulation Step, whoever ran it (paused stepping or real-time playback). */
|
|
108
114
|
private advance;
|
|
109
115
|
unregister(): void;
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -14,9 +14,17 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
|
14
14
|
* (ADR 0019) announces that every metadata carries `assets: { pending,
|
|
15
15
|
* loaded, failed }` from `game.assets.status`, so a Run Session can wait
|
|
16
16
|
* for Assets Ready (`pending === 0`) at readiness, after a `scene`
|
|
17
|
-
* operation and before a screenshot.
|
|
17
|
+
* operation and before a screenshot. `camera-effects` (issue #74, ADR 0020)
|
|
18
|
+
* announces that every snapshot carries `camera: { shake, fade, flash }`
|
|
19
|
+
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
18
20
|
*/
|
|
19
|
-
export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
21
|
+
export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
22
|
+
'click',
|
|
23
|
+
'scene',
|
|
24
|
+
'fixed-step',
|
|
25
|
+
'assets',
|
|
26
|
+
'camera-effects',
|
|
27
|
+
];
|
|
20
28
|
export class RuntimeBridgeOperationError extends Error {
|
|
21
29
|
code;
|
|
22
30
|
availableActions;
|
|
@@ -88,29 +96,11 @@ export class EngineRuntimeBridge {
|
|
|
88
96
|
case 'press':
|
|
89
97
|
case 'hold':
|
|
90
98
|
case 'release':
|
|
91
|
-
|
|
92
|
-
const available = this.host.availableActions();
|
|
93
|
-
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown action "${request.action}". Available actions: ${available.join(', ') || '(none)'}.`, available);
|
|
94
|
-
}
|
|
99
|
+
this.injectAction(request);
|
|
95
100
|
break;
|
|
96
|
-
case 'step':
|
|
97
|
-
|
|
98
|
-
throw new RuntimeBridgeOperationError('runtime-invalid-state', 'step is only available while the Runtime Bridge is paused.');
|
|
99
|
-
}
|
|
100
|
-
// A caller-chosen dt is rejected outright, never ignored: a pre-ADR-0014
|
|
101
|
-
// client that still sends one would otherwise believe it stepped by it.
|
|
102
|
-
if ('dt' in request) {
|
|
103
|
-
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.');
|
|
104
|
-
}
|
|
105
|
-
const frames = request.frames ?? 1;
|
|
106
|
-
if (!Number.isInteger(frames) || frames < 1 || frames > 600) {
|
|
107
|
-
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'frames must be an integer from 1 through 600.');
|
|
108
|
-
}
|
|
109
|
-
for (let index = 0; index < frames; index += 1) {
|
|
110
|
-
this.host.step(() => this.advance());
|
|
111
|
-
}
|
|
101
|
+
case 'step':
|
|
102
|
+
this.step(request);
|
|
112
103
|
break;
|
|
113
|
-
}
|
|
114
104
|
case 'click': {
|
|
115
105
|
if (!Number.isFinite(request.x) || !Number.isFinite(request.y)) {
|
|
116
106
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'x and y must be finite numbers.');
|
|
@@ -118,13 +108,9 @@ export class EngineRuntimeBridge {
|
|
|
118
108
|
this.host.click(request.x, request.y);
|
|
119
109
|
break;
|
|
120
110
|
}
|
|
121
|
-
case 'scene':
|
|
122
|
-
|
|
123
|
-
const available = this.host.availableScenes();
|
|
124
|
-
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown scene "${request.scene}". Available scenes: ${available.join(', ') || '(none)'}.`, undefined, available);
|
|
125
|
-
}
|
|
111
|
+
case 'scene':
|
|
112
|
+
this.loadScene(request.scene);
|
|
126
113
|
break;
|
|
127
|
-
}
|
|
128
114
|
default: {
|
|
129
115
|
const unsupported = request;
|
|
130
116
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unsupported runtime control operation "${unsupported.operation}".`);
|
|
@@ -132,6 +118,36 @@ export class EngineRuntimeBridge {
|
|
|
132
118
|
}
|
|
133
119
|
return { ...this.metadata(), heldActions: this.host.heldActions() };
|
|
134
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
|
+
}
|
|
135
151
|
/** Counts one Simulation Step, whoever ran it (paused stepping or real-time playback). */
|
|
136
152
|
advance() {
|
|
137
153
|
this.frame += 1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AudioChannelState, LiveSoundInfo } from './audio/types.js';
|
|
2
|
+
import type { CameraEffectsState } from './camera-effects.js';
|
|
2
3
|
import type { Game } from './game.js';
|
|
3
4
|
import type { RuntimeMetadata } from './runtime-bridge.js';
|
|
4
5
|
import type { StatValue } from './stats.js';
|
|
@@ -106,6 +107,13 @@ export interface RuntimeSnapshotUi {
|
|
|
106
107
|
values: Record<string, StatValue | ProjectionMarker> | ProjectionMarker;
|
|
107
108
|
}>;
|
|
108
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* `game.cameraEffects.state` (issue #74 CA-17), beside `time` and `ui`: the
|
|
112
|
+
* shake offset (world units, added to the camera only while drawing) and
|
|
113
|
+
* the Fade and Flash layers' `#rrggbb` color and opacity, all from the
|
|
114
|
+
* last completed Simulation Step. Emitted unconditionally — never filtered.
|
|
115
|
+
*/
|
|
116
|
+
export type RuntimeSnapshotCamera = CameraEffectsState;
|
|
109
117
|
export interface RuntimeSnapshot extends RuntimeMetadata {
|
|
110
118
|
stats: Record<string, StatValue>;
|
|
111
119
|
/** The live scene's name (its catalog key), or null with no scene loaded. */
|
|
@@ -115,6 +123,7 @@ export interface RuntimeSnapshot extends RuntimeMetadata {
|
|
|
115
123
|
audio: RuntimeSnapshotAudio;
|
|
116
124
|
time: RuntimeSnapshotTime;
|
|
117
125
|
ui: RuntimeSnapshotUi;
|
|
126
|
+
camera: RuntimeSnapshotCamera;
|
|
118
127
|
}
|
|
119
128
|
export declare const RUNTIME_PROJECTION_LIMITS: {
|
|
120
129
|
readonly depth: 5;
|