@waica/engine 0.4.2 → 0.6.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 +85 -0
- package/dist/authoring-defaults.d.ts +11 -0
- package/dist/authoring-defaults.js +51 -0
- package/dist/component-update-schedule.d.ts +39 -0
- package/dist/component-update-schedule.js +157 -0
- package/dist/component.d.ts +15 -1
- package/dist/component.js +2 -0
- package/dist/components/animated-sprite.d.ts +2 -0
- package/dist/components/animated-sprite.js +11 -0
- package/dist/components/sprite.d.ts +1 -0
- package/dist/components/sprite.js +1 -0
- package/dist/game.d.ts +7 -0
- package/dist/game.js +79 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +4 -0
- package/dist/input.d.ts +10 -0
- package/dist/input.js +40 -2
- package/dist/runtime-bridge.d.ts +69 -0
- package/dist/runtime-bridge.js +105 -0
- package/dist/runtime-inspection.d.ts +71 -0
- package/dist/runtime-inspection.js +279 -0
- package/dist/state/state-machine.d.ts +2 -1
- package/dist/state/state-machine.js +7 -1
- package/package.json +3 -2
package/dist/input.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ActionName = string;
|
|
2
|
+
export type InjectedActionOperation = 'press' | 'hold' | 'release';
|
|
2
3
|
/** Action → KeyboardEvent.code list. */
|
|
3
4
|
export type InputBindings = Record<string, string[]>;
|
|
4
5
|
/** Neutral engine baseline; archetypes own their action vocabulary. */
|
|
@@ -11,6 +12,9 @@ export declare class Input {
|
|
|
11
12
|
private readonly bindings;
|
|
12
13
|
private readonly down;
|
|
13
14
|
private readonly justDown;
|
|
15
|
+
private readonly injectedDown;
|
|
16
|
+
private readonly injectedJustDown;
|
|
17
|
+
private readonly injectedPresses;
|
|
14
18
|
private readonly used;
|
|
15
19
|
/** Installs exactly the action map supplied by the active archetype/project. */
|
|
16
20
|
constructor(bindings?: Readonly<InputBindings>);
|
|
@@ -18,6 +22,12 @@ export declare class Input {
|
|
|
18
22
|
held(action: ActionName): boolean;
|
|
19
23
|
/** Was the action pressed exactly this frame? */
|
|
20
24
|
justPressed(action: ActionName): boolean;
|
|
25
|
+
/** Installed semantic action names in deterministic order. */
|
|
26
|
+
availableActions(): ActionName[];
|
|
27
|
+
/** Currently held semantic action names in deterministic order. */
|
|
28
|
+
heldActions(): ActionName[];
|
|
29
|
+
/** Injects an action by semantic name; false means the action is not installed. */
|
|
30
|
+
injectAction(action: ActionName, operation: InjectedActionOperation): boolean;
|
|
21
31
|
/** -1..1 axis from two actions (left/right by default). */
|
|
22
32
|
axis(negative?: ActionName, positive?: ActionName): number;
|
|
23
33
|
/**
|
package/dist/input.js
CHANGED
|
@@ -8,6 +8,9 @@ export class Input {
|
|
|
8
8
|
bindings = new Map();
|
|
9
9
|
down = new Set();
|
|
10
10
|
justDown = new Set();
|
|
11
|
+
injectedDown = new Set();
|
|
12
|
+
injectedJustDown = new Set();
|
|
13
|
+
injectedPresses = new Set();
|
|
11
14
|
used = new Set();
|
|
12
15
|
/** Installs exactly the action map supplied by the active archetype/project. */
|
|
13
16
|
constructor(bindings = DEFAULT_BINDINGS) {
|
|
@@ -21,11 +24,39 @@ export class Input {
|
|
|
21
24
|
}
|
|
22
25
|
/** Is the action held this frame? */
|
|
23
26
|
held(action) {
|
|
24
|
-
return this.isActive(action, this.down);
|
|
27
|
+
return this.injectedDown.has(action) || this.isActive(action, this.down);
|
|
25
28
|
}
|
|
26
29
|
/** Was the action pressed exactly this frame? */
|
|
27
30
|
justPressed(action) {
|
|
28
|
-
return this.isActive(action, this.justDown);
|
|
31
|
+
return this.injectedJustDown.has(action) || this.isActive(action, this.justDown);
|
|
32
|
+
}
|
|
33
|
+
/** Installed semantic action names in deterministic order. */
|
|
34
|
+
availableActions() {
|
|
35
|
+
return [...this.bindings.keys()].sort();
|
|
36
|
+
}
|
|
37
|
+
/** Currently held semantic action names in deterministic order. */
|
|
38
|
+
heldActions() {
|
|
39
|
+
return this.availableActions().filter((action) => this.held(action));
|
|
40
|
+
}
|
|
41
|
+
/** Injects an action by semantic name; false means the action is not installed. */
|
|
42
|
+
injectAction(action, operation) {
|
|
43
|
+
if (!this.bindings.has(action))
|
|
44
|
+
return false;
|
|
45
|
+
if (operation === 'release') {
|
|
46
|
+
this.injectedDown.delete(action);
|
|
47
|
+
this.injectedPresses.delete(action);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (this.held(action)) {
|
|
51
|
+
if (operation === 'hold')
|
|
52
|
+
this.injectedPresses.delete(action);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
this.injectedDown.add(action);
|
|
56
|
+
this.injectedJustDown.add(action);
|
|
57
|
+
if (operation === 'press')
|
|
58
|
+
this.injectedPresses.add(action);
|
|
59
|
+
return true;
|
|
29
60
|
}
|
|
30
61
|
/** -1..1 axis from two actions (left/right by default). */
|
|
31
62
|
axis(negative = 'left', positive = 'right') {
|
|
@@ -47,9 +78,16 @@ export class Input {
|
|
|
47
78
|
/** Called by the Game at the end of each frame. */
|
|
48
79
|
endFrame() {
|
|
49
80
|
this.justDown.clear();
|
|
81
|
+
this.injectedJustDown.clear();
|
|
82
|
+
for (const action of this.injectedPresses)
|
|
83
|
+
this.injectedDown.delete(action);
|
|
84
|
+
this.injectedPresses.clear();
|
|
50
85
|
this.used.clear();
|
|
51
86
|
}
|
|
52
87
|
dispose() {
|
|
88
|
+
this.injectedDown.clear();
|
|
89
|
+
this.injectedJustDown.clear();
|
|
90
|
+
this.injectedPresses.clear();
|
|
53
91
|
window.removeEventListener('keydown', this.onKeyDown);
|
|
54
92
|
window.removeEventListener('keyup', this.onKeyUp);
|
|
55
93
|
window.removeEventListener('blur', this.releaseAll);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { RuntimeSnapshot, RuntimeSnapshotFilters } from './runtime-inspection.js';
|
|
2
|
+
export declare const RUNTIME_BRIDGE_PROTOCOL_VERSION: 1;
|
|
3
|
+
export declare const RUNTIME_BRIDGE_SYMBOL: unique symbol;
|
|
4
|
+
export type RuntimeMode = 'paused' | 'real-time';
|
|
5
|
+
export interface RuntimeMetadata {
|
|
6
|
+
bridgeVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
7
|
+
engineVersion: string;
|
|
8
|
+
mode: RuntimeMode;
|
|
9
|
+
frame: number;
|
|
10
|
+
simulationTime: number;
|
|
11
|
+
}
|
|
12
|
+
export type RuntimeControlRequest = {
|
|
13
|
+
operation: 'press' | 'hold' | 'release';
|
|
14
|
+
action: string;
|
|
15
|
+
} | {
|
|
16
|
+
operation: 'pause' | 'resume';
|
|
17
|
+
} | {
|
|
18
|
+
operation: 'step';
|
|
19
|
+
dt?: number;
|
|
20
|
+
frames?: number;
|
|
21
|
+
};
|
|
22
|
+
export interface RuntimeControlResult extends RuntimeMetadata {
|
|
23
|
+
heldActions: string[];
|
|
24
|
+
}
|
|
25
|
+
export declare class RuntimeBridgeOperationError extends Error {
|
|
26
|
+
readonly code: 'runtime-invalid-state' | 'runtime-operation-failed';
|
|
27
|
+
readonly availableActions?: string[] | undefined;
|
|
28
|
+
readonly stage: 'control';
|
|
29
|
+
constructor(code: 'runtime-invalid-state' | 'runtime-operation-failed', message: string, availableActions?: string[] | undefined);
|
|
30
|
+
}
|
|
31
|
+
/** Engine-owned capability registered only in an MCP-activated page. */
|
|
32
|
+
export interface RuntimeBridge {
|
|
33
|
+
readonly surface: HTMLCanvasElement;
|
|
34
|
+
metadata(): RuntimeMetadata;
|
|
35
|
+
inspect(filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
36
|
+
control(request: RuntimeControlRequest): RuntimeControlResult;
|
|
37
|
+
}
|
|
38
|
+
/** Ephemeral pre-page hook installed by the owner of a browser context. */
|
|
39
|
+
export interface RuntimeBridgeActivation {
|
|
40
|
+
readonly protocolVersion: typeof RUNTIME_BRIDGE_PROTOCOL_VERSION;
|
|
41
|
+
register(bridge: RuntimeBridge): void;
|
|
42
|
+
unregister(bridge: RuntimeBridge): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function activeRuntimeBridgeHook(): RuntimeBridgeActivation | null;
|
|
45
|
+
export interface RuntimeBridgeHost {
|
|
46
|
+
step(dt: number): void;
|
|
47
|
+
resume(frame: (dt: number) => void): void;
|
|
48
|
+
pause(): void;
|
|
49
|
+
injectAction(action: string, operation: 'press' | 'hold' | 'release'): boolean;
|
|
50
|
+
availableActions(): string[];
|
|
51
|
+
heldActions(): string[];
|
|
52
|
+
inspect(metadata: RuntimeMetadata, filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
53
|
+
}
|
|
54
|
+
export declare class EngineRuntimeBridge implements RuntimeBridge {
|
|
55
|
+
readonly surface: HTMLCanvasElement;
|
|
56
|
+
private readonly activation;
|
|
57
|
+
private readonly host;
|
|
58
|
+
readonly engineVersion: string;
|
|
59
|
+
private registered;
|
|
60
|
+
private mode;
|
|
61
|
+
private frame;
|
|
62
|
+
private simulationTime;
|
|
63
|
+
constructor(surface: HTMLCanvasElement, activation: RuntimeBridgeActivation, host: RuntimeBridgeHost);
|
|
64
|
+
metadata(): RuntimeMetadata;
|
|
65
|
+
inspect(filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
66
|
+
control(request: RuntimeControlRequest): RuntimeControlResult;
|
|
67
|
+
private advance;
|
|
68
|
+
unregister(): void;
|
|
69
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import enginePackage from '../package.json' with { type: 'json' };
|
|
2
|
+
export const RUNTIME_BRIDGE_PROTOCOL_VERSION = 1;
|
|
3
|
+
export const RUNTIME_BRIDGE_SYMBOL = Symbol.for('@waica/runtime-bridge/v1');
|
|
4
|
+
export class RuntimeBridgeOperationError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
availableActions;
|
|
7
|
+
stage = 'control';
|
|
8
|
+
constructor(code, message, availableActions) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.availableActions = availableActions;
|
|
12
|
+
this.name = 'RuntimeBridgeOperationError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function activeRuntimeBridgeHook() {
|
|
16
|
+
const candidate = globalThis[RUNTIME_BRIDGE_SYMBOL];
|
|
17
|
+
if (!candidate || typeof candidate !== 'object')
|
|
18
|
+
return null;
|
|
19
|
+
const hook = candidate;
|
|
20
|
+
if (hook.protocolVersion !== RUNTIME_BRIDGE_PROTOCOL_VERSION ||
|
|
21
|
+
typeof hook.register !== 'function' ||
|
|
22
|
+
typeof hook.unregister !== 'function') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return hook;
|
|
26
|
+
}
|
|
27
|
+
export class EngineRuntimeBridge {
|
|
28
|
+
surface;
|
|
29
|
+
activation;
|
|
30
|
+
host;
|
|
31
|
+
engineVersion = enginePackage.version;
|
|
32
|
+
registered = true;
|
|
33
|
+
mode = 'paused';
|
|
34
|
+
frame = 0;
|
|
35
|
+
simulationTime = 0;
|
|
36
|
+
constructor(surface, activation, host) {
|
|
37
|
+
this.surface = surface;
|
|
38
|
+
this.activation = activation;
|
|
39
|
+
this.host = host;
|
|
40
|
+
}
|
|
41
|
+
metadata() {
|
|
42
|
+
return {
|
|
43
|
+
bridgeVersion: RUNTIME_BRIDGE_PROTOCOL_VERSION,
|
|
44
|
+
engineVersion: this.engineVersion,
|
|
45
|
+
mode: this.mode,
|
|
46
|
+
frame: this.frame,
|
|
47
|
+
simulationTime: this.simulationTime,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
inspect(filters = {}) {
|
|
51
|
+
return this.host.inspect(this.metadata(), filters);
|
|
52
|
+
}
|
|
53
|
+
control(request) {
|
|
54
|
+
switch (request.operation) {
|
|
55
|
+
case 'pause':
|
|
56
|
+
if (this.mode === 'real-time') {
|
|
57
|
+
this.host.pause();
|
|
58
|
+
this.mode = 'paused';
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case 'resume':
|
|
62
|
+
if (this.mode === 'paused') {
|
|
63
|
+
this.mode = 'real-time';
|
|
64
|
+
this.host.resume((dt) => this.advance(dt));
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
case 'press':
|
|
68
|
+
case 'hold':
|
|
69
|
+
case 'release':
|
|
70
|
+
if (!this.host.injectAction(request.action, request.operation)) {
|
|
71
|
+
const available = this.host.availableActions();
|
|
72
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', `Unknown action "${request.action}". Available actions: ${available.join(', ') || '(none)'}.`, available);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'step': {
|
|
76
|
+
if (this.mode !== 'paused') {
|
|
77
|
+
throw new RuntimeBridgeOperationError('runtime-invalid-state', 'step is only available while the Runtime Bridge is paused.');
|
|
78
|
+
}
|
|
79
|
+
const dt = request.dt ?? 1 / 60;
|
|
80
|
+
const frames = request.frames ?? 1;
|
|
81
|
+
if (!Number.isFinite(dt) || dt <= 0 || dt > 0.1) {
|
|
82
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'dt must be finite and greater than 0 and at most 0.1.');
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isInteger(frames) || frames < 1 || frames > 600) {
|
|
85
|
+
throw new RuntimeBridgeOperationError('runtime-operation-failed', 'frames must be an integer from 1 through 600.');
|
|
86
|
+
}
|
|
87
|
+
for (let index = 0; index < frames; index += 1)
|
|
88
|
+
this.advance(dt);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { ...this.metadata(), heldActions: this.host.heldActions() };
|
|
93
|
+
}
|
|
94
|
+
advance(dt) {
|
|
95
|
+
this.host.step(dt);
|
|
96
|
+
this.frame += 1;
|
|
97
|
+
this.simulationTime += dt;
|
|
98
|
+
}
|
|
99
|
+
unregister() {
|
|
100
|
+
if (!this.registered)
|
|
101
|
+
return;
|
|
102
|
+
this.registered = false;
|
|
103
|
+
this.activation.unregister(this);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Game } from './game.js';
|
|
2
|
+
import type { RuntimeMetadata } from './runtime-bridge.js';
|
|
3
|
+
import type { StatValue } from './stats.js';
|
|
4
|
+
export type ProjectionMarkerKind = 'cycle' | 'unsupported' | 'error' | 'truncated';
|
|
5
|
+
export interface ProjectionMarker {
|
|
6
|
+
$waica: ProjectionMarkerKind | 'date' | 'bigint' | 'map' | 'set';
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
export type ProjectedValue = null | boolean | number | string | ProjectionMarker | ProjectedValue[] | {
|
|
10
|
+
[key: string]: ProjectedValue;
|
|
11
|
+
};
|
|
12
|
+
export interface ProjectionIssue {
|
|
13
|
+
path: string;
|
|
14
|
+
marker: ProjectionMarkerKind;
|
|
15
|
+
omitted?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RuntimeSnapshotFilters {
|
|
18
|
+
entity_ids?: string[];
|
|
19
|
+
entity_names?: string[];
|
|
20
|
+
component_types?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface RuntimeTransformSnapshot {
|
|
23
|
+
position: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
z: number;
|
|
27
|
+
};
|
|
28
|
+
rotation: {
|
|
29
|
+
x: number;
|
|
30
|
+
y: number;
|
|
31
|
+
z: number;
|
|
32
|
+
order: string;
|
|
33
|
+
};
|
|
34
|
+
scale: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
z: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface RuntimeComponentSnapshot {
|
|
41
|
+
type: string;
|
|
42
|
+
index: number;
|
|
43
|
+
state: ProjectedValue;
|
|
44
|
+
}
|
|
45
|
+
export interface RuntimeEntitySnapshot {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
transform: RuntimeTransformSnapshot;
|
|
49
|
+
components: RuntimeComponentSnapshot[];
|
|
50
|
+
}
|
|
51
|
+
export interface RuntimeSnapshot extends RuntimeMetadata {
|
|
52
|
+
stats: Record<string, StatValue>;
|
|
53
|
+
entities: RuntimeEntitySnapshot[];
|
|
54
|
+
projectionIssues: ProjectionIssue[];
|
|
55
|
+
}
|
|
56
|
+
export declare const RUNTIME_PROJECTION_LIMITS: {
|
|
57
|
+
readonly depth: 5;
|
|
58
|
+
readonly entries: 100;
|
|
59
|
+
readonly stringBytes: number;
|
|
60
|
+
readonly componentBytes: number;
|
|
61
|
+
readonly snapshotBytes: number;
|
|
62
|
+
};
|
|
63
|
+
export declare class RuntimeInspector {
|
|
64
|
+
private readonly game;
|
|
65
|
+
private readonly ids;
|
|
66
|
+
private nextId;
|
|
67
|
+
constructor(game: Game);
|
|
68
|
+
snapshot(metadata: RuntimeMetadata, filters?: RuntimeSnapshotFilters): RuntimeSnapshot;
|
|
69
|
+
private capSnapshot;
|
|
70
|
+
private idFor;
|
|
71
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
export const RUNTIME_PROJECTION_LIMITS = {
|
|
2
|
+
depth: 5,
|
|
3
|
+
entries: 100,
|
|
4
|
+
stringBytes: 4 * 1024,
|
|
5
|
+
componentBytes: 64 * 1024,
|
|
6
|
+
snapshotBytes: 1024 * 1024,
|
|
7
|
+
};
|
|
8
|
+
function marker(context, path, kind, detail = {}) {
|
|
9
|
+
context.issues.push({ path, marker: kind });
|
|
10
|
+
return { $waica: kind, ...detail };
|
|
11
|
+
}
|
|
12
|
+
function isPlainRecord(value) {
|
|
13
|
+
const prototype = Object.getPrototypeOf(value);
|
|
14
|
+
return prototype === Object.prototype || prototype === null;
|
|
15
|
+
}
|
|
16
|
+
const textEncoder = new TextEncoder();
|
|
17
|
+
function utf8Bytes(value) {
|
|
18
|
+
return textEncoder.encode(value).byteLength;
|
|
19
|
+
}
|
|
20
|
+
function stringPreview(value, byteLimit) {
|
|
21
|
+
let low = 0;
|
|
22
|
+
let high = value.length;
|
|
23
|
+
while (low < high) {
|
|
24
|
+
const middle = Math.ceil((low + high) / 2);
|
|
25
|
+
if (utf8Bytes(value.slice(0, middle)) <= byteLimit)
|
|
26
|
+
low = middle;
|
|
27
|
+
else
|
|
28
|
+
high = middle - 1;
|
|
29
|
+
}
|
|
30
|
+
const preview = value.slice(0, low);
|
|
31
|
+
const last = preview.charCodeAt(preview.length - 1);
|
|
32
|
+
return last >= 0xd800 && last <= 0xdbff ? preview.slice(0, -1) : preview;
|
|
33
|
+
}
|
|
34
|
+
function projectValue(value, path, context, depth = 0) {
|
|
35
|
+
if (depth > RUNTIME_PROJECTION_LIMITS.depth) {
|
|
36
|
+
return marker(context, path, 'truncated', {
|
|
37
|
+
reason: 'depth',
|
|
38
|
+
maxDepth: RUNTIME_PROJECTION_LIMITS.depth,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (value === null || typeof value === 'boolean')
|
|
42
|
+
return value;
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
const bytes = utf8Bytes(value);
|
|
45
|
+
if (bytes <= RUNTIME_PROJECTION_LIMITS.stringBytes)
|
|
46
|
+
return value;
|
|
47
|
+
return marker(context, path, 'truncated', {
|
|
48
|
+
reason: 'string',
|
|
49
|
+
preview: stringPreview(value, RUNTIME_PROJECTION_LIMITS.stringBytes),
|
|
50
|
+
originalLength: value.length,
|
|
51
|
+
originalBytes: bytes,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'bigint')
|
|
55
|
+
return { $waica: 'bigint', value: value.toString() };
|
|
56
|
+
if (typeof value === 'number') {
|
|
57
|
+
return Number.isFinite(value)
|
|
58
|
+
? value
|
|
59
|
+
: marker(context, path, 'unsupported', { type: 'non-finite-number' });
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== 'object') {
|
|
62
|
+
return marker(context, path, 'unsupported', { type: typeof value });
|
|
63
|
+
}
|
|
64
|
+
const previousPath = context.seen.get(value);
|
|
65
|
+
if (previousPath)
|
|
66
|
+
return marker(context, path, 'cycle', { path: previousPath });
|
|
67
|
+
context.seen.set(value, path);
|
|
68
|
+
if (value instanceof Date) {
|
|
69
|
+
return Number.isFinite(value.getTime())
|
|
70
|
+
? { $waica: 'date', value: value.toISOString() }
|
|
71
|
+
: marker(context, path, 'error', { message: 'Invalid Date' });
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
const entries = value.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
|
|
75
|
+
.map((entry, index) => projectValue(entry, `${path}[${index}]`, context, depth + 1));
|
|
76
|
+
const omitted = value.length - entries.length;
|
|
77
|
+
if (omitted > 0) {
|
|
78
|
+
entries.push(marker(context, path, 'truncated', { reason: 'entries', omitted }));
|
|
79
|
+
}
|
|
80
|
+
return entries;
|
|
81
|
+
}
|
|
82
|
+
if (value instanceof Map) {
|
|
83
|
+
const sourceEntries = [...value.entries()];
|
|
84
|
+
const entries = sourceEntries.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
|
|
85
|
+
.map(([key, entry], index) => [
|
|
86
|
+
projectValue(key, `${path}.entries[${index}].key`, context, depth + 1),
|
|
87
|
+
projectValue(entry, `${path}.entries[${index}].value`, context, depth + 1),
|
|
88
|
+
]);
|
|
89
|
+
const omitted = sourceEntries.length - entries.length;
|
|
90
|
+
return {
|
|
91
|
+
$waica: 'map',
|
|
92
|
+
entries,
|
|
93
|
+
...(omitted > 0
|
|
94
|
+
? { truncated: marker(context, path, 'truncated', { reason: 'entries', omitted }) }
|
|
95
|
+
: {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (value instanceof Set) {
|
|
99
|
+
const sourceValues = [...value];
|
|
100
|
+
const values = sourceValues.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
|
|
101
|
+
.map((entry, index) => projectValue(entry, `${path}.values[${index}]`, context, depth + 1));
|
|
102
|
+
const omitted = sourceValues.length - values.length;
|
|
103
|
+
return {
|
|
104
|
+
$waica: 'set',
|
|
105
|
+
values,
|
|
106
|
+
...(omitted > 0
|
|
107
|
+
? { truncated: marker(context, path, 'truncated', { reason: 'entries', omitted }) }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (isPlainRecord(value)) {
|
|
112
|
+
const keys = Object.keys(value).sort();
|
|
113
|
+
const projected = Object.fromEntries(keys.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
|
|
114
|
+
.map((key) => [
|
|
115
|
+
key,
|
|
116
|
+
projectValue(value[key], `${path}.${key}`, context, depth + 1),
|
|
117
|
+
]));
|
|
118
|
+
const omitted = keys.length - Object.keys(projected).length;
|
|
119
|
+
if (omitted === 0)
|
|
120
|
+
return projected;
|
|
121
|
+
return marker(context, path, 'truncated', {
|
|
122
|
+
reason: 'entries',
|
|
123
|
+
omitted,
|
|
124
|
+
value: projected,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return marker(context, path, 'unsupported', {
|
|
128
|
+
type: value.constructor?.name ?? 'object',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function errorMessage(error) {
|
|
132
|
+
return error instanceof Error ? error.message : String(error);
|
|
133
|
+
}
|
|
134
|
+
function componentState(component, path, context) {
|
|
135
|
+
if (typeof component.inspectState === 'function') {
|
|
136
|
+
try {
|
|
137
|
+
return projectValue(component.inspectState(), path, context);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return marker(context, path, 'error', { message: errorMessage(error) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const keys = new Set(Object.keys(component));
|
|
144
|
+
for (let prototype = Object.getPrototypeOf(component); prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)) {
|
|
145
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(prototype))) {
|
|
146
|
+
if (typeof descriptor.set === 'function')
|
|
147
|
+
keys.add(key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const state = {};
|
|
151
|
+
for (const key of [...keys].sort()) {
|
|
152
|
+
if (key === 'entity' || key === 'game' || key.startsWith('_'))
|
|
153
|
+
continue;
|
|
154
|
+
const valuePath = `${path}.${key}`;
|
|
155
|
+
try {
|
|
156
|
+
const value = component[key];
|
|
157
|
+
if (typeof value === 'function')
|
|
158
|
+
continue;
|
|
159
|
+
state[key] = projectValue(value, valuePath, context, 1);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
state[key] = marker(context, valuePath, 'error', { message: errorMessage(error) });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return state;
|
|
166
|
+
}
|
|
167
|
+
function boundedComponentState(component, path, context) {
|
|
168
|
+
const state = componentState(component, path, context);
|
|
169
|
+
const originalBytes = utf8Bytes(JSON.stringify(state));
|
|
170
|
+
if (originalBytes <= RUNTIME_PROJECTION_LIMITS.componentBytes)
|
|
171
|
+
return state;
|
|
172
|
+
return marker(context, path, 'truncated', {
|
|
173
|
+
reason: 'component-size',
|
|
174
|
+
limit: RUNTIME_PROJECTION_LIMITS.componentBytes,
|
|
175
|
+
originalBytes,
|
|
176
|
+
path,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export class RuntimeInspector {
|
|
180
|
+
game;
|
|
181
|
+
ids = new WeakMap();
|
|
182
|
+
nextId = 1;
|
|
183
|
+
constructor(game) {
|
|
184
|
+
this.game = game;
|
|
185
|
+
}
|
|
186
|
+
snapshot(metadata, filters = {}) {
|
|
187
|
+
const projectionIssues = [];
|
|
188
|
+
const idFilter = filters.entity_ids ? new Set(filters.entity_ids) : null;
|
|
189
|
+
const nameFilter = filters.entity_names ? new Set(filters.entity_names) : null;
|
|
190
|
+
const componentFilter = filters.component_types ? new Set(filters.component_types) : null;
|
|
191
|
+
const live = this.game.entities.map((entity) => ({ entity, id: this.idFor(entity) }));
|
|
192
|
+
const entities = live.flatMap(({ entity, id }) => {
|
|
193
|
+
if (idFilter && !idFilter.has(id))
|
|
194
|
+
return [];
|
|
195
|
+
if (nameFilter && !nameFilter.has(entity.name))
|
|
196
|
+
return [];
|
|
197
|
+
const components = entity.components.flatMap((component, index) => {
|
|
198
|
+
const Class = component.constructor;
|
|
199
|
+
if (componentFilter && !componentFilter.has(Class.componentName))
|
|
200
|
+
return [];
|
|
201
|
+
const context = { issues: projectionIssues, seen: new Map() };
|
|
202
|
+
return [{
|
|
203
|
+
type: Class.componentName,
|
|
204
|
+
index,
|
|
205
|
+
state: boundedComponentState(component, `entities[${id}].components[${index}].state`, context),
|
|
206
|
+
}];
|
|
207
|
+
});
|
|
208
|
+
if (componentFilter && components.length === 0)
|
|
209
|
+
return [];
|
|
210
|
+
return [{
|
|
211
|
+
id,
|
|
212
|
+
name: entity.name,
|
|
213
|
+
transform: {
|
|
214
|
+
position: {
|
|
215
|
+
x: entity.position.x,
|
|
216
|
+
y: entity.position.y,
|
|
217
|
+
z: entity.position.z,
|
|
218
|
+
},
|
|
219
|
+
rotation: {
|
|
220
|
+
x: entity.node.rotation.x,
|
|
221
|
+
y: entity.node.rotation.y,
|
|
222
|
+
z: entity.node.rotation.z,
|
|
223
|
+
order: entity.node.rotation.order,
|
|
224
|
+
},
|
|
225
|
+
scale: {
|
|
226
|
+
x: entity.scale.x,
|
|
227
|
+
y: entity.scale.y,
|
|
228
|
+
z: entity.scale.z,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
components,
|
|
232
|
+
}];
|
|
233
|
+
});
|
|
234
|
+
return this.capSnapshot({
|
|
235
|
+
...metadata,
|
|
236
|
+
stats: Object.fromEntries([...this.game.stats.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))),
|
|
237
|
+
entities,
|
|
238
|
+
projectionIssues,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
capSnapshot(snapshot) {
|
|
242
|
+
if (utf8Bytes(JSON.stringify(snapshot)) <= RUNTIME_PROJECTION_LIMITS.snapshotBytes) {
|
|
243
|
+
return snapshot;
|
|
244
|
+
}
|
|
245
|
+
const retained = [...snapshot.entities];
|
|
246
|
+
const removedIds = new Set();
|
|
247
|
+
while (retained.length > 0) {
|
|
248
|
+
const removed = retained.pop();
|
|
249
|
+
if (removed)
|
|
250
|
+
removedIds.add(removed.id);
|
|
251
|
+
const omitted = snapshot.entities.length - retained.length;
|
|
252
|
+
const projectionIssues = snapshot.projectionIssues
|
|
253
|
+
.filter((issue) => [...removedIds].every((id) => !issue.path.startsWith(`entities[${id}]`)))
|
|
254
|
+
.concat({ path: `entities[${retained.length}]`, marker: 'truncated', omitted });
|
|
255
|
+
const candidate = { ...snapshot, entities: retained, projectionIssues };
|
|
256
|
+
if (utf8Bytes(JSON.stringify(candidate)) <= RUNTIME_PROJECTION_LIMITS.snapshotBytes) {
|
|
257
|
+
return candidate;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...snapshot,
|
|
262
|
+
entities: [],
|
|
263
|
+
projectionIssues: [{
|
|
264
|
+
path: 'entities[0]',
|
|
265
|
+
marker: 'truncated',
|
|
266
|
+
omitted: snapshot.entities.length,
|
|
267
|
+
}],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
idFor(entity) {
|
|
271
|
+
const existing = this.ids.get(entity);
|
|
272
|
+
if (existing)
|
|
273
|
+
return existing;
|
|
274
|
+
const id = `entity-${this.nextId}`;
|
|
275
|
+
this.nextId += 1;
|
|
276
|
+
this.ids.set(entity, id);
|
|
277
|
+
return id;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -13,7 +13,7 @@ export interface StateTransitionJson {
|
|
|
13
13
|
* transitions instead of a real state.
|
|
14
14
|
*/
|
|
15
15
|
export interface StateJson {
|
|
16
|
-
/** Clip
|
|
16
|
+
/** Clip reference resolved against the sibling AnimatedSprite; defaults to the state's name. */
|
|
17
17
|
clip?: string;
|
|
18
18
|
transitions?: StateTransitionJson[];
|
|
19
19
|
}
|
|
@@ -53,6 +53,7 @@ export declare class StateMachine extends Component {
|
|
|
53
53
|
label: string;
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
+
static transient: string[];
|
|
56
57
|
/** The character's role — names the logic set providing its state code. */
|
|
57
58
|
role: string;
|
|
58
59
|
/** Starting state; defaults to the first declared state. */
|
|
@@ -47,6 +47,7 @@ export class StateMachine extends Component {
|
|
|
47
47
|
static params = {
|
|
48
48
|
role: { label: 'Role' },
|
|
49
49
|
};
|
|
50
|
+
static transient = ['current', 'elapsed', 'instanceHooks', 'signals', 'warnedClips'];
|
|
50
51
|
/** The character's role — names the logic set providing its state code. */
|
|
51
52
|
role = '';
|
|
52
53
|
/** Starting state; defaults to the first declared state. */
|
|
@@ -96,7 +97,12 @@ export class StateMachine extends Component {
|
|
|
96
97
|
// capped so a degenerate cyclic graph can't hang the loop.
|
|
97
98
|
for (let hops = 0; hops < 8; hops++) {
|
|
98
99
|
const edge = nextTransition(this.states, this.current, this.env());
|
|
99
|
-
|
|
100
|
+
// A '*' edge is re-merged against whatever state the loop just
|
|
101
|
+
// entered, so a still-queued signal (signals.clear() only runs after
|
|
102
|
+
// this loop) keeps firing every remaining hop. Without this guard
|
|
103
|
+
// that replays onExit/onEnter on the state hops already settled into
|
|
104
|
+
// — goto() has the same guard for the same reason.
|
|
105
|
+
if (!edge || edge.to === this.current)
|
|
100
106
|
break;
|
|
101
107
|
// A transition fired by a key press spends it: one press, one
|
|
102
108
|
// transition — it can't fire again from the state just entered.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waica/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Waica game engine core — archetype-driven, web-first, 2D & 3D",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"directory": "packages/engine"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"dist"
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
14
15
|
],
|
|
15
16
|
"exports": {
|
|
16
17
|
".": {
|