@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 +62 -10
- package/dist/index.d.ts +350 -8
- package/dist/index.js +1 -1
- package/docs/deterministic-mode.md +132 -0
- package/docs/events.md +203 -0
- package/docs/headless-harness.md +144 -0
- package/package.json +3 -2
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
|
-
|
|
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
|
-
|
|
233
|
+
Full options, `click` clock rule, Node-only `captureFrame`, remount, and the reproduce command: [headless harness](docs/headless-harness.md).
|
|
193
234
|
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
/**
|
|
266
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|