@jgengine/shell 0.19.0 → 0.22.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 (215) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +113 -15
  5. package/dist/Shell3dPresentation.d.ts +2 -2
  6. package/dist/Shell3dPresentation.js +49 -7
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +11 -1
  10. package/dist/ShellHudPresentation.js +20 -6
  11. package/dist/audio/audioEngine.d.ts +16 -0
  12. package/dist/audio/audioEngine.js +178 -89
  13. package/dist/audio/audioWire.js +2 -2
  14. package/dist/audio/synthEngine.d.ts +7 -1
  15. package/dist/audio/synthEngine.js +103 -38
  16. package/dist/camera/GameCameraRig.js +2 -1
  17. package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
  18. package/dist/camera/GameFirstPersonCamera.js +17 -13
  19. package/dist/camera/cameraRigs.js +8 -2
  20. package/dist/camera/chaseHeading.d.ts +9 -0
  21. package/dist/camera/chaseHeading.js +10 -0
  22. package/dist/camera/customMuzzle.d.ts +14 -0
  23. package/dist/camera/customMuzzle.js +68 -0
  24. package/dist/camera/index.d.ts +1 -0
  25. package/dist/camera/index.js +1 -0
  26. package/dist/camera/rigResolve.d.ts +3 -0
  27. package/dist/camera/rigResolve.js +11 -0
  28. package/dist/combat/ProjectileModels.d.ts +31 -0
  29. package/dist/combat/ProjectileModels.js +70 -0
  30. package/dist/commandSink.d.ts +1 -2
  31. package/dist/commandSink.js +3 -2
  32. package/dist/defineGame.d.ts +12 -5
  33. package/dist/defineGame.js +25 -10
  34. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  35. package/dist/devtools/DevtoolsOverlay.js +14 -168
  36. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  37. package/dist/devtools/DevtoolsRuntime.js +61 -0
  38. package/dist/devtools/agentBridge.js +1 -1
  39. package/dist/devtools/devtoolsReports.d.ts +68 -0
  40. package/dist/devtools/devtoolsReports.js +100 -0
  41. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  42. package/dist/devtools/latencyInstrumentation.js +11 -0
  43. package/dist/devtools/playtestProbe.d.ts +4 -0
  44. package/dist/devtools/playtestProbe.js +22 -0
  45. package/dist/drivers/FrameDriver.js +25 -21
  46. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  47. package/dist/drivers/HudOnlyDriver.js +32 -14
  48. package/dist/environment/EnvironmentScene.js +19 -15
  49. package/dist/environment/terrainGroundColor.d.ts +5 -4
  50. package/dist/environment/terrainGroundColor.js +19 -8
  51. package/dist/inputSink.d.ts +6 -6
  52. package/dist/inputSink.js +60 -21
  53. package/dist/lazyShell3dPresentation.d.ts +9 -0
  54. package/dist/lazyShell3dPresentation.js +24 -0
  55. package/dist/materialOverride.d.ts +42 -0
  56. package/dist/materialOverride.js +95 -36
  57. package/dist/multiplayer.d.ts +33 -3
  58. package/dist/multiplayer.js +105 -22
  59. package/dist/pointer/pointerService.d.ts +2 -0
  60. package/dist/pointer/pointerService.js +12 -2
  61. package/dist/postfx/PostProcessing.d.ts +10 -0
  62. package/dist/postfx/PostProcessing.js +9 -41
  63. package/dist/postfx/gradeShader.d.ts +1 -1
  64. package/dist/postfx/gradeShader.js +30 -2
  65. package/dist/postfx/shadowPrepass.d.ts +3 -0
  66. package/dist/postfx/shadowPrepass.js +18 -0
  67. package/dist/presentationRecovery.d.ts +75 -0
  68. package/dist/presentationRecovery.js +204 -0
  69. package/dist/presentationRecovery3d.d.ts +4 -0
  70. package/dist/presentationRecovery3d.js +32 -0
  71. package/dist/presentationResolve.d.ts +8 -1
  72. package/dist/presentationResolve.js +5 -2
  73. package/dist/render/CascadedShadows.d.ts +7 -2
  74. package/dist/render/CascadedShadows.js +98 -20
  75. package/dist/render/EnvironmentLighting.js +74 -35
  76. package/dist/render/HairCards.d.ts +7 -0
  77. package/dist/render/HairCards.js +23 -0
  78. package/dist/render/SceneModels.js +51 -77
  79. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  80. package/dist/render/authoredSurfaceMaterial.js +108 -0
  81. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  82. package/dist/render/materialAppearanceSignals.js +58 -0
  83. package/dist/render/materialAsset.d.ts +98 -0
  84. package/dist/render/materialAsset.js +454 -0
  85. package/dist/render/measureBounds.js +16 -3
  86. package/dist/render/measureCollisionMesh.js +8 -1
  87. package/dist/render/modelAssets.d.ts +8 -0
  88. package/dist/render/modelAssets.js +43 -0
  89. package/dist/render/modelBindPose.d.ts +5 -0
  90. package/dist/render/modelBindPose.js +35 -0
  91. package/dist/render/modelLoad.d.ts +3 -0
  92. package/dist/render/modelLoad.js +9 -0
  93. package/dist/render/modelNodeVisibility.d.ts +6 -0
  94. package/dist/render/modelNodeVisibility.js +18 -0
  95. package/dist/render/modelRender.d.ts +18 -1
  96. package/dist/render/modelRender.js +81 -3
  97. package/dist/render/projectedOverdraw.d.ts +35 -0
  98. package/dist/render/projectedOverdraw.js +204 -0
  99. package/dist/render/resolveModel.d.ts +12 -2
  100. package/dist/render/resolveModel.js +21 -4
  101. package/dist/render/rigNode.d.ts +26 -0
  102. package/dist/render/rigNode.js +32 -0
  103. package/dist/render/spriteRender.d.ts +4 -0
  104. package/dist/render/spriteRender.js +5 -0
  105. package/dist/render/staticShapeInstances.d.ts +34 -0
  106. package/dist/render/staticShapeInstances.js +78 -0
  107. package/dist/render/useFootIk.js +76 -11
  108. package/dist/render/useModelAnimation.d.ts +28 -2
  109. package/dist/render/useModelAnimation.js +194 -69
  110. package/dist/render/useModelInstance.d.ts +25 -0
  111. package/dist/render/useModelInstance.js +43 -0
  112. package/dist/scatter/scatterModels.d.ts +3 -2
  113. package/dist/scatter/scatterModels.js +15 -7
  114. package/dist/scene/AuthoredScene.d.ts +14 -6
  115. package/dist/scene/AuthoredScene.js +30 -9
  116. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  117. package/dist/scene/GrassFieldRenderer.js +20 -2
  118. package/dist/settings/SettingsMenu.js +11 -18
  119. package/dist/settings/SettingsRuntime.d.ts +5 -0
  120. package/dist/settings/SettingsRuntime.js +29 -2
  121. package/dist/shellControlSuspension.d.ts +18 -0
  122. package/dist/shellControlSuspension.js +28 -0
  123. package/dist/shellInputPublication.d.ts +28 -0
  124. package/dist/shellInputPublication.js +49 -0
  125. package/dist/shellPointerInput.d.ts +18 -0
  126. package/dist/shellPointerInput.js +49 -0
  127. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  128. package/dist/structures/BuildingKitBatch.js +34 -59
  129. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  130. package/dist/structures/BuildingRenderBudget.js +54 -0
  131. package/dist/structures/GeneratedBuilding.js +13 -8
  132. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  133. package/dist/structures/buildingKitMaterials.js +70 -0
  134. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  135. package/dist/structures/buildingSpatialBatch.js +45 -0
  136. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  137. package/dist/terrain/CarvedTerrain.js +2 -2
  138. package/dist/terrain/GrassField.d.ts +3 -1
  139. package/dist/terrain/GrassField.js +61 -6
  140. package/dist/terrain/grassGeometry.d.ts +13 -0
  141. package/dist/terrain/grassGeometry.js +57 -1
  142. package/dist/terrain/grassMaterial.js +2 -2
  143. package/dist/terrain/index.d.ts +1 -0
  144. package/dist/terrain/index.js +1 -0
  145. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  146. package/dist/terrain/terrainDetailMaterial.js +7 -7
  147. package/dist/terrain/terrainMath.d.ts +2 -0
  148. package/dist/terrain/terrainMath.js +2 -0
  149. package/dist/terrain/terrainPaintColor.d.ts +11 -0
  150. package/dist/terrain/terrainPaintColor.js +47 -0
  151. package/dist/touch/TouchControlsOverlay.js +4 -4
  152. package/dist/useShellMultiplayerSync.js +4 -0
  153. package/dist/vfx/ParticleField.d.ts +23 -20
  154. package/dist/vfx/ParticleField.js +168 -50
  155. package/dist/visibility/CullingProvider.js +2 -62
  156. package/dist/visibility/cullingDriver.d.ts +9 -0
  157. package/dist/visibility/cullingDriver.js +68 -0
  158. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  159. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  160. package/dist/weather/DustField.d.ts +2 -1
  161. package/dist/weather/DustField.js +17 -3
  162. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  163. package/dist/weather/FireSpreadLayer.js +5 -11
  164. package/dist/weather/LightningStrike.d.ts +4 -1
  165. package/dist/weather/LightningStrike.js +13 -17
  166. package/dist/weather/RainField.d.ts +2 -1
  167. package/dist/weather/RainField.js +17 -3
  168. package/dist/weather/RainImpactField.d.ts +16 -0
  169. package/dist/weather/RainImpactField.js +89 -0
  170. package/dist/weather/SnowField.d.ts +2 -1
  171. package/dist/weather/SnowField.js +17 -3
  172. package/dist/weather/WeatherLayer.d.ts +11 -2
  173. package/dist/weather/WeatherLayer.js +30 -6
  174. package/dist/weather/index.d.ts +2 -0
  175. package/dist/weather/index.js +2 -0
  176. package/dist/weather/weatherMath.d.ts +2 -0
  177. package/dist/weather/weatherMath.js +8 -0
  178. package/dist/weather/weatherShelter.d.ts +18 -0
  179. package/dist/weather/weatherShelter.js +75 -0
  180. package/dist/weather/weatherUniforms.d.ts +40 -1
  181. package/dist/weather/weatherUniforms.js +29 -9
  182. package/dist/world/AuthoredFlocks.d.ts +22 -0
  183. package/dist/world/AuthoredFlocks.js +87 -0
  184. package/dist/world/StaticObjectModels.d.ts +8 -0
  185. package/dist/world/StaticObjectModels.js +75 -0
  186. package/dist/world/WorldHud.d.ts +14 -6
  187. package/dist/world/WorldHud.js +25 -12
  188. package/dist/world/WorldParticles.d.ts +10 -23
  189. package/dist/world/WorldParticles.js +209 -61
  190. package/dist/world/WorldScene.d.ts +3 -1
  191. package/dist/world/WorldScene.js +41 -26
  192. package/dist/world/staticObjectBatches.d.ts +35 -0
  193. package/dist/world/staticObjectBatches.js +163 -0
  194. package/dist/world/worldBarSamples.d.ts +37 -33
  195. package/dist/world/worldBarSamples.js +117 -75
  196. package/package.json +4 -4
  197. package/skills/ce-handoff/SKILL.md +1 -1
  198. package/skills/game-design/SKILL.md +2 -0
  199. package/skills/jgengine/SKILL.md +23 -2
  200. package/skills/jgengine/api.md +28 -17
  201. package/skills/jgengine/recipes/minimal-game.md +7 -2
  202. package/skills/jgengine-ui/SKILL.md +3 -1
  203. package/skills/jgengine-ui/api.md +219 -70
  204. package/skills/jgengine-ui/capabilities.md +119 -1
  205. package/skills/jgengine-ui/reference-primitives.md +93 -3
  206. package/skills/jgengine-ui/reference.md +2 -0
  207. package/skills/jgengine-world/SKILL.md +12 -3
  208. package/skills/jgengine-world/api.md +201 -58
  209. package/skills/jgengine-world/capabilities.md +116 -3
  210. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  211. package/skills/jgengine-world/recipes/controls.md +4 -0
  212. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  213. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  214. package/skills/jgengine-world/reference.md +167 -5
  215. package/skills/jgengine-world/visibility.md +28 -1
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Resolves a bone or attachment slot by its exact runtime name first, then by the original
3
+ * imported name preserved as `userData.name`. GLTFLoader sanitizes names such as `handslot.r`
4
+ * to `handslotr`; authored references remain usable without renaming the animated hierarchy.
5
+ * Both lookups require a unique target. Call on model load or slot changes, not each frame,
6
+ * and keep the result with that model instance rather than reusing a cached asset's node.
7
+ *
8
+ * @capability imported-rig-node resolve original imported bone and slot names with ambiguity diagnostics
9
+ */
10
+ export function resolveRigNode(rig, name) {
11
+ const exact = [];
12
+ const authored = [];
13
+ rig.traverse((node) => {
14
+ if (node.name === name)
15
+ exact.push(node);
16
+ if (node.userData.name === name)
17
+ authored.push(node);
18
+ });
19
+ const matches = exact.length > 0 ? exact : authored;
20
+ const matchedBy = exact.length > 0 ? "runtime" : "authored";
21
+ if (matches.length === 1)
22
+ return { node: matches[0], matchedBy };
23
+ if (matches.length > 1)
24
+ return {
25
+ node: undefined,
26
+ diagnostic: { code: "ambiguous-node", message: `bone/slot "${name}" matches ${matches.length} ${matchedBy} names on this rig. Choose a unique runtime node name or repair duplicate imported names; no node was selected.` },
27
+ };
28
+ return {
29
+ node: undefined,
30
+ diagnostic: { code: "missing-node", message: `bone/slot "${name}" was not found by runtime or original imported name on this rig. Check the model's node names and configure a supported slot.` },
31
+ };
32
+ }
@@ -0,0 +1,4 @@
1
+ import type * as THREE from "three";
2
+ import type { SpriteAtlas } from "@jgengine/core/assets/spriteAtlas";
3
+ /** Update atlas UVs without invalidating the shared image upload. @internal */
4
+ export declare function syncSpriteFrame(texture: THREE.Texture, frame: SpriteAtlas["frames"][string], size: readonly [number, number]): void;
@@ -0,0 +1,5 @@
1
+ /** Update atlas UVs without invalidating the shared image upload. @internal */
2
+ export function syncSpriteFrame(texture, frame, size) {
3
+ texture.repeat.set(frame.w / size[0], frame.h / size[1]);
4
+ texture.offset.set(frame.x / size[0], 1 - (frame.y + frame.h) / size[1]);
5
+ }
@@ -0,0 +1,34 @@
1
+ import * as THREE from "three";
2
+ import type { ModelShadowMode } from "./modelRender.js";
3
+ import { type AuthoredSurfaceConfig, type SurfaceShape } from "./authoredSurfaceMaterial.js";
4
+ /** One editor-derived static primitive. A box spans one unit; a cylinder has unit radius and height before scale. */
5
+ export interface StaticShapeInstance {
6
+ shape: SurfaceShape;
7
+ position: readonly [number, number, number];
8
+ rotation?: readonly [number, number, number];
9
+ scale: readonly [number, number, number];
10
+ surface: AuthoredSurfaceConfig;
11
+ shadows?: ModelShadowMode;
12
+ /** Cylinder tessellation; default 16. */
13
+ radialSegments?: number;
14
+ }
15
+ /** A compatible static draw group. @internal */
16
+ export interface StaticShapeBatch {
17
+ key: string;
18
+ shape: SurfaceShape;
19
+ radialSegments: number;
20
+ surface: AuthoredSurfaceConfig;
21
+ shadows: ModelShadowMode;
22
+ instances: readonly StaticShapeInstance[];
23
+ }
24
+ /** Plan compatible material/shape/shadow groups without changing authored transforms. @internal */
25
+ export declare function groupStaticShapeInstances(instances: readonly StaticShapeInstance[]): StaticShapeBatch[];
26
+ /** Upload static transforms and refresh conservative aggregate bounds after a prop change. @internal */
27
+ export declare function writeStaticShapeInstances(mesh: THREE.InstancedMesh, instances: readonly StaticShapeInstance[]): void;
28
+ /**
29
+ * Batch editor-derived boxes/cylinders by shape, PBR metadata and shadows. Uploads and bounds run only when instances change; map loading stays isolated per draw group.
30
+ * @capability static-shape-instances render authored primitive transforms as static material-compatible instance batches
31
+ */
32
+ export declare function StaticShapeInstances({ instances }: {
33
+ instances: readonly StaticShapeInstance[];
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,78 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense, useLayoutEffect, useMemo, useRef } from "react";
3
+ import * as THREE from "three";
4
+ import { AuthoredSurfaceMaterial, authoredSurfaceKey } from "./authoredSurfaceMaterial.js";
5
+ import { ModelFallbackBoundary } from "./SceneModels.js";
6
+ import { useDisposable } from "./useDisposable.js";
7
+ /** Plan compatible material/shape/shadow groups without changing authored transforms. @internal */
8
+ export function groupStaticShapeInstances(instances) {
9
+ const groups = new Map();
10
+ for (const instance of instances) {
11
+ if (![...instance.position, ...instance.scale, ...(instance.rotation ?? [])].every(Number.isFinite) || instance.scale.some(value => value <= 0))
12
+ throw new Error("Static shape transforms must be finite with positive scale");
13
+ if ([instance.surface.displacementScale, instance.surface.displacementBias].some(value => value !== undefined && !Number.isFinite(value)))
14
+ throw new Error("Static shape displacement must be finite");
15
+ const radialSegments = instance.shape === "cylinder" ? instance.radialSegments ?? 16 : 0;
16
+ if (instance.shape === "cylinder" && (!Number.isInteger(radialSegments) || radialSegments < 3))
17
+ throw new Error("Cylinder radialSegments must be an integer >= 3");
18
+ const shadows = instance.shadows ?? "both";
19
+ const key = JSON.stringify([instance.shape, radialSegments, shadows, authoredSurfaceKey(instance.surface)]);
20
+ let group = groups.get(key);
21
+ if (group === undefined) {
22
+ group = { key, shape: instance.shape, radialSegments, surface: instance.surface, shadows, instances: [] };
23
+ groups.set(key, group);
24
+ }
25
+ group.instances.push(instance);
26
+ }
27
+ return [...groups.values()];
28
+ }
29
+ /** Upload static transforms and refresh conservative aggregate bounds after a prop change. @internal */
30
+ export function writeStaticShapeInstances(mesh, instances) {
31
+ const transform = new THREE.Object3D();
32
+ instances.forEach((instance, index) => {
33
+ transform.position.set(...instance.position);
34
+ transform.rotation.set(...(instance.rotation ?? [0, 0, 0]));
35
+ transform.scale.set(...instance.scale);
36
+ transform.updateMatrix();
37
+ mesh.setMatrixAt(index, transform.matrix);
38
+ });
39
+ mesh.count = instances.length;
40
+ if (mesh.geometry.boundingBox === null)
41
+ mesh.geometry.computeBoundingBox();
42
+ const heightSurface = instances[0]?.surface;
43
+ const displacedBounds = mesh.geometry.boundingBox.clone();
44
+ if (heightSurface?.maps?.height !== undefined) {
45
+ const bias = heightSurface.displacementBias ?? 0;
46
+ const scale = heightSurface.displacementScale ?? 1;
47
+ const displacement = Math.max(Math.abs(bias), Math.abs(bias + scale));
48
+ displacedBounds.expandByScalar(displacement + Number.EPSILON * Math.max(1, displacement) * 8);
49
+ }
50
+ mesh.instanceMatrix.needsUpdate = true;
51
+ mesh.boundingBox = new THREE.Box3();
52
+ const matrix = new THREE.Matrix4(), bounds = new THREE.Box3();
53
+ for (let index = 0; index < instances.length; index++) {
54
+ mesh.getMatrixAt(index, matrix);
55
+ mesh.boundingBox.union(bounds.copy(displacedBounds).applyMatrix4(matrix));
56
+ }
57
+ mesh.boundingSphere = mesh.boundingBox.getBoundingSphere(new THREE.Sphere());
58
+ // Three scales spheres by the longest matrix column. sqrt(3) also encloses affine parent shear.
59
+ mesh.boundingSphere.radius *= Math.sqrt(3);
60
+ }
61
+ function ShapeBatch({ batch }) {
62
+ const ref = useRef(null);
63
+ const geometry = useDisposable(() => batch.shape === "box" ? new THREE.BoxGeometry(1, 1, 1) : new THREE.CylinderGeometry(1, 1, 1, batch.radialSegments), [batch.shape, batch.radialSegments]);
64
+ useLayoutEffect(() => {
65
+ if (ref.current !== null)
66
+ writeStaticShapeInstances(ref.current, batch.instances);
67
+ }, [batch.instances]);
68
+ const fallback = _jsx(AuthoredSurfaceMaterial, { surface: { ...batch.surface, maps: undefined }, shape: batch.shape });
69
+ return _jsx("instancedMesh", { ref: ref, args: [geometry, undefined, batch.instances.length], castShadow: batch.shadows === "both" || batch.shadows === "cast", receiveShadow: batch.shadows === "both" || batch.shadows === "receive", children: _jsx(ModelFallbackBoundary, { fallback: fallback, url: Object.values(batch.surface.maps ?? {}).join(","), seam: "object", children: _jsx(Suspense, { fallback: fallback, children: _jsx(AuthoredSurfaceMaterial, { surface: batch.surface, shape: batch.shape }) }) }) });
70
+ }
71
+ /**
72
+ * Batch editor-derived boxes/cylinders by shape, PBR metadata and shadows. Uploads and bounds run only when instances change; map loading stays isolated per draw group.
73
+ * @capability static-shape-instances render authored primitive transforms as static material-compatible instance batches
74
+ */
75
+ export function StaticShapeInstances({ instances }) {
76
+ const batches = useMemo(() => groupStaticShapeInstances(instances), [instances]);
77
+ return _jsx(_Fragment, { children: batches.map(batch => _jsx(ShapeBatch, { batch: batch }, batch.key)) });
78
+ }
@@ -1,8 +1,9 @@
1
1
  import { useFrame, useThree } from "@react-three/fiber";
2
- import { useMemo, useRef } from "react";
2
+ import { useLayoutEffect, useMemo, useRef } from "react";
3
3
  import * as THREE from "three";
4
4
  import { inferLegChains, placeFeet } from "@jgengine/core/anim/footPlacement";
5
5
  import { lookAt, solveTwoBone } from "@jgengine/core/anim/ikSolver";
6
+ import { resolveRigNode } from "./rigNode.js";
6
7
  const DEFAULT_MAX_ADJUST = 0.4;
7
8
  const UP = new THREE.Vector3(0, 1, 0);
8
9
  const scratch = {
@@ -45,12 +46,45 @@ function restAnkleRatio(scene, leg) {
45
46
  });
46
47
  return ratio;
47
48
  }
49
+ function isAncestor(ancestor, child) {
50
+ for (let parent = child.parent; parent !== null; parent = parent.parent) {
51
+ if (parent === ancestor)
52
+ return true;
53
+ }
54
+ return false;
55
+ }
48
56
  /**
49
57
  * Resolves foot-IK bones on a loaded rig: explicit chains, or legs found by bone name for `"auto"`
50
58
  * and configs without `feet`. Returns `null` when no leg resolves.
51
59
  */
52
60
  export function resolveFootIkRig(scene, config) {
53
61
  const options = config === "auto" ? {} : config;
62
+ const named = new Map();
63
+ scene.traverse((object) => {
64
+ if (object.name.length === 0)
65
+ return;
66
+ const matches = named.get(object.name);
67
+ if (matches === undefined)
68
+ named.set(object.name, [object]);
69
+ else
70
+ matches.push(object);
71
+ });
72
+ const available = [...named.keys()].filter((name) => named.get(name).some((object) => object.isBone === true)).slice(0, 12).join(", ");
73
+ const warn = (message) => console.warn(`[jgengine foot IK] ${message}${available.length > 0 ? ` Available bones include: ${available}.` : ""}`);
74
+ const resolve = (name, role) => {
75
+ const matches = named.get(name);
76
+ if (matches?.length === 1)
77
+ return matches[0];
78
+ if (matches === undefined) {
79
+ const imported = resolveRigNode(scene, name);
80
+ if (imported.node !== undefined)
81
+ return imported.node;
82
+ warn(`${role} "${name}" ${imported.diagnostic.code === "ambiguous-node" ? "matches multiple original imported names; use a unique runtime name or repair duplicate imported names" : "was not found by runtime or original imported name; check the loaded rig names"}.`);
83
+ return null;
84
+ }
85
+ warn(`${role} "${name}" matches multiple objects; use a unique rig name.`);
86
+ return null;
87
+ };
54
88
  let chains = options.feet;
55
89
  if (chains === undefined) {
56
90
  const bones = [];
@@ -59,20 +93,37 @@ export function resolveFootIkRig(scene, config) {
59
93
  bones.push({ name: object.name, parent: object.parent?.isBone === true ? object.parent.name : null });
60
94
  });
61
95
  chains = inferLegChains(bones);
96
+ if (chains.length === 0 && options.lookAt === undefined)
97
+ warn("No thigh → shin → foot chain was recognized. Configure feet with the rig's bone names, or disable IK for this model.");
62
98
  }
63
99
  const legs = [];
100
+ const used = new Set();
64
101
  for (const chain of chains) {
65
- const root = scene.getObjectByName(chain.root);
66
- const mid = scene.getObjectByName(chain.mid);
67
- const tip = scene.getObjectByName(chain.tip);
68
- if (root === undefined || mid === undefined || tip === undefined)
102
+ const root = resolve(chain.root, "Foot-chain root");
103
+ const mid = resolve(chain.mid, "Foot-chain middle");
104
+ const tip = resolve(chain.tip, "Foot-chain tip");
105
+ if (root === null || mid === null || tip === null)
106
+ continue;
107
+ if (!isAncestor(root, mid) || !isAncestor(mid, tip)) {
108
+ warn(`Skipped chain "${chain.root}" → "${chain.mid}" → "${chain.tip}": each joint must descend from the previous joint.`);
69
109
  continue;
110
+ }
111
+ if ([root, mid, tip].some((bone) => used.has(bone))) {
112
+ warn(`Skipped chain "${chain.root}" → "${chain.mid}" → "${chain.tip}": a joint is already corrected by another foot chain.`);
113
+ continue;
114
+ }
115
+ for (const bone of [root, mid, tip])
116
+ used.add(bone);
70
117
  legs.push({ root, mid, tip, ankleRatio: restAnkleRatio(scene, { root, mid, tip }) });
71
118
  }
72
- const head = options.lookAt === undefined ? null : (scene.getObjectByName(options.lookAt.bone) ?? null);
119
+ const head = options.lookAt === undefined ? null : resolve(options.lookAt.bone, "Look-at bone");
73
120
  if (legs.length === 0 && head === null)
74
121
  return null;
75
- const found = options.pelvis !== undefined ? (scene.getObjectByName(options.pelvis) ?? null) : (legs[0]?.root.parent ?? null);
122
+ let found = options.pelvis !== undefined ? resolve(options.pelvis, "Pelvis") : (legs[0]?.root.parent ?? null);
123
+ if (options.pelvis === undefined) {
124
+ while (found !== null && !legs.every((leg) => isAncestor(found, leg.root)))
125
+ found = found.parent;
126
+ }
76
127
  const pelvis = found === scene ? null : found;
77
128
  const bones = new Set(legs.flatMap((leg) => [leg.root, leg.mid, leg.tip]));
78
129
  if (pelvis !== null)
@@ -113,6 +164,14 @@ function legLength(leg) {
113
164
  leg.tip.getWorldPosition(scratch.tip);
114
165
  return scratch.root.distanceTo(scratch.mid) + scratch.mid.distanceTo(scratch.tip);
115
166
  }
167
+ function restoreFootIkPose(rig) {
168
+ for (const record of rig.poses) {
169
+ if (record.bone.quaternion.equals(record.written.q))
170
+ record.bone.quaternion.copy(record.animated.q);
171
+ if (record.bone.position.equals(record.written.p))
172
+ record.bone.position.copy(record.animated.p);
173
+ }
174
+ }
116
175
  /**
117
176
  * Applies one frame of foot IK after the animation mixer: probes the ground under each foot,
118
177
  * resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending
@@ -120,11 +179,8 @@ function legLength(leg) {
120
179
  * grounded; `state` carries the faded weight between frames. Exported for tests and custom hosts.
121
180
  */
122
181
  export function applyFootIk(rig, originY, probe, state, delta, cameraTarget) {
182
+ restoreFootIkPose(rig);
123
183
  for (const record of rig.poses) {
124
- if (record.bone.quaternion.equals(record.written.q))
125
- record.bone.quaternion.copy(record.animated.q);
126
- if (record.bone.position.equals(record.written.p))
127
- record.bone.position.copy(record.animated.p);
128
184
  record.animated.q.copy(record.bone.quaternion);
129
185
  record.animated.p.copy(record.bone.position);
130
186
  }
@@ -200,6 +256,15 @@ export function useFootIk(scene, config, ctx, instanceId, groundOffset = 0) {
200
256
  const camera = useThree((three) => three.camera);
201
257
  const rig = useMemo(() => (config === undefined ? null : resolveFootIkRig(scene, config)), [scene, config]);
202
258
  const state = useRef({ weight: 0, pelvis: 0 });
259
+ useLayoutEffect(() => {
260
+ state.current = { weight: 0, pelvis: 0 };
261
+ return () => {
262
+ if (rig !== null) {
263
+ restoreFootIkPose(rig);
264
+ scene.updateMatrixWorld(true);
265
+ }
266
+ };
267
+ }, [rig, scene]);
203
268
  useFrame((_three, delta) => {
204
269
  if (rig === null || ctx === null || instanceId === undefined)
205
270
  return;
@@ -1,6 +1,26 @@
1
1
  import * as THREE from "three";
2
2
  import type { ModelAnimationConfig } from "@jgengine/core/game/playableGame";
3
- import { type AnimClipOutput, type AnimGraph } from "@jgengine/core/anim/animGraph";
3
+ import { type AnimClipOutput, type AnimGraph, type AnimParamValue } from "@jgengine/core/anim/animGraph";
4
+ import type { GameContext } from "@jgengine/core/runtime/gameContextTypes";
5
+ /** A repairable mismatch between a model animation config and its imported rig. */
6
+ export interface ModelAnimationDiagnostic {
7
+ code: "missing-clip" | "missing-root-track" | "empty-layer-mask" | "incomplete-locomotion" | "missing-auto-idle";
8
+ message: string;
9
+ }
10
+ /**
11
+ * Checks clip mappings, masked layers and root-motion assumptions without starting a mixer.
12
+ *
13
+ * @capability model-animation-diagnostics inspect imported clip mappings, layer masks and root-track compatibility
14
+ */
15
+ export declare function diagnoseModelAnimation(scene: THREE.Object3D, animation: ModelAnimationConfig, clips: readonly THREE.AnimationClip[]): readonly ModelAnimationDiagnostic[];
16
+ /**
17
+ * Refreshes a caller-owned graph parameter dictionary from authored extras and known physical movement.
18
+ * Grounded, verticalSpeed and crouched use the shared motor's resolved state when available; custom movers
19
+ * retain their authored values. Speed always uses the supplied measured ground speed. Does not write game state.
20
+ *
21
+ * @capability animation-motion-params read resolved player movement into reusable animation graph parameters
22
+ */
23
+ export declare function readModelAnimationParams(ctx: GameContext, instanceId: string, speed: number, out: Record<string, AnimParamValue>): Record<string, AnimParamValue>;
4
24
  /**
5
25
  * For a frame where a `rootMotion` state is current: pins the root bone's horizontal translation to
6
26
  * its bind pose, so the clip plays in place, and returns that step's root travel as a world-space
@@ -13,9 +33,13 @@ export interface GraphPose {
13
33
  /** Clip durations read from the rig, the `clips` argument `runtime.advance` expects. */
14
34
  durations: Readonly<Record<string, {
15
35
  duration: number;
36
+ rootTrack?: {
37
+ times: Float32Array;
38
+ values: Float32Array;
39
+ };
16
40
  }>>;
17
41
  /** Poses the rig with one advance's clip weights and times. */
18
- apply(clips: readonly AnimClipOutput[]): void;
42
+ apply(clips: readonly AnimClipOutput[], rootMotion?: boolean): void;
19
43
  dispose(): void;
20
44
  }
21
45
  /**
@@ -32,5 +56,7 @@ export declare function createGraphPose(scene: THREE.Object3D, graph: AnimGraph,
32
56
  * `entity.animation` / `combat.hitReaction` / `entity.died`, held poses, and the death clamp.
33
57
  * With `animation.graph` set, the headless `AnimGraph` runtime owns every clip's time and weight
34
58
  * and the mixer only applies them; clip events surface as `animation.event`.
59
+ * Equivalent same-order config data retains playback across rerenders; actual data edits reset it.
60
+ * Entity-bound playback holds while context game time is frozen; unbound previews keep playing.
35
61
  */
36
62
  export declare function useModelAnimation(scene: THREE.Object3D, clips: THREE.AnimationClip[], animationInput: ModelAnimationConfig | "auto" | "none" | undefined, instanceId?: string): void;