@jgengine/shell 0.10.0 → 0.12.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 (286) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/CombatPresentation.d.ts +5 -0
  5. package/dist/CombatPresentation.js +8 -0
  6. package/dist/GameHost.d.ts +25 -1
  7. package/dist/GameHost.js +60 -4
  8. package/dist/GamePhaseStamp.d.ts +1 -0
  9. package/dist/GamePhaseStamp.js +9 -0
  10. package/dist/GamePlayer.d.ts +5 -0
  11. package/dist/GamePlayer.js +5 -0
  12. package/dist/GamePlayerShell.d.ts +5 -12
  13. package/dist/GamePlayerShell.js +67 -1332
  14. package/dist/GameUiPreview.js +3 -2
  15. package/dist/Shell3dPresentation.d.ts +63 -0
  16. package/dist/Shell3dPresentation.js +302 -0
  17. package/dist/ShellChrome.d.ts +51 -0
  18. package/dist/ShellChrome.js +51 -0
  19. package/dist/ShellHudPresentation.d.ts +41 -0
  20. package/dist/ShellHudPresentation.js +22 -0
  21. package/dist/audio/audioEngine.d.ts +4 -0
  22. package/dist/audio/audioEngine.js +128 -21
  23. package/dist/audio/audioWire.d.ts +40 -0
  24. package/dist/audio/audioWire.js +62 -0
  25. package/dist/audio/loopParams.d.ts +20 -0
  26. package/dist/audio/loopParams.js +28 -0
  27. package/dist/behaviour.d.ts +2 -1
  28. package/dist/behaviour.js +2 -1
  29. package/dist/behaviourAttach.d.ts +1 -0
  30. package/dist/behaviourAttach.js +1 -0
  31. package/dist/behaviourDriver.d.ts +1 -0
  32. package/dist/behaviourDriver.js +1 -0
  33. package/dist/boundActionDispatch.d.ts +13 -0
  34. package/dist/boundActionDispatch.js +37 -0
  35. package/dist/camera/GameCameraRig.d.ts +5 -2
  36. package/dist/camera/GameCameraRig.js +8 -3
  37. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  38. package/dist/camera/GameFirstPersonCamera.js +8 -3
  39. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  40. package/dist/camera/GameInspectionCamera.js +2 -2
  41. package/dist/camera/GameOrbitCamera.js +24 -9
  42. package/dist/camera/cameraBlendMath.d.ts +3 -0
  43. package/dist/camera/cameraBlendMath.js +3 -0
  44. package/dist/camera/cameraRigs.d.ts +3 -1
  45. package/dist/camera/cameraRigs.js +21 -3
  46. package/dist/camera/fovPreference.d.ts +6 -1
  47. package/dist/camera/fovPreference.js +6 -1
  48. package/dist/camera/index.d.ts +4 -7
  49. package/dist/camera/index.js +4 -7
  50. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  51. package/dist/camera/inspectionCameraMath.js +7 -2
  52. package/dist/camera/orbitCameraMath.d.ts +36 -6
  53. package/dist/camera/orbitCameraMath.js +56 -6
  54. package/dist/camera/rigMath.d.ts +114 -24
  55. package/dist/camera/rigMath.js +129 -24
  56. package/dist/camera/rigResolve.d.ts +4 -2
  57. package/dist/camera/rigResolve.js +4 -2
  58. package/dist/camera/shakeChannel.d.ts +6 -2
  59. package/dist/camera/shakeChannel.js +6 -2
  60. package/dist/camera/shakeChannelMath.d.ts +1 -0
  61. package/dist/camera/shakeChannelMath.js +1 -0
  62. package/dist/commandSink.d.ts +8 -3
  63. package/dist/commandSink.js +8 -3
  64. package/dist/defineGame.d.ts +15 -1
  65. package/dist/defineGame.js +50 -9
  66. package/dist/devtools/ColPanel.d.ts +1 -0
  67. package/dist/devtools/ColPanel.js +14 -0
  68. package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
  69. package/dist/devtools/DevtoolsOverlay.js +47 -462
  70. package/dist/devtools/KeysPanel.d.ts +4 -0
  71. package/dist/devtools/KeysPanel.js +31 -0
  72. package/dist/devtools/LogsPanel.d.ts +1 -0
  73. package/dist/devtools/LogsPanel.js +12 -0
  74. package/dist/devtools/NetPanel.d.ts +4 -0
  75. package/dist/devtools/NetPanel.js +10 -0
  76. package/dist/devtools/PerfPanel.d.ts +4 -0
  77. package/dist/devtools/PerfPanel.js +53 -0
  78. package/dist/devtools/TunePanel.d.ts +3 -0
  79. package/dist/devtools/TunePanel.js +309 -0
  80. package/dist/devtools/agentBridge.d.ts +41 -0
  81. package/dist/devtools/agentBridge.js +266 -0
  82. package/dist/devtools/collisionDebug.d.ts +6 -0
  83. package/dist/devtools/collisionDebug.js +6 -0
  84. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  85. package/dist/devtools/collisionDebugMath.js +24 -5
  86. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  87. package/dist/devtools/devtoolsOverrides.js +48 -0
  88. package/dist/devtools/panelAtoms.d.ts +9 -0
  89. package/dist/devtools/panelAtoms.js +10 -0
  90. package/dist/devtools/perfDiagnose.d.ts +2 -0
  91. package/dist/devtools/perfDiagnose.js +43 -0
  92. package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
  93. package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
  94. package/dist/drivers/FrameDriver.d.ts +37 -0
  95. package/dist/drivers/FrameDriver.js +232 -0
  96. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  97. package/dist/drivers/HudOnlyDriver.js +60 -0
  98. package/dist/environment/Daylight.d.ts +23 -7
  99. package/dist/environment/Daylight.js +106 -13
  100. package/dist/environment/EnvironmentScene.js +44 -9
  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/environment/terrainGroundColor.d.ts +9 -0
  112. package/dist/environment/terrainGroundColor.js +22 -0
  113. package/dist/gameKit.d.ts +27 -0
  114. package/dist/gameKit.js +29 -0
  115. package/dist/hotbarActions.d.ts +26 -0
  116. package/dist/hotbarActions.js +27 -0
  117. package/dist/inputSink.d.ts +16 -4
  118. package/dist/inputSink.js +57 -7
  119. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  120. package/dist/materialOverride.d.ts +10 -0
  121. package/dist/materialOverride.js +15 -3
  122. package/dist/multiplayer.js +8 -2
  123. package/dist/postfx/PostProcessing.d.ts +9 -1
  124. package/dist/postfx/PostProcessing.js +56 -11
  125. package/dist/postfx/gradeShader.js +9 -1
  126. package/dist/presentationResolve.d.ts +35 -0
  127. package/dist/presentationResolve.js +51 -0
  128. package/dist/registry.d.ts +4 -2
  129. package/dist/render/EnvironmentLighting.d.ts +10 -0
  130. package/dist/render/EnvironmentLighting.js +34 -0
  131. package/dist/render/SceneLighting.d.ts +7 -0
  132. package/dist/render/SceneLighting.js +18 -0
  133. package/dist/render/SceneModels.d.ts +14 -0
  134. package/dist/render/SceneModels.js +337 -0
  135. package/dist/render/assetBase.d.ts +17 -0
  136. package/dist/render/assetBase.js +35 -0
  137. package/dist/render/defaultSurface.d.ts +29 -0
  138. package/dist/render/defaultSurface.js +101 -0
  139. package/dist/render/modelLoad.d.ts +24 -0
  140. package/dist/render/modelLoad.js +76 -0
  141. package/dist/render/modelRender.d.ts +11 -0
  142. package/dist/render/modelRender.js +15 -0
  143. package/dist/render/resolveModel.d.ts +54 -3
  144. package/dist/render/resolveModel.js +116 -4
  145. package/dist/render/useEntityRenderCues.d.ts +13 -0
  146. package/dist/render/useEntityRenderCues.js +62 -0
  147. package/dist/scatter/InstancedScatter.d.ts +24 -0
  148. package/dist/scatter/InstancedScatter.js +135 -0
  149. package/dist/scatter/scatterModels.d.ts +25 -0
  150. package/dist/scatter/scatterModels.js +61 -0
  151. package/dist/scatter/scatterProxies.d.ts +6 -0
  152. package/dist/scatter/scatterProxies.js +118 -0
  153. package/dist/scatter.d.ts +1 -0
  154. package/dist/scatter.js +1 -0
  155. package/dist/scene/AuthoredScene.d.ts +84 -0
  156. package/dist/scene/AuthoredScene.js +178 -0
  157. package/dist/scene/CityRenderer.d.ts +2 -0
  158. package/dist/scene/CityRenderer.js +1031 -0
  159. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  160. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  161. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  162. package/dist/scene/GrassFieldRenderer.js +73 -0
  163. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  164. package/dist/scene/PoleLineRenderer.js +287 -0
  165. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  166. package/dist/scene/SoilPatchRenderer.js +84 -0
  167. package/dist/scene/StudioStage.d.ts +37 -0
  168. package/dist/scene/StudioStage.js +42 -0
  169. package/dist/scene/WaterRenderer.d.ts +2 -0
  170. package/dist/scene/WaterRenderer.js +61 -0
  171. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  172. package/dist/scene/builtinSceneKindRenderers.js +26 -0
  173. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  174. package/dist/scene/cityWindowMaterial.js +81 -0
  175. package/dist/scene/sceneKindRenderers.d.ts +30 -0
  176. package/dist/scene/sceneKindRenderers.js +17 -0
  177. package/dist/scene.d.ts +1 -0
  178. package/dist/scene.js +1 -0
  179. package/dist/settings/appliedSettings.d.ts +7 -1
  180. package/dist/settings/appliedSettings.js +13 -2
  181. package/dist/settings/settingsController.d.ts +2 -0
  182. package/dist/settings/settingsController.js +22 -1
  183. package/dist/shellConstants.d.ts +4 -0
  184. package/dist/shellConstants.js +4 -0
  185. package/dist/shellMovement.d.ts +8 -0
  186. package/dist/shellMovement.js +11 -0
  187. package/dist/shellPointer.d.ts +20 -0
  188. package/dist/shellPointer.js +28 -0
  189. package/dist/structures/PlacementGhost.d.ts +4 -0
  190. package/dist/structures/PlacementGhost.js +4 -0
  191. package/dist/structures/TransformGizmo.d.ts +53 -0
  192. package/dist/structures/TransformGizmo.js +59 -0
  193. package/dist/structures/index.d.ts +1 -0
  194. package/dist/structures/index.js +1 -0
  195. package/dist/terrain/GrassField.d.ts +11 -3
  196. package/dist/terrain/GrassField.js +16 -6
  197. package/dist/terrain/TerraformBrushCursor.js +3 -0
  198. package/dist/terrain/grassBudget.d.ts +11 -2
  199. package/dist/terrain/grassBudget.js +20 -4
  200. package/dist/terrain/grassGeometry.d.ts +35 -0
  201. package/dist/terrain/grassGeometry.js +85 -20
  202. package/dist/terrain/grassMaterial.d.ts +20 -0
  203. package/dist/terrain/grassMaterial.js +68 -13
  204. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  205. package/dist/terrain/heightfieldGeometry.js +109 -0
  206. package/dist/terrain/index.d.ts +1 -0
  207. package/dist/terrain/index.js +1 -0
  208. package/dist/terrain/random.d.ts +3 -0
  209. package/dist/terrain/random.js +3 -0
  210. package/dist/terrain/soilPatchMaterial.d.ts +23 -0
  211. package/dist/terrain/soilPatchMaterial.js +147 -0
  212. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  213. package/dist/terrain/terrainDetailMaterial.js +87 -7
  214. package/dist/terrain/terrainMath.d.ts +8 -1
  215. package/dist/terrain/terrainMath.js +8 -1
  216. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  217. package/dist/touch/TouchControlsOverlay.js +201 -22
  218. package/dist/useShellMultiplayerSync.d.ts +9 -0
  219. package/dist/useShellMultiplayerSync.js +98 -0
  220. package/dist/visibility/CullingProvider.js +1 -1
  221. package/dist/water/Ocean.d.ts +7 -1
  222. package/dist/water/Ocean.js +16 -3
  223. package/dist/water/OceanConfig.d.ts +11 -0
  224. package/dist/water/OceanConfig.js +8 -0
  225. package/dist/water/OceanMaterial.d.ts +12 -0
  226. package/dist/water/OceanMaterial.js +9 -0
  227. package/dist/water/OceanShader.d.ts +2 -2
  228. package/dist/water/OceanShader.js +78 -10
  229. package/dist/weather/fireSpreadPose.d.ts +1 -0
  230. package/dist/weather/fireSpreadPose.js +1 -0
  231. package/dist/weather/weatherGeometry.d.ts +1 -0
  232. package/dist/weather/weatherGeometry.js +1 -0
  233. package/dist/weather/weatherMath.d.ts +3 -0
  234. package/dist/weather/weatherMath.js +3 -0
  235. package/dist/weather/weatherUniforms.d.ts +3 -0
  236. package/dist/weather/weatherUniforms.js +3 -0
  237. package/dist/world/PlaceScene.d.ts +11 -0
  238. package/dist/world/PlaceScene.js +27 -0
  239. package/dist/world/RetainedVfx.d.ts +10 -0
  240. package/dist/world/RetainedVfx.js +136 -0
  241. package/dist/world/WorldHud.d.ts +49 -2
  242. package/dist/world/WorldHud.js +87 -2
  243. package/dist/world/WorldScene.d.ts +21 -0
  244. package/dist/world/WorldScene.js +186 -0
  245. package/dist/world/WorldVfx.d.ts +9 -0
  246. package/dist/world/WorldVfx.js +184 -0
  247. package/dist/world/entityPose.d.ts +2 -0
  248. package/dist/world/entityPose.js +2 -0
  249. package/dist/world/floatTextStyle.d.ts +1 -0
  250. package/dist/world/floatTextStyle.js +1 -0
  251. package/dist/world/telegraphPulse.d.ts +1 -0
  252. package/dist/world/telegraphPulse.js +1 -0
  253. package/dist/world/worldBarSamples.d.ts +25 -0
  254. package/dist/world/worldBarSamples.js +38 -0
  255. package/dist/worldSky.d.ts +11 -0
  256. package/dist/worldSky.js +21 -0
  257. package/dist/worldSync.d.ts +2 -1
  258. package/dist/worldSync.js +2 -1
  259. package/package.json +6 -6
  260. package/skills/game-design/SKILL.md +28 -0
  261. package/skills/game-design/agents/openai.yaml +4 -0
  262. package/skills/game-design/references/application-playbook.md +170 -0
  263. package/skills/game-design/references/field-guide.md +288 -0
  264. package/skills/jgengine/SKILL.md +54 -0
  265. package/skills/jgengine/api.md +527 -0
  266. package/skills/jgengine/capabilities.md +131 -0
  267. package/skills/jgengine/recipes/minimal-game.md +88 -0
  268. package/skills/jgengine-ui/SKILL.md +41 -0
  269. package/skills/jgengine-ui/api.md +2007 -0
  270. package/skills/jgengine-ui/capabilities.md +211 -0
  271. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  272. package/skills/jgengine-ui/reference.md +513 -0
  273. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  274. package/skills/jgengine-world/SKILL.md +51 -0
  275. package/skills/jgengine-world/api.md +3056 -0
  276. package/skills/jgengine-world/capabilities.md +476 -0
  277. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  278. package/skills/jgengine-world/reference.md +261 -0
  279. package/skills/level-design/SKILL.md +54 -0
  280. package/skills/level-design/agents/openai.yaml +4 -0
  281. package/skills/level-design/references/application-playbook.md +192 -0
  282. package/skills/level-design/references/field-guide.md +337 -0
  283. package/skills/level-design/references/live-editor-loop.md +179 -0
  284. package/dist/cartridge.d.ts +0 -145
  285. package/dist/cartridge.js +0 -246
  286. package/llms.txt +0 -2761
@@ -1,6 +1,11 @@
1
1
  import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
2
+ import { patchDuration } from "@jgengine/core/audio/synth";
3
+ import { createDisposer } from "@jgengine/core/game/defineGame";
4
+ import { clampLoopGain, clampLoopRate } from "./loopParams.js";
2
5
  import { MusicDirector } from "./musicDirector.js";
3
6
  import { createNoiseBuffer, realizeSynthPatch } from "./synthEngine.js";
7
+ /** setTargetAtTime time constant (~20 ms) for zipper-free live rate/gain ramps on retained loops (#1051). */
8
+ const LOOP_PARAM_SMOOTH_TC = 0.02;
4
9
  function createNoopEngine() {
5
10
  return {
6
11
  setListenerPose: () => undefined,
@@ -19,6 +24,12 @@ function resolveAudioContextCtor() {
19
24
  return (window.AudioContext ??
20
25
  window.webkitAudioContext);
21
26
  }
27
+ function resolveOfflineAudioContextCtor() {
28
+ if (typeof window === "undefined")
29
+ return undefined;
30
+ return (window.OfflineAudioContext ??
31
+ window.webkitOfflineAudioContext);
32
+ }
22
33
  export function createAudioEngine(config = {}) {
23
34
  const sounds = config.sounds ?? {};
24
35
  const busDefs = config.buses ?? {};
@@ -73,51 +84,144 @@ export function createAudioEngine(config = {}) {
73
84
  }
74
85
  return pending;
75
86
  }
87
+ // Retained synth loops render the procedural patch to one cached buffer, then loop it as a plain
88
+ // BufferSource — so live pitch is `playbackRate` and live gain is a gain node, exactly like sample loops (#1051).
89
+ const synthBufferCache = new Map();
90
+ function renderSynthBuffer(soundId, patch) {
91
+ let pending = synthBufferCache.get(soundId);
92
+ if (pending === undefined) {
93
+ const OfflineCtor = resolveOfflineAudioContextCtor();
94
+ if (OfflineCtor === undefined) {
95
+ pending = Promise.resolve(null);
96
+ }
97
+ else {
98
+ try {
99
+ const seconds = Math.max(patchDuration(patch), 0.05);
100
+ const frames = Math.max(1, Math.ceil(seconds * context.sampleRate));
101
+ const offline = new OfflineCtor(1, frames, context.sampleRate);
102
+ realizeSynthPatch(offline, offline.destination, createNoiseBuffer(offline), patch);
103
+ pending = offline.startRendering().catch(() => null);
104
+ }
105
+ catch {
106
+ pending = Promise.resolve(null);
107
+ }
108
+ }
109
+ synthBufferCache.set(soundId, pending);
110
+ }
111
+ return pending;
112
+ }
76
113
  let listenerPosition = { x: 0, y: 0, z: 0 };
114
+ const activeSpatialUpdaters = new Set();
115
+ const activeLoops = new Set();
116
+ const NOOP_HANDLE = {
117
+ setPosition: () => undefined,
118
+ setRate: () => undefined,
119
+ setGain: () => undefined,
120
+ stop: () => undefined,
121
+ };
77
122
  function playInternal(soundId, position, loop) {
78
123
  const sound = sounds[soundId];
79
124
  if (sound === undefined)
80
125
  return null;
81
126
  const bus = busGainNode(sound.bus);
82
- let currentPosition = position ?? listenerPosition;
83
- if (sound.synth !== undefined) {
127
+ const currentPosition = position ?? listenerPosition;
128
+ // A one-shot synth cue realises its decaying voices live and is fire-and-forget.
129
+ if (sound.synth !== undefined && !loop) {
84
130
  const cueGain = context.createGain();
85
131
  cueGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
86
132
  cueGain.connect(bus);
87
133
  realizeSynthPatch(context, cueGain, sharedNoiseBuffer(), sound.synth);
88
- return { setPosition: () => undefined, stop: () => undefined };
134
+ return NOOP_HANDLE;
89
135
  }
90
- if (sound.url === undefined)
136
+ // Everything else resolves to one AudioBuffer we loop/play: sample URL, or a synth patch rendered once.
137
+ let bufferPromise;
138
+ if (sound.synth !== undefined)
139
+ bufferPromise = renderSynthBuffer(soundId, sound.synth);
140
+ else if (sound.url !== undefined)
141
+ bufferPromise = loadBuffer(sound.url);
142
+ else
91
143
  return null;
92
- let gainNode = null;
93
- let stopped = false;
94
- void loadBuffer(sound.url).then((buffer) => {
95
- if (stopped || buffer === null)
144
+ return playBufferSource(sound, bus, bufferPromise, currentPosition, loop);
145
+ }
146
+ function playBufferSource(sound, bus, bufferPromise, initialPosition, loop) {
147
+ const disposer = createDisposer();
148
+ let currentPosition = initialPosition;
149
+ // Live control state, applied on source creation so updates that race the async buffer load stick.
150
+ let currentRate = 1;
151
+ let currentUserGain = 1;
152
+ let sourceNode = null;
153
+ let userGainNode = null;
154
+ let falloffGain = null;
155
+ function updateFalloff() {
156
+ if (falloffGain !== null) {
157
+ falloffGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
158
+ }
159
+ }
160
+ void bufferPromise.then((buffer) => {
161
+ if (buffer === null)
96
162
  return;
97
- const sourceNode = context.createBufferSource();
98
- sourceNode.buffer = buffer;
99
- sourceNode.loop = loop || (sound.loop ?? false);
100
- gainNode = context.createGain();
101
- gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
102
- sourceNode.connect(gainNode);
103
- gainNode.connect(bus);
104
- sourceNode.start();
163
+ const src = context.createBufferSource();
164
+ src.buffer = buffer;
165
+ src.loop = loop || (sound.loop ?? false);
166
+ src.playbackRate.value = currentRate;
167
+ // source → per-loop user gain (setGain) → distance falloff → bus
168
+ const uGain = context.createGain();
169
+ uGain.gain.value = currentUserGain;
170
+ const fGain = context.createGain();
171
+ fGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
172
+ src.connect(uGain);
173
+ uGain.connect(fGain);
174
+ fGain.connect(bus);
175
+ src.start();
176
+ sourceNode = src;
177
+ userGainNode = uGain;
178
+ falloffGain = fGain;
179
+ disposer.onDispose(() => {
180
+ try {
181
+ src.stop();
182
+ }
183
+ catch {
184
+ }
185
+ src.disconnect();
186
+ uGain.disconnect();
187
+ fGain.disconnect();
188
+ sourceNode = null;
189
+ userGainNode = null;
190
+ falloffGain = null;
191
+ });
105
192
  });
106
- return {
193
+ if (loop)
194
+ activeSpatialUpdaters.add(updateFalloff);
195
+ const handle = {
107
196
  setPosition(next) {
108
197
  currentPosition = next;
109
- if (gainNode !== null) {
110
- gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
111
- }
198
+ updateFalloff();
199
+ },
200
+ setRate(rate) {
201
+ currentRate = clampLoopRate(rate);
202
+ if (sourceNode !== null)
203
+ sourceNode.playbackRate.setTargetAtTime(currentRate, context.currentTime, LOOP_PARAM_SMOOTH_TC);
204
+ },
205
+ setGain(gain) {
206
+ currentUserGain = clampLoopGain(gain);
207
+ if (userGainNode !== null)
208
+ userGainNode.gain.setTargetAtTime(currentUserGain, context.currentTime, LOOP_PARAM_SMOOTH_TC);
112
209
  },
113
210
  stop() {
114
- stopped = true;
211
+ activeSpatialUpdaters.delete(updateFalloff);
212
+ activeLoops.delete(handle);
213
+ disposer.dispose();
115
214
  },
116
215
  };
216
+ if (loop)
217
+ activeLoops.add(handle);
218
+ return handle;
117
219
  }
118
220
  return {
119
221
  setListenerPose(position) {
120
222
  listenerPosition = position;
223
+ for (const updateGain of activeSpatialUpdaters)
224
+ updateGain();
121
225
  },
122
226
  playOneShot(soundId, position) {
123
227
  void context.resume().catch(() => undefined);
@@ -140,6 +244,9 @@ export function createAudioEngine(config = {}) {
140
244
  void context.resume().catch(() => undefined);
141
245
  },
142
246
  dispose() {
247
+ for (const handle of [...activeLoops])
248
+ handle.stop();
249
+ activeSpatialUpdaters.clear();
143
250
  director?.dispose();
144
251
  void context.close().catch(() => undefined);
145
252
  },
@@ -0,0 +1,40 @@
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
+ type AudioLoopStartPayload = {
11
+ id: string;
12
+ sound: string;
13
+ at?: readonly [number, number, number];
14
+ };
15
+ type AudioLoopSetPayload = {
16
+ id: string;
17
+ rate?: number;
18
+ gain?: number;
19
+ at?: readonly [number, number, number];
20
+ };
21
+ type AudioLoopStopPayload = {
22
+ id: string;
23
+ };
24
+ /** Minimal event bus the shell wires to the audio engine, including retained id-keyed loops (#1051). @internal */
25
+ export type AudioEventBus = {
26
+ on(event: "audio.play", handler: (payload: AudioPlayPayload) => void): () => void;
27
+ on(event: "audio.music", handler: (payload: AudioMusicPayload) => void): () => void;
28
+ on(event: "audio.resume", handler: () => void): () => void;
29
+ on(event: "audio.loopStart", handler: (payload: AudioLoopStartPayload) => void): () => void;
30
+ on(event: "audio.loopSet", handler: (payload: AudioLoopSetPayload) => void): () => void;
31
+ on(event: "audio.loopStop", handler: (payload: AudioLoopStopPayload) => void): () => void;
32
+ };
33
+ /**
34
+ * Subscribe game audio events to the shell audio engine. Manages the retained-loop registry
35
+ * (`audio.loopStart`/`audio.loopSet`/`audio.loopStop`), keyed by stable id. Returns a single
36
+ * unsubscribe that tears every listener down and stops any loops still playing.
37
+ * @internal
38
+ */
39
+ export declare function attachAudioEventWire(events: AudioEventBus, audioEngine: Pick<AudioEngine, "playOneShot" | "playLoop" | "playMusic" | "resume">): () => void;
40
+ export {};
@@ -0,0 +1,62 @@
1
+ function toVec(at) {
2
+ return at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] };
3
+ }
4
+ /**
5
+ * Subscribe game audio events to the shell audio engine. Manages the retained-loop registry
6
+ * (`audio.loopStart`/`audio.loopSet`/`audio.loopStop`), keyed by stable id. Returns a single
7
+ * unsubscribe that tears every listener down and stops any loops still playing.
8
+ * @internal
9
+ */
10
+ export function attachAudioEventWire(events, audioEngine) {
11
+ const loops = new Map();
12
+ const offPlay = events.on("audio.play", ({ sound, at }) => {
13
+ audioEngine.playOneShot(sound, toVec(at));
14
+ });
15
+ const offMusic = events.on("audio.music", ({ theme, transpose }) => {
16
+ audioEngine.playMusic(theme, transpose === undefined ? undefined : { transpose });
17
+ });
18
+ const offResume = events.on("audio.resume", () => audioEngine.resume());
19
+ const offLoopStart = events.on("audio.loopStart", ({ id, sound, at }) => {
20
+ const existing = loops.get(id);
21
+ if (existing !== undefined) {
22
+ // Same sound: idempotent — keep the running source so a per-tick re-issue never clicks.
23
+ if (existing.sound === sound)
24
+ return;
25
+ existing.handle.stop();
26
+ loops.delete(id);
27
+ }
28
+ const handle = audioEngine.playLoop(sound, toVec(at));
29
+ if (handle !== null)
30
+ loops.set(id, { sound, handle });
31
+ });
32
+ const offLoopSet = events.on("audio.loopSet", ({ id, rate, gain, at }) => {
33
+ const entry = loops.get(id);
34
+ // Unknown id: a live update may race a stop — silently drop it.
35
+ if (entry === undefined)
36
+ return;
37
+ if (rate !== undefined)
38
+ entry.handle.setRate(rate);
39
+ if (gain !== undefined)
40
+ entry.handle.setGain(gain);
41
+ if (at !== undefined)
42
+ entry.handle.setPosition({ x: at[0], y: at[1], z: at[2] });
43
+ });
44
+ const offLoopStop = events.on("audio.loopStop", ({ id }) => {
45
+ const entry = loops.get(id);
46
+ if (entry === undefined)
47
+ return;
48
+ entry.handle.stop();
49
+ loops.delete(id);
50
+ });
51
+ return () => {
52
+ offPlay();
53
+ offMusic();
54
+ offResume();
55
+ offLoopStart();
56
+ offLoopSet();
57
+ offLoopStop();
58
+ for (const { handle } of loops.values())
59
+ handle.stop();
60
+ loops.clear();
61
+ };
62
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure clamps for retained-loop live control (#1051). Kept dependency-free so the
3
+ * playback-rate/gain windows are one testable source of truth the audio engine reads.
4
+ */
5
+ /** Lowest live playback-rate multiplier (two octaves below authored pitch). */
6
+ export declare const MIN_LOOP_RATE = 0.25;
7
+ /** Highest live playback-rate multiplier (two octaves above authored pitch). */
8
+ export declare const MAX_LOOP_RATE = 4;
9
+ /**
10
+ * Clamp a loop playback-rate multiplier into the safe pitch window `MIN_LOOP_RATE`–`MAX_LOOP_RATE`
11
+ * (1 = authored pitch). Non-finite input (NaN/±Infinity) falls back to the authored `1`.
12
+ * @internal — the audio engine applies this; game code sets rate through `ctx.game.audio.setLoop`.
13
+ */
14
+ export declare function clampLoopRate(rate: number): number;
15
+ /**
16
+ * Clamp a loop gain multiplier into `0`–`1`. Non-finite input falls back to `0` (silence) so a
17
+ * stray NaN never blasts a loop to full volume.
18
+ * @internal — the audio engine applies this; game code sets gain through `ctx.game.audio.setLoop`.
19
+ */
20
+ export declare function clampLoopGain(gain: number): number;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Pure clamps for retained-loop live control (#1051). Kept dependency-free so the
3
+ * playback-rate/gain windows are one testable source of truth the audio engine reads.
4
+ */
5
+ /** Lowest live playback-rate multiplier (two octaves below authored pitch). */
6
+ export const MIN_LOOP_RATE = 0.25;
7
+ /** Highest live playback-rate multiplier (two octaves above authored pitch). */
8
+ export const MAX_LOOP_RATE = 4;
9
+ /**
10
+ * Clamp a loop playback-rate multiplier into the safe pitch window `MIN_LOOP_RATE`–`MAX_LOOP_RATE`
11
+ * (1 = authored pitch). Non-finite input (NaN/±Infinity) falls back to the authored `1`.
12
+ * @internal — the audio engine applies this; game code sets rate through `ctx.game.audio.setLoop`.
13
+ */
14
+ export function clampLoopRate(rate) {
15
+ if (!Number.isFinite(rate))
16
+ return 1;
17
+ return Math.min(MAX_LOOP_RATE, Math.max(MIN_LOOP_RATE, rate));
18
+ }
19
+ /**
20
+ * Clamp a loop gain multiplier into `0`–`1`. Non-finite input falls back to `0` (silence) so a
21
+ * stray NaN never blasts a loop to full volume.
22
+ * @internal — the audio engine applies this; game code sets gain through `ctx.game.audio.setLoop`.
23
+ */
24
+ export function clampLoopGain(gain) {
25
+ if (!Number.isFinite(gain))
26
+ return 0;
27
+ return Math.min(1, Math.max(0, gain));
28
+ }
@@ -8,5 +8,6 @@ export { attachObject3D, Object3DBehaviour } from "./behaviourAttach.js";
8
8
  * receive — pass the game clock's scaling to keep behaviours on game time, or
9
9
  * omit for real-time seconds. Games driving `world.update` themselves from
10
10
  * `loop.onTick` should not also render this.
11
- */
11
+ * @internal
12
+ */
12
13
  export declare function useBehaviourWorld(world: BehaviourWorld, scaleDt?: (rawDt: number) => number): void;
package/dist/behaviour.js CHANGED
@@ -10,7 +10,8 @@ export { attachObject3D, Object3DBehaviour } from "./behaviourAttach.js";
10
10
  * receive — pass the game clock's scaling to keep behaviours on game time, or
11
11
  * omit for real-time seconds. Games driving `world.update` themselves from
12
12
  * `loop.onTick` should not also render this.
13
- */
13
+ * @internal
14
+ */
14
15
  export function useBehaviourWorld(world, scaleDt) {
15
16
  const driverRef = useRef(createBehaviourWorldDriver(world));
16
17
  useEffect(() => {
@@ -1,5 +1,6 @@
1
1
  import type { Object3D } from "three";
2
2
  import { Behaviour, type BehaviourWorld } from "@jgengine/core/behaviour/behaviour";
3
+ /** @internal */
3
4
  export declare class Object3DBehaviour extends Behaviour {
4
5
  /** @internal */ _object: Object3D | null;
5
6
  get object(): Object3D;
@@ -1,4 +1,5 @@
1
1
  import { Behaviour } from "@jgengine/core/behaviour/behaviour";
2
+ /** @internal */
2
3
  export class Object3DBehaviour extends Behaviour {
3
4
  /** @internal */ _object = null;
4
5
  get object() {
@@ -1,4 +1,5 @@
1
1
  import type { BehaviourWorld } from "@jgengine/core/behaviour/behaviour";
2
+ /** @internal */
2
3
  export declare function createBehaviourWorldDriver(world: BehaviourWorld): {
3
4
  start(): void;
4
5
  stop(): void;
@@ -1,3 +1,4 @@
1
+ /** @internal */
1
2
  export function createBehaviourWorldDriver(world) {
2
3
  let running = false;
3
4
  return {
@@ -0,0 +1,13 @@
1
+ import { type ActionStateTracker } from "@jgengine/core/input/actionBindings";
2
+ import type { Aim } from "@jgengine/core/scene/spatial";
3
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
4
+ import type { ActionCodesMap } from "@jgengine/core/input/actionBindings";
5
+ import { type CommandSink } from "./commandSink.js";
6
+ /** Actions the shell consumes natively and never routes to a same-named command. @internal */
7
+ export declare const RESERVED_INPUT_ACTIONS: ReadonlySet<string>;
8
+ /** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. @internal */
9
+ export declare function heldActionsFor(tracker: Pick<ActionStateTracker<string>, "isDown">, actions: readonly string[]): string[];
10
+ /** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
11
+ export declare function shouldFireBoundAction(tracker: Pick<ActionStateTracker<string>, "isDown" | "wasPressed">, action: string, input: ActionCodesMap | undefined, repeatFiredAt: ReadonlyMap<string, number>, now: number): boolean;
12
+ /** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
13
+ export declare function dispatchBoundAction(ctx: GameContext, action: string, yaw: number, pitch: number, aim: Aim, reserved?: ReadonlySet<string>, sink?: CommandSink): void;
@@ -0,0 +1,37 @@
1
+ import { actionRepeatMs, resolveActionCommand, shouldDispatchAction, } from "@jgengine/core/input/actionBindings";
2
+ import { localCommandSink } from "./commandSink.js";
3
+ /** Actions the shell consumes natively and never routes to a same-named command. @internal */
4
+ export const RESERVED_INPUT_ACTIONS = new Set([
5
+ "moveForward",
6
+ "moveBack",
7
+ "moveLeft",
8
+ "moveRight",
9
+ "turnLeft",
10
+ "turnRight",
11
+ "sprint",
12
+ "jump",
13
+ "tabTarget",
14
+ "clearTarget",
15
+ "useAbility",
16
+ "interact",
17
+ ]);
18
+ /** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. @internal */
19
+ export function heldActionsFor(tracker, actions) {
20
+ return actions.filter((action) => tracker.isDown(action));
21
+ }
22
+ /** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
23
+ export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now) {
24
+ return shouldDispatchAction({
25
+ pressed: tracker.wasPressed(action),
26
+ down: tracker.isDown(action),
27
+ repeatMs: actionRepeatMs(input?.[action]),
28
+ lastFiredAt: repeatFiredAt.get(action) ?? null,
29
+ now,
30
+ });
31
+ }
32
+ /** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
33
+ export function dispatchBoundAction(ctx, action, yaw, pitch, aim, reserved = RESERVED_INPUT_ACTIONS, sink = localCommandSink(ctx)) {
34
+ const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), reserved);
35
+ if (command !== null)
36
+ sink.run(command, { yaw, pitch, aim });
37
+ }
@@ -1,6 +1,7 @@
1
- import { type MutableRefObject } from "react";
1
+ import { type ComponentType, type MutableRefObject } from "react";
2
2
  import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
3
3
  import type { CameraDirector } from "@jgengine/core/runtime/cameraDirector";
4
+ import { type ViewmodelProps } from "./GameFirstPersonCamera.js";
4
5
  export { resolveRigKind } from "./rigResolve.js";
5
6
  export interface GameCameraRigProps {
6
7
  yawRef: MutableRefObject<number>;
@@ -12,5 +13,7 @@ export interface GameCameraRigProps {
12
13
  panKeysEnabled?: boolean;
13
14
  /** Runtime follow/cinematic override (#196.2). While present it wins over the static `config` for any field it reports non-`undefined`/non-`null`; absent (or an all-`undefined` snapshot) is a no-op. */
14
15
  director?: CameraDirector;
16
+ /** Custom first-person viewmodel (#542); read only when the resolved rig is `"first"`. */
17
+ viewmodel?: ComponentType<ViewmodelProps>;
15
18
  }
16
- export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): import("react").JSX.Element;
19
+ export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, viewmodel, }: GameCameraRigProps): import("react").JSX.Element;
@@ -8,7 +8,7 @@ import { resolveDirectedCamera } from "./rigMath.js";
8
8
  import { resolveRigKind, turntableAsObserver } from "./rigResolve.js";
9
9
  import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js";
10
10
  export { resolveRigKind } from "./rigResolve.js";
11
- export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }) {
11
+ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, viewmodel, }) {
12
12
  const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
13
13
  const [, notifyDirectorChange] = useReducer((count) => count + 1, 0);
14
14
  useEffect(() => {
@@ -22,12 +22,17 @@ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerC
22
22
  const followEntityId = directed.followEntityId;
23
23
  const rig = (() => {
24
24
  if (directed.cinematic !== undefined) {
25
- return (_jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: { ...config, cinematic: directed.cinematic }, followEntityId: followEntityId, absoluteFov: true }));
25
+ // A director-set cinematic auto-clears when its keyframe path completes, so a
26
+ // one-shot flyover can never strand the camera if a game-side clear timer drifts
27
+ // (the rig plays in real time; sim-clock timers do not). Static config cinematics
28
+ // hold their final frame as before.
29
+ const owner = director !== undefined && director.cinematic() !== null ? director : undefined;
30
+ return (_jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: { ...config, cinematic: directed.cinematic }, followEntityId: followEntityId, absoluteFov: true, ...(owner === undefined ? {} : { onComplete: () => owner.setCinematic(null) }) }));
26
31
  }
27
32
  const kind = resolveRigKind(config);
28
33
  switch (kind) {
29
34
  case "first":
30
- return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined }));
35
+ return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined, viewmodel: viewmodel }));
31
36
  case "topDown":
32
37
  return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
33
38
  case "rts":
@@ -1,12 +1,19 @@
1
- import { type MutableRefObject } from "react";
1
+ import { type ComponentType, type MutableRefObject } from "react";
2
2
  import * as THREE from "three";
3
3
  import type { FirstPersonCameraConfig } from "@jgengine/core/game/playableGame";
4
+ import type { EntityRenderCues } from "@jgengine/core/combat/renderCues";
4
5
  /** World position of the first-person weapon muzzle, or false when no viewmodel is mounted. */
5
6
  export declare function readFirstPersonMuzzle(target: THREE.Vector3): boolean;
7
+ /** Props handed to a custom viewmodel component (#542): a live cue ref (velocity/bob/firing/reloading/recoil/hit) for the followed entity, driven from your own `useFrame` — read `cuesRef.current` there rather than storing it as render state. */
8
+ export interface ViewmodelProps {
9
+ cuesRef: MutableRefObject<EntityRenderCues>;
10
+ }
6
11
  export interface GameFirstPersonCameraProps {
7
12
  yawRef: MutableRefObject<number>;
8
13
  pitchRef: MutableRefObject<number>;
9
14
  config?: FirstPersonCameraConfig;
10
15
  followEntityId?: string;
16
+ /** Custom viewmodel component replacing the built-in three-mesh gun, rendered inside the same camera-locked, muzzle-tracked anchor. Ignored when `config.viewmodel === false`. */
17
+ viewmodel?: ComponentType<ViewmodelProps>;
11
18
  }
12
- export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
19
+ export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
@@ -5,6 +5,7 @@ import * as THREE from "three";
5
5
  import { DEFAULT_EYE_HEIGHT } from "@jgengine/core/combat/shotOrigin";
6
6
  import { useGameContext } from "@jgengine/react/provider";
7
7
  import { usePlayer } from "@jgengine/react/hooks";
8
+ import { useEntityRenderCues } from "../render/useEntityRenderCues.js";
8
9
  import { usePlayerFov } from "./PlayerFov.js";
9
10
  import { GAME_SIM_FRAME_PRIORITY, ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
10
11
  const DEFAULT_SENSITIVITY = 0.0025;
@@ -21,7 +22,7 @@ export function readFirstPersonMuzzle(target) {
21
22
  target.copy(muzzleWorld);
22
23
  return true;
23
24
  }
24
- export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }) {
25
+ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }) {
25
26
  const eyeHeight = config?.eyeHeight ?? DEFAULT_EYE_HEIGHT;
26
27
  const sensitivity = config?.sensitivity ?? DEFAULT_SENSITIVITY;
27
28
  const maxPitch = config?.maxPitch ?? DEFAULT_MAX_PITCH;
@@ -32,6 +33,7 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
32
33
  const domElement = useThree((state) => state.gl.domElement);
33
34
  const followId = followEntityId ?? userId;
34
35
  const seededRef = useRef(false);
36
+ const cuesRef = useEntityRenderCues(followId);
35
37
  useEffect(() => {
36
38
  const requestLock = () => {
37
39
  if (window.matchMedia?.("(pointer: coarse)").matches)
@@ -73,9 +75,9 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
73
75
  }, ORBIT_CAMERA_FRAME_PRIORITY);
74
76
  if (config?.viewmodel === false)
75
77
  return null;
76
- return _jsx(FirstPersonViewmodel, { camera: camera });
78
+ return _jsx(FirstPersonViewmodel, { camera: camera, viewmodel: viewmodel, cuesRef: cuesRef });
77
79
  }
78
- function FirstPersonViewmodel({ camera }) {
80
+ function FirstPersonViewmodel({ camera, viewmodel: Viewmodel, cuesRef, }) {
79
81
  const groupRef = useRef(null);
80
82
  useEffect(() => () => {
81
83
  muzzleTracked = false;
@@ -92,5 +94,8 @@ function FirstPersonViewmodel({ camera }) {
92
94
  muzzleWorld.copy(MUZZLE_OFFSET).applyQuaternion(camera.quaternion).add(camera.position);
93
95
  muzzleTracked = true;
94
96
  }, GAME_SIM_FRAME_PRIORITY);
97
+ if (Viewmodel !== undefined) {
98
+ return (_jsx("group", { ref: groupRef, children: _jsx(Viewmodel, { cuesRef: cuesRef }) }));
99
+ }
95
100
  return (_jsxs("group", { ref: groupRef, children: [_jsxs("mesh", { position: [0, 0, -0.22], children: [_jsx("boxGeometry", { args: [0.09, 0.11, 0.55] }), _jsx("meshStandardMaterial", { color: "#22262d", metalness: 0.6, roughness: 0.35 })] }), _jsxs("mesh", { position: [0, -0.13, 0.04], rotation: [0.35, 0, 0], children: [_jsx("boxGeometry", { args: [0.08, 0.18, 0.11] }), _jsx("meshStandardMaterial", { color: "#33373f", metalness: 0.4, roughness: 0.5 })] }), _jsxs("mesh", { position: [0, 0.03, -0.52], rotation: [Math.PI / 2, 0, 0], children: [_jsx("cylinderGeometry", { args: [0.022, 0.022, 0.18, 10] }), _jsx("meshStandardMaterial", { color: "#0e0f12", metalness: 0.7, roughness: 0.3 })] })] }));
96
101
  }
@@ -2,5 +2,5 @@ import { type InspectionCameraConfig } from "./inspectionCameraMath.js";
2
2
  export interface GameInspectionCameraProps {
3
3
  config?: InspectionCameraConfig;
4
4
  }
5
- /** Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
5
+ /** Model-viewer / editor rig (#207.7, #866): left-click selects (editor), middle-drag pans, right-drag orbits, scroll zooms toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
6
6
  export declare function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): import("react").JSX.Element;
@@ -4,7 +4,7 @@ import { useThree } from "@react-three/fiber";
4
4
  import { useEffect, useRef } from "react";
5
5
  import { MOUSE } from "three";
6
6
  import { resolveInspectionCameraConfig, resolveInspectionZoomToCursor, seedInspectionCamera, } from "./inspectionCameraMath.js";
7
- /** Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
7
+ /** Model-viewer / editor rig (#207.7, #866): left-click selects (editor), middle-drag pans, right-drag orbits, scroll zooms toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
8
8
  export function GameInspectionCamera({ config: configPatch }) {
9
9
  const config = resolveInspectionCameraConfig(configPatch);
10
10
  const controlsRef = useRef(null);
@@ -31,5 +31,5 @@ export function GameInspectionCamera({ config: configPatch }) {
31
31
  camera.position.add(controls.target.clone().sub(previous));
32
32
  controls.update();
33
33
  }, [camera, config.target.x, config.target.y, config.target.z]);
34
- return (_jsx(OrbitControls, { ref: controlsRef, makeDefault: true, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, zoomToCursor: resolveInspectionZoomToCursor(config.anchor), enablePan: config.pan, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, minPolarAngle: config.minPolarAngle, maxPolarAngle: config.maxPolarAngle, screenSpacePanning: true, mouseButtons: { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.PAN } }));
34
+ return (_jsx(OrbitControls, { ref: controlsRef, makeDefault: true, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, zoomToCursor: resolveInspectionZoomToCursor(config.anchor), enablePan: config.pan, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, minPolarAngle: config.minPolarAngle, maxPolarAngle: config.maxPolarAngle, screenSpacePanning: true, mouseButtons: { LEFT: undefined, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.ROTATE } }));
35
35
  }