@cyberart-io/engine 0.0.3 → 0.0.4
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 +77 -16
- package/dist/headless.d.ts +169 -1
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +366 -1
- package/dist/index.js +1 -1
- package/docs/capability-manifest.md +84 -0
- package/docs/deterministic-mode.md +1 -1
- package/docs/events.md +8 -65
- package/docs/headless-harness.md +3 -3
- package/docs/normalized-geometry.md +88 -0
- package/docs/presentation-adapter.md +2 -2
- package/docs/presentation-cue.md +1 -1
- package/docs/runtime-group.md +120 -0
- package/package.json +2 -2
package/docs/events.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Events and router
|
|
2
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`), [presentation adapter](presentation-adapter.md) (`presentation.state.model`), [
|
|
3
|
+
Mailbox (one runtime) and `createEventRouter` (many carts). Prefer `createRuntimeGroup` when several carts should share one router and lockstep clock. Carts never receive the router object or another cart’s `HostChannel`. Related: [deterministic mode](deterministic-mode.md) (`now` / `createId` / `turn` per `step`), [presentation adapter](presentation-adapter.md) (`presentation.state.model`), [runtime group](runtime-group.md), [capability manifest](capability-manifest.md).
|
|
4
4
|
|
|
5
5
|
Back to the [package README](../README.md).
|
|
6
6
|
|
|
@@ -10,14 +10,16 @@ Back to the [package README](../README.md).
|
|
|
10
10
|
pnpm exec vitest run packages/engine/src/canvas/cyb-59-event-contract.repro.spec.ts
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
## When to use which
|
|
14
15
|
|
|
15
16
|
| Path | Use |
|
|
16
17
|
|---|---|
|
|
17
18
|
| Mailbox only | One cart, host `dispatch` / cart `emit`. No permissions, hops, or loop checks. |
|
|
18
19
|
| Router | Several carts, host reducer in the middle, Adventure-style intent vs state. |
|
|
20
|
+
| Runtime group | Same router plus lockstep `step`, pause/reset/teardown, and a routed trace. |
|
|
19
21
|
|
|
20
|
-
`createRuntime` does **not** attach a router. Attach `runtime.hostChannel` yourself. Cart unload clears the inbound queue only; router listeners stay until `runtime.destroy()
|
|
22
|
+
`createRuntime` does **not** attach a router. Attach `runtime.hostChannel` yourself, or use `createRuntimeGroup()` which calls `router.attach(id, runtime.hostChannel, options)` for each participant. Cart unload clears the inbound queue only; router listeners stay until `runtime.destroy()` (or `group.destroy()`). If the channel is attached to a router, use `router.subscribe` for host logic — a second `mount({ onEvent })` will see every cart emit twice.
|
|
21
23
|
|
|
22
24
|
## Mailbox (`HostChannel`)
|
|
23
25
|
|
|
@@ -69,65 +71,9 @@ Routed events are normalized to `EventEnvelope` (`EVENT_ENVELOPE_VERSION = 1`).
|
|
|
69
71
|
- Else the first dotted segment of `type` that is `intent`, `state`, or `diagnostic` (not necessarily the second segment — `adventure.presentation.intent.cue-started` is an intent).
|
|
70
72
|
- Else `malformed` (`cannot infer kind`). `adventure.presentation.cue.started` does not infer a kind; define it with `defineIntent` / `defineStateEvent` / `defineDiagnostic` so the kind is in the name.
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## Typed contracts
|
|
75
|
-
|
|
76
|
-
Define each routed type once. That object is the TypeScript payload shape (`InferredPayload`), the runtime validator, the router permission source, and the JSON manifest row.
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import {
|
|
80
|
-
defineIntent,
|
|
81
|
-
defineStateEvent,
|
|
82
|
-
createContractRegistry,
|
|
83
|
-
deriveAttachOptions,
|
|
84
|
-
createEventRouter,
|
|
85
|
-
} from '@cyberart-io/engine';
|
|
86
|
-
|
|
87
|
-
const exit = defineIntent('adventure.intent.exit-requested', {
|
|
88
|
-
version: 1,
|
|
89
|
-
fields: { exitId: { type: 'string' } },
|
|
90
|
-
});
|
|
91
|
-
const room = defineStateEvent('adventure.state.room-changed', {
|
|
92
|
-
version: 1,
|
|
93
|
-
fields: { roomId: { type: 'string' } },
|
|
94
|
-
});
|
|
95
|
-
if (!exit.ok || !room.ok) {
|
|
96
|
-
// structured errors — these helpers do not throw
|
|
97
|
-
}
|
|
98
|
-
const contracts = [exit.contract, room.contract];
|
|
99
|
-
const registry = createContractRegistry(contracts);
|
|
100
|
-
const router = createEventRouter({ validate: registry.asRouterValidate });
|
|
101
|
-
router.attach('presentation', runtime.hostChannel, deriveAttachOptions(contracts, 'cart'));
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
`adventure.presentation.cue.started` has no `intent` / `state` / `diagnostic` segment, so `inferEventKind` returns undefined and `defineIntent` returns `{ ok: false, errors }` with `kind-mismatch`. Use `adventure.presentation.intent.cue-started` (kind anywhere after the namespace, not only the second segment). Cue timeline names (`cue.started`, …) are **not** routed envelopes until you wrap them in a contract.
|
|
74
|
+
Typed contracts (`defineIntent`, `defineStateEvent`, `defineDiagnostic`) fail at definition time with structured `{ ok: false, errors }` instead of throwing. `createContractRegistry` produces manifest JSON and a `validate` hook for `createEventRouter`. `deriveAttachOptions` / `verifyAttachOptions` keep non-authoritative carts from emitting `state` / `diagnostic` contracts. Payload schema versioning: adding optional fields with a version bump is backward-compatible; removals, type changes, and version downgrades are breaking.
|
|
105
75
|
|
|
106
|
-
|
|
107
|
-
|---|---|---|
|
|
108
|
-
| `defineIntent(type, schema)` | `intent` | `type` must contain an `intent` dotted segment |
|
|
109
|
-
| `defineStateEvent(type, schema)` | `state` | `type` must contain a `state` dotted segment |
|
|
110
|
-
| `defineDiagnostic(type, schema)` | `diagnostic` | `type` must contain a `diagnostic` dotted segment |
|
|
111
|
-
|
|
112
|
-
`schema` is `{ version: integer >= 1, fields: { name: { type: 'string' \| 'number' \| 'boolean' \| 'object' \| 'array', optional?: true } } }`. Extra payload keys are allowed. Missing required fields and wrong JSON types return `{ ok: false, errors: ContractDiagnostic[] }` (`code`, `detail`, optional `path`). Invalid definitions return the same shape; they never throw.
|
|
113
|
-
|
|
114
|
-
| Export | Role |
|
|
115
|
-
|---|---|
|
|
116
|
-
| `createContractRegistry(contracts)` | `get`, `manifest()` (JSON-serializable), `validateEnvelope`, `asRouterValidate` (safe to pass as `EventRouterOptions.validate`; does not rely on `this`) |
|
|
117
|
-
| `deriveAttachOptions(contracts, 'cart' \| 'authoritative')` | Cart: emit intent family patterns only. Authoritative: emit every contract family. Subscribe to non-intent families. |
|
|
118
|
-
| `verifyAttachOptions(options, contracts)` | `{ ok: true }` or unauthorized-emit errors when a non-authoritative attach can emit a `state` / `diagnostic` contract |
|
|
119
|
-
| `comparePayloadSchemas(from, to)` | `'identical'` / `'backward-compatible'` / `'breaking'` |
|
|
120
|
-
| `kindSegmentInType(type)` / `familyPatternForType(type)` | First kind segment; last-segment `*` glob (`adventure.presentation.intent.*`) |
|
|
121
|
-
|
|
122
|
-
Default router `emit: ['*.intent.*']` is **three** segments. Four-segment names such as `adventure.presentation.intent.cue-started` need `deriveAttachOptions` (or an explicit four-segment pattern). Duplicate `type`s in a registry last-win.
|
|
123
|
-
|
|
124
|
-
### Payload versioning (`comparePayloadSchemas`)
|
|
125
|
-
|
|
126
|
-
| Change | Result |
|
|
127
|
-
|---|---|
|
|
128
|
-
| Same version, same fields | `identical` |
|
|
129
|
-
| Version bump, only new **optional** fields (or unchanged fields) | `backward-compatible` |
|
|
130
|
-
| Removed field, type change, optional → required, version downgrade, optional add **without** a version bump | `breaking` |
|
|
76
|
+
Only the host (`publish`) or an `authoritative` participant may emit `state` / `diagnostic`. Carts emit allowed `intent` patterns. Domain data belongs in `payload`.
|
|
131
77
|
|
|
132
78
|
## `createEventRouter(options?)`
|
|
133
79
|
|
|
@@ -247,14 +193,11 @@ Type `REJECTED_EVENT_TYPE` (`cyberart.diagnostic.rejected`). Payload (`Rejection
|
|
|
247
193
|
| `EVENT_ENVELOPE_VERSION` | `1`. |
|
|
248
194
|
| `DEFAULT_MAX_HOPS` | `8`. |
|
|
249
195
|
| `REJECTED_EVENT_TYPE` | `'cyberart.diagnostic.rejected'`. |
|
|
250
|
-
| `inferEventKind(input)` | Kind from `kind` or
|
|
196
|
+
| `inferEventKind(input)` | Kind from `kind` or type name. |
|
|
251
197
|
| `matchEventPattern(pattern, type)` | One-segment `*` glob. |
|
|
252
198
|
| `normalizeEvent(input, context)` | Build an envelope or `{ ok: false, reason: 'malformed', detail }`. The router calls this; hosts rarely need it. |
|
|
253
|
-
| `defineIntent` / `defineStateEvent` / `defineDiagnostic` | Contract constructors (see Typed contracts). |
|
|
254
|
-
| `createContractRegistry` / `deriveAttachOptions` / `verifyAttachOptions` / `comparePayloadSchemas` | Registry, permissions, schema compatibility. |
|
|
255
|
-
| `kindSegmentInType` / `familyPatternForType` | Name helpers used by contracts. |
|
|
256
199
|
|
|
257
|
-
Types: `EventEnvelope`, `EventInput`, `EventKind`, `NormalizeContext`, `NormalizeResult`, `RejectionPayload`, `RejectionReason`, `AttachOptions`, `EventRouter`, `EventRouterOptions`, `PublishExtras`, `ValidateResult
|
|
200
|
+
Types: `EventEnvelope`, `EventInput`, `EventKind`, `NormalizeContext`, `NormalizeResult`, `RejectionPayload`, `RejectionReason`, `AttachOptions`, `EventRouter`, `EventRouterOptions`, `PublishExtras`, `ValidateResult`.
|
|
258
201
|
|
|
259
202
|
`matchesAnyPattern` / `clonePayload` / `isEventKind` are not on the public package surface.
|
|
260
203
|
|
package/docs/headless-harness.md
CHANGED
|
@@ -4,7 +4,7 @@ CI / agent wrapper around production `createRuntime({ deterministic })`. Not a s
|
|
|
4
4
|
|
|
5
5
|
Import from **`@cyberart-io/engine/headless`**. Production carts and browser hosts keep using `@cyberart-io/engine` so Vite never resolves `node:fs/promises`.
|
|
6
6
|
|
|
7
|
-
Depends on [deterministic mode](deterministic-mode.md). Host reducers: [events](events.md). Presentation overlay: [presentation adapter](presentation-adapter.md).
|
|
7
|
+
Depends on [deterministic mode](deterministic-mode.md). Host reducers: [events](events.md). Presentation overlay: [presentation adapter](presentation-adapter.md). Several carts: [runtime group](runtime-group.md) (`createHeadlessMultiCartHarness`).
|
|
8
8
|
|
|
9
9
|
Back to the [package README](../README.md).
|
|
10
10
|
|
|
@@ -12,8 +12,8 @@ Back to the [package README](../README.md).
|
|
|
12
12
|
|
|
13
13
|
| Surface | Import |
|
|
14
14
|
|---|---|
|
|
15
|
-
| Carts, Player, kaleidoscope, `/art` | `@cyberart-io/engine` (`createRuntime`, events,
|
|
16
|
-
| Vitest / jsdom / CI capture | `@cyberart-io/engine/headless` (`createHeadlessHarness`, `installHeadlessCanvas`, `captureFrame`) |
|
|
15
|
+
| Carts, Player, kaleidoscope, `/art` | `@cyberart-io/engine` (`createRuntime`, events, assets, presentation adapter) |
|
|
16
|
+
| Vitest / jsdom / CI capture | `@cyberart-io/engine/headless` (`createHeadlessHarness`, `createHeadlessMultiCartHarness`, `installHeadlessCanvas`, `captureFrame`) |
|
|
17
17
|
|
|
18
18
|
The main export does not re-export the harness. A browser bundle that only imports `@cyberart-io/engine` must not warn about `node:fs/promises`.
|
|
19
19
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Normalized geometry
|
|
2
|
+
|
|
3
|
+
Shared 0–1 content-box coordinates, anchors, hit regions, and resize-stable
|
|
4
|
+
contain/cover/crop layouts. Pixel `PresentationRegion` on the presentation
|
|
5
|
+
adapter is unchanged; hosts can adopt this contract later.
|
|
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/cyb-60-normalized-geometry.repro.spec.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Contract (`GEOMETRY_CONTRACT_VERSION = 1`)
|
|
16
|
+
|
|
17
|
+
| Field | Meaning |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `version` | Always `1`. Other versions fail `validateGeometry`. |
|
|
20
|
+
| `landmarks` | Named anchors (`id` + normalized point + optional origin keyword). |
|
|
21
|
+
| `regions` | Hitboxes: axis-aligned `rect` and/or `polygon` in normalized space. |
|
|
22
|
+
| `padding` | Normalized insets `{ top, right, bottom, left }`. |
|
|
23
|
+
| `safeArea` | Same inset shape; authoring hint, not applied by the layout math. |
|
|
24
|
+
|
|
25
|
+
Documents are JSON-serializable (`JSON.parse(JSON.stringify(doc))` round-trips). Import from **`@cyberart-io/engine`** (`createPresentationLayout`, `pointerToRegion`, `serializeGeometry`, …).
|
|
26
|
+
|
|
27
|
+
If a region sets both `rect` and `polygon`, hit-testing uses the polygon and overlay/AABB helpers use the rect. Prefer one shape per region.
|
|
28
|
+
|
|
29
|
+
## Coordinate spaces
|
|
30
|
+
|
|
31
|
+
Normalized is **0–1 of the content box** (intrinsic artwork), not the letterboxed viewport.
|
|
32
|
+
|
|
33
|
+
| Space | Unit | Box |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `normalized` | 0–1 | Full content (`contentWidth` × `contentHeight`) |
|
|
36
|
+
| `asset` | px | Same content, intrinsic pixels |
|
|
37
|
+
| `css` / `viewport` | CSS px | Layout (`viewportWidth` × `viewportHeight`) |
|
|
38
|
+
| `canvas` | buffer px | `css * devicePixelRatio` (default dpr `1`) |
|
|
39
|
+
|
|
40
|
+
`pointerToRegion` defaults to **canvas** space (same as `PointerManager` / harness `click`). Pass `{ space: 'css' }` for layout pixels.
|
|
41
|
+
|
|
42
|
+
## Layout (`createPresentationLayout`)
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createPresentationLayout } from '@cyberart-io/engine';
|
|
46
|
+
|
|
47
|
+
const layout = createPresentationLayout({
|
|
48
|
+
contentWidth: 1920,
|
|
49
|
+
contentHeight: 1080,
|
|
50
|
+
viewportWidth: 1280,
|
|
51
|
+
viewportHeight: 800,
|
|
52
|
+
mode: 'contain', // or 'cover' | 'crop'
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Mode | Scale | Offsets |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `contain` | `min(vw/cw, vh/ch)` | Letterbox ≥ 0 |
|
|
59
|
+
| `cover` | `max(vw/cw, vh/ch)` | Crop ≤ 0 (centered) |
|
|
60
|
+
| `crop` | Same as `cover` | Same as `cover` |
|
|
61
|
+
|
|
62
|
+
Resize-stable under **contain**: a normalized point stays on the same content location when only the viewport changes. Cover/crop keep the content centered and report `visibleNormalizedRect` for the uncropped slice.
|
|
63
|
+
|
|
64
|
+
## Round-trip tolerance
|
|
65
|
+
|
|
66
|
+
| Constant | Value | Use |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `ROUND_TRIP_TOLERANCE` | `1e-6` | Max \|Δ\| in normalized units after canvas/css round-trip |
|
|
69
|
+
| `ROUND_TRIP_TOLERANCE_CANVAS_PX` | `0.5` | Max \|Δ\| in canvas pixels after normalized round-trip |
|
|
70
|
+
|
|
71
|
+
## Helpers
|
|
72
|
+
|
|
73
|
+
| Export | Role |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `normalizedToCanvas` / `canvasToNormalized` | Content box ↔ drawing buffer |
|
|
76
|
+
| `normalizedToCss` / `cssToNormalized` | Content box ↔ layout pixels |
|
|
77
|
+
| `normalizedToAsset` / `assetToNormalized` | Content box ↔ intrinsic pixels |
|
|
78
|
+
| `pointerToRegion(pointer, layout, doc)` | First matching region (document order) |
|
|
79
|
+
| `regionToViewport(region, layout)` | CSS rect (polygon AABB) |
|
|
80
|
+
| `createLandmarkRegistry()` | `register` / `get` / `list`; duplicate ids → `{ ok: false, error: 'duplicate-id' }` |
|
|
81
|
+
| `validateGeometry(doc)` | Overlap, out-of-bounds, duplicate id, invalid shape; diagnostics include region ids |
|
|
82
|
+
| `drawGeometryDebug(ctx, layout, doc, diagnostics)` | Canvas2D `fillRect` / `strokeRect` / `fillText` when present; always returns recorded calls with region ids |
|
|
83
|
+
|
|
84
|
+
Origin keywords: `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right`, `top`, `bottom`, `left`, `right`. `pointFromOrigin` yields the matching unit-square point.
|
|
85
|
+
|
|
86
|
+
## Fixture (CYB-60)
|
|
87
|
+
|
|
88
|
+
Content **1920×1080**. Hotspot point `(0.25, 0.4)` inside region `hotspot` `{ x: 0.2, y: 0.35, width: 0.1, height: 0.1 }`. Contain viewports **1280×800** and **390×844**.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Presentation adapter
|
|
2
2
|
|
|
3
|
-
Host-owned render models in, interaction intents out. Cyberart is not authoritative for game rules. Depends on [events](events.md). CI: [headless harness](headless-harness.md).
|
|
3
|
+
Host-owned render models in, interaction intents out. Cyberart is not authoritative for game rules. Depends on [events](events.md). CI: [headless harness](headless-harness.md). Normalized 0–1 regions: [normalized geometry](normalized-geometry.md).
|
|
4
4
|
|
|
5
5
|
Back to the [package README](../README.md).
|
|
6
6
|
|
|
@@ -64,7 +64,7 @@ const model: PresentationModel = {
|
|
|
64
64
|
};
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`view` is host-owned. The reference cart hit-tests `regions` and paints `background`. `title` is inspectable state, not drawn. Extra fields (characters, exits, inventory) are ignored — map clickable things onto `regions`. Coordinates are drawing-space pixels, the same space as `PointerManager` and harness `click`.
|
|
67
|
+
`view` is host-owned. The reference cart hit-tests `regions` and paints `background`. `title` is inspectable state, not drawn. Extra fields (characters, exits, inventory) are ignored — map clickable things onto `regions`. Coordinates on `PresentationRegion` are drawing-space pixels, the same space as `PointerManager` and harness `click`. Convert 0–1 content-box geometry with `createPresentationLayout` / `pointerToRegion` from [normalized geometry](normalized-geometry.md).
|
|
68
68
|
|
|
69
69
|
`adapter.model` / `adapter.phase` are what this session last presented (or the boot model). The getter returns a **copy**. After `step`, `inspect().state.model` is what the cart applied.
|
|
70
70
|
|
package/docs/presentation-cue.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Deterministic cue/effect primitive. Carts call `step(frames)` with the same clock they use for CYB-23; there are no `setTimeout` / rAF timers. Gold checkmarks, ripples, room transitions, weather pulses, and NPC beats should share this lifecycle instead of ad-hoc frame counters.
|
|
4
4
|
|
|
5
|
-
Back to the [package README](../README.md). Related: [deterministic mode](deterministic-mode.md)
|
|
5
|
+
Back to the [package README](../README.md). Related: [deterministic mode](deterministic-mode.md).
|
|
6
6
|
|
|
7
7
|
## One-command reproduce (this repo)
|
|
8
8
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Runtime group
|
|
2
|
+
|
|
3
|
+
Host helper that mounts several production `createRuntime` carts, attaches each mailbox with `router.attach(id, runtime.hostChannel, attachOptions)`, and locksteps one shared deterministic clock. Carts never receive the router. Not a second engine. Do not call the headless wrapper from Player or kaleidoscope.
|
|
4
|
+
|
|
5
|
+
Capability-manifest integration is optional and structural (`capability?: { emit, subscribe, authoritative }`). This module does not import the manifest.
|
|
6
|
+
|
|
7
|
+
Related: [events](events.md), [headless harness](headless-harness.md), [deterministic mode](deterministic-mode.md).
|
|
8
|
+
|
|
9
|
+
Back to the [package README](../README.md).
|
|
10
|
+
|
|
11
|
+
## One-command reproduce (this repo)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm exec vitest run packages/engine/src/canvas/cyb-61-runtime-group.repro.spec.ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## When to use which
|
|
18
|
+
|
|
19
|
+
| Surface | Import | Use |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Production host | `createRuntimeGroup` from `@cyberart-io/engine` | Several carts in one page; host injects containers or lets the group create them. |
|
|
22
|
+
| Vitest / jsdom | `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless` | Same group API after `installHeadlessCanvas()`. |
|
|
23
|
+
|
|
24
|
+
`createRuntime` still does **not** attach a router by itself. The group is the attach + lockstep layer.
|
|
25
|
+
|
|
26
|
+
## `createRuntimeGroup(options)`
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createRuntimeGroup } from '@cyberart-io/engine';
|
|
30
|
+
|
|
31
|
+
const group = createRuntimeGroup({
|
|
32
|
+
origin: 0,
|
|
33
|
+
createId: (() => {
|
|
34
|
+
let n = 0;
|
|
35
|
+
return () => `g-${++n}`;
|
|
36
|
+
})(),
|
|
37
|
+
participants: [
|
|
38
|
+
{
|
|
39
|
+
id: 'effects',
|
|
40
|
+
cart: effectsCart,
|
|
41
|
+
seed: `0x${'61'.repeat(32)}`,
|
|
42
|
+
emit: ['ambience.intent.*'],
|
|
43
|
+
subscribe: ['adventure.state.*'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'ambience',
|
|
47
|
+
cart: ambienceCart,
|
|
48
|
+
kind: 'render',
|
|
49
|
+
seed: `0x${'62'.repeat(32)}`,
|
|
50
|
+
subscribe: ['ambience.intent.*'],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
group.publish({
|
|
56
|
+
type: 'adventure.state.loon-whistle',
|
|
57
|
+
kind: 'state',
|
|
58
|
+
payload: { habitat: 'pond' },
|
|
59
|
+
});
|
|
60
|
+
await group.step(2);
|
|
61
|
+
const { participants, trace, diagnostics } = await group.inspect();
|
|
62
|
+
group.destroy();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Adventure-style mapping (fixture, not Adventure Kit): an accepted `adventure.state.loon-whistle` is consumed by the effects cart, which emits `ambience.intent.play`; the router delivers that cue to the ambience cart. No project-specific bridge.
|
|
66
|
+
|
|
67
|
+
### Options (`CreateRuntimeGroupOptions`)
|
|
68
|
+
|
|
69
|
+
| Option | Default | Meaning |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `participants` | required | One entry per cart. Ids must be unique. Sorted lexicographically for lockstep. |
|
|
72
|
+
| `origin` | `0` | Passed to every `createRuntime({ deterministic: { origin } })`. |
|
|
73
|
+
| `width` / `height` | `320` / `180` | Used when the group creates a container. |
|
|
74
|
+
| `createId` / `now` | `evt-1`… / first cart clock | Injected into the **one** shared router. |
|
|
75
|
+
| `validate` | none | Router `validate` for cart-originated events. |
|
|
76
|
+
| `router` | `{}` | Extra `createEventRouter` options (`maxHops`, `maxPerTurn`, …). |
|
|
77
|
+
|
|
78
|
+
### Participant config
|
|
79
|
+
|
|
80
|
+
| Field | Meaning |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `id` | Router participant id. Reserved: `host`, `router`. |
|
|
83
|
+
| `cart` | `AnimationCart` mounted through `createRuntime`. |
|
|
84
|
+
| `kind` | `'render'` (default) or `'calculation'` (no-op/minimal render; still a real cart). |
|
|
85
|
+
| `seed` | `0x` + 64 hex, or any seed `createRuntime` accepts. Default: derived from `id`. |
|
|
86
|
+
| `container` | Injected mount node. If omitted, the group creates and owns a sized `div`. |
|
|
87
|
+
| `emit` / `subscribe` / `authoritative` | Passed to `router.attach`. |
|
|
88
|
+
| `capability` | Structural fallback for those three fields when the explicit ones are omitted. |
|
|
89
|
+
| `initialState` / `gameManager` / `onEvent` | `mount` options. The group still records outbound events. |
|
|
90
|
+
|
|
91
|
+
### Handle (`RuntimeGroup`)
|
|
92
|
+
|
|
93
|
+
| Member | Meaning |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `router` | The shared `EventRouter`. Do not create a second one. |
|
|
96
|
+
| `step(n)` | For each frame: `router.turn()` once, then `await cart.step(1)` in sorted id order. No-op while paused. Throws after `destroy()`. |
|
|
97
|
+
| `pause` / `resume` | Group flag plus every cart. `step` does not advance while paused. |
|
|
98
|
+
| `reset` | Remounts every cart to its initial state, clears traces / logs, `router.turn()`. Does not change `paused`; call `resume()` if you need lockstep after a paused reset. |
|
|
99
|
+
| `dispatch(id, event)` | Mailbox inbound on that participant. |
|
|
100
|
+
| `publish(event)` | Host `router.publish`. Unknown `target` → `unknown-target` rejection. |
|
|
101
|
+
| `inspect()` | `{ participants: Record<id, { state, events, errors, kind, clock }>, trace, diagnostics }`. |
|
|
102
|
+
| `destroy()` | Detach router listeners, `runtime.destroy()` each cart, remove owned containers. Idempotent. |
|
|
103
|
+
|
|
104
|
+
Idempotency, correlation, causation, unauthorized emit, hop/loop checks are the existing router. The group only attaches and locksteps.
|
|
105
|
+
|
|
106
|
+
## `createHeadlessMultiCartHarness(options)`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { createHeadlessMultiCartHarness } from '@cyberart-io/engine/headless';
|
|
110
|
+
|
|
111
|
+
const harness = createHeadlessMultiCartHarness({
|
|
112
|
+
participants: [effects, ambience, telemetry],
|
|
113
|
+
});
|
|
114
|
+
await harness.step(2);
|
|
115
|
+
harness.destroy();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Calls `installHeadlessCanvas()` then `createRuntimeGroup`. Same handle. Three-cart CI fixture: effects + ambience + a calculation telemetry cart.
|
|
119
|
+
|
|
120
|
+
Cleanup: `afterEach(() => group.destroy())` so a failed assertion does not leak nodes. After `destroy`, `step` throws and owned containers are gone from `document.body`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyberart-io/engine",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "CyberArt host engine: mount
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "CyberArt host engine: mount carts, events, capability manifests, geometry, runtime groups, and a Node/jsdom headless entry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"files": [
|