@waica/cli 0.5.0 → 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.
Files changed (58) hide show
  1. package/README.md +42 -37
  2. package/dist/editor/assets/{cssMode-2_8FrcPt.js → cssMode-DfLN2qUF.js} +1 -1
  3. package/dist/editor/assets/{freemarker2-BgSLfjNS.js → freemarker2-BM-wWcXR.js} +1 -1
  4. package/dist/editor/assets/{handlebars-CZIkUhwZ.js → handlebars-C68SV5M9.js} +1 -1
  5. package/dist/editor/assets/{html-B66ooLhD.js → html-dQgLbIki.js} +1 -1
  6. package/dist/editor/assets/{htmlMode-BiIT--Mv.js → htmlMode-Dx91fUdd.js} +1 -1
  7. package/dist/editor/assets/{index-Br-M9Tdo.js → index-592ABIaz.js} +2787 -406
  8. package/dist/editor/assets/{index-q7IkLe-P.css → index-Cy5pmBhN.css} +1 -1
  9. package/dist/editor/assets/{javascript-BZrbbzWQ.js → javascript-BmsSq5ka.js} +1 -1
  10. package/dist/editor/assets/{jsonMode-CABkbZgt.js → jsonMode-CWoPtX2M.js} +1 -1
  11. package/dist/editor/assets/{liquid-CT7pyNYe.js → liquid-DwnJlNJZ.js} +1 -1
  12. package/dist/editor/assets/{mdx-DW7MPqdm.js → mdx-DaLkvlR1.js} +1 -1
  13. package/dist/editor/assets/{python-CgBwEaNf.js → python-Cs5X5zFp.js} +1 -1
  14. package/dist/editor/assets/{razor-Dvggr_Q1.js → razor-PTwWoRk8.js} +1 -1
  15. package/dist/editor/assets/{tsMode-OfEpAJE7.js → tsMode-BqUQdgsR.js} +1 -1
  16. package/dist/editor/assets/{typescript-BQbhlbBK.js → typescript-BsJ7r2RJ.js} +1 -1
  17. package/dist/editor/assets/{xml-5qToUR1k.js → xml-DyyoPey4.js} +1 -1
  18. package/dist/editor/assets/{yaml-CFM1jPNu.js → yaml-Cjqtlp29.js} +1 -1
  19. package/dist/editor/index.html +2 -2
  20. package/dist/mcp/introspection.js +2 -0
  21. package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
  22. package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +4 -3
  23. package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +12 -15
  24. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +1 -0
  25. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +1 -0
  26. package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
  27. package/dist/mcp/node_modules/@waica/engine/README.md +85 -0
  28. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.d.ts +39 -0
  29. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.js +157 -0
  30. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +5 -0
  31. package/dist/mcp/node_modules/@waica/engine/dist/component.js +1 -0
  32. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +1 -0
  33. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +1 -0
  34. package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +7 -0
  35. package/dist/mcp/node_modules/@waica/engine/dist/game.js +79 -1
  36. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +7 -1
  37. package/dist/mcp/node_modules/@waica/engine/dist/index.js +3 -0
  38. package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +10 -0
  39. package/dist/mcp/node_modules/@waica/engine/dist/input.js +40 -2
  40. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.d.ts +69 -0
  41. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.js +105 -0
  42. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.d.ts +71 -0
  43. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.js +279 -0
  44. package/dist/mcp/node_modules/@waica/engine/package.json +3 -2
  45. package/dist/mcp/project-component-loader.js +506 -240
  46. package/dist/mcp/project-component-runner.js +237 -0
  47. package/dist/mcp/runtime-browser.js +336 -0
  48. package/dist/mcp/runtime-dev-server.js +273 -0
  49. package/dist/mcp/runtime-preflight.js +249 -0
  50. package/dist/mcp/runtime-service.js +8 -0
  51. package/dist/mcp/runtime-session-manager.js +265 -0
  52. package/dist/mcp/scaffolds.js +126 -6
  53. package/dist/mcp/server.js +330 -9
  54. package/dist/mcp/stdio.js +0 -25
  55. package/dist/mcp/template/src/main.ts +6 -1
  56. package/dist/mcp/validation.js +77 -6
  57. package/package.json +2 -1
  58. package/dist/mcp/native-import.cjs +0 -3
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/engine",
3
- "version": "0.5.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
  ".": {