@waica/engine 0.20.0 → 0.22.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 +11 -0
- package/dist/game.d.ts +5 -3
- package/dist/game.js +6 -4
- package/dist/gamepad.d.ts +72 -0
- package/dist/gamepad.js +137 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/input.d.ts +61 -11
- package/dist/input.js +130 -24
- package/dist/runtime-bridge.d.ts +15 -3
- package/dist/runtime-bridge.js +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,6 +137,17 @@ A seeded Mulberry32 stream makes equivalent runs deterministic; `emit(count)` us
|
|
|
137
137
|
|
|
138
138
|
Textures use the Game's shared `AssetLoader`; `pixelArt` chooses nearest or linear filtering, and `blend` selects normal or additive blending. All three are reactive authoring props. A failed or empty texture falls back to the flat colored quad. Batching keeps one draw submission but does not promise order-independent or globally exact alpha composition between meshes. `destroyMode: 'clear'` removes the batch with its owner; `'drain'` transfers it to scene scope, stops emission, and simulates it until empty. Scene unload and `game.dispose()` always clear drains.
|
|
139
139
|
|
|
140
|
+
The three stock archetypes ship authored effects built on this component. In each case the emitter holds the look, and a small cue component from `@waica/behaviors` decides when it emits:
|
|
141
|
+
|
|
142
|
+
| Cue | What it does | Archetype |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `SwingSparks` | Bursts `count` particles when the sibling `StateMachine` enters `state`. | isometric (player sword sparks) |
|
|
145
|
+
| `DamagePuff` | Moves its entity to whatever `Health` reports on the `damage` event and bursts there. | isometric (`objects/hurt-smoke`) |
|
|
146
|
+
| `DustPuffs` | Puffs on a ground takeoff into `jump` and bursts on any grounded exit from `jump` or `fall`. | platformer (player dust) |
|
|
147
|
+
| `DustTrail` | Turns `emitting` on only while the machine is in `state`. | topdown (player walking dust) |
|
|
148
|
+
|
|
149
|
+
The cues that hook a `StateMachine` must come after it and after the `ParticleEmitter` in the component list. The isometric demo also places plain emitter objects: `objects/wind` in the meadow and `objects/cave-dust` in the cave.
|
|
150
|
+
|
|
140
151
|
`active`, `capacity`, and `emitting` are the only fields exposed by Runtime Snapshot. Particle arrays remain private. This first CPU implementation is intended for bounded 2D effects, not collision, per-particle scripting, GPU simulation, trails, rotation, or sub-emitters.
|
|
141
152
|
|
|
142
153
|
## Component lifecycle
|
package/dist/game.d.ts
CHANGED
|
@@ -29,8 +29,10 @@ export interface GameOptions {
|
|
|
29
29
|
viewHeight?: number;
|
|
30
30
|
/** Fixed resolution (from the project's game.json); absent = fill the canvas. */
|
|
31
31
|
resolution?: GameResolution;
|
|
32
|
-
/** Control overrides (action → key codes) on top of the defaults. */
|
|
32
|
+
/** Control overrides (action → key and `Gamepad:` codes) on top of the defaults. */
|
|
33
33
|
bindings?: InputBindings;
|
|
34
|
+
/** Radial stick dead zone of `game.input`, in [0, 1); absent or invalid means 0.2 (ADR 0023). */
|
|
35
|
+
gamepadDeadZone?: number;
|
|
34
36
|
/** Initial stat values (points, lives…) from the project's stats.json. */
|
|
35
37
|
stats?: Record<string, StatValue>;
|
|
36
38
|
/**
|
|
@@ -233,8 +235,8 @@ export declare class Game {
|
|
|
233
235
|
*/
|
|
234
236
|
private runFrame;
|
|
235
237
|
/**
|
|
236
|
-
* One Simulation Step:
|
|
237
|
-
*
|
|
238
|
+
* One Simulation Step: the pad is read first (issue #75 CA-6), then
|
|
239
|
+
* game.time's start-of-step pass (ADR 0017, CA-3), then the Component Update Schedule (ADR 0004) in full,
|
|
238
240
|
* collisions, the scene camera and the host's callbacks, every one of
|
|
239
241
|
* them handed exactly SIMULATION_STEP (CA-1); then the input frame ends.
|
|
240
242
|
*/
|
package/dist/game.js
CHANGED
|
@@ -109,7 +109,7 @@ export class Game {
|
|
|
109
109
|
this.baseViewHeight = viewHeight;
|
|
110
110
|
this.viewHeight = viewHeight;
|
|
111
111
|
this.resolution = options.resolution ?? null;
|
|
112
|
-
this.input = new Input(options.bindings);
|
|
112
|
+
this.input = new Input(options.bindings, { gamepadDeadZone: options.gamepadDeadZone });
|
|
113
113
|
this.query = createSpatialQuery(this);
|
|
114
114
|
this.stats = new Stats(options.stats);
|
|
115
115
|
this.ui = new GameUi(this.stats, () => canvas.parentElement ?? document.body);
|
|
@@ -339,9 +339,10 @@ export class Game {
|
|
|
339
339
|
step: (onStep) => this.runFrame(1, onStep),
|
|
340
340
|
resume: (onStep) => this.resumeRuntime(onStep),
|
|
341
341
|
pause: () => this.stop(),
|
|
342
|
-
injectAction: (action, operation) => this.input.injectAction(action, operation),
|
|
342
|
+
injectAction: (action, operation, value) => this.input.injectAction(action, operation, value),
|
|
343
343
|
availableActions: () => this.input.availableActions(),
|
|
344
344
|
heldActions: () => this.input.heldActions(),
|
|
345
|
+
actionValues: () => this.input.actionValues(),
|
|
345
346
|
inspect: (metadata, filters) => inspector.snapshot(metadata, filters),
|
|
346
347
|
click: (x, y) => {
|
|
347
348
|
this.pointer.injectClick(x, y);
|
|
@@ -489,12 +490,13 @@ export class Game {
|
|
|
489
490
|
}
|
|
490
491
|
}
|
|
491
492
|
/**
|
|
492
|
-
* One Simulation Step:
|
|
493
|
-
*
|
|
493
|
+
* One Simulation Step: the pad is read first (issue #75 CA-6), then
|
|
494
|
+
* game.time's start-of-step pass (ADR 0017, CA-3), then the Component Update Schedule (ADR 0004) in full,
|
|
494
495
|
* collisions, the scene camera and the host's callbacks, every one of
|
|
495
496
|
* them handed exactly SIMULATION_STEP (CA-1); then the input frame ends.
|
|
496
497
|
*/
|
|
497
498
|
simulateStep() {
|
|
499
|
+
this.input.pollGamepad();
|
|
498
500
|
advanceGameTime(this.time);
|
|
499
501
|
// Existing drains advance before entity updates, so one detached during
|
|
500
502
|
// this step begins on the next Simulation Step (ADR 0022).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamepad sources for Actions (ADR 0023): the `Gamepad:<control>` binding
|
|
3
|
+
* vocabulary over the W3C `standard` mapping, the radial dead zone, and the
|
|
4
|
+
* player-1 pad slot. `Input` consumes this module; behaviors never see a pad.
|
|
5
|
+
*/
|
|
6
|
+
/** The slice of the W3C `Gamepad` object Waica reads, so tests can hand in plain objects. */
|
|
7
|
+
export interface GamepadLike {
|
|
8
|
+
readonly index: number;
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly mapping: string;
|
|
11
|
+
readonly connected: boolean;
|
|
12
|
+
readonly axes: readonly number[];
|
|
13
|
+
readonly buttons: readonly {
|
|
14
|
+
readonly value: number;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
/** One pad control a binding code names: a button by index, or one half of a stick axis. */
|
|
18
|
+
export type GamepadControl = {
|
|
19
|
+
kind: 'button';
|
|
20
|
+
index: number;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'stick';
|
|
23
|
+
axis: number;
|
|
24
|
+
sign: 1 | -1;
|
|
25
|
+
};
|
|
26
|
+
export declare const GAMEPAD_CODE_PREFIX = "Gamepad:";
|
|
27
|
+
/** An Action's source counts as held at this value or above (keys are always 1 while down). */
|
|
28
|
+
export declare const ACTION_HELD_THRESHOLD = 0.5;
|
|
29
|
+
/** Radial dead zone used when `GameOptions.gamepadDeadZone` is absent or invalid. */
|
|
30
|
+
export declare const DEFAULT_GAMEPAD_DEAD_ZONE = 0.2;
|
|
31
|
+
/** Every known `Gamepad:` code: buttons in index order, then the stick halves. */
|
|
32
|
+
export declare const GAMEPAD_CODES: readonly string[];
|
|
33
|
+
/** The control a binding code names; undefined for a keyboard code or an unknown pad name. */
|
|
34
|
+
export declare function gamepadControl(code: string): GamepadControl | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Radial dead zone: a stick shorter than `deadZone` reads as centered on
|
|
37
|
+
* both axes; past it the direction is kept and the magnitude is rescaled
|
|
38
|
+
* to `(m − d) / (1 − d)`, clamped to 1.
|
|
39
|
+
*/
|
|
40
|
+
export declare function radialDeadZone(x: number, y: number, deadZone: number): {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
};
|
|
44
|
+
/** The configured dead zone if it is a finite number in [0, 1); otherwise warns once and uses the default. */
|
|
45
|
+
export declare function resolveGamepadDeadZone(value: number | undefined, warn: (message: string) => void): number;
|
|
46
|
+
/** Each known code's 0..1 value on this pad: buttons by `value`, stick halves dead-zoned per stick. */
|
|
47
|
+
export declare function gamepadValues(pad: GamepadLike, deadZone: number): Map<string, number>;
|
|
48
|
+
/** The browser's pads, or none where the Gamepad API is missing or refused (Node, happy-dom, a policy-blocked frame). */
|
|
49
|
+
export declare function connectedGamepads(): readonly (GamepadLike | null)[];
|
|
50
|
+
/** One poll of the player-1 slot: its pad, and whether the poll lost the pad that was active before it. */
|
|
51
|
+
export interface GamepadSlotPoll {
|
|
52
|
+
pad: GamepadLike | null;
|
|
53
|
+
lostActive: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* @internal Editor support (the controls panel's pad capture), not a stable
|
|
57
|
+
* API: each known code's value on the first connected standard pad at the
|
|
58
|
+
* default dead zone, or null when no such pad is present.
|
|
59
|
+
*/
|
|
60
|
+
export declare function firstStandardPadValues(): Map<string, number> | null;
|
|
61
|
+
/**
|
|
62
|
+
* Player 1's pad: the first `standard` pad to connect, sticky until it
|
|
63
|
+
* disconnects; then the next one in connection order takes over. Order is
|
|
64
|
+
* the order polls first saw each pad (by index on the same poll).
|
|
65
|
+
*/
|
|
66
|
+
export declare class GamepadSlot {
|
|
67
|
+
private order;
|
|
68
|
+
private activeKey;
|
|
69
|
+
poll(pads: readonly (GamepadLike | null)[]): GamepadSlotPoll;
|
|
70
|
+
}
|
|
71
|
+
/** A connected pad with the W3C `standard` mapping; any other pad is ignored entirely. */
|
|
72
|
+
export declare function isStandardPad(pad: GamepadLike | null | undefined): pad is GamepadLike;
|
package/dist/gamepad.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gamepad sources for Actions (ADR 0023): the `Gamepad:<control>` binding
|
|
3
|
+
* vocabulary over the W3C `standard` mapping, the radial dead zone, and the
|
|
4
|
+
* player-1 pad slot. `Input` consumes this module; behaviors never see a pad.
|
|
5
|
+
*/
|
|
6
|
+
export const GAMEPAD_CODE_PREFIX = 'Gamepad:';
|
|
7
|
+
/** An Action's source counts as held at this value or above (keys are always 1 while down). */
|
|
8
|
+
export const ACTION_HELD_THRESHOLD = 0.5;
|
|
9
|
+
/** Radial dead zone used when `GameOptions.gamepadDeadZone` is absent or invalid. */
|
|
10
|
+
export const DEFAULT_GAMEPAD_DEAD_ZONE = 0.2;
|
|
11
|
+
/** Standard-mapping buttons in W3C index order (0–16). */
|
|
12
|
+
const BUTTON_NAMES = [
|
|
13
|
+
'A', 'B', 'X', 'Y', 'LB', 'RB', 'LT', 'RT', 'Back', 'Start', 'LeftStick', 'RightStick',
|
|
14
|
+
'DPadUp', 'DPadDown', 'DPadLeft', 'DPadRight', 'Home',
|
|
15
|
+
];
|
|
16
|
+
/** Stick halves: axes 0/1 are the left stick, 2/3 the right; "Up" is the negative Y axis. */
|
|
17
|
+
const STICK_HALVES = {
|
|
18
|
+
LeftStickLeft: { axis: 0, sign: -1 },
|
|
19
|
+
LeftStickRight: { axis: 0, sign: 1 },
|
|
20
|
+
LeftStickUp: { axis: 1, sign: -1 },
|
|
21
|
+
LeftStickDown: { axis: 1, sign: 1 },
|
|
22
|
+
RightStickLeft: { axis: 2, sign: -1 },
|
|
23
|
+
RightStickRight: { axis: 2, sign: 1 },
|
|
24
|
+
RightStickUp: { axis: 3, sign: -1 },
|
|
25
|
+
RightStickDown: { axis: 3, sign: 1 },
|
|
26
|
+
};
|
|
27
|
+
const CONTROLS = new Map([
|
|
28
|
+
...BUTTON_NAMES.map((name, index) => [
|
|
29
|
+
`${GAMEPAD_CODE_PREFIX}${name}`,
|
|
30
|
+
{ kind: 'button', index },
|
|
31
|
+
]),
|
|
32
|
+
...Object.entries(STICK_HALVES).map(([name, half]) => [
|
|
33
|
+
`${GAMEPAD_CODE_PREFIX}${name}`,
|
|
34
|
+
{ kind: 'stick', ...half },
|
|
35
|
+
]),
|
|
36
|
+
]);
|
|
37
|
+
/** Every known `Gamepad:` code: buttons in index order, then the stick halves. */
|
|
38
|
+
export const GAMEPAD_CODES = [...CONTROLS.keys()];
|
|
39
|
+
/** The control a binding code names; undefined for a keyboard code or an unknown pad name. */
|
|
40
|
+
export function gamepadControl(code) {
|
|
41
|
+
const control = CONTROLS.get(code);
|
|
42
|
+
return control ? { ...control } : undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Radial dead zone: a stick shorter than `deadZone` reads as centered on
|
|
46
|
+
* both axes; past it the direction is kept and the magnitude is rescaled
|
|
47
|
+
* to `(m − d) / (1 − d)`, clamped to 1.
|
|
48
|
+
*/
|
|
49
|
+
export function radialDeadZone(x, y, deadZone) {
|
|
50
|
+
const magnitude = Math.hypot(x, y);
|
|
51
|
+
if (magnitude === 0 || magnitude < deadZone)
|
|
52
|
+
return { x: 0, y: 0 };
|
|
53
|
+
const scaled = Math.min(1, (magnitude - deadZone) / (1 - deadZone));
|
|
54
|
+
return { x: (x / magnitude) * scaled, y: (y / magnitude) * scaled };
|
|
55
|
+
}
|
|
56
|
+
/** The configured dead zone if it is a finite number in [0, 1); otherwise warns once and uses the default. */
|
|
57
|
+
export function resolveGamepadDeadZone(value, warn) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return DEFAULT_GAMEPAD_DEAD_ZONE;
|
|
60
|
+
if (Number.isFinite(value) && value >= 0 && value < 1)
|
|
61
|
+
return value;
|
|
62
|
+
warn(`[waica] gamepadDeadZone must be a finite number from 0 up to (not including) 1; ` +
|
|
63
|
+
`got ${String(value)}, using ${DEFAULT_GAMEPAD_DEAD_ZONE}.`);
|
|
64
|
+
return DEFAULT_GAMEPAD_DEAD_ZONE;
|
|
65
|
+
}
|
|
66
|
+
/** Each known code's 0..1 value on this pad: buttons by `value`, stick halves dead-zoned per stick. */
|
|
67
|
+
export function gamepadValues(pad, deadZone) {
|
|
68
|
+
const axis = (index) => pad.axes[index] ?? 0;
|
|
69
|
+
const sticks = [
|
|
70
|
+
radialDeadZone(axis(0), axis(1), deadZone),
|
|
71
|
+
radialDeadZone(axis(2), axis(3), deadZone),
|
|
72
|
+
];
|
|
73
|
+
const values = new Map();
|
|
74
|
+
for (const [code, control] of CONTROLS) {
|
|
75
|
+
if (control.kind === 'button') {
|
|
76
|
+
values.set(code, pad.buttons[control.index]?.value ?? 0);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const stick = sticks[control.axis >> 1] ?? { x: 0, y: 0 };
|
|
80
|
+
const component = control.axis % 2 === 0 ? stick.x : stick.y;
|
|
81
|
+
values.set(code, Math.max(0, component * control.sign));
|
|
82
|
+
}
|
|
83
|
+
return values;
|
|
84
|
+
}
|
|
85
|
+
/** The browser's pads, or none where the Gamepad API is missing or refused (Node, happy-dom, a policy-blocked frame). */
|
|
86
|
+
export function connectedGamepads() {
|
|
87
|
+
if (typeof navigator === 'undefined' || typeof navigator.getGamepads !== 'function')
|
|
88
|
+
return [];
|
|
89
|
+
try {
|
|
90
|
+
return navigator.getGamepads();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// A permissions policy can make getGamepads throw; input stays keyboard-only.
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* @internal Editor support (the controls panel's pad capture), not a stable
|
|
99
|
+
* API: each known code's value on the first connected standard pad at the
|
|
100
|
+
* default dead zone, or null when no such pad is present.
|
|
101
|
+
*/
|
|
102
|
+
export function firstStandardPadValues() {
|
|
103
|
+
const pad = connectedGamepads().find(isStandardPad);
|
|
104
|
+
return pad ? gamepadValues(pad, DEFAULT_GAMEPAD_DEAD_ZONE) : null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Player 1's pad: the first `standard` pad to connect, sticky until it
|
|
108
|
+
* disconnects; then the next one in connection order takes over. Order is
|
|
109
|
+
* the order polls first saw each pad (by index on the same poll).
|
|
110
|
+
*/
|
|
111
|
+
export class GamepadSlot {
|
|
112
|
+
order = [];
|
|
113
|
+
activeKey = null;
|
|
114
|
+
poll(pads) {
|
|
115
|
+
const present = new Map();
|
|
116
|
+
for (const pad of pads)
|
|
117
|
+
if (isStandardPad(pad))
|
|
118
|
+
present.set(slotKey(pad), pad);
|
|
119
|
+
this.order = this.order.filter((key) => present.has(key));
|
|
120
|
+
for (const key of present.keys())
|
|
121
|
+
if (!this.order.includes(key))
|
|
122
|
+
this.order.push(key);
|
|
123
|
+
const previous = this.activeKey;
|
|
124
|
+
this.activeKey = this.order[0] ?? null;
|
|
125
|
+
return {
|
|
126
|
+
pad: this.activeKey === null ? null : (present.get(this.activeKey) ?? null),
|
|
127
|
+
lostActive: previous !== null && previous !== this.activeKey,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** A connected pad with the W3C `standard` mapping; any other pad is ignored entirely. */
|
|
132
|
+
export function isStandardPad(pad) {
|
|
133
|
+
return pad?.connected === true && pad.mapping === 'standard';
|
|
134
|
+
}
|
|
135
|
+
function slotKey(pad) {
|
|
136
|
+
return `${pad.index}|${pad.id}`;
|
|
137
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,9 @@ export type { ComponentModule } from './component-registry.js';
|
|
|
31
31
|
export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
32
32
|
export type { ComponentUpdateCycleIssue, ComponentUpdateRegistry, ComponentUpdateScheduleIssue, ComponentUpdateScheduleResult, DuplicateComponentUpdateIssue, InvalidComponentUpdateConstraintIssue, InvalidComponentUpdateSchedule, InvalidUpdateConstraintReason, ValidComponentUpdateSchedule, } from './component-update-schedule.js';
|
|
33
33
|
export { Input, DEFAULT_BINDINGS } from './input.js';
|
|
34
|
-
export type { ActionName, InjectedActionOperation, InputBindings } from './input.js';
|
|
34
|
+
export type { ActionName, InjectedActionOperation, InputBindings, InputOptions } from './input.js';
|
|
35
|
+
export { ACTION_HELD_THRESHOLD, firstStandardPadValues, GAMEPAD_CODE_PREFIX, gamepadControl, } from './gamepad.js';
|
|
36
|
+
export type { GamepadControl } from './gamepad.js';
|
|
35
37
|
export { Pointer } from './pointer.js';
|
|
36
38
|
export type { PointerCamera, PointerDeps, PointerPick, PointerResolution } from './pointer.js';
|
|
37
39
|
export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { authoringDefaults } from './authoring-defaults.js';
|
|
|
15
15
|
export { collectModuleComponents, mergeRegistryComponents } from './component-registry.js';
|
|
16
16
|
export { resolveComponentUpdateSchedule } from './component-update-schedule.js';
|
|
17
17
|
export { Input, DEFAULT_BINDINGS } from './input.js';
|
|
18
|
+
export { ACTION_HELD_THRESHOLD, firstStandardPadValues, GAMEPAD_CODE_PREFIX, gamepadControl, } from './gamepad.js';
|
|
18
19
|
export { Pointer } from './pointer.js';
|
|
19
20
|
export { RUNTIME_BRIDGE_CAPABILITIES, RUNTIME_BRIDGE_PROTOCOL_VERSION, RUNTIME_BRIDGE_SYMBOL, RuntimeBridgeOperationError, } from './runtime-bridge.js';
|
|
20
21
|
export { RUNTIME_PROJECTION_LIMITS } from './runtime-inspection.js';
|
package/dist/input.d.ts
CHANGED
|
@@ -1,41 +1,89 @@
|
|
|
1
1
|
export type ActionName = string;
|
|
2
2
|
export type InjectedActionOperation = 'press' | 'hold' | 'release';
|
|
3
|
-
/** Action → KeyboardEvent.code
|
|
3
|
+
/** Action → source codes: `KeyboardEvent.code` values and `Gamepad:<control>` codes (ADR 0023). */
|
|
4
4
|
export type InputBindings = Record<string, string[]>;
|
|
5
5
|
/** Neutral engine baseline; archetypes own their action vocabulary. */
|
|
6
6
|
export declare const DEFAULT_BINDINGS: Readonly<InputBindings>;
|
|
7
|
+
/** An injection's checked `value`, or why it is rejected. */
|
|
8
|
+
export type InjectedValueCheck = {
|
|
9
|
+
value: number | undefined;
|
|
10
|
+
} | {
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
7
13
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
14
|
+
* The injection value rule Input and the Runtime Bridge share: only `hold`
|
|
15
|
+
* takes a value, a finite number in (0, 1]; an absent value always passes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkInjectedValue(operation: InjectedActionOperation, value: unknown): InjectedValueCheck;
|
|
18
|
+
export interface InputOptions {
|
|
19
|
+
/** Radial stick dead zone in [0, 1); absent or invalid means 0.2. */
|
|
20
|
+
gamepadDeadZone?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Action-based input with archetype default bindings. Sources are keys and
|
|
24
|
+
* player 1's standard gamepad (ADR 0023); every Action has a 0..1 value, the
|
|
25
|
+
* max across its sources, and is held at 0.5 or above. Touch is still TODO.
|
|
10
26
|
*/
|
|
11
27
|
export declare class Input {
|
|
12
28
|
private readonly bindings;
|
|
13
29
|
private readonly down;
|
|
14
30
|
private readonly justDown;
|
|
15
|
-
private readonly
|
|
31
|
+
private readonly injectedHolds;
|
|
16
32
|
private readonly injectedJustDown;
|
|
17
33
|
private readonly injectedPresses;
|
|
18
34
|
private readonly used;
|
|
35
|
+
private readonly deadZone;
|
|
36
|
+
private readonly padSlot;
|
|
37
|
+
/** Live pad values by code, after the last poll; disarmed codes read 0. */
|
|
38
|
+
private padValues;
|
|
39
|
+
/** Pad codes released while held: they count again only after dropping below the threshold. */
|
|
40
|
+
private readonly disarmed;
|
|
41
|
+
/** Actions held when the last step closed: a press is a step that starts from none of them (CA-5). */
|
|
42
|
+
private heldAtStepEnd;
|
|
19
43
|
/** Installs exactly the action map supplied by the active archetype/project. */
|
|
20
|
-
constructor(bindings?: Readonly<InputBindings
|
|
21
|
-
/** Is the action held this frame? */
|
|
44
|
+
constructor(bindings?: Readonly<InputBindings>, options?: InputOptions);
|
|
45
|
+
/** Is the action held this frame (some source at 0.5 or above)? */
|
|
22
46
|
held(action: ActionName): boolean;
|
|
23
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Was the action pressed exactly this frame? True when no source held it
|
|
49
|
+
* as the last step closed and some source holds it now — or a key or an
|
|
50
|
+
* injection held it since, even if already released (a tap between steps).
|
|
51
|
+
*/
|
|
24
52
|
justPressed(action: ActionName): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The action's 0..1 value: the max across its sources — 1 for a key that
|
|
55
|
+
* is down, a pad button's value, a stick half's dead-zoned component, an
|
|
56
|
+
* injected hold's value. 0 for an unknown or unbound action.
|
|
57
|
+
*/
|
|
58
|
+
value(action: ActionName): number;
|
|
25
59
|
/** Installed semantic action names in deterministic order. */
|
|
26
60
|
availableActions(): ActionName[];
|
|
27
61
|
/**
|
|
28
|
-
* The
|
|
62
|
+
* The source codes bound to the action, in their declared order (e.g.
|
|
29
63
|
* `['KeyE', 'Space']`); `[]` for an unknown or unbound action. A new
|
|
30
64
|
* array every call: mutating it never changes the bindings.
|
|
31
65
|
*/
|
|
32
66
|
bindingsFor(action: ActionName): string[];
|
|
33
67
|
/** Currently held semantic action names in deterministic order. */
|
|
34
68
|
heldActions(): ActionName[];
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
69
|
+
/** The value of every held action, keyed in the same order as heldActions(). */
|
|
70
|
+
actionValues(): Record<ActionName, number>;
|
|
71
|
+
/**
|
|
72
|
+
* Injects an action by semantic name; false means the action is not
|
|
73
|
+
* installed. `hold` takes an optional value in (0, 1] (default 1) and is
|
|
74
|
+
* one more source of the action: below 0.5 it moves it without holding it.
|
|
75
|
+
* A value outside (0, 1], or any value on `press`/`release`, throws a
|
|
76
|
+
* RangeError before anything changes (checkInjectedValue, the Runtime
|
|
77
|
+
* Bridge's rule).
|
|
78
|
+
*/
|
|
79
|
+
injectAction(action: ActionName, operation: InjectedActionOperation, value?: number): boolean;
|
|
80
|
+
/** -1..1 axis from two actions (left/right by default): value(positive) − value(negative). */
|
|
38
81
|
axis(negative?: ActionName, positive?: ActionName): number;
|
|
82
|
+
/**
|
|
83
|
+
* Reads player 1's pad. The Game calls this once per Simulation Step,
|
|
84
|
+
* before component updates.
|
|
85
|
+
*/
|
|
86
|
+
pollGamepad(): void;
|
|
39
87
|
/**
|
|
40
88
|
* Marks this frame's press of the action as spent, so consumers that
|
|
41
89
|
* honor consumption (state-machine 'input:' triggers) ignore it. One
|
|
@@ -49,6 +97,8 @@ export declare class Input {
|
|
|
49
97
|
endFrame(): void;
|
|
50
98
|
dispose(): void;
|
|
51
99
|
private isActive;
|
|
100
|
+
/** Every pad source reads 0; the ones that were held stay released until they drop below the threshold. */
|
|
101
|
+
private releasePad;
|
|
52
102
|
private releaseAll;
|
|
53
103
|
private onVisibilityChange;
|
|
54
104
|
private onKeyDown;
|
package/dist/input.js
CHANGED
|
@@ -1,41 +1,96 @@
|
|
|
1
|
+
import { ACTION_HELD_THRESHOLD, connectedGamepads, GAMEPAD_CODE_PREFIX, gamepadControl, gamepadValues, GamepadSlot, resolveGamepadDeadZone, } from './gamepad.js';
|
|
1
2
|
/** Neutral engine baseline; archetypes own their action vocabulary. */
|
|
2
3
|
export const DEFAULT_BINDINGS = {};
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
5
|
+
* The injection value rule Input and the Runtime Bridge share: only `hold`
|
|
6
|
+
* takes a value, a finite number in (0, 1]; an absent value always passes.
|
|
7
|
+
*/
|
|
8
|
+
export function checkInjectedValue(operation, value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return { value };
|
|
11
|
+
if (operation !== 'hold')
|
|
12
|
+
return { error: `${operation} does not accept a value; only hold does.` };
|
|
13
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) {
|
|
14
|
+
return { error: 'value must be a finite number greater than 0 and at most 1.' };
|
|
15
|
+
}
|
|
16
|
+
return { value };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Action-based input with archetype default bindings. Sources are keys and
|
|
20
|
+
* player 1's standard gamepad (ADR 0023); every Action has a 0..1 value, the
|
|
21
|
+
* max across its sources, and is held at 0.5 or above. Touch is still TODO.
|
|
6
22
|
*/
|
|
7
23
|
export class Input {
|
|
8
24
|
bindings = new Map();
|
|
9
25
|
down = new Set();
|
|
10
26
|
justDown = new Set();
|
|
11
|
-
|
|
27
|
+
injectedHolds = new Map();
|
|
12
28
|
injectedJustDown = new Set();
|
|
13
29
|
injectedPresses = new Set();
|
|
14
30
|
used = new Set();
|
|
31
|
+
deadZone;
|
|
32
|
+
padSlot = new GamepadSlot();
|
|
33
|
+
/** Live pad values by code, after the last poll; disarmed codes read 0. */
|
|
34
|
+
padValues = new Map();
|
|
35
|
+
/** Pad codes released while held: they count again only after dropping below the threshold. */
|
|
36
|
+
disarmed = new Set();
|
|
37
|
+
/** Actions held when the last step closed: a press is a step that starts from none of them (CA-5). */
|
|
38
|
+
heldAtStepEnd = new Set();
|
|
15
39
|
/** Installs exactly the action map supplied by the active archetype/project. */
|
|
16
|
-
constructor(bindings = DEFAULT_BINDINGS) {
|
|
40
|
+
constructor(bindings = DEFAULT_BINDINGS, options = {}) {
|
|
41
|
+
const warned = new Set();
|
|
17
42
|
for (const [action, codes] of Object.entries(bindings)) {
|
|
18
43
|
this.bindings.set(action, new Set(codes));
|
|
44
|
+
for (const code of codes)
|
|
45
|
+
warnUnknownPadCode(code, warned);
|
|
19
46
|
}
|
|
47
|
+
this.deadZone = resolveGamepadDeadZone(options.gamepadDeadZone, (message) => {
|
|
48
|
+
console.warn(message);
|
|
49
|
+
});
|
|
20
50
|
window.addEventListener('keydown', this.onKeyDown);
|
|
21
51
|
window.addEventListener('keyup', this.onKeyUp);
|
|
22
52
|
window.addEventListener('blur', this.releaseAll);
|
|
23
53
|
document.addEventListener('visibilitychange', this.onVisibilityChange);
|
|
24
54
|
}
|
|
25
|
-
/** Is the action held this frame? */
|
|
55
|
+
/** Is the action held this frame (some source at 0.5 or above)? */
|
|
26
56
|
held(action) {
|
|
27
|
-
return this.
|
|
57
|
+
return this.value(action) >= ACTION_HELD_THRESHOLD;
|
|
28
58
|
}
|
|
29
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Was the action pressed exactly this frame? True when no source held it
|
|
61
|
+
* as the last step closed and some source holds it now — or a key or an
|
|
62
|
+
* injection held it since, even if already released (a tap between steps).
|
|
63
|
+
*/
|
|
30
64
|
justPressed(action) {
|
|
31
|
-
|
|
65
|
+
if (this.heldAtStepEnd.has(action))
|
|
66
|
+
return false;
|
|
67
|
+
return (this.held(action) ||
|
|
68
|
+
this.injectedJustDown.has(action) ||
|
|
69
|
+
this.isActive(action, this.justDown));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The action's 0..1 value: the max across its sources — 1 for a key that
|
|
73
|
+
* is down, a pad button's value, a stick half's dead-zoned component, an
|
|
74
|
+
* injected hold's value. 0 for an unknown or unbound action.
|
|
75
|
+
*/
|
|
76
|
+
value(action) {
|
|
77
|
+
const codes = this.bindings.get(action);
|
|
78
|
+
if (!codes)
|
|
79
|
+
return 0;
|
|
80
|
+
let value = this.injectedPresses.has(action) ? 1 : (this.injectedHolds.get(action) ?? 0);
|
|
81
|
+
for (const code of codes) {
|
|
82
|
+
if (this.down.has(code))
|
|
83
|
+
return 1;
|
|
84
|
+
value = Math.max(value, this.padValues.get(code) ?? 0);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
32
87
|
}
|
|
33
88
|
/** Installed semantic action names in deterministic order. */
|
|
34
89
|
availableActions() {
|
|
35
90
|
return [...this.bindings.keys()].sort();
|
|
36
91
|
}
|
|
37
92
|
/**
|
|
38
|
-
* The
|
|
93
|
+
* The source codes bound to the action, in their declared order (e.g.
|
|
39
94
|
* `['KeyE', 'Space']`); `[]` for an unknown or unbound action. A new
|
|
40
95
|
* array every call: mutating it never changes the bindings.
|
|
41
96
|
*/
|
|
@@ -46,29 +101,65 @@ export class Input {
|
|
|
46
101
|
heldActions() {
|
|
47
102
|
return this.availableActions().filter((action) => this.held(action));
|
|
48
103
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
104
|
+
/** The value of every held action, keyed in the same order as heldActions(). */
|
|
105
|
+
actionValues() {
|
|
106
|
+
return Object.fromEntries(this.heldActions().map((action) => [action, this.value(action)]));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Injects an action by semantic name; false means the action is not
|
|
110
|
+
* installed. `hold` takes an optional value in (0, 1] (default 1) and is
|
|
111
|
+
* one more source of the action: below 0.5 it moves it without holding it.
|
|
112
|
+
* A value outside (0, 1], or any value on `press`/`release`, throws a
|
|
113
|
+
* RangeError before anything changes (checkInjectedValue, the Runtime
|
|
114
|
+
* Bridge's rule).
|
|
115
|
+
*/
|
|
116
|
+
injectAction(action, operation, value) {
|
|
117
|
+
const checked = checkInjectedValue(operation, value);
|
|
118
|
+
if ('error' in checked)
|
|
119
|
+
throw new RangeError(checked.error);
|
|
51
120
|
if (!this.bindings.has(action))
|
|
52
121
|
return false;
|
|
53
122
|
if (operation === 'release') {
|
|
54
|
-
this.
|
|
123
|
+
this.injectedHolds.delete(action);
|
|
55
124
|
this.injectedPresses.delete(action);
|
|
56
125
|
return true;
|
|
57
126
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
127
|
+
const wasHeld = this.held(action);
|
|
128
|
+
if (operation === 'hold') {
|
|
129
|
+
// A queued press becomes this persistent hold rather than a second edge.
|
|
130
|
+
this.injectedPresses.delete(action);
|
|
131
|
+
this.injectedHolds.set(action, checked.value ?? 1);
|
|
62
132
|
}
|
|
63
|
-
|
|
64
|
-
this.injectedJustDown.add(action);
|
|
65
|
-
if (operation === 'press')
|
|
133
|
+
else if (!wasHeld) {
|
|
66
134
|
this.injectedPresses.add(action);
|
|
135
|
+
}
|
|
136
|
+
if (!wasHeld && this.held(action))
|
|
137
|
+
this.injectedJustDown.add(action);
|
|
67
138
|
return true;
|
|
68
139
|
}
|
|
69
|
-
/** -1..1 axis from two actions (left/right by default). */
|
|
140
|
+
/** -1..1 axis from two actions (left/right by default): value(positive) − value(negative). */
|
|
70
141
|
axis(negative = 'left', positive = 'right') {
|
|
71
|
-
return
|
|
142
|
+
return this.value(positive) - this.value(negative);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Reads player 1's pad. The Game calls this once per Simulation Step,
|
|
146
|
+
* before component updates.
|
|
147
|
+
*/
|
|
148
|
+
pollGamepad() {
|
|
149
|
+
const { pad, lostActive } = this.padSlot.poll(connectedGamepads());
|
|
150
|
+
// No pad, none lost and no pad values: nothing can change (the common keyboard-only case).
|
|
151
|
+
if (!pad && !lostActive && this.padValues.size === 0)
|
|
152
|
+
return;
|
|
153
|
+
if (lostActive)
|
|
154
|
+
this.releasePad();
|
|
155
|
+
const raw = pad ? gamepadValues(pad, this.deadZone) : new Map();
|
|
156
|
+
for (const [code, value] of raw) {
|
|
157
|
+
if (value < ACTION_HELD_THRESHOLD)
|
|
158
|
+
this.disarmed.delete(code);
|
|
159
|
+
else if (this.disarmed.has(code))
|
|
160
|
+
raw.set(code, 0);
|
|
161
|
+
}
|
|
162
|
+
this.padValues = raw;
|
|
72
163
|
}
|
|
73
164
|
/**
|
|
74
165
|
* Marks this frame's press of the action as spent, so consumers that
|
|
@@ -87,13 +178,12 @@ export class Input {
|
|
|
87
178
|
endFrame() {
|
|
88
179
|
this.justDown.clear();
|
|
89
180
|
this.injectedJustDown.clear();
|
|
90
|
-
for (const action of this.injectedPresses)
|
|
91
|
-
this.injectedDown.delete(action);
|
|
92
181
|
this.injectedPresses.clear();
|
|
93
182
|
this.used.clear();
|
|
183
|
+
this.heldAtStepEnd = new Set(this.heldActions());
|
|
94
184
|
}
|
|
95
185
|
dispose() {
|
|
96
|
-
this.
|
|
186
|
+
this.injectedHolds.clear();
|
|
97
187
|
this.injectedJustDown.clear();
|
|
98
188
|
this.injectedPresses.clear();
|
|
99
189
|
window.removeEventListener('keydown', this.onKeyDown);
|
|
@@ -110,9 +200,18 @@ export class Input {
|
|
|
110
200
|
return true;
|
|
111
201
|
return false;
|
|
112
202
|
}
|
|
203
|
+
/** Every pad source reads 0; the ones that were held stay released until they drop below the threshold. */
|
|
204
|
+
releasePad() {
|
|
205
|
+
for (const [code, value] of this.padValues) {
|
|
206
|
+
if (value >= ACTION_HELD_THRESHOLD)
|
|
207
|
+
this.disarmed.add(code);
|
|
208
|
+
}
|
|
209
|
+
this.padValues = new Map();
|
|
210
|
+
}
|
|
113
211
|
releaseAll = () => {
|
|
114
212
|
this.down.clear();
|
|
115
213
|
this.justDown.clear();
|
|
214
|
+
this.releasePad();
|
|
116
215
|
};
|
|
117
216
|
onVisibilityChange = () => {
|
|
118
217
|
if (document.visibilityState === 'hidden')
|
|
@@ -128,3 +227,10 @@ export class Input {
|
|
|
128
227
|
this.down.delete(e.code);
|
|
129
228
|
};
|
|
130
229
|
}
|
|
230
|
+
/** An unknown `Gamepad:` code never matches; it is reported once per code per Input. */
|
|
231
|
+
function warnUnknownPadCode(code, warned) {
|
|
232
|
+
if (!code.startsWith(GAMEPAD_CODE_PREFIX) || warned.has(code) || gamepadControl(code))
|
|
233
|
+
return;
|
|
234
|
+
warned.add(code);
|
|
235
|
+
console.warn(`[waica] unknown gamepad binding "${code}": it never matches.`);
|
|
236
|
+
}
|
package/dist/runtime-bridge.d.ts
CHANGED
|
@@ -18,8 +18,11 @@ export type RuntimeMode = 'paused' | 'real-time';
|
|
|
18
18
|
* operation and before a screenshot. `camera-effects` (issue #74, ADR 0020)
|
|
19
19
|
* announces that every snapshot carries `camera: { shake, fade, flash }`
|
|
20
20
|
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
21
|
+
* `analog-actions` (issue #75, ADR 0023) announces that `hold` takes an
|
|
22
|
+
* optional `value` in (0, 1] and that every control result carries
|
|
23
|
+
* `actionValues` beside `heldActions`.
|
|
21
24
|
*/
|
|
22
|
-
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects'];
|
|
25
|
+
export declare const RUNTIME_BRIDGE_CAPABILITIES: readonly ['click', 'scene', 'fixed-step', 'assets', 'camera-effects', 'analog-actions'];
|
|
23
26
|
export interface RuntimeMetadata {
|
|
24
27
|
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
25
28
|
engineVersion: string;
|
|
@@ -31,8 +34,14 @@ export interface RuntimeMetadata {
|
|
|
31
34
|
assets: AssetStatus;
|
|
32
35
|
}
|
|
33
36
|
export type RuntimeControlRequest = {
|
|
34
|
-
operation: 'press' | '
|
|
37
|
+
operation: 'press' | 'release';
|
|
35
38
|
action: string;
|
|
39
|
+
}
|
|
40
|
+
/** Holds at `value` in (0, 1]; without one, at 1. */
|
|
41
|
+
| {
|
|
42
|
+
operation: 'hold';
|
|
43
|
+
action: string;
|
|
44
|
+
value?: number;
|
|
36
45
|
} | {
|
|
37
46
|
operation: 'pause' | 'resume';
|
|
38
47
|
}
|
|
@@ -50,6 +59,8 @@ export type RuntimeControlRequest = {
|
|
|
50
59
|
};
|
|
51
60
|
export interface RuntimeControlResult extends RuntimeMetadata {
|
|
52
61
|
heldActions: string[];
|
|
62
|
+
/** The value of each held action, in the same order as `heldActions`. */
|
|
63
|
+
actionValues: Record<string, number>;
|
|
53
64
|
}
|
|
54
65
|
export declare class RuntimeBridgeOperationError extends Error {
|
|
55
66
|
readonly code: 'runtime-invalid-state' | 'runtime-operation-failed';
|
|
@@ -82,9 +93,10 @@ export interface RuntimeBridgeHost {
|
|
|
82
93
|
/** Starts clock-driven playback; `onStep` is told after every Simulation Step. */
|
|
83
94
|
resume(onStep: () => void): void;
|
|
84
95
|
pause(): void;
|
|
85
|
-
injectAction(action: string, operation: 'press' | 'hold' | 'release'): boolean;
|
|
96
|
+
injectAction(action: string, operation: 'press' | 'hold' | 'release', value?: number): boolean;
|
|
86
97
|
availableActions(): string[];
|
|
87
98
|
heldActions(): string[];
|
|
99
|
+
actionValues(): Record<string, number>;
|
|
88
100
|
inspect(metadata: RuntimeMetadata, filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
89
101
|
click(x: number, y: number): void;
|
|
90
102
|
/** Resolves `name` through the registered catalog and loads it. */
|
package/dist/runtime-bridge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import enginePackage from '../package.json' with { type: 'json' };
|
|
2
2
|
import { SIMULATION_STEP } from './fixed-step.js';
|
|
3
|
+
import { checkInjectedValue } from './input.js';
|
|
3
4
|
export const RUNTIME_BRIDGE_PROTOCOL_VERSION = 1;
|
|
4
5
|
export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
5
6
|
/**
|
|
@@ -17,6 +18,9 @@ export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
|
17
18
|
* operation and before a screenshot. `camera-effects` (issue #74, ADR 0020)
|
|
18
19
|
* announces that every snapshot carries `camera: { shake, fade, flash }`
|
|
19
20
|
* from `game.cameraEffects`; the `scene` operation stays a hard cut.
|
|
21
|
+
* `analog-actions` (issue #75, ADR 0023) announces that `hold` takes an
|
|
22
|
+
* optional `value` in (0, 1] and that every control result carries
|
|
23
|
+
* `actionValues` beside `heldActions`.
|
|
20
24
|
*/
|
|
21
25
|
export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
22
26
|
'click',
|
|
@@ -24,6 +28,7 @@ export const RUNTIME_BRIDGE_CAPABILITIES = [
|
|
|
24
28
|
'fixed-step',
|
|
25
29
|
'assets',
|
|
26
30
|
'camera-effects',
|
|
31
|
+
'analog-actions',
|
|
27
32
|
];
|
|
28
33
|
export class RuntimeBridgeOperationError extends Error {
|
|
29
34
|
code;
|
|
@@ -116,10 +121,17 @@ export class EngineRuntimeBridge {
|
|
|
116
121
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unsupported runtime control operation "${unsupported.operation}".`);
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
...this.metadata(),
|
|
126
|
+
heldActions: this.host.heldActions(),
|
|
127
|
+
actionValues: this.host.actionValues(),
|
|
128
|
+
};
|
|
120
129
|
}
|
|
121
130
|
injectAction(request) {
|
|
122
|
-
|
|
131
|
+
const checked = checkInjectedValue(request.operation, 'value' in request ? request.value : undefined);
|
|
132
|
+
if ('error' in checked)
|
|
133
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', checked.error);
|
|
134
|
+
if (this.host.injectAction(request.action, request.operation, checked.value))
|
|
123
135
|
return;
|
|
124
136
|
const available = this.host.availableActions();
|
|
125
137
|
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown action "${request.action}". Available actions: ${available.join(', ') || '(none)'}.`, available);
|