@sadhaka/loom-engine 0.13.0 → 0.15.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 (89) hide show
  1. package/dist/audio/audio-asset-cache.d.ts +12 -0
  2. package/dist/audio/audio-asset-cache.d.ts.map +1 -0
  3. package/dist/audio/audio-asset-cache.js +53 -0
  4. package/dist/audio/audio-asset-cache.js.map +1 -0
  5. package/dist/audio/audio-asset-loader.d.ts +16 -0
  6. package/dist/audio/audio-asset-loader.d.ts.map +1 -0
  7. package/dist/audio/audio-asset-loader.js +100 -0
  8. package/dist/audio/audio-asset-loader.js.map +1 -0
  9. package/dist/audio/audio-listener-resource.d.ts +19 -0
  10. package/dist/audio/audio-listener-resource.d.ts.map +1 -0
  11. package/dist/audio/audio-listener-resource.js +57 -0
  12. package/dist/audio/audio-listener-resource.js.map +1 -0
  13. package/dist/audio/cue-catalog.d.ts +39 -0
  14. package/dist/audio/cue-catalog.d.ts.map +1 -0
  15. package/dist/audio/cue-catalog.js +186 -0
  16. package/dist/audio/cue-catalog.js.map +1 -0
  17. package/dist/audio/music-director.d.ts +18 -0
  18. package/dist/audio/music-director.d.ts.map +1 -0
  19. package/dist/audio/music-director.js +177 -0
  20. package/dist/audio/music-director.js.map +1 -0
  21. package/dist/audio/spatial-audio-bus.d.ts +66 -0
  22. package/dist/audio/spatial-audio-bus.d.ts.map +1 -0
  23. package/dist/audio/spatial-audio-bus.js +341 -0
  24. package/dist/audio/spatial-audio-bus.js.map +1 -0
  25. package/dist/audio/spatial-audio-system.d.ts +17 -0
  26. package/dist/audio/spatial-audio-system.d.ts.map +1 -0
  27. package/dist/audio/spatial-audio-system.js +98 -0
  28. package/dist/audio/spatial-audio-system.js.map +1 -0
  29. package/dist/audio/zone-audio-system.d.ts +78 -0
  30. package/dist/audio/zone-audio-system.d.ts.map +1 -0
  31. package/dist/audio/zone-audio-system.js +206 -0
  32. package/dist/audio/zone-audio-system.js.map +1 -0
  33. package/dist/director/ai/ai-plugin-registry.d.ts +20 -0
  34. package/dist/director/ai/ai-plugin-registry.d.ts.map +1 -0
  35. package/dist/director/ai/ai-plugin-registry.js +232 -0
  36. package/dist/director/ai/ai-plugin-registry.js.map +1 -0
  37. package/dist/director/ai/mock-ai-plugin.d.ts +24 -0
  38. package/dist/director/ai/mock-ai-plugin.d.ts.map +1 -0
  39. package/dist/director/ai/mock-ai-plugin.js +83 -0
  40. package/dist/director/ai/mock-ai-plugin.js.map +1 -0
  41. package/dist/director/ai/plugin-context.d.ts +27 -0
  42. package/dist/director/ai/plugin-context.d.ts.map +1 -0
  43. package/dist/director/ai/plugin-context.js +152 -0
  44. package/dist/director/ai/plugin-context.js.map +1 -0
  45. package/dist/director/ai/plugin.d.ts +57 -0
  46. package/dist/director/ai/plugin.d.ts.map +1 -0
  47. package/dist/director/ai/plugin.js +32 -0
  48. package/dist/director/ai/plugin.js.map +1 -0
  49. package/dist/director/index.d.ts +27 -0
  50. package/dist/director/index.d.ts.map +1 -0
  51. package/dist/director/index.js +26 -0
  52. package/dist/director/index.js.map +1 -0
  53. package/dist/director/zone/mock-zone-bridge.d.ts +22 -0
  54. package/dist/director/zone/mock-zone-bridge.d.ts.map +1 -0
  55. package/dist/director/zone/mock-zone-bridge.js +107 -0
  56. package/dist/director/zone/mock-zone-bridge.js.map +1 -0
  57. package/dist/director/zone/sse-zone-bridge.d.ts +40 -0
  58. package/dist/director/zone/sse-zone-bridge.d.ts.map +1 -0
  59. package/dist/director/zone/sse-zone-bridge.js +164 -0
  60. package/dist/director/zone/sse-zone-bridge.js.map +1 -0
  61. package/dist/director/zone/zone-event-bridge.d.ts +21 -0
  62. package/dist/director/zone/zone-event-bridge.d.ts.map +1 -0
  63. package/dist/director/zone/zone-event-bridge.js +24 -0
  64. package/dist/director/zone/zone-event-bridge.js.map +1 -0
  65. package/dist/director/zone/zone-event-envelope.d.ts +90 -0
  66. package/dist/director/zone/zone-event-envelope.d.ts.map +1 -0
  67. package/dist/director/zone/zone-event-envelope.js +104 -0
  68. package/dist/director/zone/zone-event-envelope.js.map +1 -0
  69. package/dist/director/zone/zone-event-log.d.ts +17 -0
  70. package/dist/director/zone/zone-event-log.d.ts.map +1 -0
  71. package/dist/director/zone/zone-event-log.js +46 -0
  72. package/dist/director/zone/zone-event-log.js.map +1 -0
  73. package/dist/director/zone/zone-event-system.d.ts +14 -0
  74. package/dist/director/zone/zone-event-system.d.ts.map +1 -0
  75. package/dist/director/zone/zone-event-system.js +179 -0
  76. package/dist/director/zone/zone-event-system.js.map +1 -0
  77. package/dist/director/zone/zone-state-resource.d.ts +15 -0
  78. package/dist/director/zone/zone-state-resource.d.ts.map +1 -0
  79. package/dist/director/zone/zone-state-resource.js +60 -0
  80. package/dist/director/zone/zone-state-resource.js.map +1 -0
  81. package/dist/index.d.ts +27 -1
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +23 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/server/index.d.ts +7 -0
  86. package/dist/server/index.d.ts.map +1 -0
  87. package/dist/server/index.js +33 -0
  88. package/dist/server/index.js.map +1 -0
  89. package/package.json +6 -2
@@ -0,0 +1,98 @@
1
+ // SpatialAudioSystem - pushes the local character's transform into
2
+ // the AudioListener resource and the SpatialAudioBus each frame.
3
+ //
4
+ // Phasing rationale (matches PeerRenderSystem / animation-system):
5
+ // PHASE_RENDER, AFTER any camera/transform sync systems, BEFORE
6
+ // the renderer submits draw calls. The listener pose for THIS frame
7
+ // reflects the THIS-FRAME world transform of the player - so a
8
+ // positional sound triggered by a render-phase system (e.g. cue
9
+ // reactions to zone events drained earlier in PHASE_LOGIC) hears
10
+ // the world from the up-to-date listener pose.
11
+ //
12
+ // Per LOOM-AUDIO-SPEC.md §3.3 the work is one-line: read the local
13
+ // character's TransformPool entry by entity id, write its (x, y, z)
14
+ // into the AudioListenerResource.pose, stamp lastUpdateFrame, and
15
+ // hand the pose to SpatialAudioBus.setListener.
16
+ //
17
+ // Identity binding: the engine doesn't know which entity is the local
18
+ // character - that's a consumer concern (TWT wires it after server
19
+ // auth completes). Consumers call setLocalCharacterEntity(entity) on
20
+ // the system. setLocalCharacterEntity(null) puts the system back into
21
+ // no-op mode (e.g. between zones during a teleport).
22
+ //
23
+ // Tolerates:
24
+ // - missing AudioListenerResource (silent no-op; engine probably
25
+ // not yet fully wired)
26
+ // - missing SpatialAudioBus (silent no-op; consumer might have
27
+ // opted out of spatial audio entirely - the system still keeps
28
+ // the resource fresh for debug/HUD readers)
29
+ // - missing TransformPool (silent no-op; engine without ECS
30
+ // transforms is unusual but valid in headless server tests)
31
+ // - local character set but transform not attached yet (no-op)
32
+ //
33
+ // What this system does NOT do (deferred to future phases):
34
+ // - rotate the listener with the player's facing (spec §8.4 fixes
35
+ // forward = (0, 0, -1))
36
+ // - apply velocity-based Doppler (PannerNode.setVelocity is
37
+ // deprecated; v1 ships flat doppler-off audio)
38
+ // - any per-source position update (handle.setPosition owns that)
39
+ import { entityIndex } from '../entity.js';
40
+ import { POOL_TRANSFORM } from '../world.js';
41
+ import { RESOURCE_TIME, } from '../resources.js';
42
+ import { RESOURCE_AUDIO_LISTENER, } from './audio-listener-resource.js';
43
+ export class SpatialAudioSystem {
44
+ name = 'spatial-audio';
45
+ localCharacter = null;
46
+ spatialBus = null;
47
+ // Optional spatial bus reference. The system can also work without
48
+ // a bus (resource-only mode) - useful for tests and for consumers
49
+ // who do not want positional audio but still want a tracked listener
50
+ // pose (e.g. for visual SFX triggered relative to the player).
51
+ constructor(opts = {}) {
52
+ this.spatialBus = opts.spatialBus ?? null;
53
+ }
54
+ // Wire (or rewire) the spatial bus. Useful when the AudioBus is
55
+ // created lazily after first user gesture - the system can be
56
+ // registered upfront and the bus attached later.
57
+ setSpatialBus(bus) {
58
+ this.spatialBus = bus;
59
+ }
60
+ // Bind the local character entity. null means "no local character"
61
+ // (e.g. mid-teleport, character not yet allocated, headless mode).
62
+ // The system tolerates an entity that doesn't have a transform yet
63
+ // by silently skipping the update.
64
+ setLocalCharacterEntity(entity) {
65
+ this.localCharacter = entity;
66
+ }
67
+ getLocalCharacterEntity() {
68
+ return this.localCharacter;
69
+ }
70
+ update(world, _dt) {
71
+ if (this.localCharacter === null)
72
+ return;
73
+ var listener = world.resources.get(RESOURCE_AUDIO_LISTENER);
74
+ if (!listener)
75
+ return;
76
+ var transforms = world.getPool(POOL_TRANSFORM);
77
+ if (!transforms)
78
+ return;
79
+ var idx = entityIndex(this.localCharacter);
80
+ // Bounds check against the high-water mark so a fresh entity that
81
+ // the consumer registered before attaching a transform doesn't
82
+ // produce undefined reads on the typed arrays.
83
+ if (idx >= transforms.getHighWaterMark())
84
+ return;
85
+ var x = transforms.x[idx] ?? 0;
86
+ var y = transforms.y[idx] ?? 0;
87
+ var z = transforms.z[idx] ?? 0;
88
+ listener.pose.x = x;
89
+ listener.pose.y = y;
90
+ listener.pose.z = z;
91
+ var time = world.resources.get(RESOURCE_TIME);
92
+ listener.lastUpdateFrame = time ? time.frame : listener.lastUpdateFrame + 1;
93
+ if (this.spatialBus) {
94
+ this.spatialBus.setListener(listener.pose);
95
+ }
96
+ }
97
+ }
98
+ //# sourceMappingURL=spatial-audio-system.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatial-audio-system.js","sourceRoot":"","sources":["../../src/audio/spatial-audio-system.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import type { System } from '../system.js';
2
+ import type { World } from '../world.js';
3
+ import type { ZoneEvent, ZoneEventType } from '../director/zone/zone-event-envelope.js';
4
+ export interface PositionalPlayOptionsStub {
5
+ x?: number;
6
+ y?: number;
7
+ z?: number;
8
+ gain?: number;
9
+ rate?: number;
10
+ loop?: boolean;
11
+ refDistance?: number;
12
+ maxDistance?: number;
13
+ rolloffFactor?: number;
14
+ distanceModel?: 'linear' | 'inverse' | 'exponential';
15
+ }
16
+ export interface AudioListenerPoseStub {
17
+ x: number;
18
+ y: number;
19
+ z?: number;
20
+ forward?: {
21
+ x: number;
22
+ y: number;
23
+ z: number;
24
+ };
25
+ up?: {
26
+ x: number;
27
+ y: number;
28
+ z: number;
29
+ };
30
+ }
31
+ export interface AudioListenerResourceStub {
32
+ pose: AudioListenerPoseStub;
33
+ lastUpdateFrame: number;
34
+ }
35
+ export interface CueCatalogStub {
36
+ play(name: string, options?: PositionalPlayOptionsStub): unknown;
37
+ }
38
+ export interface MusicDirectorStub {
39
+ playMusic(name: string, fadeInMs?: number): void;
40
+ stopMusic(fadeOutMs?: number): Promise<void> | void;
41
+ crossfadeMusic(name: string, fadeMs?: number): void;
42
+ currentMusic(): string | null;
43
+ }
44
+ export declare const RESOURCE_AUDIO_LISTENER_STUB = "audio_listener";
45
+ export declare const RESOURCE_CUE_CATALOG_STUB = "cue_catalog";
46
+ export declare const RESOURCE_MUSIC_DIRECTOR_STUB = "music_director";
47
+ export interface ZoneCuePlay {
48
+ cue: string;
49
+ options?: PositionalPlayOptionsStub;
50
+ }
51
+ export interface ZoneAudioContext {
52
+ cues: CueCatalogStub | null;
53
+ music: MusicDirectorStub | null;
54
+ localZone: string | null;
55
+ listener: AudioListenerPoseStub;
56
+ }
57
+ export interface ZoneAudioMapping {
58
+ eventType: ZoneEventType;
59
+ handle(event: ZoneEvent, ctx: ZoneAudioContext): ZoneCuePlay | null;
60
+ }
61
+ export interface ZoneAudioSystemOptions {
62
+ currentZone?: () => string | null;
63
+ verbose?: boolean;
64
+ }
65
+ export declare class ZoneAudioSystem implements System {
66
+ readonly name: string;
67
+ private readonly mappings;
68
+ private readonly currentZone;
69
+ private readonly verbose;
70
+ private readonly lastProcessedIdByZone;
71
+ constructor(opts?: ZoneAudioSystemOptions);
72
+ registerMapping(mapping: ZoneAudioMapping): void;
73
+ unregisterMapping(eventType: ZoneEventType): void;
74
+ hasMapping(eventType: ZoneEventType): boolean;
75
+ mappingCount(): number;
76
+ update(world: World, _dt: number): void;
77
+ }
78
+ //# sourceMappingURL=zone-audio-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zone-audio-system.d.ts","sourceRoot":"","sources":["../../src/audio/zone-audio-system.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACd,MAAM,yCAAyC,CAAC;AAmBjD,MAAM,WAAW,yBAAyB;IACxC,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;CACtD;AAKD,MAAM,WAAW,qBAAqB;IACpC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,EAAE,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAID,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,CACF,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC;CACZ;AAMD,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjD,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpD,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpD,YAAY,IAAI,MAAM,GAAG,IAAI,CAAC;CAC/B;AAQD,eAAO,MAAM,4BAA4B,mBAAmB,CAAC;AAC7D,eAAO,MAAM,yBAAyB,gBAAgB,CAAC;AACvD,eAAO,MAAM,4BAA4B,mBAAmB,CAAC;AAI7D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACrC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAIhC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAGzB,QAAQ,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,aAAa,CAAC;IAKzB,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,GAAG,WAAW,GAAG,IAAI,CAAC;CACrE;AAED,MAAM,WAAW,sBAAsB;IAIrC,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAGlC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,qBAAa,eAAgB,YAAW,MAAM;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAgB;IAErC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAIlC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAsB;gBAEhD,IAAI,GAAE,sBAA2B;IAO7C,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAQhD,iBAAiB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAKjD,UAAU,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO;IAI7C,YAAY,IAAI,MAAM;IAItB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;CAsGxC"}
@@ -0,0 +1,206 @@
1
+ // ZoneAudioSystem - zone-event audio integration shell (Phase 17, Track C).
2
+ //
3
+ // Runs PHASE_RENDER, AFTER ZoneEventSystem (which appends every
4
+ // observed zone event to the per-zone ring buffer in
5
+ // ZoneEventLog.byZone[<zone>].recent). Each tick the audio system
6
+ // drains the local zone's ring buffer for events that landed THIS
7
+ // frame (i.e. events with id strictly greater than the last id we
8
+ // processed for that zone), looks up the consumer-registered mapping
9
+ // for each event type, and dispatches the resulting cue play through
10
+ // the registered CueCatalog / MusicDirector resources.
11
+ //
12
+ // This file ships ZERO mappings. Engine consumers (TheWorldTable.ai,
13
+ // etc.) register their own mappings for the cues they want to fire on
14
+ // each zone event type.
15
+ //
16
+ // Tolerances:
17
+ // - Missing mapping for an event type: log + skip silently. Most
18
+ // consumers will register only a subset of the seven zone event
19
+ // types.
20
+ // - Missing CueCatalog / MusicDirector resources: no-op gracefully.
21
+ // The system keeps draining events but cue plays evaporate. This
22
+ // lets a consumer wire ZoneAudioSystem before the catalog is built
23
+ // (e.g. while audio assets are still loading) without crashing.
24
+ // - Missing ZoneEventLog: no-op (nothing to drain).
25
+ // - Missing AudioListener resource: cue plays still dispatch but
26
+ // `listener` in the handler context falls back to a zero pose.
27
+ // - Multiple mappings for the SAME event type: registerMapping
28
+ // overwrites. unregisterMapping by eventType.
29
+ //
30
+ // Dispatch ORDER: registerMapping registration order is irrelevant
31
+ // since each event type has at most ONE handler. The order in which
32
+ // events are processed within a tick is the order they appear in
33
+ // ZoneEventLog.byZone[<zone>].recent (newest first, per Phase 16
34
+ // log semantics). Per spec we replay newest-first so a consumer that
35
+ // registers a music crossfade reacts to the latest knot pulse before
36
+ // older ones; the zone-events log is a deduped ring, so duplicates
37
+ // are not a concern.
38
+ import { RESOURCE_ZONE_EVENT_LOG, } from '../director/zone/zone-event-log.js';
39
+ // Resource keys (defined here as duplicate string constants to keep
40
+ // the system standalone). The values MATCH the ones declared in the
41
+ // Track A / Track B files; coordination merge will swap to a single
42
+ // import after 0.15.0 assembles.
43
+ // TODO[phase-17-merge]: replace these constants with re-imports from
44
+ // audio-listener-resource.ts / cue-catalog.ts / music-director.ts.
45
+ export const RESOURCE_AUDIO_LISTENER_STUB = 'audio_listener';
46
+ export const RESOURCE_CUE_CATALOG_STUB = 'cue_catalog';
47
+ export const RESOURCE_MUSIC_DIRECTOR_STUB = 'music_director';
48
+ // ---- The system ----
49
+ export class ZoneAudioSystem {
50
+ name = 'zone-audio';
51
+ mappings;
52
+ currentZone;
53
+ verbose;
54
+ // Per-zone last processed event id. Drives the "events landed this
55
+ // frame" filter so the system reads the per-zone ring buffer once
56
+ // per tick and fires only fresh events.
57
+ lastProcessedIdByZone;
58
+ constructor(opts = {}) {
59
+ this.mappings = new Map();
60
+ this.currentZone = opts.currentZone ?? null;
61
+ this.verbose = opts.verbose ?? false;
62
+ this.lastProcessedIdByZone = new Map();
63
+ }
64
+ registerMapping(mapping) {
65
+ if (!mapping || typeof mapping.eventType !== 'string'
66
+ || typeof mapping.handle !== 'function') {
67
+ return;
68
+ }
69
+ this.mappings.set(mapping.eventType, mapping);
70
+ }
71
+ unregisterMapping(eventType) {
72
+ this.mappings.delete(eventType);
73
+ }
74
+ // For tests + introspection.
75
+ hasMapping(eventType) {
76
+ return this.mappings.has(eventType);
77
+ }
78
+ mappingCount() {
79
+ return this.mappings.size;
80
+ }
81
+ update(world, _dt) {
82
+ const log = world.resources.get(RESOURCE_ZONE_EVENT_LOG);
83
+ if (!log)
84
+ return;
85
+ const cues = world.resources.get(RESOURCE_CUE_CATALOG_STUB) ?? null;
86
+ const music = world.resources.get(RESOURCE_MUSIC_DIRECTOR_STUB) ?? null;
87
+ const listenerRes = world.resources.get(RESOURCE_AUDIO_LISTENER_STUB);
88
+ const listener = listenerRes
89
+ ? listenerRes.pose
90
+ : { x: 0, y: 0, z: 0 };
91
+ const localZone = safeCurrentZone(this.currentZone);
92
+ // Decide which zones to drain. With a local-zone filter set, we
93
+ // drain ONLY that zone; without it, we drain every zone present
94
+ // in the log (single-zone consumers + tests).
95
+ const zonesToDrain = [];
96
+ if (localZone !== null) {
97
+ if (log.byZone.has(localZone))
98
+ zonesToDrain.push(localZone);
99
+ }
100
+ else {
101
+ for (const z of log.byZone.keys())
102
+ zonesToDrain.push(z);
103
+ }
104
+ for (let zi = 0; zi < zonesToDrain.length; zi++) {
105
+ const zone = zonesToDrain[zi];
106
+ const entry = log.byZone.get(zone);
107
+ if (!entry)
108
+ continue;
109
+ // recent[] is newest-first per Phase 16 ZoneEventLog convention.
110
+ // We walk from oldest -> newest among UNSEEN events so handlers
111
+ // see events in chronological order (e.g. boss.spawn before
112
+ // boss.tick). Determine the cutoff by lastProcessedId for this
113
+ // zone.
114
+ const lastId = this.lastProcessedIdByZone.get(zone) ?? 0;
115
+ let maxIdSeen = lastId;
116
+ // Walk newest->oldest, collect unseen, then reverse so dispatch
117
+ // is oldest->newest.
118
+ const fresh = [];
119
+ for (let i = 0; i < entry.recent.length; i++) {
120
+ const ev = entry.recent[i];
121
+ if (!ev)
122
+ continue;
123
+ if (ev.id > lastId) {
124
+ fresh.push(ev);
125
+ if (ev.id > maxIdSeen)
126
+ maxIdSeen = ev.id;
127
+ }
128
+ else {
129
+ // Past the boundary; older events have already been
130
+ // processed.
131
+ break;
132
+ }
133
+ }
134
+ if (fresh.length === 0)
135
+ continue;
136
+ // Reverse to oldest-first.
137
+ for (let i = fresh.length - 1; i >= 0; i--) {
138
+ const ev = fresh[i];
139
+ const mapping = this.mappings.get(ev.type);
140
+ if (!mapping) {
141
+ if (this.verbose) {
142
+ try {
143
+ console.log('[zone-audio] no mapping for ' + ev.type + ' (zone='
144
+ + ev.zone_id + ', id=' + ev.id + ')');
145
+ }
146
+ catch { /* ignore */ }
147
+ }
148
+ continue;
149
+ }
150
+ const ctx = {
151
+ cues,
152
+ music,
153
+ localZone,
154
+ listener,
155
+ };
156
+ let result = null;
157
+ try {
158
+ result = mapping.handle(ev, ctx);
159
+ }
160
+ catch (err) {
161
+ if (this.verbose) {
162
+ try {
163
+ console.warn('[zone-audio] mapping handle threw', err);
164
+ }
165
+ catch { /* ignore */ }
166
+ }
167
+ result = null;
168
+ }
169
+ if (!result)
170
+ continue;
171
+ if (typeof result.cue !== 'string' || result.cue.length === 0)
172
+ continue;
173
+ if (!cues) {
174
+ // Catalog absent: silently drop the cue. The handler
175
+ // already ran (e.g. it called ctx.music.crossfadeMusic) so
176
+ // music side-effects still happen.
177
+ continue;
178
+ }
179
+ try {
180
+ cues.play(result.cue, result.options);
181
+ }
182
+ catch (err) {
183
+ if (this.verbose) {
184
+ try {
185
+ console.warn('[zone-audio] cues.play threw', err);
186
+ }
187
+ catch { /* ignore */ }
188
+ }
189
+ }
190
+ }
191
+ this.lastProcessedIdByZone.set(zone, maxIdSeen);
192
+ }
193
+ }
194
+ }
195
+ function safeCurrentZone(fn) {
196
+ if (!fn)
197
+ return null;
198
+ try {
199
+ const z = fn();
200
+ return typeof z === 'string' && z.length > 0 ? z : null;
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ //# sourceMappingURL=zone-audio-system.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zone-audio-system.js","sourceRoot":"","sources":["../../src/audio/zone-audio-system.ts"],"names":[],"mappings":"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"}
@@ -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