@waica/engine 0.21.0 → 0.23.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 +14 -0
- package/dist/collision-category.js +5 -2
- package/dist/collision-dispatch.d.ts +7 -1
- package/dist/collision-dispatch.js +18 -11
- package/dist/components/animated-sprite.d.ts +2 -0
- package/dist/components/animated-sprite.js +41 -6
- package/dist/components/sprite.d.ts +6 -0
- package/dist/components/sprite.js +52 -4
- package/dist/game.d.ts +7 -3
- package/dist/game.js +15 -5
- 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/dist/scene.d.ts +10 -0
- package/dist/spatial-broadphase.d.ts +7 -1
- package/dist/spatial-broadphase.js +16 -45
- package/dist/spatial-pairs.d.ts +17 -0
- package/dist/spatial-pairs.js +79 -0
- package/dist/sprite-batch.d.ts +93 -0
- package/dist/sprite-batch.js +211 -0
- package/dist/sprite-batches.d.ts +107 -0
- package/dist/sprite-batches.js +260 -0
- package/dist/sprite-draw-collector.d.ts +42 -0
- package/dist/sprite-draw-collector.js +107 -0
- package/dist/sprite-runs.d.ts +40 -0
- package/dist/sprite-runs.js +49 -0
- package/package.json +1 -1
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);
|
package/dist/scene.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ export interface SceneRenderJson {
|
|
|
37
37
|
sort?: 'y';
|
|
38
38
|
/** Logical positions are projected to the fixed 2:1 render lattice. */
|
|
39
39
|
projection?: 'isometric';
|
|
40
|
+
/**
|
|
41
|
+
* Sprite Batches (ADR 0024). Absent or true: sprites that share art —
|
|
42
|
+
* same texture (an AnimatedSprite's current sheet), pixel-art filtering and
|
|
43
|
+
* shape — and sit next to each other in draw order are drawn together in
|
|
44
|
+
* one call, each one an instance with its own placement, color and frame;
|
|
45
|
+
* what is drawn in front of what never changes. false: one mesh, one
|
|
46
|
+
* material and one draw call per sprite, the pre-batching path — set it to
|
|
47
|
+
* rule batching out of a visual difference or while debugging draw order.
|
|
48
|
+
*/
|
|
49
|
+
batch?: boolean;
|
|
40
50
|
}
|
|
41
51
|
export interface SceneJson {
|
|
42
52
|
waicaScene: 1 | 2 | 3;
|
|
@@ -15,7 +15,13 @@ export interface SpatialBroadphaseStats {
|
|
|
15
15
|
export interface SpatialBroadphase<T> {
|
|
16
16
|
readonly stats: SpatialBroadphaseStats;
|
|
17
17
|
candidates(bounds: CollisionBounds): T[];
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Visits every distinct pair sharing a cell, or involving an overflow
|
|
20
|
+
* entry, once, in ascending query order. The grid is frozen at creation,
|
|
21
|
+
* so callbacks that move, spawn or destroy entities do not change which
|
|
22
|
+
* pairs are visited.
|
|
23
|
+
*/
|
|
24
|
+
forEachPair(visit: (first: T, second: T) => void): void;
|
|
19
25
|
}
|
|
20
26
|
/**
|
|
21
27
|
* Package-internal fresh uniform grid. Bodies above the fixed occupancy cap
|
|
@@ -4,6 +4,7 @@ import { Hitbox } from './components/hitbox.js';
|
|
|
4
4
|
import { Solid } from './components/solid.js';
|
|
5
5
|
import { Tilemap } from './components/tilemap.js';
|
|
6
6
|
import { sceneSolids } from './scene-solids.js';
|
|
7
|
+
import { forEachCandidatePair } from './spatial-pairs.js';
|
|
7
8
|
import { usableCollisionBody } from './spatial-query-geometry.js';
|
|
8
9
|
const CELL_OCCUPANCY_CAP = 256;
|
|
9
10
|
function rangeSize(range) {
|
|
@@ -54,13 +55,6 @@ function entryValue(entries, index) {
|
|
|
54
55
|
throw new Error(`Spatial broadphase has no entry at index ${index}`);
|
|
55
56
|
return entry.value;
|
|
56
57
|
}
|
|
57
|
-
/** The entry index stored at `position` of a bucket that is iterated within its bounds. */
|
|
58
|
-
function indexAt(bucket, position) {
|
|
59
|
-
const index = bucket[position];
|
|
60
|
-
if (index === undefined)
|
|
61
|
-
throw new Error(`Spatial broadphase bucket has no index at position ${position}`);
|
|
62
|
-
return index;
|
|
63
|
-
}
|
|
64
58
|
/** The indexable sources in query order, each with the cell range it covers. */
|
|
65
59
|
function spatialEntries(sources, cellSize, isIndexable) {
|
|
66
60
|
return sources
|
|
@@ -79,47 +73,25 @@ function spatialEntries(sources, cellSize, isIndexable) {
|
|
|
79
73
|
function spatialGrid(entries) {
|
|
80
74
|
const buckets = new Map();
|
|
81
75
|
const overflow = [];
|
|
76
|
+
const entryBuckets = [];
|
|
82
77
|
for (const [index, entry] of entries.entries()) {
|
|
78
|
+
const own = [];
|
|
79
|
+
entryBuckets.push(own);
|
|
83
80
|
if (!entry.range) {
|
|
84
81
|
overflow.push(index);
|
|
85
82
|
continue;
|
|
86
83
|
}
|
|
87
84
|
eachCell(entry.range, (key) => {
|
|
88
|
-
|
|
89
|
-
if (bucket)
|
|
90
|
-
bucket
|
|
91
|
-
|
|
92
|
-
buckets.set(key, [index]);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return { buckets, overflow };
|
|
96
|
-
}
|
|
97
|
-
/** Distinct index pairs sharing a cell, or involving an overflow entry, in ascending order. */
|
|
98
|
-
function candidatePairIndices(grid, entryCount) {
|
|
99
|
-
const pairKeys = new Set();
|
|
100
|
-
const width = entryCount;
|
|
101
|
-
const addPair = (left, right) => {
|
|
102
|
-
if (left === right)
|
|
103
|
-
return;
|
|
104
|
-
const first = Math.min(left, right);
|
|
105
|
-
const second = Math.max(left, right);
|
|
106
|
-
pairKeys.add(first * width + second);
|
|
107
|
-
};
|
|
108
|
-
for (const bucket of grid.buckets.values()) {
|
|
109
|
-
for (let left = 0; left < bucket.length; left += 1) {
|
|
110
|
-
for (let right = left + 1; right < bucket.length; right += 1) {
|
|
111
|
-
addPair(indexAt(bucket, left), indexAt(bucket, right));
|
|
85
|
+
let bucket = buckets.get(key);
|
|
86
|
+
if (!bucket) {
|
|
87
|
+
bucket = [];
|
|
88
|
+
buckets.set(key, bucket);
|
|
112
89
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
for (let index = 0; index < entryCount; index += 1) {
|
|
117
|
-
addPair(overflowIndex, index);
|
|
118
|
-
}
|
|
90
|
+
bucket.push(index);
|
|
91
|
+
own.push(bucket);
|
|
92
|
+
});
|
|
119
93
|
}
|
|
120
|
-
return
|
|
121
|
-
.map((key) => [Math.floor(key / width), key % width])
|
|
122
|
-
.sort(([a1, a2], [b1, b2]) => a1 - b1 || a2 - b2);
|
|
94
|
+
return { buckets, overflow, entryBuckets };
|
|
123
95
|
}
|
|
124
96
|
/**
|
|
125
97
|
* Package-internal fresh uniform grid. Bodies above the fixed occupancy cap
|
|
@@ -144,11 +116,10 @@ export function createSpatialBroadphase(sources, cellSize, isIndexable = usableC
|
|
|
144
116
|
.sort((a, b) => a - b)
|
|
145
117
|
.map((index) => entryValue(entries, index));
|
|
146
118
|
},
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
entryValue(entries, first),
|
|
150
|
-
|
|
151
|
-
]);
|
|
119
|
+
forEachPair(visit) {
|
|
120
|
+
forEachCandidatePair(grid, (first, second) => {
|
|
121
|
+
visit(entryValue(entries, first), entryValue(entries, second));
|
|
122
|
+
});
|
|
152
123
|
},
|
|
153
124
|
};
|
|
154
125
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The grid shape the pair walk needs: which buckets each entry is in, and the overflow. */
|
|
2
|
+
export interface PairGrid {
|
|
3
|
+
/** Entries too large to bucket, ascending; they pair with every other entry. */
|
|
4
|
+
readonly overflow: readonly number[];
|
|
5
|
+
/** Per entry, the buckets it was added to, each ascending by entry index (empty for overflow entries). */
|
|
6
|
+
readonly entryBuckets: readonly (readonly (readonly number[])[])[];
|
|
7
|
+
}
|
|
8
|
+
type PairVisitor = (first: number, second: number) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Visits every distinct index pair sharing a cell, or involving an overflow
|
|
11
|
+
* entry, in ascending (first, second) order. Buckets fill in index order, so
|
|
12
|
+
* each one is already ascending: a body in one cell, with no overflow after
|
|
13
|
+
* it, needs no deduplication or sort. Equivalent to collecting all pairs,
|
|
14
|
+
* deduplicating and sorting them, without allocating per pair.
|
|
15
|
+
*/
|
|
16
|
+
export declare function forEachCandidatePair(grid: PairGrid, visit: PairVisitor): void;
|
|
17
|
+
export {};
|