@waica/engine 0.18.0 → 0.19.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/camera-effects.d.ts +142 -0
- package/dist/camera-effects.js +334 -0
- 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 +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/runtime-bridge.d.ts +4 -2
- package/dist/runtime-bridge.js +10 -2
- package/dist/runtime-inspection.d.ts +9 -0
- package/dist/runtime-inspection.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -194,6 +194,28 @@ game.time.now // seconds of Game Time since the Game started
|
|
|
194
194
|
- **Scope.** A timer or tween is scene-scoped by default: `unloadScene()` and every scene load — including a Game's first — cancel it, running no callback. `{ scope: 'session' }` survives a scene change. `{ owner: entity }` cancels it immediately when that entity is destroyed, whatever its scope; an owner already dead at scheduling time yields an inactive handle. `game.dispose()` cancels everything in both scopes. This is the opposite default from `game.onUpdate`/`game.events` (ADR 0011), which survive a scene change by construction, and the same one `game.audio.play()` uses (ADR 0012) — see ADR 0017 for why timers follow audio's rule rather than the host-subscription one: a timer's callback almost always closes over the scene that scheduled it.
|
|
195
195
|
- **No Promises.** Nothing here returns one, and nothing is async — a `.then` continuation is not step-exact (it runs after the whole synchronous frame), which is exactly what `game.time` exists to avoid. Compose delays with `after`, not `await`.
|
|
196
196
|
|
|
197
|
+
## game.cameraEffects: shake, fade and flash
|
|
198
|
+
|
|
199
|
+
Every `Game` owns a `cameraEffects: CameraEffects` service beside `game.camera`, which stays the plain `THREE.OrthographicCamera` (ADR 0020). Effects advance only inside a Simulation Step, right after the scene camera, so nothing moves while the Game is not simulating, is stopped, or a Run Session is paused; `step { frames: N }` advances them by exactly N steps.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
game.cameraEffects.shake({ intensity: 0.3, seconds: 0.25 }) // world units, decaying to 0
|
|
203
|
+
const out = game.cameraEffects.fade({ to: 'black', seconds: 0.4 }) // 'black' | 'white' | '#rrggbb' | 'clear'
|
|
204
|
+
game.cameraEffects.flash({ color: 'white', seconds: 0.15 }) // up to 1, back to exactly 0
|
|
205
|
+
out.cancel() // opacity stays where it was
|
|
206
|
+
await out.done // true: completed; false: cancelled or replaced
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- **Shake.** An offset added to the camera only while drawing (the world render and Anchored Pieces), never to the base center that follow, deadzone and limits compute, so smoothing never feeds on it. Each axis stays within `intensity · (1 − ease(t / seconds))` (`easing` takes `game.time`'s names, `'linear'` by default) and is exactly `{0, 0}` from `seconds` on. Overlapping shakes apply the largest current amplitude. The jitter is deterministic per Simulation Step (no `Math.random`), so a Run Session reproduces it. It is not re-clamped to `limits`: at a map edge up to `intensity` beyond them can show. Under a fixed `resolution` the offset snaps to whole screen pixels; the base center is never snapped.
|
|
210
|
+
- **Fade.** Carries an HTML layer over the game viewport — inside the letterbox, above the world, UI Pieces and Anchored Pieces — from its current opacity to 1 in the given color, or to 0 with `'clear'`, and then holds. A new `fade` cancels a running one (its `done` resolves `false`) and starts from the current opacity.
|
|
211
|
+
- **Flash.** Its own layer above the Fade, shown at opacity 1 and returned to exactly 0 over `seconds`; it never changes the Fade.
|
|
212
|
+
- **Scope.** A Fade is session-scoped: it survives `unloadScene()` and every scene load with its color, opacity and any running progress, so it can cover a scene change. A Shake or Flash is scene-scoped: `unloadScene()` ends it, running no callback (its `done` resolves `false`). `game.dispose()` ends everything and removes the layers. This follows the audio and timer scope notes above (ADR 0011, ADR 0017): a transition fade is exactly the case that must cross a scene change.
|
|
213
|
+
- **Invalid arguments never throw.** A non-finite or negative `seconds`/`intensity`, an unknown color or an unknown easing logs one `[waica]` warning and returns a handle whose `done` resolves `false`; no effect changes.
|
|
214
|
+
- **Scene Transition.** `SceneTransition` takes `fadeSeconds` (default 0: a hard cut) and `fadeColor` (default `'black'`): it fades out over `fadeSeconds`, loads its destination, then clears over `fadeSeconds` in the incoming scene. Simulation and input keep running; the component ignores further triggers during its outgoing fade. The Runtime Bridge `scene` operation stays a hard cut.
|
|
215
|
+
- **Runtime Snapshot.** Every snapshot carries `camera: { shake: { x, y }, fade: { color, opacity }, flash: { color, opacity } }` from the last completed step, under the `'camera-effects'` capability.
|
|
216
|
+
|
|
217
|
+
A Fade to a color that nothing clears leaves the view covered; the engine does not warn.
|
|
218
|
+
|
|
197
219
|
## game.ui.attach: Anchored Pieces
|
|
198
220
|
|
|
199
221
|
A UI Piece shown with `game.ui.show(name)` is a screen-space singleton. `game.ui.attach(piece, entity, options?)` instead creates an **Anchored Piece**: a new instance of the piece that follows `entity` across the screen, with its own shadow root and its own values. Every call is a new instance — five orcs can each carry a `health-bar` — and the screen piece of the same name is never mounted, shown or changed by it.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { ViewportRect } from './anchored-pieces.js';
|
|
2
|
+
import { type EasingName } from './game-time.js';
|
|
3
|
+
/** A Camera Effect color: a name or a six-digit hex, case-insensitive. */
|
|
4
|
+
export type CameraEffectColor = 'black' | 'white' | `#${string}`;
|
|
5
|
+
export interface ShakeOptions {
|
|
6
|
+
/** The largest offset, in world units, on each axis. Finite and >= 0. */
|
|
7
|
+
intensity: number;
|
|
8
|
+
/** Seconds of Game Time until the offset reaches 0. Finite and >= 0. */
|
|
9
|
+
seconds: number;
|
|
10
|
+
/** How the amplitude decays from `intensity` to 0; default `'linear'`. */
|
|
11
|
+
easing?: EasingName;
|
|
12
|
+
}
|
|
13
|
+
export interface FadeOptions {
|
|
14
|
+
/** The color to carry the view to (opacity 1), or `'clear'` (opacity 0, color kept). */
|
|
15
|
+
to: CameraEffectColor | 'clear';
|
|
16
|
+
/** Seconds of Game Time from the current opacity to the target. Finite and >= 0. */
|
|
17
|
+
seconds: number;
|
|
18
|
+
easing?: EasingName;
|
|
19
|
+
}
|
|
20
|
+
export interface FlashOptions {
|
|
21
|
+
color: CameraEffectColor;
|
|
22
|
+
/** Seconds of Game Time from opacity 1 back to 0. Finite and >= 0. */
|
|
23
|
+
seconds: number;
|
|
24
|
+
}
|
|
25
|
+
/** What `shake`, `fade` and `flash` hand back. */
|
|
26
|
+
export interface CameraEffectHandle {
|
|
27
|
+
/** Stops the effect where it is. Idempotent; resolves `done` with false. */
|
|
28
|
+
cancel(): void;
|
|
29
|
+
/** True once the effect completed; false if cancelled, replaced, ended by its scope or invalid. */
|
|
30
|
+
readonly done: Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* True once a *later* call to the same kind of effect has taken over
|
|
33
|
+
* before this one ended on its own — false while it is still running,
|
|
34
|
+
* and false once it ends on its own accord (completes, is cancelled, or
|
|
35
|
+
* ends by its scope) with nothing else in its place. A Shake never
|
|
36
|
+
* supersedes another (CA-7: overlapping shakes coexist), so this is
|
|
37
|
+
* always false for a Shake handle. Unlike `done`, this reads
|
|
38
|
+
* synchronously: a Promise only notifies its `.then()` in a microtask,
|
|
39
|
+
* which can land well after a caller scheduled on Game Time needs the
|
|
40
|
+
* answer on an exact Simulation Step (PR #98 review, `scene-
|
|
41
|
+
* transition.ts`).
|
|
42
|
+
*/
|
|
43
|
+
readonly superseded: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** The effects' state after the last completed Simulation Step (read-only copy). */
|
|
46
|
+
export interface CameraEffectsState {
|
|
47
|
+
/** The offset, in world units, added to the camera only while drawing. */
|
|
48
|
+
shake: {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
};
|
|
52
|
+
fade: {
|
|
53
|
+
color: string;
|
|
54
|
+
opacity: number;
|
|
55
|
+
};
|
|
56
|
+
flash: {
|
|
57
|
+
color: string;
|
|
58
|
+
opacity: number;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export interface CameraEffectsOptions {
|
|
62
|
+
/** Where the Fade and Flash layers mount (the canvas's parent); resolved lazily. */
|
|
63
|
+
host: () => HTMLElement;
|
|
64
|
+
/** One screen pixel in world units under a fixed resolution, or null (no snap). */
|
|
65
|
+
pixel: () => number | null;
|
|
66
|
+
}
|
|
67
|
+
declare const ADVANCE: unique symbol;
|
|
68
|
+
declare const LAYOUT: unique symbol;
|
|
69
|
+
/**
|
|
70
|
+
* Screen-level Camera Effects (issue #74, ADR 0020): Shake, Fade and Flash,
|
|
71
|
+
* beside `game.camera` and never written into it. They advance only inside
|
|
72
|
+
* a Simulation Step, right after the scene camera (spec inference 9), so
|
|
73
|
+
* nothing moves while the Game is not simulating or a Run Session is
|
|
74
|
+
* paused. A Shake is an offset the Game adds to the camera only while it
|
|
75
|
+
* draws; a Fade and a Flash are HTML layers over the letterboxed game
|
|
76
|
+
* viewport, above the UI overlay. A Fade is session-scoped (it survives a
|
|
77
|
+
* scene change, ADR 0011); a Shake or Flash dies with its scene.
|
|
78
|
+
*/
|
|
79
|
+
export declare class CameraEffects {
|
|
80
|
+
private readonly options;
|
|
81
|
+
private steps;
|
|
82
|
+
private readonly shakes;
|
|
83
|
+
private offset;
|
|
84
|
+
private fadeRun;
|
|
85
|
+
private flashRun;
|
|
86
|
+
private readonly fadeLayer;
|
|
87
|
+
private readonly flashLayer;
|
|
88
|
+
private rect;
|
|
89
|
+
constructor(options: CameraEffectsOptions);
|
|
90
|
+
/** A fresh copy of the effects after the last completed Simulation Step. */
|
|
91
|
+
get state(): CameraEffectsState;
|
|
92
|
+
/**
|
|
93
|
+
* Jitters the drawn view by up to `intensity` world units per axis,
|
|
94
|
+
* decaying to 0 over `seconds`. Overlapping shakes apply the largest
|
|
95
|
+
* current amplitude. Not re-clamped to the scene camera's limits; snapped
|
|
96
|
+
* to whole screen pixels under a fixed resolution.
|
|
97
|
+
*/
|
|
98
|
+
shake(options: ShakeOptions): CameraEffectHandle;
|
|
99
|
+
/**
|
|
100
|
+
* Carries the Fade layer from its current opacity to 1 in `to`'s color,
|
|
101
|
+
* or to 0 with `'clear'`, over `seconds`; the result then holds. A new
|
|
102
|
+
* fade cancels a running one (its `done` resolves false).
|
|
103
|
+
*/
|
|
104
|
+
fade(options: FadeOptions): CameraEffectHandle;
|
|
105
|
+
/**
|
|
106
|
+
* Shows the Flash layer at opacity 1 in `color` and returns it to exactly
|
|
107
|
+
* 0 over `seconds`, independent of (and drawn above) the Fade. A new flash
|
|
108
|
+
* replaces a running one.
|
|
109
|
+
*/
|
|
110
|
+
flash(options: FlashOptions): CameraEffectHandle;
|
|
111
|
+
/**
|
|
112
|
+
* Ends every Shake and Flash (their `done` resolves false) and keeps the
|
|
113
|
+
* Fade — opacity, color and any running progress. Called by
|
|
114
|
+
* `Game.unloadScene()`.
|
|
115
|
+
*/
|
|
116
|
+
unloadScene(): void;
|
|
117
|
+
/** Ends every effect and removes the layers (`Game.dispose()`). */
|
|
118
|
+
dispose(): void;
|
|
119
|
+
/** Engine-internal: see advanceCameraEffects. */
|
|
120
|
+
[ADVANCE](): void;
|
|
121
|
+
/** Engine-internal: see layoutCameraEffects. */
|
|
122
|
+
[LAYOUT](rect: ViewportRect): void;
|
|
123
|
+
/** Normalized progress of a timed effect after the current step: 1 at (and past) its end. */
|
|
124
|
+
private progress;
|
|
125
|
+
private advanceShakes;
|
|
126
|
+
/** Mirrors both layers' state onto their elements, mounting them on first need. */
|
|
127
|
+
private draw;
|
|
128
|
+
private mount;
|
|
129
|
+
/** Sizes a layer to the game viewport (the whole host until the Game lays it out). */
|
|
130
|
+
private place;
|
|
131
|
+
private invalid;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Engine-internal: advances every Camera Effect by exactly one Simulation
|
|
135
|
+
* Step. `Game.simulateStep()` calls it right after the scene camera, so
|
|
136
|
+
* nothing advances while the Game is not simulating. Not re-exported from
|
|
137
|
+
* the package entry.
|
|
138
|
+
*/
|
|
139
|
+
export declare function advanceCameraEffects(effects: CameraEffects): void;
|
|
140
|
+
/** Engine-internal: sizes the Fade and Flash layers to the game viewport (`Game.resize()`). */
|
|
141
|
+
export declare function layoutCameraEffects(effects: CameraEffects, rect: ViewportRect): void;
|
|
142
|
+
export {};
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { SIMULATION_STEP, SIMULATION_TIME_EPSILON } from './fixed-step.js';
|
|
2
|
+
import { resolveEasing } from './game-time.js';
|
|
3
|
+
const COLORS = { black: '#000000', white: '#ffffff' };
|
|
4
|
+
/** Above the UI overlay (`ui.ts`, z-index 9000): a Fade covers the HUD too. */
|
|
5
|
+
const FADE_Z = 9001;
|
|
6
|
+
const FLASH_Z = 9002;
|
|
7
|
+
const ADVANCE = Symbol('waica.cameraEffects.advance');
|
|
8
|
+
const LAYOUT = Symbol('waica.cameraEffects.layout');
|
|
9
|
+
function normalizeColor(value) {
|
|
10
|
+
if (typeof value !== 'string')
|
|
11
|
+
return null;
|
|
12
|
+
if (Object.prototype.hasOwnProperty.call(COLORS, value))
|
|
13
|
+
return COLORS[value];
|
|
14
|
+
return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toLowerCase() : null;
|
|
15
|
+
}
|
|
16
|
+
function validSeconds(value) {
|
|
17
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
18
|
+
}
|
|
19
|
+
function easingOf(value) {
|
|
20
|
+
if (value !== undefined && typeof value !== 'string')
|
|
21
|
+
return null;
|
|
22
|
+
return resolveEasing(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Integer-only hash of (shake creation step, current step, axis) into
|
|
26
|
+
* [0, 1): the same bits in Node and every browser (spec inference 3), so a
|
|
27
|
+
* Run Session reproduces a shake frame for frame. No global RNG.
|
|
28
|
+
*/
|
|
29
|
+
function jitter(seed, step, axis) {
|
|
30
|
+
let h = (Math.imul(seed + 1, 0x9e3779b1) ^ Math.imul(step + 1, 0x85ebca77) ^ Math.imul(axis + 1, 0xc2b2ae3d)) >>> 0;
|
|
31
|
+
h = Math.imul(h ^ (h >>> 16), 0x7feb352d) >>> 0;
|
|
32
|
+
h = Math.imul(h ^ (h >>> 15), 0x846ca68b) >>> 0;
|
|
33
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
34
|
+
}
|
|
35
|
+
function settledHandle(done, cancel, superseded) {
|
|
36
|
+
return {
|
|
37
|
+
cancel,
|
|
38
|
+
done,
|
|
39
|
+
get superseded() {
|
|
40
|
+
return superseded();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Screen-level Camera Effects (issue #74, ADR 0020): Shake, Fade and Flash,
|
|
46
|
+
* beside `game.camera` and never written into it. They advance only inside
|
|
47
|
+
* a Simulation Step, right after the scene camera (spec inference 9), so
|
|
48
|
+
* nothing moves while the Game is not simulating or a Run Session is
|
|
49
|
+
* paused. A Shake is an offset the Game adds to the camera only while it
|
|
50
|
+
* draws; a Fade and a Flash are HTML layers over the letterboxed game
|
|
51
|
+
* viewport, above the UI overlay. A Fade is session-scoped (it survives a
|
|
52
|
+
* scene change, ADR 0011); a Shake or Flash dies with its scene.
|
|
53
|
+
*/
|
|
54
|
+
export class CameraEffects {
|
|
55
|
+
options;
|
|
56
|
+
steps = 0;
|
|
57
|
+
shakes = [];
|
|
58
|
+
offset = { x: 0, y: 0 };
|
|
59
|
+
fadeRun = null;
|
|
60
|
+
flashRun = null;
|
|
61
|
+
fadeLayer = { color: '#000000', opacity: 0, element: null };
|
|
62
|
+
flashLayer = { color: '#ffffff', opacity: 0, element: null };
|
|
63
|
+
rect = null;
|
|
64
|
+
constructor(options) {
|
|
65
|
+
this.options = options;
|
|
66
|
+
}
|
|
67
|
+
/** A fresh copy of the effects after the last completed Simulation Step. */
|
|
68
|
+
get state() {
|
|
69
|
+
return {
|
|
70
|
+
shake: { ...this.offset },
|
|
71
|
+
fade: { color: this.fadeLayer.color, opacity: this.fadeLayer.opacity },
|
|
72
|
+
flash: { color: this.flashLayer.color, opacity: this.flashLayer.opacity },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Jitters the drawn view by up to `intensity` world units per axis,
|
|
77
|
+
* decaying to 0 over `seconds`. Overlapping shakes apply the largest
|
|
78
|
+
* current amplitude. Not re-clamped to the scene camera's limits; snapped
|
|
79
|
+
* to whole screen pixels under a fixed resolution.
|
|
80
|
+
*/
|
|
81
|
+
shake(options) {
|
|
82
|
+
const { intensity, seconds } = options;
|
|
83
|
+
if (!validSeconds(intensity))
|
|
84
|
+
return this.invalid('shake(): intensity must be a finite number >= 0');
|
|
85
|
+
if (!validSeconds(seconds))
|
|
86
|
+
return this.invalid('shake(): seconds must be a finite number >= 0');
|
|
87
|
+
const ease = easingOf(options.easing);
|
|
88
|
+
if (!ease)
|
|
89
|
+
return this.invalid('shake(): easing must be a known easing name');
|
|
90
|
+
let resolve = () => { };
|
|
91
|
+
const done = new Promise((r) => (resolve = r));
|
|
92
|
+
const entry = { start: this.steps, seconds, ease, resolve, intensity, superseded: false };
|
|
93
|
+
this.shakes.push(entry);
|
|
94
|
+
return settledHandle(done, () => {
|
|
95
|
+
const index = this.shakes.indexOf(entry);
|
|
96
|
+
if (index === -1)
|
|
97
|
+
return;
|
|
98
|
+
this.shakes.splice(index, 1);
|
|
99
|
+
entry.resolve(false);
|
|
100
|
+
},
|
|
101
|
+
// Never superseded (CA-7): overlapping shakes coexist, so nothing
|
|
102
|
+
// ever sets a Shake entry's flag; it stays false for its own life.
|
|
103
|
+
() => entry.superseded);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Carries the Fade layer from its current opacity to 1 in `to`'s color,
|
|
107
|
+
* or to 0 with `'clear'`, over `seconds`; the result then holds. A new
|
|
108
|
+
* fade cancels a running one (its `done` resolves false).
|
|
109
|
+
*/
|
|
110
|
+
fade(options) {
|
|
111
|
+
const clear = options.to === 'clear';
|
|
112
|
+
const color = clear ? this.fadeLayer.color : normalizeColor(options.to);
|
|
113
|
+
if (!color)
|
|
114
|
+
return this.invalid(`fade(): unknown color "${String(options.to)}"; use 'black', 'white', '#rrggbb' or 'clear'`);
|
|
115
|
+
if (!validSeconds(options.seconds))
|
|
116
|
+
return this.invalid('fade(): seconds must be a finite number >= 0');
|
|
117
|
+
const ease = easingOf(options.easing);
|
|
118
|
+
if (!ease)
|
|
119
|
+
return this.invalid('fade(): easing must be a known easing name');
|
|
120
|
+
// Marks the outgoing fade superseded (not just cancelled) before
|
|
121
|
+
// resolving `done` false: a caller checking `.superseded` later must
|
|
122
|
+
// see this, whatever happens to the layer afterwards (PR #98 review).
|
|
123
|
+
if (this.fadeRun)
|
|
124
|
+
this.fadeRun.superseded = true;
|
|
125
|
+
this.fadeRun?.resolve(false);
|
|
126
|
+
let resolve = () => { };
|
|
127
|
+
const done = new Promise((r) => (resolve = r));
|
|
128
|
+
const entry = {
|
|
129
|
+
start: this.steps,
|
|
130
|
+
seconds: options.seconds,
|
|
131
|
+
ease,
|
|
132
|
+
resolve,
|
|
133
|
+
from: this.fadeLayer.opacity,
|
|
134
|
+
to: clear ? 0 : 1,
|
|
135
|
+
superseded: false,
|
|
136
|
+
};
|
|
137
|
+
this.fadeRun = entry;
|
|
138
|
+
this.fadeLayer.color = color;
|
|
139
|
+
this.draw();
|
|
140
|
+
return settledHandle(done, () => {
|
|
141
|
+
if (this.fadeRun !== entry)
|
|
142
|
+
return;
|
|
143
|
+
this.fadeRun = null;
|
|
144
|
+
entry.resolve(false);
|
|
145
|
+
}, () => entry.superseded);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Shows the Flash layer at opacity 1 in `color` and returns it to exactly
|
|
149
|
+
* 0 over `seconds`, independent of (and drawn above) the Fade. A new flash
|
|
150
|
+
* replaces a running one.
|
|
151
|
+
*/
|
|
152
|
+
flash(options) {
|
|
153
|
+
const color = normalizeColor(options.color);
|
|
154
|
+
if (!color)
|
|
155
|
+
return this.invalid(`flash(): unknown color "${String(options.color)}"; use 'black', 'white' or '#rrggbb'`);
|
|
156
|
+
if (!validSeconds(options.seconds))
|
|
157
|
+
return this.invalid('flash(): seconds must be a finite number >= 0');
|
|
158
|
+
if (this.flashRun)
|
|
159
|
+
this.flashRun.superseded = true;
|
|
160
|
+
this.flashRun?.resolve(false);
|
|
161
|
+
let resolve = () => { };
|
|
162
|
+
const done = new Promise((r) => (resolve = r));
|
|
163
|
+
const entry = { start: this.steps, seconds: options.seconds, ease: (t) => t, resolve, superseded: false };
|
|
164
|
+
this.flashRun = entry;
|
|
165
|
+
this.flashLayer.color = color;
|
|
166
|
+
this.flashLayer.opacity = 1;
|
|
167
|
+
this.draw();
|
|
168
|
+
return settledHandle(done, () => {
|
|
169
|
+
if (this.flashRun !== entry)
|
|
170
|
+
return;
|
|
171
|
+
this.flashRun = null;
|
|
172
|
+
this.flashLayer.opacity = 0;
|
|
173
|
+
this.draw();
|
|
174
|
+
entry.resolve(false);
|
|
175
|
+
}, () => entry.superseded);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Ends every Shake and Flash (their `done` resolves false) and keeps the
|
|
179
|
+
* Fade — opacity, color and any running progress. Called by
|
|
180
|
+
* `Game.unloadScene()`.
|
|
181
|
+
*/
|
|
182
|
+
unloadScene() {
|
|
183
|
+
for (const shake of this.shakes.splice(0))
|
|
184
|
+
shake.resolve(false);
|
|
185
|
+
this.offset = { x: 0, y: 0 };
|
|
186
|
+
this.flashRun?.resolve(false);
|
|
187
|
+
this.flashRun = null;
|
|
188
|
+
this.flashLayer.opacity = 0;
|
|
189
|
+
this.draw();
|
|
190
|
+
}
|
|
191
|
+
/** Ends every effect and removes the layers (`Game.dispose()`). */
|
|
192
|
+
dispose() {
|
|
193
|
+
this.unloadScene();
|
|
194
|
+
this.fadeRun?.resolve(false);
|
|
195
|
+
this.fadeRun = null;
|
|
196
|
+
this.fadeLayer.color = '#000000';
|
|
197
|
+
this.fadeLayer.opacity = 0;
|
|
198
|
+
for (const layer of [this.fadeLayer, this.flashLayer]) {
|
|
199
|
+
layer.element?.remove();
|
|
200
|
+
layer.element = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Engine-internal: see advanceCameraEffects. */
|
|
204
|
+
[ADVANCE]() {
|
|
205
|
+
this.steps += 1;
|
|
206
|
+
this.advanceShakes();
|
|
207
|
+
const fade = this.fadeRun;
|
|
208
|
+
if (fade) {
|
|
209
|
+
const t = this.progress(fade);
|
|
210
|
+
this.fadeLayer.opacity = t >= 1 ? fade.to : fade.from + (fade.to - fade.from) * fade.ease(t);
|
|
211
|
+
if (t >= 1) {
|
|
212
|
+
this.fadeRun = null;
|
|
213
|
+
fade.resolve(true);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const flash = this.flashRun;
|
|
217
|
+
if (flash) {
|
|
218
|
+
const t = this.progress(flash);
|
|
219
|
+
this.flashLayer.opacity = t >= 1 ? 0 : 1 - flash.ease(t);
|
|
220
|
+
if (t >= 1) {
|
|
221
|
+
this.flashRun = null;
|
|
222
|
+
flash.resolve(true);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.draw();
|
|
226
|
+
}
|
|
227
|
+
/** Engine-internal: see layoutCameraEffects. */
|
|
228
|
+
[LAYOUT](rect) {
|
|
229
|
+
this.rect = { ...rect };
|
|
230
|
+
for (const layer of [this.fadeLayer, this.flashLayer]) {
|
|
231
|
+
if (layer.element)
|
|
232
|
+
this.place(layer.element);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Normalized progress of a timed effect after the current step: 1 at (and past) its end. */
|
|
236
|
+
progress(entry) {
|
|
237
|
+
const elapsed = (this.steps - entry.start) * SIMULATION_STEP;
|
|
238
|
+
if (elapsed >= entry.seconds - SIMULATION_TIME_EPSILON)
|
|
239
|
+
return 1;
|
|
240
|
+
return elapsed / entry.seconds;
|
|
241
|
+
}
|
|
242
|
+
advanceShakes() {
|
|
243
|
+
let amplitude = 0;
|
|
244
|
+
let seed = 0;
|
|
245
|
+
for (const shake of [...this.shakes]) {
|
|
246
|
+
const t = this.progress(shake);
|
|
247
|
+
if (t >= 1) {
|
|
248
|
+
this.shakes.splice(this.shakes.indexOf(shake), 1);
|
|
249
|
+
shake.resolve(true);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const current = shake.intensity * (1 - shake.ease(t));
|
|
253
|
+
if (current > amplitude) {
|
|
254
|
+
amplitude = current;
|
|
255
|
+
seed = shake.start;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (amplitude <= 0) {
|
|
259
|
+
this.offset = { x: 0, y: 0 };
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const pixel = this.options.pixel();
|
|
263
|
+
const axis = (index) => {
|
|
264
|
+
const raw = amplitude * (2 * jitter(seed, this.steps, index) - 1);
|
|
265
|
+
// Truncating toward zero keeps a snapped offset inside the envelope.
|
|
266
|
+
const snapped = pixel ? Math.trunc(raw / pixel) * pixel : raw;
|
|
267
|
+
return snapped + 0;
|
|
268
|
+
};
|
|
269
|
+
this.offset = { x: axis(0), y: axis(1) };
|
|
270
|
+
}
|
|
271
|
+
/** Mirrors both layers' state onto their elements, mounting them on first need. */
|
|
272
|
+
draw() {
|
|
273
|
+
const needed = this.fadeLayer.opacity > 0 || this.flashLayer.opacity > 0;
|
|
274
|
+
if (needed && !this.fadeLayer.element)
|
|
275
|
+
this.mount();
|
|
276
|
+
for (const layer of [this.fadeLayer, this.flashLayer]) {
|
|
277
|
+
const element = layer.element;
|
|
278
|
+
if (!element)
|
|
279
|
+
continue;
|
|
280
|
+
element.style.backgroundColor = layer.color;
|
|
281
|
+
element.style.opacity = String(layer.opacity);
|
|
282
|
+
element.style.display = layer.opacity > 0 ? '' : 'none';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
mount() {
|
|
286
|
+
const host = this.options.host();
|
|
287
|
+
if (getComputedStyle(host).position === 'static')
|
|
288
|
+
host.style.position = 'relative';
|
|
289
|
+
for (const [layer, name, z] of [
|
|
290
|
+
[this.fadeLayer, 'fade', FADE_Z],
|
|
291
|
+
[this.flashLayer, 'flash', FLASH_Z],
|
|
292
|
+
]) {
|
|
293
|
+
const element = document.createElement('div');
|
|
294
|
+
element.dataset.waicaCameraEffect = name;
|
|
295
|
+
element.style.position = 'absolute';
|
|
296
|
+
element.style.pointerEvents = 'none';
|
|
297
|
+
element.style.zIndex = String(z);
|
|
298
|
+
element.style.display = 'none';
|
|
299
|
+
this.place(element);
|
|
300
|
+
host.append(element);
|
|
301
|
+
layer.element = element;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** Sizes a layer to the game viewport (the whole host until the Game lays it out). */
|
|
305
|
+
place(element) {
|
|
306
|
+
const rect = this.rect;
|
|
307
|
+
if (!rect) {
|
|
308
|
+
element.style.inset = '0';
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
element.style.inset = '';
|
|
312
|
+
element.style.left = `${rect.x}px`;
|
|
313
|
+
element.style.top = `${rect.y}px`;
|
|
314
|
+
element.style.width = `${rect.width}px`;
|
|
315
|
+
element.style.height = `${rect.height}px`;
|
|
316
|
+
}
|
|
317
|
+
invalid(message) {
|
|
318
|
+
console.warn(`[waica] cameraEffects.${message}`);
|
|
319
|
+
return settledHandle(Promise.resolve(false), () => { }, () => false);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Engine-internal: advances every Camera Effect by exactly one Simulation
|
|
324
|
+
* Step. `Game.simulateStep()` calls it right after the scene camera, so
|
|
325
|
+
* nothing advances while the Game is not simulating. Not re-exported from
|
|
326
|
+
* the package entry.
|
|
327
|
+
*/
|
|
328
|
+
export function advanceCameraEffects(effects) {
|
|
329
|
+
effects[ADVANCE]();
|
|
330
|
+
}
|
|
331
|
+
/** Engine-internal: sizes the Fade and Flash layers to the game viewport (`Game.resize()`). */
|
|
332
|
+
export function layoutCameraEffects(effects, rect) {
|
|
333
|
+
effects[LAYOUT](rect);
|
|
334
|
+
}
|
package/dist/game-time.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The closed set of named Penner curves a Tween can ease through. */
|
|
2
2
|
export type EasingName = 'linear' | 'quadIn' | 'quadOut' | 'quadInOut' | 'cubicIn' | 'cubicOut' | 'cubicInOut' | 'sineInOut';
|
|
3
|
+
/** A resolved easing: normalized progress in, eased progress out. */
|
|
4
|
+
type EasingFn = (t: number) => number;
|
|
3
5
|
/** What `after`/`every`/`tween` hand back: see CA-6. */
|
|
4
6
|
export interface TimerHandle {
|
|
5
7
|
/** Idempotent and silent, even on an already-inactive handle. */
|
|
@@ -59,6 +61,8 @@ export interface TweenOptions {
|
|
|
59
61
|
/** Same as `TimerOptions.scope`: `'session'` survives a scene change, anything else means scene. */
|
|
60
62
|
scope?: 'scene' | 'session';
|
|
61
63
|
}
|
|
64
|
+
/** Engine-internal (not re-exported from the package entry): also resolves Camera Effect easings. */
|
|
65
|
+
export declare function resolveEasing(easing: EasingName | ((t: number) => number) | undefined): EasingFn | null;
|
|
62
66
|
/**
|
|
63
67
|
* Module-private key for the per-step advance pass. Not exported, so
|
|
64
68
|
* `time[ADVANCE]()` cannot be spelled outside this file — `advanceGameTime`
|
package/dist/game-time.js
CHANGED
|
@@ -15,7 +15,8 @@ const EASINGS = {
|
|
|
15
15
|
cubicInOut: (t) => (t < 0.5 ? 4 * t * t * t : 1 - ((-2 * t + 2) ** 3) / 2),
|
|
16
16
|
sineInOut: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
17
17
|
};
|
|
18
|
-
|
|
18
|
+
/** Engine-internal (not re-exported from the package entry): also resolves Camera Effect easings. */
|
|
19
|
+
export function resolveEasing(easing) {
|
|
19
20
|
if (easing === undefined)
|
|
20
21
|
return EASINGS.linear;
|
|
21
22
|
if (typeof easing === 'function')
|
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,6 +4,7 @@ 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 { advanceCameraEffects, CameraEffects, layoutCameraEffects } from './camera-effects.js';
|
|
7
8
|
import { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
8
9
|
import { Entity } from './entity.js';
|
|
9
10
|
import { Emitter } from './events.js';
|
|
@@ -42,6 +43,11 @@ export class Game {
|
|
|
42
43
|
* (ADR 0011): `unloadScene()` never touches it. See ADR 0019.
|
|
43
44
|
*/
|
|
44
45
|
assets;
|
|
46
|
+
/**
|
|
47
|
+
* Shake, Fade and Flash, beside `camera` and never written into it
|
|
48
|
+
* (ADR 0020). A Fade survives `unloadScene()`; a Shake or Flash does not.
|
|
49
|
+
*/
|
|
50
|
+
cameraEffects;
|
|
45
51
|
/** Simulated scheduling: `after`, `every`, `tween`, `now`. See ADR 0017. */
|
|
46
52
|
time = new GameTime();
|
|
47
53
|
/** Registry retained by loadScene for runtime prefab spawning. */
|
|
@@ -116,6 +122,11 @@ export class Game {
|
|
|
116
122
|
// catalog is registered at call time, which outlives every scene.
|
|
117
123
|
resolveAsset: (uri) => this.sceneCatalog?.registry.resolveAsset?.(uri) ?? uri,
|
|
118
124
|
});
|
|
125
|
+
this.cameraEffects = new CameraEffects({
|
|
126
|
+
host: () => canvas.parentElement ?? document.body,
|
|
127
|
+
// One screen pixel in world units: the shake snaps to it (never the base).
|
|
128
|
+
pixel: () => (this.resolution ? this.viewHeight / this.resolution.height : null),
|
|
129
|
+
});
|
|
119
130
|
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
|
120
131
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
121
132
|
this.scene.background = new THREE.Color(background);
|
|
@@ -173,6 +184,7 @@ export class Game {
|
|
|
173
184
|
// Scene-scoped timers/tweens die here too (ADR 0017): before entities are
|
|
174
185
|
// destroyed below, so an owner's own destroy() cancellation is a no-op.
|
|
175
186
|
this.time.cancelSceneScoped();
|
|
187
|
+
this.cameraEffects.unloadScene();
|
|
176
188
|
// An explicit unload means "no scene": a swap queued earlier this frame
|
|
177
189
|
// would otherwise flush next frame and resurrect one.
|
|
178
190
|
this.pendingSceneLoad = null;
|
|
@@ -366,6 +378,7 @@ export class Game {
|
|
|
366
378
|
this.resizeObserver.disconnect();
|
|
367
379
|
this.ui.dispose();
|
|
368
380
|
this.audio.dispose();
|
|
381
|
+
this.cameraEffects.dispose();
|
|
369
382
|
// Cancels both scopes, including session-scoped work no entity owns
|
|
370
383
|
// (entity.destroy() below only ever reaches owned work) — ADR 0017.
|
|
371
384
|
this.time.cancelAll();
|
|
@@ -478,6 +491,7 @@ export class Game {
|
|
|
478
491
|
}
|
|
479
492
|
this.dispatchCollisions();
|
|
480
493
|
this.updateSceneCamera(SIMULATION_STEP);
|
|
494
|
+
advanceCameraEffects(this.cameraEffects);
|
|
481
495
|
this.finishStep();
|
|
482
496
|
}
|
|
483
497
|
/** Closes a step (real or the non-simulating stand-in): host callbacks, then the input frame. */
|
|
@@ -535,15 +549,28 @@ export class Game {
|
|
|
535
549
|
if (this.renderSort === 'y')
|
|
536
550
|
this.applyYSort();
|
|
537
551
|
this.ui.setActive(this.simulate);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
552
|
+
// The shake offset exists only while drawing (ADR 0020): Anchored Pieces
|
|
553
|
+
// and the render see the drawn center, then the base comes back exactly,
|
|
554
|
+
// so follow smoothing never starts from its own jitter.
|
|
555
|
+
const { x, y } = this.camera.position;
|
|
556
|
+
const { shake } = this.cameraEffects.state;
|
|
557
|
+
this.camera.position.x = x + shake.x;
|
|
558
|
+
this.camera.position.y = y + shake.y;
|
|
559
|
+
try {
|
|
560
|
+
anchoredPiecesOf(this.ui).place();
|
|
561
|
+
if (this.resolution) {
|
|
562
|
+
// Letterbox bars: clear the whole canvas, then render inside the scissor.
|
|
563
|
+
this.renderer.setScissorTest(false);
|
|
564
|
+
this.renderer.setClearColor(0x000000, 1);
|
|
565
|
+
this.renderer.clear(true, false, false);
|
|
566
|
+
this.renderer.setScissorTest(true);
|
|
567
|
+
}
|
|
568
|
+
this.renderer.render(this.scene, this.camera);
|
|
569
|
+
}
|
|
570
|
+
finally {
|
|
571
|
+
this.camera.position.x = x;
|
|
572
|
+
this.camera.position.y = y;
|
|
545
573
|
}
|
|
546
|
-
this.renderer.render(this.scene, this.camera);
|
|
547
574
|
}
|
|
548
575
|
componentUpdateSchedule(entity) {
|
|
549
576
|
const components = [...entity.components];
|
|
@@ -655,5 +682,6 @@ export class Game {
|
|
|
655
682
|
this.camera.top = halfH;
|
|
656
683
|
this.camera.bottom = -halfH;
|
|
657
684
|
this.camera.updateProjectionMatrix();
|
|
685
|
+
layoutCameraEffects(this.cameraEffects, gameViewport(w, h, this.resolution));
|
|
658
686
|
}
|
|
659
687
|
}
|
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/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;
|
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;
|
|
@@ -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;
|