@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
@@ -1,3 +1,7 @@
1
+ import { decodeEditorSimulation } from "./simulation.js";
2
+ import { parseMaterialAssignments, validateMaterialAsset, validateMaterialAssignments } from "../material/materialAsset.js";
3
+ import { authoredHiddenNodes } from "./modelNodeVisibility.js";
4
+ import { parseStaticPrefabBake } from "./staticPrefab.js";
1
5
  import { cloneEditorUiDocument, decodeEditorUiDocument } from "../ui/hudDocument.js";
2
6
  import { migrateTerrainSnapshot } from "../world/terraform.js";
3
7
  import { cloneGridLayer, migrateGridLayer, } from "./grid.js";
@@ -36,7 +40,7 @@ function cloneDirective(directive) {
36
40
  ...(directive.region === undefined ? {} : { region: directive.region }),
37
41
  ...(area === undefined ? {} : { area }),
38
42
  ...(directive.seed === undefined ? {} : { seed: directive.seed }),
39
- ...(directive.meta === undefined ? {} : { meta: { ...directive.meta } }),
43
+ ...(directive.meta === undefined ? {} : { meta: structuredClone(directive.meta) }),
40
44
  };
41
45
  if (directive.kind === "scatter") {
42
46
  return { ...base, kind: "scatter", asset: directive.asset, density: directive.density,
@@ -68,6 +72,8 @@ function upsertGrids(base, overlay) {
68
72
  /** Spread-ready non-placeable fields preserved across document rebuilds. @internal */
69
73
  export function editorDocumentExtras(doc) {
70
74
  return {
75
+ ...(doc.simulation === undefined ? {} : { simulation: structuredClone(doc.simulation) }),
76
+ ...(doc.materialAssets === undefined ? {} : { materialAssets: doc.materialAssets }),
71
77
  prefabs: doc.prefabs,
72
78
  collections: doc.collections,
73
79
  catalogs: doc.catalogs,
@@ -99,14 +105,14 @@ function cloneCatalogEntry(entry) {
99
105
  return {
100
106
  id: entry.id,
101
107
  ...(entry.label === undefined ? {} : { label: entry.label }),
102
- ...(entry.meta === undefined ? {} : { meta: { ...entry.meta } }),
108
+ ...(entry.meta === undefined ? {} : { meta: structuredClone(entry.meta) }),
103
109
  };
104
110
  }
105
111
  function cloneCatalogs(catalogs) {
106
112
  return (catalogs ?? []).map((catalog) => ({
107
113
  id: catalog.id,
108
114
  ...(catalog.label === undefined ? {} : { label: catalog.label }),
109
- ...(catalog.schema === undefined ? {} : { schema: catalog.schema }),
115
+ ...(catalog.schema === undefined ? {} : { schema: structuredClone(catalog.schema) }),
110
116
  entries: catalog.entries.map(cloneCatalogEntry),
111
117
  }));
112
118
  }
@@ -121,38 +127,36 @@ export function cloneEditorDocument(doc) {
121
127
  const directives = cloneDirectives(doc.directives);
122
128
  return {
123
129
  version: 1,
130
+ ...(doc.simulation === undefined ? {} : { simulation: structuredClone(doc.simulation) }),
124
131
  markers: doc.markers.map((marker) => ({
125
132
  ...marker,
126
133
  position: { ...marker.position },
127
- ...(marker.meta === undefined ? {} : { meta: { ...marker.meta } }),
134
+ ...(marker.meta === undefined ? {} : { meta: structuredClone(marker.meta) }),
128
135
  })),
129
136
  volumes: doc.volumes.map((volume) => ({
130
137
  ...volume,
131
138
  center: { ...volume.center },
132
139
  ...(volume.halfExtents === undefined ? {} : { halfExtents: { ...volume.halfExtents } }),
133
- ...(volume.meta === undefined ? {} : { meta: { ...volume.meta } }),
140
+ ...(volume.meta === undefined ? {} : { meta: structuredClone(volume.meta) }),
134
141
  })),
135
142
  paths: doc.paths.map((path) => ({
136
143
  ...path,
137
144
  points: path.points.map((point) => ({ ...point })),
138
- ...(path.meta === undefined ? {} : { meta: { ...path.meta } }),
145
+ ...(path.meta === undefined ? {} : { meta: structuredClone(path.meta) }),
139
146
  })),
140
147
  annotations: doc.annotations.map((note) => ({
141
148
  ...note,
142
149
  position: { ...note.position },
143
- ...(note.meta === undefined ? {} : { meta: { ...note.meta } }),
150
+ ...(note.meta === undefined ? {} : { meta: structuredClone(note.meta) }),
144
151
  })),
145
152
  prefabs: doc.prefabs.map((prefab) => ({
146
153
  ...prefab,
147
- fragment: {
148
- markers: prefab.fragment.markers.map((marker) => ({ ...marker, position: { ...marker.position } })),
149
- volumes: prefab.fragment.volumes.map((volume) => ({ ...volume, center: { ...volume.center } })),
150
- paths: prefab.fragment.paths.map((path) => ({ ...path, points: path.points.map((point) => ({ ...point })) })),
151
- annotations: prefab.fragment.annotations.map((note) => ({ ...note, position: { ...note.position } })),
152
- },
154
+ ...(prefab.staticBake === undefined ? {} : { staticBake: parseStaticPrefabBake(prefab.staticBake) }),
155
+ fragment: structuredClone(prefab.fragment),
153
156
  })),
154
157
  collections: doc.collections.map((collection) => ({ ...collection, memberIds: [...collection.memberIds] })),
155
158
  catalogs: cloneCatalogs(doc.catalogs),
159
+ ...(doc.materialAssets === undefined ? {} : { materialAssets: structuredClone(doc.materialAssets) }),
156
160
  ...(doc.grids === undefined ? {} : { grids: doc.grids.map(cloneGridLayer) }),
157
161
  ...(doc.terrain === undefined ? {} : { terrain: doc.terrain }),
158
162
  ...(ui === undefined ? {} : { ui }),
@@ -174,7 +178,7 @@ function upsertCatalogs(base, overlay) {
174
178
  {
175
179
  id: catalog.id,
176
180
  ...(catalog.label === undefined ? {} : { label: catalog.label }),
177
- ...(catalog.schema === undefined ? {} : { schema: catalog.schema }),
181
+ ...(catalog.schema === undefined ? {} : { schema: structuredClone(catalog.schema) }),
178
182
  entries: catalog.entries.map(cloneCatalogEntry),
179
183
  },
180
184
  ]));
@@ -204,6 +208,7 @@ export function normalizeEditorLayers(input) {
204
208
  const directives = cloneDirectives(resolved.directives);
205
209
  return {
206
210
  version: 1,
211
+ ...(resolved.simulation === undefined ? {} : { simulation: decodeEditorSimulation(resolved.simulation, { paths: asArray(resolved.paths) }) }),
207
212
  markers: asArray(resolved.markers),
208
213
  volumes: asArray(resolved.volumes),
209
214
  paths: asArray(resolved.paths),
@@ -211,6 +216,7 @@ export function normalizeEditorLayers(input) {
211
216
  prefabs: asArray(resolved.prefabs),
212
217
  collections: asArray(resolved.collections),
213
218
  catalogs: cloneCatalogs(resolved.catalogs),
219
+ ...(resolved.materialAssets === undefined ? {} : { materialAssets: structuredClone(resolved.materialAssets) }),
214
220
  ...(grids === undefined ? {} : { grids }),
215
221
  ...(resolved.terrain === undefined ? {} : { terrain: migrateTerrainSnapshot(resolved.terrain) }),
216
222
  ...(ui === undefined ? {} : { ui }),
@@ -262,6 +268,8 @@ export function mergeEditorDocuments(...docs) {
262
268
  out.prefabs.push(...doc.prefabs);
263
269
  out.collections.push(...doc.collections);
264
270
  out.catalogs = upsertCatalogs(out.catalogs, doc.catalogs);
271
+ if (doc.materialAssets !== undefined)
272
+ out.materialAssets = structuredClone(upsertById(out.materialAssets ?? [], doc.materialAssets));
265
273
  const mergedGrids = upsertGrids(out.grids, doc.grids);
266
274
  if (mergedGrids !== undefined)
267
275
  out.grids = mergedGrids;
@@ -271,6 +279,8 @@ export function mergeEditorDocuments(...docs) {
271
279
  out.minimap = cloneMinimapBake(doc.minimap);
272
280
  if (doc.environment !== undefined)
273
281
  out.environment = cloneEnvironment(doc.environment);
282
+ if (doc.simulation !== undefined)
283
+ out.simulation = structuredClone(doc.simulation);
274
284
  if (doc.directives !== undefined) {
275
285
  out.directives = upsertById(out.directives ?? [], doc.directives);
276
286
  }
@@ -292,6 +302,7 @@ export function extractEditorFragment(doc, ids) {
292
302
  const wanted = new Set(ids);
293
303
  return cloneEditorDocument({
294
304
  version: 1,
305
+ ...(doc.materialAssets === undefined ? {} : { materialAssets: doc.materialAssets.filter((asset) => doc.markers.some((marker) => wanted.has(marker.id) && parseMaterialAssignments(marker.meta?.materialAssignments ?? []).some((item) => item.materialId === asset.id))) }),
295
306
  markers: doc.markers.filter((marker) => wanted.has(marker.id)),
296
307
  volumes: doc.volumes.filter((volume) => wanted.has(volume.id)),
297
308
  paths: doc.paths.filter((path) => wanted.has(path.id)),
@@ -435,6 +446,7 @@ export function createPrefabFragment(doc, ids) {
435
446
  z: point.z - origin.z,
436
447
  });
437
448
  return {
449
+ ...(extracted.materialAssets === undefined ? {} : { materialAssets: extracted.materialAssets }),
438
450
  markers: extracted.markers.map((marker) => ({ ...marker, position: shift(marker.position) })),
439
451
  volumes: extracted.volumes.map((volume) => ({ ...volume, center: shift(volume.center) })),
440
452
  paths: extracted.paths.map((path) => ({ ...path, points: path.points.map(shift) })),
@@ -608,7 +620,21 @@ function decodeMeta(value, path, errors) {
608
620
  errors.push({ path, message: "expected an object" });
609
621
  return undefined;
610
622
  }
611
- return value;
623
+ if (value.materialAssignments !== undefined) {
624
+ try {
625
+ parseMaterialAssignments(value.materialAssignments);
626
+ }
627
+ catch (error) {
628
+ errors.push({ path: `${path}.materialAssignments`, message: String(error) });
629
+ }
630
+ }
631
+ try {
632
+ authoredHiddenNodes(value);
633
+ }
634
+ catch (error) {
635
+ errors.push({ path: `${path}.hiddenNodes`, message: String(error) });
636
+ }
637
+ return structuredClone(value);
612
638
  }
613
639
  function decodeMarker(item, path, errors) {
614
640
  if (!isPlainObject(item)) {
@@ -744,18 +770,55 @@ function decodeNote(item, path, errors) {
744
770
  note.meta = meta;
745
771
  return note;
746
772
  }
773
+ function decodeMaterialAssets(value, path, errors) {
774
+ if (!Array.isArray(value)) {
775
+ errors.push({ path, message: "expected material assets" });
776
+ return [];
777
+ }
778
+ const ids = new Set();
779
+ const assets = [];
780
+ value.forEach((asset, index) => {
781
+ const diagnostics = validateMaterialAsset(asset).filter((diagnostic) => diagnostic.severity === "error");
782
+ for (const diagnostic of diagnostics)
783
+ errors.push({ path: `${path}[${index}].${diagnostic.path}`, message: diagnostic.message });
784
+ if (diagnostics.length === 0) {
785
+ if (ids.has(asset.id))
786
+ errors.push({ path: `${path}[${index}].id`, message: "duplicate material ID" });
787
+ ids.add(asset.id);
788
+ assets.push(structuredClone(asset));
789
+ }
790
+ });
791
+ return assets;
792
+ }
747
793
  function decodeFragmentContent(value, path, errors) {
748
794
  if (!isPlainObject(value)) {
749
795
  errors.push({ path, message: "expected an object" });
750
796
  return null;
751
797
  }
798
+ const materialAssets = value.materialAssets === undefined ? undefined : decodeMaterialAssets(value.materialAssets, `${path}.materialAssets`, errors);
799
+ const markers = decodeArray(value.markers, `${path}.markers`, decodeMarker, errors);
800
+ validateMarkerMaterialReferences(markers, materialAssets ?? [], path, errors);
752
801
  return {
753
- markers: decodeArray(value.markers, `${path}.markers`, decodeMarker, errors),
802
+ ...(materialAssets === undefined ? {} : { materialAssets }),
803
+ markers,
754
804
  volumes: decodeArray(value.volumes, `${path}.volumes`, decodeVolume, errors),
755
805
  paths: decodeArray(value.paths, `${path}.paths`, decodePath, errors),
756
806
  annotations: decodeArray(value.annotations, `${path}.annotations`, decodeNote, errors),
757
807
  };
758
808
  }
809
+ function validateMarkerMaterialReferences(markers, assets, path, errors) {
810
+ for (const [index, marker] of markers.entries()) {
811
+ if (marker.meta?.materialAssignments === undefined)
812
+ continue;
813
+ try {
814
+ for (const diagnostic of validateMaterialAssignments(parseMaterialAssignments(marker.meta.materialAssignments), assets)) {
815
+ if (diagnostic.severity === "error")
816
+ errors.push({ path: `${path}.markers[${index}].meta.${diagnostic.path}`, message: diagnostic.message });
817
+ }
818
+ }
819
+ catch { }
820
+ }
821
+ }
759
822
  function decodePrefab(item, path, errors) {
760
823
  if (!isPlainObject(item)) {
761
824
  errors.push({ path, message: "expected an object" });
@@ -768,7 +831,16 @@ function decodePrefab(item, path, errors) {
768
831
  const fragment = decodeFragmentContent(item.fragment, `${path}.fragment`, errors);
769
832
  if (typeof item.id !== "string" || typeof item.name !== "string" || fragment === null)
770
833
  return null;
771
- return { id: item.id, name: item.name, fragment };
834
+ const prefab = { id: item.id, name: item.name, fragment };
835
+ if (item.staticBake !== undefined) {
836
+ try {
837
+ prefab.staticBake = parseStaticPrefabBake(item.staticBake);
838
+ }
839
+ catch (error) {
840
+ errors.push({ path: `${path}.staticBake`, message: error instanceof Error ? error.message : String(error) });
841
+ }
842
+ }
843
+ return prefab;
772
844
  }
773
845
  function decodeCollection(item, path, errors) {
774
846
  if (!isPlainObject(item)) {
@@ -1228,6 +1300,7 @@ export function decodeEditorDocument(raw) {
1228
1300
  return { ok: false, errors: [{ path: "$", message: "editor document must be an object" }] };
1229
1301
  }
1230
1302
  const errors = [];
1303
+ const materialAssets = raw.materialAssets === undefined ? undefined : decodeMaterialAssets(raw.materialAssets, "$.materialAssets", errors);
1231
1304
  const markers = decodeArray(raw.markers, "$.markers", decodeMarker, errors);
1232
1305
  const volumes = decodeArray(raw.volumes, "$.volumes", decodeVolume, errors);
1233
1306
  const paths = decodeArray(raw.paths, "$.paths", decodePath, errors);
@@ -1243,6 +1316,15 @@ export function decodeEditorDocument(raw) {
1243
1316
  const minimap = raw.minimap === undefined ? undefined : decodeMinimapBake(raw.minimap, "$.minimap", errors);
1244
1317
  const bakes = raw.bakes === undefined ? undefined : decodeBakes(raw.bakes, "$.bakes", errors);
1245
1318
  const environment = raw.environment === undefined ? undefined : decodeEnvironment(raw.environment, "$.environment", errors);
1319
+ let simulation;
1320
+ if (raw.simulation !== undefined) {
1321
+ try {
1322
+ simulation = decodeEditorSimulation(raw.simulation, { paths });
1323
+ }
1324
+ catch (error) {
1325
+ errors.push({ path: "$.simulation", message: error instanceof Error ? error.message : String(error) });
1326
+ }
1327
+ }
1246
1328
  // Placeable object ids form one document-global namespace (selection, parenting, and removal all
1247
1329
  // treat them that way), so a document that reuses an id — even across two different collections —
1248
1330
  // is malformed and rejected here with the offending path, rather than silently loading a scene
@@ -1264,6 +1346,7 @@ export function decodeEditorDocument(raw) {
1264
1346
  }
1265
1347
  });
1266
1348
  }
1349
+ validateMarkerMaterialReferences(markers, materialAssets ?? [], "$", errors);
1267
1350
  if (errors.length > 0)
1268
1351
  return { ok: false, errors };
1269
1352
  const terrain = raw.terrain === undefined ? undefined : migrateTerrainSnapshot(raw.terrain);
@@ -1272,6 +1355,8 @@ export function decodeEditorDocument(raw) {
1272
1355
  ok: true,
1273
1356
  document: {
1274
1357
  version: 1,
1358
+ ...(simulation === undefined ? {} : { simulation }),
1359
+ ...(materialAssets === undefined ? {} : { materialAssets }),
1275
1360
  markers,
1276
1361
  volumes,
1277
1362
  paths,
@@ -1315,6 +1400,7 @@ export function applyEditorDocumentOverlay(base, overlay) {
1315
1400
  const terrain = overlay.terrain ?? base.terrain;
1316
1401
  const minimap = overlay.minimap ?? base.minimap;
1317
1402
  const environment = overlay.environment ?? base.environment;
1403
+ const simulation = overlay.simulation ?? base.simulation;
1318
1404
  const ui = overlay.ui === undefined
1319
1405
  ? cloneEditorUiDocument(base.ui)
1320
1406
  : {
@@ -1326,6 +1412,7 @@ export function applyEditorDocumentOverlay(base, overlay) {
1326
1412
  const grids = upsertGrids(base.grids, overlay.grids);
1327
1413
  return {
1328
1414
  version: 1,
1415
+ ...(simulation === undefined ? {} : { simulation: structuredClone(simulation) }),
1329
1416
  markers: upsertById(base.markers, overlay.markers),
1330
1417
  volumes: upsertById(base.volumes, overlay.volumes),
1331
1418
  paths: upsertById(base.paths, overlay.paths),
@@ -1333,6 +1420,7 @@ export function applyEditorDocumentOverlay(base, overlay) {
1333
1420
  prefabs: upsertById(base.prefabs, overlay.prefabs),
1334
1421
  collections: upsertById(base.collections, overlay.collections),
1335
1422
  catalogs: upsertCatalogs(base.catalogs, overlay.catalogs),
1423
+ ...(base.materialAssets === undefined && overlay.materialAssets === undefined ? {} : { materialAssets: structuredClone(upsertById(base.materialAssets ?? [], overlay.materialAssets ?? [])) }),
1336
1424
  ...(grids === undefined ? {} : { grids }),
1337
1425
  ...(terrain === undefined ? {} : { terrain }),
1338
1426
  ...(ui === undefined ? {} : { ui }),
@@ -1382,6 +1470,17 @@ export function editorDocumentBounds(doc) {
1382
1470
  for (const note of doc.annotations) {
1383
1471
  include(note.position.x, note.position.y, note.position.z);
1384
1472
  }
1473
+ if (doc.terrain !== undefined) {
1474
+ const terrain = doc.terrain;
1475
+ let low = 0;
1476
+ let high = 0;
1477
+ for (const offset of terrain.offsets) {
1478
+ low = Math.min(low, offset);
1479
+ high = Math.max(high, offset);
1480
+ }
1481
+ include(terrain.bounds.minX, low, terrain.bounds.minZ);
1482
+ include(terrain.bounds.maxX, high, terrain.bounds.maxZ);
1483
+ }
1385
1484
  if (!any)
1386
1485
  return null;
1387
1486
  return {
@@ -90,6 +90,8 @@ export type { EditorDispatchOptions } from "./commands.js";
90
90
  export type { EditorSession } from "./commands.js";
91
91
  /** The document plus current selection at a point in editor history. */
92
92
  export type { EditorSessionState } from "./commands.js";
93
+ /** Atomic command-batch outcome; a failure identifies the zero-based command index. */
94
+ export type { EditorTransactionResult } from "./commands.js";
93
95
  export { applyDocumentPatch, applyRuntimeStateDelta, createDocumentLiveSync, getDocumentLiveSync, installDocumentLiveSync, runtimeEntityWriteBackCommand, subscribeDocumentLiveSyncInstall, } from "./liveSync.js";
94
96
  /** Versioned document mutation on the editorΓåöruntime live-sync stream. */
95
97
  export type { DocumentPatch, ApplyDocumentPatchResult } from "./liveSync.js";
@@ -122,3 +124,4 @@ export type { AsciiGridExportOptions } from "./gridAdapters.js";
122
124
  export type { CsvGridImportOptions } from "./gridAdapters.js";
123
125
  /** Options for exporting a grid layer as CSV. */
124
126
  export type { CsvGridExportOptions } from "./gridAdapters.js";
127
+ export { TERRAIN_MATERIALS, TERRAIN_MATERIAL_COLORS, type TerrainMaterial } from "./terrainMaterials.js";
@@ -15,3 +15,4 @@ export { applyDocumentPatch, applyRuntimeStateDelta, createDocumentLiveSync, get
15
15
  export { consumeRuntimePlayStep, createRuntimePlayControl, getRuntimeInspectorValue, planRuntimeInspectorSet, runtimeEntityMetaWriteBackCommand, summarizeRuntimeInspector, } from "./runtimeInspector.js";
16
16
  export { CURRENT_GRID_SCHEMA_VERSION, cloneGridLayer, createGridLayer, eraseGridCell, eyedropGridCell, fillGridRect, findGridPaletteEntry, floodFillGrid, forEachGridCell, getGridCell, getGridCellAtWorld, gridCellCount, gridCellEntries, gridCellKey, gridCellToWorld, gridCellsOfValue, gridEmptyValue, gridGlyphMap, inGridBounds, migrateGridLayer, paintGridCells, parseGridCellKey, resizeGridLayer, setGridCell, worldToGridCell, } from "./grid.js";
17
17
  export { exportAsciiGrid, exportCsvGrid, importAsciiGrid, importCsvGrid, } from "./gridAdapters.js";
18
+ export { TERRAIN_MATERIALS, TERRAIN_MATERIAL_COLORS } from "./terrainMaterials.js";
@@ -4,7 +4,8 @@ import type { EditorDocument, EditorVec3 } from "./types.js";
4
4
  * One versioned document mutation on the live-sync stream. `snapshot` replaces the whole
5
5
  * document; `commands` replays structural editor commands onto the current document.
6
6
  * `baseRevision` must match the receiver's current revision unless `force` is set (document
7
- * authority from the editor).
7
+ * authority from the editor). `revision` preserves the authority cursor when replaying: snapshots
8
+ * may jump forward for reconnect recovery; commands must be the next revision.
8
9
  */
9
10
  export type DocumentPatch = {
10
11
  type: "snapshot";
@@ -26,6 +27,7 @@ export type ApplyDocumentPatchResult = {
26
27
  } | {
27
28
  ok: false;
28
29
  error: string;
30
+ commandIndex?: number;
29
31
  };
30
32
  /** One live entity row the runtime may stream to the editor (play-mode inspector feed). */
31
33
  export interface RuntimeEntityState {
@@ -92,6 +94,7 @@ export interface DocumentLiveSync {
92
94
  }): ApplyDocumentPatchResult;
93
95
  /** Document-authoritative full replace (editor session mirror). */
94
96
  replaceDocument(document: EditorDocument): DocumentLiveEvent;
97
+ /** Returns a current snapshot when the requested revision predates retained history. */
95
98
  pullPatches(sinceRevision: number): DocumentPatch[];
96
99
  subscribeDocument(listener: (event: DocumentLiveEvent) => void): () => void;
97
100
  getRuntimeState(): RuntimeStateSnapshot;
@@ -8,6 +8,15 @@ const MAX_RUNTIME_LOG = 128;
8
8
  * @capability editor-live-sync apply a versioned document patch onto a live scene document
9
9
  */
10
10
  export function applyDocumentPatch(document, revision, patch, options) {
11
+ if (!Number.isSafeInteger(patch.baseRevision) || patch.baseRevision < 0) {
12
+ return { ok: false, error: "baseRevision must be a nonnegative safe integer" };
13
+ }
14
+ if (patch.revision !== undefined && (!Number.isSafeInteger(patch.revision) || patch.revision <= revision)) {
15
+ return { ok: false, error: "revision must be a safe integer newer than the current revision" };
16
+ }
17
+ if (patch.type === "commands" && patch.revision !== undefined && patch.revision !== revision + 1) {
18
+ return { ok: false, error: "commands revision must immediately follow the current revision" };
19
+ }
11
20
  if (!options?.force && patch.baseRevision !== revision) {
12
21
  return {
13
22
  ok: false,
@@ -16,7 +25,7 @@ export function applyDocumentPatch(document, revision, patch, options) {
16
25
  }
17
26
  if (patch.type === "snapshot") {
18
27
  const nextDoc = cloneEditorDocument(patch.document);
19
- const nextRevision = revision + 1;
28
+ const nextRevision = patch.revision ?? revision + 1;
20
29
  return {
21
30
  ok: true,
22
31
  document: nextDoc,
@@ -24,15 +33,17 @@ export function applyDocumentPatch(document, revision, patch, options) {
24
33
  patch: { ...patch, revision: nextRevision, document: nextDoc },
25
34
  };
26
35
  }
27
- if (patch.commands.length === 0) {
36
+ if (!Array.isArray(patch.commands) || patch.commands.length === 0) {
28
37
  return { ok: false, error: "commands patch is empty" };
29
38
  }
30
- const session = createEditorSession(cloneEditorDocument(document));
31
- for (const command of patch.commands) {
32
- session.dispatch(command);
33
- }
34
- const nextDoc = session.getState().document;
35
- const nextRevision = revision + 1;
39
+ const session = createEditorSession(document);
40
+ const before = session.getState().document;
41
+ const result = session.transaction(patch.commands);
42
+ if (!result.ok)
43
+ return result;
44
+ const changed = result.state.document !== before;
45
+ const nextDoc = changed ? result.state.document : document;
46
+ const nextRevision = changed ? revision + 1 : revision;
36
47
  return {
37
48
  ok: true,
38
49
  document: nextDoc,
@@ -146,7 +157,8 @@ export function createDocumentLiveSync(initial) {
146
157
  const result = applyDocumentPatch(document, revision, patch, options);
147
158
  if (!result.ok)
148
159
  return result;
149
- commit(result);
160
+ if (result.revision !== revision)
161
+ commit(result);
150
162
  return result;
151
163
  },
152
164
  replaceDocument(next) {
@@ -157,7 +169,11 @@ export function createDocumentLiveSync(initial) {
157
169
  return commit(result);
158
170
  },
159
171
  pullPatches(sinceRevision) {
160
- return patchLog.filter((patch) => (patch.revision ?? 0) > sinceRevision);
172
+ const patches = patchLog.filter((patch) => (patch.revision ?? 0) > sinceRevision);
173
+ if (patches.length > 0 && patches[0].baseRevision !== sinceRevision) {
174
+ return [{ type: "snapshot", baseRevision: sinceRevision, revision, document: cloneEditorDocument(document) }];
175
+ }
176
+ return patches;
161
177
  },
162
178
  subscribeDocument(listener) {
163
179
  documentListeners.add(listener);
@@ -0,0 +1,14 @@
1
+ import type { ModelConfig } from "../game/playableGame.js";
2
+ import { type MaterialAsset, type MaterialAssignment, type MaterialSelector } from "../material/materialAsset.js";
3
+ import type { EditorDocument } from "./types.js";
4
+ /** Validate and copy a material asset at the editor boundary. */
5
+ export declare function parseEditorMaterialAsset(value: unknown): MaterialAsset;
6
+ /**
7
+ * Read named assignments from authored marker metadata without inferring whole-model overrides.
8
+ * @capability authored-material-slots read validated material assignments from editor marker metadata
9
+ */
10
+ export declare function authoredMaterialAssignments(meta: Record<string, unknown> | undefined): MaterialAssignment[];
11
+ /** Compare selector meaning independently of serialized property order. @internal */
12
+ export declare function sameMaterialSelector(a: MaterialSelector, b: MaterialSelector): boolean;
13
+ /** Apply authored references; document assets and selectors override model defaults of the same identity. */
14
+ export declare function modelWithAuthoredMaterials(model: ModelConfig | undefined, document: Pick<EditorDocument, "markers" | "materialAssets">, instanceId: string): ModelConfig | undefined;
@@ -0,0 +1,38 @@
1
+ import { parseMaterialAssignments, validateMaterialAsset, validateMaterialAssignments } from "../material/materialAsset.js";
2
+ /** Validate and copy a material asset at the editor boundary. */
3
+ export function parseEditorMaterialAsset(value) {
4
+ const errors = validateMaterialAsset(value).filter((item) => item.severity === "error");
5
+ if (errors.length > 0)
6
+ throw new TypeError(errors.map((item) => `${item.path}: ${item.message}`).join("; "));
7
+ return structuredClone(value);
8
+ }
9
+ /**
10
+ * Read named assignments from authored marker metadata without inferring whole-model overrides.
11
+ * @capability authored-material-slots read validated material assignments from editor marker metadata
12
+ */
13
+ export function authoredMaterialAssignments(meta) {
14
+ return meta?.materialAssignments === undefined ? [] : parseMaterialAssignments(meta.materialAssignments);
15
+ }
16
+ /** Compare selector meaning independently of serialized property order. @internal */
17
+ export function sameMaterialSelector(a, b) {
18
+ return a.mesh === b.mesh && a.slot === b.slot && a.slotIndex === b.slotIndex;
19
+ }
20
+ /** Apply authored references; document assets and selectors override model defaults of the same identity. */
21
+ export function modelWithAuthoredMaterials(model, document, instanceId) {
22
+ if (model === undefined)
23
+ return model;
24
+ const marker = document.markers.find((item) => item.id === instanceId);
25
+ const assignments = authoredMaterialAssignments(marker?.meta);
26
+ const modelAssignments = model.materialAssignments ?? [];
27
+ if (assignments.length === 0 && modelAssignments.length === 0)
28
+ return model;
29
+ const assetsById = new Map((model.materialAssets ?? []).map((asset) => [asset.id, asset]));
30
+ for (const asset of document.materialAssets ?? [])
31
+ assetsById.set(asset.id, asset);
32
+ const assets = [...assetsById.values()];
33
+ const resolvedAssignments = [...modelAssignments.filter((item) => !assignments.some((authored) => sameMaterialSelector(item.selector, authored.selector))), ...assignments];
34
+ const errors = validateMaterialAssignments(resolvedAssignments, assets).filter((item) => item.severity === "error");
35
+ if (errors.length > 0)
36
+ throw new TypeError(errors.map((item) => `${item.path}: ${item.message}`).join("; "));
37
+ return { ...model, materialAssets: assets, materialAssignments: resolvedAssignments };
38
+ }
@@ -0,0 +1,11 @@
1
+ import type { ModelConfig } from "../game/playableGame.js";
2
+ import type { EditorMarker } from "./types.js";
3
+ /** Validate and copy exact node names; an empty list selects no hidden subtrees. */
4
+ export declare function parseModelHiddenNodes(value: unknown): readonly string[] | undefined;
5
+ /**
6
+ * Read marker visibility metadata; an empty list clears model defaults.
7
+ * @capability model-node-visibility read authored per-placement model subtree selections
8
+ */
9
+ export declare function authoredHiddenNodes(meta: Record<string, unknown> | undefined): readonly string[] | undefined;
10
+ /** Apply a placement's authored visibility without erasing omitted game model defaults. */
11
+ export declare function modelWithAuthoredNodeVisibility(model: ModelConfig | undefined, marker: Pick<EditorMarker, "meta"> | undefined): ModelConfig | undefined;
@@ -0,0 +1,29 @@
1
+ /** Validate and copy exact node names; an empty list selects no hidden subtrees. */
2
+ export function parseModelHiddenNodes(value) {
3
+ if (value === undefined)
4
+ return undefined;
5
+ if (!Array.isArray(value)) {
6
+ throw new TypeError("hiddenNodes must be an array of nonblank node names");
7
+ }
8
+ for (const name of value) {
9
+ if (typeof name !== "string" || name.trim().length === 0)
10
+ throw new TypeError("hiddenNodes must be an array of nonblank node names");
11
+ }
12
+ return [...value];
13
+ }
14
+ /**
15
+ * Read marker visibility metadata; an empty list clears model defaults.
16
+ * @capability model-node-visibility read authored per-placement model subtree selections
17
+ */
18
+ export function authoredHiddenNodes(meta) {
19
+ return parseModelHiddenNodes(meta?.hiddenNodes);
20
+ }
21
+ /** Apply a placement's authored visibility without erasing omitted game model defaults. */
22
+ export function modelWithAuthoredNodeVisibility(model, marker) {
23
+ if (model === undefined)
24
+ return undefined;
25
+ const hiddenNodes = authoredHiddenNodes(marker?.meta);
26
+ if (hiddenNodes === undefined || (model.hiddenNodes?.length === hiddenNodes.length && hiddenNodes.every((name, index) => model.hiddenNodes[index] === name)))
27
+ return model;
28
+ return { ...model, hiddenNodes };
29
+ }
@@ -0,0 +1,90 @@
1
+ import type { PlayerMovementConfig } from "../game/playableGame.js";
2
+ import type { PhysicsConfig } from "../game/defineGame.js";
3
+ import type { ParamSchema } from "../scene/sceneKinds.js";
4
+ import type { EditorCatalogData } from "./types.js";
5
+ /** Numeric character controls a game elects to author through its own named catalog rows. */
6
+ export interface AuthoredMovementValues {
7
+ walkSpeed?: number;
8
+ gravity?: number;
9
+ jumpVelocity?: number;
10
+ stepHeight?: number;
11
+ /** Optional positive heightfield collision span from feet; independent of model and camera scale. */
12
+ collisionHeight?: number;
13
+ /** Optional heightfield rise/run limit; omitted controls retain the game's movement policy. */
14
+ maxClimbGrade?: number;
15
+ groundAcceleration?: number;
16
+ airAcceleration?: number;
17
+ groundFriction?: number;
18
+ runMultiplier?: number;
19
+ crouchMultiplier?: number;
20
+ jumpBufferMs?: number;
21
+ coyoteMs?: number;
22
+ /** Rising speed retained on early jump release, from 0 to 1. */
23
+ jumpCutFactor?: number;
24
+ apexGravityScale?: number;
25
+ apexSpeed?: number;
26
+ fallGravityScale?: number;
27
+ landingRecoveryMs?: number;
28
+ /** Initial landing speed multiplier; values above 1 retain the existing runtime contract. */
29
+ landingSpeedScale?: number;
30
+ }
31
+ /** Only authored values, ready to overlay on a game's existing movement and physics policy. */
32
+ export interface AuthoredMovementConfig {
33
+ readonly walkSpeed?: number;
34
+ readonly physics?: {
35
+ readonly gravity?: number;
36
+ readonly jumpVelocity?: number;
37
+ };
38
+ readonly movement?: Pick<PlayerMovementConfig, "stepHeight" | "collisionHeight" | "maxClimbGrade" | "feel">;
39
+ }
40
+ /** A persisted movement value rejected before it can replace the game's current setting. */
41
+ export interface AuthoredMovementDiagnostic {
42
+ readonly path: string;
43
+ readonly message: string;
44
+ readonly repair: string;
45
+ }
46
+ /** Valid partial tuning plus located rejected values; absent rows leave game defaults intact. */
47
+ export interface AuthoredMovementResult {
48
+ readonly config: AuthoredMovementConfig;
49
+ readonly diagnostics: readonly AuthoredMovementDiagnostic[];
50
+ }
51
+ /** Structural authored catalog input; an immutable EditorDocument satisfies it. */
52
+ export interface MovementCatalogDocumentLike {
53
+ readonly catalogs: readonly EditorCatalogData[];
54
+ }
55
+ /** Game-owned callbacks, collision and physics settings retained beneath authored numeric controls. */
56
+ export interface AuthoredMovementDefaults {
57
+ readonly movement?: PlayerMovementConfig;
58
+ readonly physics?: PhysicsConfig;
59
+ }
60
+ /** Stable runtime objects whose numeric getters resolve the current authored document. */
61
+ export interface AuthoredMovementBinding {
62
+ readonly movement: PlayerMovementConfig;
63
+ readonly physics: PhysicsConfig;
64
+ readonly diagnostics: readonly AuthoredMovementDiagnostic[];
65
+ }
66
+ /**
67
+ * Creates editor/RPC controls only for the values a game supplies, with those game's defaults.
68
+ * Gravity retains the game's sign convention; collision height is positive, other controls nonnegative.
69
+ * Jump release is bounded to 0..1; supplied deeper jump controls share a collapsed response group.
70
+ * @capability editor-movement author game-chosen character movement controls through ordinary catalog rows
71
+ */
72
+ export declare function createMovementSchema(defaults: AuthoredMovementValues): ParamSchema;
73
+ /**
74
+ * Reads one game-named row as partial runtime tuning, retaining game defaults for omitted or invalid
75
+ * values. Rejected values include their document path and repair; no policy callbacks are replaced.
76
+ * @capability editor-movement resolve saved movement controls without replacing a game's character rules
77
+ */
78
+ export declare function readAuthoredMovement(document: MovementCatalogDocumentLike, catalogId: string, entryId: string): AuthoredMovementResult;
79
+ /**
80
+ * Memoizes movement resolution by immutable document identity; reads the latest document on every
81
+ * call while catalog lookup and diagnostics run only after authoring changes.
82
+ * @capability editor-movement consume live saved movement tuning with bounded per-frame work
83
+ */
84
+ export declare function createAuthoredMovementReader(getDocument: () => MovementCatalogDocumentLike, catalogId: string, entryId: string): () => AuthoredMovementResult;
85
+ /**
86
+ * Binds live authored numeric controls onto stable runtime objects, preserving the game's callbacks,
87
+ * collision, backend and other policy. The movement controller reads these getters each step.
88
+ * @capability editor-movement bind live character tuning while preserving distinctive game policy
89
+ */
90
+ export declare function bindAuthoredMovement(read: () => AuthoredMovementResult, defaults: AuthoredMovementDefaults): AuthoredMovementBinding;