@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
@@ -0,0 +1,7 @@
1
+ import type { BackdropConfig, LightingConfig } from "@jgengine/core/game/playableGame";
2
+ export declare function ConfiguredLighting({ lighting }: {
3
+ lighting: LightingConfig;
4
+ }): import("react").JSX.Element;
5
+ export declare function BackdropFog({ fog }: {
6
+ fog: BackdropConfig["fog"];
7
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,18 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
3
+ function DirectionalShadowLight({ entry }) {
4
+ const size = entry.shadowCameraSize ?? 40;
5
+ return (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false, "shadow-mapSize-width": entry.shadowMapSize ?? 1024, "shadow-mapSize-height": entry.shadowMapSize ?? 1024, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": Math.max(200, size * 6), "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02 }));
6
+ }
7
+ export function ConfiguredLighting({ lighting }) {
8
+ return (_jsxs(_Fragment, { children: [lighting.ambient !== undefined ? (_jsx("ambientLight", { color: lighting.ambient.color, intensity: lighting.ambient.intensity ?? 0.55 })) : null, lighting.hemisphere !== undefined ? (_jsx("hemisphereLight", { args: [
9
+ lighting.hemisphere.skyColor ?? "#bfe3ff",
10
+ lighting.hemisphere.groundColor ?? "#4c6b34",
11
+ lighting.hemisphere.intensity ?? 0.55,
12
+ ] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry }, index)))] }));
13
+ }
14
+ export function BackdropFog({ fog }) {
15
+ if (fog === undefined)
16
+ return null;
17
+ return fog.density !== undefined ? (_jsx("fogExp2", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.density] })) : (_jsx("fog", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.near ?? 10, fog.far ?? 200] }));
18
+ }
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from "react";
2
+ import type { EntitySpriteConfig, ModelConfig } from "@jgengine/core/game/playableGame";
3
+ export declare function EntitySprite({ sprite }: {
4
+ sprite: EntitySpriteConfig;
5
+ }): import("react").JSX.Element;
6
+ export declare function IsolatedEntityModel({ model, instanceId, fallback, }: {
7
+ model: ModelConfig;
8
+ instanceId?: string;
9
+ fallback?: ReactNode;
10
+ }): import("react").JSX.Element;
11
+ export declare function EntityModel({ model, instanceId }: {
12
+ model: ModelConfig;
13
+ instanceId?: string;
14
+ }): import("react").JSX.Element;
@@ -0,0 +1,337 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame, useLoader } from "@react-three/fiber";
3
+ import { useTexture } from "@react-three/drei";
4
+ import { Component, Suspense, useEffect, useMemo, useRef } from "react";
5
+ import * as THREE from "three";
6
+ import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
7
+ import { useGameContext } from "@jgengine/react/provider";
8
+ import { sharedGltfLoader } from "./modelLoad.js";
9
+ import { applyMaterialOverride } from "../materialOverride.js";
10
+ import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, disposePaintCanvas, syncPaintCanvas, } from "./modelRender.js";
11
+ export function EntitySprite({ sprite }) {
12
+ const texture = useLoader(THREE.TextureLoader, sprite.url);
13
+ useEffect(() => {
14
+ texture.colorSpace = THREE.SRGBColorSpace;
15
+ texture.anisotropy = 4;
16
+ texture.needsUpdate = true;
17
+ }, [texture]);
18
+ return (_jsx("sprite", { "position-y": sprite.y, scale: [sprite.width, sprite.height, 1], children: _jsx("spriteMaterial", { map: texture, transparent: true, alphaTest: 0.08, depthWrite: false }) }));
19
+ }
20
+ class ModelFallbackBoundary extends Component {
21
+ state = { failed: false };
22
+ static getDerivedStateFromError() {
23
+ return { failed: true };
24
+ }
25
+ render() {
26
+ return this.state.failed ? this.props.fallback : this.props.children;
27
+ }
28
+ }
29
+ export function IsolatedEntityModel({ model, instanceId, fallback, }) {
30
+ return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId }) }) }));
31
+ }
32
+ function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
33
+ const gltf = useLoader(sharedGltfLoader, model.url);
34
+ const weaponScene = useMemo(() => cloneModelScene(gltf.scene), [gltf]);
35
+ const px = position?.[0] ?? 0;
36
+ const py = position?.[1] ?? 0;
37
+ const pz = position?.[2] ?? 0;
38
+ const rx = rotation?.[0] ?? 0;
39
+ const ry = rotation?.[1] ?? 0;
40
+ const rz = rotation?.[2] ?? 0;
41
+ const s = scale ?? 1;
42
+ useEffect(() => {
43
+ const bone = rig.getObjectByName(slot);
44
+ if (bone === undefined) {
45
+ if (typeof console !== "undefined") {
46
+ console.warn(`[jgengine] entityModels attachment: bone/slot "${slot}" not found on the rig`);
47
+ }
48
+ return;
49
+ }
50
+ weaponScene.position.set(px, py, pz);
51
+ weaponScene.rotation.set(rx, ry, rz);
52
+ weaponScene.scale.setScalar(s);
53
+ bone.add(weaponScene);
54
+ return () => {
55
+ bone.remove(weaponScene);
56
+ };
57
+ }, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
58
+ useEffect(() => () => disposeClonedMaterials(weaponScene), [weaponScene]);
59
+ return null;
60
+ }
61
+ /** Renders a static kit-of-parts child at a fixed local offset under the parent model — no bone/rig lookup, unlike `BoneAttachment`. The nested `EntityModel` still applies its own `dims`/anchor centering, so scale/pivot sanity carries over per part. */
62
+ function ModelPartGroup({ model, position, rotation, scale, }) {
63
+ return (_jsx("group", { position: position ?? [0, 0, 0], rotation: rotation ?? [0, 0, 0], scale: scale ?? 1, children: _jsx(EntityModel, { model: model }) }));
64
+ }
65
+ function ModelMaterialMapsApplier({ scene, maps }) {
66
+ const entries = useMemo(() => {
67
+ const record = {};
68
+ if (maps.color !== undefined)
69
+ record.color = maps.color;
70
+ if (maps.normal !== undefined)
71
+ record.normal = maps.normal;
72
+ if (maps.roughness !== undefined)
73
+ record.roughness = maps.roughness;
74
+ if (maps.ao !== undefined)
75
+ record.ao = maps.ao;
76
+ return record;
77
+ }, [maps.color, maps.normal, maps.roughness, maps.ao]);
78
+ const textures = useTexture(entries);
79
+ useEffect(() => {
80
+ if (textures.color !== undefined)
81
+ textures.color.colorSpace = THREE.SRGBColorSpace;
82
+ applyMaterialOverride(scene, {}, { clone: false, textures });
83
+ }, [scene, textures]);
84
+ return null;
85
+ }
86
+ export function EntityModel({ model, instanceId }) {
87
+ const gltf = useLoader(sharedGltfLoader, model.url);
88
+ const ctx = useGameContext();
89
+ const material = model.material;
90
+ const baseY = model.y ?? 0;
91
+ const dims = model.dims;
92
+ const scene = useMemo(() => {
93
+ const cloned = cloneModelScene(gltf.scene);
94
+ if (material !== undefined)
95
+ applyMaterialOverride(cloned, material, { clone: false });
96
+ return cloned;
97
+ }, [gltf, material]);
98
+ const measured = useMemo(() => {
99
+ if (model.targetHeight === undefined)
100
+ return null;
101
+ const box = new THREE.Box3().setFromObject(scene);
102
+ const height = box.max.y - box.min.y;
103
+ if (!Number.isFinite(height) || height <= 0)
104
+ return null;
105
+ return {
106
+ normalize: model.targetHeight / height,
107
+ minY: box.min.y,
108
+ centerX: (box.min.x + box.max.x) / 2,
109
+ centerZ: (box.min.z + box.max.z) / 2,
110
+ };
111
+ }, [scene, model.targetHeight]);
112
+ const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
113
+ const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
114
+ const position = measured !== null
115
+ ? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
116
+ : centered
117
+ ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
118
+ : [0, baseY, 0];
119
+ useEffect(() => () => {
120
+ disposeClonedMaterials(scene);
121
+ }, [scene]);
122
+ const animation = model.animation;
123
+ const mixerRef = useRef(null);
124
+ const animationPausedRef = useRef(false);
125
+ const stateActionsRef = useRef(null);
126
+ const states = animation?.states;
127
+ const oneShots = animation?.oneShots;
128
+ const oneShotPlayRef = useRef(null);
129
+ const activeOneShotRef = useRef(null);
130
+ useEffect(() => {
131
+ if (animation === undefined || gltf.animations.length === 0) {
132
+ mixerRef.current = null;
133
+ stateActionsRef.current = null;
134
+ return;
135
+ }
136
+ const mixer = new THREE.AnimationMixer(scene);
137
+ if (states !== undefined) {
138
+ const clipFor = (name) => THREE.AnimationClip.findByName(gltf.animations, name) ?? gltf.animations[0];
139
+ const actions = {
140
+ idle: mixer.clipAction(clipFor(states.idle)),
141
+ walk: mixer.clipAction(clipFor(states.walk)),
142
+ ...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
143
+ };
144
+ for (const action of Object.values(actions)) {
145
+ action.setLoop(THREE.LoopRepeat, Infinity);
146
+ action.timeScale = animation.timeScale ?? 1;
147
+ action.enabled = true;
148
+ }
149
+ actions.idle.play();
150
+ mixer.update(0);
151
+ mixerRef.current = mixer;
152
+ stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
153
+ animationPausedRef.current = false;
154
+ let onOneShotFinished = null;
155
+ if (oneShots !== undefined) {
156
+ const clipNames = new Set();
157
+ for (const spec of Object.values(oneShots)) {
158
+ if (typeof spec === "string")
159
+ clipNames.add(spec);
160
+ else
161
+ for (const name of spec)
162
+ clipNames.add(name);
163
+ }
164
+ const oneShotActions = new Map();
165
+ for (const name of clipNames) {
166
+ const found = THREE.AnimationClip.findByName(gltf.animations, name);
167
+ if (found === null)
168
+ continue;
169
+ const oneShotAction = mixer.clipAction(found);
170
+ oneShotAction.setLoop(THREE.LoopOnce, 1);
171
+ oneShotAction.enabled = true;
172
+ oneShotActions.set(name, oneShotAction);
173
+ }
174
+ onOneShotFinished = ({ action }) => {
175
+ const active = activeOneShotRef.current;
176
+ if (active === null || action !== active.action || active.isDeath)
177
+ return;
178
+ const machine = stateActionsRef.current;
179
+ const back = machine?.actions[machine.active];
180
+ action.fadeOut(0.15);
181
+ if (back !== undefined)
182
+ back.reset().fadeIn(0.15).play();
183
+ activeOneShotRef.current = null;
184
+ };
185
+ mixer.addEventListener("finished", onOneShotFinished);
186
+ oneShotPlayRef.current = (event) => {
187
+ const active = activeOneShotRef.current;
188
+ if (active !== null && active.isDeath)
189
+ return;
190
+ const clipName = resolveOneShotClip(oneShots, event, Math.random());
191
+ if (clipName === null)
192
+ return;
193
+ const oneShotAction = oneShotActions.get(clipName);
194
+ if (oneShotAction === undefined)
195
+ return;
196
+ const machine = stateActionsRef.current;
197
+ machine?.actions[machine.active]?.fadeOut(0.1);
198
+ if (active !== null && active.action !== oneShotAction)
199
+ active.action.stop();
200
+ oneShotAction.clampWhenFinished = event === "death";
201
+ oneShotAction.reset().fadeIn(0.1).play();
202
+ activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
203
+ };
204
+ }
205
+ return () => {
206
+ if (onOneShotFinished !== null)
207
+ mixer.removeEventListener("finished", onOneShotFinished);
208
+ oneShotPlayRef.current = null;
209
+ activeOneShotRef.current = null;
210
+ mixer.stopAllAction();
211
+ mixerRef.current = null;
212
+ stateActionsRef.current = null;
213
+ };
214
+ }
215
+ const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
216
+ gltf.animations[0];
217
+ const action = mixer.clipAction(clip);
218
+ action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
219
+ action.clampWhenFinished = animation.loop === false;
220
+ action.timeScale = animation.timeScale ?? 1;
221
+ action.enabled = true;
222
+ action.paused = animation.paused === true;
223
+ action.play();
224
+ if (animation.time !== undefined)
225
+ action.time = animation.time;
226
+ mixer.update(0);
227
+ mixerRef.current = mixer;
228
+ animationPausedRef.current = animation.paused === true;
229
+ return () => {
230
+ mixer.stopAllAction();
231
+ mixerRef.current = null;
232
+ };
233
+ }, [
234
+ scene,
235
+ gltf,
236
+ animation?.clip,
237
+ animation?.loop,
238
+ animation?.timeScale,
239
+ animation?.paused,
240
+ animation?.time,
241
+ states,
242
+ oneShots,
243
+ ]);
244
+ useEffect(() => {
245
+ if (instanceId === undefined || oneShots === undefined)
246
+ return;
247
+ const fire = (event) => oneShotPlayRef.current?.(event);
248
+ const offAnimation = ctx.game.events.on("entity.animation", (event) => {
249
+ if (event.instanceId === instanceId)
250
+ fire(event.event);
251
+ });
252
+ const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
253
+ if (event.instanceId === instanceId)
254
+ fire("hit");
255
+ });
256
+ const offDied = ctx.game.events.on("entity.died", (event) => {
257
+ if (event.instanceId === instanceId)
258
+ fire("death");
259
+ });
260
+ return () => {
261
+ offAnimation();
262
+ offHit();
263
+ offDied();
264
+ };
265
+ }, [ctx, instanceId, oneShots]);
266
+ const paintCanvasRef = useRef(null);
267
+ const paintDrawnCountRef = useRef(0);
268
+ const paintVersionRef = useRef(-1);
269
+ const materialCacheRef = useRef(null);
270
+ useEffect(() => () => {
271
+ if (paintCanvasRef.current !== null) {
272
+ disposePaintCanvas(paintCanvasRef.current);
273
+ paintCanvasRef.current = null;
274
+ }
275
+ paintDrawnCountRef.current = 0;
276
+ paintVersionRef.current = -1;
277
+ materialCacheRef.current = null;
278
+ }, [scene]);
279
+ useFrame((_state, delta) => {
280
+ const stateMachine = stateActionsRef.current;
281
+ if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
282
+ const entity = ctx.scene.entity.get(instanceId);
283
+ if (entity !== null) {
284
+ const [x, , z] = entity.position;
285
+ if (stateMachine.lastPos !== null) {
286
+ const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
287
+ stateMachine.smoothedSpeed +=
288
+ (instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
289
+ }
290
+ stateMachine.lastPos = [x, entity.position[1], z];
291
+ const walkSpeed = states.walkSpeed ?? 0.5;
292
+ const runSpeed = states.runSpeed ?? 6;
293
+ const next = stateMachine.smoothedSpeed < walkSpeed
294
+ ? "idle"
295
+ : stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
296
+ ? "run"
297
+ : "walk";
298
+ if (next !== stateMachine.active) {
299
+ if (activeOneShotRef.current === null) {
300
+ const fade = states.fadeSec ?? 0.2;
301
+ const from = stateMachine.actions[stateMachine.active];
302
+ const to = stateMachine.actions[next];
303
+ if (from !== undefined && to !== undefined) {
304
+ to.reset().fadeIn(fade).play();
305
+ from.fadeOut(fade);
306
+ }
307
+ }
308
+ stateMachine.active = next;
309
+ }
310
+ }
311
+ }
312
+ if (mixerRef.current !== null && !animationPausedRef.current)
313
+ mixerRef.current.update(delta);
314
+ if (instanceId === undefined)
315
+ return;
316
+ const paint = ctx.scene.entity.paint;
317
+ const version = paint.version(instanceId);
318
+ if (version === paintVersionRef.current)
319
+ return;
320
+ paintVersionRef.current = version;
321
+ const strokes = paint.strokes(instanceId);
322
+ const cache = cacheStandardMaterials(scene, materialCacheRef.current);
323
+ materialCacheRef.current = cache;
324
+ if (paintCanvasRef.current === null) {
325
+ if (strokes.length === 0)
326
+ return;
327
+ const seed = cache.materials[0];
328
+ if (seed === undefined)
329
+ return;
330
+ const paintCanvas = createPaintCanvas(seed);
331
+ paintCanvasRef.current = paintCanvas;
332
+ applyPaintTextureToMaterials(cache.materials, paintCanvas);
333
+ }
334
+ paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
335
+ });
336
+ return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), material?.maps !== undefined ? _jsx(ModelMaterialMapsApplier, { scene: scene, maps: material.maps }) : null, (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }, `${attachment.slot}-${index}`))), (model.parts ?? []).map((part, index) => typeof part.model === "string" ? null : (_jsx(ModelPartGroup, { model: part.model, position: part.position, rotation: part.rotation, scale: part.scale }, index)))] }));
337
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
3
+ * installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
4
+ * `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
5
+ * as the URL modifier on THREE loading managers so every model and texture
6
+ * load resolves correctly wherever the app is mounted.
7
+ */
8
+ export declare function resolveAssetBaseUrl(url: string): string;
9
+ /**
10
+ * Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
11
+ * asset paths load from under it. Call once at app startup, before any game
12
+ * loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
13
+ * default. Also registers {@link resolveAssetBaseUrl} on
14
+ * `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
15
+ * / `useGLTF`, and every other loader on the default manager.
16
+ */
17
+ export declare function installAssetBase(base: string): void;
@@ -0,0 +1,35 @@
1
+ import * as THREE from "three";
2
+ /**
3
+ * The app base URL every root-absolute asset path resolves under. `/` (the
4
+ * default) leaves URLs untouched — standalone game runners serve assets from
5
+ * the site root. The website serves the runner under `/play/`, where a game's
6
+ * `/models/…` or `/materials/…` must load from `/play/models/…`.
7
+ * @internal
8
+ */
9
+ let assetBase = "/";
10
+ /**
11
+ * Resolves a root-absolute asset URL (`/models/…`, `/materials/…`) against the
12
+ * installed app base. Relative, protocol-relative, absolute-scheme (`https:`,
13
+ * `blob:`, `data:`), and already-based URLs pass through unchanged. Installed
14
+ * as the URL modifier on THREE loading managers so every model and texture
15
+ * load resolves correctly wherever the app is mounted.
16
+ */
17
+ export function resolveAssetBaseUrl(url) {
18
+ if (assetBase === "/" || !url.startsWith("/") || url.startsWith("//") || url.startsWith(assetBase)) {
19
+ return url;
20
+ }
21
+ return `${assetBase.replace(/\/+$/, "")}${url}`;
22
+ }
23
+ /**
24
+ * Installs the app base URL (pass `import.meta.env.BASE_URL`) so root-absolute
25
+ * asset paths load from under it. Call once at app startup, before any game
26
+ * loads. Bases that are not root-absolute (`/`, `./`) reset to the pass-through
27
+ * default. Also registers {@link resolveAssetBaseUrl} on
28
+ * `THREE.DefaultLoadingManager`, covering `TextureLoader`, drei's `useTexture`
29
+ * / `useGLTF`, and every other loader on the default manager.
30
+ */
31
+ export function installAssetBase(base) {
32
+ const normalized = base.endsWith("/") ? base : `${base}/`;
33
+ assetBase = normalized.startsWith("/") && !normalized.startsWith("//") ? normalized : "/";
34
+ THREE.DefaultLoadingManager.setURLModifier(resolveAssetBaseUrl);
35
+ }
@@ -0,0 +1,29 @@
1
+ import { type ThreeElements } from "@react-three/fiber";
2
+ import * as THREE from "three";
3
+ /**
4
+ * Shared, lazily-built procedural detail maps (a subtle tangent-space normal map plus a
5
+ * roughness-variation map) used by every un-modeled primitive so a flat colored box reads as a
6
+ * surface, not plastic. Pure JS `DataTexture` — no canvas/DOM, so it imports safely under `bun test`.
7
+ * @internal shell-internal default render surface; games never import it.
8
+ */
9
+ export declare function detailMaps(): {
10
+ normal: THREE.DataTexture;
11
+ roughness: THREE.DataTexture;
12
+ };
13
+ /**
14
+ * Props for {@link DefaultSurface}: a required base `color` plus optional PBR overrides.
15
+ * @internal shell-internal default render surface; games never import it.
16
+ */
17
+ export interface DefaultSurfaceProps extends Omit<ThreeElements["meshStandardMaterial"], "args" | "normalMap" | "roughnessMap"> {
18
+ color: THREE.ColorRepresentation;
19
+ roughness?: number;
20
+ metalness?: number;
21
+ detail?: boolean;
22
+ }
23
+ /**
24
+ * The upgraded default material for un-modeled entities/objects: tuned PBR roughness/metalness plus
25
+ * the shared procedural detail maps, so primitive fallbacks catch light and image-based reflections
26
+ * instead of reading as flat plastic. Drop-in replacement for a bare `<meshStandardMaterial color>`.
27
+ * @internal shell-internal default render surface; games never import it.
28
+ */
29
+ export declare function DefaultSurface({ color, roughness, metalness, detail, ...rest }: DefaultSurfaceProps): import("react").JSX.Element;
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import {} from "@react-three/fiber";
3
+ import { useMemo } from "react";
4
+ import * as THREE from "three";
5
+ const DETAIL_SIZE = 64;
6
+ const DETAIL_REPEAT = 3;
7
+ const NORMAL_STRENGTH = 2.2;
8
+ const ROUGHNESS_SPREAD = 0.14;
9
+ function periodicValueNoise(size, cells, seed) {
10
+ const lattice = new Float32Array(cells * cells);
11
+ for (let index = 0; index < lattice.length; index += 1) {
12
+ const h = Math.sin((index + 1) * 12.9898 + seed * 78.233) * 43758.5453;
13
+ lattice[index] = h - Math.floor(h);
14
+ }
15
+ const at = (cx, cy) => lattice[((cy % cells) + cells) % cells * cells + (((cx % cells) + cells) % cells)];
16
+ const field = new Float32Array(size * size);
17
+ const step = cells / size;
18
+ for (let y = 0; y < size; y += 1) {
19
+ for (let x = 0; x < size; x += 1) {
20
+ const fx = x * step;
21
+ const fy = y * step;
22
+ const x0 = Math.floor(fx);
23
+ const y0 = Math.floor(fy);
24
+ const tx = fx - x0;
25
+ const ty = fy - y0;
26
+ const sx = tx * tx * (3 - 2 * tx);
27
+ const sy = ty * ty * (3 - 2 * ty);
28
+ const top = at(x0, y0) * (1 - sx) + at(x0 + 1, y0) * sx;
29
+ const bottom = at(x0, y0 + 1) * (1 - sx) + at(x0 + 1, y0 + 1) * sx;
30
+ field[y * size + x] = top * (1 - sy) + bottom * sy;
31
+ }
32
+ }
33
+ return field;
34
+ }
35
+ function heightField(size) {
36
+ const coarse = periodicValueNoise(size, 4, 1);
37
+ const medium = periodicValueNoise(size, 8, 7);
38
+ const fine = periodicValueNoise(size, 16, 13);
39
+ const field = new Float32Array(size * size);
40
+ for (let index = 0; index < field.length; index += 1) {
41
+ field[index] = coarse[index] * 0.55 + medium[index] * 0.3 + fine[index] * 0.15;
42
+ }
43
+ return field;
44
+ }
45
+ let cached = null;
46
+ /**
47
+ * Shared, lazily-built procedural detail maps (a subtle tangent-space normal map plus a
48
+ * roughness-variation map) used by every un-modeled primitive so a flat colored box reads as a
49
+ * surface, not plastic. Pure JS `DataTexture` — no canvas/DOM, so it imports safely under `bun test`.
50
+ * @internal shell-internal default render surface; games never import it.
51
+ */
52
+ export function detailMaps() {
53
+ if (cached !== null)
54
+ return cached;
55
+ const size = DETAIL_SIZE;
56
+ const height = heightField(size);
57
+ const sample = (x, y) => height[(((y % size) + size) % size) * size + (((x % size) + size) % size)];
58
+ const normalData = new Uint8Array(size * size * 4);
59
+ const roughnessData = new Uint8Array(size * size * 4);
60
+ for (let y = 0; y < size; y += 1) {
61
+ for (let x = 0; x < size; x += 1) {
62
+ const dx = (sample(x - 1, y) - sample(x + 1, y)) * NORMAL_STRENGTH;
63
+ const dy = (sample(x, y - 1) - sample(x, y + 1)) * NORMAL_STRENGTH;
64
+ const length = Math.hypot(dx, dy, 1);
65
+ const offset = (y * size + x) * 4;
66
+ normalData[offset] = Math.round((dx / length * 0.5 + 0.5) * 255);
67
+ normalData[offset + 1] = Math.round((dy / length * 0.5 + 0.5) * 255);
68
+ normalData[offset + 2] = Math.round((1 / length * 0.5 + 0.5) * 255);
69
+ normalData[offset + 3] = 255;
70
+ const rough = Math.round((0.5 + (sample(x, y) - 0.5) * 2 * ROUGHNESS_SPREAD * 0.5) * 255);
71
+ roughnessData[offset] = rough;
72
+ roughnessData[offset + 1] = rough;
73
+ roughnessData[offset + 2] = rough;
74
+ roughnessData[offset + 3] = 255;
75
+ }
76
+ }
77
+ const build = (data) => {
78
+ const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat);
79
+ texture.wrapS = THREE.RepeatWrapping;
80
+ texture.wrapT = THREE.RepeatWrapping;
81
+ texture.repeat.set(DETAIL_REPEAT, DETAIL_REPEAT);
82
+ texture.minFilter = THREE.LinearMipmapLinearFilter;
83
+ texture.magFilter = THREE.LinearFilter;
84
+ texture.generateMipmaps = true;
85
+ texture.anisotropy = 4;
86
+ texture.needsUpdate = true;
87
+ return texture;
88
+ };
89
+ cached = { normal: build(normalData), roughness: build(roughnessData) };
90
+ return cached;
91
+ }
92
+ /**
93
+ * The upgraded default material for un-modeled entities/objects: tuned PBR roughness/metalness plus
94
+ * the shared procedural detail maps, so primitive fallbacks catch light and image-based reflections
95
+ * instead of reading as flat plastic. Drop-in replacement for a bare `<meshStandardMaterial color>`.
96
+ * @internal shell-internal default render surface; games never import it.
97
+ */
98
+ export function DefaultSurface({ color, roughness = 0.72, metalness = 0.04, detail = true, ...rest }) {
99
+ const maps = useMemo(() => (detail ? detailMaps() : null), [detail]);
100
+ return (_jsx("meshStandardMaterial", { color: color, roughness: roughness, metalness: metalness, envMapIntensity: 0.6, normalMap: maps?.normal ?? null, normalScale: maps === null ? undefined : new THREE.Vector2(0.6, 0.6), roughnessMap: maps?.roughness ?? null, ...rest }));
101
+ }
@@ -0,0 +1,24 @@
1
+ import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
2
+ import { type AssetLoadDiagnosis } from "@jgengine/core/scene/assetDiagnostics";
3
+ /**
4
+ * Re-fetch a model URL that just failed to load and classify what actually came
5
+ * back — a 404, a dev-server HTML fallback, a corrupt or non-model body — so the
6
+ * runtime can replace an opaque GLTF parse error with a diagnosis naming the
7
+ * broken asset contract. Runs only on the error path (loads rarely fail), and
8
+ * never throws: a failed probe returns a `missing` diagnosis instead.
9
+ * @internal
10
+ */
11
+ export declare function probeModelUrl(url: string, logicalId?: string, fetchImpl?: typeof fetch): Promise<AssetLoadDiagnosis>;
12
+ /**
13
+ * A {@link GLTFLoader} whose success path is unchanged but whose failures are
14
+ * enriched: when a load errors, it probes the URL and rejects with an
15
+ * {@link AssetLoadDiagnosis}-derived message (missing / HTML fallback / corrupt /
16
+ * unsupported) instead of the raw parse error. Drop-in for the shared loader, so
17
+ * `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
18
+ * @internal
19
+ */
20
+ export declare class DiagnosticGLTFLoader extends GLTFLoader {
21
+ load(url: string, onLoad: (gltf: GLTF) => void, onProgress?: (event: ProgressEvent) => void, onError?: (event: unknown) => void): void;
22
+ }
23
+ /** Shared GLTF loader used by shell entity/object model mounts. @internal */
24
+ export declare const sharedGltfLoader: DiagnosticGLTFLoader;
@@ -0,0 +1,76 @@
1
+ import * as THREE from "three";
2
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
+ import { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
4
+ import { classifyAssetResponse, } from "@jgengine/core/scene/assetDiagnostics";
5
+ import { resolveAssetBaseUrl } from "./assetBase.js";
6
+ /**
7
+ * Dedicated LoadingManager for every GLB load instead of THREE.DefaultLoadingManager.
8
+ * The shared default manager is process-wide; under repeated dev-server navigations its
9
+ * AbortController can already be aborted, which silently stalls GLTFLoader.fetch forever
10
+ * with no thrown error. A private manager sidesteps that (ported from the duet-keys ship).
11
+ * @internal
12
+ */
13
+ const modelLoadingManager = new THREE.LoadingManager();
14
+ modelLoadingManager.setURLModifier(resolveAssetBaseUrl);
15
+ /** How many leading bytes to read when probing a failed model URL for its signature. @internal */
16
+ const PROBE_BYTE_LIMIT = 64;
17
+ /**
18
+ * Re-fetch a model URL that just failed to load and classify what actually came
19
+ * back — a 404, a dev-server HTML fallback, a corrupt or non-model body — so the
20
+ * runtime can replace an opaque GLTF parse error with a diagnosis naming the
21
+ * broken asset contract. Runs only on the error path (loads rarely fail), and
22
+ * never throws: a failed probe returns a `missing` diagnosis instead.
23
+ * @internal
24
+ */
25
+ export async function probeModelUrl(url, logicalId, fetchImpl = fetch) {
26
+ const base = logicalId === undefined ? { url } : { url, logicalId };
27
+ try {
28
+ const response = await fetchImpl(url);
29
+ const buffer = await response.arrayBuffer();
30
+ const bytes = new Uint8Array(buffer.slice(0, PROBE_BYTE_LIMIT));
31
+ return classifyAssetResponse({
32
+ ...base,
33
+ status: response.status,
34
+ statusText: response.statusText,
35
+ contentType: response.headers.get("content-type") ?? undefined,
36
+ bytes,
37
+ });
38
+ }
39
+ catch {
40
+ // The probe fetch itself failed (network/CORS) — the file is effectively unreachable.
41
+ return classifyAssetResponse({ ...base, status: 0 });
42
+ }
43
+ }
44
+ /**
45
+ * A {@link GLTFLoader} whose success path is unchanged but whose failures are
46
+ * enriched: when a load errors, it probes the URL and rejects with an
47
+ * {@link AssetLoadDiagnosis}-derived message (missing / HTML fallback / corrupt /
48
+ * unsupported) instead of the raw parse error. Drop-in for the shared loader, so
49
+ * `useLoader(sharedGltfLoader, url)` benefits with no call-site change.
50
+ * @internal
51
+ */
52
+ export class DiagnosticGLTFLoader extends GLTFLoader {
53
+ load(url, onLoad, onProgress, onError) {
54
+ super.load(url, onLoad, onProgress, (event) => {
55
+ if (onError === undefined)
56
+ return;
57
+ // Probe the URL the loader actually fetched (base-resolved), not the raw input.
58
+ void probeModelUrl(this.manager.resolveURL(url))
59
+ .then((diagnosis) => {
60
+ if (diagnosis.ok) {
61
+ // The bytes look fine on re-fetch — surface the original parse error unchanged.
62
+ onError(event);
63
+ return;
64
+ }
65
+ const enriched = new Error(`[jgengine] ${diagnosis.message}`);
66
+ enriched.diagnosis = diagnosis;
67
+ enriched.cause = event;
68
+ onError(enriched);
69
+ })
70
+ .catch(() => onError(event));
71
+ });
72
+ }
73
+ }
74
+ /** Shared GLTF loader used by shell entity/object model mounts. @internal */
75
+ export const sharedGltfLoader = new DiagnosticGLTFLoader(modelLoadingManager);
76
+ sharedGltfLoader.setMeshoptDecoder(MeshoptDecoder);