@sadhaka/loom-engine 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +113 -1
  2. package/dist/components/peer-sprite.d.ts +25 -0
  3. package/dist/components/peer-sprite.d.ts.map +1 -0
  4. package/dist/components/peer-sprite.js +48 -0
  5. package/dist/components/peer-sprite.js.map +1 -0
  6. package/dist/director/ai/ai-plugin-registry.d.ts +20 -0
  7. package/dist/director/ai/ai-plugin-registry.d.ts.map +1 -0
  8. package/dist/director/ai/ai-plugin-registry.js +232 -0
  9. package/dist/director/ai/ai-plugin-registry.js.map +1 -0
  10. package/dist/director/ai/mock-ai-plugin.d.ts +24 -0
  11. package/dist/director/ai/mock-ai-plugin.d.ts.map +1 -0
  12. package/dist/director/ai/mock-ai-plugin.js +83 -0
  13. package/dist/director/ai/mock-ai-plugin.js.map +1 -0
  14. package/dist/director/ai/plugin-context.d.ts +27 -0
  15. package/dist/director/ai/plugin-context.d.ts.map +1 -0
  16. package/dist/director/ai/plugin-context.js +152 -0
  17. package/dist/director/ai/plugin-context.js.map +1 -0
  18. package/dist/director/ai/plugin.d.ts +57 -0
  19. package/dist/director/ai/plugin.d.ts.map +1 -0
  20. package/dist/director/ai/plugin.js +32 -0
  21. package/dist/director/ai/plugin.js.map +1 -0
  22. package/dist/director/index.d.ts +27 -0
  23. package/dist/director/index.d.ts.map +1 -0
  24. package/dist/director/index.js +26 -0
  25. package/dist/director/index.js.map +1 -0
  26. package/dist/director/zone/mock-zone-bridge.d.ts +22 -0
  27. package/dist/director/zone/mock-zone-bridge.d.ts.map +1 -0
  28. package/dist/director/zone/mock-zone-bridge.js +107 -0
  29. package/dist/director/zone/mock-zone-bridge.js.map +1 -0
  30. package/dist/director/zone/sse-zone-bridge.d.ts +40 -0
  31. package/dist/director/zone/sse-zone-bridge.d.ts.map +1 -0
  32. package/dist/director/zone/sse-zone-bridge.js +164 -0
  33. package/dist/director/zone/sse-zone-bridge.js.map +1 -0
  34. package/dist/director/zone/zone-event-bridge.d.ts +21 -0
  35. package/dist/director/zone/zone-event-bridge.d.ts.map +1 -0
  36. package/dist/director/zone/zone-event-bridge.js +24 -0
  37. package/dist/director/zone/zone-event-bridge.js.map +1 -0
  38. package/dist/director/zone/zone-event-envelope.d.ts +90 -0
  39. package/dist/director/zone/zone-event-envelope.d.ts.map +1 -0
  40. package/dist/director/zone/zone-event-envelope.js +104 -0
  41. package/dist/director/zone/zone-event-envelope.js.map +1 -0
  42. package/dist/director/zone/zone-event-log.d.ts +17 -0
  43. package/dist/director/zone/zone-event-log.d.ts.map +1 -0
  44. package/dist/director/zone/zone-event-log.js +46 -0
  45. package/dist/director/zone/zone-event-log.js.map +1 -0
  46. package/dist/director/zone/zone-event-system.d.ts +14 -0
  47. package/dist/director/zone/zone-event-system.d.ts.map +1 -0
  48. package/dist/director/zone/zone-event-system.js +179 -0
  49. package/dist/director/zone/zone-event-system.js.map +1 -0
  50. package/dist/director/zone/zone-state-resource.d.ts +15 -0
  51. package/dist/director/zone/zone-state-resource.d.ts.map +1 -0
  52. package/dist/director/zone/zone-state-resource.js +60 -0
  53. package/dist/director/zone/zone-state-resource.js.map +1 -0
  54. package/dist/index.d.ts +25 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +14 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/network/mock-multiplayer-bridge.d.ts +34 -0
  59. package/dist/network/mock-multiplayer-bridge.d.ts.map +1 -0
  60. package/dist/network/mock-multiplayer-bridge.js +91 -0
  61. package/dist/network/mock-multiplayer-bridge.js.map +1 -0
  62. package/dist/network/multiplayer-bridge.d.ts +45 -0
  63. package/dist/network/multiplayer-bridge.d.ts.map +1 -0
  64. package/dist/network/multiplayer-bridge.js +34 -0
  65. package/dist/network/multiplayer-bridge.js.map +1 -0
  66. package/dist/network/peer-pool.d.ts +46 -0
  67. package/dist/network/peer-pool.d.ts.map +1 -0
  68. package/dist/network/peer-pool.js +185 -0
  69. package/dist/network/peer-pool.js.map +1 -0
  70. package/dist/network/sse-multiplayer-bridge.d.ts +36 -0
  71. package/dist/network/sse-multiplayer-bridge.d.ts.map +1 -0
  72. package/dist/network/sse-multiplayer-bridge.js +264 -0
  73. package/dist/network/sse-multiplayer-bridge.js.map +1 -0
  74. package/dist/server/index.d.ts +7 -0
  75. package/dist/server/index.d.ts.map +1 -0
  76. package/dist/server/index.js +33 -0
  77. package/dist/server/index.js.map +1 -0
  78. package/dist/systems/peer-presence-system.d.ts +19 -0
  79. package/dist/systems/peer-presence-system.d.ts.map +1 -0
  80. package/dist/systems/peer-presence-system.js +118 -0
  81. package/dist/systems/peer-presence-system.js.map +1 -0
  82. package/package.json +6 -2
package/README.md CHANGED
@@ -146,7 +146,8 @@ public API surface will evolve until 1.0.
146
146
  | 11B.3 | shipped | MIT license + npm publish posture (this release) |
147
147
  | 12.4 | shipped | License pivot from MIT to BUSL 1.1 with $1M revenue cap |
148
148
  | 13.2 | shipped | Engine hardening: 12.6 audit lows L-08..L-12 closed |
149
- | 14.1 | shipped | WebGL2 instanced sprite batcher (this release) |
149
+ | 14.1 | shipped | WebGL2 instanced sprite batcher |
150
+ | 15.1 | shipped | Multiplayer presence: pluggable bridge (SSE / Mock), peer pool with per-peer linear interpolation, render system (this release) |
150
151
 
151
152
  See [LOOM-ENGINE-SPEC.md](../docker/LOOM-ENGINE-SPEC.md) Section 7
152
153
  for the full phase plan with effort estimates.
@@ -276,6 +277,14 @@ the browser without a build step on the consumer side.
276
277
  Demonstrates that the same ECS / resource model that runs the action
277
278
  demos also fits a UI-only game: custom `Resource`, custom `System`
278
279
  reading both `InputSnapshot` and DOM events, DOM as the primary UI.
280
+ - **[Plaza Multiplayer](./demo/plaza-multiplayer/)** - walkable iso
281
+ plaza with three synthetic peers wandering randomly, driven by a
282
+ `MockMultiplayerBridge`. WASD to walk; the local player broadcasts
283
+ position at 10 Hz and the three peers (Alice / Bob / Carol) lerp
284
+ smoothly between presence updates. Demonstrates the pluggable
285
+ multiplayer bridge, `PeerPool` linear interpolation, and the
286
+ `PeerPresenceSystem` / `PeerRenderSystem` pipeline. See the
287
+ [Multiplayer](#multiplayer) section below for the wire protocol.
279
288
 
280
289
  The legacy reference demos (Phase 6 director, Phase 7 combat, Phase 8
281
290
  ARPG slice) stay accessible from the gallery index.
@@ -285,6 +294,109 @@ Controls in the legacy director demo (`demo/director.html`):
285
294
  - **Click**: burst 24 particles + play SFX chirp (after first click, AudioContext unlocks)
286
295
  - **Hover**: stats panel shows the iso tile under the cursor
287
296
 
297
+ ## Multiplayer
298
+
299
+ Phase 15.1 adds a thin presence layer for showing other players in
300
+ real time on the same world. The transport is pluggable: the engine
301
+ ships an `SSEMultiplayerBridge` (server-sent events) and a
302
+ `MockMultiplayerBridge` (in-process; tests + offline demos), and the
303
+ `IMultiplayerBridge` interface is small enough to swap in WebSocket
304
+ or WebRTC without touching anything above it. No CRDT - peers carry
305
+ position only, and conflict resolution is "last write wins" at the
306
+ server. Shared state beyond position is deferred to a later phase.
307
+
308
+ ### Wire protocol
309
+
310
+ The bridge layer hides this from gameplay code, but for anyone
311
+ implementing a server (or a custom transport) the contract is:
312
+
313
+ **Server -> client (SSE event types):**
314
+ - `presence.snapshot` `{ peers: [{ character_id, x, y, zone, ts_ms, name? }] }`
315
+ emitted once on connect with the full current peer roster. The
316
+ client treats this as authoritative and drops any peer not in the
317
+ snapshot.
318
+ - `presence.update` `{ character_id, x, y, zone, ts_ms, name? }`
319
+ emitted as peers move.
320
+ - `presence.depart` `{ character_id }`
321
+ emitted when a peer disconnects.
322
+
323
+ **Client -> server (HTTP POST):**
324
+ - `POST <broadcastUrl>` `{ character_id, x, y, zone, ts_ms }`
325
+ the engine rate-limits to **10 Hz** (`BROADCAST_HZ`); excess calls
326
+ to `broadcastPosition()` are silently dropped and counted in
327
+ `bridge.stats().rateLimitedDrops`. Calling once per frame is fine.
328
+
329
+ `ts_ms` is the wall clock at which the position was true. The
330
+ `PeerPool` uses it to interpolate between successive samples so peers
331
+ don't jitter at the network rate. Acceptable lag is ~150 ms (one
332
+ update interval at 10 Hz), imperceptible at walk speed.
333
+
334
+ ### Setup
335
+
336
+ ```ts
337
+ import {
338
+ Engine,
339
+ // Multiplayer
340
+ MockMultiplayerBridge, // or SSEMultiplayerBridge
341
+ PeerPool,
342
+ PeerSpritePool,
343
+ PeerPresenceSystem,
344
+ PeerRenderSystem,
345
+ POOL_PEER_SPRITE,
346
+ RESOURCE_MULTIPLAYER_BRIDGE,
347
+ RESOURCE_PEER_POOL,
348
+ SYSTEM_PHASE_INPUT,
349
+ SYSTEM_PHASE_RENDER,
350
+ } from '@sadhaka/loom-engine';
351
+
352
+ const engine = Engine.create({ canvas });
353
+
354
+ // 1. Create a bridge. Production code uses SSEMultiplayerBridge:
355
+ //
356
+ // const bridge = new SSEMultiplayerBridge({
357
+ // baseUrl: '/api/v1/loom/presence/events',
358
+ // characterId: 'me',
359
+ // zone: 'plaza',
360
+ // });
361
+ //
362
+ // Tests + offline dev use the in-process mock:
363
+ const bridge = new MockMultiplayerBridge();
364
+ bridge.connect();
365
+
366
+ // 2. PeerPool stores all known peers + their interpolated positions.
367
+ // Self-filter: tell the pool which character_id is the local player
368
+ // so it isn't rendered as a ghost when the server echoes it back.
369
+ const peerPool = new PeerPool();
370
+ peerPool.setLocalCharacterId('me');
371
+
372
+ // 3. PeerSpritePool maps character_id -> rendering hint. A default
373
+ // atlas + frame is enough for an undifferentiated demo; setOverride()
374
+ // lets you assign per-class sprites or cosmetic shards.
375
+ const peerSprites = new PeerSpritePool({ defaultAtlas: peerAtlas });
376
+
377
+ engine.world.resources.set(RESOURCE_MULTIPLAYER_BRIDGE, bridge);
378
+ engine.world.resources.set(RESOURCE_PEER_POOL, peerPool);
379
+ engine.world.registerPool(POOL_PEER_SPRITE, peerSprites);
380
+
381
+ // 4. Wire the systems. PeerPresenceSystem drains the bridge each
382
+ // frame; PeerRenderSystem submits a drawSprite + name label per
383
+ // peer at their interpolated position.
384
+ engine.world.addSystem(new PeerPresenceSystem(), SYSTEM_PHASE_INPUT);
385
+ engine.world.addSystem(new PeerRenderSystem(), SYSTEM_PHASE_RENDER);
386
+
387
+ // 5. Inside your walk-system, call broadcastPosition() each frame.
388
+ // The bridge enforces the 10 Hz wire rate.
389
+ bridge.broadcastPosition(playerX, playerY, 'plaza', Date.now());
390
+ ```
391
+
392
+ ### Swapping transports
393
+
394
+ The bridge interface is just five methods (`connect`, `disconnect`,
395
+ `status`, `pollMessages`, `broadcastPosition`, plus `stats`). To use
396
+ WebSocket or WebRTC, implement `IMultiplayerBridge` with the same
397
+ `PresenceMessage` shape (`update` / `depart` / `snapshot`); none of
398
+ the systems above the bridge change.
399
+
288
400
  ## Layout
289
401
 
290
402
  ```
@@ -0,0 +1,25 @@
1
+ import type { AtlasHandle } from '../renderer/graphics-device.js';
2
+ import type { ColorRGBA } from '../util/color.js';
3
+ export interface PeerSpriteEntry {
4
+ atlas: AtlasHandle;
5
+ frame: number;
6
+ tint: Readonly<ColorRGBA> | null;
7
+ }
8
+ export interface PeerSpritePoolOptions {
9
+ defaultAtlas: AtlasHandle;
10
+ defaultFrame?: number;
11
+ defaultTint?: Readonly<ColorRGBA>;
12
+ }
13
+ export declare class PeerSpritePool {
14
+ private readonly defaultEntry;
15
+ private overrides;
16
+ constructor(opts: PeerSpritePoolOptions);
17
+ setOverride(characterId: string, entry: PeerSpriteEntry): void;
18
+ removeOverride(characterId: string): void;
19
+ resolve(characterId: string): Readonly<PeerSpriteEntry>;
20
+ getDefault(): Readonly<PeerSpriteEntry>;
21
+ hasOverride(characterId: string): boolean;
22
+ clear(): void;
23
+ }
24
+ export declare const POOL_PEER_SPRITE = "peer_sprite";
25
+ //# sourceMappingURL=peer-sprite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-sprite.d.ts","sourceRoot":"","sources":["../../src/components/peer-sprite.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,WAAW,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,qBAAqB;IAEpC,YAAY,EAAE,WAAW,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;CACnC;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkB;IAC/C,OAAO,CAAC,SAAS,CAA2C;gBAEhD,IAAI,EAAE,qBAAqB;IAWvC,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI;IAI9D,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAMzC,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,CAAC,eAAe,CAAC;IAIvD,UAAU,IAAI,QAAQ,CAAC,eAAe,CAAC;IAIvC,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAIzC,KAAK,IAAI,IAAI;CAGd;AAED,eAAO,MAAM,gBAAgB,gBAAgB,CAAC"}
@@ -0,0 +1,48 @@
1
+ // PeerSpritePool - per-peer rendering hints (atlas, frame, tint,
2
+ // optional name label) keyed by character_id rather than by EntityId.
3
+ //
4
+ // Peers don't get a stable EntityId because they appear and disappear
5
+ // based on network presence, not gameplay state. The PeerPresenceSystem
6
+ // resolves a peer's interpolated (x, y) from PeerPool each frame and
7
+ // draws via this pool's per-peer atlas/frame.
8
+ //
9
+ // Default style: unspecified peers render with a fallback atlas/frame
10
+ // supplied at pool construction. That keeps single-line setup simple
11
+ // for the demo while still allowing per-peer customization (cosmetic
12
+ // shards, visible class, etc.) once the dev wants to differentiate.
13
+ export class PeerSpritePool {
14
+ defaultEntry;
15
+ overrides = new Map();
16
+ constructor(opts) {
17
+ this.defaultEntry = {
18
+ atlas: opts.defaultAtlas,
19
+ frame: opts.defaultFrame ?? 0,
20
+ tint: opts.defaultTint ?? null,
21
+ };
22
+ }
23
+ // Per-peer override. Apply once on join (e.g. after a server-emitted
24
+ // class hint) and forget; the pool keeps it until clear() / removed
25
+ // explicitly.
26
+ setOverride(characterId, entry) {
27
+ this.overrides.set(characterId, entry);
28
+ }
29
+ removeOverride(characterId) {
30
+ this.overrides.delete(characterId);
31
+ }
32
+ // Resolve the rendering entry for a peer. Returns the override if
33
+ // one exists, otherwise the default. Never null for a known peer.
34
+ resolve(characterId) {
35
+ return this.overrides.get(characterId) ?? this.defaultEntry;
36
+ }
37
+ getDefault() {
38
+ return this.defaultEntry;
39
+ }
40
+ hasOverride(characterId) {
41
+ return this.overrides.has(characterId);
42
+ }
43
+ clear() {
44
+ this.overrides.clear();
45
+ }
46
+ }
47
+ export const POOL_PEER_SPRITE = 'peer_sprite';
48
+ //# sourceMappingURL=peer-sprite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-sprite.js","sourceRoot":"","sources":["../../src/components/peer-sprite.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,sEAAsE;AACtE,EAAE;AACF,sEAAsE;AACtE,wEAAwE;AACxE,qEAAqE;AACrE,8CAA8C;AAC9C,EAAE;AACF,sEAAsE;AACtE,qEAAqE;AACrE,qEAAqE;AACrE,oEAAoE;AAkBpE,MAAM,OAAO,cAAc;IACR,YAAY,CAAkB;IACvC,SAAS,GAAiC,IAAI,GAAG,EAAE,CAAC;IAE5D,YAAY,IAA2B;QACrC,IAAI,CAAC,YAAY,GAAG;YAClB,KAAK,EAAE,IAAI,CAAC,YAAY;YACxB,KAAK,EAAE,IAAI,CAAC,YAAY,IAAI,CAAC;YAC7B,IAAI,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;SAC/B,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,oEAAoE;IACpE,cAAc;IACd,WAAW,CAAC,WAAmB,EAAE,KAAsB;QACrD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAED,cAAc,CAAC,WAAmB;QAChC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACrC,CAAC;IAED,kEAAkE;IAClE,kEAAkE;IAClE,OAAO,CAAC,WAAmB;QACzB,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC;IAC9D,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,WAAW,CAAC,WAAmB;QAC7B,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACzC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,aAAa,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { IAIPlugin, EmittedEvents, PluginContext, PeerInfo, PlayerAction } from './plugin.js';
2
+ export declare class AIPluginDuplicateError extends Error {
3
+ readonly pluginName: string;
4
+ constructor(pluginName: string);
5
+ }
6
+ export declare class AIPluginRegistry {
7
+ private plugins;
8
+ register(plugin: IAIPlugin): void;
9
+ unregister(name: string): Promise<boolean>;
10
+ list(): ReadonlyArray<IAIPlugin>;
11
+ get(name: string): IAIPlugin | undefined;
12
+ dispatchTick(ctx: PluginContext): Promise<EmittedEvents>;
13
+ dispatchPeerJoin(ctx: PluginContext, peer: PeerInfo): Promise<EmittedEvents>;
14
+ dispatchPeerLeave(ctx: PluginContext, peer: PeerInfo): Promise<EmittedEvents>;
15
+ dispatchZoneEnter(ctx: PluginContext, peer: PeerInfo, fromZone: string | null): Promise<EmittedEvents>;
16
+ dispatchPlayerAction(ctx: PluginContext, peer: PeerInfo, action: PlayerAction): Promise<EmittedEvents>;
17
+ private dispatch;
18
+ private errorMeta;
19
+ }
20
+ //# sourceMappingURL=ai-plugin-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-plugin-registry.d.ts","sourceRoot":"","sources":["../../../src/director/ai/ai-plugin-registry.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,QAAQ,EACR,YAAY,EACb,MAAM,aAAa,CAAC;AAErB,qBAAa,sBAAuB,SAAQ,KAAK;aACnB,UAAU,EAAE,MAAM;gBAAlB,UAAU,EAAE,MAAM;CAI/C;AAED,qBAAa,gBAAgB;IAK3B,OAAO,CAAC,OAAO,CAAmB;IAOlC,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IA0B3B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BhD,IAAI,IAAI,aAAa,CAAC,SAAS,CAAC;IAMhC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAgBlC,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IAMxD,gBAAgB,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC;IAM5E,iBAAiB,CAAC,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,CAAC;IAM7E,iBAAiB,CACrB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,aAAa,CAAC;IAMnB,oBAAoB,CACxB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,aAAa,CAAC;YAYX,QAAQ;IAiEtB,OAAO,CAAC,SAAS;CAgBlB"}
@@ -0,0 +1,232 @@
1
+ // AIPluginRegistry - dispatches lifecycle hooks across registered plugins.
2
+ //
3
+ // Per LOOM-DIRECTOR-PROTOCOL-V2 Section 5.3: the registry holds N
4
+ // plugins, dispatches each hook in priority order (lower runs first),
5
+ // and concatenates the EmittedEvents from every plugin into a single
6
+ // merged result. The caller (the engine's emit path / TWT's loom_director
7
+ // orchestrator) takes the merged events and routes them - character
8
+ // events to the v1 stream, zone events to the v2 zone log + presence
9
+ // fanout.
10
+ //
11
+ // Error isolation guarantee (open question 8.3): if a plugin's hook
12
+ // throws synchronously OR the returned Promise rejects, the registry
13
+ // logs the failure via that plugin's logger, drops that plugin's
14
+ // contribution for THIS dispatch only, and continues with the next
15
+ // plugin. The dispatch never throws to the caller. Plugin authors
16
+ // remain responsible for their own internal try/catch; the registry
17
+ // is the safety net of last resort.
18
+ //
19
+ // Implementation notes:
20
+ // - Plugins are kept in a sorted array; register/unregister keep
21
+ // it sorted so the dispatch hot path is a simple iteration.
22
+ // - The registry never instantiates a PluginContext; the caller
23
+ // supplies one per dispatch (or reuses one and refreshes views).
24
+ // This keeps the registry decoupled from world-state plumbing.
25
+ // - dispose() is awaited inside unregister() so plugins can flush
26
+ // pending work; if dispose throws we log and continue (drop
27
+ // guarantee extends to dispose so the caller never sees plugin
28
+ // errors leak).
29
+ export class AIPluginDuplicateError extends Error {
30
+ pluginName;
31
+ constructor(pluginName) {
32
+ super('AIPluginDuplicateError: plugin already registered: ' + pluginName);
33
+ this.pluginName = pluginName;
34
+ this.name = 'AIPluginDuplicateError';
35
+ }
36
+ }
37
+ export class AIPluginRegistry {
38
+ // Sorted by priority ascending, then by registration order. The
39
+ // registration index is encoded by inserting at the right position
40
+ // on register() rather than re-sorting, which preserves stable
41
+ // ordering for equal priorities.
42
+ plugins = [];
43
+ // ----- Lifecycle -----
44
+ // Register a plugin. Throws AIPluginDuplicateError if a plugin with
45
+ // the same name is already registered (names are the registry key).
46
+ // Insertion is O(n) but n is small (typically 1-10 plugins).
47
+ register(plugin) {
48
+ for (var i = 0; i < this.plugins.length; i++) {
49
+ var existing = this.plugins[i];
50
+ if (existing && existing.name === plugin.name) {
51
+ throw new AIPluginDuplicateError(plugin.name);
52
+ }
53
+ }
54
+ // Find the first plugin with strictly higher priority and insert
55
+ // before it. This preserves stable ordering for equal priorities
56
+ // (a later-registered plugin runs after an earlier one with the
57
+ // same priority).
58
+ var insertAt = this.plugins.length;
59
+ for (var j = 0; j < this.plugins.length; j++) {
60
+ var p = this.plugins[j];
61
+ if (p && p.priority > plugin.priority) {
62
+ insertAt = j;
63
+ break;
64
+ }
65
+ }
66
+ this.plugins.splice(insertAt, 0, plugin);
67
+ }
68
+ // Unregister a plugin by name. Awaits the plugin's dispose() if
69
+ // present; logs and drops if dispose throws. Returns true if a
70
+ // plugin was removed, false if no plugin with that name was
71
+ // registered.
72
+ async unregister(name) {
73
+ var idx = -1;
74
+ for (var i = 0; i < this.plugins.length; i++) {
75
+ var p = this.plugins[i];
76
+ if (p && p.name === name) {
77
+ idx = i;
78
+ break;
79
+ }
80
+ }
81
+ if (idx === -1)
82
+ return false;
83
+ var plugin = this.plugins[idx];
84
+ this.plugins.splice(idx, 1);
85
+ if (plugin && typeof plugin.dispose === 'function') {
86
+ try {
87
+ await plugin.dispose();
88
+ }
89
+ catch (err) {
90
+ // Drop guarantee extends to dispose. We can't log via the
91
+ // plugin's logger because we have no PluginContext here;
92
+ // fall back to console with the plugin name tag.
93
+ console.error('[plugin: ' + plugin.name + '] dispose() threw', this.errorMeta(err));
94
+ }
95
+ }
96
+ return true;
97
+ }
98
+ // Read-only snapshot of the plugin list, in dispatch order. Returns
99
+ // a fresh array; mutating it does not affect the registry.
100
+ list() {
101
+ return this.plugins.slice();
102
+ }
103
+ // Look up a plugin by name. Returns undefined if no plugin with
104
+ // that name is registered.
105
+ get(name) {
106
+ for (var i = 0; i < this.plugins.length; i++) {
107
+ var p = this.plugins[i];
108
+ if (p && p.name === name)
109
+ return p;
110
+ }
111
+ return undefined;
112
+ }
113
+ // ----- Dispatchers -----
114
+ // All five dispatchers share the same shape: iterate plugins in
115
+ // priority order, call the hook if defined, await the result, merge
116
+ // into the running EmittedEvents. Errors are caught per-plugin and
117
+ // logged via the plugin's logger if reachable through ctx; never
118
+ // thrown to the caller.
119
+ async dispatchTick(ctx) {
120
+ return this.dispatch(ctx, 'onTick', function (plugin, ctx) {
121
+ return plugin.onTick(ctx);
122
+ });
123
+ }
124
+ async dispatchPeerJoin(ctx, peer) {
125
+ return this.dispatch(ctx, 'onPeerJoin', function (plugin, ctx) {
126
+ return plugin.onPeerJoin(ctx, peer);
127
+ });
128
+ }
129
+ async dispatchPeerLeave(ctx, peer) {
130
+ return this.dispatch(ctx, 'onPeerLeave', function (plugin, ctx) {
131
+ return plugin.onPeerLeave(ctx, peer);
132
+ });
133
+ }
134
+ async dispatchZoneEnter(ctx, peer, fromZone) {
135
+ return this.dispatch(ctx, 'onZoneEnter', function (plugin, ctx) {
136
+ return plugin.onZoneEnter(ctx, peer, fromZone);
137
+ });
138
+ }
139
+ async dispatchPlayerAction(ctx, peer, action) {
140
+ return this.dispatch(ctx, 'onPlayerAction', function (plugin, ctx) {
141
+ return plugin.onPlayerAction(ctx, peer, action);
142
+ });
143
+ }
144
+ // ----- Internals -----
145
+ // Generic dispatch helper. `hookName` is the property name we check
146
+ // for definition on each plugin; `invoke` calls the actual hook
147
+ // (the spread of args differs per dispatcher). Per-plugin try/catch
148
+ // around the await isolates failures.
149
+ async dispatch(ctx, hookName, invoke) {
150
+ var merged = {};
151
+ var characterEvents = undefined;
152
+ var zoneEvents = undefined;
153
+ // Snapshot the plugin list at dispatch start so a mutation during
154
+ // the dispatch (a hook calls registry.register/unregister) cannot
155
+ // change which plugins run for THIS dispatch. Spec doesn't forbid
156
+ // mutation but the predictable behavior is "this dispatch sees
157
+ // the registry as it was at start".
158
+ var snapshot = this.plugins.slice();
159
+ for (var i = 0; i < snapshot.length; i++) {
160
+ var plugin = snapshot[i];
161
+ if (!plugin)
162
+ continue;
163
+ // Hook not implemented by this plugin? Skip without allocating
164
+ // anything. Use bracket access so the keyof typing carries through.
165
+ var hook = plugin[hookName];
166
+ if (typeof hook !== 'function')
167
+ continue;
168
+ var emitted;
169
+ try {
170
+ emitted = await invoke(plugin, ctx);
171
+ }
172
+ catch (err) {
173
+ // Log via the plugin's logger so the failure is tagged with
174
+ // the plugin name. ctx.logger may itself throw; if so, fall
175
+ // back to console (cannot let logger errors break dispatch).
176
+ try {
177
+ ctx.logger.error('plugin hook ' + String(hookName) + ' threw', this.errorMeta(err));
178
+ }
179
+ catch {
180
+ console.error('[plugin: ' + plugin.name + '] hook ' + String(hookName) + ' threw', this.errorMeta(err));
181
+ }
182
+ // Drop this plugin's contribution for this dispatch.
183
+ continue;
184
+ }
185
+ if (!emitted)
186
+ continue;
187
+ if (emitted.characterEvents && emitted.characterEvents.length > 0) {
188
+ if (!characterEvents)
189
+ characterEvents = [];
190
+ for (var ci = 0; ci < emitted.characterEvents.length; ci++) {
191
+ var ce = emitted.characterEvents[ci];
192
+ if (ce)
193
+ characterEvents.push(ce);
194
+ }
195
+ }
196
+ if (emitted.zoneEvents && emitted.zoneEvents.length > 0) {
197
+ if (!zoneEvents)
198
+ zoneEvents = [];
199
+ for (var zi = 0; zi < emitted.zoneEvents.length; zi++) {
200
+ var ze = emitted.zoneEvents[zi];
201
+ if (ze)
202
+ zoneEvents.push(ze);
203
+ }
204
+ }
205
+ }
206
+ if (characterEvents)
207
+ merged.characterEvents = characterEvents;
208
+ if (zoneEvents)
209
+ merged.zoneEvents = zoneEvents;
210
+ return merged;
211
+ }
212
+ // Pull useful fields out of an unknown error for log meta. Avoids
213
+ // throwing on non-Error throws (strings, objects, undefined).
214
+ errorMeta(err) {
215
+ if (err instanceof Error) {
216
+ return {
217
+ error_name: err.name,
218
+ error_message: err.message,
219
+ error_stack: err.stack ?? null,
220
+ };
221
+ }
222
+ var safe;
223
+ try {
224
+ safe = JSON.stringify(err);
225
+ }
226
+ catch {
227
+ safe = String(err);
228
+ }
229
+ return { error: safe };
230
+ }
231
+ }
232
+ //# sourceMappingURL=ai-plugin-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-plugin-registry.js","sourceRoot":"","sources":["../../../src/director/ai/ai-plugin-registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { DirectorEvent } from '../event-envelope.js';
2
+ import type { IAIPlugin, EmittedEvents, PluginContext, ZoneEvent } from './plugin.js';
3
+ export interface MockAIPluginScriptEntry {
4
+ atTick: number;
5
+ characterEvents?: DirectorEvent[];
6
+ zoneEvents?: ZoneEvent[];
7
+ }
8
+ export interface MockAIPluginOptions {
9
+ name?: string;
10
+ script: ReadonlyArray<MockAIPluginScriptEntry>;
11
+ priority?: number;
12
+ }
13
+ export declare class MockAIPlugin implements IAIPlugin {
14
+ readonly name: string;
15
+ readonly version = "0.0.1";
16
+ readonly priority: number;
17
+ private readonly script;
18
+ private tick;
19
+ constructor(opts: MockAIPluginOptions);
20
+ onTick(_ctx: PluginContext): Promise<EmittedEvents>;
21
+ currentTick(): number;
22
+ resetTick(): void;
23
+ }
24
+ //# sourceMappingURL=mock-ai-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-ai-plugin.d.ts","sourceRoot":"","sources":["../../../src/director/ai/mock-ai-plugin.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,aAAa,EACb,SAAS,EACV,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,uBAAuB;IAGtC,MAAM,EAAE,MAAM,CAAC;IAEf,eAAe,CAAC,EAAE,aAAa,EAAE,CAAC;IAElC,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAIlC,IAAI,CAAC,EAAE,MAAM,CAAC;IAId,MAAM,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAE/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,YAAa,YAAW,SAAS;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,WAAW;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAIhE,OAAO,CAAC,IAAI,CAAK;gBAEL,IAAI,EAAE,mBAAmB;IAM/B,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;IA+BzD,WAAW,IAAI,MAAM;IAOrB,SAAS,IAAI,IAAI;CAGlB"}
@@ -0,0 +1,83 @@
1
+ // MockAIPlugin - deterministic synthetic events for tests + offline demo.
2
+ //
3
+ // Per LOOM-DIRECTOR-PROTOCOL-V2 Section 5.4: replaces real LLM
4
+ // dispatch with a scripted sequence of events keyed by tick number.
5
+ // Engine tests can wire this in place of an Anthropic-backed plugin
6
+ // to verify registry behavior, dispatch order, and downstream emit
7
+ // paths without burning API budget. The offline demo can use it to
8
+ // drive a showcase loop with predictable events.
9
+ //
10
+ // Determinism: the script is consulted on each onTick() call. The
11
+ // plugin keeps an internal tick counter that increments every call;
12
+ // the constructor's `tickKey` option lets the caller provide a
13
+ // counter source instead (e.g. tying it to the engine's frame index
14
+ // for cross-process consistency).
15
+ //
16
+ // Multiple instances: the constructor accepts an optional `name`
17
+ // override so a test can register two MockAIPlugins with different
18
+ // scripts. Without override, the name is 'mock' and registering two
19
+ // instances throws AIPluginDuplicateError (intentional - the spec
20
+ // keys plugins by name).
21
+ export class MockAIPlugin {
22
+ name;
23
+ version = '0.0.1';
24
+ priority;
25
+ script;
26
+ // Current tick count; increments every onTick() call. The first
27
+ // tick observed is 1 (matches the typical engine convention where
28
+ // tick 0 is "before-first-update").
29
+ tick = 0;
30
+ constructor(opts) {
31
+ this.name = opts.name ?? 'mock';
32
+ this.priority = opts.priority ?? 999;
33
+ this.script = opts.script;
34
+ }
35
+ async onTick(_ctx) {
36
+ this.tick++;
37
+ var characterEvents;
38
+ var zoneEvents;
39
+ for (var i = 0; i < this.script.length; i++) {
40
+ var entry = this.script[i];
41
+ if (!entry)
42
+ continue;
43
+ if (entry.atTick !== this.tick)
44
+ continue;
45
+ if (entry.characterEvents && entry.characterEvents.length > 0) {
46
+ if (!characterEvents)
47
+ characterEvents = [];
48
+ for (var ci = 0; ci < entry.characterEvents.length; ci++) {
49
+ var ce = entry.characterEvents[ci];
50
+ if (ce)
51
+ characterEvents.push(ce);
52
+ }
53
+ }
54
+ if (entry.zoneEvents && entry.zoneEvents.length > 0) {
55
+ if (!zoneEvents)
56
+ zoneEvents = [];
57
+ for (var zi = 0; zi < entry.zoneEvents.length; zi++) {
58
+ var ze = entry.zoneEvents[zi];
59
+ if (ze)
60
+ zoneEvents.push(ze);
61
+ }
62
+ }
63
+ }
64
+ var emitted = {};
65
+ if (characterEvents)
66
+ emitted.characterEvents = characterEvents;
67
+ if (zoneEvents)
68
+ emitted.zoneEvents = zoneEvents;
69
+ return emitted;
70
+ }
71
+ // Inspect-only: current tick count. Useful in tests to assert the
72
+ // plugin saw the expected number of dispatches.
73
+ currentTick() {
74
+ return this.tick;
75
+ }
76
+ // Reset the tick counter to 0 so the script can replay from the
77
+ // start. Tests that re-use a single plugin across multiple cases
78
+ // call this between cases.
79
+ resetTick() {
80
+ this.tick = 0;
81
+ }
82
+ }
83
+ //# sourceMappingURL=mock-ai-plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-ai-plugin.js","sourceRoot":"","sources":["../../../src/director/ai/mock-ai-plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { PluginContext, PluginLogger, PluginStorage, PeerInfo, CharacterState } from './plugin.js';
2
+ export declare class MapPluginStorage {
3
+ private readonly store;
4
+ forPlugin(pluginName: string): PluginStorage;
5
+ clearPlugin(pluginName: string): void;
6
+ size(): number;
7
+ private composeKey;
8
+ }
9
+ export declare class ConsolePluginLogger implements PluginLogger {
10
+ private readonly pluginName;
11
+ constructor(pluginName: string);
12
+ info(msg: string, meta?: Record<string, unknown>): void;
13
+ warn(msg: string, meta?: Record<string, unknown>): void;
14
+ error(msg: string, meta?: Record<string, unknown>): void;
15
+ private write;
16
+ }
17
+ export interface BuildPluginContextOptions {
18
+ pluginName: string;
19
+ storage: MapPluginStorage;
20
+ logger?: PluginLogger;
21
+ getZonePeers?: (zoneId: string) => ReadonlyArray<PeerInfo>;
22
+ getCharacterState?: (characterId: string) => Readonly<CharacterState>;
23
+ getZoneState?: (zoneId: string) => ReadonlyMap<string, unknown>;
24
+ now?: () => number;
25
+ }
26
+ export declare function buildPluginContext(opts: BuildPluginContextOptions): PluginContext;
27
+ //# sourceMappingURL=plugin-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-context.d.ts","sourceRoot":"","sources":["../../../src/director/ai/plugin-context.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,cAAc,EACf,MAAM,aAAa,CAAC;AAQrB,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA8B;IAKpD,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa;IAkB5C,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAsBrC,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,UAAU;CAGnB;AAQD,qBAAa,mBAAoB,YAAW,YAAY;IAC1C,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,MAAM;IAE/C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIvD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIvD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIxD,OAAO,CAAC,KAAK;CAwBd;AAYD,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,gBAAgB,CAAC;IAE1B,MAAM,CAAC,EAAE,YAAY,CAAC;IAGtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC3D,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,QAAQ,CAAC,cAAc,CAAC,CAAC;IACtE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEhE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,aAAa,CAmCjF"}