@jgengine/core 0.11.0 → 0.13.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 (256) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat/effects.d.ts +11 -4
  9. package/dist/combat/effects.js +24 -17
  10. package/dist/combat/magazine.d.ts +13 -0
  11. package/dist/combat/magazine.js +27 -0
  12. package/dist/combat/vfxPresets.d.ts +176 -0
  13. package/dist/combat/vfxPresets.js +74 -0
  14. package/dist/combat/weaponFire.d.ts +114 -0
  15. package/dist/combat/weaponFire.js +96 -0
  16. package/dist/combat.d.ts +5 -2
  17. package/dist/combat.js +4 -1
  18. package/dist/devtools/controls.d.ts +38 -0
  19. package/dist/devtools/controls.js +181 -0
  20. package/dist/devtools/devtools.d.ts +5 -197
  21. package/dist/devtools/devtools.js +38 -774
  22. package/dist/devtools/discover.d.ts +32 -0
  23. package/dist/devtools/discover.js +240 -0
  24. package/dist/devtools/fallbackSeams.d.ts +50 -0
  25. package/dist/devtools/fallbackSeams.js +111 -0
  26. package/dist/devtools/frame.d.ts +15 -0
  27. package/dist/devtools/frame.js +247 -0
  28. package/dist/devtools/logs.d.ts +15 -0
  29. package/dist/devtools/logs.js +86 -0
  30. package/dist/devtools/overrides.d.ts +16 -0
  31. package/dist/devtools/overrides.js +66 -0
  32. package/dist/devtools/types.d.ts +212 -0
  33. package/dist/devtools/types.js +1 -0
  34. package/dist/devtools/urlFlags.d.ts +22 -0
  35. package/dist/devtools/urlFlags.js +62 -0
  36. package/dist/economy/auctionBook.d.ts +122 -0
  37. package/dist/economy/auctionBook.js +213 -0
  38. package/dist/economy/priceHistory.d.ts +44 -0
  39. package/dist/economy/priceHistory.js +89 -0
  40. package/dist/economy/unlockPoints.d.ts +92 -0
  41. package/dist/economy/unlockPoints.js +103 -0
  42. package/dist/editor/commandApply.d.ts +5 -0
  43. package/dist/editor/commandApply.js +755 -0
  44. package/dist/editor/commands.d.ts +48 -3
  45. package/dist/editor/commands.js +40 -754
  46. package/dist/editor/directives.d.ts +94 -0
  47. package/dist/editor/directives.js +206 -0
  48. package/dist/editor/document.d.ts +37 -2
  49. package/dist/editor/document.js +422 -6
  50. package/dist/editor/entityCatalog.d.ts +42 -0
  51. package/dist/editor/entityCatalog.js +101 -0
  52. package/dist/editor/environment.d.ts +74 -0
  53. package/dist/editor/environment.js +169 -0
  54. package/dist/editor/index.d.ts +26 -1
  55. package/dist/editor/index.js +8 -1
  56. package/dist/editor/minimap.d.ts +31 -0
  57. package/dist/editor/minimap.js +73 -0
  58. package/dist/editor/streamer.d.ts +66 -0
  59. package/dist/editor/streamer.js +68 -0
  60. package/dist/editor/types.d.ts +143 -0
  61. package/dist/editor/world.d.ts +136 -0
  62. package/dist/editor/world.js +303 -0
  63. package/dist/game/breeding.d.ts +161 -0
  64. package/dist/game/breeding.js +181 -0
  65. package/dist/game/cameraConfig.d.ts +451 -0
  66. package/dist/game/cameraConfig.js +1 -0
  67. package/dist/game/clipRoles.d.ts +51 -0
  68. package/dist/game/clipRoles.js +142 -0
  69. package/dist/game/defineGame.d.ts +16 -9
  70. package/dist/game/defineGame.js +14 -6
  71. package/dist/game/defineSystem.d.ts +2 -0
  72. package/dist/game/defineSystem.js +2 -0
  73. package/dist/game/events.d.ts +28 -0
  74. package/dist/game/partAnimation.d.ts +78 -0
  75. package/dist/game/partAnimation.js +147 -0
  76. package/dist/game/playableGame.d.ts +72 -440
  77. package/dist/game/playableGame.js +1 -1
  78. package/dist/game/progression.d.ts +35 -0
  79. package/dist/game/progression.js +22 -1
  80. package/dist/game/quest.js +1 -3
  81. package/dist/game/tribe.d.ts +184 -0
  82. package/dist/game/tribe.js +432 -0
  83. package/dist/gameplay.d.ts +3 -1
  84. package/dist/gameplay.js +3 -1
  85. package/dist/inventory/encumbrance.d.ts +69 -0
  86. package/dist/inventory/encumbrance.js +68 -0
  87. package/dist/inventory/inventoryModel.d.ts +24 -8
  88. package/dist/inventory/inventoryModel.js +47 -8
  89. package/dist/item/durability.js +1 -3
  90. package/dist/meta/changelog.d.ts +1 -1
  91. package/dist/meta/changelog.js +48 -1
  92. package/dist/movement/movementModel.d.ts +15 -1
  93. package/dist/movement/movementModel.js +33 -15
  94. package/dist/movement/playerMovement.js +102 -2
  95. package/dist/multiplayer/combatSnapshot.js +13 -8
  96. package/dist/multiplayer/presenceModel.d.ts +2 -0
  97. package/dist/multiplayer/presenceModel.js +8 -0
  98. package/dist/physics/drivableVehicle.d.ts +2 -0
  99. package/dist/physics/drivableVehicle.js +1 -1
  100. package/dist/physics/flightDynamics.d.ts +82 -0
  101. package/dist/physics/flightDynamics.js +165 -0
  102. package/dist/physics/gravityField.d.ts +25 -0
  103. package/dist/physics/gravityField.js +41 -0
  104. package/dist/physics/kinematicVehicle.d.ts +87 -0
  105. package/dist/physics/kinematicVehicle.js +193 -11
  106. package/dist/physics/physicsWorld.d.ts +26 -0
  107. package/dist/physics/physicsWorld.js +109 -117
  108. package/dist/physics/vehicleObstacles.d.ts +44 -0
  109. package/dist/physics/vehicleObstacles.js +115 -0
  110. package/dist/random/rng.d.ts +4 -1
  111. package/dist/random/rng.js +4 -1
  112. package/dist/runtime/adapter.d.ts +13 -0
  113. package/dist/runtime/adapter.js +13 -0
  114. package/dist/runtime/context/combat.d.ts +51 -0
  115. package/dist/runtime/context/combat.js +131 -0
  116. package/dist/runtime/context/combatFx.js +15 -5
  117. package/dist/runtime/context/deathLoot.d.ts +30 -0
  118. package/dist/runtime/context/deathLoot.js +39 -0
  119. package/dist/runtime/context/player.d.ts +48 -0
  120. package/dist/runtime/context/player.js +125 -0
  121. package/dist/runtime/context/scene.d.ts +60 -0
  122. package/dist/runtime/context/scene.js +329 -0
  123. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  124. package/dist/runtime/context/snapshotCodecs.js +26 -0
  125. package/dist/runtime/descriptors/baseline.js +26 -3
  126. package/dist/runtime/descriptors/features.d.ts +5 -0
  127. package/dist/runtime/descriptors/features.js +20 -2
  128. package/dist/runtime/gameContext.d.ts +2 -428
  129. package/dist/runtime/gameContext.js +133 -500
  130. package/dist/runtime/gameContextTypes.d.ts +500 -0
  131. package/dist/runtime/gameContextTypes.js +1 -0
  132. package/dist/runtime/headlessRunner.d.ts +17 -2
  133. package/dist/runtime/headlessRunner.js +15 -0
  134. package/dist/runtime/hostPolicy.d.ts +51 -0
  135. package/dist/runtime/hostPolicy.js +68 -0
  136. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  137. package/dist/runtime/hostedGameRunner.js +1 -0
  138. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  139. package/dist/runtime/hostedWorldSession.js +1 -0
  140. package/dist/runtime/worldSnapshot.d.ts +13 -1
  141. package/dist/runtime/worldSnapshot.js +15 -3
  142. package/dist/scene/assetCatalog.d.ts +7 -0
  143. package/dist/scene/assetSpace.js +1 -3
  144. package/dist/scene/authoredTriggers.d.ts +50 -0
  145. package/dist/scene/authoredTriggers.js +114 -0
  146. package/dist/scene/builtinSceneKinds.js +8 -3
  147. package/dist/scene/colliders.d.ts +79 -0
  148. package/dist/scene/colliders.js +170 -0
  149. package/dist/scene/collisionMesh.d.ts +94 -0
  150. package/dist/scene/collisionMesh.js +699 -0
  151. package/dist/scene/companion.d.ts +147 -0
  152. package/dist/scene/companion.js +193 -0
  153. package/dist/scene/entityStats.d.ts +7 -9
  154. package/dist/scene/entityStats.js +19 -15
  155. package/dist/scene/sceneKinds.d.ts +16 -0
  156. package/dist/scene/sceneKinds.js +4 -0
  157. package/dist/scene/sceneRaycast.js +15 -0
  158. package/dist/stats/spawnLevelStats.d.ts +110 -0
  159. package/dist/stats/spawnLevelStats.js +140 -0
  160. package/dist/stats/statModifiers.d.ts +16 -0
  161. package/dist/stats/statModifiers.js +30 -1
  162. package/dist/stats/statPool.d.ts +70 -0
  163. package/dist/stats/statPool.js +62 -0
  164. package/dist/store/changeSignal.d.ts +3 -0
  165. package/dist/store/changeSignal.js +2 -0
  166. package/dist/store/observableKeyedStore.d.ts +2 -0
  167. package/dist/store/observableKeyedStore.js +1 -0
  168. package/dist/survival/decayMeter.js +1 -3
  169. package/dist/survival/regionHealth.js +1 -3
  170. package/dist/ui/hudLayout.d.ts +5 -0
  171. package/dist/ui/hudLayout.js +17 -1
  172. package/dist/ui/panelModel.d.ts +120 -0
  173. package/dist/ui/panelModel.js +168 -0
  174. package/dist/ui.d.ts +1 -0
  175. package/dist/ui.js +1 -0
  176. package/dist/world/authoredEntities.d.ts +29 -0
  177. package/dist/world/authoredEntities.js +29 -0
  178. package/dist/world/authoredObjects.d.ts +15 -1
  179. package/dist/world/authoredObjects.js +13 -1
  180. package/dist/world/authoredSpawn.d.ts +39 -0
  181. package/dist/world/authoredSpawn.js +37 -0
  182. package/dist/world/buildSockets.d.ts +152 -0
  183. package/dist/world/buildSockets.js +188 -0
  184. package/dist/world/buildingLots.d.ts +84 -0
  185. package/dist/world/buildingLots.js +119 -0
  186. package/dist/world/buildings.d.ts +6 -0
  187. package/dist/world/cityBlocks.d.ts +152 -0
  188. package/dist/world/cityBlocks.js +670 -0
  189. package/dist/world/cityContent.d.ts +77 -0
  190. package/dist/world/cityContent.js +261 -0
  191. package/dist/world/cityGenerator.d.ts +41 -0
  192. package/dist/world/cityGenerator.js +58 -0
  193. package/dist/world/cityGeometry.d.ts +114 -0
  194. package/dist/world/cityGeometry.js +573 -0
  195. package/dist/world/cityKind.d.ts +314 -0
  196. package/dist/world/cityKind.js +1578 -0
  197. package/dist/world/environmentSummary.js +57 -1
  198. package/dist/world/features.d.ts +53 -5
  199. package/dist/world/features.js +15 -3
  200. package/dist/world/grassKind.js +2 -2
  201. package/dist/world/markers.d.ts +50 -4
  202. package/dist/world/markers.js +49 -0
  203. package/dist/world/massing.js +1 -1
  204. package/dist/world/minimapBake.d.ts +78 -0
  205. package/dist/world/minimapBake.js +208 -0
  206. package/dist/world/minimapTrack.d.ts +17 -0
  207. package/dist/world/minimapTrack.js +22 -0
  208. package/dist/world/place.d.ts +166 -0
  209. package/dist/world/place.js +96 -0
  210. package/dist/world/placeAsset.js +3 -1
  211. package/dist/world/poleLineKind.d.ts +40 -0
  212. package/dist/world/poleLineKind.js +97 -0
  213. package/dist/world/polyline.js +9 -2
  214. package/dist/world/resourceNode.d.ts +109 -0
  215. package/dist/world/resourceNode.js +140 -0
  216. package/dist/world/roads.d.ts +19 -2
  217. package/dist/world/roads.js +60 -3
  218. package/dist/world/scatter.js +2 -20
  219. package/dist/world/streetGenerator.d.ts +159 -0
  220. package/dist/world/streetGenerator.js +684 -0
  221. package/dist/world/streets.d.ts +103 -1
  222. package/dist/world/streets.js +273 -0
  223. package/dist/world/terraform.js +58 -88
  224. package/dist/world/terrain.d.ts +65 -1
  225. package/dist/world/terrain.js +119 -2
  226. package/dist/world.d.ts +14 -5
  227. package/dist/world.js +12 -3
  228. package/package.json +1 -1
  229. package/skills/game-design/SKILL.md +28 -0
  230. package/skills/game-design/agents/openai.yaml +4 -0
  231. package/skills/game-design/references/application-playbook.md +170 -0
  232. package/skills/game-design/references/field-guide.md +288 -0
  233. package/skills/jgengine/SKILL.md +8 -4
  234. package/skills/jgengine/api.md +182 -81
  235. package/skills/jgengine/capabilities.md +104 -2
  236. package/skills/jgengine/recipes/minimal-game.md +88 -0
  237. package/skills/jgengine-combat/SKILL.md +19 -1
  238. package/skills/jgengine-combat/api.md +75 -2
  239. package/skills/jgengine-combat/capabilities.md +61 -1
  240. package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
  241. package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
  242. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  243. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
  244. package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
  245. package/skills/jgengine-combat/reference.md +33 -4
  246. package/skills/jgengine-gameplay/SKILL.md +8 -0
  247. package/skills/jgengine-gameplay/api.md +182 -26
  248. package/skills/jgengine-gameplay/capabilities.md +96 -2
  249. package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
  250. package/skills/jgengine-gameplay/reference-systems.md +10 -5
  251. package/skills/jgengine-world/SKILL.md +13 -0
  252. package/skills/jgengine-world/api.md +453 -21
  253. package/skills/jgengine-world/capabilities.md +112 -2
  254. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  255. package/skills/jgengine-world/reference.md +43 -4
  256. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -0,0 +1,755 @@
1
+ import { applyDeltaToSnapshot, applySurfaceDeltaToSnapshot, applyWeightDeltaToSnapshot, } from "../world/terraform.js";
2
+ import { patchUiPanel, removeUiPanel } from "../ui/hudDocument.js";
3
+ import { cloneEditorDocument, collectDescendants, createPrefabFragment, editorDocumentExtras, ensureUniqueEditorId, extractEditorFragment, findEditorCollection, findEditorMarker, findEditorPrefab, findEditorVolume, importEditorDocumentJson, isEditorObjectLocked, wouldCreateCycle, } from "./document.js";
4
+ import { fillGridRect, floodFillGrid, migrateGridLayer, paintGridCells, resizeGridLayer, } from "./grid.js";
5
+ import { parseParams } from "../scene/sceneKinds.js";
6
+ /** Pure document mutation helpers + exhaustive EditorCommand apply table. */
7
+ function removeByIds(doc, ids) {
8
+ return {
9
+ version: 1,
10
+ markers: doc.markers.filter((marker) => !ids.has(marker.id)),
11
+ volumes: doc.volumes.filter((volume) => !ids.has(volume.id)),
12
+ paths: doc.paths.filter((path) => !ids.has(path.id)),
13
+ annotations: doc.annotations.filter((note) => !ids.has(note.id)),
14
+ ...editorDocumentExtras(doc),
15
+ collections: doc.collections.map((collection) => ({
16
+ ...collection,
17
+ memberIds: collection.memberIds.filter((id) => !ids.has(id)),
18
+ })),
19
+ };
20
+ }
21
+ function shifted(point, delta) {
22
+ return { x: point.x + delta.x, y: point.y + delta.y, z: point.z + delta.z };
23
+ }
24
+ function translateByIds(doc, ids, delta) {
25
+ return {
26
+ version: 1,
27
+ markers: doc.markers.map((marker) => ids.has(marker.id) ? { ...marker, position: shifted(marker.position, delta) } : marker),
28
+ volumes: doc.volumes.map((volume) => ids.has(volume.id) ? { ...volume, center: shifted(volume.center, delta) } : volume),
29
+ paths: doc.paths.map((path) => ids.has(path.id) ? { ...path, points: path.points.map((point) => shifted(point, delta)) } : path),
30
+ annotations: doc.annotations.map((note) => ids.has(note.id) ? { ...note, position: shifted(note.position, delta) } : note),
31
+ ...editorDocumentExtras(doc),
32
+ };
33
+ }
34
+ function collectIds(doc) {
35
+ const all = new Set();
36
+ for (const marker of doc.markers)
37
+ all.add(marker.id);
38
+ for (const volume of doc.volumes)
39
+ all.add(volume.id);
40
+ for (const path of doc.paths)
41
+ all.add(path.id);
42
+ for (const note of doc.annotations)
43
+ all.add(note.id);
44
+ return all;
45
+ }
46
+ /**
47
+ * Adding to a collection replaces a same-id member in that same collection (the existing
48
+ * upsert behavior); anything else collides with the document-global id space shared by markers,
49
+ * volumes, paths, and annotations, so it gets re-idded instead of shadowing whatever already
50
+ * holds that id in a different collection.
51
+ */
52
+ function ownIdOrGloballyUnique(doc, item, ownCollection) {
53
+ if (ownCollection.some((entry) => entry.id === item.id))
54
+ return item;
55
+ const id = ensureUniqueEditorId(item.id, collectIds(doc));
56
+ return id === item.id ? item : { ...item, id };
57
+ }
58
+ function copyId(base, taken) {
59
+ let candidate = `${base}_copy`;
60
+ let n = 2;
61
+ while (taken.has(candidate)) {
62
+ candidate = `${base}_copy${n}`;
63
+ n += 1;
64
+ }
65
+ taken.add(candidate);
66
+ return candidate;
67
+ }
68
+ function insertFragment(state, fragment, offset, renameAll) {
69
+ const doc = state.document;
70
+ const taken = collectIds(doc);
71
+ const clones = cloneEditorDocument(fragment);
72
+ const newIds = [];
73
+ const withId = (item) => {
74
+ const id = renameAll || taken.has(item.id) ? copyId(item.id, taken) : item.id;
75
+ taken.add(id);
76
+ newIds.push(id);
77
+ return { ...item, id };
78
+ };
79
+ const markers = clones.markers.map((marker) => withId({ ...marker, position: shifted(marker.position, offset) }));
80
+ const volumes = clones.volumes.map((volume) => withId({ ...volume, center: shifted(volume.center, offset) }));
81
+ const paths = clones.paths.map((path) => withId({ ...path, points: path.points.map((point) => shifted(point, offset)) }));
82
+ const annotations = clones.annotations.map((note) => withId({ ...note, position: shifted(note.position, offset) }));
83
+ if (newIds.length === 0)
84
+ return state;
85
+ return {
86
+ document: {
87
+ version: 1,
88
+ markers: [...doc.markers, ...markers],
89
+ volumes: [...doc.volumes, ...volumes],
90
+ paths: [...doc.paths, ...paths],
91
+ annotations: [...doc.annotations, ...annotations],
92
+ ...editorDocumentExtras(doc),
93
+ },
94
+ selection: newIds,
95
+ };
96
+ }
97
+ function fragmentAsDocument(fragment) {
98
+ return {
99
+ version: 1,
100
+ markers: [...fragment.markers],
101
+ volumes: [...fragment.volumes],
102
+ paths: [...fragment.paths],
103
+ annotations: [...fragment.annotations],
104
+ prefabs: [],
105
+ collections: [],
106
+ catalogs: [],
107
+ };
108
+ }
109
+ function tagWithInstance(item, prefabId, instanceId) {
110
+ return { ...item, meta: { ...item.meta, prefabId, prefabInstanceId: instanceId } };
111
+ }
112
+ /**
113
+ * Replaces one grid layer in place via `transform`, returning `null` when the layer is missing and
114
+ * the same state when the transform is a no-op (so history skips it).
115
+ */
116
+ function updateGridLayer(state, id, transform) {
117
+ const grids = state.document.grids;
118
+ if (grids === undefined)
119
+ return null;
120
+ const index = grids.findIndex((layer) => layer.id === id);
121
+ if (index < 0)
122
+ return null;
123
+ const next = transform(grids[index]);
124
+ if (next === grids[index])
125
+ return state;
126
+ const updated = grids.map((layer, i) => (i === index ? next : layer));
127
+ return { ...state, document: { ...state.document, grids: updated } };
128
+ }
129
+ /**
130
+ * Maps a single `fn` over all four placeable collections (markers/volumes/paths/annotations),
131
+ * returning just those four fields for spreading back into a document. Notes lack a `label` field,
132
+ * so callers that treat notes differently pass a dedicated `fnNote`; by default notes share `fn`.
133
+ */
134
+ function mapPlaceables(doc, fn, fnNote = fn) {
135
+ return {
136
+ markers: doc.markers.map(fn),
137
+ volumes: doc.volumes.map(fn),
138
+ paths: doc.paths.map(fn),
139
+ annotations: doc.annotations.map(fnNote),
140
+ };
141
+ }
142
+ const mutationHandlers = {
143
+ select: (state, command) => ({ ...state, selection: [...command.ids] }),
144
+ clearSelection: (state) => ({ ...state, selection: [] }),
145
+ setTransform: (state, command) => {
146
+ if (isEditorObjectLocked(state.document, command.id))
147
+ return null;
148
+ const marker = findEditorMarker(state.document, command.id);
149
+ const volume = findEditorVolume(state.document, command.id);
150
+ const note = state.document.annotations.find((n) => n.id === command.id);
151
+ const previous = marker?.position ?? volume?.center ?? note?.position;
152
+ // Moving a parent to a new position carries its whole subtree by the same delta.
153
+ const childDelta = command.position === undefined || previous === undefined
154
+ ? null
155
+ : {
156
+ x: command.position.x - previous.x,
157
+ y: command.position.y - previous.y,
158
+ z: command.position.z - previous.z,
159
+ };
160
+ const descendants = childDelta === null ? new Set() : collectDescendants(state.document, [command.id]);
161
+ const shift = (point) => childDelta === null ? point : shifted(point, childDelta);
162
+ const markers = state.document.markers.map((entry) => {
163
+ if (entry.id === command.id) {
164
+ return {
165
+ ...entry,
166
+ position: command.position === undefined ? entry.position : { ...command.position },
167
+ ...(command.rotationY === undefined ? {} : { rotationY: command.rotationY }),
168
+ };
169
+ }
170
+ return descendants.has(entry.id) ? { ...entry, position: shift(entry.position) } : entry;
171
+ });
172
+ const volumes = state.document.volumes.map((entry) => {
173
+ if (entry.id === command.id)
174
+ return command.position === undefined ? entry : { ...entry, center: { ...command.position } };
175
+ return descendants.has(entry.id) ? { ...entry, center: shift(entry.center) } : entry;
176
+ });
177
+ const paths = state.document.paths.map((entry) => descendants.has(entry.id) ? { ...entry, points: entry.points.map(shift) } : entry);
178
+ const annotations = state.document.annotations.map((entry) => {
179
+ if (entry.id === command.id)
180
+ return command.position === undefined ? entry : { ...entry, position: { ...command.position } };
181
+ return descendants.has(entry.id) ? { ...entry, position: shift(entry.position) } : entry;
182
+ });
183
+ return {
184
+ ...state,
185
+ document: { ...state.document, markers, volumes, paths, annotations },
186
+ };
187
+ },
188
+ translate: (state, command) => {
189
+ const movable = command.ids.filter((id) => !isEditorObjectLocked(state.document, id));
190
+ if (movable.length === 0)
191
+ return null;
192
+ const ids = new Set(movable);
193
+ for (const descendant of collectDescendants(state.document, movable))
194
+ ids.add(descendant);
195
+ return {
196
+ ...state,
197
+ document: translateByIds(state.document, ids, command.delta),
198
+ };
199
+ },
200
+ setParent: (state, command) => {
201
+ const targets = command.ids.filter((id) => !wouldCreateCycle(state.document, id, command.parentId));
202
+ if (targets.length === 0)
203
+ return state;
204
+ const wanted = new Set(targets);
205
+ const reparent = (entry) => {
206
+ if (!wanted.has(entry.id))
207
+ return entry;
208
+ if (command.parentId === null) {
209
+ if (entry.parentId === undefined)
210
+ return entry;
211
+ const next = { ...entry };
212
+ delete next.parentId;
213
+ return next;
214
+ }
215
+ return { ...entry, parentId: command.parentId };
216
+ };
217
+ return { ...state, document: { ...state.document, ...mapPlaceables(state.document, reparent) } };
218
+ },
219
+ addMarker: (state, command) => {
220
+ const marker = ownIdOrGloballyUnique(state.document, command.marker, state.document.markers);
221
+ return {
222
+ ...state,
223
+ document: {
224
+ ...state.document,
225
+ markers: [...state.document.markers.filter((m) => m.id !== marker.id), marker],
226
+ },
227
+ selection: [marker.id],
228
+ };
229
+ },
230
+ addVolume: (state, command) => {
231
+ const volume = ownIdOrGloballyUnique(state.document, command.volume, state.document.volumes);
232
+ return {
233
+ ...state,
234
+ document: {
235
+ ...state.document,
236
+ volumes: [...state.document.volumes.filter((v) => v.id !== volume.id), volume],
237
+ },
238
+ selection: [volume.id],
239
+ };
240
+ },
241
+ addPath: (state, command) => {
242
+ const path = ownIdOrGloballyUnique(state.document, command.path, state.document.paths);
243
+ return {
244
+ ...state,
245
+ document: {
246
+ ...state.document,
247
+ paths: [...state.document.paths.filter((p) => p.id !== path.id), path],
248
+ },
249
+ selection: [path.id],
250
+ };
251
+ },
252
+ addNote: (state, command) => {
253
+ const note = ownIdOrGloballyUnique(state.document, command.note, state.document.annotations);
254
+ return {
255
+ ...state,
256
+ document: {
257
+ ...state.document,
258
+ annotations: [...state.document.annotations.filter((n) => n.id !== note.id), note],
259
+ },
260
+ selection: [note.id],
261
+ };
262
+ },
263
+ setMarker: (state, command) => {
264
+ const markers = state.document.markers.map((marker) => {
265
+ if (marker.id !== command.id)
266
+ return marker;
267
+ return {
268
+ ...marker,
269
+ ...command.patch,
270
+ position: command.patch.position === undefined ? marker.position : { ...command.patch.position },
271
+ meta: command.patch.meta === undefined ? marker.meta : { ...command.patch.meta },
272
+ };
273
+ });
274
+ return { ...state, document: { ...state.document, markers } };
275
+ },
276
+ setVolume: (state, command) => {
277
+ const volumes = state.document.volumes.map((volume) => {
278
+ if (volume.id !== command.id)
279
+ return volume;
280
+ return {
281
+ ...volume,
282
+ ...command.patch,
283
+ center: command.patch.center === undefined ? volume.center : { ...command.patch.center },
284
+ halfExtents: command.patch.halfExtents === undefined
285
+ ? volume.halfExtents
286
+ : { ...command.patch.halfExtents },
287
+ meta: command.patch.meta === undefined ? volume.meta : { ...command.patch.meta },
288
+ };
289
+ });
290
+ return { ...state, document: { ...state.document, volumes } };
291
+ },
292
+ setPath: (state, command) => {
293
+ const paths = state.document.paths.map((path) => {
294
+ if (path.id !== command.id)
295
+ return path;
296
+ return {
297
+ ...path,
298
+ ...command.patch,
299
+ points: command.patch.points === undefined
300
+ ? path.points
301
+ : command.patch.points.map((point) => ({ ...point })),
302
+ meta: command.patch.meta === undefined ? path.meta : { ...command.patch.meta },
303
+ };
304
+ });
305
+ return { ...state, document: { ...state.document, paths } };
306
+ },
307
+ setNote: (state, command) => {
308
+ const annotations = state.document.annotations.map((note) => {
309
+ if (note.id !== command.id)
310
+ return note;
311
+ return {
312
+ ...note,
313
+ ...command.patch,
314
+ position: command.patch.position === undefined ? note.position : { ...command.patch.position },
315
+ meta: command.patch.meta === undefined ? note.meta : { ...command.patch.meta },
316
+ };
317
+ });
318
+ return { ...state, document: { ...state.document, annotations } };
319
+ },
320
+ setCatalogEntry: (state, command) => {
321
+ const catalog = state.document.catalogs.find((entry) => entry.id === command.catalogId);
322
+ if (catalog === undefined)
323
+ return null;
324
+ if (!catalog.entries.some((entry) => entry.id === command.entryId))
325
+ return null;
326
+ const catalogs = state.document.catalogs.map((row) => {
327
+ if (row.id !== command.catalogId)
328
+ return row;
329
+ return {
330
+ ...row,
331
+ entries: row.entries.map((entry) => {
332
+ if (entry.id !== command.entryId)
333
+ return entry;
334
+ const next = { id: entry.id };
335
+ const label = command.patch.label ?? entry.label;
336
+ if (label !== undefined)
337
+ next.label = label;
338
+ if (command.patch.meta !== undefined)
339
+ next.meta = { ...entry.meta, ...command.patch.meta };
340
+ else if (entry.meta !== undefined)
341
+ next.meta = { ...entry.meta };
342
+ return next;
343
+ }),
344
+ };
345
+ });
346
+ return { ...state, document: { ...state.document, catalogs } };
347
+ },
348
+ addCatalogEntry: (state, command) => {
349
+ const trimmedId = command.entry.id.trim();
350
+ if (trimmedId.length === 0)
351
+ return null;
352
+ const existing = state.document.catalogs.find((catalog) => catalog.id === command.catalogId);
353
+ const taken = new Set((existing?.entries ?? []).map((entry) => entry.id));
354
+ let id = trimmedId;
355
+ let n = 2;
356
+ while (taken.has(id)) {
357
+ id = `${trimmedId}_${n}`;
358
+ n += 1;
359
+ }
360
+ const entry = {
361
+ id,
362
+ ...(command.entry.label === undefined ? {} : { label: command.entry.label }),
363
+ ...(command.entry.meta === undefined ? {} : { meta: { ...command.entry.meta } }),
364
+ };
365
+ const catalogs = existing === undefined
366
+ ? [...state.document.catalogs, { id: command.catalogId, entries: [entry] }]
367
+ : state.document.catalogs.map((catalog) => catalog.id === command.catalogId ? { ...catalog, entries: [...catalog.entries, entry] } : catalog);
368
+ return { ...state, document: { ...state.document, catalogs } };
369
+ },
370
+ removeCatalogEntry: (state, command) => {
371
+ const catalog = state.document.catalogs.find((entry) => entry.id === command.catalogId);
372
+ if (catalog === undefined || !catalog.entries.some((entry) => entry.id === command.entryId))
373
+ return null;
374
+ const catalogs = state.document.catalogs.map((row) => row.id === command.catalogId
375
+ ? { ...row, entries: row.entries.filter((entry) => entry.id !== command.entryId) }
376
+ : row);
377
+ return { ...state, document: { ...state.document, catalogs } };
378
+ },
379
+ addCatalog: (state, command) => {
380
+ const id = command.id.trim();
381
+ if (id.length === 0)
382
+ return null;
383
+ if (state.document.catalogs.some((catalog) => catalog.id === id))
384
+ return null;
385
+ const catalog = {
386
+ id,
387
+ ...(command.label === undefined ? {} : { label: command.label }),
388
+ ...(command.schema === undefined ? {} : { schema: command.schema }),
389
+ entries: [],
390
+ };
391
+ return { ...state, document: { ...state.document, catalogs: [...state.document.catalogs, catalog] } };
392
+ },
393
+ removeCatalog: (state, command) => {
394
+ const next = state.document.catalogs.filter((catalog) => catalog.id !== command.id);
395
+ if (next.length === state.document.catalogs.length)
396
+ return null;
397
+ return { ...state, document: { ...state.document, catalogs: next } };
398
+ },
399
+ setCatalogSchema: (state, command) => {
400
+ if (!state.document.catalogs.some((catalog) => catalog.id === command.id))
401
+ return null;
402
+ // Re-parse every row's meta against the NEW schema so removed keys drop, added keys default in,
403
+ // and range/number values clamp — the whole clamp contract for a schema edit in one step.
404
+ const catalogs = state.document.catalogs.map((catalog) => catalog.id === command.id
405
+ ? {
406
+ ...catalog,
407
+ schema: command.schema,
408
+ ...(command.label === undefined ? {} : { label: command.label }),
409
+ entries: catalog.entries.map((entry) => ({
410
+ ...entry,
411
+ meta: parseParams(command.schema, entry.meta),
412
+ })),
413
+ }
414
+ : catalog);
415
+ return { ...state, document: { ...state.document, catalogs } };
416
+ },
417
+ remove: (state, command) => {
418
+ if (isEditorObjectLocked(state.document, command.id))
419
+ return null;
420
+ return {
421
+ document: removeByIds(state.document, new Set([command.id])),
422
+ selection: state.selection.filter((id) => id !== command.id),
423
+ };
424
+ },
425
+ removeMany: (state, command) => {
426
+ const gone = new Set(command.ids.filter((id) => !isEditorObjectLocked(state.document, id)));
427
+ if (gone.size === 0)
428
+ return null;
429
+ return {
430
+ document: removeByIds(state.document, gone),
431
+ selection: state.selection.filter((id) => !gone.has(id)),
432
+ };
433
+ },
434
+ duplicate: (state, command) => insertFragment(state, extractEditorFragment(state.document, command.ids), command.offset ?? { x: 2, y: 0, z: 2 }, true),
435
+ addFragment: (state, command) => insertFragment(state, command.fragment, command.offset ?? { x: 0, y: 0, z: 0 }, false),
436
+ importDocument: (_state, command) => ({
437
+ document: cloneEditorDocument(command.document),
438
+ selection: [],
439
+ }),
440
+ replaceDocument: (_state, command) => ({
441
+ document: cloneEditorDocument(command.document),
442
+ selection: [],
443
+ }),
444
+ importJson: (_state, command) => ({
445
+ document: importEditorDocumentJson(command.json),
446
+ selection: [],
447
+ }),
448
+ setTerrain: (state, command) => ({ ...state, document: { ...state.document, terrain: command.terrain } }),
449
+ setMinimapBake: (state, command) => ({ ...state, document: { ...state.document, minimap: command.minimap } }),
450
+ setEnvironment: (state, command) => {
451
+ if (command.environment === undefined) {
452
+ const { environment: _removed, ...rest } = state.document;
453
+ void _removed;
454
+ return { ...state, document: rest };
455
+ }
456
+ return { ...state, document: { ...state.document, environment: command.environment } };
457
+ },
458
+ clearTerrain: (state) => {
459
+ const nextDoc = {
460
+ version: 1,
461
+ markers: state.document.markers,
462
+ volumes: state.document.volumes,
463
+ paths: state.document.paths,
464
+ annotations: state.document.annotations,
465
+ prefabs: state.document.prefabs,
466
+ collections: state.document.collections,
467
+ catalogs: state.document.catalogs,
468
+ ...(state.document.grids === undefined ? {} : { grids: state.document.grids }),
469
+ ...(state.document.ui === undefined ? {} : { ui: state.document.ui }),
470
+ ...(state.document.directives === undefined ? {} : { directives: state.document.directives }),
471
+ ...(state.document.minimap === undefined ? {} : { minimap: state.document.minimap }),
472
+ ...(state.document.environment === undefined ? {} : { environment: state.document.environment }),
473
+ };
474
+ return { ...state, document: nextDoc };
475
+ },
476
+ setUiPanel: (state, command) => {
477
+ const ui = patchUiPanel(state.document.ui, command.id, command.patch);
478
+ return { ...state, document: { ...state.document, ui } };
479
+ },
480
+ removeUiPanel: (state, command) => {
481
+ const ui = removeUiPanel(state.document.ui, command.id);
482
+ if (ui === undefined) {
483
+ const { ui: _removed, ...rest } = state.document;
484
+ void _removed;
485
+ return { ...state, document: rest };
486
+ }
487
+ return { ...state, document: { ...state.document, ui } };
488
+ },
489
+ setUi: (state, command) => {
490
+ if (command.ui === undefined) {
491
+ const { ui: _removed, ...rest } = state.document;
492
+ void _removed;
493
+ return { ...state, document: rest };
494
+ }
495
+ return { ...state, document: { ...state.document, ui: command.ui } };
496
+ },
497
+ sculptTerrain: (state, command) => {
498
+ if (state.document.terrain === undefined)
499
+ return state;
500
+ return {
501
+ ...state,
502
+ document: { ...state.document, terrain: applyDeltaToSnapshot(state.document.terrain, command.delta) },
503
+ };
504
+ },
505
+ paintTerrain: (state, command) => {
506
+ if (state.document.terrain === undefined)
507
+ return state;
508
+ return {
509
+ ...state,
510
+ document: { ...state.document, terrain: applySurfaceDeltaToSnapshot(state.document.terrain, command.delta) },
511
+ };
512
+ },
513
+ blendTerrain: (state, command) => {
514
+ if (state.document.terrain === undefined)
515
+ return state;
516
+ return {
517
+ ...state,
518
+ document: { ...state.document, terrain: applyWeightDeltaToSnapshot(state.document.terrain, command.delta) },
519
+ };
520
+ },
521
+ setTerrainLayers: (state, command) => {
522
+ if (state.document.terrain === undefined)
523
+ return state;
524
+ const terrain = state.document.terrain;
525
+ const layers = command.layers.map((layer) => ({ ...layer }));
526
+ const before = terrain.layers ?? [];
527
+ // Keep hand-painted weights only when the layer id sequence is unchanged (a params-only edit).
528
+ const sameSequence = before.length === layers.length && before.every((layer, i) => layer.id === layers[i].id);
529
+ const next = sameSequence
530
+ ? { ...terrain, layers }
531
+ : (() => {
532
+ const copy = { ...terrain, layers };
533
+ delete copy.weights;
534
+ return copy;
535
+ })();
536
+ return { ...state, document: { ...state.document, terrain: next } };
537
+ },
538
+ convertScatterToObjects: (state, command) => {
539
+ const path = state.document.paths.find((entry) => entry.id === command.pathId);
540
+ if (path === undefined)
541
+ return state;
542
+ const taken = collectIds(state.document);
543
+ const markers = command.markers.map((marker) => {
544
+ let id = marker.id;
545
+ while (taken.has(id))
546
+ id = copyId(marker.id, taken);
547
+ taken.add(id);
548
+ return { ...marker, id };
549
+ });
550
+ return {
551
+ document: {
552
+ ...state.document,
553
+ markers: [...state.document.markers, ...markers],
554
+ paths: state.document.paths.filter((entry) => entry.id !== command.pathId),
555
+ },
556
+ selection: markers.map((marker) => marker.id),
557
+ };
558
+ },
559
+ createPrefab: (state, command) => {
560
+ if (command.ids.length === 0)
561
+ return state;
562
+ const fragment = createPrefabFragment(state.document, command.ids);
563
+ const prefab = { id: command.id, name: command.name, fragment };
564
+ return {
565
+ ...state,
566
+ document: {
567
+ ...state.document,
568
+ prefabs: [...state.document.prefabs.filter((entry) => entry.id !== command.id), prefab],
569
+ },
570
+ };
571
+ },
572
+ deletePrefab: (state, command) => ({
573
+ ...state,
574
+ document: {
575
+ ...state.document,
576
+ prefabs: state.document.prefabs.filter((entry) => entry.id !== command.prefabId),
577
+ },
578
+ }),
579
+ insertPrefab: (state, command) => {
580
+ const prefab = findEditorPrefab(state.document, command.prefabId);
581
+ if (prefab === undefined)
582
+ return state;
583
+ const instanceId = command.instanceId ??
584
+ `${prefab.id}_inst_${Date.now().toString(36)}_${Math.floor(Math.random() * 1_296_000).toString(36)}`;
585
+ const tagged = fragmentAsDocument({
586
+ markers: prefab.fragment.markers.map((marker) => tagWithInstance(marker, prefab.id, instanceId)),
587
+ volumes: prefab.fragment.volumes.map((volume) => tagWithInstance(volume, prefab.id, instanceId)),
588
+ paths: prefab.fragment.paths.map((path) => tagWithInstance(path, prefab.id, instanceId)),
589
+ annotations: prefab.fragment.annotations.map((note) => tagWithInstance(note, prefab.id, instanceId)),
590
+ });
591
+ return insertFragment(state, tagged, command.at, true);
592
+ },
593
+ detachPrefabInstance: (state, command) => {
594
+ const strip = (item) => {
595
+ if (item.meta?.prefabInstanceId !== command.instanceId)
596
+ return item;
597
+ const meta = { ...item.meta };
598
+ delete meta.prefabId;
599
+ delete meta.prefabInstanceId;
600
+ return { ...item, meta };
601
+ };
602
+ return { ...state, document: { ...state.document, ...mapPlaceables(state.document, strip) } };
603
+ },
604
+ createCollection: (state, command) => {
605
+ const collection = {
606
+ id: command.id,
607
+ name: command.name,
608
+ memberIds: [...(command.memberIds ?? [])],
609
+ };
610
+ return {
611
+ ...state,
612
+ document: {
613
+ ...state.document,
614
+ collections: [...state.document.collections.filter((entry) => entry.id !== command.id), collection],
615
+ },
616
+ };
617
+ },
618
+ renameCollection: (state, command) => {
619
+ const collections = state.document.collections.map((collection) => collection.id === command.id ? { ...collection, name: command.name } : collection);
620
+ return { ...state, document: { ...state.document, collections } };
621
+ },
622
+ deleteCollection: (state, command) => ({
623
+ ...state,
624
+ document: {
625
+ ...state.document,
626
+ collections: state.document.collections.filter((entry) => entry.id !== command.id),
627
+ },
628
+ }),
629
+ setCollectionMembers: (state, command) => {
630
+ const collections = state.document.collections.map((collection) => collection.id === command.id ? { ...collection, memberIds: [...command.memberIds] } : collection);
631
+ return { ...state, document: { ...state.document, collections } };
632
+ },
633
+ addToCollection: (state, command) => {
634
+ const adding = new Set(command.ids);
635
+ const collections = state.document.collections.map((collection) => {
636
+ if (collection.id !== command.id)
637
+ return collection;
638
+ const merged = new Set(collection.memberIds);
639
+ for (const id of adding)
640
+ merged.add(id);
641
+ return { ...collection, memberIds: [...merged] };
642
+ });
643
+ return { ...state, document: { ...state.document, collections } };
644
+ },
645
+ removeFromCollection: (state, command) => {
646
+ const removing = new Set(command.ids);
647
+ const collections = state.document.collections.map((collection) => collection.id === command.id
648
+ ? { ...collection, memberIds: collection.memberIds.filter((id) => !removing.has(id)) }
649
+ : collection);
650
+ return { ...state, document: { ...state.document, collections } };
651
+ },
652
+ setCollectionFlags: (state, command) => {
653
+ const collections = state.document.collections.map((collection) => collection.id === command.id ? { ...collection, ...command.patch } : collection);
654
+ return { ...state, document: { ...state.document, collections } };
655
+ },
656
+ setObjectFlags: (state, command) => {
657
+ const ids = new Set(command.ids);
658
+ if (ids.size === 0)
659
+ return null;
660
+ const { locked, hidden } = command.patch;
661
+ if (locked === undefined && hidden === undefined)
662
+ return null;
663
+ const apply = (item) => {
664
+ if (!ids.has(item.id))
665
+ return item;
666
+ const next = { ...item };
667
+ if (locked !== undefined) {
668
+ if (locked)
669
+ next.locked = true;
670
+ else
671
+ delete next.locked;
672
+ }
673
+ if (hidden !== undefined) {
674
+ if (hidden)
675
+ next.hidden = true;
676
+ else
677
+ delete next.hidden;
678
+ }
679
+ return next;
680
+ };
681
+ return { ...state, document: { ...state.document, ...mapPlaceables(state.document, apply) } };
682
+ },
683
+ selectCollection: (state, command) => {
684
+ const collection = findEditorCollection(state.document, command.id);
685
+ if (collection === undefined)
686
+ return null;
687
+ return { ...state, selection: [...collection.memberIds] };
688
+ },
689
+ batchSetProperties: (state, command) => {
690
+ const ids = new Set(command.ids);
691
+ const patch = command.patch;
692
+ // Notes carry no `label`, so they take color/meta only via a dedicated mapper.
693
+ const apply = (item) => ids.has(item.id)
694
+ ? {
695
+ ...item,
696
+ ...(patch.color === undefined ? {} : { color: patch.color }),
697
+ ...(patch.label === undefined ? {} : { label: patch.label }),
698
+ ...(patch.meta === undefined ? {} : { meta: { ...item.meta, ...patch.meta } }),
699
+ }
700
+ : item;
701
+ const applyNote = (note) => ids.has(note.id)
702
+ ? {
703
+ ...note,
704
+ ...(patch.color === undefined ? {} : { color: patch.color }),
705
+ ...(patch.meta === undefined ? {} : { meta: { ...note.meta, ...patch.meta } }),
706
+ }
707
+ : note;
708
+ return { ...state, document: { ...state.document, ...mapPlaceables(state.document, apply, applyNote) } };
709
+ },
710
+ addGridLayer: (state, command) => {
711
+ const layer = migrateGridLayer(command.layer);
712
+ const grids = state.document.grids ?? [];
713
+ return {
714
+ ...state,
715
+ document: {
716
+ ...state.document,
717
+ grids: [...grids.filter((entry) => entry.id !== layer.id), layer],
718
+ },
719
+ };
720
+ },
721
+ removeGridLayer: (state, command) => {
722
+ const grids = state.document.grids;
723
+ if (grids === undefined)
724
+ return null;
725
+ const next = grids.filter((layer) => layer.id !== command.id);
726
+ if (next.length === grids.length)
727
+ return null;
728
+ return { ...state, document: { ...state.document, grids: next } };
729
+ },
730
+ setGridLayer: (state, command) => updateGridLayer(state, command.id, (layer) => migrateGridLayer({ ...layer, ...command.patch })),
731
+ paintGridCells: (state, command) => updateGridLayer(state, command.id, (layer) => paintGridCells(layer, command.cells)),
732
+ fillGridRect: (state, command) => updateGridLayer(state, command.id, (layer) => fillGridRect(layer, command.col0, command.row0, command.col1, command.row1, command.value)),
733
+ floodFillGrid: (state, command) => updateGridLayer(state, command.id, (layer) => floodFillGrid(layer, command.col, command.row, command.value)),
734
+ resizeGridLayer: (state, command) => updateGridLayer(state, command.id, (layer) => resizeGridLayer(layer, command.cols, command.rows)),
735
+ assignMaterial: (state, command) => {
736
+ const ids = new Set(command.ids);
737
+ const stamp = (item) => ids.has(item.id) ? { ...item, meta: { ...item.meta, materialId: command.materialId } } : item;
738
+ return { ...state, document: { ...state.document, ...mapPlaceables(state.document, stamp) } };
739
+ },
740
+ undo: () => null,
741
+ redo: () => null,
742
+ };
743
+ /** Apply a mutating editor command; returns null when the command type has no handler. @internal */
744
+ export function applyMutating(state, command) {
745
+ const handler = mutationHandlers[command.type];
746
+ return handler(state, command);
747
+ }
748
+ /** True when the command changes document structure (not select/undo/redo). @internal */
749
+ export function isStructural(command) {
750
+ return (command.type !== "select" &&
751
+ command.type !== "clearSelection" &&
752
+ command.type !== "selectCollection" &&
753
+ command.type !== "undo" &&
754
+ command.type !== "redo");
755
+ }