@jgengine/core 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +26 -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.d.ts +1 -0
  9. package/dist/combat.js +1 -0
  10. package/dist/devtools/controls.d.ts +38 -0
  11. package/dist/devtools/controls.js +181 -0
  12. package/dist/devtools/devtools.d.ts +5 -197
  13. package/dist/devtools/devtools.js +38 -774
  14. package/dist/devtools/discover.d.ts +32 -0
  15. package/dist/devtools/discover.js +240 -0
  16. package/dist/devtools/fallbackSeams.d.ts +50 -0
  17. package/dist/devtools/fallbackSeams.js +111 -0
  18. package/dist/devtools/frame.d.ts +15 -0
  19. package/dist/devtools/frame.js +247 -0
  20. package/dist/devtools/logs.d.ts +15 -0
  21. package/dist/devtools/logs.js +86 -0
  22. package/dist/devtools/overrides.d.ts +16 -0
  23. package/dist/devtools/overrides.js +66 -0
  24. package/dist/devtools/types.d.ts +212 -0
  25. package/dist/devtools/types.js +1 -0
  26. package/dist/economy/auctionBook.d.ts +122 -0
  27. package/dist/economy/auctionBook.js +213 -0
  28. package/dist/economy/priceHistory.d.ts +44 -0
  29. package/dist/economy/priceHistory.js +89 -0
  30. package/dist/economy/unlockPoints.d.ts +92 -0
  31. package/dist/economy/unlockPoints.js +103 -0
  32. package/dist/editor/commandApply.d.ts +5 -0
  33. package/dist/editor/commandApply.js +755 -0
  34. package/dist/editor/commands.d.ts +48 -3
  35. package/dist/editor/commands.js +40 -754
  36. package/dist/editor/directives.d.ts +94 -0
  37. package/dist/editor/directives.js +206 -0
  38. package/dist/editor/document.d.ts +37 -2
  39. package/dist/editor/document.js +422 -6
  40. package/dist/editor/entityCatalog.d.ts +42 -0
  41. package/dist/editor/entityCatalog.js +101 -0
  42. package/dist/editor/environment.d.ts +74 -0
  43. package/dist/editor/environment.js +169 -0
  44. package/dist/editor/index.d.ts +26 -1
  45. package/dist/editor/index.js +8 -1
  46. package/dist/editor/minimap.d.ts +31 -0
  47. package/dist/editor/minimap.js +73 -0
  48. package/dist/editor/streamer.d.ts +66 -0
  49. package/dist/editor/streamer.js +68 -0
  50. package/dist/editor/types.d.ts +143 -0
  51. package/dist/editor/world.d.ts +136 -0
  52. package/dist/editor/world.js +303 -0
  53. package/dist/game/breeding.d.ts +161 -0
  54. package/dist/game/breeding.js +181 -0
  55. package/dist/game/cameraConfig.d.ts +451 -0
  56. package/dist/game/cameraConfig.js +1 -0
  57. package/dist/game/defineGame.d.ts +16 -9
  58. package/dist/game/defineGame.js +14 -6
  59. package/dist/game/defineSystem.d.ts +2 -0
  60. package/dist/game/defineSystem.js +2 -0
  61. package/dist/game/events.d.ts +28 -0
  62. package/dist/game/playableGame.d.ts +34 -435
  63. package/dist/game/playableGame.js +1 -1
  64. package/dist/game/progression.d.ts +35 -0
  65. package/dist/game/progression.js +19 -0
  66. package/dist/game/quest.js +1 -3
  67. package/dist/game/tribe.d.ts +184 -0
  68. package/dist/game/tribe.js +432 -0
  69. package/dist/gameplay.d.ts +3 -1
  70. package/dist/gameplay.js +3 -1
  71. package/dist/inventory/encumbrance.d.ts +69 -0
  72. package/dist/inventory/encumbrance.js +68 -0
  73. package/dist/item/durability.js +1 -3
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +24 -1
  76. package/dist/movement/movementModel.d.ts +15 -1
  77. package/dist/movement/movementModel.js +33 -15
  78. package/dist/movement/playerMovement.js +102 -2
  79. package/dist/multiplayer/combatSnapshot.js +13 -8
  80. package/dist/multiplayer/presenceModel.d.ts +2 -0
  81. package/dist/multiplayer/presenceModel.js +8 -0
  82. package/dist/physics/drivableVehicle.d.ts +2 -0
  83. package/dist/physics/drivableVehicle.js +1 -1
  84. package/dist/physics/flightDynamics.d.ts +82 -0
  85. package/dist/physics/flightDynamics.js +165 -0
  86. package/dist/physics/gravityField.d.ts +25 -0
  87. package/dist/physics/gravityField.js +41 -0
  88. package/dist/physics/kinematicVehicle.d.ts +87 -0
  89. package/dist/physics/kinematicVehicle.js +193 -11
  90. package/dist/physics/physicsWorld.d.ts +26 -0
  91. package/dist/physics/physicsWorld.js +109 -117
  92. package/dist/physics/vehicleObstacles.d.ts +44 -0
  93. package/dist/physics/vehicleObstacles.js +115 -0
  94. package/dist/random/rng.d.ts +4 -1
  95. package/dist/random/rng.js +4 -1
  96. package/dist/runtime/adapter.d.ts +13 -0
  97. package/dist/runtime/adapter.js +13 -0
  98. package/dist/runtime/context/combat.d.ts +51 -0
  99. package/dist/runtime/context/combat.js +130 -0
  100. package/dist/runtime/context/deathLoot.d.ts +30 -0
  101. package/dist/runtime/context/deathLoot.js +39 -0
  102. package/dist/runtime/context/player.d.ts +48 -0
  103. package/dist/runtime/context/player.js +125 -0
  104. package/dist/runtime/context/scene.d.ts +59 -0
  105. package/dist/runtime/context/scene.js +291 -0
  106. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  107. package/dist/runtime/context/snapshotCodecs.js +26 -0
  108. package/dist/runtime/descriptors/baseline.js +26 -3
  109. package/dist/runtime/descriptors/features.d.ts +5 -0
  110. package/dist/runtime/descriptors/features.js +20 -2
  111. package/dist/runtime/gameContext.d.ts +2 -428
  112. package/dist/runtime/gameContext.js +88 -501
  113. package/dist/runtime/gameContextTypes.d.ts +467 -0
  114. package/dist/runtime/gameContextTypes.js +1 -0
  115. package/dist/runtime/headlessRunner.d.ts +4 -2
  116. package/dist/runtime/headlessRunner.js +1 -0
  117. package/dist/runtime/hostPolicy.d.ts +51 -0
  118. package/dist/runtime/hostPolicy.js +68 -0
  119. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  120. package/dist/runtime/hostedGameRunner.js +1 -0
  121. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  122. package/dist/runtime/hostedWorldSession.js +1 -0
  123. package/dist/runtime/worldSnapshot.d.ts +13 -1
  124. package/dist/runtime/worldSnapshot.js +15 -3
  125. package/dist/scene/assetCatalog.d.ts +5 -0
  126. package/dist/scene/assetSpace.js +1 -3
  127. package/dist/scene/authoredTriggers.d.ts +50 -0
  128. package/dist/scene/authoredTriggers.js +114 -0
  129. package/dist/scene/builtinSceneKinds.js +8 -3
  130. package/dist/scene/colliders.d.ts +58 -0
  131. package/dist/scene/colliders.js +108 -0
  132. package/dist/scene/collisionMesh.d.ts +94 -0
  133. package/dist/scene/collisionMesh.js +699 -0
  134. package/dist/scene/companion.d.ts +147 -0
  135. package/dist/scene/companion.js +193 -0
  136. package/dist/scene/entityStats.d.ts +7 -9
  137. package/dist/scene/entityStats.js +10 -14
  138. package/dist/scene/sceneKinds.d.ts +16 -0
  139. package/dist/scene/sceneKinds.js +4 -0
  140. package/dist/scene/sceneRaycast.js +15 -0
  141. package/dist/stats/spawnLevelStats.d.ts +110 -0
  142. package/dist/stats/spawnLevelStats.js +140 -0
  143. package/dist/stats/statPool.d.ts +70 -0
  144. package/dist/stats/statPool.js +59 -0
  145. package/dist/store/changeSignal.d.ts +3 -0
  146. package/dist/store/changeSignal.js +2 -0
  147. package/dist/store/observableKeyedStore.d.ts +2 -0
  148. package/dist/store/observableKeyedStore.js +1 -0
  149. package/dist/survival/decayMeter.js +1 -3
  150. package/dist/survival/regionHealth.js +1 -3
  151. package/dist/ui/hudLayout.d.ts +5 -0
  152. package/dist/ui/hudLayout.js +17 -1
  153. package/dist/world/authoredEntities.d.ts +29 -0
  154. package/dist/world/authoredEntities.js +29 -0
  155. package/dist/world/authoredObjects.d.ts +15 -1
  156. package/dist/world/authoredObjects.js +13 -1
  157. package/dist/world/authoredSpawn.d.ts +39 -0
  158. package/dist/world/authoredSpawn.js +37 -0
  159. package/dist/world/buildSockets.d.ts +152 -0
  160. package/dist/world/buildSockets.js +188 -0
  161. package/dist/world/cityBlocks.d.ts +152 -0
  162. package/dist/world/cityBlocks.js +670 -0
  163. package/dist/world/cityContent.d.ts +77 -0
  164. package/dist/world/cityContent.js +261 -0
  165. package/dist/world/cityGeometry.d.ts +114 -0
  166. package/dist/world/cityGeometry.js +573 -0
  167. package/dist/world/cityKind.d.ts +314 -0
  168. package/dist/world/cityKind.js +1578 -0
  169. package/dist/world/features.d.ts +18 -3
  170. package/dist/world/features.js +10 -1
  171. package/dist/world/grassKind.js +2 -2
  172. package/dist/world/markers.d.ts +50 -4
  173. package/dist/world/markers.js +49 -0
  174. package/dist/world/massing.js +1 -1
  175. package/dist/world/minimapBake.d.ts +78 -0
  176. package/dist/world/minimapBake.js +208 -0
  177. package/dist/world/minimapTrack.d.ts +17 -0
  178. package/dist/world/minimapTrack.js +22 -0
  179. package/dist/world/pathNetwork.d.ts +159 -0
  180. package/dist/world/pathNetwork.js +684 -0
  181. package/dist/world/place.d.ts +166 -0
  182. package/dist/world/place.js +96 -0
  183. package/dist/world/placeAsset.js +3 -1
  184. package/dist/world/poleLineKind.d.ts +40 -0
  185. package/dist/world/poleLineKind.js +97 -0
  186. package/dist/world/polyline.js +9 -2
  187. package/dist/world/resourceNode.d.ts +109 -0
  188. package/dist/world/resourceNode.js +140 -0
  189. package/dist/world/scatter.js +2 -20
  190. package/dist/world/streets.d.ts +29 -0
  191. package/dist/world/streets.js +55 -0
  192. package/dist/world/terraform.js +58 -88
  193. package/dist/world/terrain.d.ts +45 -0
  194. package/dist/world/terrain.js +78 -0
  195. package/dist/world.d.ts +14 -5
  196. package/dist/world.js +12 -3
  197. package/package.json +1 -1
  198. package/skills/game-design/SKILL.md +28 -0
  199. package/skills/game-design/agents/openai.yaml +4 -0
  200. package/skills/game-design/references/application-playbook.md +170 -0
  201. package/skills/game-design/references/field-guide.md +288 -0
  202. package/skills/jgengine/SKILL.md +7 -4
  203. package/skills/jgengine/api.md +172 -79
  204. package/skills/jgengine/capabilities.md +104 -2
  205. package/skills/jgengine/recipes/minimal-game.md +88 -0
  206. package/skills/jgengine-combat/SKILL.md +5 -1
  207. package/skills/jgengine-combat/api.md +36 -0
  208. package/skills/jgengine-combat/capabilities.md +44 -0
  209. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  210. package/skills/jgengine-combat/reference.md +12 -2
  211. package/skills/jgengine-gameplay/api.md +152 -26
  212. package/skills/jgengine-gameplay/capabilities.md +78 -2
  213. package/skills/jgengine-gameplay/reference-systems.md +1 -5
  214. package/skills/jgengine-world/SKILL.md +13 -0
  215. package/skills/jgengine-world/api.md +419 -15
  216. package/skills/jgengine-world/capabilities.md +108 -2
  217. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  218. package/skills/jgengine-world/reference.md +43 -4
  219. 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
+ }