@jgengine/shell 0.18.1 → 0.21.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 (261) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +132 -17
  5. package/dist/Shell3dPresentation.d.ts +5 -11
  6. package/dist/Shell3dPresentation.js +73 -11
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +20 -2
  10. package/dist/ShellHudPresentation.js +41 -6
  11. package/dist/audio/AudioComponents.js +6 -2
  12. package/dist/audio/audioEngine.d.ts +24 -0
  13. package/dist/audio/audioEngine.js +231 -83
  14. package/dist/audio/audioWire.d.ts +3 -0
  15. package/dist/audio/audioWire.js +9 -3
  16. package/dist/audio/loopParams.d.ts +39 -0
  17. package/dist/audio/loopParams.js +43 -0
  18. package/dist/audio/synthEngine.d.ts +7 -1
  19. package/dist/audio/synthEngine.js +103 -38
  20. package/dist/camera/GameCameraRig.js +3 -2
  21. package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
  22. package/dist/camera/GameFirstPersonCamera.js +68 -27
  23. package/dist/camera/Viewports.d.ts +34 -0
  24. package/dist/camera/Viewports.js +123 -0
  25. package/dist/camera/cameraRigs.js +76 -62
  26. package/dist/camera/chaseHeading.d.ts +9 -0
  27. package/dist/camera/chaseHeading.js +10 -0
  28. package/dist/camera/customMuzzle.d.ts +14 -0
  29. package/dist/camera/customMuzzle.js +68 -0
  30. package/dist/camera/index.d.ts +1 -0
  31. package/dist/camera/index.js +1 -0
  32. package/dist/camera/orbitCameraMath.js +2 -2
  33. package/dist/camera/rigMath.d.ts +82 -4
  34. package/dist/camera/rigMath.js +133 -15
  35. package/dist/camera/rigResolve.d.ts +3 -0
  36. package/dist/camera/rigResolve.js +11 -0
  37. package/dist/combat/ProjectileModels.d.ts +31 -0
  38. package/dist/combat/ProjectileModels.js +70 -0
  39. package/dist/commandSink.d.ts +1 -2
  40. package/dist/commandSink.js +3 -2
  41. package/dist/defineGame.d.ts +19 -3
  42. package/dist/defineGame.js +39 -7
  43. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  44. package/dist/devtools/DevtoolsOverlay.js +15 -167
  45. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  46. package/dist/devtools/DevtoolsRuntime.js +61 -0
  47. package/dist/devtools/agentBridge.d.ts +6 -0
  48. package/dist/devtools/agentBridge.js +16 -2
  49. package/dist/devtools/devtoolsReports.d.ts +68 -0
  50. package/dist/devtools/devtoolsReports.js +100 -0
  51. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  52. package/dist/devtools/latencyInstrumentation.js +11 -0
  53. package/dist/devtools/playtestProbe.d.ts +4 -0
  54. package/dist/devtools/playtestProbe.js +22 -0
  55. package/dist/diagnostics/FpsCounter.d.ts +17 -0
  56. package/dist/diagnostics/FpsCounter.js +32 -0
  57. package/dist/drivers/FrameDriver.js +35 -20
  58. package/dist/drivers/FrameRateLimiter.d.ts +9 -0
  59. package/dist/drivers/FrameRateLimiter.js +39 -0
  60. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  61. package/dist/drivers/HudOnlyDriver.js +32 -14
  62. package/dist/environment/DayNightSky.js +5 -5
  63. package/dist/environment/Daylight.d.ts +35 -1
  64. package/dist/environment/Daylight.js +98 -28
  65. package/dist/environment/EnvironmentScene.js +11 -10
  66. package/dist/environment/VolumetricClouds.js +20 -2
  67. package/dist/environment/daylightCycle.d.ts +20 -0
  68. package/dist/environment/daylightCycle.js +59 -7
  69. package/dist/environment/sunShadowMath.d.ts +45 -0
  70. package/dist/environment/sunShadowMath.js +50 -0
  71. package/dist/input/gamepadPoll.d.ts +47 -0
  72. package/dist/input/gamepadPoll.js +142 -0
  73. package/dist/input/gamepadSource.d.ts +11 -1
  74. package/dist/input/gamepadSource.js +117 -71
  75. package/dist/input/mouseLook.js +2 -1
  76. package/dist/input/padHaptics.d.ts +17 -0
  77. package/dist/input/padHaptics.js +32 -0
  78. package/dist/input/pointerLock.d.ts +11 -0
  79. package/dist/input/pointerLock.js +19 -0
  80. package/dist/inputSink.d.ts +6 -6
  81. package/dist/inputSink.js +60 -21
  82. package/dist/lazyShell3dPresentation.d.ts +9 -0
  83. package/dist/lazyShell3dPresentation.js +24 -0
  84. package/dist/materialOverride.d.ts +42 -0
  85. package/dist/materialOverride.js +95 -36
  86. package/dist/multiplayer.d.ts +33 -3
  87. package/dist/multiplayer.js +105 -22
  88. package/dist/pointer/pointerService.d.ts +2 -0
  89. package/dist/pointer/pointerService.js +12 -2
  90. package/dist/postfx/PostProcessing.d.ts +11 -1
  91. package/dist/postfx/PostProcessing.js +14 -42
  92. package/dist/postfx/gradeShader.d.ts +1 -1
  93. package/dist/postfx/gradeShader.js +30 -2
  94. package/dist/postfx/shadowPrepass.d.ts +3 -0
  95. package/dist/postfx/shadowPrepass.js +18 -0
  96. package/dist/postfx/stylizePass.d.ts +23 -0
  97. package/dist/postfx/stylizePass.js +216 -0
  98. package/dist/presentationRecovery.d.ts +75 -0
  99. package/dist/presentationRecovery.js +204 -0
  100. package/dist/presentationRecovery3d.d.ts +4 -0
  101. package/dist/presentationRecovery3d.js +32 -0
  102. package/dist/presentationResolve.d.ts +8 -1
  103. package/dist/presentationResolve.js +5 -2
  104. package/dist/render/CascadedShadows.d.ts +7 -2
  105. package/dist/render/CascadedShadows.js +98 -20
  106. package/dist/render/EnvironmentLighting.js +74 -35
  107. package/dist/render/HairCards.d.ts +7 -0
  108. package/dist/render/HairCards.js +23 -0
  109. package/dist/render/SceneModels.js +52 -78
  110. package/dist/render/SkinnedInstances.d.ts +91 -0
  111. package/dist/render/SkinnedInstances.js +249 -0
  112. package/dist/render/TextureFiltering.d.ts +16 -0
  113. package/dist/render/TextureFiltering.js +64 -0
  114. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  115. package/dist/render/authoredSurfaceMaterial.js +108 -0
  116. package/dist/render/displayColor.d.ts +33 -0
  117. package/dist/render/displayColor.js +138 -0
  118. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  119. package/dist/render/materialAppearanceSignals.js +58 -0
  120. package/dist/render/materialAsset.d.ts +98 -0
  121. package/dist/render/materialAsset.js +454 -0
  122. package/dist/render/measureBounds.js +16 -3
  123. package/dist/render/measureCollisionMesh.js +8 -1
  124. package/dist/render/modelAssets.d.ts +8 -0
  125. package/dist/render/modelAssets.js +43 -0
  126. package/dist/render/modelBindPose.d.ts +5 -0
  127. package/dist/render/modelBindPose.js +35 -0
  128. package/dist/render/modelLoad.d.ts +32 -0
  129. package/dist/render/modelLoad.js +70 -1
  130. package/dist/render/modelNodeVisibility.d.ts +6 -0
  131. package/dist/render/modelNodeVisibility.js +18 -0
  132. package/dist/render/modelRender.d.ts +18 -1
  133. package/dist/render/modelRender.js +81 -3
  134. package/dist/render/projectedOverdraw.d.ts +35 -0
  135. package/dist/render/projectedOverdraw.js +204 -0
  136. package/dist/render/resolveModel.d.ts +12 -2
  137. package/dist/render/resolveModel.js +25 -4
  138. package/dist/render/rigNode.d.ts +26 -0
  139. package/dist/render/rigNode.js +32 -0
  140. package/dist/render/spriteRender.d.ts +4 -0
  141. package/dist/render/spriteRender.js +5 -0
  142. package/dist/render/staticShapeInstances.d.ts +34 -0
  143. package/dist/render/staticShapeInstances.js +78 -0
  144. package/dist/render/useFootIk.d.ts +53 -4
  145. package/dist/render/useFootIk.js +262 -70
  146. package/dist/render/useModelAnimation.d.ts +50 -0
  147. package/dist/render/useModelAnimation.js +245 -99
  148. package/dist/render/useModelInstance.d.ts +25 -0
  149. package/dist/render/useModelInstance.js +43 -0
  150. package/dist/scatter/scatterModels.d.ts +3 -2
  151. package/dist/scatter/scatterModels.js +15 -7
  152. package/dist/scene/AuthoredScene.d.ts +23 -6
  153. package/dist/scene/AuthoredScene.js +46 -9
  154. package/dist/scene/CityRenderer.js +6 -6
  155. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  156. package/dist/settings/QuickControls.js +1 -1
  157. package/dist/settings/SettingsMenu.js +11 -18
  158. package/dist/settings/SettingsRuntime.d.ts +5 -0
  159. package/dist/settings/SettingsRuntime.js +29 -2
  160. package/dist/settings/appliedSettings.d.ts +9 -3
  161. package/dist/settings/appliedSettings.js +6 -5
  162. package/dist/settings/settingsController.d.ts +3 -0
  163. package/dist/settings/settingsController.js +47 -3
  164. package/dist/shellControlSuspension.d.ts +18 -0
  165. package/dist/shellControlSuspension.js +28 -0
  166. package/dist/shellInputPublication.d.ts +28 -0
  167. package/dist/shellInputPublication.js +49 -0
  168. package/dist/shellPointerInput.d.ts +18 -0
  169. package/dist/shellPointerInput.js +49 -0
  170. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  171. package/dist/structures/BuildingKitBatch.js +35 -56
  172. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  173. package/dist/structures/BuildingRenderBudget.js +54 -0
  174. package/dist/structures/GeneratedBuilding.d.ts +3 -15
  175. package/dist/structures/GeneratedBuilding.js +33 -22
  176. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  177. package/dist/structures/buildingKitMaterials.js +70 -0
  178. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  179. package/dist/structures/buildingSpatialBatch.js +45 -0
  180. package/dist/structures/buildingSurface.d.ts +21 -0
  181. package/dist/structures/buildingSurface.js +134 -0
  182. package/dist/terrain/GrassField.d.ts +2 -1
  183. package/dist/terrain/GrassField.js +59 -5
  184. package/dist/terrain/grassGeometry.d.ts +10 -0
  185. package/dist/terrain/grassGeometry.js +55 -0
  186. package/dist/terrain/grassMaterial.js +2 -2
  187. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  188. package/dist/terrain/terrainDetailMaterial.js +7 -7
  189. package/dist/touch/TouchControlsOverlay.js +4 -4
  190. package/dist/useShellMultiplayerSync.d.ts +1 -1
  191. package/dist/useShellMultiplayerSync.js +9 -3
  192. package/dist/vfx/ParticleField.d.ts +23 -20
  193. package/dist/vfx/ParticleField.js +168 -50
  194. package/dist/visibility/CullingProvider.js +2 -62
  195. package/dist/visibility/cullingDriver.d.ts +9 -0
  196. package/dist/visibility/cullingDriver.js +68 -0
  197. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  198. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  199. package/dist/weather/DustField.d.ts +2 -1
  200. package/dist/weather/DustField.js +17 -3
  201. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  202. package/dist/weather/FireSpreadLayer.js +5 -11
  203. package/dist/weather/LightningStrike.d.ts +4 -1
  204. package/dist/weather/LightningStrike.js +13 -17
  205. package/dist/weather/RainField.d.ts +2 -1
  206. package/dist/weather/RainField.js +17 -3
  207. package/dist/weather/RainImpactField.d.ts +16 -0
  208. package/dist/weather/RainImpactField.js +89 -0
  209. package/dist/weather/SnowField.d.ts +2 -1
  210. package/dist/weather/SnowField.js +17 -3
  211. package/dist/weather/WeatherLayer.d.ts +11 -2
  212. package/dist/weather/WeatherLayer.js +30 -6
  213. package/dist/weather/index.d.ts +2 -0
  214. package/dist/weather/index.js +2 -0
  215. package/dist/weather/weatherMath.d.ts +2 -0
  216. package/dist/weather/weatherMath.js +8 -0
  217. package/dist/weather/weatherShelter.d.ts +18 -0
  218. package/dist/weather/weatherShelter.js +75 -0
  219. package/dist/weather/weatherUniforms.d.ts +40 -1
  220. package/dist/weather/weatherUniforms.js +29 -9
  221. package/dist/world/AuthoredFlocks.d.ts +22 -0
  222. package/dist/world/AuthoredFlocks.js +87 -0
  223. package/dist/world/StaticObjectModels.d.ts +8 -0
  224. package/dist/world/StaticObjectModels.js +75 -0
  225. package/dist/world/WorldHud.d.ts +14 -6
  226. package/dist/world/WorldHud.js +25 -12
  227. package/dist/world/WorldParticles.d.ts +10 -23
  228. package/dist/world/WorldParticles.js +210 -62
  229. package/dist/world/WorldScene.d.ts +3 -1
  230. package/dist/world/WorldScene.js +41 -26
  231. package/dist/world/staticObjectBatches.d.ts +35 -0
  232. package/dist/world/staticObjectBatches.js +163 -0
  233. package/dist/world/worldBarSamples.d.ts +37 -33
  234. package/dist/world/worldBarSamples.js +117 -75
  235. package/package.json +4 -4
  236. package/skills/ce-handoff/SKILL.md +2 -2
  237. package/skills/game-design/SKILL.md +2 -2
  238. package/skills/jgengine/SKILL.md +28 -6
  239. package/skills/jgengine/api.md +139 -99
  240. package/skills/jgengine/capabilities.md +28 -0
  241. package/skills/jgengine/recipes/minimal-game.md +10 -2
  242. package/skills/jgengine-ui/SKILL.md +16 -17
  243. package/skills/jgengine-ui/api.md +603 -365
  244. package/skills/jgengine-ui/capabilities.md +131 -1
  245. package/skills/jgengine-ui/reference-primitives.md +179 -0
  246. package/skills/jgengine-ui/reference.md +4 -81
  247. package/skills/jgengine-world/SKILL.md +25 -4
  248. package/skills/jgengine-world/api.md +1045 -655
  249. package/skills/jgengine-world/capabilities.md +246 -9
  250. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  251. package/skills/jgengine-world/recipes/controls.md +48 -0
  252. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  253. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  254. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  255. package/skills/jgengine-world/reference.md +180 -4
  256. package/skills/jgengine-world/visibility.md +139 -0
  257. package/skills/level-design/SKILL.md +1 -1
  258. package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
  259. package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
  260. package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
  261. package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
package/dist/inputSink.js CHANGED
@@ -5,56 +5,91 @@ import { INPUT_COMMAND } from "@jgengine/core/runtime/hostedGameRunner";
5
5
  export function noopInputSink() {
6
6
  return { send() { } };
7
7
  }
8
- const remoteInputSources = new Map();
9
- function remoteInputSourceFor(serverId) {
10
- let source = remoteInputSources.get(serverId);
8
+ const remoteInputSources = new WeakMap();
9
+ let lastInputSeq = 0;
10
+ function remoteInputSourceFor(backend, serverId) {
11
+ let sources = remoteInputSources.get(backend.transport);
12
+ if (sources === undefined) {
13
+ sources = new Map();
14
+ remoteInputSources.set(backend.transport, sources);
15
+ }
16
+ let source = sources.get(serverId);
11
17
  if (source === undefined) {
12
- source = { pending: null, inFlight: false };
13
- remoteInputSources.set(serverId, source);
18
+ source = { pending: null, inFlight: null, latest: null, presses: new Map() };
19
+ sources.set(serverId, source);
14
20
  }
15
21
  return source;
16
22
  }
23
+ // Use the browser time origin so a full reload stays ahead of the previous client.
24
+ // The local high-water mark also preserves release ordering if its clock regresses.
17
25
  function monotonicInputSeq() {
18
- return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
26
+ const now = typeof performance !== "undefined" ? performance.timeOrigin + performance.now() : Date.now();
27
+ return lastInputSeq = Math.max(now, lastInputSeq + 0.001);
19
28
  }
20
29
  function pumpRemoteInput(backend, serverId, source) {
21
- if (source.inFlight)
30
+ if (source.inFlight !== null)
22
31
  return;
23
32
  const frame = source.pending;
24
33
  if (frame === null) {
25
- remoteInputSources.delete(serverId);
34
+ if (source.presses.size === 0)
35
+ remoteInputSources.get(backend.transport)?.delete(serverId);
26
36
  return;
27
37
  }
28
38
  source.pending = null;
29
- source.inFlight = true;
39
+ const owner = {};
40
+ source.inFlight = owner;
41
+ const presses = [...source.presses.values()].map(press => ({ ...press }));
30
42
  const seq = monotonicInputSeq();
43
+ // Live clients and the joined host have independent simulation clocks. The host
44
+ // stamps receipt on its next tick; explicit ticks remain supported by replay.
45
+ const { tick: _clientTick, presses: _framePresses, ...intent } = frame;
31
46
  void backend.transport
32
- .runCommand({ serverId, command: INPUT_COMMAND, input: { ...frame, seq } })
47
+ .runCommand({ serverId, command: INPUT_COMMAND, input: { ...intent, ...(presses.length > 0 ? { presses } : {}), seq } })
33
48
  .then((result) => {
34
- if (!result.ok) {
49
+ if (result.ok)
50
+ for (const press of presses)
51
+ source.presses.delete(press.seq);
52
+ if (!result.ok)
35
53
  console.warn(`[jgengine:input] frame seq=${seq} to server "${serverId}" rejected: ${result.reason}`);
36
- }
37
54
  })
38
55
  .catch((error) => {
39
56
  console.warn(`[jgengine:input] frame seq=${seq} to server "${serverId}" failed to send`, error);
40
57
  })
41
58
  .finally(() => {
42
- source.inFlight = false;
59
+ // An urgent discrete intent supersedes this flight. Its old completion must not
60
+ // release the replacement's ACK gate or replay the superseded pending frame.
61
+ if (source.inFlight !== owner)
62
+ return;
63
+ source.inFlight = null;
43
64
  pumpRemoteInput(backend, serverId, source);
44
65
  });
45
66
  }
46
- /**
47
- * Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via
48
- * {@link INPUT_COMMAND}. Sends are sequenced one-in-flight-at-a-time per `serverId` and stamped with a monotonic
49
- * seq: a frame sent while one is already in flight replaces the pending frame rather than racing it, so the
50
- * latest intent always wins and a stale frame can never resolve after (and overwrite) a newer one.
67
+ /** Coalesces continuous frames. Discrete release/reset intents bypass a pending ACK;
68
+ * the host's monotonic sequence admission rejects any older input arriving afterwards.
51
69
  * @internal
52
70
  */
53
71
  export function remoteInputSink(backend, serverId) {
54
72
  return {
55
- send(frame) {
56
- const source = remoteInputSourceFor(serverId);
57
- source.pending = frame;
73
+ send(frame, options) {
74
+ const source = remoteInputSourceFor(backend, serverId);
75
+ if (options?.reset)
76
+ source.presses.clear();
77
+ for (const press of frame.presses ?? [])
78
+ source.presses.set(press.seq, { ...press });
79
+ // A reset must overtake an identical neutral release that still carries an
80
+ // unacknowledged press. Intent equality alone does not describe that flight.
81
+ if (!options?.reset && source.latest !== null && inputFramesEqual(source.latest, frame) && source.presses.size === 0)
82
+ return;
83
+ const snapshot = {
84
+ ...frame, held: [...frame.held],
85
+ ...(frame.presses === undefined ? {} : { presses: frame.presses.map(press => ({ ...press })) }),
86
+ pointer: frame.pointer === null ? null : { ...frame.pointer },
87
+ ...(frame.analog === undefined ? {} : { analog: frame.analog === null ? null : { ...frame.analog } }),
88
+ };
89
+ source.latest = snapshot;
90
+ source.pending = snapshot;
91
+ if (options?.urgent)
92
+ source.inFlight = null;
58
93
  pumpRemoteInput(backend, serverId, source);
59
94
  },
60
95
  };
@@ -72,6 +107,10 @@ export function resolveInputSink(opts) {
72
107
  * @internal
73
108
  */
74
109
  export function inputFramesEqual(a, b) {
110
+ const ap = a.presses ?? [];
111
+ const bp = b.presses ?? [];
112
+ if (ap.length !== bp.length || ap.some((press, index) => press.seq !== bp[index].seq || press.action !== bp[index].action))
113
+ return false;
75
114
  if (a.held.length !== b.held.length)
76
115
  return false;
77
116
  for (let i = 0; i < a.held.length; i++)
@@ -0,0 +1,9 @@
1
+ import { type ComponentProps } from "react";
2
+ import type { Shell3dPresentation } from "./Shell3dPresentation.js";
3
+ type Presentation = typeof Shell3dPresentation;
4
+ type Props = ComponentProps<Presentation> & {
5
+ onPresentationReveal?: () => void;
6
+ };
7
+ /** Loads the normal 3D implementation only for a 3D presentation. @internal */
8
+ export declare function LazyShell3dPresentation({ onPresentationReveal, ...props }: Props): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { lazy, Suspense, useLayoutEffect, useState } from "react";
3
+ import { suspendPlayControls } from "@jgengine/core/game/controlGate";
4
+ import { markPresentationCodeLoadFailure } from "./presentationRecovery.js";
5
+ let loadedPresentation;
6
+ function getPresentation() {
7
+ return loadedPresentation ??= lazy(() => import("./Shell3dPresentation.js").then((module) => ({ default: module.Shell3dPresentation }), (error) => {
8
+ throw markPresentationCodeLoadFailure(error);
9
+ }));
10
+ }
11
+ function LoadingPresentation({ ctx }) {
12
+ useLayoutEffect(() => suspendPlayControls(ctx), [ctx]);
13
+ return (_jsx("div", { role: "status", "aria-live": "polite", "data-shell-code-loading": true, style: { display: "grid", placeItems: "center", width: "100%", height: "100%", minHeight: 160, background: "#0f172a", color: "#e2e8f0", fontFamily: "ui-sans-serif, system-ui, sans-serif" }, children: "Loading game view\u2026" }));
14
+ }
15
+ function PresentationReveal({ onReveal }) {
16
+ // React repeats layout effects when a suspended tree becomes visible again.
17
+ useLayoutEffect(() => { onReveal?.(); });
18
+ return null;
19
+ }
20
+ /** Loads the normal 3D implementation only for a 3D presentation. @internal */
21
+ export function LazyShell3dPresentation({ onPresentationReveal, ...props }) {
22
+ const [Presentation] = useState(getPresentation);
23
+ return (_jsxs(Suspense, { fallback: _jsx(LoadingPresentation, { ctx: props.ctx }), children: [_jsx(Presentation, { ...props }), _jsx(PresentationReveal, { onReveal: onPresentationReveal })] }));
24
+ }
@@ -4,6 +4,8 @@ export interface MaterialOverrideOptions {
4
4
  clone?: boolean;
5
5
  /** Loaded PBR textures matching `ModelMaterialOverride.maps`' roles, applied onto each standard or physical material alongside the tint override. The caller (a React component, via `useTexture`) owns loading; this function never fetches a URL. */
6
6
  textures?: MaterialOverrideTextures;
7
+ /** Records material replacements for the caller's resource owner. */
8
+ ownMaterial?: (material: THREE.Material) => void;
7
9
  }
8
10
  /** Loaded PBR textures for `applyMaterialOverride`'s `textures` option — matches `ModelMaterialMaps`' roles. */
9
11
  export interface MaterialOverrideTextures {
@@ -14,6 +16,46 @@ export interface MaterialOverrideTextures {
14
16
  metalness?: THREE.Texture;
15
17
  emissive?: THREE.Texture;
16
18
  height?: THREE.Texture;
19
+ alpha?: THREE.Texture;
20
+ sheenColor?: THREE.Texture;
21
+ sheenRoughness?: THREE.Texture;
22
+ anisotropy?: THREE.Texture;
23
+ clearcoat?: THREE.Texture;
24
+ clearcoatRoughness?: THREE.Texture;
25
+ clearcoatNormal?: THREE.Texture;
26
+ specularIntensity?: THREE.Texture;
27
+ specularColor?: THREE.Texture;
28
+ transmission?: THREE.Texture;
29
+ thickness?: THREE.Texture;
30
+ iridescence?: THREE.Texture;
31
+ iridescenceThickness?: THREE.Texture;
17
32
  }
33
+ /** Physical texture roles mapped to the pinned Three renderer's native properties. @internal */
34
+ export declare const MATERIAL_TEXTURE_PROPERTIES: {
35
+ readonly color: "map";
36
+ readonly normal: "normalMap";
37
+ readonly roughness: "roughnessMap";
38
+ readonly ao: "aoMap";
39
+ readonly metalness: "metalnessMap";
40
+ readonly emissive: "emissiveMap";
41
+ readonly height: "displacementMap";
42
+ readonly alpha: "alphaMap";
43
+ readonly sheenColor: "sheenColorMap";
44
+ readonly sheenRoughness: "sheenRoughnessMap";
45
+ readonly anisotropy: "anisotropyMap";
46
+ readonly clearcoat: "clearcoatMap";
47
+ readonly clearcoatRoughness: "clearcoatRoughnessMap";
48
+ readonly clearcoatNormal: "clearcoatNormalMap";
49
+ readonly specularIntensity: "specularIntensityMap";
50
+ readonly specularColor: "specularColorMap";
51
+ readonly transmission: "transmissionMap";
52
+ readonly thickness: "thicknessMap";
53
+ readonly iridescence: "iridescenceMap";
54
+ readonly iridescenceThickness: "iridescenceThicknessMap";
55
+ };
56
+ /** Whether authored settings require a physical shader; ordinary PBR stays standard. @internal */
57
+ export declare function requiresPhysicalMaterial(override: ModelMaterialOverride, textures?: MaterialOverrideTextures): boolean;
18
58
  /** @internal */
19
59
  export declare function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride, options?: MaterialOverrideOptions): void;
60
+ /** Apply the shared override contract to one material. @internal */
61
+ export declare function applyMaterialOverrideToMaterial(material: THREE.Material, override: ModelMaterialOverride, clone: boolean, textures: MaterialOverrideTextures | undefined): THREE.Material;
@@ -1,50 +1,106 @@
1
1
  import * as THREE from "three";
2
+ /** Physical texture roles mapped to the pinned Three renderer's native properties. @internal */
3
+ export const MATERIAL_TEXTURE_PROPERTIES = {
4
+ color: "map", normal: "normalMap", roughness: "roughnessMap", ao: "aoMap", metalness: "metalnessMap", emissive: "emissiveMap", height: "displacementMap", alpha: "alphaMap",
5
+ sheenColor: "sheenColorMap", sheenRoughness: "sheenRoughnessMap", anisotropy: "anisotropyMap", clearcoat: "clearcoatMap", clearcoatRoughness: "clearcoatRoughnessMap", clearcoatNormal: "clearcoatNormalMap", specularIntensity: "specularIntensityMap", specularColor: "specularColorMap", transmission: "transmissionMap", thickness: "thicknessMap", iridescence: "iridescenceMap", iridescenceThickness: "iridescenceThicknessMap",
6
+ };
7
+ const physicalNumbers = ["sheen", "sheenRoughness", "anisotropy", "anisotropyRotation", "clearcoat", "clearcoatRoughness", "ior", "specularIntensity", "transmission", "thickness", "attenuationDistance", "iridescence", "iridescenceIOR"];
8
+ const physicalColors = ["sheenColor", "specularColor", "attenuationColor"];
9
+ /** Whether authored settings require a physical shader; ordinary PBR stays standard. @internal */
10
+ export function requiresPhysicalMaterial(override, textures) {
11
+ return [...physicalNumbers, ...physicalColors, "iridescenceThicknessRange"].some(key => override[key] !== undefined) || Object.keys(textures ?? {}).some(key => ["sheenColor", "sheenRoughness", "anisotropy", "clearcoat", "clearcoatRoughness", "clearcoatNormal", "specularIntensity", "specularColor", "transmission", "thickness", "iridescence", "iridescenceThickness"].includes(key));
12
+ }
2
13
  /** @internal */
3
14
  export function applyMaterialOverride(root, override, options) {
4
15
  const clone = options?.clone !== false;
5
16
  const textures = options?.textures;
17
+ const applied = new Map();
18
+ const apply = (material) => {
19
+ const cached = applied.get(material);
20
+ if (cached !== undefined)
21
+ return cached;
22
+ const target = applyMaterialOverrideToMaterial(material, override, clone, textures);
23
+ applied.set(material, target);
24
+ if (target !== material)
25
+ options?.ownMaterial?.(target);
26
+ return target;
27
+ };
6
28
  root.traverse((node) => {
7
29
  const mesh = node;
8
30
  if (!mesh.isMesh)
9
31
  return;
10
- mesh.material = Array.isArray(mesh.material)
11
- ? mesh.material.map((material) => overrideOne(material, override, clone, textures))
12
- : overrideOne(mesh.material, override, clone, textures);
32
+ mesh.material = Array.isArray(mesh.material) ? mesh.material.map(apply) : apply(mesh.material);
13
33
  });
14
34
  }
15
- function overrideOne(material, override, clone, textures) {
35
+ /** Apply the shared override contract to one material. @internal */
36
+ export function applyMaterialOverrideToMaterial(material, override, clone, textures) {
16
37
  if (!isStandardOrPhysicalMaterial(material))
17
38
  return material;
18
- const target = clone ? material.clone() : material;
19
- if (override.color !== undefined)
20
- target.color.set(override.color);
21
- if (override.metalness !== undefined)
22
- target.metalness = override.metalness;
23
- if (override.roughness !== undefined)
24
- target.roughness = override.roughness;
25
- if (override.emissive !== undefined)
26
- target.emissive.set(override.emissive);
27
- if (override.emissiveIntensity !== undefined)
28
- target.emissiveIntensity = override.emissiveIntensity;
29
- if (textures?.color !== undefined)
30
- target.map = textures.color;
31
- if (textures?.normal !== undefined)
32
- target.normalMap = textures.normal;
33
- if (textures?.roughness !== undefined)
34
- target.roughnessMap = textures.roughness;
35
- if (textures?.ao !== undefined)
36
- target.aoMap = textures.ao;
37
- if (textures?.metalness !== undefined)
38
- target.metalnessMap = textures.metalness;
39
- if (textures?.emissive !== undefined)
40
- target.emissiveMap = textures.emissive;
41
- if (textures?.height !== undefined)
42
- target.displacementMap = textures.height;
43
- if (textures !== undefined)
44
- target.needsUpdate = true;
45
- if (override.rim !== undefined)
46
- applyRimLight(target, override.rim);
47
- return target;
39
+ const promote = requiresPhysicalMaterial(override, textures) && !material.isMeshPhysicalMaterial;
40
+ const target = promote ? new THREE.MeshPhysicalMaterial() : clone ? material.clone() : material;
41
+ try {
42
+ if (promote)
43
+ THREE.MeshStandardMaterial.prototype.copy.call(target, material);
44
+ if (clone || promote) {
45
+ target.onBeforeCompile = material.onBeforeCompile;
46
+ target.customProgramCacheKey = material.customProgramCacheKey;
47
+ }
48
+ if (override.color !== undefined)
49
+ target.color.set(override.color);
50
+ if (override.metalness !== undefined)
51
+ target.metalness = override.metalness;
52
+ if (override.roughness !== undefined)
53
+ target.roughness = override.roughness;
54
+ if (override.emissive !== undefined)
55
+ target.emissive.set(override.emissive);
56
+ if (override.emissiveIntensity !== undefined)
57
+ target.emissiveIntensity = override.emissiveIntensity;
58
+ if (override.normalScale !== undefined)
59
+ target.normalScale.set(...override.normalScale);
60
+ const physical = target;
61
+ for (const key of physicalNumbers)
62
+ if (override[key] !== undefined)
63
+ physical[key] = override[key];
64
+ for (const key of physicalColors)
65
+ if (override[key] !== undefined)
66
+ physical[key].set(override[key]);
67
+ if (override.iridescenceThicknessRange !== undefined)
68
+ physical.iridescenceThicknessRange = [...override.iridescenceThicknessRange];
69
+ if (override.alphaMode !== undefined) {
70
+ target.transparent = override.alphaMode === "blend";
71
+ target.alphaTest = override.alphaMode === "mask" ? override.alphaCutoff ?? 0.5 : 0;
72
+ target.depthWrite = override.alphaMode !== "blend";
73
+ }
74
+ else if (override.alphaCutoff !== undefined)
75
+ target.alphaTest = override.alphaCutoff;
76
+ if (override.opacity !== undefined)
77
+ target.opacity = override.opacity;
78
+ if (override.doubleSided !== undefined)
79
+ target.side = override.doubleSided ? THREE.DoubleSide : THREE.FrontSide;
80
+ for (const [role, texture] of Object.entries(textures ?? {})) {
81
+ const property = MATERIAL_TEXTURE_PROPERTIES[role];
82
+ if (property !== undefined)
83
+ target[property] = texture;
84
+ }
85
+ if (textures !== undefined)
86
+ target.needsUpdate = true;
87
+ if (override.rim !== undefined)
88
+ applyRimLight(target, override.rim);
89
+ if (override.alphaMode !== undefined || override.doubleSided !== undefined)
90
+ target.needsUpdate = true;
91
+ return target;
92
+ }
93
+ catch (error) {
94
+ if (target !== material) {
95
+ try {
96
+ target.dispose();
97
+ }
98
+ finally {
99
+ throw error;
100
+ }
101
+ }
102
+ throw error;
103
+ }
48
104
  }
49
105
  function isStandardOrPhysicalMaterial(material) {
50
106
  return material.isMeshStandardMaterial === true;
@@ -60,7 +116,10 @@ function applyRimLight(target, rim) {
60
116
  const power = rim.power ?? 2.5;
61
117
  if (strength <= 0)
62
118
  return;
63
- target.onBeforeCompile = (shader) => {
119
+ const previousCompile = target.onBeforeCompile;
120
+ const previousKey = target.customProgramCacheKey();
121
+ target.onBeforeCompile = (shader, renderer) => {
122
+ previousCompile.call(target, shader, renderer);
64
123
  shader.uniforms.uJgRimColor = { value: color };
65
124
  shader.uniforms.uJgRimStrength = { value: strength };
66
125
  shader.uniforms.uJgRimPower = { value: power };
@@ -74,6 +133,6 @@ outgoingLight += uJgRimColor * jgRim * uJgRimStrength;
74
133
  #include <opaque_fragment>`);
75
134
  };
76
135
  // Two materials differing only in rim tuning must not share a compiled program.
77
- target.customProgramCacheKey = () => `jgengine-rim-${rim.color ?? "#ffffff"}-${strength}-${power}`;
136
+ target.customProgramCacheKey = () => `${previousKey}|jgengine-rim-${rim.color ?? "#ffffff"}-${strength}-${power}`;
78
137
  target.needsUpdate = true;
79
138
  }
@@ -1,5 +1,8 @@
1
1
  import type { GameDefinition } from "@jgengine/core/game/defineGame";
2
2
  import type { MultiplayerSession } from "@jgengine/core/runtime/transport";
3
+ import { type HostedWorldStore } from "@jgengine/core/runtime/hostedWorldSession";
4
+ import type { PlayableGame } from "./registry.js";
5
+ import { type PeerHost, type PeerHostOptions, type PeerGuest, type PeerGuestOptions, type PeerSignaling } from "@jgengine/ws/peer";
3
6
  export type ShellMultiplayer = MultiplayerSession;
4
7
  export declare const DEFAULT_FEED_ACTIONS: string[];
5
8
  export declare function randomPlayerId(): string;
@@ -12,12 +15,39 @@ export type ResolveShellMultiplayerArgs = {
12
15
  feedActions?: string[];
13
16
  };
14
17
  export declare function resolveShellMultiplayer(args: ResolveShellMultiplayerArgs): ShellMultiplayer | null;
15
- export declare function resolvePeerShellMultiplayer(args: {
18
+ /** Alternate peer factories for hosts that provide their own WebRTC implementation or transport pipe. */
19
+ export interface PeerShellFactories {
20
+ host(options: PeerHostOptions): PeerHost;
21
+ guest(options: PeerGuestOptions): PeerGuest;
22
+ }
23
+ /** Peer bootstrap options; passing a playable hosts its authoritative GameContext world. */
24
+ export interface ResolvePeerShellMultiplayerArgs {
16
25
  gameId: string;
17
26
  role: "host" | "join";
18
27
  room?: string;
19
28
  userId?: string;
20
29
  feedActions?: string[];
21
- }): Promise<ShellMultiplayer & {
22
- close: () => void;
30
+ /** Requires an adapter with `authority: "server"`; joiners mount the same playable. */
31
+ playable?: PlayableGame;
32
+ /** Maximum active players in the authoritative world, including the host player. */
33
+ slotsPerServer?: number;
34
+ /** Authoritative tick interval in milliseconds; defaults to 1000 / 30. */
35
+ tickMs?: number;
36
+ /** Minimum automatic save interval; joins, commands and close still save immediately. Defaults to 1000 ms. */
37
+ saveIntervalMs?: number;
38
+ /** Persistence for the authoritative host world; defaults to an in-memory store. */
39
+ store?: HostedWorldStore;
40
+ /** Signaling ownership transfers to the returned session, which closes it on teardown. */
41
+ signaling?: PeerSignaling;
42
+ peers?: PeerShellFactories;
43
+ /** Inject host scheduling; the returned cancellation is called before transport teardown. */
44
+ scheduleTicks?: (tick: () => void, intervalMs: number) => () => void;
45
+ }
46
+ /**
47
+ * Opens a peer session. With `playable`, one host runs the game loop and guests mirror its shared world.
48
+ * Await `close()` to finish authoritative persistence. Same-origin rooms use BroadcastChannel signaling.
49
+ * @capability multiplayer-p2p bootstrap a playable authoritative world for browser peer co-op
50
+ */
51
+ export declare function resolvePeerShellMultiplayer(args: ResolvePeerShellMultiplayerArgs): Promise<ShellMultiplayer & {
52
+ close: () => Promise<void>;
23
53
  }>;
@@ -1,4 +1,6 @@
1
1
  import { adapterOf, isServerAuthoritative } from "@jgengine/core/runtime/adapter";
2
+ import { createHostedWorldSession, createHostedWorldSessionAsync } from "@jgengine/core/runtime/hostedWorldSession";
3
+ import { createWorldGameHost } from "@jgengine/ws/worldHost";
2
4
  import { createWsBackend } from "@jgengine/ws/createWsBackend";
3
5
  import { announcePeerHost, broadcastChannelSignaling, createPeerGuest, createPeerHost, joinPeerSession, } from "@jgengine/ws/peer";
4
6
  export const DEFAULT_FEED_ACTIONS = ["entity.died"];
@@ -47,35 +49,116 @@ export function resolveShellMultiplayer(args) {
47
49
  `so the game shipped single-player. Wire up the ${adapter.kind} transport or switch to ws()/lan().`);
48
50
  return null;
49
51
  }
52
+ /**
53
+ * Opens a peer session. With `playable`, one host runs the game loop and guests mirror its shared world.
54
+ * Await `close()` to finish authoritative persistence. Same-origin rooms use BroadcastChannel signaling.
55
+ * @capability multiplayer-p2p bootstrap a playable authoritative world for browser peer co-op
56
+ */
50
57
  export async function resolvePeerShellMultiplayer(args) {
58
+ if (args.playable !== undefined && !isServerAuthoritative(args.playable.game.multiplayer)) {
59
+ throw new Error("playable peer worlds require authority: server");
60
+ }
61
+ const tickMs = args.tickMs ?? 1000 / 30;
62
+ if (!Number.isFinite(tickMs) || tickMs <= 0 || tickMs > 1000)
63
+ throw new Error("tickMs must be greater than zero and at most 1000");
64
+ const saveIntervalMs = args.saveIntervalMs ?? 1000;
65
+ if (!Number.isFinite(saveIntervalMs) || saveIntervalMs < 0)
66
+ throw new Error("saveIntervalMs must be nonnegative and finite");
51
67
  const userId = args.userId ?? randomPlayerId();
52
68
  const feedActions = args.feedActions ?? DEFAULT_FEED_ACTIONS;
53
- const signaling = broadcastChannelSignaling(args.room ?? `jg-p2p-${args.gameId}`);
69
+ const signaling = args.signaling ?? broadcastChannelSignaling(args.room ?? `jg-p2p-${args.gameId}`);
70
+ const peers = args.peers ?? { host: createPeerHost, guest: createPeerGuest };
54
71
  if (args.role === "host") {
55
- const peerHost = createPeerHost({ userId });
56
- const stopAnnouncing = announcePeerHost(peerHost, signaling);
72
+ let closed = false;
73
+ let activeSession = null;
74
+ let host;
75
+ let cancelTicks = () => { };
76
+ let stopAnnouncing = () => { };
77
+ let peerHost;
78
+ let closing = null;
79
+ try {
80
+ if (args.playable !== undefined) {
81
+ const playable = args.playable;
82
+ let loading = null;
83
+ host = createWorldGameHost({
84
+ slotsPerServer: args.slotsPerServer,
85
+ session({ gameId, serverId }) {
86
+ if (closed || gameId !== args.gameId || serverId !== args.gameId)
87
+ return null;
88
+ if (loading === null) {
89
+ const attempt = args.store === undefined
90
+ ? Promise.resolve(createHostedWorldSession({ definition: playable.game, content: playable.content, host: { userId, isNew: true }, saveIntervalMs }))
91
+ : createHostedWorldSessionAsync({ definition: playable.game, content: playable.content, host: { userId, isNew: true }, store: args.store, saveIntervalMs });
92
+ loading = attempt;
93
+ void attempt.catch(() => { if (loading === attempt)
94
+ loading = null; });
95
+ }
96
+ return loading.then(session => { activeSession = session; return session; });
97
+ },
98
+ });
99
+ }
100
+ peerHost = peers.host({ userId, ...(host === undefined ? {} : { host }) });
101
+ stopAnnouncing = announcePeerHost(peerHost, signaling);
102
+ if (host !== undefined) {
103
+ const worldHost = host;
104
+ const tick = () => { if (!closed && activeSession !== null && activeSession.members().length > 0)
105
+ worldHost.tick(tickMs / 1000); };
106
+ cancelTicks = args.scheduleTicks?.(tick, tickMs) ?? (() => {
107
+ const timer = setInterval(tick, tickMs);
108
+ return () => clearInterval(timer);
109
+ })();
110
+ }
111
+ const resolvedPeerHost = peerHost;
112
+ return {
113
+ gameId: args.gameId, userId, backend: resolvedPeerHost.backend, feedActions,
114
+ close() {
115
+ if (closing !== null)
116
+ return closing;
117
+ closed = true;
118
+ cancelTicks();
119
+ stopAnnouncing();
120
+ signaling.close();
121
+ resolvedPeerHost.close();
122
+ closing = resolvedPeerHost.router.drain().then(async () => {
123
+ if (host !== undefined)
124
+ await host.stop();
125
+ });
126
+ return closing;
127
+ },
128
+ };
129
+ }
130
+ catch (error) {
131
+ closed = true;
132
+ cancelTicks();
133
+ stopAnnouncing();
134
+ signaling.close();
135
+ peerHost?.close();
136
+ await peerHost?.router.drain();
137
+ if (host !== undefined)
138
+ await host.stop();
139
+ throw error;
140
+ }
141
+ }
142
+ let guest;
143
+ try {
144
+ guest = peers.guest({ userId });
145
+ const backend = await joinPeerSession(guest, signaling);
146
+ const resolvedGuest = guest;
147
+ let closed = false;
57
148
  return {
58
- gameId: args.gameId,
59
- userId,
60
- backend: peerHost.backend,
61
- feedActions,
62
- close: () => {
63
- stopAnnouncing();
149
+ gameId: args.gameId, userId, backend, feedActions,
150
+ async close() {
151
+ if (closed)
152
+ return;
153
+ closed = true;
64
154
  signaling.close();
65
- peerHost.close();
155
+ resolvedGuest.close();
66
156
  },
67
157
  };
68
158
  }
69
- const guest = createPeerGuest({ userId });
70
- const backend = await joinPeerSession(guest, signaling);
71
- return {
72
- gameId: args.gameId,
73
- userId,
74
- backend,
75
- feedActions,
76
- close: () => {
77
- signaling.close();
78
- guest.close();
79
- },
80
- };
159
+ catch (error) {
160
+ signaling.close();
161
+ guest?.close();
162
+ throw error;
163
+ }
81
164
  }
@@ -2,6 +2,8 @@ import * as THREE from "three";
2
2
  import type { PointerHit, PointerVec3 } from "@jgengine/core/input/pointer";
3
3
  export declare const POINTER_ENTITY_KEY = "jgEntityId";
4
4
  export declare const POINTER_OBJECT_KEY = "jgObjectId";
5
+ /** Object-store identities indexed by the current compacted GPU instance slot. @internal */
6
+ export declare const POINTER_OBJECT_INSTANCES_KEY = "jgObjectInstances";
5
7
  export type PointerHitFilter = (object: THREE.Object3D) => boolean;
6
8
  interface PointerDeps {
7
9
  camera: THREE.Camera;
@@ -1,6 +1,8 @@
1
1
  import * as THREE from "three";
2
2
  export const POINTER_ENTITY_KEY = "jgEntityId";
3
3
  export const POINTER_OBJECT_KEY = "jgObjectId";
4
+ /** Object-store identities indexed by the current compacted GPU instance slot. @internal */
5
+ export const POINTER_OBJECT_INSTANCES_KEY = "jgObjectInstances";
4
6
  function tagOf(object, key) {
5
7
  let cursor = object;
6
8
  while (cursor !== null) {
@@ -24,6 +26,8 @@ export function createPointerService() {
24
26
  const centerNdc = new THREE.Vector2(0, 0);
25
27
  const scratch = new THREE.Vector3();
26
28
  const normalMatrix = new THREE.Matrix3();
29
+ const instanceMatrix = new THREE.Matrix4();
30
+ const hitMatrix = new THREE.Matrix4();
27
31
  const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
28
32
  let deps = null;
29
33
  let cursorPresent = false;
@@ -45,7 +49,12 @@ export function createPointerService() {
45
49
  const point = [hit.point.x, hit.point.y, hit.point.z];
46
50
  let normal = [0, 1, 0];
47
51
  if (hit.face !== null && hit.face !== undefined) {
48
- normalMatrix.getNormalMatrix(hit.object.matrixWorld);
52
+ hitMatrix.copy(hit.object.matrixWorld);
53
+ if (mesh.isInstancedMesh && hit.instanceId !== undefined) {
54
+ mesh.getMatrixAt(hit.instanceId, instanceMatrix);
55
+ hitMatrix.multiply(instanceMatrix);
56
+ }
57
+ normalMatrix.getNormalMatrix(hitMatrix);
49
58
  scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
50
59
  normal = [scratch.x, scratch.y, scratch.z];
51
60
  }
@@ -54,7 +63,8 @@ export function createPointerService() {
54
63
  point,
55
64
  normal,
56
65
  entity: tagOf(hit.object, POINTER_ENTITY_KEY),
57
- object: tagOf(hit.object, POINTER_OBJECT_KEY),
66
+ object: hit.instanceId === undefined ? tagOf(hit.object, POINTER_OBJECT_KEY) :
67
+ hit.object.userData[POINTER_OBJECT_INSTANCES_KEY]?.[hit.instanceId] ?? tagOf(hit.object, POINTER_OBJECT_KEY),
58
68
  ...(uv !== undefined ? { uv } : {}),
59
69
  material: standardMaterialSample(mesh.material),
60
70
  ...(hit.instanceId !== undefined ? { instanceId: hit.instanceId } : {}),
@@ -1,9 +1,18 @@
1
+ import { EffectComposer, type ShaderPass } from "three-stdlib";
1
2
  import type { PostProcessingConfig } from "@jgengine/core/render/postProcessing";
2
3
  import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
4
+ interface BuiltGraph {
5
+ composer: EffectComposer;
6
+ grade: ShaderPass | null;
7
+ }
8
+ /** Release each pass and the composer's owned targets. @internal */
9
+ export declare function disposeGraph(built: BuiltGraph): void;
10
+ /** Resize the composer and its passes at the renderer's pixel ratio. @internal */
11
+ export declare function syncSize(built: BuiltGraph, width: number, height: number, pixelRatio: number): void;
3
12
  /**
4
13
  * Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
5
14
  * (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
6
- * post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
15
+ * post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Stylize → Grade. Rendered only
7
16
  * when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
8
17
  *
9
18
  * `stages` (resolved from the player's graphics profile) controls expensive passes
@@ -20,3 +29,4 @@ export declare function PostProcessing({ config, quality, stages }: {
20
29
  smaa: boolean;
21
30
  };
22
31
  }): null;
32
+ export {};