@jgengine/core 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 (266) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/ai/decisionGraph.d.ts +2 -0
  3. package/dist/ai/decisionGraph.js +6 -0
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/anim/animGraph.d.ts +33 -2
  7. package/dist/anim/animGraph.js +200 -12
  8. package/dist/anim/locomotionGraph.d.ts +11 -3
  9. package/dist/anim/locomotionGraph.js +25 -15
  10. package/dist/audio/audioFalloff.d.ts +4 -0
  11. package/dist/audio/voiceAllocator.d.ts +46 -0
  12. package/dist/audio/voiceAllocator.js +76 -0
  13. package/dist/combat/abilityKit.d.ts +19 -0
  14. package/dist/combat/abilityKit.js +40 -4
  15. package/dist/combat/death.d.ts +4 -3
  16. package/dist/combat/death.js +3 -3
  17. package/dist/combat/magazine.d.ts +9 -0
  18. package/dist/combat/magazine.js +24 -2
  19. package/dist/combat/projectiles.d.ts +94 -2
  20. package/dist/combat/projectiles.js +282 -61
  21. package/dist/combat/regenShield.d.ts +14 -0
  22. package/dist/combat/regenShield.js +23 -7
  23. package/dist/crafting/recipeCatalog.d.ts +16 -0
  24. package/dist/crafting/recipeCatalog.js +55 -0
  25. package/dist/economy/auctionBook.d.ts +1 -1
  26. package/dist/economy/auctionBook.js +11 -1
  27. package/dist/economy/listingBook.d.ts +1 -1
  28. package/dist/economy/listingBook.js +11 -1
  29. package/dist/economy/resourceLedger.js +3 -3
  30. package/dist/economy/shopStock.js +7 -6
  31. package/dist/economy/techTree.js +7 -6
  32. package/dist/economy/unlockPoints.js +3 -6
  33. package/dist/editor/commandApply.js +89 -16
  34. package/dist/editor/commands.d.ts +38 -1
  35. package/dist/editor/commands.js +320 -3
  36. package/dist/editor/creatorStorage.d.ts +59 -0
  37. package/dist/editor/creatorStorage.js +164 -0
  38. package/dist/editor/document.d.ts +4 -0
  39. package/dist/editor/document.js +116 -17
  40. package/dist/editor/index.d.ts +3 -0
  41. package/dist/editor/index.js +1 -0
  42. package/dist/editor/liveSync.d.ts +4 -1
  43. package/dist/editor/liveSync.js +26 -10
  44. package/dist/editor/materialAuthoring.d.ts +14 -0
  45. package/dist/editor/materialAuthoring.js +38 -0
  46. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  47. package/dist/editor/modelNodeVisibility.js +29 -0
  48. package/dist/editor/movementCatalog.d.ts +90 -0
  49. package/dist/editor/movementCatalog.js +144 -0
  50. package/dist/editor/simulation.d.ts +60 -0
  51. package/dist/editor/simulation.js +202 -0
  52. package/dist/editor/staticPrefab.d.ts +17 -0
  53. package/dist/editor/staticPrefab.js +51 -0
  54. package/dist/editor/terrainMaterials.d.ts +10 -0
  55. package/dist/editor/terrainMaterials.js +13 -0
  56. package/dist/editor/types.d.ts +11 -0
  57. package/dist/game/cameraConfig.d.ts +3 -1
  58. package/dist/game/clipRoles.d.ts +2 -1
  59. package/dist/game/clipRoles.js +7 -3
  60. package/dist/game/contentValidation.d.ts +69 -0
  61. package/dist/game/contentValidation.js +118 -0
  62. package/dist/game/controlGate.d.ts +8 -0
  63. package/dist/game/controlGate.js +43 -6
  64. package/dist/game/defineGame.d.ts +32 -3
  65. package/dist/game/dialogueGraph.d.ts +20 -0
  66. package/dist/game/dialogueGraph.js +107 -0
  67. package/dist/game/events.d.ts +5 -0
  68. package/dist/game/keyValueStore.d.ts +17 -4
  69. package/dist/game/keyValueStore.js +35 -23
  70. package/dist/game/lootModifiers.d.ts +17 -0
  71. package/dist/game/lootModifiers.js +26 -0
  72. package/dist/game/narrativeAuthoring.d.ts +67 -0
  73. package/dist/game/narrativeAuthoring.js +122 -0
  74. package/dist/game/playableGame.d.ts +52 -20
  75. package/dist/game/quest.d.ts +15 -0
  76. package/dist/game/quest.js +209 -63
  77. package/dist/game/questCatalog.d.ts +50 -0
  78. package/dist/game/questCatalog.js +205 -0
  79. package/dist/game/saveStore.d.ts +6 -5
  80. package/dist/game/saveStore.js +57 -57
  81. package/dist/game/systemRuntime.d.ts +2 -2
  82. package/dist/game/systemRuntime.js +84 -33
  83. package/dist/gameplay.d.ts +3 -0
  84. package/dist/gameplay.js +3 -0
  85. package/dist/interaction/promptRegistry.d.ts +5 -4
  86. package/dist/interaction/promptRegistry.js +8 -2
  87. package/dist/inventory/inventoryModel.js +9 -6
  88. package/dist/inventory/shapedGrid.d.ts +48 -9
  89. package/dist/inventory/shapedGrid.js +69 -10
  90. package/dist/item/durability.js +4 -0
  91. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  92. package/dist/item/itemInstanceRegistry.js +111 -7
  93. package/dist/item/itemgen.d.ts +1 -1
  94. package/dist/item/itemgen.js +1 -1
  95. package/dist/item/modularItem.js +7 -1
  96. package/dist/material/appearanceSignals.d.ts +53 -0
  97. package/dist/material/appearanceSignals.js +47 -0
  98. package/dist/material/hairCards.d.ts +47 -0
  99. package/dist/material/hairCards.js +136 -0
  100. package/dist/material/materialAsset.d.ts +162 -0
  101. package/dist/material/materialAsset.js +382 -0
  102. package/dist/meta/changelog.d.ts +1 -1
  103. package/dist/meta/changelog.js +348 -1
  104. package/dist/movement/characterController.d.ts +5 -2
  105. package/dist/movement/characterController.js +50 -23
  106. package/dist/movement/freeFlight.d.ts +2 -2
  107. package/dist/movement/freeFlight.js +3 -3
  108. package/dist/movement/movementModel.d.ts +24 -1
  109. package/dist/movement/movementModel.js +74 -7
  110. package/dist/movement/playerMovement.d.ts +28 -1
  111. package/dist/movement/playerMovement.js +425 -102
  112. package/dist/movement/poseState.d.ts +2 -0
  113. package/dist/movement/poseState.js +29 -1
  114. package/dist/movement/solidObstacles.d.ts +3 -1
  115. package/dist/movement/solidObstacles.js +8 -5
  116. package/dist/movement/terrainGrade.d.ts +13 -0
  117. package/dist/movement/terrainGrade.js +33 -0
  118. package/dist/movement/voxelController.d.ts +2 -2
  119. package/dist/movement/voxelController.js +3 -3
  120. package/dist/nav/navGrid.d.ts +19 -1
  121. package/dist/nav/navGrid.js +26 -10
  122. package/dist/nav/solidRoute.d.ts +50 -0
  123. package/dist/nav/solidRoute.js +144 -0
  124. package/dist/physics/ballisticSweep.d.ts +10 -3
  125. package/dist/physics/ballisticSweep.js +53 -29
  126. package/dist/physics/forceVolume.d.ts +41 -0
  127. package/dist/physics/forceVolume.js +88 -0
  128. package/dist/physics/kinematicVehicle.d.ts +8 -3
  129. package/dist/physics/kinematicVehicle.js +2 -2
  130. package/dist/physics/physicsWorldBackend.js +19 -3
  131. package/dist/physics/spatialGrid.d.ts +1 -1
  132. package/dist/physics/spatialGrid.js +8 -9
  133. package/dist/physics/vehicleDynamics.d.ts +3 -2
  134. package/dist/physics/vehicleDynamics.js +2 -2
  135. package/dist/physics/vehicleObstacles.d.ts +3 -2
  136. package/dist/physics/vehicleObstacles.js +29 -5
  137. package/dist/runtime/cameraDirector.d.ts +39 -2
  138. package/dist/runtime/cameraDirector.js +162 -0
  139. package/dist/runtime/commandDispatch.d.ts +14 -0
  140. package/dist/runtime/commandDispatch.js +19 -0
  141. package/dist/runtime/context/combat.d.ts +11 -4
  142. package/dist/runtime/context/combat.js +178 -11
  143. package/dist/runtime/context/deathLoot.d.ts +9 -8
  144. package/dist/runtime/context/deathLoot.js +16 -16
  145. package/dist/runtime/context/scene.d.ts +2 -0
  146. package/dist/runtime/context/scene.js +13 -21
  147. package/dist/runtime/descriptors/baseline.js +34 -3
  148. package/dist/runtime/descriptors/features.d.ts +2 -1
  149. package/dist/runtime/descriptors/features.js +18 -0
  150. package/dist/runtime/gameContext.js +79 -13
  151. package/dist/runtime/gameContextTypes.d.ts +14 -0
  152. package/dist/runtime/headlessRunner.d.ts +3 -1
  153. package/dist/runtime/headlessRunner.js +11 -11
  154. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  155. package/dist/runtime/hostedGameRunner.js +156 -10
  156. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  157. package/dist/runtime/hostedWorldSession.js +154 -16
  158. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  159. package/dist/runtime/inputRecorder.d.ts +1 -1
  160. package/dist/runtime/inputRecorder.js +21 -1
  161. package/dist/runtime/inputSnapshot.d.ts +16 -4
  162. package/dist/runtime/inputSnapshot.js +31 -3
  163. package/dist/runtime/runtimeSave.d.ts +10 -6
  164. package/dist/runtime/runtimeSave.js +11 -5
  165. package/dist/runtime/simContext.d.ts +8 -3
  166. package/dist/runtime/simContext.js +7 -3
  167. package/dist/runtime/simSnapshot.js +2 -0
  168. package/dist/runtime/worldReplication.d.ts +2 -0
  169. package/dist/runtime/worldReplication.js +1 -1
  170. package/dist/scene/assetCatalog.d.ts +2 -0
  171. package/dist/scene/behaviorRuntime.d.ts +26 -5
  172. package/dist/scene/behaviorRuntime.js +263 -16
  173. package/dist/scene/behaviors.d.ts +30 -1
  174. package/dist/scene/behaviors.js +29 -0
  175. package/dist/scene/entityStore.d.ts +14 -3
  176. package/dist/scene/entityStore.js +45 -9
  177. package/dist/scene/mount.d.ts +9 -0
  178. package/dist/scene/mount.js +35 -0
  179. package/dist/scene/possession.d.ts +2 -0
  180. package/dist/scene/possession.js +21 -1
  181. package/dist/scene/sceneRaycast.d.ts +7 -0
  182. package/dist/scene/sceneRaycast.js +55 -0
  183. package/dist/scene/spatial.d.ts +20 -1
  184. package/dist/scene/spatial.js +94 -2
  185. package/dist/scene/vehicleSeat.d.ts +36 -2
  186. package/dist/scene/vehicleSeat.js +89 -2
  187. package/dist/stats/accumulatorMeter.d.ts +10 -0
  188. package/dist/stats/accumulatorMeter.js +6 -0
  189. package/dist/stats/eventMeter.d.ts +9 -2
  190. package/dist/stats/eventMeter.js +5 -2
  191. package/dist/store/defineStore.d.ts +3 -1
  192. package/dist/store/defineStore.js +3 -1
  193. package/dist/store/observableKeyedStore.d.ts +1 -1
  194. package/dist/store/observableKeyedStore.js +8 -6
  195. package/dist/time/simClock.d.ts +6 -2
  196. package/dist/time/simClock.js +5 -0
  197. package/dist/vfx/particleDirector.d.ts +58 -6
  198. package/dist/vfx/particleDirector.js +113 -22
  199. package/dist/vfx/particles.d.ts +68 -5
  200. package/dist/vfx/particles.js +284 -32
  201. package/dist/visibility/assetStreaming.d.ts +21 -3
  202. package/dist/visibility/assetStreaming.js +108 -24
  203. package/dist/visibility/settings.d.ts +8 -2
  204. package/dist/visibility/settings.js +3 -0
  205. package/dist/visibility/visibilitySystem.js +1 -0
  206. package/dist/work/expedition.d.ts +110 -0
  207. package/dist/work/expedition.js +195 -0
  208. package/dist/work/staffedStation.d.ts +29 -0
  209. package/dist/work/staffedStation.js +27 -0
  210. package/dist/world/authoredAnimation.d.ts +38 -0
  211. package/dist/world/authoredAnimation.js +99 -0
  212. package/dist/world/authoredObjects.d.ts +24 -4
  213. package/dist/world/authoredObjects.js +43 -16
  214. package/dist/world/authoredSimulation.d.ts +74 -0
  215. package/dist/world/authoredSimulation.js +302 -0
  216. package/dist/world/authoredWeather.d.ts +120 -0
  217. package/dist/world/authoredWeather.js +314 -0
  218. package/dist/world/buildingKit.d.ts +8 -3
  219. package/dist/world/buildingKit.js +21 -8
  220. package/dist/world/environmentForces.d.ts +21 -0
  221. package/dist/world/environmentForces.js +42 -0
  222. package/dist/world/features.d.ts +3 -3
  223. package/dist/world/footprintGrid.d.ts +55 -0
  224. package/dist/world/footprintGrid.js +201 -0
  225. package/dist/world/grassKind.d.ts +10 -0
  226. package/dist/world/grassKind.js +22 -0
  227. package/dist/world/pathTerrain.d.ts +1 -1
  228. package/dist/world/terraform.d.ts +2 -2
  229. package/dist/world/terraform.js +46 -18
  230. package/dist/world/weather.d.ts +17 -0
  231. package/dist/world/weather.js +116 -20
  232. package/dist/world/worldSolids.d.ts +11 -0
  233. package/dist/world/worldSolids.js +112 -1
  234. package/dist/world.d.ts +5 -5
  235. package/dist/world.js +4 -4
  236. package/package.json +1 -1
  237. package/skills/ce-handoff/SKILL.md +1 -1
  238. package/skills/game-design/SKILL.md +2 -0
  239. package/skills/jgengine/SKILL.md +23 -2
  240. package/skills/jgengine/api.md +28 -17
  241. package/skills/jgengine/recipes/minimal-game.md +7 -2
  242. package/skills/jgengine-combat/SKILL.md +8 -2
  243. package/skills/jgengine-combat/api.md +22 -10
  244. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  245. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  246. package/skills/jgengine-combat/reference.md +24 -2
  247. package/skills/jgengine-gameplay/SKILL.md +63 -0
  248. package/skills/jgengine-gameplay/api.md +131 -37
  249. package/skills/jgengine-gameplay/capabilities.md +68 -0
  250. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  251. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  252. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  253. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  254. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  255. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  256. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  257. package/skills/jgengine-gameplay/reference-systems.md +119 -0
  258. package/skills/jgengine-world/SKILL.md +12 -3
  259. package/skills/jgengine-world/api.md +201 -58
  260. package/skills/jgengine-world/capabilities.md +116 -3
  261. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  262. package/skills/jgengine-world/recipes/controls.md +4 -0
  263. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  264. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  265. package/skills/jgengine-world/reference.md +167 -5
  266. package/skills/jgengine-world/visibility.md +28 -1
@@ -0,0 +1,144 @@
1
+ const FIELDS = [
2
+ { key: "walkSpeed", label: "Walk speed", group: "movement", min: 0, step: 0.1 },
3
+ { key: "gravity", label: "Gravity", group: "jump", step: 0.5 },
4
+ { key: "jumpVelocity", label: "Jump velocity", group: "jump", min: 0, step: 0.1 },
5
+ { key: "stepHeight", label: "Step height", group: "movement", min: 0, step: 0.05 },
6
+ { key: "collisionHeight", label: "Collision height", group: "movement", min: Number.MIN_VALUE, step: 0.1 },
7
+ { key: "maxClimbGrade", label: "Max climb grade", group: "movement", min: 0, step: 0.05 },
8
+ { key: "groundAcceleration", label: "Ground acceleration", group: "response", min: 0, step: 1 },
9
+ { key: "airAcceleration", label: "Air control", group: "response", min: 0, step: 1 },
10
+ { key: "groundFriction", label: "Braking", group: "response", min: 0, step: 1 },
11
+ { key: "runMultiplier", label: "Sprint multiplier", group: "movement", min: 0, step: 0.1 },
12
+ { key: "crouchMultiplier", label: "Crouch multiplier", group: "movement", min: 0, step: 0.05 },
13
+ { key: "jumpBufferMs", label: "Jump buffer (ms)", group: "jump-response", min: 0, step: 10 },
14
+ { key: "coyoteMs", label: "Coyote time (ms)", group: "jump-response", min: 0, step: 10 },
15
+ { key: "jumpCutFactor", label: "Jump release factor", group: "jump-response", min: 0, max: 1, step: 0.05 },
16
+ { key: "apexGravityScale", label: "Apex gravity multiplier", group: "jump-response", min: 0, step: 0.05 },
17
+ { key: "apexSpeed", label: "Apex speed threshold (m/s)", group: "jump-response", min: 0, step: 0.1 },
18
+ { key: "fallGravityScale", label: "Fall gravity multiplier", group: "jump-response", min: 0, step: 0.05 },
19
+ { key: "landingRecoveryMs", label: "Landing recovery (ms)", group: "jump-response", min: 0, step: 10 },
20
+ { key: "landingSpeedScale", label: "Landing speed multiplier", group: "jump-response", min: 0, step: 0.05 },
21
+ ];
22
+ function validValue(field, value) {
23
+ return typeof value === "number" && Number.isFinite(value) && (!("min" in field) || value >= field.min) && (!("max" in field) || value <= field.max);
24
+ }
25
+ function numberRequirement(field) {
26
+ if (field.key === "jumpCutFactor")
27
+ return "finite number between 0 and 1";
28
+ return `finite${field.key === "collisionHeight" ? " positive" : "min" in field ? " nonnegative" : ""} number`;
29
+ }
30
+ /**
31
+ * Creates editor/RPC controls only for the values a game supplies, with those game's defaults.
32
+ * Gravity retains the game's sign convention; collision height is positive, other controls nonnegative.
33
+ * Jump release is bounded to 0..1; supplied deeper jump controls share a collapsed response group.
34
+ * @capability editor-movement author game-chosen character movement controls through ordinary catalog rows
35
+ */
36
+ export function createMovementSchema(defaults) {
37
+ const fields = [];
38
+ for (const field of FIELDS) {
39
+ const value = defaults[field.key];
40
+ if (value === undefined)
41
+ continue;
42
+ if (!validValue(field, value))
43
+ throw new Error(`Invalid movement default ${field.key}: use a ${numberRequirement(field)}`);
44
+ fields.push({ ...field, type: "number", default: value });
45
+ }
46
+ return {
47
+ fields,
48
+ groups: [
49
+ { id: "movement", label: "Movement" },
50
+ { id: "jump", label: "Jump" },
51
+ { id: "response", label: "Response", collapsed: true },
52
+ { id: "jump-response", label: "Jump response", collapsed: true },
53
+ ].filter((group) => fields.some((field) => field.group === group.id)),
54
+ };
55
+ }
56
+ /**
57
+ * Reads one game-named row as partial runtime tuning, retaining game defaults for omitted or invalid
58
+ * values. Rejected values include their document path and repair; no policy callbacks are replaced.
59
+ * @capability editor-movement resolve saved movement controls without replacing a game's character rules
60
+ */
61
+ export function readAuthoredMovement(document, catalogId, entryId) {
62
+ const catalogIndex = document.catalogs.findIndex((catalog) => catalog.id === catalogId);
63
+ const catalog = document.catalogs[catalogIndex];
64
+ const entryIndex = catalog?.entries.findIndex((entry) => entry.id === entryId) ?? -1;
65
+ const meta = catalog?.entries[entryIndex]?.meta;
66
+ const config = {};
67
+ const diagnostics = [];
68
+ if (meta === undefined)
69
+ return { config, diagnostics };
70
+ for (const field of FIELDS) {
71
+ const value = meta[field.key];
72
+ if (value === undefined)
73
+ continue;
74
+ if (!validValue(field, value)) {
75
+ diagnostics.push({
76
+ path: `catalogs[${catalogIndex}].entries[${entryIndex}].meta.${field.key}`,
77
+ message: `${field.key} must be a ${numberRequirement(field)}`,
78
+ repair: `Set ${field.key} to a valid number or remove it to inherit the game setting.`,
79
+ });
80
+ continue;
81
+ }
82
+ if (field.key === "walkSpeed")
83
+ config.walkSpeed = value;
84
+ else if (field.key === "gravity" || field.key === "jumpVelocity")
85
+ (config.physics ??= {})[field.key] = value;
86
+ else if (field.key === "stepHeight" || field.key === "collisionHeight" || field.key === "maxClimbGrade")
87
+ (config.movement ??= {})[field.key] = value;
88
+ else
89
+ ((config.movement ??= {}).feel ??= {})[field.key] = value;
90
+ }
91
+ return { config, diagnostics };
92
+ }
93
+ /**
94
+ * Memoizes movement resolution by immutable document identity; reads the latest document on every
95
+ * call while catalog lookup and diagnostics run only after authoring changes.
96
+ * @capability editor-movement consume live saved movement tuning with bounded per-frame work
97
+ */
98
+ export function createAuthoredMovementReader(getDocument, catalogId, entryId) {
99
+ let previous;
100
+ let result;
101
+ return () => {
102
+ const document = getDocument();
103
+ if (document !== previous) {
104
+ result = readAuthoredMovement(document, catalogId, entryId);
105
+ previous = document;
106
+ }
107
+ return result;
108
+ };
109
+ }
110
+ /**
111
+ * Binds live authored numeric controls onto stable runtime objects, preserving the game's callbacks,
112
+ * collision, backend and other policy. The movement controller reads these getters each step.
113
+ * @capability editor-movement bind live character tuning while preserving distinctive game policy
114
+ */
115
+ export function bindAuthoredMovement(read, defaults) {
116
+ let previous;
117
+ let previousBase;
118
+ let feel;
119
+ return {
120
+ movement: {
121
+ ...defaults.movement,
122
+ get stepHeight() { return read().config.movement?.stepHeight ?? defaults.movement?.stepHeight; },
123
+ get collisionHeight() { return read().config.movement?.collisionHeight ?? defaults.movement?.collisionHeight; },
124
+ get maxClimbGrade() { return read().config.movement?.maxClimbGrade ?? defaults.movement?.maxClimbGrade; },
125
+ get feel() {
126
+ const current = read();
127
+ const base = defaults.movement?.feel;
128
+ if (current !== previous || base !== previousBase) {
129
+ const authored = current.config.movement?.feel;
130
+ feel = authored === undefined ? base : { ...base, ...authored };
131
+ previous = current;
132
+ previousBase = base;
133
+ }
134
+ return feel;
135
+ },
136
+ },
137
+ physics: {
138
+ ...defaults.physics,
139
+ get gravity() { return read().config.physics?.gravity ?? defaults.physics?.gravity; },
140
+ get jumpVelocity() { return read().config.physics?.jumpVelocity ?? defaults.physics?.jumpVelocity; },
141
+ },
142
+ get diagnostics() { return read().diagnostics; },
143
+ };
144
+ }
@@ -0,0 +1,60 @@
1
+ import { type ForceFieldConfig } from "../physics/forceVolume.js";
2
+ import type { AuthoredWeatherConfig } from "../world/authoredWeather.js";
3
+ import { type EmitterConfig } from "../vfx/particles.js";
4
+ import { type ParticleAttachOptions } from "../vfx/particleDirector.js";
5
+ import type { FireGridConfig } from "../world/weather.js";
6
+ import type { FlockConfig } from "../ai/flock.js";
7
+ import type { EditorDocument, EditorVec3 } from "./types.js";
8
+ /** A named emitter authored in metres, with game-owned appearance and attachment ids. */
9
+ export interface AuthoredEmitter {
10
+ id: string;
11
+ /** Spawn only while this authored fire area burns; live particles finish their lifetime. */
12
+ fireAreaId?: string;
13
+ position: EditorVec3;
14
+ config: EmitterConfig;
15
+ options?: ParticleAttachOptions;
16
+ }
17
+ /** A bounded fire grid; fuel and damage policy remain game content. */
18
+ export interface AuthoredFireArea {
19
+ id: string;
20
+ /** Rain cooling in heat units/second at full exposure; zero disables automatic extinguishing. */
21
+ extinguishRate?: number;
22
+ position: EditorVec3;
23
+ config: Omit<FireGridConfig, "fuelAt" | "origin" | "wind">;
24
+ fuel?: readonly number[];
25
+ ignitions?: readonly {
26
+ col: number;
27
+ row: number;
28
+ }[];
29
+ }
30
+ /** An authored habitat selects a game species and optionally follows a document path. */
31
+ export interface AuthoredFlockHabitat {
32
+ id: string;
33
+ species: string;
34
+ position: EditorVec3;
35
+ radius: number;
36
+ count: number;
37
+ seed: string | number;
38
+ role: "cosmetic" | "gameplay";
39
+ routeId?: string;
40
+ steering: FlockConfig;
41
+ /** Opt into authored wind/force acceleration; maxAcceleration is the game policy cap. */
42
+ /** Coupling in 1/second, bounded to 0..1e12. */
43
+ windResponse?: number;
44
+ /** Maximum acceleration in metres/second², bounded to 0..1e12. */
45
+ maxAcceleration?: number;
46
+ forceMask?: number;
47
+ }
48
+ /** Authored simulation inputs. Runtime particles, heat, accumulation and actors save separately. */
49
+ export interface EditorSimulation {
50
+ weather?: AuthoredWeatherConfig;
51
+ /** Authoritative acceleration volumes; games select target masks and caps. */
52
+ forces?: readonly ForceFieldConfig[];
53
+ emitters?: readonly AuthoredEmitter[];
54
+ fires?: readonly AuthoredFireArea[];
55
+ habitats?: readonly AuthoredFlockHabitat[];
56
+ }
57
+ /** Decode authoring inputs with located diagnostics and bounded work before any document mutation.
58
+ * @capability validate-simulation diagnose authored weather, effect, fire, force and habitat documents before publication
59
+ */
60
+ export declare function decodeEditorSimulation(value: unknown, document?: Pick<EditorDocument, "paths">): EditorSimulation;
@@ -0,0 +1,202 @@
1
+ import { validateForceField } from "../physics/forceVolume.js";
2
+ import { validateAuthoredWeather } from "../world/authoredWeather.js";
3
+ import { validateEmitterConfig } from "../vfx/particles.js";
4
+ import { validateParticleAttachOptions } from "../vfx/particleDirector.js";
5
+ function object(value, path) {
6
+ if (value === null || typeof value !== "object" || Array.isArray(value))
7
+ throw new Error(`${path}: expected an object`);
8
+ return value;
9
+ }
10
+ function known(value, allowed, path) {
11
+ for (const key of Object.keys(value))
12
+ if (!allowed.includes(key))
13
+ throw new Error(`${path}.${key}: unknown field`);
14
+ }
15
+ function finite(value, path, min = -Infinity, max = Infinity) {
16
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max)
17
+ throw new Error(`${path}: expected a finite number in ${min}..${max}`);
18
+ return value;
19
+ }
20
+ function text(value, path) {
21
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 128)
22
+ throw new Error(`${path}: expected a nonempty string of at most 128 characters`);
23
+ }
24
+ function vector(value, path) {
25
+ const point = object(value, path);
26
+ for (const axis of ["x", "y", "z"])
27
+ finite(point[axis], `${path}.${axis}`);
28
+ }
29
+ function tuple(value, path) {
30
+ if (!Array.isArray(value) || value.length !== 3)
31
+ throw new Error(`${path}: expected [x,y,z]`);
32
+ value.forEach((component, index) => finite(component, `${path}[${index}]`));
33
+ }
34
+ function serializable(value, path, depth = 0) {
35
+ if (depth > 24)
36
+ throw new Error(`${path}: nesting exceeds 24 levels`);
37
+ if (value === null || typeof value === "string" || typeof value === "boolean" || value === undefined)
38
+ return;
39
+ if (typeof value === "number") {
40
+ finite(value, path);
41
+ return;
42
+ }
43
+ if (typeof value !== "object")
44
+ throw new Error(`${path}: expected serializable data`);
45
+ for (const [key, item] of Object.entries(value))
46
+ serializable(item, `${path}.${key}`, depth + 1);
47
+ }
48
+ function rows(value, path, limit, check) {
49
+ if (value === undefined)
50
+ return;
51
+ if (!Array.isArray(value) || value.length > limit)
52
+ throw new Error(`${path}: expected at most ${limit} entries`);
53
+ const ids = new Set();
54
+ value.forEach((entry, index) => {
55
+ const location = `${path}[${index}]`;
56
+ const row = object(entry, location);
57
+ text(row.id, `${location}.id`);
58
+ if (ids.has(row.id))
59
+ throw new Error(`${location}.id: duplicate id ${row.id}`);
60
+ ids.add(row.id);
61
+ vector(row.position, `${location}.position`);
62
+ check(row, location);
63
+ });
64
+ }
65
+ /** Decode authoring inputs with located diagnostics and bounded work before any document mutation.
66
+ * @capability validate-simulation diagnose authored weather, effect, fire, force and habitat documents before publication
67
+ */
68
+ export function decodeEditorSimulation(value, document) {
69
+ const source = object(value, "$.simulation");
70
+ known(source, ["weather", "forces", "emitters", "fires", "habitats"], "$.simulation");
71
+ serializable(source, "$.simulation");
72
+ if (source.weather !== undefined) {
73
+ object(source.weather, "$.simulation.weather");
74
+ try {
75
+ validateAuthoredWeather(source.weather);
76
+ }
77
+ catch (error) {
78
+ throw new Error(`$.simulation.weather: ${error instanceof Error ? error.message : String(error)}`);
79
+ }
80
+ }
81
+ if (source.forces !== undefined) {
82
+ if (!Array.isArray(source.forces) || source.forces.length > 128)
83
+ throw new Error("$.simulation.forces: expected at most 128 fields");
84
+ source.forces.forEach((value, index) => {
85
+ try {
86
+ validateForceField(value);
87
+ }
88
+ catch (error) {
89
+ throw new Error(`$.simulation.forces[${index}]: ${error instanceof Error ? error.message : String(error)}`);
90
+ }
91
+ });
92
+ }
93
+ rows(source.emitters, "$.simulation.emitters", 128, (row, path) => {
94
+ known(row, ["id", "position", "config", "options", "fireAreaId"], path);
95
+ if (row.fireAreaId !== undefined) {
96
+ text(row.fireAreaId, `${path}.fireAreaId`);
97
+ if (!Array.isArray(source.fires) || !source.fires.some((area) => area !== null && typeof area === "object" && area.id === row.fireAreaId))
98
+ throw new Error(`${path}.fireAreaId: missing fire area ${row.fireAreaId}`);
99
+ }
100
+ const diagnostics = validateEmitterConfig(row.config);
101
+ if (diagnostics.length)
102
+ throw new Error(`${path}.config: ${diagnostics.join("; ")}`);
103
+ if (row.options !== undefined) {
104
+ const errors = validateParticleAttachOptions(row.options);
105
+ if (errors.length)
106
+ throw new Error(`${path}.options: ${errors.join("; ")}`);
107
+ }
108
+ const config = object(row.config, `${path}.config`);
109
+ for (const key of ["max", "rate", "spread", "drag"])
110
+ if (config[key] !== undefined)
111
+ finite(config[key], `${path}.config.${key}`, 0, key === "max" ? 100_000 : key === "drag" ? 1 : Infinity);
112
+ if (config.max !== undefined && !Number.isInteger(config.max))
113
+ throw new Error(`${path}.config.max: expected an integer`);
114
+ for (const key of ["position", "spawnJitter", "direction", "gravity"])
115
+ if (config[key] !== undefined)
116
+ tuple(config[key], `${path}.config.${key}`);
117
+ for (const key of ["lifetime", "speed"])
118
+ if (config[key] !== undefined) {
119
+ const range = object(config[key], `${path}.config.${key}`);
120
+ finite(range.min, `${path}.config.${key}.min`, 0);
121
+ finite(range.max, `${path}.config.${key}.max`, range.min);
122
+ }
123
+ if (row.options !== undefined) {
124
+ const options = object(row.options, `${path}.options`);
125
+ if (options.space !== undefined && options.space !== "local" && options.space !== "world")
126
+ throw new Error(`${path}.options.space: expected local or world`);
127
+ if (options.active !== undefined && typeof options.active !== "boolean")
128
+ throw new Error(`${path}.options.active: expected boolean`);
129
+ }
130
+ });
131
+ rows(source.fires, "$.simulation.fires", 32, (row, path) => {
132
+ known(row, ["id", "position", "config", "fuel", "ignitions", "extinguishRate"], path);
133
+ if (row.extinguishRate !== undefined)
134
+ finite(row.extinguishRate, `${path}.extinguishRate`, 0);
135
+ const config = object(row.config, `${path}.config`);
136
+ known(config, ["cols", "rows", "cellSize", "ignitionThreshold", "spreadRate", "burnRate", "windBias", "maxCells"], `${path}.config`);
137
+ const cols = finite(config.cols, `${path}.config.cols`, 1, 256);
138
+ const count = cols * finite(config.rows, `${path}.config.rows`, 1, 256);
139
+ if (!Number.isInteger(cols) || !Number.isInteger(config.rows) || count > 16_384)
140
+ throw new Error(`${path}.config: expected integral dimensions with at most 16384 cells`);
141
+ finite(config.cellSize, `${path}.config.cellSize`, Number.EPSILON);
142
+ if (config.maxCells !== undefined) {
143
+ const capacity = finite(config.maxCells, `${path}.config.maxCells`, count, 1_048_576);
144
+ if (!Number.isInteger(capacity))
145
+ throw new Error(`${path}.config.maxCells: expected an integer`);
146
+ }
147
+ for (const key of ["ignitionThreshold", "spreadRate", "burnRate", "windBias"])
148
+ if (config[key] !== undefined)
149
+ finite(config[key], `${path}.config.${key}`, 0, key === "windBias" ? 1 : Infinity);
150
+ if (row.fuel !== undefined) {
151
+ if (!Array.isArray(row.fuel) || row.fuel.length !== count)
152
+ throw new Error(`${path}.fuel: expected ${count} cell values`);
153
+ row.fuel.forEach((fuel, index) => finite(fuel, `${path}.fuel[${index}]`, 0, 1));
154
+ }
155
+ if (row.ignitions !== undefined) {
156
+ if (!Array.isArray(row.ignitions) || row.ignitions.length > count)
157
+ throw new Error(`${path}.ignitions: expected at most ${count} cells`);
158
+ for (const cell of row.ignitions) {
159
+ const point = object(cell, `${path}.ignitions`);
160
+ known(point, ["col", "row"], `${path}.ignitions`);
161
+ finite(point.col, `${path}.ignitions.col`, 0, cols - 1);
162
+ finite(point.row, `${path}.ignitions.row`, 0, config.rows - 1);
163
+ if (!Number.isInteger(point.col) || !Number.isInteger(point.row))
164
+ throw new Error(`${path}.ignitions: expected integral cell coordinates`);
165
+ }
166
+ }
167
+ });
168
+ rows(source.habitats, "$.simulation.habitats", 32, (row, path) => {
169
+ known(row, ["id", "species", "position", "radius", "count", "seed", "role", "routeId", "steering", "windResponse", "maxAcceleration", "forceMask"], path);
170
+ text(row.species, `${path}.species`);
171
+ finite(row.radius, `${path}.radius`, Number.EPSILON);
172
+ const count = finite(row.count, `${path}.count`, 0, 256);
173
+ if (!Number.isInteger(count))
174
+ throw new Error(`${path}.count: expected an integer`);
175
+ if (row.role !== "cosmetic" && row.role !== "gameplay")
176
+ throw new Error(`${path}.role: expected cosmetic or gameplay`);
177
+ if (typeof row.seed !== "string" && typeof row.seed !== "number")
178
+ throw new Error(`${path}.seed: expected string or number`);
179
+ if (typeof row.seed === "number")
180
+ finite(row.seed, `${path}.seed`);
181
+ for (const key of ["windResponse", "maxAcceleration"])
182
+ if (row[key] !== undefined)
183
+ finite(row[key], `${path}.${key}`, 0, 1e12);
184
+ if (row.forceMask !== undefined) {
185
+ finite(row.forceMask, `${path}.forceMask`, 0, 0xffffffff);
186
+ if (!Number.isInteger(row.forceMask))
187
+ throw new Error(`${path}.forceMask: expected a uint32`);
188
+ }
189
+ const steering = object(row.steering, `${path}.steering`);
190
+ known(steering, ["maxSpeed", "accel", "separationRadius", "separationWeight", "neighborRadius", "cohesionWeight", "alignmentWeight", "seekWeight", "stragglerRadius", "stragglerBoost"], `${path}.steering`);
191
+ for (const key of ["maxSpeed", "separationRadius", "neighborRadius"])
192
+ finite(steering[key], `${path}.steering.${key}`, Number.EPSILON);
193
+ for (const [key, item] of Object.entries(steering))
194
+ finite(item, `${path}.steering.${key}`, 0);
195
+ if (row.routeId !== undefined) {
196
+ text(row.routeId, `${path}.routeId`);
197
+ if (document !== undefined && !document.paths.some((route) => route.id === row.routeId && route.points.length > 1))
198
+ throw new Error(`${path}.routeId: missing route ${row.routeId} with at least two points`);
199
+ }
200
+ });
201
+ return structuredClone(source);
202
+ }
@@ -0,0 +1,17 @@
1
+ /** A solid or clearance volume in the prefab's unnormalized local asset space. */
2
+ export interface StaticPrefabBox {
3
+ min: readonly [number, number, number];
4
+ max: readonly [number, number, number];
5
+ }
6
+ /** Persisted static export settings; source parts remain editable in the prefab fragment. */
7
+ export interface StaticPrefabBake {
8
+ assetId: string;
9
+ /** Omit for a conservative closed exterior. Explicit boxes preserve authored openings. */
10
+ collisionBoxes?: readonly StaticPrefabBox[];
11
+ /** Required empty spaces, checked against movement solids before export. */
12
+ clearances?: readonly (StaticPrefabBox & {
13
+ id: string;
14
+ })[];
15
+ }
16
+ /** Validate and copy static prefab export settings from document or RPC input. */
17
+ export declare function parseStaticPrefabBake(value: unknown): StaticPrefabBake;
@@ -0,0 +1,51 @@
1
+ /** Validate and copy static prefab export settings from document or RPC input. */
2
+ export function parseStaticPrefabBake(value) {
3
+ const record = (input) => {
4
+ if (input === null || typeof input !== "object" || Array.isArray(input))
5
+ throw new Error("expected an object");
6
+ return input;
7
+ };
8
+ const input = record(value);
9
+ for (const key of Object.keys(input)) {
10
+ if (!["assetId", "collisionBoxes", "clearances"].includes(key))
11
+ throw new Error(`unknown static bake field: ${key}`);
12
+ }
13
+ if (typeof input.assetId !== "string" || input.assetId.trim().length === 0)
14
+ throw new Error("assetId: expected a nonempty string");
15
+ const boxes = (items, clearance) => {
16
+ if (!Array.isArray(items) || items.length === 0 || items.length > 4096)
17
+ throw new Error("expected 1..4096 boxes");
18
+ const ids = new Set();
19
+ return items.map((item, index) => {
20
+ const box = record(item);
21
+ for (const key of Object.keys(box)) {
22
+ if (!["min", "max", ...(clearance ? ["id"] : [])].includes(key))
23
+ throw new Error(`box ${index}: unknown field ${key}`);
24
+ }
25
+ for (const key of ["min", "max"]) {
26
+ const vector = box[key];
27
+ if (!Array.isArray(vector) || vector.length !== 3 || !vector.every((axis) => typeof axis === "number" && Number.isFinite(axis)))
28
+ throw new Error(`box ${index}.${key}: expected three finite numbers`);
29
+ }
30
+ const min = box.min;
31
+ const max = box.max;
32
+ if (!min.every((axis, i) => axis < max[i]))
33
+ throw new Error(`box ${index}: min must be less than max on every axis`);
34
+ if (clearance) {
35
+ if (typeof box.id !== "string" || box.id.trim().length === 0 || ids.has(box.id))
36
+ throw new Error(`clearance ${index}: expected a unique nonempty id`);
37
+ ids.add(box.id);
38
+ }
39
+ return { min: [...min], max: [...max], ...(clearance ? { id: box.id } : {}) };
40
+ });
41
+ };
42
+ const collisionBoxes = input.collisionBoxes === undefined ? undefined : boxes(input.collisionBoxes, false);
43
+ const clearances = input.clearances === undefined ? undefined : boxes(input.clearances, true);
44
+ if (clearances !== undefined && collisionBoxes === undefined)
45
+ throw new Error("clearances require explicit collisionBoxes");
46
+ for (const clearance of clearances ?? []) {
47
+ if (collisionBoxes.some((solid) => solid.min.every((axis, i) => axis < clearance.max[i] && solid.max[i] > clearance.min[i])))
48
+ throw new Error(`clearance ${clearance.id} intersects a collision box`);
49
+ }
50
+ return { assetId: input.assetId, ...(collisionBoxes === undefined ? {} : { collisionBoxes }), ...(clearances === undefined ? {} : { clearances }) };
51
+ }
@@ -0,0 +1,10 @@
1
+ /** A paintable terrain material layer — a surface id plus the color it renders as. */
2
+ export interface TerrainMaterial {
3
+ id: string;
4
+ label: string;
5
+ color: string;
6
+ }
7
+ /** The default terrain paint palette (surface id → color) shared by the panel and the mesh. */
8
+ export declare const TERRAIN_MATERIALS: readonly TerrainMaterial[];
9
+ /** Maps every default material id to its render color, for the sculpt mesh's per-cell surface tint. */
10
+ export declare const TERRAIN_MATERIAL_COLORS: Record<string, string>;
@@ -0,0 +1,13 @@
1
+ /** The default terrain paint palette (surface id → color) shared by the panel and the mesh. */
2
+ export const TERRAIN_MATERIALS = [
3
+ { id: "grass", label: "Grass", color: "#4b7f3f" },
4
+ { id: "dirt", label: "Dirt", color: "#6b4f34" },
5
+ { id: "rock", label: "Rock", color: "#7c7f86" },
6
+ { id: "sand", label: "Sand", color: "#c2b283" },
7
+ { id: "mud", label: "Mud", color: "#574433" },
8
+ { id: "snow", label: "Snow", color: "#eef3f7" },
9
+ { id: "road", label: "Road", color: "#3a3a3d" },
10
+ { id: "gravel", label: "Gravel", color: "#8d8d84" },
11
+ ];
12
+ /** Maps every default material id to its render color, for the sculpt mesh's per-cell surface tint. */
13
+ export const TERRAIN_MATERIAL_COLORS = Object.fromEntries(TERRAIN_MATERIALS.map((material) => [material.id, material.color]));
@@ -1,3 +1,6 @@
1
+ import type { EditorSimulation } from "./simulation.js";
2
+ import type { MaterialAsset } from "../material/materialAsset.js";
3
+ import type { StaticPrefabBake } from "./staticPrefab.js";
1
4
  import type { ParamSchema } from "../scene/sceneKinds.js";
2
5
  import type { EditorUiDocument } from "../ui/hudDocument.js";
3
6
  import type { MinimapBakeBounds } from "../world/minimapBake.js";
@@ -100,6 +103,8 @@ export interface EditorNote {
100
103
  export type EditorTerrain = TerraformSnapshot;
101
104
  /** The four placeable-object collections a prefab fragment or clipboard fragment carries. */
102
105
  export interface EditorFragmentContent {
106
+ /** Referenced reusable materials travel with prefab and clipboard content. */
107
+ materialAssets?: MaterialAsset[];
103
108
  markers: readonly EditorMarker[];
104
109
  volumes: readonly EditorVolume[];
105
110
  paths: readonly EditorPath[];
@@ -115,6 +120,8 @@ export interface EditorPrefab {
115
120
  id: string;
116
121
  name: string;
117
122
  fragment: EditorFragmentContent;
123
+ /** Offline export settings; world instances remain authored markers. */
124
+ staticBake?: StaticPrefabBake;
118
125
  }
119
126
  /**
120
127
  * A named, persisted list of object ids — a selection bookmark (restore, add-to) that can also
@@ -227,7 +234,11 @@ export interface EditorEnvironment {
227
234
  }
228
235
  /** The full authored scene: every marker, volume, path, note, and sculpted terrain for a game. */
229
236
  export interface EditorDocument {
237
+ /** Reusable material library; per-marker named assignments live in meta.materialAssignments. */
238
+ materialAssets?: MaterialAsset[];
230
239
  version: 1;
240
+ /** Authored weather, emitter, fire and habitat inputs; runtime state saves separately. */
241
+ simulation?: EditorSimulation;
231
242
  markers: EditorMarker[];
232
243
  volumes: EditorVolume[];
233
244
  paths: EditorPath[];
@@ -169,6 +169,8 @@ export interface LockOnCameraConfig {
169
169
  export type ChaseView = "chase" | "cockpit" | "hood" | "rear";
170
170
  /** Speed-reactive vehicle chase rig (#27) — speed→FOV, spring arm, procedural shake, interior views. */
171
171
  export interface ChaseCameraConfig {
172
+ /** Heading the chase boom follows: target body (default), or independent input/aim yaw for camera-relative walking. Seat views always follow the body. */
173
+ headingSource?: "body" | "input";
172
174
  /** Boom distance behind the vehicle. Default 6. */
173
175
  distance?: number;
174
176
  /** Camera height above the follow point. Default 2.6. */
@@ -477,7 +479,7 @@ export interface GameCameraConfig {
477
479
  };
478
480
  initialDistance?: number;
479
481
  initialHeight?: number;
480
- /** Initial horizontal boom facing (radians): 0 = camera behind on -Z, PI = camera on +Z. Seeds the orbit rig; unset keeps the legacy -Z placement. */
482
+ /** Initial horizontal boom facing (radians): 0 = camera behind on -Z, PI = camera on +Z. Seeds orbit and input-owned chase once; unset keeps orbit's -Z placement or seeds chase from its first acquired target's heading. */
481
483
  initialYaw?: number;
482
484
  /** Initial boom elevation (radians): 0 = level, positive = camera above the target looking down. Seeds the orbit rig; unset derives elevation from `initialHeight`/`initialDistance`. */
483
485
  initialPitch?: number;
@@ -46,6 +46,7 @@ export declare function defaultAnimationForClips(clips: readonly string[], table
46
46
  * configs pass through, `"none"`/absent render the bind pose, and `"auto"` derives states and
47
47
  * one-shots from the model's clip names via {@link defaultAnimationForClips}. The shell calls
48
48
  * this with the loaded GLB's actual clip names, so `"auto"` works on any rigged model —
49
- * catalog-resolved ids are stamped `"auto"` automatically.
49
+ * catalog-resolved ids are stamped `"auto"` automatically. An object with `auto: true` derives
50
+ * the same defaults, then replaces each explicitly supplied field whole, including empty maps.
50
51
  */
51
52
  export declare function resolveAnimationConfig(animation: ModelAnimationConfig | "auto" | "none" | undefined, clips: readonly string[] | undefined, table?: ClipRoleTable): ModelAnimationConfig | undefined;
@@ -129,14 +129,18 @@ export function defaultAnimationForClips(clips, table = DEFAULT_CLIP_ROLE_TABLE)
129
129
  * configs pass through, `"none"`/absent render the bind pose, and `"auto"` derives states and
130
130
  * one-shots from the model's clip names via {@link defaultAnimationForClips}. The shell calls
131
131
  * this with the loaded GLB's actual clip names, so `"auto"` works on any rigged model —
132
- * catalog-resolved ids are stamped `"auto"` automatically.
132
+ * catalog-resolved ids are stamped `"auto"` automatically. An object with `auto: true` derives
133
+ * the same defaults, then replaces each explicitly supplied field whole, including empty maps.
133
134
  */
134
135
  export function resolveAnimationConfig(animation, clips, table = DEFAULT_CLIP_ROLE_TABLE) {
135
136
  if (animation === undefined || animation === "none")
136
137
  return undefined;
137
- if (animation !== "auto")
138
+ if (animation !== "auto" && animation.auto !== true)
138
139
  return animation;
139
140
  if (clips === undefined || clips.length === 0)
140
141
  return undefined;
141
- return defaultAnimationForClips(clips, table);
142
+ const defaults = defaultAnimationForClips(clips, table);
143
+ if (defaults === undefined || animation === "auto")
144
+ return defaults;
145
+ return { ...defaults, ...animation };
142
146
  }