@jgengine/shell 0.9.0 → 0.11.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 (248) hide show
  1. package/CHANGELOG.md +34 -3
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/GamePhaseStamp.d.ts +1 -0
  5. package/dist/GamePhaseStamp.js +9 -0
  6. package/dist/GamePlayerShell.d.ts +4 -31
  7. package/dist/GamePlayerShell.js +162 -932
  8. package/dist/GameUiPreview.js +0 -2
  9. package/dist/audio/audioEngine.d.ts +8 -0
  10. package/dist/audio/audioEngine.js +63 -8
  11. package/dist/audio/audioWire.d.ts +22 -0
  12. package/dist/audio/audioWire.js +19 -0
  13. package/dist/audio/musicDirector.d.ts +31 -0
  14. package/dist/audio/musicDirector.js +125 -0
  15. package/dist/audio/musicVoices.d.ts +10 -0
  16. package/dist/audio/musicVoices.js +338 -0
  17. package/dist/audio/synthEngine.d.ts +9 -0
  18. package/dist/audio/synthEngine.js +57 -0
  19. package/dist/behaviour.d.ts +2 -1
  20. package/dist/behaviour.js +2 -1
  21. package/dist/behaviourAttach.d.ts +1 -0
  22. package/dist/behaviourAttach.js +1 -0
  23. package/dist/behaviourDriver.d.ts +1 -0
  24. package/dist/behaviourDriver.js +1 -0
  25. package/dist/boundActionDispatch.d.ts +13 -0
  26. package/dist/boundActionDispatch.js +37 -0
  27. package/dist/camera/GameCameraRig.d.ts +5 -2
  28. package/dist/camera/GameCameraRig.js +2 -2
  29. package/dist/camera/GameFirstPersonCamera.d.ts +12 -2
  30. package/dist/camera/GameFirstPersonCamera.js +29 -7
  31. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  32. package/dist/camera/GameInspectionCamera.js +13 -2
  33. package/dist/camera/GameOrbitCamera.js +59 -10
  34. package/dist/camera/cameraBlendMath.d.ts +3 -0
  35. package/dist/camera/cameraBlendMath.js +3 -0
  36. package/dist/camera/fovPreference.d.ts +6 -1
  37. package/dist/camera/fovPreference.js +6 -1
  38. package/dist/camera/index.d.ts +4 -7
  39. package/dist/camera/index.js +4 -7
  40. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  41. package/dist/camera/inspectionCameraMath.js +7 -2
  42. package/dist/camera/orbitCameraMath.d.ts +54 -6
  43. package/dist/camera/orbitCameraMath.js +61 -6
  44. package/dist/camera/rigMath.d.ts +94 -24
  45. package/dist/camera/rigMath.js +100 -24
  46. package/dist/camera/rigResolve.d.ts +4 -2
  47. package/dist/camera/rigResolve.js +4 -2
  48. package/dist/camera/shakeChannel.d.ts +6 -2
  49. package/dist/camera/shakeChannel.js +6 -2
  50. package/dist/camera/shakeChannelMath.d.ts +1 -0
  51. package/dist/camera/shakeChannelMath.js +1 -0
  52. package/dist/cartridge/runtime.d.ts +15 -0
  53. package/dist/cartridge/runtime.js +556 -0
  54. package/dist/cartridge/spec.d.ts +188 -0
  55. package/dist/cartridge/spec.js +19 -0
  56. package/dist/cartridge/validate.d.ts +2 -0
  57. package/dist/cartridge/validate.js +78 -0
  58. package/dist/cartridge.d.ts +1 -1
  59. package/dist/cartridge.js +4 -3
  60. package/dist/commandSink.d.ts +25 -0
  61. package/dist/commandSink.js +32 -0
  62. package/dist/defineGame.js +26 -5
  63. package/dist/devtools/ColPanel.d.ts +1 -0
  64. package/dist/devtools/ColPanel.js +14 -0
  65. package/dist/devtools/CollisionDebugWorld.js +1 -0
  66. package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
  67. package/dist/devtools/DevtoolsOverlay.js +19 -450
  68. package/dist/devtools/KeysPanel.d.ts +4 -0
  69. package/dist/devtools/KeysPanel.js +31 -0
  70. package/dist/devtools/LogsPanel.d.ts +1 -0
  71. package/dist/devtools/LogsPanel.js +12 -0
  72. package/dist/devtools/NetPanel.d.ts +4 -0
  73. package/dist/devtools/NetPanel.js +10 -0
  74. package/dist/devtools/PerfPanel.d.ts +4 -0
  75. package/dist/devtools/PerfPanel.js +53 -0
  76. package/dist/devtools/TunePanel.d.ts +3 -0
  77. package/dist/devtools/TunePanel.js +309 -0
  78. package/dist/devtools/agentBridge.d.ts +41 -0
  79. package/dist/devtools/agentBridge.js +262 -0
  80. package/dist/devtools/collisionDebug.d.ts +6 -0
  81. package/dist/devtools/collisionDebug.js +6 -0
  82. package/dist/devtools/collisionDebugMath.d.ts +9 -2
  83. package/dist/devtools/collisionDebugMath.js +25 -5
  84. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  85. package/dist/devtools/devtoolsOverrides.js +48 -0
  86. package/dist/devtools/panelAtoms.d.ts +9 -0
  87. package/dist/devtools/panelAtoms.js +10 -0
  88. package/dist/devtools/perfDiagnose.d.ts +2 -0
  89. package/dist/devtools/perfDiagnose.js +43 -0
  90. package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
  91. package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
  92. package/dist/drivers/FrameDriver.d.ts +37 -0
  93. package/dist/drivers/FrameDriver.js +232 -0
  94. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  95. package/dist/drivers/HudOnlyDriver.js +60 -0
  96. package/dist/environment/Daylight.d.ts +29 -7
  97. package/dist/environment/Daylight.js +153 -13
  98. package/dist/environment/EnvironmentScene.js +42 -7
  99. package/dist/environment/RoadRibbons.d.ts +7 -0
  100. package/dist/environment/RoadRibbons.js +57 -0
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/hotbarActions.d.ts +26 -0
  112. package/dist/hotbarActions.js +27 -0
  113. package/dist/inputSink.d.ts +30 -0
  114. package/dist/inputSink.js +85 -0
  115. package/dist/materialOverride.d.ts +10 -0
  116. package/dist/materialOverride.js +15 -3
  117. package/dist/multiplayer.js +14 -4
  118. package/dist/postfx/PostProcessing.d.ts +10 -0
  119. package/dist/postfx/PostProcessing.js +120 -0
  120. package/dist/postfx/gradeShader.d.ts +4 -0
  121. package/dist/postfx/gradeShader.js +72 -0
  122. package/dist/registry.d.ts +3 -2
  123. package/dist/render/EnvironmentLighting.d.ts +10 -0
  124. package/dist/render/EnvironmentLighting.js +34 -0
  125. package/dist/render/SceneLighting.d.ts +7 -0
  126. package/dist/render/SceneLighting.js +18 -0
  127. package/dist/render/SceneModels.d.ts +14 -0
  128. package/dist/render/SceneModels.js +337 -0
  129. package/dist/render/defaultSurface.d.ts +29 -0
  130. package/dist/render/defaultSurface.js +101 -0
  131. package/dist/render/modelLoad.d.ts +24 -0
  132. package/dist/render/modelLoad.js +73 -0
  133. package/dist/render/modelRender.d.ts +11 -0
  134. package/dist/render/modelRender.js +15 -0
  135. package/dist/render/resolveModel.d.ts +39 -3
  136. package/dist/render/resolveModel.js +79 -2
  137. package/dist/render/useEntityRenderCues.d.ts +13 -0
  138. package/dist/render/useEntityRenderCues.js +62 -0
  139. package/dist/scatter/InstancedScatter.d.ts +24 -0
  140. package/dist/scatter/InstancedScatter.js +130 -0
  141. package/dist/scatter/scatterModels.d.ts +25 -0
  142. package/dist/scatter/scatterModels.js +61 -0
  143. package/dist/scatter/scatterProxies.d.ts +6 -0
  144. package/dist/scatter/scatterProxies.js +94 -0
  145. package/dist/scatter.d.ts +1 -0
  146. package/dist/scatter.js +1 -0
  147. package/dist/scene/AuthoredScene.d.ts +82 -0
  148. package/dist/scene/AuthoredScene.js +173 -0
  149. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  150. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  151. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  152. package/dist/scene/GrassFieldRenderer.js +25 -0
  153. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  154. package/dist/scene/SoilPatchRenderer.js +21 -0
  155. package/dist/scene/StudioStage.d.ts +37 -0
  156. package/dist/scene/StudioStage.js +42 -0
  157. package/dist/scene/WaterRenderer.d.ts +2 -0
  158. package/dist/scene/WaterRenderer.js +37 -0
  159. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  160. package/dist/scene/builtinSceneKindRenderers.js +22 -0
  161. package/dist/scene/sceneKindRenderers.d.ts +28 -0
  162. package/dist/scene/sceneKindRenderers.js +17 -0
  163. package/dist/scene.d.ts +1 -0
  164. package/dist/scene.js +1 -0
  165. package/dist/settings/appliedSettings.d.ts +5 -0
  166. package/dist/settings/appliedSettings.js +9 -0
  167. package/dist/settings/settingsController.d.ts +2 -0
  168. package/dist/settings/settingsController.js +22 -1
  169. package/dist/shellConstants.d.ts +4 -0
  170. package/dist/shellConstants.js +4 -0
  171. package/dist/shellMovement.d.ts +8 -0
  172. package/dist/shellMovement.js +11 -0
  173. package/dist/shellPointer.d.ts +20 -0
  174. package/dist/shellPointer.js +28 -0
  175. package/dist/structures/PlacementGhost.d.ts +4 -0
  176. package/dist/structures/PlacementGhost.js +4 -0
  177. package/dist/structures/TransformGizmo.d.ts +43 -0
  178. package/dist/structures/TransformGizmo.js +55 -0
  179. package/dist/structures/index.d.ts +1 -0
  180. package/dist/structures/index.js +1 -0
  181. package/dist/terrain/CarvedTerrain.d.ts +4 -1
  182. package/dist/terrain/CarvedTerrain.js +4 -3
  183. package/dist/terrain/GrassField.js +1 -1
  184. package/dist/terrain/TerraformBrushCursor.js +3 -0
  185. package/dist/terrain/grassBudget.d.ts +11 -2
  186. package/dist/terrain/grassBudget.js +20 -4
  187. package/dist/terrain/grassGeometry.d.ts +3 -0
  188. package/dist/terrain/grassGeometry.js +3 -0
  189. package/dist/terrain/grassMaterial.d.ts +2 -0
  190. package/dist/terrain/grassMaterial.js +2 -0
  191. package/dist/terrain/random.d.ts +3 -0
  192. package/dist/terrain/random.js +3 -0
  193. package/dist/terrain/soilPatchMaterial.d.ts +10 -0
  194. package/dist/terrain/soilPatchMaterial.js +109 -0
  195. package/dist/terrain/terrainDetailMaterial.d.ts +31 -0
  196. package/dist/terrain/terrainDetailMaterial.js +157 -0
  197. package/dist/terrain/terrainMath.d.ts +8 -1
  198. package/dist/terrain/terrainMath.js +8 -1
  199. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  200. package/dist/touch/TouchControlsOverlay.js +211 -22
  201. package/dist/useShellMultiplayerSync.d.ts +9 -0
  202. package/dist/useShellMultiplayerSync.js +98 -0
  203. package/dist/visibility/CullingProvider.js +1 -1
  204. package/dist/water/OceanConfig.d.ts +2 -0
  205. package/dist/water/OceanConfig.js +2 -0
  206. package/dist/weather/fireSpreadPose.d.ts +1 -0
  207. package/dist/weather/fireSpreadPose.js +1 -0
  208. package/dist/weather/weatherGeometry.d.ts +1 -0
  209. package/dist/weather/weatherGeometry.js +1 -0
  210. package/dist/weather/weatherMath.d.ts +3 -0
  211. package/dist/weather/weatherMath.js +3 -0
  212. package/dist/weather/weatherUniforms.d.ts +3 -0
  213. package/dist/weather/weatherUniforms.js +3 -0
  214. package/dist/world/RetainedVfx.d.ts +10 -0
  215. package/dist/world/RetainedVfx.js +136 -0
  216. package/dist/world/WorldHud.d.ts +52 -3
  217. package/dist/world/WorldHud.js +94 -9
  218. package/dist/world/WorldScene.d.ts +21 -0
  219. package/dist/world/WorldScene.js +159 -0
  220. package/dist/world/WorldVfx.d.ts +9 -0
  221. package/dist/world/WorldVfx.js +184 -0
  222. package/dist/world/entityPose.d.ts +2 -0
  223. package/dist/world/entityPose.js +2 -0
  224. package/dist/world/floatTextStyle.d.ts +1 -0
  225. package/dist/world/floatTextStyle.js +1 -0
  226. package/dist/world/telegraphPulse.d.ts +1 -0
  227. package/dist/world/telegraphPulse.js +1 -0
  228. package/dist/world/worldBarSamples.d.ts +26 -1
  229. package/dist/world/worldBarSamples.js +45 -1
  230. package/dist/worldSky.d.ts +5 -0
  231. package/dist/worldSky.js +11 -0
  232. package/dist/worldSync.d.ts +11 -0
  233. package/dist/worldSync.js +20 -0
  234. package/package.json +6 -6
  235. package/skills/jgengine/SKILL.md +51 -0
  236. package/skills/jgengine/api.md +434 -0
  237. package/skills/jgengine/capabilities.md +29 -0
  238. package/skills/jgengine/reference-cartridge.md +72 -0
  239. package/skills/jgengine-ui/SKILL.md +36 -0
  240. package/skills/jgengine-ui/api.md +1852 -0
  241. package/skills/jgengine-ui/capabilities.md +142 -0
  242. package/skills/jgengine-ui/reference.md +500 -0
  243. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  244. package/skills/jgengine-world/SKILL.md +38 -0
  245. package/skills/jgengine-world/api.md +2652 -0
  246. package/skills/jgengine-world/capabilities.md +370 -0
  247. package/skills/jgengine-world/reference.md +222 -0
  248. package/llms.txt +0 -2643
@@ -37,7 +37,6 @@ export const defaultUiScenario = (ctx, playable) => {
37
37
  export function GameUiPreview({ playable, scenario = defaultUiScenario, }) {
38
38
  const [ctx, setCtx] = useState(null);
39
39
  useEffect(() => {
40
- playable.game.scene.clear();
41
40
  const context = createGameContext({
42
41
  definition: playable.game,
43
42
  content: playable.content,
@@ -53,7 +52,6 @@ export function GameUiPreview({ playable, scenario = defaultUiScenario, }) {
53
52
  }
54
53
  setCtx(context);
55
54
  return () => {
56
- playable.game.scene.clear();
57
55
  setCtx(null);
58
56
  };
59
57
  }, [playable, scenario]);
@@ -1,4 +1,6 @@
1
1
  import { type AudioBusDef, type SoundDef } from "@jgengine/core/audio/audioFalloff";
2
+ import type { MusicTheme } from "@jgengine/core/audio/music";
3
+ import { type CrossfadeOptions } from "./musicDirector.js";
2
4
  export interface Vec3 {
3
5
  x: number;
4
6
  y: number;
@@ -7,6 +9,10 @@ export interface Vec3 {
7
9
  export interface AudioSceneConfig {
8
10
  sounds?: Record<string, SoundDef>;
9
11
  buses?: Record<string, AudioBusDef>;
12
+ /** Procedural music themes, crossfaded by {@link AudioEngine.playMusic}. Mixed through the `musicBus` (default "music") so the settings volume applies. */
13
+ music?: Record<string, MusicTheme>;
14
+ /** Bus id the procedural music director mixes through. Default "music". */
15
+ musicBus?: string;
10
16
  }
11
17
  export interface AudioEmitterHandle {
12
18
  setPosition(position: Vec3): void;
@@ -16,6 +22,8 @@ export interface AudioEngine {
16
22
  setListenerPose(position: Vec3): void;
17
23
  playOneShot(soundId: string, position?: Vec3): void;
18
24
  playLoop(soundId: string, position?: Vec3): AudioEmitterHandle | null;
25
+ /** Crossfade the procedural soundtrack to `themeId` (null fades out). No-op when no `music` catalog is configured. */
26
+ playMusic(themeId: string | null, options?: CrossfadeOptions): void;
19
27
  setBusGain(busId: string, gain: number): void;
20
28
  setMasterGain(gain: number): void;
21
29
  resume(): void;
@@ -1,9 +1,13 @@
1
1
  import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
2
+ import { createDisposer } from "@jgengine/core/game/defineGame";
3
+ import { MusicDirector } from "./musicDirector.js";
4
+ import { createNoiseBuffer, realizeSynthPatch } from "./synthEngine.js";
2
5
  function createNoopEngine() {
3
6
  return {
4
7
  setListenerPose: () => undefined,
5
8
  playOneShot: () => undefined,
6
9
  playLoop: () => null,
10
+ playMusic: () => undefined,
7
11
  setBusGain: () => undefined,
8
12
  setMasterGain: () => undefined,
9
13
  resume: () => undefined,
@@ -43,6 +47,21 @@ export function createAudioEngine(config = {}) {
43
47
  }
44
48
  return node;
45
49
  }
50
+ let noiseBuffer = null;
51
+ function sharedNoiseBuffer() {
52
+ if (noiseBuffer === null)
53
+ noiseBuffer = createNoiseBuffer(context);
54
+ return noiseBuffer;
55
+ }
56
+ let director = null;
57
+ function musicDirector() {
58
+ if (config.music === undefined)
59
+ return null;
60
+ if (director === null) {
61
+ director = new MusicDirector(context, busGainNode(config.musicBus ?? "music"), config.music);
62
+ }
63
+ return director;
64
+ }
46
65
  const bufferCache = new Map();
47
66
  function loadBuffer(url) {
48
67
  let pending = bufferCache.get(url);
@@ -56,48 +75,82 @@ export function createAudioEngine(config = {}) {
56
75
  return pending;
57
76
  }
58
77
  let listenerPosition = { x: 0, y: 0, z: 0 };
78
+ const activeSpatialUpdaters = new Set();
59
79
  function playInternal(soundId, position, loop) {
60
80
  const sound = sounds[soundId];
61
81
  if (sound === undefined)
62
82
  return null;
63
83
  const bus = busGainNode(sound.bus);
64
84
  let currentPosition = position ?? listenerPosition;
65
- let gainNode = null;
66
- let stopped = false;
85
+ if (sound.synth !== undefined) {
86
+ const cueGain = context.createGain();
87
+ cueGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
88
+ cueGain.connect(bus);
89
+ realizeSynthPatch(context, cueGain, sharedNoiseBuffer(), sound.synth);
90
+ return { setPosition: () => undefined, stop: () => undefined };
91
+ }
92
+ if (sound.url === undefined)
93
+ return null;
94
+ const disposer = createDisposer();
95
+ let liveGain = null;
96
+ function updateGain() {
97
+ if (liveGain !== null) {
98
+ liveGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
99
+ }
100
+ }
67
101
  void loadBuffer(sound.url).then((buffer) => {
68
- if (stopped || buffer === null)
102
+ if (buffer === null)
69
103
  return;
70
104
  const sourceNode = context.createBufferSource();
71
105
  sourceNode.buffer = buffer;
72
106
  sourceNode.loop = loop || (sound.loop ?? false);
73
- gainNode = context.createGain();
107
+ const gainNode = context.createGain();
74
108
  gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
75
109
  sourceNode.connect(gainNode);
76
110
  gainNode.connect(bus);
77
111
  sourceNode.start();
112
+ liveGain = gainNode;
113
+ disposer.onDispose(() => {
114
+ try {
115
+ sourceNode.stop();
116
+ }
117
+ catch {
118
+ }
119
+ sourceNode.disconnect();
120
+ gainNode.disconnect();
121
+ liveGain = null;
122
+ });
78
123
  });
124
+ if (loop)
125
+ activeSpatialUpdaters.add(updateGain);
79
126
  return {
80
127
  setPosition(next) {
81
128
  currentPosition = next;
82
- if (gainNode !== null) {
83
- gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
84
- }
129
+ updateGain();
85
130
  },
86
131
  stop() {
87
- stopped = true;
132
+ activeSpatialUpdaters.delete(updateGain);
133
+ disposer.dispose();
88
134
  },
89
135
  };
90
136
  }
91
137
  return {
92
138
  setListenerPose(position) {
93
139
  listenerPosition = position;
140
+ for (const updateGain of activeSpatialUpdaters)
141
+ updateGain();
94
142
  },
95
143
  playOneShot(soundId, position) {
144
+ void context.resume().catch(() => undefined);
96
145
  playInternal(soundId, position, false);
97
146
  },
98
147
  playLoop(soundId, position) {
99
148
  return playInternal(soundId, position, true);
100
149
  },
150
+ playMusic(themeId, options) {
151
+ void context.resume().catch(() => undefined);
152
+ musicDirector()?.crossfadeTo(themeId, options);
153
+ },
101
154
  setBusGain(busId, gain) {
102
155
  busGainNode(busId).gain.value = gain;
103
156
  },
@@ -108,6 +161,8 @@ export function createAudioEngine(config = {}) {
108
161
  void context.resume().catch(() => undefined);
109
162
  },
110
163
  dispose() {
164
+ activeSpatialUpdaters.clear();
165
+ director?.dispose();
111
166
  void context.close().catch(() => undefined);
112
167
  },
113
168
  };
@@ -0,0 +1,22 @@
1
+ import type { AudioEngine } from "./audioEngine.js";
2
+ type AudioPlayPayload = {
3
+ sound: string;
4
+ at?: readonly [number, number, number];
5
+ };
6
+ type AudioMusicPayload = {
7
+ theme: string | null;
8
+ transpose?: number;
9
+ };
10
+ /** Minimal event bus the shell wires to the audio engine. @internal */
11
+ export type AudioEventBus = {
12
+ on(event: "audio.play", handler: (payload: AudioPlayPayload) => void): () => void;
13
+ on(event: "audio.music", handler: (payload: AudioMusicPayload) => void): () => void;
14
+ on(event: "audio.resume", handler: () => void): () => void;
15
+ };
16
+ /**
17
+ * Subscribe game audio events to the shell audio engine.
18
+ * Returns a single unsubscribe that tears all three listeners down.
19
+ * @internal
20
+ */
21
+ export declare function attachAudioEventWire(events: AudioEventBus, audioEngine: Pick<AudioEngine, "playOneShot" | "playMusic" | "resume">): () => void;
22
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Subscribe game audio events to the shell audio engine.
3
+ * Returns a single unsubscribe that tears all three listeners down.
4
+ * @internal
5
+ */
6
+ export function attachAudioEventWire(events, audioEngine) {
7
+ const offPlay = events.on("audio.play", ({ sound, at }) => {
8
+ audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
9
+ });
10
+ const offMusic = events.on("audio.music", ({ theme, transpose }) => {
11
+ audioEngine.playMusic(theme, transpose === undefined ? undefined : { transpose });
12
+ });
13
+ const offResume = events.on("audio.resume", () => audioEngine.resume());
14
+ return () => {
15
+ offPlay();
16
+ offMusic();
17
+ offResume();
18
+ };
19
+ }
@@ -0,0 +1,31 @@
1
+ import { type MusicTheme } from "@jgengine/core/audio/music";
2
+ /** Options for a music crossfade. */
3
+ export interface CrossfadeOptions {
4
+ /** Semitone shift applied to every note of the incoming theme (per-zone key). Default 0. */
5
+ transpose?: number;
6
+ }
7
+ /**
8
+ * Runs a set of looping {@link MusicTheme}s as crossfadeable layers on one shared
9
+ * Web Audio graph: master → compressor → destination, with a fixed convolution
10
+ * reverb send. `crossfadeTo` swaps the audible theme; a 110ms lookahead scheduler
11
+ * keeps each active layer's notes queued ahead of the clock so loops are seamless.
12
+ */
13
+ export declare class MusicDirector {
14
+ private readonly ctx;
15
+ private readonly master;
16
+ private readonly reverbSend;
17
+ private readonly layers;
18
+ private timer;
19
+ private volume;
20
+ private current;
21
+ constructor(ctx: BaseAudioContext, destination: AudioNode, themes: Record<string, MusicTheme>);
22
+ /** The currently faded-in theme id, or null when music is stopped. */
23
+ currentTheme(): string | null;
24
+ /** Fade to `themeId` (or fade all out when null). No-op if it is already current. */
25
+ crossfadeTo(themeId: string | null, options?: CrossfadeOptions): void;
26
+ /** Music volume 0..1, scaling the master gain. */
27
+ setVolume(volume: number): void;
28
+ /** Stop the scheduler and release the graph. */
29
+ dispose(): void;
30
+ private tick;
31
+ }
@@ -0,0 +1,125 @@
1
+ import { notesInWindow, themeLoopSeconds } from "@jgengine/core/audio/music";
2
+ import { playMusicNote } from "./musicVoices.js";
3
+ const LOOKAHEAD = 0.6;
4
+ const TICK_MS = 110;
5
+ const FADE_IN = 0.75;
6
+ const FADE_OUT = 0.35;
7
+ const MASTER_BASE = 0.2;
8
+ /**
9
+ * Runs a set of looping {@link MusicTheme}s as crossfadeable layers on one shared
10
+ * Web Audio graph: master → compressor → destination, with a fixed convolution
11
+ * reverb send. `crossfadeTo` swaps the audible theme; a 110ms lookahead scheduler
12
+ * keeps each active layer's notes queued ahead of the clock so loops are seamless.
13
+ */
14
+ export class MusicDirector {
15
+ ctx;
16
+ master;
17
+ reverbSend;
18
+ layers = new Map();
19
+ timer;
20
+ volume = 1;
21
+ current = null;
22
+ constructor(ctx, destination, themes) {
23
+ this.ctx = ctx;
24
+ this.master = ctx.createGain();
25
+ this.master.gain.value = MASTER_BASE;
26
+ const compressor = ctx.createDynamicsCompressor();
27
+ compressor.threshold.value = -18;
28
+ compressor.knee.value = 18;
29
+ compressor.ratio.value = 2.2;
30
+ compressor.attack.value = 0.015;
31
+ compressor.release.value = 0.25;
32
+ this.master.connect(compressor);
33
+ compressor.connect(destination);
34
+ const irSeconds = 2.6;
35
+ const len = Math.floor(ctx.sampleRate * irSeconds);
36
+ const ir = ctx.createBuffer(2, len, ctx.sampleRate);
37
+ for (let ch = 0; ch < 2; ch += 1) {
38
+ const data = ir.getChannelData(ch);
39
+ for (let i = 0; i < len; i += 1)
40
+ data[i] = (Math.random() * 2 - 1) * (1 - i / len) ** 2.4;
41
+ }
42
+ const reverb = ctx.createConvolver();
43
+ reverb.buffer = ir;
44
+ this.reverbSend = ctx.createGain();
45
+ this.reverbSend.gain.value = 0.55;
46
+ this.reverbSend.connect(reverb);
47
+ reverb.connect(this.master);
48
+ for (const [id, theme] of Object.entries(themes)) {
49
+ const gain = ctx.createGain();
50
+ gain.gain.value = 0;
51
+ gain.connect(this.master);
52
+ gain.connect(this.reverbSend);
53
+ this.layers.set(id, {
54
+ theme,
55
+ gain,
56
+ trim: theme.trim ?? 1,
57
+ active: false,
58
+ target: 0,
59
+ anchor: 0,
60
+ nextFrom: 0,
61
+ transpose: 0,
62
+ });
63
+ }
64
+ if (typeof setInterval === "function") {
65
+ this.timer = setInterval(() => this.tick(), TICK_MS);
66
+ }
67
+ }
68
+ /** The currently faded-in theme id, or null when music is stopped. */
69
+ currentTheme() {
70
+ return this.current;
71
+ }
72
+ /** Fade to `themeId` (or fade all out when null). No-op if it is already current. */
73
+ crossfadeTo(themeId, options = {}) {
74
+ if (themeId === this.current)
75
+ return;
76
+ this.current = themeId;
77
+ const now = this.ctx.currentTime;
78
+ for (const [id, layer] of this.layers) {
79
+ const target = id === themeId ? layer.trim : 0;
80
+ if (layer.target === target)
81
+ continue;
82
+ const rising = target > layer.target;
83
+ layer.target = target;
84
+ layer.gain.gain.setTargetAtTime(target, now, rising ? FADE_IN : FADE_OUT);
85
+ if (rising) {
86
+ layer.active = true;
87
+ layer.anchor = now + 0.15;
88
+ layer.nextFrom = layer.anchor - 1e-6;
89
+ layer.transpose = options.transpose ?? 0;
90
+ }
91
+ else {
92
+ layer.active = false;
93
+ }
94
+ }
95
+ }
96
+ /** Music volume 0..1, scaling the master gain. */
97
+ setVolume(volume) {
98
+ this.volume = Math.max(0, volume);
99
+ this.master.gain.value = MASTER_BASE * this.volume;
100
+ }
101
+ /** Stop the scheduler and release the graph. */
102
+ dispose() {
103
+ if (this.timer !== undefined)
104
+ clearInterval(this.timer);
105
+ this.timer = undefined;
106
+ this.master.disconnect();
107
+ this.reverbSend.disconnect();
108
+ }
109
+ tick() {
110
+ const horizon = this.ctx.currentTime + LOOKAHEAD;
111
+ const spbAvailable = 60;
112
+ for (const layer of this.layers.values()) {
113
+ if (!layer.active)
114
+ continue;
115
+ if (themeLoopSeconds(layer.theme) <= 0)
116
+ continue;
117
+ const spb = spbAvailable / layer.theme.bpm;
118
+ const notes = notesInWindow(layer.theme, layer.anchor, layer.nextFrom, horizon);
119
+ for (const { event, when } of notes) {
120
+ playMusicNote(this.ctx, event, when, spb, layer.gain, layer.transpose);
121
+ }
122
+ layer.nextFrom = horizon;
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,10 @@
1
+ import { type NoteEvent } from "@jgengine/core/audio/music";
2
+ type Ctx = BaseAudioContext;
3
+ /**
4
+ * Play one theme note through its instrument voice into `out` (a per-layer gain
5
+ * node the director wires to master + reverb). `spb` is seconds-per-beat and
6
+ * `transpose` shifts the note in semitones for per-zone key changes. This is the
7
+ * engine's reusable instrument library; unknown instruments fall back to a sine.
8
+ */
9
+ export declare function playMusicNote(ctx: Ctx, event: NoteEvent, when: number, spb: number, out: GainNode, transpose?: number): void;
10
+ export {};