@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.
- package/README.md +113 -1
- package/dist/components/peer-sprite.d.ts +25 -0
- package/dist/components/peer-sprite.d.ts.map +1 -0
- package/dist/components/peer-sprite.js +48 -0
- package/dist/components/peer-sprite.js.map +1 -0
- package/dist/director/ai/ai-plugin-registry.d.ts +20 -0
- package/dist/director/ai/ai-plugin-registry.d.ts.map +1 -0
- package/dist/director/ai/ai-plugin-registry.js +232 -0
- package/dist/director/ai/ai-plugin-registry.js.map +1 -0
- package/dist/director/ai/mock-ai-plugin.d.ts +24 -0
- package/dist/director/ai/mock-ai-plugin.d.ts.map +1 -0
- package/dist/director/ai/mock-ai-plugin.js +83 -0
- package/dist/director/ai/mock-ai-plugin.js.map +1 -0
- package/dist/director/ai/plugin-context.d.ts +27 -0
- package/dist/director/ai/plugin-context.d.ts.map +1 -0
- package/dist/director/ai/plugin-context.js +152 -0
- package/dist/director/ai/plugin-context.js.map +1 -0
- package/dist/director/ai/plugin.d.ts +57 -0
- package/dist/director/ai/plugin.d.ts.map +1 -0
- package/dist/director/ai/plugin.js +32 -0
- package/dist/director/ai/plugin.js.map +1 -0
- package/dist/director/index.d.ts +27 -0
- package/dist/director/index.d.ts.map +1 -0
- package/dist/director/index.js +26 -0
- package/dist/director/index.js.map +1 -0
- package/dist/director/zone/mock-zone-bridge.d.ts +22 -0
- package/dist/director/zone/mock-zone-bridge.d.ts.map +1 -0
- package/dist/director/zone/mock-zone-bridge.js +107 -0
- package/dist/director/zone/mock-zone-bridge.js.map +1 -0
- package/dist/director/zone/sse-zone-bridge.d.ts +40 -0
- package/dist/director/zone/sse-zone-bridge.d.ts.map +1 -0
- package/dist/director/zone/sse-zone-bridge.js +164 -0
- package/dist/director/zone/sse-zone-bridge.js.map +1 -0
- package/dist/director/zone/zone-event-bridge.d.ts +21 -0
- package/dist/director/zone/zone-event-bridge.d.ts.map +1 -0
- package/dist/director/zone/zone-event-bridge.js +24 -0
- package/dist/director/zone/zone-event-bridge.js.map +1 -0
- package/dist/director/zone/zone-event-envelope.d.ts +90 -0
- package/dist/director/zone/zone-event-envelope.d.ts.map +1 -0
- package/dist/director/zone/zone-event-envelope.js +104 -0
- package/dist/director/zone/zone-event-envelope.js.map +1 -0
- package/dist/director/zone/zone-event-log.d.ts +17 -0
- package/dist/director/zone/zone-event-log.d.ts.map +1 -0
- package/dist/director/zone/zone-event-log.js +46 -0
- package/dist/director/zone/zone-event-log.js.map +1 -0
- package/dist/director/zone/zone-event-system.d.ts +14 -0
- package/dist/director/zone/zone-event-system.d.ts.map +1 -0
- package/dist/director/zone/zone-event-system.js +179 -0
- package/dist/director/zone/zone-event-system.js.map +1 -0
- package/dist/director/zone/zone-state-resource.d.ts +15 -0
- package/dist/director/zone/zone-state-resource.d.ts.map +1 -0
- package/dist/director/zone/zone-state-resource.js +60 -0
- package/dist/director/zone/zone-state-resource.js.map +1 -0
- package/dist/index.d.ts +25 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/network/mock-multiplayer-bridge.d.ts +34 -0
- package/dist/network/mock-multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/mock-multiplayer-bridge.js +91 -0
- package/dist/network/mock-multiplayer-bridge.js.map +1 -0
- package/dist/network/multiplayer-bridge.d.ts +45 -0
- package/dist/network/multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/multiplayer-bridge.js +34 -0
- package/dist/network/multiplayer-bridge.js.map +1 -0
- package/dist/network/peer-pool.d.ts +46 -0
- package/dist/network/peer-pool.d.ts.map +1 -0
- package/dist/network/peer-pool.js +185 -0
- package/dist/network/peer-pool.js.map +1 -0
- package/dist/network/sse-multiplayer-bridge.d.ts +36 -0
- package/dist/network/sse-multiplayer-bridge.d.ts.map +1 -0
- package/dist/network/sse-multiplayer-bridge.js +264 -0
- package/dist/network/sse-multiplayer-bridge.js.map +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +33 -0
- package/dist/server/index.js.map +1 -0
- package/dist/systems/peer-presence-system.d.ts +19 -0
- package/dist/systems/peer-presence-system.d.ts.map +1 -0
- package/dist/systems/peer-presence-system.js +118 -0
- package/dist/systems/peer-presence-system.js.map +1 -0
- 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
|
|
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"}
|