@cyberart-io/engine 0.0.1 → 0.0.2

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 CHANGED
@@ -83,6 +83,14 @@ window.addEventListener('pagehide', () => {
83
83
 
84
84
  `createRuntime` and `mount` are synchronous. `mount` **prepares** the cart (`getDefaultState`) but does not start the loop — that is `start()`. If prepare fails, `mount` throws.
85
85
 
86
+ ## Feature guides
87
+
88
+ Full API for the event router, deterministic replay, and CI harness (so agents can use every export):
89
+
90
+ - [Events and router](docs/events.md) — mailbox, envelope, `createEventRouter`, hops, idempotency, rejections
91
+ - [Deterministic mode](docs/deterministic-mode.md) — `step` / `schedule`, clocks, `ScriptedAction`, replay diffs
92
+ - [Headless harness](docs/headless-harness.md) — `createHeadlessHarness`, jsdom canvas, inspect / screenshot
93
+
86
94
  ## Write a cart
87
95
 
88
96
  A cart is an `AnimationCart`. Required:
@@ -127,9 +135,10 @@ const cart = runtime.mount(artProject, {
127
135
  | Option | Default | Meaning |
128
136
  |---|---|---|
129
137
  | `container` | required | Element that will hold the canvas. |
130
- | `seed` | generated | Token hash for `Random`. Instance-local; does not clobber a global cache. |
131
- | `captureKeyboard` | `false` | When true, the cart listens for `window` keydown. Full-page players pass `true`. |
138
+ | `seed` | generated | Token hash for `Random`. Instance-local; does not clobber a global cache. 64-hex hashes pass through; other seeds are mixed via `canonicalizeSeed`. |
139
+ | `captureKeyboard` | `false` | When true, the cart listens for `window` keydown. Full-page players pass `true`. Off in deterministic mode. |
132
140
  | `audio` | none | Libraries to unlock if `unlockAudio()` runs before `mount`. After mount, the cart’s `metadata.audio` wins. |
141
+ | `deterministic` | off | Host-controlled clock, `step`/`advance`, and scripted input/assets. Leave unset for live kaleidoscope / Art Blocks. |
133
142
 
134
143
  `CartHandle` (what `mount` returns):
135
144
 
@@ -139,6 +148,9 @@ const cart = runtime.mount(artProject, {
139
148
  | `pause()` / `resume()` | Freeze / continue the loop. |
140
149
  | `snapshot()` | PNG data URL + seed + metadata. Not live `state`. |
141
150
  | `dispatch(event)` | Queue an inbound host event for the cart. |
151
+ | `step(frames)` / `advance(ms)` | Deterministic ticks only. Throw if `deterministic` was not set. |
152
+ | `schedule(action)` | Queue a pointer/key/host-event/asset for a future frame. |
153
+ | `getClock()` / `getRandomState()` / `getReplayMetadata()` | Replay inspection. |
142
154
  | `exportState()` / `exportStateJSON()` | Pause-safe serializable bundle. |
143
155
  | `importState(bundle)` | Restore a bundle (or JSON string). |
144
156
  | `destroy()` | Unload this cart. Idempotent. |
@@ -187,12 +199,53 @@ const runtime = createRuntime({
187
199
 
188
200
  `Random` in `getDefaultState` / `update` / `render` follows that hash. Mark `metadata.generative: true` when the piece is a function of the seed — saves then refuse to load onto a different hash.
189
201
 
190
- ## Events
202
+ `Random.getState()` / `setState()` snapshot the dual sfc32 generators (post-warmup). Same hash always yields the same sequence; do not use `Math.random()` or `Date.now()` in cart logic. `setState` throws if the snapshot seed does not match.
203
+
204
+ A 64-hex Art Blocks hash is used as-is (optional `0x`, lowercased). `canonicalizeSeed` mixes any other value — including a number such as `42` — into a 64-hex hash. `createRuntime` uses `resolveRuntimeSeed`: live mode keeps a short `?hash=` string as `0x…` so kaleidoscope local hashes do not change; deterministic mode always canonicalizes.
205
+
206
+ ## Deterministic mode
207
+
208
+ Seeded `Random` is not enough to reproduce an interactive scene. Opt in with `deterministic` so the host owns time and input. **Leave this unset for kaleidoscope / Art Blocks.**
209
+
210
+ ```ts
211
+ const runtime = createRuntime({
212
+ container,
213
+ seed: 42,
214
+ deterministic: {
215
+ origin: 0,
216
+ actions: [
217
+ { type: 'asset', atFrame: 2, id: 'room', status: 'ready' },
218
+ { type: 'pointer', atFrame: 7, pointer: { kind: 'down', x: 40, y: 20 } },
219
+ ],
220
+ },
221
+ });
222
+ const cart = runtime.mount(artProject);
223
+ await cart.step(10);
224
+ const replay = await cart.getReplayMetadata();
225
+ ```
226
+
227
+ `step` / `advance` / `schedule` / `getClock` / `getReplayMetadata` / `describeReplayMismatch` are documented in [deterministic mode](docs/deterministic-mode.md) (`ScriptedAction` includes `key` and `event`; asset types are `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT`; clocks are `createVirtualClock` / `createWallClock`).
228
+
229
+ ## Headless harness
230
+
231
+ CI and agents should drive the **same** `createRuntime({ deterministic })` path. `installHeadlessCanvas` is the documented jsdom install (test-only). `createHeadlessHarness` sizes a container, mounts, and wraps step / input / inspect / snapshot.
191
232
 
192
- The runtime does not ship an event schema. Types are yours.
233
+ Full options, `click` clock rule, Node-only `captureFrame`, remount, and the reproduce command: [headless harness](docs/headless-harness.md).
193
234
 
194
- - Host → cart: `cart.dispatch({ type, payload })`. The cart opts in by calling `hostChannel.consume()` (typically in `update`). Unconsumed events keep the newest 32.
195
- - Cart → host: `hostChannel.emit({ type, payload })`. Pass `onEvent` at `mount` to hear them.
235
+ ```ts
236
+ import { createHeadlessHarness } from '@cyberart-io/engine';
237
+
238
+ const harness = createHeadlessHarness({ cart: artProject, seed: 42 });
239
+ await harness.step(5);
240
+ harness.click(x, y); // pointer-down, canvas pixels, next frame
241
+ await harness.step(1);
242
+ const { state, events } = await harness.inspect();
243
+ harness.destroy();
244
+ ```
245
+
246
+ ## Events
247
+
248
+ Default path: per-runtime **mailbox** (`dispatch` / `consume` / `emit`). Multi-cart hosts attach each `HostChannel` to `createEventRouter` — carts never see the router.
196
249
 
197
250
  ```ts
198
251
  const cart = runtime.mount(artProject, {
@@ -202,11 +255,10 @@ const cart = runtime.mount(artProject, {
202
255
  }
203
256
  },
204
257
  });
205
-
206
258
  cart.dispatch({ type: 'art-project.theme', payload: 'dusk' });
207
259
  ```
208
260
 
209
- Carts that never mention `HostChannel` ignore both directions.
261
+ Envelope, `attach` / `detach` / `publish` / `turn`, hops, idempotency, budgets, and rejection reasons: [events and router](docs/events.md).
210
262
 
211
263
  ## Save and load
212
264
 
@@ -258,7 +310,7 @@ const cart = runtime.mount(artProject, {
258
310
  });
259
311
  ```
260
312
 
261
- Pass `captureKeyboard: true` on `createRuntime` or the keys never reach the cart. Without `persist: 'localStorage'`, the host adapter ignores the events.
313
+ Pass `captureKeyboard: true` on `createRuntime` or the keys never reach the cart. Without `persist: 'localStorage'`, the host adapter ignores the events. W/E emits `cyberart.state.save` / `cyberart.state.load`; those names are historical — they are intents, not authoritative state events.
262
314
 
263
315
  Slots are `localStorage['cyberart.state.' + cartId]`, or per-hash when the cart is generative. JSON can persist even if a framebuffer write fails.
264
316
 
@@ -281,7 +333,7 @@ A canvas the host adopted is left in place on destroy; a canvas the engine creat
281
333
 
282
334
  ## Publishing this package (maintainers)
283
335
 
284
- Not part of writing a cart. The npm tarball is built from `packages/engine/src/index.ts` and contains minified `dist/index.js`, rolled-up `dist/index.d.ts`, `LICENSE`, `README.md`, and `package.json`.
336
+ Not part of writing a cart. Engine source lives in `packages/engine/src/` (not mixed into the site). The npm tarball is built from `packages/engine/src/index.ts` and contains minified `dist/index.js`, rolled-up `dist/index.d.ts`, `LICENSE`, `README.md`, `docs/` (event / deterministic / harness API), and `package.json`. Site code that still imports `src/ui/lib/...` hits thin re-export shims so those paths keep working.
285
337
 
286
338
  ```bash
287
339
  pnpm run pack:engine
package/dist/index.d.ts CHANGED
@@ -43,6 +43,23 @@ type TokenData = {
43
43
  /** Same idea for Arweave-typed external assets. We don't currently use it. */
44
44
  preferredArweaveGateway?: string;
45
45
  };
46
+ /**
47
+ * Canonical token hash for `Random` / Art Blocks.
48
+ *
49
+ * A full 64-hex hash (optional `0x`) is returned unchanged aside from a
50
+ * lowercase `0x` prefix — this is the kaleidoscope / Art Blocks path.
51
+ * Any other seed (including the Adventure Kit example `42`) is mixed into
52
+ * a 64-hex hash so `Random` always sees the same shape.
53
+ */
54
+ declare function canonicalizeSeed(seed: string | number): string;
55
+ /**
56
+ * Seed policy for `createRuntime`.
57
+ *
58
+ * Live kaleidoscope / Art Blocks must keep a caller-supplied 64-hex hash (and
59
+ * a short `?hash=` string as `0x…`, not remixed). Deterministic mode and
60
+ * numeric seeds such as `42` go through `canonicalizeSeed`.
61
+ */
62
+ declare function resolveRuntimeSeed(seed: string | number, mode: 'live' | 'deterministic'): string;
46
63
 
47
64
  /**
48
65
  * Copyright (c) 2026 Aaron Boyarsky
@@ -50,11 +67,29 @@ type TokenData = {
50
67
  * See packages/engine/LICENSE
51
68
  */
52
69
 
70
+ type Sfc32Regs = {
71
+ a: number;
72
+ b: number;
73
+ c: number;
74
+ d: number;
75
+ };
76
+ /** Snapshot of the dual sfc32 generators after warmup (or after `setState`). */
77
+ type RandomState = {
78
+ seed: string;
79
+ useA: boolean;
80
+ prngA: Sfc32Regs;
81
+ prngB: Sfc32Regs;
82
+ };
53
83
  declare class Random {
84
+ readonly seed: string;
54
85
  private useA;
55
86
  private prngA;
56
87
  private prngB;
88
+ private genA;
89
+ private genB;
57
90
  constructor(tokenData: TokenData);
91
+ getState(): RandomState;
92
+ setState(state: RandomState): void;
58
93
  r_zero_one(): number;
59
94
  dec(min?: number, max?: number): number;
60
95
  int(min: number, max?: number): number;
@@ -86,12 +121,18 @@ type KeypressHandler = () => void;
86
121
  declare class KeyboardManager {
87
122
  private actionMap;
88
123
  private debugMode;
124
+ private listening;
89
125
  constructor(debugMode?: boolean, captureKeyboard?: boolean);
90
126
  /**
91
127
  * When a key is pressed, check if there's a corresponding action, and execute it.
92
128
  * @param e
93
129
  */
94
130
  checkKeypress({ key }: KeyboardEvent): void;
131
+ /**
132
+ * Inject a key without a DOM event. Deterministic hosts call this at a
133
+ * chosen frame instead of listening on `window`.
134
+ */
135
+ inject(key: string): void;
95
136
  /**
96
137
  * Register a certain action to be performed when a given key is pressed.
97
138
  * @param key
@@ -116,22 +157,110 @@ type PointerClick = {
116
157
  x: number;
117
158
  y: number;
118
159
  };
160
+ type PointerManagerOptions = {
161
+ /**
162
+ * When false, do not attach canvas pointer listeners. Deterministic mode
163
+ * injects coordinates instead of reading the live pointer.
164
+ */
165
+ listen?: boolean;
166
+ };
119
167
  declare class PointerManager {
120
168
  x: number;
121
169
  y: number;
122
170
  isDown: boolean;
123
171
  private clicks;
124
172
  private canvas;
125
- constructor(canvas: HTMLCanvasElement);
173
+ private listening;
174
+ constructor(canvas: HTMLCanvasElement, options?: PointerManagerOptions);
126
175
  private toCanvasCoords;
127
176
  private onPointerDown;
128
177
  private onPointerMove;
129
178
  private onPointerUp;
130
179
  hasClick(): boolean;
131
180
  consumeClick(): PointerClick | null;
181
+ /**
182
+ * Inject a pointer sample in canvas pixel space. Deterministic hosts call
183
+ * this at a chosen frame instead of waiting on DOM pointer events.
184
+ */
185
+ inject(kind: 'down' | 'move' | 'up', x: number, y: number): void;
132
186
  destroy(): void;
133
187
  }
134
188
 
189
+ /**
190
+ * Copyright (c) 2026 Aaron Boyarsky
191
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
192
+ * See packages/engine/LICENSE
193
+ *
194
+ * Versioned envelope for routed host ↔ cart events. Unattached HostChannel
195
+ * mailboxes still accept thin `{ type, payload }` events; this module is the
196
+ * schema the EventRouter normalizes to.
197
+ */
198
+ declare const EVENT_ENVELOPE_VERSION: 1;
199
+ type EventKind = 'intent' | 'state' | 'diagnostic';
200
+ type RejectionReason = 'malformed' | 'unauthorized' | 'host-rejected' | 'rate-limited' | 'loop-detected' | 'storm-detected' | 'unknown-target' | 'not-subscribed' | 'hop-limit';
201
+ declare const REJECTED_EVENT_TYPE = "cyberart.diagnostic.rejected";
202
+ /** Remaining hops on a new root event; each caused follow-up decrements by 1. */
203
+ declare const DEFAULT_MAX_HOPS = 8;
204
+ type EventEnvelope = {
205
+ schemaVersion: typeof EVENT_ENVELOPE_VERSION;
206
+ type: string;
207
+ kind: EventKind;
208
+ source: string;
209
+ target?: string;
210
+ id: string;
211
+ correlationId: string;
212
+ causationId?: string;
213
+ seq: number;
214
+ hops: number;
215
+ idempotencyKey?: string;
216
+ payload?: unknown;
217
+ };
218
+ type EventInput = {
219
+ type: string;
220
+ payload?: unknown;
221
+ schemaVersion?: number;
222
+ kind?: EventKind;
223
+ source?: string;
224
+ target?: string;
225
+ id?: string;
226
+ correlationId?: string;
227
+ causationId?: string;
228
+ seq?: number;
229
+ hops?: number;
230
+ idempotencyKey?: string;
231
+ };
232
+ type RejectionPayload = {
233
+ reason: RejectionReason;
234
+ detail?: string;
235
+ eventType: string;
236
+ source: string;
237
+ };
238
+ type NormalizeContext = {
239
+ source: string;
240
+ createId: () => string;
241
+ seq: number;
242
+ causationId?: string;
243
+ maxHops?: number;
244
+ parentHops?: number;
245
+ };
246
+ type NormalizeSuccess = {
247
+ ok: true;
248
+ envelope: EventEnvelope;
249
+ };
250
+ type NormalizeFailure = {
251
+ ok: false;
252
+ reason: 'malformed';
253
+ detail: string;
254
+ };
255
+ type NormalizeResult = NormalizeSuccess | NormalizeFailure;
256
+ /**
257
+ * `kind` on the input wins. Otherwise persistence types are intents, then
258
+ * the second dotted segment of `type` if it is a known kind.
259
+ */
260
+ declare function inferEventKind(input: EventInput): EventKind | undefined;
261
+ declare function matchEventPattern(pattern: string, type: string): boolean;
262
+ declare function normalizeEvent(input: EventInput, context: NormalizeContext): NormalizeResult;
263
+
135
264
  /**
136
265
  * Copyright (c) 2026 Aaron Boyarsky
137
266
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -142,11 +271,12 @@ declare class PointerManager {
142
271
  * `emit`ted by the cart and forwarded to host `onEvent` listeners.
143
272
  *
144
273
  * Carts that never mention this object behave as they do today.
274
+ *
275
+ * Routed traffic is a full EventEnvelope; thin `{ type, payload }` remains
276
+ * valid on an unattached mailbox. Extra envelope fields are optional here.
145
277
  */
146
- type HostEvent = {
147
- type: string;
148
- payload?: unknown;
149
- };
278
+
279
+ type HostEvent = EventInput;
150
280
  type HostEventListener = (event: HostEvent) => void;
151
281
  declare class HostChannel {
152
282
  private inbound;
@@ -158,6 +288,25 @@ declare class HostChannel {
158
288
  clear(): void;
159
289
  }
160
290
 
291
+ /**
292
+ * Copyright (c) 2026 Aaron Boyarsky
293
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
294
+ * See packages/engine/LICENSE
295
+ *
296
+ * Time source for animation. Production uses the wall clock (`performance.now`);
297
+ * deterministic mode uses a host-advanced virtual clock so replays do not
298
+ * depend on rAF, `setTimeout`, or wall time.
299
+ */
300
+ type Clock = {
301
+ now(): number;
302
+ };
303
+ type VirtualClock = Clock & {
304
+ set(ms: number): void;
305
+ advance(ms: number): void;
306
+ };
307
+ declare function createWallClock(): Clock;
308
+ declare function createVirtualClock(origin?: number): VirtualClock;
309
+
161
310
  /**
162
311
  * Copyright (c) 2026 Aaron Boyarsky
163
312
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -248,6 +397,69 @@ declare class IncompatibleCartStateError extends Error {
248
397
  constructor(message: string);
249
398
  }
250
399
 
400
+ /**
401
+ * Copyright (c) 2026 Aaron Boyarsky
402
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
403
+ * See packages/engine/LICENSE
404
+ *
405
+ * Host-controlled time, input, and asset completion for deterministic replays.
406
+ * Production kaleidoscope / Art Blocks playback does not enable this mode.
407
+ */
408
+
409
+ declare const ASSET_READY_EVENT = "cyberart.asset.ready";
410
+ declare const ASSET_FAILED_EVENT = "cyberart.asset.failed";
411
+ type PointerKind = 'down' | 'move' | 'up';
412
+ type ScriptedAction = {
413
+ atFrame: number;
414
+ } & ({
415
+ type: 'pointer';
416
+ pointer: {
417
+ kind: PointerKind;
418
+ x: number;
419
+ y: number;
420
+ };
421
+ } | {
422
+ type: 'key';
423
+ key: string;
424
+ } | {
425
+ type: 'event';
426
+ event: HostEvent;
427
+ } | {
428
+ type: 'asset';
429
+ id: string;
430
+ status: 'ready' | 'failed';
431
+ detail?: unknown;
432
+ });
433
+ type DeterministicRuntimeOptions = {
434
+ /** Virtual clock origin in ms. Default 0. */
435
+ origin?: number;
436
+ /** Actions applied at the start of `atFrame`, before `update`. */
437
+ actions?: ScriptedAction[];
438
+ };
439
+ type ClockSnapshot = {
440
+ now: number;
441
+ framesElapsed: number;
442
+ frameRate: number;
443
+ };
444
+ type ReplayMetadata = {
445
+ seed: string;
446
+ clock: ClockSnapshot;
447
+ rng: RandomState;
448
+ actions: ScriptedAction[];
449
+ applied: AppliedAction[];
450
+ events: HostEvent[];
451
+ state: unknown;
452
+ };
453
+ type AppliedAction = {
454
+ frame: number;
455
+ action: ScriptedAction;
456
+ };
457
+ /**
458
+ * Compare two replay captures. Empty array means identical; otherwise each
459
+ * string names the first disagreement on that field.
460
+ */
461
+ declare function describeReplayMismatch(a: ReplayMetadata, b: ReplayMetadata): string[];
462
+
251
463
  /**
252
464
  * Copyright (c) 2026 Aaron Boyarsky
253
465
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -262,11 +474,16 @@ type FrameErrorInfo = {
262
474
  type CreateRuntimeOptions = {
263
475
  /** Required mount point. The runtime creates or adopts a canvas inside this element. */
264
476
  container: HTMLElement;
265
- /** Optional token hash (`0x` + 64 hex). Instance-local; does not clobber an existing global cache. */
266
- seed?: string;
477
+ /**
478
+ * Token hash (`0x` + 64 hex) or any seed mixed into one. Instance-local;
479
+ * does not clobber an existing global cache. Kaleidoscope / Art Blocks
480
+ * pass the platform hash unchanged.
481
+ */
482
+ seed?: string | number;
267
483
  /**
268
484
  * When true, the cart listens for window keydown (full-page players).
269
485
  * Defaults to false so an embed does not steal keys from the host.
486
+ * Ignored when `deterministic` is set — input is injected per frame.
270
487
  */
271
488
  captureKeyboard?: boolean;
272
489
  /**
@@ -274,6 +491,12 @@ type CreateRuntimeOptions = {
274
491
  * (click can beat idle prebuild). After mount, `cart.metadata.audio` wins.
275
492
  */
276
493
  audio?: AudioLibrarySpec;
494
+ /**
495
+ * Host-controlled time, input, and asset events. Production playback
496
+ * (kaleidoscope locally and `build:art`) leaves this unset so rAF and the
497
+ * token hash drive the piece as they do today.
498
+ */
499
+ deterministic?: boolean | DeterministicRuntimeOptions;
277
500
  };
278
501
  type MountOptions<T = unknown> = {
279
502
  /** Boot overrides passed as `customState` into `getDefaultState`. Not a live-state replay. */
@@ -310,6 +533,18 @@ type CartHandle = {
310
533
  peekExportedFramebuffer(): ImageData | null;
311
534
  peekSeed(): string | undefined;
312
535
  isGenerative(): boolean;
536
+ /**
537
+ * Run `frames` ticks on the virtual clock. Requires `deterministic`.
538
+ * Does not use rAF; live kaleidoscope playback never calls this.
539
+ */
540
+ step(frames?: number): Promise<void>;
541
+ /** Run the frames that span `ms` at the cart frame rate. Requires `deterministic`. */
542
+ advance(ms: number): Promise<void>;
543
+ /** Queue a scripted input/asset/host event for a future frame. */
544
+ schedule(action: ScriptedAction): void;
545
+ getClock(): ClockSnapshot;
546
+ getRandomState(): RandomState;
547
+ getReplayMetadata(): Promise<ReplayMetadata>;
313
548
  readonly canvas: HTMLCanvasElement | undefined;
314
549
  paused: boolean;
315
550
  readonly tokenData: TokenData;
@@ -327,6 +562,68 @@ type CyberArtRuntime = {
327
562
  };
328
563
  declare function createRuntime(options: CreateRuntimeOptions): CyberArtRuntime;
329
564
 
565
+ /**
566
+ * Copyright (c) 2026 Aaron Boyarsky
567
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
568
+ * See packages/engine/LICENSE
569
+ *
570
+ * CI / agent harness around production `createRuntime({ deterministic })`.
571
+ * `installHeadlessCanvas` is test-only — do not call it from Player or kaleidoscope.
572
+ */
573
+
574
+ /** 1×1 PNG so `captureFrame(path)` writes a file that actually opens. */
575
+ declare const HEADLESS_PNG_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
576
+ declare const DEFAULT_HEADLESS_WIDTH = 320;
577
+ declare const DEFAULT_HEADLESS_HEIGHT = 180;
578
+ /**
579
+ * Documented jsdom canvas install. Mutates `HTMLCanvasElement.prototype`.
580
+ * Idempotent. Not for production playback.
581
+ */
582
+ declare function installHeadlessCanvas(): void;
583
+ type HeadlessFrameError = {
584
+ error: unknown;
585
+ info: FrameErrorInfo;
586
+ };
587
+ type HeadlessInspect = {
588
+ state: unknown;
589
+ events: HostEvent[];
590
+ errors: HeadlessFrameError[];
591
+ replay: ReplayMetadata;
592
+ clock: ClockSnapshot;
593
+ };
594
+ type CreateHeadlessHarnessOptions<T = unknown> = {
595
+ cart: AnimationCart<T>;
596
+ seed?: CreateRuntimeOptions['seed'];
597
+ width?: number;
598
+ height?: number;
599
+ /** Virtual clock origin in ms. Default 0. */
600
+ origin?: number;
601
+ actions?: ScriptedAction[];
602
+ initialState?: Partial<T>;
603
+ gameManager?: unknown;
604
+ onEvent?: HostEventListener;
605
+ onError?: (error: unknown, info: FrameErrorInfo) => void;
606
+ };
607
+ type HeadlessHarness<T = unknown> = {
608
+ readonly runtime: CyberArtRuntime;
609
+ readonly container: HTMLElement;
610
+ readonly events: readonly HostEvent[];
611
+ readonly errors: readonly HeadlessFrameError[];
612
+ readonly cart: CartHandle;
613
+ step(frames?: number): Promise<void>;
614
+ advance(ms: number): Promise<void>;
615
+ schedule(action: ScriptedAction): void;
616
+ dispatch(event: HostEvent): void;
617
+ key(key: string): void;
618
+ /** Pointer-down at the next frame. Use `schedule` for move/up. Canvas pixels, not CSS. */
619
+ click(x: number, y: number): void;
620
+ inspect(): Promise<HeadlessInspect>;
621
+ captureFrame(path?: string): Promise<CartSnapshot>;
622
+ remount(options?: MountOptions<T>): CartHandle;
623
+ destroy(): void;
624
+ };
625
+ declare function createHeadlessHarness<T>(options: CreateHeadlessHarnessOptions<T>): HeadlessHarness<T>;
626
+
330
627
  /**
331
628
  * Copyright (c) 2026 Aaron Boyarsky
332
629
  * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
@@ -374,4 +671,49 @@ type CartStateHotkeyOptions = {
374
671
  */
375
672
  declare function registerCartStateHotkeys(keyboardManager: KeyboardManager, hostChannel: HostChannel, options?: CartStateHotkeyOptions): void;
376
673
 
377
- export { type AnimationCart, type AnimationTiming, type AudioLibraryId, type AudioLibrarySpec, CYBERART_CANVAS_ATTR, type CartHandle, type CartSnapshot, type CartStateBundle, type CartStateHotkeyOptions, type CartStateMessageHandler, type CartStatePersister, type CreateRuntimeOptions, type CyberArtRuntime, type DimensionContext, type FrameErrorInfo, HostChannel, type HostEvent, type HostEventListener, type ImportCartStateExtras, IncompatibleCartStateError, KeyboardManager, type MountOptions, type PointerClick, PointerManager, Random, type TokenData, attachCartStatePersistence, createRuntime, registerCartStateHotkeys };
674
+ /**
675
+ * Copyright (c) 2026 Aaron Boyarsky
676
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
677
+ * See packages/engine/LICENSE
678
+ *
679
+ * Host-side event router. Carts keep a dumb HostChannel mailbox; the host
680
+ * attaches those channels here so events are permissioned, budgeted, and
681
+ * loop-checked before they reach another cart. Carts never receive this
682
+ * object.
683
+ */
684
+
685
+ type ValidateResult = true | {
686
+ reason: 'host-rejected';
687
+ detail?: string;
688
+ };
689
+ type AttachOptions = {
690
+ emit?: string[];
691
+ subscribe?: string[];
692
+ authoritative?: boolean;
693
+ };
694
+ type EventRouterOptions = {
695
+ validate?: (event: EventEnvelope) => ValidateResult;
696
+ createId?: () => string;
697
+ now?: () => number;
698
+ hostSource?: string;
699
+ maxPerTurn?: number;
700
+ maxPerWindow?: number;
701
+ windowMs?: number;
702
+ maxCausationDepth?: number;
703
+ maxHops?: number;
704
+ maxCorrelationPerTurn?: number;
705
+ maxIndex?: number;
706
+ };
707
+ type PublishExtras = {
708
+ cause?: EventEnvelope;
709
+ };
710
+ type EventRouter = {
711
+ attach(id: string, channel: HostChannel, options?: AttachOptions): void;
712
+ detach(id: string): void;
713
+ publish(event: EventInput, extras?: PublishExtras): EventEnvelope | undefined;
714
+ subscribe(patterns: string[], listener: (event: EventEnvelope) => void): () => void;
715
+ turn(): void;
716
+ };
717
+ declare function createEventRouter(options?: EventRouterOptions): EventRouter;
718
+
719
+ export { ASSET_FAILED_EVENT, ASSET_READY_EVENT, type AnimationCart, type AnimationTiming, type AppliedAction, type AttachOptions, type AudioLibraryId, type AudioLibrarySpec, CYBERART_CANVAS_ATTR, type CartHandle, type CartSnapshot, type CartStateBundle, type CartStateHotkeyOptions, type CartStateMessageHandler, type CartStatePersister, type Clock, type ClockSnapshot, type CreateHeadlessHarnessOptions, type CreateRuntimeOptions, type CyberArtRuntime, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, DEFAULT_MAX_HOPS, type DeterministicRuntimeOptions, type DimensionContext, EVENT_ENVELOPE_VERSION, type EventEnvelope, type EventInput, type EventKind, type EventRouter, type EventRouterOptions, type FrameErrorInfo, HEADLESS_PNG_DATA_URL, type HeadlessFrameError, type HeadlessHarness, type HeadlessInspect, HostChannel, type HostEvent, type HostEventListener, type ImportCartStateExtras, IncompatibleCartStateError, KeyboardManager, type MountOptions, type NormalizeContext, type NormalizeResult, type PointerClick, PointerManager, type PublishExtras, REJECTED_EVENT_TYPE, Random, type RandomState, type RejectionPayload, type RejectionReason, type ReplayMetadata, type ScriptedAction, type TokenData, type ValidateResult, type VirtualClock, attachCartStatePersistence, canonicalizeSeed, createEventRouter, createHeadlessHarness, createRuntime, createVirtualClock, createWallClock, describeReplayMismatch, inferEventKind, installHeadlessCanvas, matchEventPattern, normalizeEvent, registerCartStateHotkeys, resolveRuntimeSeed };
package/dist/index.js CHANGED
@@ -5,4 +5,4 @@
5
5
  * Not an OSI open-source license.
6
6
  */
7
7
 
8
- var Ft=(e,t)=>()=>(e&&(t=e(e=0)),t);var jt={};function Kt(e,t){let n=e.indexOf(yt);if(n===-1)return null;let r=n+yt.length,i=`const __cyberSkip=new Set(${JSON.stringify(t)});const __cyberOrig=registerProcessor;registerProcessor=(n,p)=>__cyberSkip.has(n)?undefined:__cyberOrig(n,p);`;return e.slice(0,r)+i+e.slice(r)}var Y,U,Ut,_,yt,wt=Ft(()=>{"use strict";Y=/AudioWorkletProcessor with name:\s*["'`][^"'`]+["'`]\s+is already registered/i,U="__cyberartWorkletPatchApplied";typeof window<"u"&&!window[U]&&(window.addEventListener("error",e=>{let t=e?.message??"";Y.test(t)&&(e.preventDefault(),e.stopImmediatePropagation())},!0),window.addEventListener("unhandledrejection",e=>{let t=e?.reason,n=typeof t=="string"?t:t?.message??"";Y.test(n)&&e.preventDefault()}),window[U]=!0);Ut=!1,_=(...e)=>{Ut&&console.log("[workletPatch]",...e)},yt="((AudioWorkletProcessor,registerProcessor)=>{";if(typeof AudioWorklet<"u"&&!AudioWorklet.prototype[U]){let e=new WeakMap,t=/registerProcessor\s*\(\s*['"`]([^'"`]+)['"`]/g,n=AudioWorklet.prototype.addModule,r=0;AudioWorklet.prototype.addModule=async function(i,s){let o=++r,u=e.get(this);u||(u=new Set,e.set(this,u)),_(`#${o} addModule`,{url:i.slice(0,64),isBlob:i.startsWith("blob:"),knownBefore:[...u]});let l=i,c=null;if(i.startsWith("blob:")){let h=null;try{h=await(await fetch(i)).text()}catch(d){_(`#${o} blob fetch failed`,d)}if(h!==null){let d=new Set;for(let m of h.matchAll(t))d.add(m[1]);if(d.size>0){let m=[...d].filter(C=>u.has(C)),S=[...d].filter(C=>!u.has(C));if(m.length>0&&S.length===0){_(`#${o} short-circuit \u2014 all processors already registered`,m);return}if(m.length>0&&S.length>0){let C=Kt(h,m);if(C!==null){let f=new Blob([C],{type:"application/javascript"});c=URL.createObjectURL(f),l=c,_(`#${o} rewrote blob; skipping`,m,"allowing",S)}else _(`#${o} blob rewrite failed; passing original through`)}S.forEach(C=>u.add(C))}}}return n.call(this,l,s).then(()=>{c&&URL.revokeObjectURL(c)},h=>{c&&URL.revokeObjectURL(c);let d=(h&&h.message)??"";if(!(Y.test(d)||/already registered/i.test(d)))throw h})},AudioWorklet.prototype[U]=!0}});var dt=()=>{let e=["ms","moz","webkit","o"];for(let t=0;t<e.length&&!window.requestAnimationFrame;++t)window.requestAnimationFrame=window[e[t]+"RequestAnimationFrame"],window.cancelAnimationFrame=window[e[t]+"CancelAnimationFrame"]||window[e[t]+"CancelRequestAnimationFrame"]},b=window.devicePixelRatio||1,k=/Headless/i.test(navigator.userAgent),Ae=/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)||/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.platform);function ut(){return Math.min(window.innerWidth,window.innerHeight)}var O="data-cyberart-canvas",T=class{constructor(t,n,r=1,a={}){this.width=t,this.height=n;let i=a.container,s=a.adoptFromDocument??!i,o=i?i.querySelector(`canvas[${O}]`)??i.querySelector("canvas"):null;if(o)this.canvas=o,this.adopted=!0;else if(s){let u=document.getElementById("canvas")??document.querySelector("canvas");u?(this.canvas=u,this.adopted=!0):(this.canvas=document.createElement("canvas"),this.canvas.setAttribute("id","canvas"),this.adopted=!1)}else this.canvas=document.createElement("canvas"),this.adopted=!1;this.canvas.hasAttribute(O)||this.canvas.setAttribute(O,""),this.canvas.style.display="block",this.canvas.style.margin="auto",this.ctx=this.canvas.getContext("2d",{colorSpace:"display-p3",alpha:!1}),this.ctx.globalAlpha=r,this.ctx.imageSmoothingEnabled=!1,this.setSize(t,n)}setSize(t,n){this.width=t,this.height=n,this.canvas.width=t,this.canvas.height=n,this.canvas.style.width=`${t/b}px`,this.canvas.style.height=`${n/b}px`,this.imageData=this.ctx.createImageData(t,n),this.ctx.putImageData(this.imageData,0,0)}_blank(t=1){let n=t?Math.max(t,.03):1;this.ctx.fillStyle=`rgba(0,0,0,${n})`,this.ctx.fillRect(0,0,this.width,this.height)}clearAll(){this.ctx.save(),this.ctx.globalAlpha=1,this._blank(),this.ctx.restore()}};function ct(e,t){let n=e/t,r,a;return n>1?(r=e,a=~~(e/n)):(a=t,r=~~(t*n)),{width:r,height:a,iWidth:1/r,iHeight:1/a,area:r*a,largeDim:Math.max(r,a),smallDim:Math.min(r,a),aspectRatio:n}}function ht(e,t=window.innerWidth,n=window.innerHeight){let r=ut(),a,i;return n<t?e>1?t>=r*e?(a=r*e,i=r):(a=t,i=a/e):(a=r*e,i=r):e>1?(a=r,i=r/e):n<r/e?(i=n,a=i*e):(a=r,i=r/e),[a,i]}var H=class{constructor(t=!1,n=!0){this.debugMode=t,this.actionMap={},this.checkKeypress=this.checkKeypress.bind(this),this.registerAction=this.registerAction.bind(this),n&&window.addEventListener("keydown",this.checkKeypress,!1)}checkKeypress({key:t}){this.debugMode&&console.log(`Pressed ${t}`),t in this.actionMap&&this.actionMap[t]()}registerAction(t,n,r=!1){if(t.length!==1)throw new Error(`Only single-character actions may be registered. Cannot register: ${t}`);if(t in this.actionMap&&!r)throw new Error(`Action already exists for code: ${t}`);this.actionMap[t]=n}destroy(){window.removeEventListener("keydown",this.checkKeypress,!1),this.actionMap={}}},lt=H;var B=class{constructor(t){this.x=-1;this.y=-1;this.isDown=!1;this.clicks=[];this.canvas=t,this.onPointerDown=this.onPointerDown.bind(this),this.onPointerMove=this.onPointerMove.bind(this),this.onPointerUp=this.onPointerUp.bind(this),t.addEventListener("pointerdown",this.onPointerDown),t.addEventListener("pointermove",this.onPointerMove),t.addEventListener("pointerup",this.onPointerUp)}toCanvasCoords(t){let n=this.canvas.getBoundingClientRect();return{x:(t.clientX-n.left)*(this.canvas.width/n.width),y:(t.clientY-n.top)*(this.canvas.height/n.height)}}onPointerDown(t){let{x:n,y:r}=this.toCanvasCoords(t);this.x=n,this.y=r,this.isDown=!0,this.clicks.push({x:n,y:r})}onPointerMove(t){let{x:n,y:r}=this.toCanvasCoords(t);this.x=n,this.y=r}onPointerUp(t){let{x:n,y:r}=this.toCanvasCoords(t);this.x=n,this.y=r,this.isDown=!1}hasClick(){return this.clicks.length>0}consumeClick(){return this.clicks.shift()??null}destroy(){this.canvas.removeEventListener("pointerdown",this.onPointerDown),this.canvas.removeEventListener("pointermove",this.onPointerMove),this.canvas.removeEventListener("pointerup",this.onPointerUp)}},pt=B;var mt=(e,t,n,r)=>({now:e,startTime:t,elapsedSinceStart:e-t,deltaSinceLastUpdate:n===null?0:e-n,deltaSinceLastRender:r===null?0:e-r}),N=class{constructor(t,n,r,a,i,s={}){this.startTime=0;this.lastUpdateAt=null;this.lastRenderAt=null;this.clockOffsetMs=0;this.pauseStartedAt=null;this.renderAnimation=this.renderAnimation.bind(this),this.dimensionContext=ct(t,n),this.R=r,this.gameManager=s.gameManager,this.hostChannel=s.hostChannel,this.canvas=new T(this.dimensionContext.width,this.dimensionContext.height,1,s.canvasMount),this.drawingContext=this.canvas.ctx,this.keyboardManager=new lt(!1,s.captureKeyboard??!0),this.pointerManager=new pt(this.canvas.canvas),this.animationCart=i;let o=s.customState;this.featureState=i.getDefaultFeatureState?.(this.R,this.dimensionContext,a,this.keyboardManager,o,this.pointerManager,this.gameManager,this.hostChannel),this.cartState=i.getDefaultState(this.R,this.dimensionContext,a,this.keyboardManager,o,this.pointerManager,this.gameManager,this.featureState,this.hostChannel),this.startTime=performance.now(),this.canvas.clearAll()}captureFramebuffer(){let t=this.canvas.imageData,n=new ImageData(t.width,t.height);return n.data.set(t.data),n}restoreFramebuffer(t){let n=this.canvas.imageData;return n.width!==t.width||n.height!==t.height||n.data.length!==t.data.length?!1:(n.data.set(t.data),this.drawingContext.putImageData(n,0,0),!0)}setPaused(t){let n=performance.now();if(t){this.pauseStartedAt===null&&(this.pauseStartedAt=n);return}this.pauseStartedAt!==null&&(this.clockOffsetMs+=n-this.pauseStartedAt,this.pauseStartedAt=null)}resolveNow(t){let n=this.pauseStartedAt!==null?t-this.pauseStartedAt:0;return t-this.clockOffsetMs-n}getElapsedSinceStart(t=performance.now()){return Math.max(0,this.resolveNow(t)-this.startTime)}restoreClock(t,n){this.pauseStartedAt=null,this.lastUpdateAt=null,this.lastRenderAt=null;let r=Math.max(0,t);if(typeof n=="number"&&Number.isFinite(n)){this.startTime=n,this.clockOffsetMs=performance.now()-n-r;return}this.clockOffsetMs=0,this.startTime=performance.now()-r}async updateAnimationState(t,n,r=performance.now()){let a=this.resolveNow(r),i=mt(a,this.startTime,this.lastUpdateAt,this.lastRenderAt);this.cartState=this.animationCart.update(this.R,t,n,this.dimensionContext,this.cartState,this.keyboardManager,this.pointerManager,this.gameManager,i,this.featureState,this.hostChannel),this.lastUpdateAt=a}async renderAnimation(t,n,r=performance.now()){let a=this.resolveNow(r),i=mt(a,this.startTime,this.lastUpdateAt,this.lastRenderAt);this.animationCart.render(this.R,t,n,this.dimensionContext,this.cartState,this.drawingContext,this.canvas.imageData,this.pointerManager,this.gameManager,i,this.featureState,this.hostChannel),this.lastRenderAt=a}getCanvas(){return this.canvas.canvas}getDrawDetails(){return[this.canvas.canvas,0,0,this.dimensionContext.width,this.dimensionContext.height]}teardown(){try{this.animationCart.teardown?.(this.cartState,this.featureState)}catch(t){console.error("Error during cart teardown",t)}this.keyboardManager.destroy(),this.pointerManager.destroy()}};var W=class extends T{constructor(t,n,r=1,a=!0,i={}){if(super(t,n,r,i),this.adopted)return;let s=i.container;if(s){this.canvas.parentNode!==s&&s.appendChild(this.canvas);return}if(a){let o=document.getElementById("canvas");o&&o!==this.canvas&&o.parentNode===document.body&&document.body.removeChild(o),document.body.appendChild(this.canvas)}}};var I=class{constructor(t){this.useA=!1;let n=function(r){let a=parseInt(r.substring(0,8),16),i=parseInt(r.substring(8,8),16),s=parseInt(r.substring(16,8),16),o=parseInt(r.substring(24,8),16);return function(){a|=0,i|=0,s|=0,o|=0;let u=(a+i|0)+o|0;return o=o+1|0,a=i^i>>>9,i=s+(s<<3)|0,s=s<<21|s>>>11,s=s+u|0,(u>>>0)/4294967296}};this.prngA=n(t.hash.substring(2,32)),this.prngB=n(t.hash.substring(34,32));for(let r=0;r<1e6;r+=2)this.prngA(),this.prngB()}r_zero_one(){return this.useA=!this.useA,this.useA?this.prngA():this.prngB()}dec(t,n){return t===void 0?this.r_zero_one():n===void 0?t*this.r_zero_one():t+(n-t)*this.r_zero_one()}int(t,n){return n===void 0&&(n=t,t=0),Math.floor(this.dec(t,n))}bool(t=.5){return this.r_zero_one()<t}sign(){return this.bool()?1:-1}choose(t){return t[this.int(t.length)]}};var gt="bafybeicjwcq5lxxtfnyj4p2ugcr7ctyb5wl62cbcechj6tsabdq5whcs7u",ft="https://ipfs.io/ipfs/";function Ot(e){return e.endsWith("/")?e:`${e}/`}var L=null;function Ht(e){return[...Array(e)].map(()=>Math.floor(Math.random()*16).toString(16)).join("")}function Bt(e){return e&&e.length>0?e:[{cid:gt}]}function Nt(e){return e.startsWith("0x")?e:`0x${e}`}function Wt(e){if(!e&&typeof tokenData<"u"&&tokenData?.hash)return{hash:tokenData.hash,tokenId:String(tokenData.tokenId),externalAssetDependencies:Bt(tokenData.externalAssetDependencies),preferredIPFSGateway:Ot(tokenData.preferredIPFSGateway||ft),preferredArweaveGateway:tokenData.preferredArweaveGateway};let t=typeof window<"u"?new URLSearchParams(window.location.search):new URLSearchParams;return{hash:e?Nt(e):t.get("hash")||`0x${Ht(64)}`,tokenId:"18009999",externalAssetDependencies:[{cid:gt}],preferredIPFSGateway:ft}}function q(e){let t=e?.useCache!==!1;if(t&&L&&!e?.hash)return L;let n=Wt(e?.hash);return t?L=n:L||(L=n),n}function bt(e){return new Array(32).fill(null).map((t,n)=>parseInt(e.hash.slice(2+n*2,4+n*2),16))}async function J(){let e=await import("tone");if(typeof e.start=="function")return e;let t=e.default;return t&&typeof t.start=="function"?t:e}var St={id:"tone",async load(){await Promise.resolve().then(()=>(wt(),jt))},async unlock(){await St.load(),await(await J()).start()},async resumeIfSuspended(){let e=await J();e.context.state==="suspended"&&await e.context.resume()},async suspendIfRunning(){let t=(await J()).context.rawContext;"suspend"in t&&t.state==="running"&&await t.suspend()}},Ct={tone:St};function X(e){if(!e)return[];let t=Array.isArray(e)?e:[e],n=[];for(let r of t){if(!(r in Ct)){console.warn(`Unknown audio library "${r}"`);continue}let a=r;n.includes(a)||n.push(a)}return n}function Q(e){return e.map(t=>Ct[t])}var K="$cyberartTa",g=class extends Error{constructor(t){super(t),this.name="IncompatibleCartStateError"}},Z={Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array};function Vt(e){for(let t of Object.keys(Z))if(e instanceof Z[t])return t}function At(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}function y(e){if(e===null||typeof e!="object"||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function tt(e){return y(e)?typeof e[K]=="string"&&Array.isArray(e.d):!1}function $t(e){return e==null?!1:typeof e=="function"?!0:!(typeof e!="object"||Array.isArray(e)||At(e)||y(e))}var Gt=new Set(["audioContextStarted","midiSynths","keepRatioPrimed","audioInitStarted","musicSeekApplied"]);function vt(e){return e===null?"null":tt(e)?String(e[K]):Array.isArray(e)?"array":y(e)?"object":typeof e}function zt(e,t){return e===t||e==="null"||t==="null"}function A(e){return e.trim().toLowerCase().replace(/^0x/,"")}function Et(e,t,n,r){if(!(!n&&!r)&&A(e)!==A(t))throw new g(`Generative cart state is for seed "${t}", but the live token is "${e}"`)}function P(e){return typeof e=="number"&&Number.isFinite(e)&&e>0}function xt(e,t){let n=e.dimensions;if(n&&P(n.width)&&P(n.height))return n;if(t&&P(t.width)&&P(t.height))return{width:t.width,height:t.height};if(y(e.state)){let r=e.state.containerWidth,a=e.state.containerHeight;if(P(r)&&P(a))return{width:r,height:a}}return null}function D(e,t=new WeakSet){if(e===null||typeof e=="string"||typeof e=="boolean")return e;if(typeof e=="number")return Number.isFinite(e)?e:null;if(!(typeof e=="function"||typeof e>"u"||typeof e=="bigint"||typeof e=="symbol")){if(At(e)){let n=Vt(e);return n?{[K]:n,d:Array.from(e)}:void 0}if(typeof e=="object"&&!t.has(e)){t.add(e);try{if(Array.isArray(e)){let a=[],i=0;for(let s of e){let o=D(s,t);o!==void 0&&(a.push(o),i++)}return e.length>0&&i===0?void 0:a}if(!y(e)||$t(e))return;let n=Object.keys(e),r={};for(let a of n){if(Gt.has(a))continue;let i=D(e[a],t);i!==void 0&&(r[a]=i)}return n.length>0&&Object.keys(r).length===0?void 0:r}finally{t.delete(e)}}}}function F(e){if(tt(e)){let t=Z[e[K]];return t?new t(e.d.map(n=>Number(n))):e}if(Array.isArray(e))return e.map(t=>F(t));if(y(e)){let t={};for(let n of Object.keys(e))t[n]=F(e[n]);return t}return e}function et(e,t,n,r,a=!0){if(a&&n&&r&&n!==r)throw new g(`Cart state is for "${r}", but the live cart is "${n}"`);if(!y(t)){if(a)throw new g("Cart state snapshot is empty");return}let i=Object.keys(t);if(i.length===0){if(a)throw new g("Cart state snapshot is empty");return}let s=D(e);if(!y(s)||Object.keys(s).length===0){if(a)throw new g("Live cart has no serializable state");return}let o=i.filter(u=>u in s);if(o.length===0){if(a)throw new g("Cart state snapshot does not overlap the live cart shape");return}for(let u of o){let l=vt(s[u]),c=vt(t[u]);if(!zt(l,c))throw new g(`Cart state field "${u}" has type ${c}, live cart expects ${l}`);if(l==="object"){let h=t[u],d=e[u];y(h)&&!tt(h)&&et(d,h,void 0,void 0,!1)}}}function kt(e,t){if(!y(e)||!y(t))return;let n=D(e);if(y(n))for(let r of Object.keys(t))r in n&&(e[r]=F(t[r]))}function Tt(e){let t=typeof e=="string"?JSON.parse(e):e;if(!t||typeof t!="object")throw new g("Cart state bundle is not an object");if(t.version!==1)throw new g(`Unsupported cart state version ${String(t.version)}`);if(typeof t.seed!="string"||typeof t.framesElapsed!="number")throw new g("Cart state bundle is missing seed or framesElapsed");return t}function Yt(e){if(!e||typeof e!="object")return 0;let t=e;return typeof t.startTime=="number"&&typeof t.lastSimulationUpdateAt=="number"?Math.max(0,t.lastSimulationUpdateAt-t.startTime):0}function Jt(e,t){if(typeof window.resizeTo!="function")return!1;try{if(window.top!==window)return!1}catch{return!1}let n=Math.max(1,Math.round(e)),r=Math.max(1,Math.round(t)),a=Math.max(0,window.outerWidth-window.innerWidth),i=Math.max(0,window.outerHeight-window.innerHeight);try{window.resizeTo(n+a,r+i)}catch{return!1}return Math.abs(window.innerWidth-n)<=8&&Math.abs(window.innerHeight-r)<=8}var Xt=[[0,0],[16,9],[9,16],[4,3],[3,4],[2,1],[1,2],[3,1],[1,3],[1,1]],rt=Xt[0],Qt=rt[0]/rt[1],nt=30,Zt=120,te=3840/2160,ee=2160/3840,j=class{constructor(t=nt,n=!1,r=window.innerWidth,a=window.innerHeight,i=!0,s,o={}){this.framesElapsed=0;this.frameRate=nt;this._paused=!1;this.savedToken=!1;this.prepared=!1;this.loopRunning=!1;this.fallbackAudio=[];this.audioLibrariesInternal=[];this.consecutiveErrorCount=0;this.lastErrorLogAt=0;this.reinitStayPaused=!1;this.reinitPending=!1;this.lastExportedFramebuffer=null;this.pinnedOutput=null;this.pinViewportAtImport=null;this.importing=!1;this.drawLoop=this.drawLoop.bind(this),this.loadCart=this.loadCart.bind(this),this.prepareCart=this.prepareCart.bind(this),this.beginPlayback=this.beginPlayback.bind(this),this.reinit=this.reinit.bind(this),this.tuneFramerate=this.tuneFramerate.bind(this),this.container=o.container,this.seed=o.seed,this.captureKeyboard=o.captureKeyboard??!0,this.customState=o.customState,this.hostChannel=o.hostChannel,this.fallbackAudio=X(o.audio),this.audioLibrariesInternal=this.fallbackAudio,this.isBodyCanvas=this.container?!1:i,this.autoMode=n,this.aspectRatio=Qt,this.selectedAsp=rt,this.isFullscreen=!!document.fullscreenElement,this.updateFrameRate(t),this.tempUnpause=!1,this.synchronous=k,this.containerWidth=r,this.containerHeight=a,this.gameManager=s,this.init(!0)}setGameManager(t){this.gameManager=t}reinit(t){if(!this.importing){if(this.shouldKeepPinnedOutput()){this.fitPinnedCanvas();return}this.reinitPending||(this.reinitStayPaused=this._paused,this.reinitPending=!0),this.paused=!0,clearTimeout(this.reinitTimeout),this.reinitTimeout=setTimeout(()=>{if(this.importing){this.paused=this.reinitStayPaused,this.reinitPending=!1;return}this.updateInProgress?this.reinit(t):(this.requestedAnimationFrame&&cancelAnimationFrame(this.requestedAnimationFrame),this.clearPinnedOutput(),this.init(),this.reloadCart(),this.paused=this.reinitStayPaused,this.reinitPending=!1)},150)}}initSeed(){this.tokenData=this.seed?q({hash:this.seed,useCache:!1}):q(),this.rawParams=bt(this.tokenData),this.mintNo=parseInt(this.tokenData.tokenId)%1e6,console.log("token",this.mintNo,this.tokenData.hash)}init(t=!1){t&&this.initSeed(),this.pureAspectUpdate(this.selectedAsp);let n=this.aspectRatio,[r,a]=ht(n,this.containerWidth,this.containerHeight);r*=b,a*=b,this.width=~~r,this.height=~~a,this.atCenterX=this.width/2,this.atCenterY=this.height/2,this.mainCanvas?this.mainCanvas.setSize(this.width,this.height):this.mainCanvas=new W(this.width,this.height,1,this.isBodyCanvas,this.canvasMountOptions())}loadCart(t){this.prepareCart(t)&&this.beginPlayback()}prepareCart(t){if(this.prepared)return!0;try{let n=~~(this.width/1),r=~~(this.height/this.width*n),a=new I(this.tokenData),i=new N(n,r,a,this.rawParams,t,{gameManager:this.gameManager,canvasMount:this.canvasMountOptions(),customState:this.customState,captureKeyboard:this.captureKeyboard,hostChannel:this.hostChannel});return this.animation=i,this.animationCart=t,this.paused=!1,this.framesElapsed=0,this.R=a,this.applyCartFrameRate(t,i),this.resolveAudioLibraries(t),this.prepared=!0,!0}catch(n){return console.error(n),!1}}applyCartFrameRate(t,n){let r=t.metadata,a=r?.frameRate??nt,i=n.cartState?.visualMode??n.featureState?.visualMode,s=i&&r?.frameRateByVisualMode?r.frameRateByVisualMode[i]:void 0;this.updateFrameRate(typeof s=="number"&&s>0?s:a)}get audioLibraries(){return this.audioLibrariesInternal}get needsAudio(){return this.audioLibrariesInternal.length>0}resolveAudioLibraries(t){this.audioLibrariesInternal=X(t.metadata?.audio)}async unlockAudio(){if(this.audioLibrariesInternal.length===0)return;let t=Q(this.audioLibrariesInternal);if(k){for(let n of t)await n.load();return}for(let n of t)await n.unlock()}async resumeAudioLibraries(){if(!(this.paused||k))for(let t of Q(this.audioLibrariesInternal))try{await t.resumeIfSuspended()}catch(n){console.error(`Failed to resume audio library "${t.id}" after reload`,n)}}beginPlayback(){!this.prepared||this.loopRunning||(this.prepared=!1,this.loopRunning=!0,this.consecutiveErrorCount=0,this.requestedAnimationFrame=requestAnimationFrame(this.drawLoop))}get isPrepared(){return this.prepared}get isLoopRunning(){return this.loopRunning}get paused(){return this._paused}set paused(t){this._paused!==t&&(this._paused=t,this.animation?.setPaused(t))}async waitUntilUpdateIdle(){for(;this.updateInProgress;)await new Promise(t=>{typeof requestAnimationFrame=="function"?requestAnimationFrame(()=>t()):setTimeout(t,0)})}requirePreparedAnimation(){if(!this.animation)throw new Error("No prepared cart");return this.animation}async exportState(){let t=this.requirePreparedAnimation(),n=this._paused;this.paused=!0,await this.waitUntilUpdateIdle();try{this.lastExportedFramebuffer=t.captureFramebuffer();let r=t.getCanvas();return{version:1,cartId:this.animationCart?.metadata?.id,generative:this.animationCart?.metadata?.generative,seed:this.tokenData.hash,framesElapsed:this.framesElapsed,elapsedSinceStart:t.getElapsedSinceStart(),dimensions:{width:r.width,height:r.height,cssWidth:this.containerWidth,cssHeight:this.containerHeight,dpr:b},state:D(t.cartState)}}finally{this.paused=n}}async exportStateJSON(){return JSON.stringify(await this.exportState())}peekExportedFramebuffer(){return this.lastExportedFramebuffer}peekSeed(){return this.tokenData?.hash}isGenerative(){return this.animationCart?.metadata?.generative===!0}async importState(t,n){let r=this.requirePreparedAnimation(),a=Tt(t),i=this._paused,s=this.loopRunning;this.paused=!0,await this.waitUntilUpdateIdle(),et(r.cartState,a.state,this.animationCart?.metadata?.id,a.cartId),Et(this.tokenData.hash,a.seed,this.animationCart?.metadata?.generative,a.generative),clearTimeout(this.reinitTimeout),this.reinitPending=!1,this.importing=!0;let o=xt(a,n?.framebuffer??this.lastExportedFramebuffer);if(o){this.pinOutputSize(o);let c=o.cssWidth??o.width/b,h=o.cssHeight??o.height/b;Jt(c,h),this.syncContainerFromDom(),this.pinViewportAtImport={w:this.containerWidth,h:this.containerHeight}}let u=this.customState,l=!1;try{if(this.customState=F(a.state),this.unloadCart(),!this.prepareCart(this.animationCart))throw new Error("Failed to prepare cart while loading state");this.animation.cartState!==this.customState&&kt(this.animation.cartState,a.state),this.framesElapsed=a.framesElapsed;let c=this.animation,h=c.cartState,d=typeof a.elapsedSinceStart=="number"?a.elapsedSinceStart:Yt(h);c.restoreClock(d,typeof h?.startTime=="number"?h.startTime:void 0),i&&c.setPaused(!0);let m=n?.framebuffer??this.lastExportedFramebuffer;m&&c.restoreFramebuffer(m)?h.keepRatioPrimed=!0:await c.renderAnimation(this.framesElapsed,this.rawParams),this.fitPinnedCanvas(),l=!0}catch(c){throw this.clearPinnedOutput(),this.customState=u,this.paused=i,this.animationCart&&(this.syncContainerFromDom(),this.init(),this.reloadCart()),c}finally{this.importing=!1,l&&(this.customState=u,this.paused=i,s&&this.beginPlayback())}}unloadCart(){this.loopRunning=!1,this.requestedAnimationFrame&&cancelAnimationFrame(this.requestedAnimationFrame),this.drawLoopTimeout&&clearTimeout(this.drawLoopTimeout),this.animation?.teardown(),this.animation=void 0,this.prepared=!1,this.audioLibrariesInternal=this.fallbackAudio,this.mainCanvas?.clearAll()}reloadCart(t=!1){let n=this.animationCart;if(!n)return;this.pinnedOutput&&(this.clearPinnedOutput(),this.syncContainerFromDom(),this.init());let r=this.reinitPending?this.reinitStayPaused:this._paused;this.unloadCart(),t&&this.initSeed(),this.loadCart(n),this.paused=r,!this.paused&&!k&&this.resumeAudioLibraries()}getFrameRate(){return this.frameRate}updateFrameRate(t){if(this.frameRate=t,t>=1)this.targetDrawTime=1e3/t;else{let r=1/(2-t);this.targetDrawTime=1e3/r}this.drawTime=this.targetDrawTime}canvasMountOptions(){return{container:this.container,adoptFromDocument:!this.container}}syncContainerFromDom(){if(this.container){this.containerWidth=this.container.clientWidth||this.containerWidth,this.containerHeight=this.container.clientHeight||this.containerHeight;return}this.containerWidth=window.innerWidth,this.containerHeight=window.innerHeight}pinOutputSize(t){this.pinnedOutput=t,this.width=~~t.width,this.height=~~t.height,this.atCenterX=this.width/2,this.atCenterY=this.height/2,this.aspectRatio=this.width/this.height,this.mainCanvas&&this.mainCanvas.setSize(this.width,this.height)}shouldKeepPinnedOutput(){return this.pinnedOutput?(this.syncContainerFromDom(),this.viewportMatches(this.pinnedCssSize())?!0:!!(this.pinViewportAtImport&&this.viewportMatches(this.pinViewportAtImport))):!1}pinnedCssSize(){let t=this.pinnedOutput;return{w:t.cssWidth??t.width/b,h:t.cssHeight??t.height/b}}viewportMatches(t){return Math.abs(this.containerWidth-t.w)<8&&Math.abs(this.containerHeight-t.h)<8}fitPinnedCanvas(){let t=this.pinnedOutput,n=this.canvas;if(!t||!n)return;this.container&&!this.container.style.position&&(this.container.style.position="relative");let r=this.containerWidth||window.innerWidth,a=this.containerHeight||window.innerHeight,i=t.width/b,s=t.height/b;if(i<1||s<1||r<1||a<1)return;let o=Math.min(r/i,a/s),u=i*o,l=s*o;n.style.position="absolute",n.style.left="50%",n.style.top="50%",n.style.transform="translate(-50%, -50%)",n.style.margin="0",n.style.width=`${u}px`,n.style.height=`${l}px`,n.style.imageRendering=Math.abs(o-1)<.01?"auto":"pixelated"}clearPinnedOutput(){let t=this.canvas;this.pinnedOutput=null,this.pinViewportAtImport=null,t&&(t.style.position="",t.style.left="",t.style.top="",t.style.transform="",t.style.imageRendering="",t.style.width="",t.style.height="",t.style.margin="auto")}get canvas(){return this.animation?.getCanvas()??this.mainCanvas?.canvas}destroy(){this.clearPinnedOutput(),this.unloadCart(),clearTimeout(this.reinitTimeout),this.mainCanvas&&!this.mainCanvas.adopted&&this.mainCanvas.canvas.remove()}drawIt(){}tuneFramerate(t){let n=this.lastCall||t;this.lastCall=t,this.drawTime=Math.max(0,this.drawTime+this.targetDrawTime-this.lastCall+n)}async drawLoop(t){if(!this.loopRunning)return;let n=!1,r,a="update";try{if(!this.paused||this.tempUnpause){if(k||this.tuneFramerate(t),!this.animation)return;if(a="update",this.updateInProgress=!0,await this.animation.updateAnimationState(this.framesElapsed,this.rawParams,t),this.updateInProgress=!1,a="render",await this.animation.renderAnimation(this.framesElapsed,this.rawParams,t),this.autoMode&&this.animation.cartState.saveToken&&!this.savedToken&&!new URLSearchParams(window.location.search).get("hash")){let o=localStorage.getItem("goodones"),u=JSON.stringify(o?[...JSON.parse(o),this.tokenData.hash]:[this.tokenData.hash]);localStorage.setItem("goodones",u),this.savedToken=!0,localStorage.setItem(this.tokenData.hash,this.mainCanvas.canvas.toDataURL("image/png")),window.location.reload(),this.loopRunning=!1;return}this.framesElapsed++,this.tempUnpause&&(this.tempUnpause=!1)}a="draw",this.drawIt()}catch(i){n=!0,r=i,this.handleFrameError(i,a)}finally{this.updateInProgress=!1}n?(this.consecutiveErrorCount++,this.consecutiveErrorCount>=Zt&&(this.loopRunning=!1,this.onError?.(r,{phase:a,consecutive:this.consecutiveErrorCount,stopped:!0}))):this.consecutiveErrorCount=0,this.loopRunning&&(this.drawLoopTimeout=setTimeout(()=>{this.requestedAnimationFrame=requestAnimationFrame(this.drawLoop)},this.drawTime))}handleFrameError(t,n){let r=performance.now();r-this.lastErrorLogAt>=1e3&&(this.lastErrorLogAt=r,console.error(`Animation ${n} error:`,t)),this.consecutiveErrorCount===0&&this.onError?.(t,{phase:n,consecutive:1,stopped:!1})}pureAspectUpdate(t){if(t[0]===0&&t[1]===0){let r=this.containerWidth||window.innerWidth,a=this.containerHeight||window.innerHeight,i=r/a;this.aspectRatio=Math.min(te,Math.max(ee,i))}else this.aspectRatio=t[0]/t[1]}};var M=class{constructor(){this.inbound=[];this.listeners=[]}dispatch(t){this.inbound.push(t),this.inbound.length>32&&this.inbound.splice(0,this.inbound.length-32)}consume(){if(this.inbound.length===0)return[];let t=this.inbound;return this.inbound=[],t}emit(t){for(let n of this.listeners)n(t)}onEvent(t){return this.listeners.push(t),()=>{this.listeners=this.listeners.filter(n=>n!==t)}}clear(){this.inbound=[],this.listeners=[]}};function ne(e){dt();let{container:t,seed:n,captureKeyboard:r=!1,audio:a}=e,i=new M,s=!1,o=null,u,l=!1,c=t.clientWidth||window.innerWidth,h=t.clientHeight||window.innerHeight,d=new j(30,!1,c,h,!1,void 0,{container:t,seed:n,captureKeyboard:r,hostChannel:i,audio:a}),m=()=>{let f=t.clientWidth||window.innerWidth,v=t.clientHeight||window.innerHeight;return f===d.containerWidth&&v===d.containerHeight?!1:(d.containerWidth=f,d.containerHeight=v,!0)},S=null;return typeof ResizeObserver<"u"&&(S=new ResizeObserver(()=>{if(s||!o)return;m()&&l&&d.reinit()}),S.observe(t)),d.onError=(f,v)=>{u?.(f,v)},{get tokenData(){return d.tokenData},get onError(){return u},set onError(f){u=f},mount(f,v={}){if(s)throw new Error("createRuntime: cannot mount on a destroyed runtime");o?.destroy(),l=!1,d.customState=v.initialState,d.setGameManager(v.gameManager);let ot=v.onEvent?i.onEvent(v.onEvent):()=>{};if(!d.prepareCart(f))throw ot(),new Error("createRuntime: failed to prepare cart");let p=!1,z={async start(){if(!p){if(m()&&d.isPrepared&&(d.init(),d.unloadCart(),!d.prepareCart(f)))throw new Error("createRuntime: failed to prepare cart");d.needsAudio&&(await d.unlockAudio(),p)||(l=!0,d.beginPlayback())}},pause(){p||(d.paused=!0)},resume(){p||(d.paused=!1)},dispatch(w){p||i.dispatch(w)},snapshot(){if(p)return{seed:"",pngDataUrl:""};let w=d.canvas;return{seed:d.tokenData.hash,metadata:f.metadata,pngDataUrl:w?w.toDataURL("image/png"):""}},destroy(){p||(p=!0,l=!1,ot(),i.clear(),d.unloadCart(),o===z&&(o=null))},reload(){if(!p){if(!l){if(d.isPrepared&&(d.unloadCart(),!d.prepareCart(f)))throw new Error("createRuntime: failed to prepare cart");return}d.reloadCart()}},reinit(w){if(!p){if(!l){m();return}d.reinit(w)}},getCartState(){return d.animation?.cartState},exportState(){return p?Promise.reject(new Error("Cart handle has been destroyed")):d.exportState()},exportStateJSON(){return p?Promise.reject(new Error("Cart handle has been destroyed")):d.exportStateJSON()},async importState(w,_t){if(p)throw new Error("Cart handle has been destroyed");await d.importState(w,_t)},peekExportedFramebuffer(){return p?null:d.peekExportedFramebuffer()},peekSeed(){if(!p)return d.peekSeed()},isGenerative(){return p?!1:d.isGenerative()},get canvas(){return d.canvas},get paused(){return d.paused},set paused(w){p||(d.paused=w)},get tokenData(){return d.tokenData},get isPrepared(){return d.isPrepared},get isLoopRunning(){return d.isLoopRunning},get needsAudio(){return d.needsAudio},get audioLibraries(){return d.audioLibraries}};return o=z,z},async unlockAudio(){await d.unlockAudio()},destroy(){s||(s=!0,o?.destroy(),o=null,S?.disconnect(),S=null,i.clear(),d.destroy())}}}var V="cyberart.state.save",$="cyberart.state.load";function re(e,t,n={}){let r=n.persist,a=r?{persist:r}:void 0,i=()=>{t.emit({type:V,payload:a})},s=()=>{t.emit({type:$,payload:a})};e.registerAction("w",i,!0),e.registerAction("W",i,!0),e.registerAction("e",s,!0),e.registerAction("E",s,!0)}function Pt(e){if(e.type!==V&&e.type!==$)return;let t=e.payload?.persist;return t==="localStorage"?t:void 0}var ae="cyberart-io",ie=1,E="framebuffers";function at(e,t){return`${e||"unknown"}:${t?A(t):"none"}`}function Mt(e,t){return`${e||"unknown"}:${t||"none"}`}function it(){return new Promise((e,t)=>{let n=indexedDB.open(ae,ie);n.onupgradeneeded=()=>{let r=n.result;r.objectStoreNames.contains(E)||r.createObjectStore(E)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error??new Error("indexedDB open failed"))})}function se(e){let t=e.data;return t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength)}function Dt(e,t){return new Promise((n,r)=>{let i=e.transaction(E,"readonly").objectStore(E).get(t);i.onsuccess=()=>n(i.result),i.onerror=()=>r(i.error??new Error("indexedDB read failed"))})}async function Rt(e,t,n){if(typeof indexedDB>"u")return;let r=await it(),a={width:n.width,height:n.height,data:se(n)};try{await new Promise((i,s)=>{let o=r.transaction(E,"readwrite");o.oncomplete=()=>i(),o.onerror=()=>s(o.error??new Error("indexedDB write failed")),o.objectStore(E).put(a,at(e,t))})}finally{r.close()}}async function It(e,t){if(typeof indexedDB>"u")return null;let n=await it();try{let r=at(e,t),a=Mt(e,t),i=await Dt(n,r)??(r===a?void 0:await Dt(n,a));if(!i||!i.data||i.width<1||i.height<1)return null;let s=new ImageData(i.width,i.height);return s.data.set(new Uint8ClampedArray(i.data)),s}finally{n.close()}}async function Lt(e,t){if(typeof indexedDB>"u")return;let n=await it();try{await new Promise((r,a)=>{let i=n.transaction(E,"readwrite");i.oncomplete=()=>r(),i.onerror=()=>a(i.error??new Error("indexedDB delete failed"));let s=i.objectStore(E);s.delete(at(e,t)),s.delete(Mt(e,t))})}finally{n.close()}}var st="cyberart.state.",oe="Saved state",de="Loaded state",ue="Failed to save state",ce="Failed to load state",he="No saved state";function x(e,t){let n=e||"unknown";return t?st+n+"."+A(t):st+n}function le(e,t,n){return r=>{if(Pt(r)==="localStorage"){if(r.type===V){pe(e,t,n);return}r.type===$&&me(e,t,n)}}}function R(e){try{let t=JSON.parse(e);if(!t||typeof t!="object")return null;let n=t;if(typeof n.savedAt=="number"&&n.bundle&&typeof n.bundle=="object")return{savedAt:n.savedAt,bundle:n.bundle};let r=t;return typeof r.seed=="string"&&typeof r.version=="number"?{savedAt:0,bundle:r}:null}catch{return null}}async function pe(e,t,n){let r=e();if(r)try{let a=await r.exportState(),i=a.cartId??t?.();a.generative===!0?(Ce(i),G(x(i,a.seed),a)):G(x(i),a);let s=r.peekExportedFramebuffer?.();if(s)try{await Rt(i,a.seed,s)}catch(o){console.warn("Failed to save cart framebuffer",o)}n?.(oe,"transient")}catch(a){console.error("Failed to save cart state",a),n?.(ue,"transient")}}async function me(e,t,n){let r=e();if(!r)return;let a=t?.(),i=r.peekSeed?.(),s=r.isGenerative?.()===!0,o=we(a,s,i);if(!o){console.warn("No saved cart state for",a||"unknown",s&&i?`(hash ${i})`:""),n?.(he,"transient");return}try{let u=await It(a,o.bundle.seed).catch(()=>null);await r.importState(o.bundle,{framebuffer:u}),n?.(de,"transient")}catch(u){console.error("Failed to load cart state",u),n?.(ce,"transient")}}function G(e,t){let n=JSON.stringify({savedAt:Date.now(),bundle:t});fe(e,n)}function fe(e,t){for(;;)try{localStorage.setItem(e,t);return}catch(n){if(!ge(n)||!be(e))throw n}}function ge(e){if(!e||typeof e!="object")return!1;let t=e.name,n=e.code;return t==="QuotaExceededError"||t==="NS_ERROR_DOM_QUOTA_REACHED"||n===22||n===1014}function be(e){let t=null,n=1/0;for(let i of ye()){if(i===e)continue;let s=localStorage.getItem(i);if(!s)continue;let u=R(s)?.savedAt??0;u<n&&(n=u,t=i)}if(!t)return!1;let r=localStorage.getItem(t),a=r?R(r):null;return localStorage.removeItem(t),a?.bundle&&Lt(a.bundle.cartId,a.bundle.seed).catch(()=>{}),!0}function ye(){let e=[];for(let t=0;t<localStorage.length;t++){let n=localStorage.key(t);n&&n.startsWith(st)&&e.push(n)}return e}function we(e,t,n){if(t&&n){let a=localStorage.getItem(x(e,n));if(a)return R(a);let i=localStorage.getItem(x(e));if(!i)return null;let s=R(i);if(!s||!Se(s.bundle,n))return null;try{G(x(e,n),s.bundle)}catch{}return s}let r=localStorage.getItem(x(e));return r?R(r):null}function Se(e,t){return typeof e.seed=="string"&&A(e.seed)===A(t)}function Ce(e){let t=x(e),n=localStorage.getItem(t);if(!n)return;let r=R(n);if(r&&typeof r.bundle.seed=="string"){let a=x(e,r.bundle.seed);if(!localStorage.getItem(a))try{G(a,r.bundle)}catch{return}}localStorage.removeItem(t)}export{O as CYBERART_CANVAS_ATTR,M as HostChannel,g as IncompatibleCartStateError,H as KeyboardManager,B as PointerManager,I as Random,le as attachCartStatePersistence,ne as createRuntime,re as registerCartStateHotkeys};
8
+ var Vt=(t,e)=>()=>(t&&(e=t(t=0)),e);var Xt={};function Yt(t,e){let n=t.indexOf(at);if(n===-1)return null;let r=n+at.length,a=`const __cyberSkip=new Set(${JSON.stringify(e)});const __cyberOrig=registerProcessor;registerProcessor=(n,p)=>__cyberSkip.has(n)?undefined:__cyberOrig(n,p);`;return t.slice(0,r)+a+t.slice(r)}var Ee,pe,Jt,ee,at,st=Vt(()=>{"use strict";Ee=/AudioWorkletProcessor with name:\s*["'`][^"'`]+["'`]\s+is already registered/i,pe="__cyberartWorkletPatchApplied";typeof window<"u"&&!window[pe]&&(window.addEventListener("error",t=>{let e=t?.message??"";Ee.test(e)&&(t.preventDefault(),t.stopImmediatePropagation())},!0),window.addEventListener("unhandledrejection",t=>{let e=t?.reason,n=typeof e=="string"?e:e?.message??"";Ee.test(n)&&t.preventDefault()}),window[pe]=!0);Jt=!1,ee=(...t)=>{Jt&&console.log("[workletPatch]",...t)},at="((AudioWorkletProcessor,registerProcessor)=>{";if(typeof AudioWorklet<"u"&&!AudioWorklet.prototype[pe]){let t=new WeakMap,e=/registerProcessor\s*\(\s*['"`]([^'"`]+)['"`]/g,n=AudioWorklet.prototype.addModule,r=0;AudioWorklet.prototype.addModule=async function(a,o){let c=++r,l=t.get(this);l||(l=new Set,t.set(this,l)),ee(`#${c} addModule`,{url:a.slice(0,64),isBlob:a.startsWith("blob:"),knownBefore:[...l]});let y=a,m=null;if(a.startsWith("blob:")){let h=null;try{h=await(await fetch(a)).text()}catch(b){ee(`#${c} blob fetch failed`,b)}if(h!==null){let b=new Set;for(let w of h.matchAll(e))b.add(w[1]);if(b.size>0){let w=[...b].filter(E=>l.has(E)),d=[...b].filter(E=>!l.has(E));if(w.length>0&&d.length===0){ee(`#${c} short-circuit \u2014 all processors already registered`,w);return}if(w.length>0&&d.length>0){let E=Yt(h,w);if(E!==null){let R=new Blob([E],{type:"application/javascript"});m=URL.createObjectURL(R),y=m,ee(`#${c} rewrote blob; skipping`,w,"allowing",d)}else ee(`#${c} blob rewrite failed; passing original through`)}d.forEach(E=>l.add(E))}}}return n.call(this,y,o).then(()=>{m&&URL.revokeObjectURL(m)},h=>{m&&URL.revokeObjectURL(m);let b=(h&&h.message)??"";if(!(Ee.test(b)||/already registered/i.test(b)))throw h})},AudioWorklet.prototype[pe]=!0}});var qe=()=>{let t=["ms","moz","webkit","o"];for(let e=0;e<t.length&&!window.requestAnimationFrame;++e)window.requestAnimationFrame=window[t[e]+"RequestAnimationFrame"],window.cancelAnimationFrame=window[t[e]+"CancelAnimationFrame"]||window[t[e]+"CancelRequestAnimationFrame"]},T=window.devicePixelRatio||1,K=/Headless/i.test(navigator.userAgent),qn=/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)||/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.platform);function Ge(){return Math.min(window.innerWidth,window.innerHeight)}var ie="data-cyberart-canvas",V=class{constructor(e,n,r=1,i={}){this.width=e,this.height=n;let a=i.container,o=i.adoptFromDocument??!a,c=a?a.querySelector(`canvas[${ie}]`)??a.querySelector("canvas"):null;if(c)this.canvas=c,this.adopted=!0;else if(o){let l=document.getElementById("canvas")??document.querySelector("canvas");l?(this.canvas=l,this.adopted=!0):(this.canvas=document.createElement("canvas"),this.canvas.setAttribute("id","canvas"),this.adopted=!1)}else this.canvas=document.createElement("canvas"),this.adopted=!1;this.canvas.hasAttribute(ie)||this.canvas.setAttribute(ie,""),this.canvas.style.display="block",this.canvas.style.margin="auto",this.ctx=this.canvas.getContext("2d",{colorSpace:"display-p3",alpha:!1}),this.ctx.globalAlpha=r,this.ctx.imageSmoothingEnabled=!1,this.setSize(e,n)}setSize(e,n){this.width=e,this.height=n,this.canvas.width=e,this.canvas.height=n,this.canvas.style.width=`${e/T}px`,this.canvas.style.height=`${n/T}px`,this.imageData=this.ctx.createImageData(e,n),this.ctx.putImageData(this.imageData,0,0)}_blank(e=1){let n=e?Math.max(e,.03):1;this.ctx.fillStyle=`rgba(0,0,0,${n})`,this.ctx.fillRect(0,0,this.width,this.height)}clearAll(){this.ctx.save(),this.ctx.globalAlpha=1,this._blank(),this.ctx.restore()}};function Je(t,e){let n=t/e,r,i;return n>1?(r=t,i=~~(t/n)):(i=e,r=~~(e*n)),{width:r,height:i,iWidth:1/r,iHeight:1/i,area:r*i,largeDim:Math.max(r,i),smallDim:Math.min(r,i),aspectRatio:n}}function Ye(t,e=window.innerWidth,n=window.innerHeight){let r=Ge(),i,a;return n<e?t>1?e>=r*t?(i=r*t,a=r):(i=e,a=i/t):(i=r*t,a=r):t>1?(i=r,a=r/t):n<r/t?(a=n,i=a*t):(i=r,a=r/t),[i,a]}var ae=class{constructor(e=!1,n=!0){this.debugMode=e,this.actionMap={},this.listening=n,this.checkKeypress=this.checkKeypress.bind(this),this.registerAction=this.registerAction.bind(this),this.listening&&window.addEventListener("keydown",this.checkKeypress,!1)}checkKeypress({key:e}){this.debugMode&&console.log(`Pressed ${e}`),e in this.actionMap&&this.actionMap[e]()}inject(e){this.checkKeypress({key:e})}registerAction(e,n,r=!1){if(e.length!==1)throw new Error(`Only single-character actions may be registered. Cannot register: ${e}`);if(e in this.actionMap&&!r)throw new Error(`Action already exists for code: ${e}`);this.actionMap[e]=n}destroy(){this.listening&&window.removeEventListener("keydown",this.checkKeypress,!1),this.actionMap={}}},Xe=ae;var se=class{constructor(e,n={}){this.x=-1;this.y=-1;this.isDown=!1;this.clicks=[];this.canvas=e,this.listening=n.listen!==!1,this.onPointerDown=this.onPointerDown.bind(this),this.onPointerMove=this.onPointerMove.bind(this),this.onPointerUp=this.onPointerUp.bind(this),this.listening&&(e.addEventListener("pointerdown",this.onPointerDown),e.addEventListener("pointermove",this.onPointerMove),e.addEventListener("pointerup",this.onPointerUp))}toCanvasCoords(e){let n=this.canvas.getBoundingClientRect();return{x:(e.clientX-n.left)*(this.canvas.width/n.width),y:(e.clientY-n.top)*(this.canvas.height/n.height)}}onPointerDown(e){let{x:n,y:r}=this.toCanvasCoords(e);this.x=n,this.y=r,this.isDown=!0,this.clicks.push({x:n,y:r})}onPointerMove(e){let{x:n,y:r}=this.toCanvasCoords(e);this.x=n,this.y=r}onPointerUp(e){let{x:n,y:r}=this.toCanvasCoords(e);this.x=n,this.y=r,this.isDown=!1}hasClick(){return this.clicks.length>0}consumeClick(){return this.clicks.shift()??null}inject(e,n,r){this.x=n,this.y=r,e==="down"?(this.isDown=!0,this.clicks.push({x:n,y:r})):e==="up"&&(this.isDown=!1)}destroy(){this.listening&&(this.canvas.removeEventListener("pointerdown",this.onPointerDown),this.canvas.removeEventListener("pointermove",this.onPointerMove),this.canvas.removeEventListener("pointerup",this.onPointerUp))}},Qe=se;function W(){return{now:()=>performance.now()}}function oe(t=0){let e=t;return{now:()=>e,set(n){e=n},advance(n){e+=n}}}var Ze=(t,e,n,r)=>({now:t,startTime:e,elapsedSinceStart:t-e,deltaSinceLastUpdate:n===null?0:t-n,deltaSinceLastRender:r===null?0:t-r}),de=class{constructor(e,n,r,i,a,o={}){this.startTime=0;this.lastUpdateAt=null;this.lastRenderAt=null;this.clockOffsetMs=0;this.pauseStartedAt=null;this.renderAnimation=this.renderAnimation.bind(this),this.dimensionContext=Je(e,n),this.R=r,this.gameManager=o.gameManager,this.hostChannel=o.hostChannel,this.clock=o.clock??W(),this.canvas=new V(this.dimensionContext.width,this.dimensionContext.height,1,o.canvasMount),this.drawingContext=this.canvas.ctx,this.keyboardManager=new Xe(!1,o.captureKeyboard??!0),this.pointerManager=new Qe(this.canvas.canvas,{listen:o.listenToPointer!==!1}),this.animationCart=a;let c=o.customState;this.featureState=a.getDefaultFeatureState?.(this.R,this.dimensionContext,i,this.keyboardManager,c,this.pointerManager,this.gameManager,this.hostChannel),this.cartState=a.getDefaultState(this.R,this.dimensionContext,i,this.keyboardManager,c,this.pointerManager,this.gameManager,this.featureState,this.hostChannel),this.startTime=this.clock.now(),this.canvas.clearAll()}captureFramebuffer(){let e=this.canvas.imageData,n=new ImageData(e.width,e.height);return n.data.set(e.data),n}restoreFramebuffer(e){let n=this.canvas.imageData;return n.width!==e.width||n.height!==e.height||n.data.length!==e.data.length?!1:(n.data.set(e.data),this.drawingContext.putImageData(n,0,0),!0)}setPaused(e){let n=this.clock.now();if(e){this.pauseStartedAt===null&&(this.pauseStartedAt=n);return}this.pauseStartedAt!==null&&(this.clockOffsetMs+=n-this.pauseStartedAt,this.pauseStartedAt=null)}resolveNow(e){let n=this.pauseStartedAt!==null?e-this.pauseStartedAt:0;return e-this.clockOffsetMs-n}getElapsedSinceStart(e=this.clock.now()){return Math.max(0,this.resolveNow(e)-this.startTime)}restoreClock(e,n){this.pauseStartedAt=null,this.lastUpdateAt=null,this.lastRenderAt=null;let r=Math.max(0,e);if(typeof n=="number"&&Number.isFinite(n)){this.startTime=n,this.clockOffsetMs=this.clock.now()-n-r;return}this.clockOffsetMs=0,this.startTime=this.clock.now()-r}getRandomState(){return this.R.getState()}injectKey(e){this.keyboardManager.inject(e)}injectPointer(e,n,r){this.pointerManager.inject(e,n,r)}async updateAnimationState(e,n,r=this.clock.now()){let i=this.resolveNow(r),a=Ze(i,this.startTime,this.lastUpdateAt,this.lastRenderAt);this.cartState=this.animationCart.update(this.R,e,n,this.dimensionContext,this.cartState,this.keyboardManager,this.pointerManager,this.gameManager,a,this.featureState,this.hostChannel),this.lastUpdateAt=i}async renderAnimation(e,n,r=this.clock.now()){let i=this.resolveNow(r),a=Ze(i,this.startTime,this.lastUpdateAt,this.lastRenderAt);this.animationCart.render(this.R,e,n,this.dimensionContext,this.cartState,this.drawingContext,this.canvas.imageData,this.pointerManager,this.gameManager,a,this.featureState,this.hostChannel),this.lastRenderAt=i}getCanvas(){return this.canvas.canvas}getDrawDetails(){return[this.canvas.canvas,0,0,this.dimensionContext.width,this.dimensionContext.height]}teardown(){try{this.animationCart.teardown?.(this.cartState,this.featureState)}catch(e){console.error("Error during cart teardown",e)}this.keyboardManager.destroy(),this.pointerManager.destroy()}};var ue=class extends V{constructor(e,n,r=1,i=!0,a={}){if(super(e,n,r,a),this.adopted)return;let o=a.container;if(o){this.canvas.parentNode!==o&&o.appendChild(this.canvas);return}if(i){let c=document.getElementById("canvas");c&&c!==this.canvas&&c.parentNode===document.body&&document.body.removeChild(c),document.body.appendChild(this.canvas)}}};function et(t){let e=parseInt(t.substring(0,8),16),n=parseInt(t.substring(8,8),16),r=parseInt(t.substring(16,8),16),i=parseInt(t.substring(24,8),16);return{next:()=>{e|=0,n|=0,r|=0,i|=0;let o=(e+n|0)+i|0;return i=i+1|0,e=n^n>>>9,n=r+(r<<3)|0,r=r<<21|r>>>11,r=r+o|0,(o>>>0)/4294967296},getRegs:()=>({a:e,b:n,c:r,d:i}),setRegs:o=>{e=o.a,n=o.b,r=o.c,i=o.d}}}var Q=class{constructor(e){this.seed=e.hash,this.useA=!1,this.genA=et(e.hash.substring(2,32)),this.genB=et(e.hash.substring(34,32)),this.prngA=this.genA.next,this.prngB=this.genB.next;for(let n=0;n<1e6;n+=2)this.prngA(),this.prngB()}getState(){let e=this.genA.getRegs(),n=this.genB.getRegs();return{seed:this.seed,useA:this.useA,prngA:{a:e.a,b:e.b,c:e.c,d:e.d},prngB:{a:n.a,b:n.b,c:n.c,d:n.d}}}setState(e){if(e.seed!==this.seed)throw new Error(`Random.setState: seed ${e.seed} does not match ${this.seed}`);this.useA=e.useA,this.genA.setRegs(e.prngA),this.genB.setRegs(e.prngB)}r_zero_one(){return this.useA=!this.useA,this.useA?this.prngA():this.prngB()}dec(e,n){return e===void 0?this.r_zero_one():n===void 0?e*this.r_zero_one():e+(n-e)*this.r_zero_one()}int(e,n){return n===void 0&&(n=e,e=0),Math.floor(this.dec(e,n))}bool(e=.5){return this.r_zero_one()<e}sign(){return this.bool()?1:-1}choose(e){return e[this.int(e.length)]}};var nt="bafybeicjwcq5lxxtfnyj4p2ugcr7ctyb5wl62cbcechj6tsabdq5whcs7u",tt="https://ipfs.io/ipfs/";function Wt(t){return t.endsWith("/")?t:`${t}/`}var Z=null;function $t(t){return[...Array(t)].map(()=>Math.floor(Math.random()*16).toString(16)).join("")}function jt(t){return t&&t.length>0?t:[{cid:nt}]}function zt(t){return t.startsWith("0x")?t:`0x${t}`}var rt=/^(0[xX])?[0-9a-fA-F]{64}$/;function qt(t){let e="",n=2166136261,r=522970236;for(let i=0;e.length<64;i++){for(let a=0;a<t.length;a++)n^=t.charCodeAt(a)+i,n=Math.imul(n,16777619),r^=t.charCodeAt(a)+i*17,r=Math.imul(r,16777619);e+=(n>>>0).toString(16).padStart(8,"0"),e+=(r>>>0).toString(16).padStart(8,"0")}return`0x${e.slice(0,64)}`}function ce(t){let e=typeof t=="number"?String(t):t;return rt.test(e)?`0x${(e.startsWith("0x")||e.startsWith("0X")?e.slice(2):e).toLowerCase()}`:qt(e)}function le(t,e){if(typeof t=="number"||e==="deterministic")return ce(t);let n=String(t);return rt.test(n)?ce(n):n.startsWith("0x")||n.startsWith("0X")?n:`0x${n}`}function Gt(t){if(!t&&typeof tokenData<"u"&&tokenData?.hash)return{hash:tokenData.hash,tokenId:String(tokenData.tokenId),externalAssetDependencies:jt(tokenData.externalAssetDependencies),preferredIPFSGateway:Wt(tokenData.preferredIPFSGateway||tt),preferredArweaveGateway:tokenData.preferredArweaveGateway};let e=typeof window<"u"?new URLSearchParams(window.location.search):new URLSearchParams;return{hash:t?zt(t):e.get("hash")||`0x${$t(64)}`,tokenId:"18009999",externalAssetDependencies:[{cid:nt}],preferredIPFSGateway:tt}}function we(t){let e=t?.useCache!==!1;if(e&&Z&&!t?.hash)return Z;let n=Gt(t?.hash);return e?Z=n:Z||(Z=n),n}function it(t){return new Array(32).fill(null).map((e,n)=>parseInt(t.hash.slice(2+n*2,4+n*2),16))}async function Se(){let t=await import("tone");if(typeof t.start=="function")return t;let e=t.default;return e&&typeof e.start=="function"?e:t}var ot={id:"tone",async load(){await Promise.resolve().then(()=>(st(),Xt))},async unlock(){await ot.load(),await(await Se()).start()},async resumeIfSuspended(){let t=await Se();t.context.state==="suspended"&&await t.context.resume()},async suspendIfRunning(){let e=(await Se()).context.rawContext;"suspend"in e&&e.state==="running"&&await e.suspend()}},dt={tone:ot};function Ae(t){if(!t)return[];let e=Array.isArray(t)?t:[t],n=[];for(let r of e){if(!(r in dt)){console.warn(`Unknown audio library "${r}"`);continue}let i=r;n.includes(i)||n.push(i)}return n}function Ce(t){return t.map(e=>dt[e])}var he="$cyberartTa",k=class extends Error{constructor(e){super(e),this.name="IncompatibleCartStateError"}},ke={Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array};function Qt(t){for(let e of Object.keys(ke))if(t instanceof ke[e])return e}function ct(t){return ArrayBuffer.isView(t)&&!(t instanceof DataView)}function I(t){if(t===null||typeof t!="object"||Array.isArray(t))return!1;let e=Object.getPrototypeOf(t);return e===Object.prototype||e===null}function xe(t){return I(t)?typeof t[he]=="string"&&Array.isArray(t.d):!1}function Zt(t){return t==null?!1:typeof t=="function"?!0:!(typeof t!="object"||Array.isArray(t)||ct(t)||I(t))}var en=new Set(["audioContextStarted","midiSynths","keepRatioPrimed","audioInitStarted","musicSeekApplied"]);function ut(t){return t===null?"null":xe(t)?String(t[he]):Array.isArray(t)?"array":I(t)?"object":typeof t}function tn(t,e){return t===e||t==="null"||e==="null"}function _(t){return t.trim().toLowerCase().replace(/^0x/,"")}function lt(t,e,n,r){if(!(!n&&!r)&&_(t)!==_(e))throw new k(`Generative cart state is for seed "${e}", but the live token is "${t}"`)}function $(t){return typeof t=="number"&&Number.isFinite(t)&&t>0}function pt(t,e){let n=t.dimensions;if(n&&$(n.width)&&$(n.height))return n;if(e&&$(e.width)&&$(e.height))return{width:e.width,height:e.height};if(I(t.state)){let r=t.state.containerWidth,i=t.state.containerHeight;if($(r)&&$(i))return{width:r,height:i}}return null}function j(t,e=new WeakSet){if(t===null||typeof t=="string"||typeof t=="boolean")return t;if(typeof t=="number")return Number.isFinite(t)?t:null;if(!(typeof t=="function"||typeof t>"u"||typeof t=="bigint"||typeof t=="symbol")){if(ct(t)){let n=Qt(t);return n?{[he]:n,d:Array.from(t)}:void 0}if(typeof t=="object"&&!e.has(t)){e.add(t);try{if(Array.isArray(t)){let i=[],a=0;for(let o of t){let c=j(o,e);c!==void 0&&(i.push(c),a++)}return t.length>0&&a===0?void 0:i}if(!I(t)||Zt(t))return;let n=Object.keys(t),r={};for(let i of n){if(en.has(i))continue;let a=j(t[i],e);a!==void 0&&(r[i]=a)}return n.length>0&&Object.keys(r).length===0?void 0:r}finally{e.delete(t)}}}}function te(t){if(xe(t)){let e=ke[t[he]];return e?new e(t.d.map(n=>Number(n))):t}if(Array.isArray(t))return t.map(e=>te(e));if(I(t)){let e={};for(let n of Object.keys(t))e[n]=te(t[n]);return e}return t}function Te(t,e,n,r,i=!0){if(i&&n&&r&&n!==r)throw new k(`Cart state is for "${r}", but the live cart is "${n}"`);if(!I(e)){if(i)throw new k("Cart state snapshot is empty");return}let a=Object.keys(e);if(a.length===0){if(i)throw new k("Cart state snapshot is empty");return}let o=j(t);if(!I(o)||Object.keys(o).length===0){if(i)throw new k("Live cart has no serializable state");return}let c=a.filter(l=>l in o);if(c.length===0){if(i)throw new k("Cart state snapshot does not overlap the live cart shape");return}for(let l of c){let y=ut(o[l]),m=ut(e[l]);if(!tn(y,m))throw new k(`Cart state field "${l}" has type ${m}, live cart expects ${y}`);if(y==="object"){let h=e[l],b=t[l];I(h)&&!xe(h)&&Te(b,h,void 0,void 0,!1)}}}function ht(t,e){if(!I(t)||!I(e))return;let n=j(t);if(I(n))for(let r of Object.keys(e))r in n&&(t[r]=te(e[r]))}function mt(t){let e=typeof t=="string"?JSON.parse(t):t;if(!e||typeof e!="object")throw new k("Cart state bundle is not an object");if(e.version!==1)throw new k(`Unsupported cart state version ${String(e.version)}`);if(typeof e.seed!="string"||typeof e.framesElapsed!="number")throw new k("Cart state bundle is missing seed or framesElapsed");return e}var Pe="cyberart.asset.ready",De="cyberart.asset.failed";function L(t){try{return JSON.parse(JSON.stringify(t))}catch{return t}}function M(t){try{return JSON.stringify(t)}catch{return String(t)}}function Re(t,e,n){let r=e??[],i=n??[],a=Math.max(r.length,i.length);for(let o=0;o<a;o++)if(M(r[o])!==M(i[o]))return`${t}[${o}]: ${M(r[o])} vs ${M(i[o])}`}function ft(t,e){let n=[];t.seed!==e.seed&&n.push(`seed: ${t.seed} vs ${e.seed}`),t.clock.framesElapsed!==e.clock.framesElapsed&&n.push(`framesElapsed: ${t.clock.framesElapsed} vs ${e.clock.framesElapsed}`),t.clock.now!==e.clock.now&&n.push(`now: ${t.clock.now} vs ${e.clock.now}`),t.clock.frameRate!==e.clock.frameRate&&n.push(`frameRate: ${t.clock.frameRate} vs ${e.clock.frameRate}`),M(t.rng)!==M(e.rng)&&n.push(`rng: ${M(t.rng)} vs ${M(e.rng)}`);let r=Re("actions",t.actions,e.actions);r&&n.push(r);let i=Re("applied",t.applied,e.applied);i&&n.push(i);let a=Re("events",t.events,e.events);return a&&n.push(a),M(t.state)!==M(e.state)&&n.push(`state: ${M(t.state)} vs ${M(e.state)}`),n}function gt(t){return{type:t.status==="ready"?Pe:De,kind:"state",payload:t.detail===void 0?{id:t.id}:{id:t.id,detail:t.detail}}}function rn(t){if(!t||typeof t!="object")return 0;let e=t;return typeof e.startTime=="number"&&typeof e.lastSimulationUpdateAt=="number"?Math.max(0,e.lastSimulationUpdateAt-e.startTime):0}function an(t,e){if(typeof window.resizeTo!="function")return!1;try{if(window.top!==window)return!1}catch{return!1}let n=Math.max(1,Math.round(t)),r=Math.max(1,Math.round(e)),i=Math.max(0,window.outerWidth-window.innerWidth),a=Math.max(0,window.outerHeight-window.innerHeight);try{window.resizeTo(n+i,r+a)}catch{return!1}return Math.abs(window.innerWidth-n)<=8&&Math.abs(window.innerHeight-r)<=8}var sn=[[0,0],[16,9],[9,16],[4,3],[3,4],[2,1],[1,2],[3,1],[1,3],[1,1]],Me=sn[0],on=Me[0]/Me[1],Ie=30,dn=120,un=3840/2160,cn=2160/3840,me=class{constructor(e=Ie,n=!1,r=window.innerWidth,i=window.innerHeight,a=!0,o,c={}){this.framesElapsed=0;this.frameRate=Ie;this._paused=!1;this.savedToken=!1;this.prepared=!1;this.loopRunning=!1;this.fallbackAudio=[];this.audioLibrariesInternal=[];this.virtualClock=null;this.scriptedActions=[];this.initialScriptedActions=[];this.appliedActions=[];this.consumedActionIndexes=new Set;this.outboundEvents=[];this.consecutiveErrorCount=0;this.lastErrorLogAt=0;this.reinitStayPaused=!1;this.reinitPending=!1;this.lastExportedFramebuffer=null;this.pinnedOutput=null;this.pinViewportAtImport=null;this.importing=!1;this.drawLoop=this.drawLoop.bind(this),this.loadCart=this.loadCart.bind(this),this.prepareCart=this.prepareCart.bind(this),this.beginPlayback=this.beginPlayback.bind(this),this.reinit=this.reinit.bind(this),this.tuneFramerate=this.tuneFramerate.bind(this),this.container=c.container,this.seed=c.seed,this.captureKeyboard=c.captureKeyboard??!0,this.customState=c.customState,this.hostChannel=c.hostChannel,this.fallbackAudio=Ae(c.audio),this.audioLibrariesInternal=this.fallbackAudio;let l=c.deterministic;this.deterministic=!!l;let y=l&&typeof l=="object"?l:{};if(this.deterministic){if(c.clock){let m=c.clock;if(typeof m.advance!="function")throw new Error("deterministic mode requires a virtual clock with advance()");this.virtualClock=m,this.clock=m}else this.virtualClock=oe(y.origin??0),this.clock=this.virtualClock;this.initialScriptedActions=L(y.actions??[]),this.scriptedActions=L(this.initialScriptedActions)}else this.clock=c.clock??W();this.isBodyCanvas=this.container?!1:a,this.autoMode=n,this.aspectRatio=on,this.selectedAsp=Me,this.isFullscreen=!!document.fullscreenElement,this.updateFrameRate(e),this.tempUnpause=!1,this.synchronous=K,this.containerWidth=r,this.containerHeight=i,this.gameManager=o,this.init(!0)}setGameManager(e){this.gameManager=e}reinit(e){if(!this.importing){if(this.shouldKeepPinnedOutput()){this.fitPinnedCanvas();return}this.reinitPending||(this.reinitStayPaused=this._paused,this.reinitPending=!0),this.paused=!0,clearTimeout(this.reinitTimeout),this.reinitTimeout=setTimeout(()=>{if(this.importing){this.paused=this.reinitStayPaused,this.reinitPending=!1;return}this.updateInProgress?this.reinit(e):(this.requestedAnimationFrame&&cancelAnimationFrame(this.requestedAnimationFrame),this.clearPinnedOutput(),this.init(),this.reloadCart(),this.paused=this.reinitStayPaused,this.reinitPending=!1)},150)}}initSeed(){this.tokenData=this.seed?we({hash:this.seed,useCache:!1}):we(),this.rawParams=it(this.tokenData),this.mintNo=parseInt(this.tokenData.tokenId)%1e6,console.log("token",this.mintNo,this.tokenData.hash)}init(e=!1){e&&this.initSeed(),this.pureAspectUpdate(this.selectedAsp);let n=this.aspectRatio,[r,i]=Ye(n,this.containerWidth,this.containerHeight);r*=T,i*=T,this.width=~~r,this.height=~~i,this.atCenterX=this.width/2,this.atCenterY=this.height/2,this.mainCanvas?this.mainCanvas.setSize(this.width,this.height):this.mainCanvas=new ue(this.width,this.height,1,this.isBodyCanvas,this.canvasMountOptions())}loadCart(e){this.prepareCart(e)&&this.beginPlayback()}prepareCart(e){if(this.prepared)return!0;try{let n=~~(this.width/1),r=~~(this.height/this.width*n),i=new Q(this.tokenData),a=new de(n,r,i,this.rawParams,e,{gameManager:this.gameManager,canvasMount:this.canvasMountOptions(),customState:this.customState,captureKeyboard:this.deterministic?!1:this.captureKeyboard,hostChannel:this.hostChannel,clock:this.clock,listenToPointer:!this.deterministic});return this.animation=a,this.animationCart=e,this.paused=!1,this.framesElapsed=0,this.outboundEvents=[],this.scriptedActions=L(this.initialScriptedActions),this.appliedActions=[],this.consumedActionIndexes=new Set,this.outboundUnsubscribe?.(),this.outboundUnsubscribe=void 0,this.deterministic&&this.hostChannel&&(this.outboundUnsubscribe=this.hostChannel.onEvent(o=>{this.outboundEvents.push(o)})),this.R=i,this.applyCartFrameRate(e,a),this.resolveAudioLibraries(e),this.prepared=!0,!0}catch(n){return console.error(n),!1}}applyCartFrameRate(e,n){let r=e.metadata,i=r?.frameRate??Ie,a=n.cartState?.visualMode??n.featureState?.visualMode,o=a&&r?.frameRateByVisualMode?r.frameRateByVisualMode[a]:void 0;this.updateFrameRate(typeof o=="number"&&o>0?o:i)}get audioLibraries(){return this.audioLibrariesInternal}get needsAudio(){return this.audioLibrariesInternal.length>0}resolveAudioLibraries(e){this.audioLibrariesInternal=Ae(e.metadata?.audio)}async unlockAudio(){if(this.audioLibrariesInternal.length===0)return;let e=Ce(this.audioLibrariesInternal);if(K){for(let n of e)await n.load();return}for(let n of e)await n.unlock()}async resumeAudioLibraries(){if(!(this.paused||K))for(let e of Ce(this.audioLibrariesInternal))try{await e.resumeIfSuspended()}catch(n){console.error(`Failed to resume audio library "${e.id}" after reload`,n)}}beginPlayback(){!this.prepared||this.loopRunning||(this.prepared=!1,this.loopRunning=!0,this.consecutiveErrorCount=0,!this.deterministic&&(this.requestedAnimationFrame=requestAnimationFrame(this.drawLoop)))}get isPrepared(){return this.prepared}get isLoopRunning(){return this.loopRunning}get isDeterministic(){return this.deterministic}getClock(){return{now:this.clock.now(),framesElapsed:this.framesElapsed,frameRate:this.frameRate}}getRandomState(){return this.requirePreparedAnimation().getRandomState()}schedule(e){this.scriptedActions.push(L(e))}async getReplayMetadata(){let e=await this.exportState();return{seed:this.tokenData.hash,clock:this.getClock(),rng:this.getRandomState(),actions:L(this.scriptedActions),applied:L(this.appliedActions),events:L(this.outboundEvents),state:e.state}}async step(e=1){if(!this.deterministic)throw new Error("step() requires createRuntime({ deterministic: true })");this.requirePreparedAnimation(),this.prepared&&!this.loopRunning&&this.beginPlayback(),this.paused=!1;let n=Math.max(0,Math.floor(e));for(let r=0;r<n&&this.animation;r++){let i=this.framesElapsed;if(this.applyScheduledActions(i),await this.processFrame(this.clock.now(),{ignorePause:!0,scheduleNext:!1}),this.framesElapsed===i||(this.virtualClock?.advance(this.targetDrawTime),!this.loopRunning))break}}async advance(e){let n=Math.max(0,Math.round(e/this.targetDrawTime));await this.step(n)}applyScheduledActions(e){let n=this.animation;n&&this.scriptedActions.forEach((r,i)=>{r.atFrame===e&&(this.consumedActionIndexes.has(i)||(this.consumedActionIndexes.add(i),r.type==="pointer"?n.injectPointer(r.pointer.kind,r.pointer.x,r.pointer.y):r.type==="key"?n.injectKey(r.key):r.type==="event"?this.hostChannel?.dispatch(r.event):r.type==="asset"&&this.hostChannel?.dispatch(gt(r)),this.appliedActions.push({frame:e,action:r})))})}get paused(){return this._paused}set paused(e){this._paused!==e&&(this._paused=e,this.animation?.setPaused(e))}async waitUntilUpdateIdle(){for(;this.updateInProgress;)await new Promise(e=>{this.deterministic?queueMicrotask(e):typeof requestAnimationFrame=="function"?requestAnimationFrame(()=>e()):setTimeout(e,0)})}requirePreparedAnimation(){if(!this.animation)throw new Error("No prepared cart");return this.animation}async exportState(){let e=this.requirePreparedAnimation(),n=this._paused;this.paused=!0,await this.waitUntilUpdateIdle();try{this.lastExportedFramebuffer=e.captureFramebuffer();let r=e.getCanvas();return{version:1,cartId:this.animationCart?.metadata?.id,generative:this.animationCart?.metadata?.generative,seed:this.tokenData.hash,framesElapsed:this.framesElapsed,elapsedSinceStart:e.getElapsedSinceStart(),dimensions:{width:r.width,height:r.height,cssWidth:this.containerWidth,cssHeight:this.containerHeight,dpr:T},state:j(e.cartState)}}finally{this.paused=n}}async exportStateJSON(){return JSON.stringify(await this.exportState())}peekExportedFramebuffer(){return this.lastExportedFramebuffer}peekSeed(){return this.tokenData?.hash}isGenerative(){return this.animationCart?.metadata?.generative===!0}async importState(e,n){let r=this.requirePreparedAnimation(),i=mt(e),a=this._paused,o=this.loopRunning;this.paused=!0,await this.waitUntilUpdateIdle(),Te(r.cartState,i.state,this.animationCart?.metadata?.id,i.cartId),lt(this.tokenData.hash,i.seed,this.animationCart?.metadata?.generative,i.generative),clearTimeout(this.reinitTimeout),this.reinitPending=!1,this.importing=!0;let c=pt(i,n?.framebuffer??this.lastExportedFramebuffer);if(c){this.pinOutputSize(c);let m=c.cssWidth??c.width/T,h=c.cssHeight??c.height/T;an(m,h),this.syncContainerFromDom(),this.pinViewportAtImport={w:this.containerWidth,h:this.containerHeight}}let l=this.customState,y=!1;try{if(this.customState=te(i.state),this.unloadCart(),!this.prepareCart(this.animationCart))throw new Error("Failed to prepare cart while loading state");this.animation.cartState!==this.customState&&ht(this.animation.cartState,i.state),this.framesElapsed=i.framesElapsed;let m=this.animation,h=m.cartState,b=typeof i.elapsedSinceStart=="number"?i.elapsedSinceStart:rn(h);m.restoreClock(b,typeof h?.startTime=="number"?h.startTime:void 0),a&&m.setPaused(!0);let w=n?.framebuffer??this.lastExportedFramebuffer;w&&m.restoreFramebuffer(w)?h.keepRatioPrimed=!0:await m.renderAnimation(this.framesElapsed,this.rawParams),this.fitPinnedCanvas(),y=!0}catch(m){throw this.clearPinnedOutput(),this.customState=l,this.paused=a,this.animationCart&&(this.syncContainerFromDom(),this.init(),this.reloadCart()),m}finally{this.importing=!1,y&&(this.customState=l,this.paused=a,o&&this.beginPlayback())}}unloadCart(){this.loopRunning=!1,this.requestedAnimationFrame&&cancelAnimationFrame(this.requestedAnimationFrame),this.drawLoopTimeout&&clearTimeout(this.drawLoopTimeout),this.animation?.teardown(),this.animation=void 0,this.prepared=!1,this.audioLibrariesInternal=this.fallbackAudio,this.mainCanvas?.clearAll()}reloadCart(e=!1){let n=this.animationCart;if(!n)return;this.pinnedOutput&&(this.clearPinnedOutput(),this.syncContainerFromDom(),this.init());let r=this.reinitPending?this.reinitStayPaused:this._paused;this.unloadCart(),e&&this.initSeed(),this.loadCart(n),this.paused=r,!this.paused&&!K&&this.resumeAudioLibraries()}getFrameRate(){return this.frameRate}updateFrameRate(e){if(this.frameRate=e,e>=1)this.targetDrawTime=1e3/e;else{let r=1/(2-e);this.targetDrawTime=1e3/r}this.drawTime=this.targetDrawTime}canvasMountOptions(){return{container:this.container,adoptFromDocument:!this.container}}syncContainerFromDom(){if(this.container){this.containerWidth=this.container.clientWidth||this.containerWidth,this.containerHeight=this.container.clientHeight||this.containerHeight;return}this.containerWidth=window.innerWidth,this.containerHeight=window.innerHeight}pinOutputSize(e){this.pinnedOutput=e,this.width=~~e.width,this.height=~~e.height,this.atCenterX=this.width/2,this.atCenterY=this.height/2,this.aspectRatio=this.width/this.height,this.mainCanvas&&this.mainCanvas.setSize(this.width,this.height)}shouldKeepPinnedOutput(){return this.pinnedOutput?(this.syncContainerFromDom(),this.viewportMatches(this.pinnedCssSize())?!0:!!(this.pinViewportAtImport&&this.viewportMatches(this.pinViewportAtImport))):!1}pinnedCssSize(){let e=this.pinnedOutput;return{w:e.cssWidth??e.width/T,h:e.cssHeight??e.height/T}}viewportMatches(e){return Math.abs(this.containerWidth-e.w)<8&&Math.abs(this.containerHeight-e.h)<8}fitPinnedCanvas(){let e=this.pinnedOutput,n=this.canvas;if(!e||!n)return;this.container&&!this.container.style.position&&(this.container.style.position="relative");let r=this.containerWidth||window.innerWidth,i=this.containerHeight||window.innerHeight,a=e.width/T,o=e.height/T;if(a<1||o<1||r<1||i<1)return;let c=Math.min(r/a,i/o),l=a*c,y=o*c;n.style.position="absolute",n.style.left="50%",n.style.top="50%",n.style.transform="translate(-50%, -50%)",n.style.margin="0",n.style.width=`${l}px`,n.style.height=`${y}px`,n.style.imageRendering=Math.abs(c-1)<.01?"auto":"pixelated"}clearPinnedOutput(){let e=this.canvas;this.pinnedOutput=null,this.pinViewportAtImport=null,e&&(e.style.position="",e.style.left="",e.style.top="",e.style.transform="",e.style.imageRendering="",e.style.width="",e.style.height="",e.style.margin="auto")}get canvas(){return this.animation?.getCanvas()??this.mainCanvas?.canvas}destroy(){this.outboundUnsubscribe?.(),this.outboundUnsubscribe=void 0,this.clearPinnedOutput(),this.unloadCart(),clearTimeout(this.reinitTimeout),this.mainCanvas&&!this.mainCanvas.adopted&&this.mainCanvas.canvas.remove()}drawIt(){}tuneFramerate(e){let n=this.lastCall||e;this.lastCall=e,this.drawTime=Math.max(0,this.drawTime+this.targetDrawTime-this.lastCall+n)}async processFrame(e,n={}){let r=n.ignorePause===!0,i=n.scheduleNext!==!1;if(!this.loopRunning&&!r)return;let a=!1,o,c="update";try{if(r||!this.paused||this.tempUnpause){if(!K&&!r&&this.tuneFramerate(e),!this.animation)return;if(c="update",this.updateInProgress=!0,await this.animation.updateAnimationState(this.framesElapsed,this.rawParams,e),this.updateInProgress=!1,c="render",await this.animation.renderAnimation(this.framesElapsed,this.rawParams,e),this.autoMode&&this.animation.cartState.saveToken&&!this.savedToken&&!new URLSearchParams(window.location.search).get("hash")){let m=localStorage.getItem("goodones"),h=JSON.stringify(m?[...JSON.parse(m),this.tokenData.hash]:[this.tokenData.hash]);localStorage.setItem("goodones",h),this.savedToken=!0,localStorage.setItem(this.tokenData.hash,this.mainCanvas.canvas.toDataURL("image/png")),window.location.reload(),this.loopRunning=!1;return}this.framesElapsed++,this.tempUnpause&&(this.tempUnpause=!1)}c="draw",this.drawIt()}catch(l){a=!0,o=l,this.handleFrameError(l,c)}finally{this.updateInProgress=!1}a?(this.consecutiveErrorCount++,this.consecutiveErrorCount>=dn&&(this.loopRunning=!1,this.onError?.(o,{phase:c,consecutive:this.consecutiveErrorCount,stopped:!0}))):this.consecutiveErrorCount=0,!(!this.loopRunning||!i)&&(this.drawLoopTimeout=setTimeout(()=>{this.requestedAnimationFrame=requestAnimationFrame(this.drawLoop)},this.drawTime))}async drawLoop(e){await this.processFrame(e,{scheduleNext:!0})}handleFrameError(e,n){let r=performance.now();r-this.lastErrorLogAt>=1e3&&(this.lastErrorLogAt=r,console.error(`Animation ${n} error:`,e)),this.consecutiveErrorCount===0&&this.onError?.(e,{phase:n,consecutive:1,stopped:!1})}pureAspectUpdate(e){if(e[0]===0&&e[1]===0){let r=this.containerWidth||window.innerWidth,i=this.containerHeight||window.innerHeight,a=r/i;this.aspectRatio=Math.min(un,Math.max(cn,a))}else this.aspectRatio=e[0]/e[1]}};var z=class{constructor(){this.inbound=[];this.listeners=[]}dispatch(e){this.inbound.push(e),this.inbound.length>32&&this.inbound.splice(0,this.inbound.length-32)}consume(){if(this.inbound.length===0)return[];let e=this.inbound;return this.inbound=[],e}emit(e){for(let n of this.listeners)n(e)}onEvent(e){return this.listeners.push(e),()=>{this.listeners=this.listeners.filter(n=>n!==e)}}clear(){this.inbound=[],this.listeners=[]}};function He(t){qe();let{container:e,seed:n,captureKeyboard:r=!1,audio:i,deterministic:a}=t,o=n===void 0?void 0:le(n,a?"deterministic":"live"),c=new z,l=!1,y=null,m,h=!1,b=e.clientWidth||window.innerWidth,w=e.clientHeight||window.innerHeight,d=new me(30,!1,b,w,!1,void 0,{container:e,seed:o,captureKeyboard:r,hostChannel:c,audio:i,deterministic:a}),E=()=>{let C=e.clientWidth||window.innerWidth,P=e.clientHeight||window.innerHeight;return C===d.containerWidth&&P===d.containerHeight?!1:(d.containerWidth=C,d.containerHeight=P,!0)},R=null;return!d.isDeterministic&&typeof ResizeObserver<"u"&&(R=new ResizeObserver(()=>{if(l||!y)return;E()&&h&&d.reinit()}),R.observe(e)),d.onError=(C,P)=>{m?.(C,P)},{get tokenData(){return d.tokenData},get onError(){return m},set onError(C){m=C},mount(C,P={}){if(l)throw new Error("createRuntime: cannot mount on a destroyed runtime");y?.destroy(),h=!1,d.customState=P.initialState,d.setGameManager(P.gameManager);let N=P.onEvent?c.onEvent(P.onEvent):()=>{};if(!d.prepareCart(C))throw N(),new Error("createRuntime: failed to prepare cart");let v=!1,B={async start(){if(!v){if(E()&&d.isPrepared&&(d.init(),d.unloadCart(),!d.prepareCart(C)))throw new Error("createRuntime: failed to prepare cart");d.needsAudio&&(await d.unlockAudio(),v)||(h=!0,d.beginPlayback())}},pause(){v||(d.paused=!0)},resume(){v||(d.paused=!1)},dispatch(S){v||c.dispatch(S)},snapshot(){if(v)return{seed:"",pngDataUrl:""};let S=d.canvas;return{seed:d.tokenData.hash,metadata:C.metadata,pngDataUrl:S?S.toDataURL("image/png"):""}},destroy(){v||(v=!0,h=!1,N(),c.clear(),d.unloadCart(),y===B&&(y=null))},reload(){if(!v){if(!h){if(d.isPrepared&&(d.unloadCart(),!d.prepareCart(C)))throw new Error("createRuntime: failed to prepare cart");return}d.reloadCart()}},reinit(S){if(!v){if(!h){E();return}d.reinit(S)}},getCartState(){return d.animation?.cartState},exportState(){return v?Promise.reject(new Error("Cart handle has been destroyed")):d.exportState()},exportStateJSON(){return v?Promise.reject(new Error("Cart handle has been destroyed")):d.exportStateJSON()},async importState(S,re){if(v)throw new Error("Cart handle has been destroyed");await d.importState(S,re)},peekExportedFramebuffer(){return v?null:d.peekExportedFramebuffer()},peekSeed(){if(!v)return d.peekSeed()},isGenerative(){return v?!1:d.isGenerative()},async step(S=1){if(v)throw new Error("Cart handle has been destroyed");await d.step(S)},async advance(S){if(v)throw new Error("Cart handle has been destroyed");await d.advance(S)},schedule(S){v||d.schedule(S)},getClock(){if(v)throw new Error("Cart handle has been destroyed");return d.getClock()},getRandomState(){if(v)throw new Error("Cart handle has been destroyed");return d.getRandomState()},getReplayMetadata(){return v?Promise.reject(new Error("Cart handle has been destroyed")):d.getReplayMetadata()},get canvas(){return d.canvas},get paused(){return d.paused},set paused(S){v||(d.paused=S)},get tokenData(){return d.tokenData},get isPrepared(){return d.isPrepared},get isLoopRunning(){return d.isLoopRunning},get needsAudio(){return d.needsAudio},get audioLibraries(){return d.audioLibraries}};return y=B,B},async unlockAudio(){await d.unlockAudio()},destroy(){l||(l=!0,y?.destroy(),y=null,R?.disconnect(),R=null,c.clear(),d.destroy())}}}var yt="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",bt=320,vt=180,ln="Headless harness has been destroyed",wt=!1;function Et(){if(typeof globalThis.ImageData>"u"){class e{constructor(r,i){this.colorSpace="srgb";this.width=r,this.height=i,this.data=new Uint8ClampedArray(r*i*4)}}globalThis.ImageData=e}let t=HTMLCanvasElement.prototype;t.getContext=function(){return{globalAlpha:1,imageSmoothingEnabled:!1,fillStyle:"",save(){},restore(){},fillRect(){},putImageData(){},createImageData(r,i){return{data:new Uint8ClampedArray(r*i*4),width:r,height:i}}}},t.toDataURL=()=>yt,wt=!0}function pn(t,e){let n=document.createElement("div");return Object.defineProperty(n,"clientWidth",{value:t,configurable:!0}),Object.defineProperty(n,"clientHeight",{value:e,configurable:!0}),document.body.appendChild(n),n}function hn(t){let e=t.indexOf(","),n=e>=0?t.slice(e+1):t,r=globalThis.atob(n),i=new Uint8Array(r.length);for(let a=0;a<r.length;a++)i[a]=r.charCodeAt(a);return i}function mn(){return typeof process<"u"&&!!process.versions?.node}async function fn(t,e){if(!mn())throw new Error("captureFrame(path) requires Node.js; omit the path to get the snapshot only");let n;try{({writeFile:n}=await import("node:fs/promises"))}catch(r){let i=r instanceof Error?r.message:String(r);throw new Error(`captureFrame(path) could not load node:fs/promises (${i}). Omit the path, or run under Node.`)}try{await n(t,hn(e))}catch(r){let i=r instanceof Error?r.message:String(r);throw new Error(`captureFrame could not write ${t}: ${i}`)}}function gn(t){wt||Et();let e=t.width??bt,n=t.height??vt,r=pn(e,n),i=[],a=[],o=!1,c=t.onEvent,l=t.onError,y=He({container:r,seed:t.seed,deterministic:{origin:t.origin,actions:t.actions??[]}});y.onError=(d,E)=>{a.push({error:d,info:E}),l?.(d,E)},Object.defineProperty(y,"onError",{configurable:!0,enumerable:!0,get(){return l},set(d){l=d}});let m=d=>{i.push(d),c?.(d)},h=y.mount(t.cart,{initialState:t.initialState,gameManager:t.gameManager,onEvent:m}),b=()=>{if(o)throw new Error(ln)};return{runtime:y,container:r,get events(){return i.slice()},get errors(){return a.slice()},get cart(){return h},async step(d=1){b(),await h.step(d)},async advance(d){b(),await h.advance(d)},schedule(d){b(),h.schedule(d)},dispatch(d){b(),h.dispatch(d)},key(d){b(),h.schedule({type:"key",atFrame:h.getClock().framesElapsed,key:d})},click(d,E){b(),h.schedule({type:"pointer",atFrame:h.getClock().framesElapsed,pointer:{kind:"down",x:d,y:E}})},async inspect(){b();let d=await h.getReplayMetadata();return{state:d.state,events:i.slice(),errors:a.slice(),replay:d,clock:h.getClock()}},async captureFrame(d){b();let E=h.snapshot();return d&&await fn(d,E.pngDataUrl),E},remount(d={}){return b(),i.length=0,a.length=0,"onEvent"in d&&(c=d.onEvent),h=y.mount(t.cart,{initialState:d.initialState??t.initialState,gameManager:d.gameManager??t.gameManager,onEvent:m}),h},destroy(){if(!o){o=!0;try{h.destroy()}finally{try{y.destroy()}finally{r.remove()}}}}}}var q="cyberart.state.save",G="cyberart.state.load";function yn(t,e,n={}){let r=n.persist,i=r?{persist:r}:void 0,a=()=>{e.emit({type:q,payload:i})},o=()=>{e.emit({type:G,payload:i})};t.registerAction("w",a,!0),t.registerAction("W",a,!0),t.registerAction("e",o,!0),t.registerAction("E",o,!0)}function St(t){if(t.type!==q&&t.type!==G)return;let e=t.payload?.persist;return e==="localStorage"?e:void 0}var bn="cyberart-io",vn=1,O="framebuffers";function _e(t,e){return`${t||"unknown"}:${e?_(e):"none"}`}function Ct(t,e){return`${t||"unknown"}:${e||"none"}`}function Le(){return new Promise((t,e)=>{let n=indexedDB.open(bn,vn);n.onupgradeneeded=()=>{let r=n.result;r.objectStoreNames.contains(O)||r.createObjectStore(O)},n.onsuccess=()=>t(n.result),n.onerror=()=>e(n.error??new Error("indexedDB open failed"))})}function wn(t){let e=t.data;return e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)}function At(t,e){return new Promise((n,r)=>{let a=t.transaction(O,"readonly").objectStore(O).get(e);a.onsuccess=()=>n(a.result),a.onerror=()=>r(a.error??new Error("indexedDB read failed"))})}async function kt(t,e,n){if(typeof indexedDB>"u")return;let r=await Le(),i={width:n.width,height:n.height,data:wn(n)};try{await new Promise((a,o)=>{let c=r.transaction(O,"readwrite");c.oncomplete=()=>a(),c.onerror=()=>o(c.error??new Error("indexedDB write failed")),c.objectStore(O).put(i,_e(t,e))})}finally{r.close()}}async function xt(t,e){if(typeof indexedDB>"u")return null;let n=await Le();try{let r=_e(t,e),i=Ct(t,e),a=await At(n,r)??(r===i?void 0:await At(n,i));if(!a||!a.data||a.width<1||a.height<1)return null;let o=new ImageData(a.width,a.height);return o.data.set(new Uint8ClampedArray(a.data)),o}finally{n.close()}}async function Tt(t,e){if(typeof indexedDB>"u")return;let n=await Le();try{await new Promise((r,i)=>{let a=n.transaction(O,"readwrite");a.oncomplete=()=>r(),a.onerror=()=>i(a.error??new Error("indexedDB delete failed"));let o=a.objectStore(O);o.delete(_e(t,e)),o.delete(Ct(t,e))})}finally{n.close()}}var Oe="cyberart.state.",En="Saved state",Sn="Loaded state",An="Failed to save state",Cn="Failed to load state",kn="No saved state";function F(t,e){let n=t||"unknown";return e?Oe+n+"."+_(e):Oe+n}function xn(t,e,n){return r=>{if(St(r)==="localStorage"){if(r.type===q){Tn(t,e,n);return}r.type===G&&Rn(t,e,n)}}}function J(t){try{let e=JSON.parse(t);if(!e||typeof e!="object")return null;let n=e;if(typeof n.savedAt=="number"&&n.bundle&&typeof n.bundle=="object")return{savedAt:n.savedAt,bundle:n.bundle};let r=e;return typeof r.seed=="string"&&typeof r.version=="number"?{savedAt:0,bundle:r}:null}catch{return null}}async function Tn(t,e,n){let r=t();if(r)try{let i=await r.exportState(),a=i.cartId??e?.();i.generative===!0?(Ln(a),fe(F(a,i.seed),i)):fe(F(a),i);let o=r.peekExportedFramebuffer?.();if(o)try{await kt(a,i.seed,o)}catch(c){console.warn("Failed to save cart framebuffer",c)}n?.(En,"transient")}catch(i){console.error("Failed to save cart state",i),n?.(An,"transient")}}async function Rn(t,e,n){let r=t();if(!r)return;let i=e?.(),a=r.peekSeed?.(),o=r.isGenerative?.()===!0,c=Hn(i,o,a);if(!c){console.warn("No saved cart state for",i||"unknown",o&&a?`(hash ${a})`:""),n?.(kn,"transient");return}try{let l=await xt(i,c.bundle.seed).catch(()=>null);await r.importState(c.bundle,{framebuffer:l}),n?.(Sn,"transient")}catch(l){console.error("Failed to load cart state",l),n?.(Cn,"transient")}}function fe(t,e){let n=JSON.stringify({savedAt:Date.now(),bundle:e});Pn(t,n)}function Pn(t,e){for(;;)try{localStorage.setItem(t,e);return}catch(n){if(!Dn(n)||!In(t))throw n}}function Dn(t){if(!t||typeof t!="object")return!1;let e=t.name,n=t.code;return e==="QuotaExceededError"||e==="NS_ERROR_DOM_QUOTA_REACHED"||n===22||n===1014}function In(t){let e=null,n=1/0;for(let a of Mn()){if(a===t)continue;let o=localStorage.getItem(a);if(!o)continue;let l=J(o)?.savedAt??0;l<n&&(n=l,e=a)}if(!e)return!1;let r=localStorage.getItem(e),i=r?J(r):null;return localStorage.removeItem(e),i?.bundle&&Tt(i.bundle.cartId,i.bundle.seed).catch(()=>{}),!0}function Mn(){let t=[];for(let e=0;e<localStorage.length;e++){let n=localStorage.key(e);n&&n.startsWith(Oe)&&t.push(n)}return t}function Hn(t,e,n){if(e&&n){let i=localStorage.getItem(F(t,n));if(i)return J(i);let a=localStorage.getItem(F(t));if(!a)return null;let o=J(a);if(!o||!_n(o.bundle,n))return null;try{fe(F(t,n),o.bundle)}catch{}return o}let r=localStorage.getItem(F(t));return r?J(r):null}function _n(t,e){return typeof t.seed=="string"&&_(t.seed)===_(e)}function Ln(t){let e=F(t),n=localStorage.getItem(e);if(!n)return;let r=J(n);if(r&&typeof r.bundle.seed=="string"){let i=F(t,r.bundle.seed);if(!localStorage.getItem(i))try{fe(i,r.bundle)}catch{return}}localStorage.removeItem(e)}var ne=1,Fe="cyberart.diagnostic.rejected",ge=8,On=new Set(["intent","state","diagnostic"]),Fn=new Set([q,G]);function Rt(t){return typeof t=="string"&&On.has(t)}function Pt(t){if(t.kind!==void 0)return Rt(t.kind)?t.kind:void 0;if(typeof t.type!="string"||t.type.length===0)return;if(Fn.has(t.type))return"intent";let e=t.type.split(".");if(e.length<2)return;let n=e[1];return Rt(n)?n:void 0}function Dt(t,e){let n=t.split("."),r=e.split(".");if(n.length!==r.length)return!1;for(let i=0;i<n.length;i++)if(n[i]!=="*"&&n[i]!==r[i])return!1;return!0}function Y(t,e){for(let n of t)if(Dt(n,e))return!0;return!1}function Nn(t){if(t===void 0)return{ok:!0,value:void 0};try{return{ok:!0,value:JSON.parse(JSON.stringify(t))}}catch{return{ok:!1}}}function ye(t,e){if(!t||typeof t.type!="string"||t.type.length===0)return{ok:!1,reason:"malformed",detail:"type is required"};if(t.schemaVersion!==void 0&&t.schemaVersion!==ne)return{ok:!1,reason:"malformed",detail:`unsupported schemaVersion ${String(t.schemaVersion)}`};let n=Pt(t);if(!n)return{ok:!1,reason:"malformed",detail:`cannot infer kind from type "${t.type}"`};let r=Nn(t.payload);if(!r.ok)return{ok:!1,reason:"malformed",detail:"payload is not JSON-serializable"};let i=e.createId(),a=typeof t.correlationId=="string"&&t.correlationId.length>0?t.correlationId:i,o=typeof t.causationId=="string"&&t.causationId.length>0?t.causationId:e.causationId,c=typeof t.target=="string"&&t.target.length>0?t.target:void 0,l=e.maxHops??ge,y=e.parentHops===void 0?l:Math.max(0,e.parentHops-1),m=typeof t.idempotencyKey=="string"&&t.idempotencyKey.length>0?t.idempotencyKey:void 0,h={schemaVersion:ne,type:t.type,kind:n,source:e.source,id:i,correlationId:a,seq:e.seq,hops:y};return c&&(h.target=c),o&&(h.causationId=o),m&&(h.idempotencyKey=m),r.value!==void 0&&(h.payload=r.value),{ok:!0,envelope:h}}var Bn=new Set(["rate-limited","storm-detected","hop-limit","loop-detected","not-subscribed"]),Un=8,Kn=8,Vn=16,Wn=256,$n=["*.intent.*"];function jn(t={}){let e=t.hostSource??"host",n="router",r=t.maxPerTurn??Un,i=t.maxCausationDepth??Kn,a=t.maxHops??ge,o=t.maxCorrelationPerTurn??Vn,c=t.maxIndex??Wn,l=t.maxPerWindow,y=t.windowMs??1e3,m=t.now??Date.now,h=0,b=t.createId??(()=>`evt-${++h}`),w=new Map,d=[],E=new Map,R=new Map,be=new Map,C=0,P=0,N=[],v=!1;function B(s,u,f){for(s.set(u,f);s.size>c;){let g=s.keys().next().value;if(g===void 0)break;s.delete(g)}}function S(s){B(E,s.id,{type:s.type,source:s.source,causationId:s.causationId,hops:s.hops,correlationId:s.correlationId,idempotencyKey:s.idempotencyKey})}function re(s,u){u&&(s.correlationId||(s.correlationId=u.correlationId),s.causationId||(s.causationId=u.id),!s.idempotencyKey&&u.idempotencyKey&&(s.idempotencyKey=`${u.idempotencyKey}::${s.type}`))}function Ne(s,u){if(s){let f=E.get(s);return f?f.hops:a}return u}function Be(s,u){return`${s}\0${u}`}function Ue(s,u){s.idempotencyKey&&B(be,Be(s.source,s.idempotencyKey),u)}function ve(s,u){if(u)return be.get(Be(s,u))}function It(s){return ve(s.source,s.idempotencyKey)}function Mt(s){return s.seq+=1,s.seq}function Ht(s,u){if(!(typeof u.causationId=="string"&&u.causationId.length>0))return s.lastDelivered}function Ke(s){let u=new Set,f=new Set([`${s.type}\0${s.source}`]),g=s.causationId,p=0;for(;g;){if(p+=1,p>i||u.has(g))return!0;u.add(g);let A=E.get(g);if(!A)break;let D=`${A.type}\0${A.source}`;if(f.has(D))return!0;f.add(D),g=A.causationId}return!1}function _t(s){let u=(R.get(s)??0)+1;return R.set(s,u),u}function Ve(s,u){l!==void 0&&(s.emitTimestamps=s.emitTimestamps.filter(f=>u-f<y))}function Lt(s,u){return(u.kind==="state"||u.kind==="diagnostic")&&!s.authoritative?!1:Y(s.emit,u.type)}function We(s){for(let u of d)if(Y(u.patterns,s.type))try{u.listener(s)}catch{}}function Ot(s){if(s.target){let u=w.get(s.target);if(!u||!Y(u.subscribe,s.type))return;u.channel.dispatch(s),u.lastDelivered={id:s.id,hops:s.hops,correlationId:s.correlationId,idempotencyKey:s.idempotencyKey};return}for(let u of w.values())Y(u.subscribe,s.type)&&(u.channel.dispatch(s),u.lastDelivered={id:s.id,hops:s.hops,correlationId:s.correlationId,idempotencyKey:s.idempotencyKey})}function Ft(s,u,f,g,p,A,D){let H={reason:g,eventType:u,source:f};p&&(H.detail=p),P+=1;let X=b(),U={schemaVersion:ne,type:Fe,kind:"diagnostic",source:n,id:X,correlationId:A??X,seq:P,hops:0,payload:H};D&&(U.causationId=D),S(U);let ze=w.get(s);ze&&ze.channel.dispatch(U),We(U)}function x(s,u,f,g,p){Ft(s,p?.type??(typeof u.type=="string"?u.type:""),p?.source??s,f,g,p?.correlationId,p?.id),p?.idempotencyKey&&!Bn.has(f)&&Ue(p,{status:"rejected"})}function Nt(s){S(s),Ue(s,{status:"accepted",envelope:s}),_t(s.correlationId),We(s),Ot(s)}function $e(s,u,f,g){if(!f.ok){x(s,u,"malformed",f.detail);return}let p=f.envelope,A=It(p);if(A)return A.status==="accepted"?A.envelope:void 0;if(p.hops<1){x(s,u,"hop-limit","no hops remaining",p);return}if(p.target&&p.target!==e){let D=w.get(p.target);if(!D){x(s,u,"unknown-target",`unknown target "${p.target}"`,p);return}if(!Y(D.subscribe,p.type)){x(s,u,"not-subscribed",`target "${p.target}" does not subscribe to ${p.type}`,p);return}}if(g){if(!Lt(g,p)){x(s,u,"unauthorized","source may not emit this event",p);return}if(g.emitsThisTurn+=1,g.emitsThisTurn>r){x(s,u,"rate-limited","maxPerTurn exceeded",p);return}if(l!==void 0){let H=m();if(Ve(g,H),g.emitTimestamps.push(H),g.emitTimestamps.length>l){x(s,u,"rate-limited","maxPerWindow exceeded",p);return}}if(Ke(p)){x(s,u,"loop-detected","causation chain cycle or depth exceeded",p);return}if((R.get(p.correlationId)??0)+1>o){x(s,u,"storm-detected","correlationId storm",p);return}if(t.validate){let H;try{H=t.validate(p)}catch(X){let U=X instanceof Error?X.message:"validate threw";x(s,u,"host-rejected",U,p);return}if(H!==!0){x(s,u,"host-rejected",H.detail,p);return}}}else{if(Ke(p)){x(s,u,"loop-detected","causation chain cycle or depth exceeded",p);return}if((R.get(p.correlationId)??0)+1>o){x(s,u,"storm-detected","correlationId storm",p);return}}return Nt(p),p}function Bt(s,u){let f=w.get(s);if(!f)return;let g={...u},p=Ht(f,g);if(re(g,p),ve(f.id,g.idempotencyKey))return;let D=Mt(f),H=ye(g,{source:f.id,createId:b,seq:D,causationId:p?.id,maxHops:a,parentHops:Ne(g.causationId??p?.id,p?.hops)});$e(s,g,H,f)}function Ut(s,u){let f=u?.cause,g={...s};re(g,f);let p=ve(e,g.idempotencyKey);if(p)return p.status==="accepted"?p.envelope:void 0;C+=1;let A=ye(g,{source:e,createId:b,seq:C,causationId:g.causationId,maxHops:a,parentHops:Ne(g.causationId,f?.hops)});return $e(e,g,A,void 0)}function Kt(){if(!v){v=!0;try{for(;N.length>0;){let s=N.shift();s&&(s.kind==="cart"?Bt(s.participantId,s.event):s.result=Ut(s.event,s.extras))}}finally{v=!1}}}function je(s){N.push(s),Kt()}return{attach(s,u,f={}){if(s===e||s===n)throw new Error(`createEventRouter: "${s}" is a reserved participant id`);for(let A of w.values())if(A.channel===u&&A.id!==s)throw new Error(`createEventRouter: channel already attached as "${A.id}"`);w.get(s)?.unsubscribe();let p={id:s,channel:u,emit:f.emit??$n,subscribe:f.subscribe??[],authoritative:f.authoritative===!0,seq:0,emitsThisTurn:0,emitTimestamps:[],unsubscribe:()=>{}};p.unsubscribe=u.onEvent(A=>{je({kind:"cart",participantId:s,event:A})}),w.set(s,p)},detach(s){let u=w.get(s);u&&(u.unsubscribe(),w.delete(s))},publish(s,u){let f={kind:"publish",event:s,extras:u};return je(f),f.result},subscribe(s,u){let f={patterns:s,listener:u};return d.push(f),()=>{let g=d.indexOf(f);g>=0&&d.splice(g,1)}},turn(){R.clear();for(let s of w.values())s.emitsThisTurn=0,s.lastDelivered=void 0,l!==void 0&&Ve(s,m())}}}export{De as ASSET_FAILED_EVENT,Pe as ASSET_READY_EVENT,ie as CYBERART_CANVAS_ATTR,vt as DEFAULT_HEADLESS_HEIGHT,bt as DEFAULT_HEADLESS_WIDTH,ge as DEFAULT_MAX_HOPS,ne as EVENT_ENVELOPE_VERSION,yt as HEADLESS_PNG_DATA_URL,z as HostChannel,k as IncompatibleCartStateError,ae as KeyboardManager,se as PointerManager,Fe as REJECTED_EVENT_TYPE,Q as Random,xn as attachCartStatePersistence,ce as canonicalizeSeed,jn as createEventRouter,gn as createHeadlessHarness,He as createRuntime,oe as createVirtualClock,W as createWallClock,ft as describeReplayMismatch,Pt as inferEventKind,Et as installHeadlessCanvas,Dt as matchEventPattern,ye as normalizeEvent,yn as registerCartStateHotkeys,le as resolveRuntimeSeed};
@@ -0,0 +1,132 @@
1
+ # Deterministic mode
2
+
3
+ Host-controlled time, input, and asset completion so two runs with the same seed, actions, and `step` count match. Production kaleidoscope / Art Blocks **leave `deterministic` unset** (rAF, `performance.now()`, token hash).
4
+
5
+ CI should use the [headless harness](headless-harness.md) on this path, not a mock engine. Router hosts: [events](events.md).
6
+
7
+ Back to the [package README](../README.md).
8
+
9
+ ## Opt in
10
+
11
+ ```ts
12
+ import {
13
+ createRuntime,
14
+ ASSET_READY_EVENT,
15
+ describeReplayMismatch,
16
+ } from '@cyberart-io/engine';
17
+
18
+ const runtime = createRuntime({
19
+ container,
20
+ seed: 42, // or a 64-hex token hash — always canonicalized in this mode
21
+ deterministic: {
22
+ origin: 0,
23
+ actions: [
24
+ { type: 'asset', atFrame: 2, id: 'room', status: 'ready' },
25
+ { type: 'pointer', atFrame: 7, pointer: { kind: 'down', x: 40, y: 20 } },
26
+ { type: 'key', atFrame: 8, key: 'x' },
27
+ { type: 'event', atFrame: 9, event: { type: 'host.ping', payload: { n: 1 } } },
28
+ ],
29
+ },
30
+ });
31
+ const cart = runtime.mount(artProject);
32
+ await cart.step(10);
33
+ const replay = await cart.getReplayMetadata();
34
+ ```
35
+
36
+ `deterministic: true` is the same as `{ origin: 0, actions: [] }`. Live DOM keyboard/pointer listeners are off; inject with `schedule` / harness `click` / `key`.
37
+
38
+ `createRuntime` uses `resolveRuntimeSeed(seed, 'deterministic')`: every seed is mixed into a 64-hex hash. Live mode keeps a short `?hash=` string as `0x…` so kaleidoscope local hashes do not change.
39
+
40
+ ## `DeterministicRuntimeOptions`
41
+
42
+ | Option | Default | Meaning |
43
+ |---|---|---|
44
+ | `origin` | `0` | Virtual clock start in ms. |
45
+ | `actions` | `[]` | Applied at the start of `atFrame`, before `update`. Each fires once. |
46
+
47
+ ## Clock
48
+
49
+ | Export | Role |
50
+ |---|---|
51
+ | `createVirtualClock(origin?)` | `{ now, set, advance }`. Mode constructs one from `origin` unless you inject a clock on the manager (not via `createRuntime` options today). |
52
+ | `createWallClock()` | `{ now: () => performance.now() }`. Live playback. |
53
+ | `Clock` | `{ now(): number }`. |
54
+ | `VirtualClock` | `Clock` plus `set(ms)` / `advance(ms)`. |
55
+
56
+ `cart.getClock()` returns `ClockSnapshot`: `{ now, framesElapsed, frameRate }`.
57
+
58
+ `step` advances the virtual clock by `1000 / frameRate` ms **after** each successful tick. A throwing `update` does **not** increment `framesElapsed` and `step(n)` stops after that tick.
59
+
60
+ ## `CartHandle` (deterministic)
61
+
62
+ | Member | Meaning |
63
+ |---|---|
64
+ | `step(frames?)` | Run that many update/render ticks. Default `1`. No rAF. Clears `paused` so `timing.now` advances. Throws if `deterministic` was not set. Throws if the handle is destroyed. |
65
+ | `advance(ms)` | `step(round(ms * fps / 1000))`. |
66
+ | `schedule(action)` | Append a `ScriptedAction` for a future `atFrame`. |
67
+ | `getClock()` | Virtual clock snapshot. |
68
+ | `getRandomState()` | Dual sfc32 snapshot (`RandomState`). |
69
+ | `getReplayMetadata()` | Full replay bundle (below). |
70
+
71
+ `atFrame` is the `framesElapsed` passed into that tick’s `update`. The first tick is `0`. After `step(5)`, `framesElapsed === 5` and that value **is** the next `atFrame`.
72
+
73
+ Remount / `prepareCart` restores the **constructor** action list. `schedule` / harness `click` / `key` from the previous mount are dropped; constructor `actions` run again.
74
+
75
+ ## `ScriptedAction`
76
+
77
+ Every action has `atFrame: number`, plus one of:
78
+
79
+ | `type` | Fields | Effect |
80
+ |---|---|---|
81
+ | `pointer` | `pointer: { kind: 'down' \| 'move' \| 'up', x, y }` | `PointerManager.inject` in **canvas pixels** (not CSS). `down` pushes a click for `hasClick` / `consumeClick`. |
82
+ | `key` | `key: string` | `KeyboardManager.inject`. The cart must `registerAction` in `getDefaultState`. |
83
+ | `event` | `event: HostEvent` | `hostChannel.dispatch` before `update`. |
84
+ | `asset` | `id`, `status: 'ready' \| 'failed'`, optional `detail` | Dispatch a host **state** event (not a Promise). |
85
+
86
+ Asset events:
87
+
88
+ | Export | Value |
89
+ |---|---|
90
+ | `ASSET_READY_EVENT` | `'cyberart.asset.ready'` |
91
+ | `ASSET_FAILED_EVENT` | `'cyberart.asset.failed'` |
92
+
93
+ Payload is `{ id }` or `{ id, detail }`. Carts that need assets `hostChannel.consume()`; they must not `await fetch` inside `update` / `render`.
94
+
95
+ ## Replay
96
+
97
+ `getReplayMetadata()` → `ReplayMetadata`:
98
+
99
+ | Field | Meaning |
100
+ |---|---|
101
+ | `seed` | Token hash. |
102
+ | `clock` | `ClockSnapshot`. |
103
+ | `rng` | `RandomState`. |
104
+ | `actions` | Current scripted tape (constructor + `schedule`). |
105
+ | `applied` | `{ frame, action }[]` already fired. |
106
+ | `events` | Outbound `hostChannel.emit` log. |
107
+ | `state` | Exported serializable cart state. |
108
+
109
+ `describeReplayMismatch(a, b)` returns `string[]`. Empty means identical; otherwise each string names the first disagreement on that field (`seed`, `framesElapsed`, `now`, `frameRate`, `rng`, `actions[i]`, `applied[i]`, `events[i]`, `state`).
110
+
111
+ ## Cart rules
112
+
113
+ Use `R` and `timing.now` / `timing.elapsedSinceStart`. Do **not** call `Math.random()`, `Date.now()`, `performance.now()`, `setTimeout`, or `requestAnimationFrame` from `update` / `render`.
114
+
115
+ Host routers: `now: () => cart.getClock().now`, seed-stable `createId`, `router.turn()` once per `step`. See [events](events.md).
116
+
117
+ ## Seed and RNG
118
+
119
+ - `canonicalizeSeed(value)` → 64-hex hash.
120
+ - `resolveRuntimeSeed(seed, 'deterministic' \| 'live')` — deterministic always canonicalizes.
121
+ - `Random.getState()` / `setState()` — `setState` throws if the snapshot seed does not match.
122
+ - `metadata.generative: true` — saves only load onto the same hash.
123
+
124
+ ## Errors
125
+
126
+ `runtime.onError(error, info)` with `FrameErrorInfo`: `{ phase: 'update' \| 'render' \| 'draw', consecutive, stopped }`.
127
+
128
+ The first throw is `{ consecutive: 1, stopped: false }`. The breaker is **120** consecutive failures (`stopped: true`). One throwing frame does not increment `framesElapsed`.
129
+
130
+ ## Related types
131
+
132
+ `AppliedAction`, `ClockSnapshot`, `DeterministicRuntimeOptions`, `ReplayMetadata`, `ScriptedAction`, `RandomState`, `FrameErrorInfo`.
package/docs/events.md ADDED
@@ -0,0 +1,203 @@
1
+ # Events and router
2
+
3
+ Mailbox (one runtime) and `createEventRouter` (many carts). Carts never receive the router object or another cart’s `HostChannel`. Related: [deterministic mode](deterministic-mode.md) (`now` / `createId` / `turn` per `step`).
4
+
5
+ Back to the [package README](../README.md).
6
+
7
+ ## When to use which
8
+
9
+ | Path | Use |
10
+ |---|---|
11
+ | Mailbox only | One cart, host `dispatch` / cart `emit`. No permissions, hops, or loop checks. |
12
+ | Router | Several carts, host reducer in the middle, Adventure-style intent vs state. |
13
+
14
+ `createRuntime` does **not** attach a router. You attach each cart’s channel yourself.
15
+
16
+ ## Mailbox (`HostChannel`)
17
+
18
+ ```ts
19
+ import { HostChannel, type HostEvent } from '@cyberart-io/engine';
20
+
21
+ // Usually you do not construct this. createRuntime owns one; mount({ onEvent }) listens.
22
+ cart.dispatch({ type: 'art-project.theme', payload: 'dusk' }); // host → cart
23
+ hostChannel.consume(); // cart drains inbound (typically in update)
24
+ hostChannel.emit({ type: 'art-project.theme', payload: 'dusk' }); // cart → host
25
+ ```
26
+
27
+ `HostEvent` is `EventInput`: at least `{ type, payload? }`. Extra envelope fields are optional on an unattached mailbox.
28
+
29
+ | Member | Meaning |
30
+ |---|---|
31
+ | `dispatch(event)` | Queue inbound. Oldest dropped past **32** unconsumed events. |
32
+ | `consume()` | Return and clear the inbound batch. Empty array if none. |
33
+ | `emit(event)` | Notify `onEvent` listeners (host and, if attached, the router). |
34
+ | `onEvent(listener)` | Subscribe. Returns an unsubscribe function. |
35
+ | `clear()` | Drop inbound queue and listeners. |
36
+
37
+ Carts that never mention `hostChannel` ignore both directions. Guard it: the argument is undefined when the cart is not mounted through `createRuntime`.
38
+
39
+ ## Envelope
40
+
41
+ Routed events are normalized to `EventEnvelope` (`EVENT_ENVELOPE_VERSION = 1`).
42
+
43
+ | Field | Set by | Meaning |
44
+ |---|---|---|
45
+ | `schemaVersion` | router | Always `1`. Other versions are `malformed`. |
46
+ | `type` | emitter | Dotted name, e.g. `adventure.intent.exit-requested`. |
47
+ | `kind` | explicit or inferred | `intent` \| `state` \| `diagnostic`. |
48
+ | `source` | router | Participant id (or `host` / `router`). Claimed `source` is overwritten. |
49
+ | `target` | emitter | Optional **participant id**, not a domain object (put the exit id in `payload`). |
50
+ | `id` | router (`createId`) | Claimed `id` is ignored. |
51
+ | `correlationId` | emitter or `id` | Groups a request and its follow-ups. |
52
+ | `causationId` | cause / auto-link | Parent event `id`. |
53
+ | `seq` | router | Monotonic **per source**. No global order across sources. |
54
+ | `hops` | router | Remaining TTL. Claimed hop counts are ignored. |
55
+ | `idempotencyKey` | emitter | Replay-safe emit; scoped by `source`. |
56
+ | `payload` | emitter | JSON-serializable. Cycles / BigInt → `malformed`. |
57
+
58
+ `kind` on the input wins. Otherwise:
59
+
60
+ - `cyberart.state.save` / `cyberart.state.load` are **intents** (historical names).
61
+ - Else the second dotted segment of `type` if it is `intent`, `state`, or `diagnostic`.
62
+ - Else `malformed` (`cannot infer kind`).
63
+
64
+ Only the host (`publish`) or an `authoritative` participant may emit `state` / `diagnostic`. Carts emit allowed `intent` patterns. Domain data belongs in `payload`.
65
+
66
+ ## `createEventRouter(options?)`
67
+
68
+ ```ts
69
+ import { createEventRouter, HostChannel } from '@cyberart-io/engine';
70
+
71
+ const router = createEventRouter({
72
+ now: () => cart.getClock().now,
73
+ createId: () => `evt-${seq++}`, // seed-stable in deterministic tests
74
+ validate: (event) => true, // or { reason: 'host-rejected', detail: '…' }
75
+ });
76
+
77
+ router.attach('presentation', presentationChannel, {
78
+ emit: ['adventure.intent.*'],
79
+ subscribe: ['adventure.state.*', 'cyberart.diagnostic.rejected'],
80
+ });
81
+
82
+ router.subscribe(['adventure.intent.*'], (event) => {
83
+ router.publish(
84
+ { type: 'adventure.state.room-changed', kind: 'state', payload: { roomId: 'brook' } },
85
+ { cause: event },
86
+ );
87
+ });
88
+
89
+ router.turn(); // once per step / frame
90
+ ```
91
+
92
+ ### Options (`EventRouterOptions`)
93
+
94
+ | Option | Default | Meaning |
95
+ |---|---|---|
96
+ | `validate` | none | Sync hook on **cart-originated** events only. Return `true` or `{ reason: 'host-rejected', detail? }`. |
97
+ | `createId` | `evt-1`, `evt-2`, … | Envelope ids. Inject a seed-stable function for replays. |
98
+ | `now` | `Date.now` | Sliding window timestamps. Inject `() => cart.getClock().now` in deterministic mode. |
99
+ | `hostSource` | `'host'` | `source` on host `publish`. |
100
+ | `maxPerTurn` | `8` | Cart emits per source per `turn()`. Excess → `rate-limited`. |
101
+ | `maxPerWindow` | off | Cart emits per source in `windowMs`. Excess → `rate-limited`. |
102
+ | `windowMs` | `1000` | Window for `maxPerWindow`. |
103
+ | `maxCausationDepth` | `8` | Causation-chain depth / cycle → `loop-detected`. |
104
+ | `maxHops` | `DEFAULT_MAX_HOPS` (`8`) | Remaining hops on a new root. A caused follow-up gets `cause.hops - 1`. At 0 → `hop-limit`. |
105
+ | `maxCorrelationPerTurn` | `16` | Events sharing a `correlationId` per turn → `storm-detected`. |
106
+ | `maxIndex` | `256` | Bound on causation / idempotency maps (oldest keys dropped). |
107
+
108
+ ### Router methods (`EventRouter`)
109
+
110
+ | Method | Meaning |
111
+ |---|---|
112
+ | `attach(id, channel, options?)` | Bind a cart mailbox. `id` is the participant source. Re-attach replaces. |
113
+ | `detach(id)` | Unbind. In-flight targeted emits to this id fail `unknown-target`. |
114
+ | `publish(event, extras?)` | Host emit. `extras.cause` sets causation and inherited idempotency/correlation. Returns the envelope, or `undefined` if rejected. |
115
+ | `subscribe(patterns, listener)` | Host listener (not a cart). Returns unsubscribe. |
116
+ | `turn()` | Reset per-turn budgets and the auto-link causation cursor. Idempotency records **survive**. Call once per `step`. |
117
+
118
+ ### `AttachOptions`
119
+
120
+ | Option | Default | Meaning |
121
+ |---|---|---|
122
+ | `emit` | `['*.intent.*']` | Types this cart may emit. |
123
+ | `subscribe` | `[]` | Types delivered to this channel. |
124
+ | `authoritative` | `false` | If true, may emit `state` / `diagnostic` **when those types are also in `emit`**. |
125
+
126
+ Patterns are dotted segments; `*` matches one segment. `adventure.intent.*` matches `adventure.intent.exit-requested`, not `adventure.intent.exit.requested`.
127
+
128
+ ## Delivery and ordering
129
+
130
+ - FIFO **per source**, monotonic `seq`.
131
+ - No global order across sources unless the host serializes `publish`.
132
+ - Delivery is always queued `dispatch`. A cart sees routed events on the next `consume()`.
133
+ - An explicit `target` that is missing is `unknown-target`. A target that is attached but does not subscribe is `not-subscribed` (not cached; a later `attach` can retry).
134
+
135
+ Cart-to-cart still goes through the router:
136
+
137
+ ```ts
138
+ presentationChannel.emit({
139
+ type: 'adventure.intent.remark',
140
+ target: 'npc',
141
+ payload: { text: 'Anyone there?' },
142
+ idempotencyKey: 'hello-1',
143
+ });
144
+ ```
145
+
146
+ ## Hops, idempotency, storms
147
+
148
+ - New root: `hops = maxHops`. Follow-up: `cause.hops - 1`. Claimed hop counts ignored.
149
+ - At 0 the router rejects `hop-limit` without consulting the causation index (replayed logs stay bounded).
150
+ - `idempotencyKey` is scoped by `source`. Terminal outcomes (accept, or a **non-transient** reject) are remembered; a second emit with the same key returns the original envelope and does not fan out.
151
+ - Transient rejects are **not** cached: `rate-limited`, `storm-detected`, `hop-limit`, `loop-detected`, `not-subscribed`. Retry after `turn()` or a later `attach`.
152
+ - Host `publish({ cause })` and same-turn cart follow-ups inherit `${cause.idempotencyKey}::${type}` and the parent `correlationId` when the follow-up omits its own.
153
+
154
+ ## Rejections
155
+
156
+ Type `REJECTED_EVENT_TYPE` (`cyberart.diagnostic.rejected`). Payload (`RejectionPayload`):
157
+
158
+ | Field | Meaning |
159
+ |---|---|
160
+ | `reason` | See below. |
161
+ | `detail` | Optional string. |
162
+ | `eventType` | The rejected type. |
163
+ | `source` | Claimed / assigned source. |
164
+
165
+ | `reason` | When |
166
+ |---|---|
167
+ | `malformed` | Bad type, kind, schemaVersion, or non-JSON payload. |
168
+ | `unauthorized` | Cart emitted a type/kind not on its allowlist. |
169
+ | `host-rejected` | `validate` returned reject. |
170
+ | `rate-limited` | `maxPerTurn` or `maxPerWindow`. |
171
+ | `loop-detected` | Causation cycle or `maxCausationDepth`. |
172
+ | `storm-detected` | `maxCorrelationPerTurn` on one `correlationId`. |
173
+ | `unknown-target` | `target` is not attached. |
174
+ | `not-subscribed` | `target` attached but that type is not in `subscribe`. |
175
+ | `hop-limit` | Remaining hops hit 0. |
176
+
177
+ ## Helpers (exported)
178
+
179
+ | Export | Role |
180
+ |---|---|
181
+ | `EVENT_ENVELOPE_VERSION` | `1`. |
182
+ | `DEFAULT_MAX_HOPS` | `8`. |
183
+ | `REJECTED_EVENT_TYPE` | `'cyberart.diagnostic.rejected'`. |
184
+ | `inferEventKind(input)` | Kind from `kind` or type name. |
185
+ | `matchEventPattern(pattern, type)` | One-segment `*` glob. |
186
+ | `normalizeEvent(input, context)` | Build an envelope or `{ ok: false, reason: 'malformed', detail }`. The router calls this; hosts rarely need it. |
187
+
188
+ Types: `EventEnvelope`, `EventInput`, `EventKind`, `NormalizeContext`, `NormalizeResult`, `RejectionPayload`, `RejectionReason`, `AttachOptions`, `EventRouter`, `EventRouterOptions`, `PublishExtras`, `ValidateResult`.
189
+
190
+ `matchesAnyPattern` / `clonePayload` / `isEventKind` are not on the public package surface.
191
+
192
+ ## Deterministic hosts
193
+
194
+ ```ts
195
+ const router = createEventRouter({
196
+ now: () => cart.getClock().now,
197
+ createId: () => `evt-${stableCounter++}`,
198
+ });
199
+ await cart.step(1);
200
+ router.turn();
201
+ ```
202
+
203
+ See [deterministic mode](deterministic-mode.md).
@@ -0,0 +1,144 @@
1
+ # Headless harness
2
+
3
+ CI / agent wrapper around production `createRuntime({ deterministic })`. Not a second engine. Do not call this from Player or kaleidoscope.
4
+
5
+ Depends on [deterministic mode](deterministic-mode.md). Host reducers: [events](events.md).
6
+
7
+ Back to the [package README](../README.md).
8
+
9
+ ## One-command reproduce (this repo)
10
+
11
+ ```bash
12
+ pnpm exec vitest run packages/engine/src/canvas/headlessHarness.spec.ts
13
+ ```
14
+
15
+ Package consumers copy the loop below into their own test file (Vitest + jsdom or equivalent).
16
+
17
+ ## Canvas install (test-only)
18
+
19
+ ```ts
20
+ import {
21
+ installHeadlessCanvas,
22
+ HEADLESS_PNG_DATA_URL,
23
+ DEFAULT_HEADLESS_WIDTH,
24
+ DEFAULT_HEADLESS_HEIGHT,
25
+ } from '@cyberart-io/engine';
26
+
27
+ installHeadlessCanvas();
28
+ ```
29
+
30
+ Mutates `HTMLCanvasElement.prototype`: `ImageData` polyfill (width/height constructor), stub `getContext` (`fillRect`, `putImageData`, `createImageData`), `toDataURL` → `HEADLESS_PNG_DATA_URL` (a real 1×1 PNG, not visual `fillRect` pixels). Idempotent. **Do not call from production playback.**
31
+
32
+ `createHeadlessHarness` calls this for you. Default container size is `DEFAULT_HEADLESS_WIDTH` × `DEFAULT_HEADLESS_HEIGHT` (320×180).
33
+
34
+ ## `createHeadlessHarness(options)`
35
+
36
+ ```ts
37
+ import { createHeadlessHarness } from '@cyberart-io/engine';
38
+
39
+ const harness = createHeadlessHarness({
40
+ cart: artProject,
41
+ seed: 42,
42
+ width: 320,
43
+ height: 180,
44
+ origin: 0,
45
+ actions: [{ type: 'asset', atFrame: 0, id: 'room-map', status: 'ready' }],
46
+ initialState: { room: 'glade' },
47
+ gameManager: hostAdapter, // existing mount injection; not a new adapter contract
48
+ onEvent: (event) => {},
49
+ onError: (error, info) => {},
50
+ });
51
+ ```
52
+
53
+ ### Options (`CreateHeadlessHarnessOptions`)
54
+
55
+ | Option | Default | Meaning |
56
+ |---|---|---|
57
+ | `cart` | required | `AnimationCart` to mount. |
58
+ | `seed` | generated | Passed to `createRuntime` (canonicalized). |
59
+ | `width` / `height` | `320` / `180` | `clientWidth` / `clientHeight` on the container. Canvas buffer is `getDrawDimensions × devicePixelRatio`. |
60
+ | `origin` | `0` | Virtual clock origin (ms). |
61
+ | `actions` | `[]` | Constructor `ScriptedAction` tape. |
62
+ | `initialState` | none | `mount` boot overrides (`customState`). |
63
+ | `gameManager` | none | Opaque host adapter into `getDefaultState` / `update`. |
64
+ | `onEvent` | none | Also forwarded; the harness still logs outbound events. |
65
+ | `onError` | none | Also forwarded; the harness still logs structured errors. Wire happens **before** `mount`. |
66
+
67
+ Always sets `deterministic: { origin, actions }`.
68
+
69
+ ## Handle (`HeadlessHarness`)
70
+
71
+ | Member | Meaning |
72
+ |---|---|
73
+ | `runtime` | The `CyberArtRuntime`. Assigning `runtime.onError` replaces **your** handler; the harness still records errors. |
74
+ | `container` | Sized mount element (on `document.body` until `destroy`). |
75
+ | `cart` | Current `CartHandle` (updates on `remount`). |
76
+ | `events` / `errors` | **Copies** of the logs. Mutating them does not change internals. Prefer `inspect()` for a snapshot. |
77
+ | `step(frames?)` / `advance(ms)` | Deterministic ticks. Throw after `destroy()`. |
78
+ | `schedule(action)` | Pass-through `ScriptedAction`. |
79
+ | `dispatch(event)` | Immediate mailbox enqueue. Cart `consume()`s on the next `update`. |
80
+ | `click(x, y)` | Pointer-**down** at `getClock().framesElapsed` (next tick). Canvas pixels, not CSS. Use `schedule` for `move` / `up`. |
81
+ | `key(key)` | Same next-frame `schedule` for a key. Cart must `registerAction`. |
82
+ | `inspect()` | `{ state, events, errors, replay, clock }`. `state` is **exported**, not live `getCartState()`. |
83
+ | `captureFrame(path?)` | `cart.snapshot()`. Optional `path` writes PNG bytes (Node only). Stub-stable. |
84
+ | `remount(options?)` | `runtime.mount` again while alive. Constructor `actions` replay; prior `click`/`key`/`schedule` dropped. `'onEvent' in options` replaces the listener; `{ onEvent: undefined }` clears it. Invalid after `destroy()`. |
85
+ | `destroy()` | Unload cart, destroy runtime, remove container. Idempotent. Always removes the container even if unload throws. |
86
+
87
+ Repeated mount/destroy = a **new** `createHeadlessHarness` (or `remount` then `destroy`). Do not `remount` after `destroy()`.
88
+
89
+ ### Click / clock
90
+
91
+ After `step(5)`, `framesElapsed === 5`. `click` / `key` schedule at that index; `step(1)` applies them. Hit-test pointer against `dimensionContext` (drawing space), not the CSS box.
92
+
93
+ ### `inspect()` (`HeadlessInspect`)
94
+
95
+ | Field | Meaning |
96
+ |---|---|
97
+ | `state` | `exportState().state` / replay `state`. |
98
+ | `events` | Outbound log copy. |
99
+ | `errors` | `{ error, info: FrameErrorInfo }[]`. |
100
+ | `replay` | `getReplayMetadata()`. |
101
+ | `clock` | `getClock()`. |
102
+
103
+ First frame throw: `{ phase: 'update', consecutive: 1, stopped: false }`. See [deterministic mode](deterministic-mode.md) for the 120-frame breaker.
104
+
105
+ ### `captureFrame(path?)`
106
+
107
+ Returns `CartSnapshot`: `{ seed, metadata?, pngDataUrl }`.
108
+
109
+ - Omit `path` for in-memory snapshot (`pngDataUrl` is `HEADLESS_PNG_DATA_URL` under the stub).
110
+ - With `path`: Node `fs.promises.writeFile` of decoded PNG bytes. Throws a harness message if not Node, if `node:fs/promises` cannot load, or if the write fails (missing directory, etc.). Write under `os.tmpdir()`, not the repo.
111
+
112
+ ## Interaction loop (agent recipe)
113
+
114
+ ```ts
115
+ const harness = createHeadlessHarness({
116
+ cart,
117
+ seed: 42,
118
+ gameManager: adapter,
119
+ actions: [{ type: 'asset', atFrame: 0, id: 'room-map', status: 'ready' }],
120
+ });
121
+
122
+ await harness.step(5);
123
+ const canvas = harness.cart.canvas!;
124
+ harness.click(canvas.width / 2, canvas.height * 0.1); // canvas pixels
125
+ await harness.step(1);
126
+
127
+ const { events, state } = await harness.inspect();
128
+ // assert exactly one intent, e.g. room.exit.requested
129
+
130
+ harness.dispatch({
131
+ type: 'adventure.state.room-changed',
132
+ payload: { roomId: 'brook' },
133
+ });
134
+ await harness.step(1);
135
+
136
+ await harness.captureFrame('/tmp/brook.png');
137
+ harness.destroy();
138
+ ```
139
+
140
+ Cleanup: cart `teardown` runs on unload; after `destroy`, `step` throws and the container is gone. `afterEach(() => harness.destroy())` so a failed assertion does not leak nodes.
141
+
142
+ ## Types
143
+
144
+ `CreateHeadlessHarnessOptions`, `HeadlessHarness`, `HeadlessInspect`, `HeadlessFrameError`.
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@cyberart-io/engine",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Embeddable CyberArt host engine: mount a cart, pause, snapshot, and save/load state.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "files": [
8
8
  "dist/index.js",
9
- "dist/index.d.ts"
9
+ "dist/index.d.ts",
10
+ "docs"
10
11
  ],
11
12
  "main": "./dist/index.js",
12
13
  "module": "./dist/index.js",