@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
@@ -1,6 +1,47 @@
1
1
  import { cloneEditorUiDocument, decodeEditorUiDocument } from "../ui/hudDocument.js";
2
2
  import { migrateTerrainSnapshot } from "../world/terraform.js";
3
3
  import { cloneGridLayer, migrateGridLayer, } from "./grid.js";
4
+ /** Copies a baked minimap so a document's PNG + bounds are never shared by reference across rebuilds. */
5
+ function cloneMinimapBake(minimap) {
6
+ return { background: minimap.background, bounds: { ...minimap.bounds } };
7
+ }
8
+ /** Deep-copies scene-document environment/lighting so edits never mutate history snapshots. */
9
+ function cloneEnvironment(environment) {
10
+ return {
11
+ ...(environment.preset === undefined ? {} : { preset: environment.preset }),
12
+ ...(environment.timeOfDay === undefined ? {} : { timeOfDay: environment.timeOfDay }),
13
+ ...(environment.horizonColor === undefined ? {} : { horizonColor: environment.horizonColor }),
14
+ ...(environment.zenithColor === undefined ? {} : { zenithColor: environment.zenithColor }),
15
+ ...(environment.sunIntensity === undefined ? {} : { sunIntensity: environment.sunIntensity }),
16
+ ...(environment.ambientIntensity === undefined ? {} : { ambientIntensity: environment.ambientIntensity }),
17
+ ...(environment.fog === undefined ? {} : { fog: { ...environment.fog } }),
18
+ };
19
+ }
20
+ function cloneDirective(directive) {
21
+ const area = directive.area === undefined
22
+ ? undefined
23
+ : { min: [...directive.area.min], max: [...directive.area.max] };
24
+ const base = {
25
+ id: directive.id,
26
+ ...(directive.region === undefined ? {} : { region: directive.region }),
27
+ ...(area === undefined ? {} : { area }),
28
+ ...(directive.seed === undefined ? {} : { seed: directive.seed }),
29
+ ...(directive.meta === undefined ? {} : { meta: { ...directive.meta } }),
30
+ };
31
+ if (directive.kind === "scatter") {
32
+ return { ...base, kind: "scatter", asset: directive.asset, density: directive.density,
33
+ ...(directive.minSpacing === undefined ? {} : { minSpacing: directive.minSpacing }),
34
+ ...(directive.jitter === undefined ? {} : { jitter: directive.jitter }),
35
+ ...(directive.minScale === undefined ? {} : { minScale: directive.minScale }),
36
+ ...(directive.maxScale === undefined ? {} : { maxScale: directive.maxScale }),
37
+ ...(directive.minYaw === undefined ? {} : { minYaw: directive.minYaw }),
38
+ ...(directive.maxYaw === undefined ? {} : { maxYaw: directive.maxYaw }) };
39
+ }
40
+ return { ...base, kind: "population", species: directive.species.map((s) => ({ ...s })) };
41
+ }
42
+ function cloneDirectives(directives) {
43
+ return directives === undefined ? undefined : directives.map(cloneDirective);
44
+ }
4
45
  function migrateGrids(grids) {
5
46
  return grids === undefined ? undefined : grids.map(migrateGridLayer);
6
47
  }
@@ -23,6 +64,9 @@ export function editorDocumentExtras(doc) {
23
64
  ...(doc.grids === undefined ? {} : { grids: doc.grids }),
24
65
  ...(doc.terrain === undefined ? {} : { terrain: doc.terrain }),
25
66
  ...(doc.ui === undefined ? {} : { ui: doc.ui }),
67
+ ...(doc.directives === undefined ? {} : { directives: doc.directives }),
68
+ ...(doc.minimap === undefined ? {} : { minimap: doc.minimap }),
69
+ ...(doc.environment === undefined ? {} : { environment: doc.environment }),
26
70
  };
27
71
  }
28
72
  /** Builds a fresh, empty editor document to start authoring a scene from scratch.
@@ -50,6 +94,8 @@ function cloneCatalogEntry(entry) {
50
94
  function cloneCatalogs(catalogs) {
51
95
  return (catalogs ?? []).map((catalog) => ({
52
96
  id: catalog.id,
97
+ ...(catalog.label === undefined ? {} : { label: catalog.label }),
98
+ ...(catalog.schema === undefined ? {} : { schema: catalog.schema }),
53
99
  entries: catalog.entries.map(cloneCatalogEntry),
54
100
  }));
55
101
  }
@@ -61,6 +107,7 @@ function cloneCatalogs(catalogs) {
61
107
  */
62
108
  export function cloneEditorDocument(doc) {
63
109
  const ui = cloneEditorUiDocument(doc.ui);
110
+ const directives = cloneDirectives(doc.directives);
64
111
  return {
65
112
  version: 1,
66
113
  markers: doc.markers.map((marker) => ({
@@ -98,6 +145,9 @@ export function cloneEditorDocument(doc) {
98
145
  ...(doc.grids === undefined ? {} : { grids: doc.grids.map(cloneGridLayer) }),
99
146
  ...(doc.terrain === undefined ? {} : { terrain: doc.terrain }),
100
147
  ...(ui === undefined ? {} : { ui }),
148
+ ...(directives === undefined ? {} : { directives }),
149
+ ...(doc.minimap === undefined ? {} : { minimap: cloneMinimapBake(doc.minimap) }),
150
+ ...(doc.environment === undefined ? {} : { environment: cloneEnvironment(doc.environment) }),
101
151
  };
102
152
  }
103
153
  function asArray(value) {
@@ -107,11 +157,24 @@ function upsertCatalogEntries(base, overlay) {
107
157
  return upsertById(base.map(cloneCatalogEntry), overlay.map(cloneCatalogEntry));
108
158
  }
109
159
  function upsertCatalogs(base, overlay) {
110
- const byId = new Map((base ?? []).map((catalog) => [catalog.id, { id: catalog.id, entries: catalog.entries.map(cloneCatalogEntry) }]));
160
+ const byId = new Map((base ?? []).map((catalog) => [
161
+ catalog.id,
162
+ {
163
+ id: catalog.id,
164
+ ...(catalog.label === undefined ? {} : { label: catalog.label }),
165
+ ...(catalog.schema === undefined ? {} : { schema: catalog.schema }),
166
+ entries: catalog.entries.map(cloneCatalogEntry),
167
+ },
168
+ ]));
111
169
  for (const catalog of overlay ?? []) {
112
170
  const existing = byId.get(catalog.id);
171
+ // Overlay schema/label win when present; otherwise keep whatever the base carried.
172
+ const label = catalog.label ?? existing?.label;
173
+ const schema = catalog.schema ?? existing?.schema;
113
174
  byId.set(catalog.id, {
114
175
  id: catalog.id,
176
+ ...(label === undefined ? {} : { label }),
177
+ ...(schema === undefined ? {} : { schema }),
115
178
  entries: existing === undefined ? catalog.entries.map(cloneCatalogEntry) : upsertCatalogEntries(existing.entries, catalog.entries),
116
179
  });
117
180
  }
@@ -125,6 +188,8 @@ export function normalizeEditorLayers(input) {
125
188
  return createEmptyEditorDocument();
126
189
  const resolved = typeof input === "function" ? input() : input;
127
190
  const ui = cloneEditorUiDocument(resolved.ui);
191
+ const grids = migrateGrids(resolved.grids);
192
+ const directives = cloneDirectives(resolved.directives);
128
193
  return {
129
194
  version: 1,
130
195
  markers: asArray(resolved.markers),
@@ -134,9 +199,12 @@ export function normalizeEditorLayers(input) {
134
199
  prefabs: asArray(resolved.prefabs),
135
200
  collections: asArray(resolved.collections),
136
201
  catalogs: cloneCatalogs(resolved.catalogs),
137
- ...(migrateGrids(resolved.grids) === undefined ? {} : { grids: migrateGrids(resolved.grids) }),
202
+ ...(grids === undefined ? {} : { grids }),
138
203
  ...(resolved.terrain === undefined ? {} : { terrain: migrateTerrainSnapshot(resolved.terrain) }),
139
204
  ...(ui === undefined ? {} : { ui }),
205
+ ...(directives === undefined ? {} : { directives }),
206
+ ...(resolved.minimap === undefined ? {} : { minimap: cloneMinimapBake(resolved.minimap) }),
207
+ ...(resolved.environment === undefined ? {} : { environment: cloneEnvironment(resolved.environment) }),
140
208
  };
141
209
  }
142
210
  /**
@@ -186,6 +254,13 @@ export function mergeEditorDocuments(...docs) {
186
254
  out.grids = mergedGrids;
187
255
  if (doc.terrain !== undefined)
188
256
  out.terrain = doc.terrain;
257
+ if (doc.minimap !== undefined)
258
+ out.minimap = cloneMinimapBake(doc.minimap);
259
+ if (doc.environment !== undefined)
260
+ out.environment = cloneEnvironment(doc.environment);
261
+ if (doc.directives !== undefined) {
262
+ out.directives = upsertById(out.directives ?? [], doc.directives);
263
+ }
189
264
  if (doc.ui !== undefined) {
190
265
  out.ui = {
191
266
  panels: {
@@ -264,6 +339,8 @@ export function seedEditorCatalogs(doc, definitions) {
264
339
  }
265
340
  const catalog = {
266
341
  id: definition.id,
342
+ ...(existing?.label === undefined ? {} : { label: existing.label }),
343
+ ...(existing?.schema === undefined ? {} : { schema: existing.schema }),
267
344
  entries: [...byId.values()],
268
345
  };
269
346
  if (existingIndex >= 0)
@@ -273,12 +350,57 @@ export function seedEditorCatalogs(doc, definitions) {
273
350
  }
274
351
  return { ...doc, catalogs: next };
275
352
  }
276
- /** True when an object id is a member of any locked collection — blocks move/delete on it.
353
+ /**
354
+ * True when an object carries a per-object `locked` flag (hierarchy lock toggle).
355
+ * Collection membership is checked separately by {@link isEditorObjectLocked}.
277
356
  * @internal
278
357
  */
279
- export function isEditorObjectLocked(doc, id) {
358
+ export function isEditorObjectSelfLocked(doc, id) {
359
+ const marker = doc.markers.find((entry) => entry.id === id);
360
+ if (marker !== undefined)
361
+ return marker.locked === true;
362
+ const volume = doc.volumes.find((entry) => entry.id === id);
363
+ if (volume !== undefined)
364
+ return volume.locked === true;
365
+ const path = doc.paths.find((entry) => entry.id === id);
366
+ if (path !== undefined)
367
+ return path.locked === true;
368
+ const note = doc.annotations.find((entry) => entry.id === id);
369
+ return note?.locked === true;
370
+ }
371
+ /**
372
+ * True when an object is a member of any locked collection (production group lock).
373
+ * @internal
374
+ */
375
+ export function isEditorObjectCollectionLocked(doc, id) {
280
376
  return doc.collections.some((collection) => collection.locked === true && collection.memberIds.includes(id));
281
377
  }
378
+ /**
379
+ * True when an object id is locked for edit — either its own `locked` flag or membership in a
380
+ * locked collection. Blocks move/delete via session mutations.
381
+ * @internal
382
+ */
383
+ export function isEditorObjectLocked(doc, id) {
384
+ return isEditorObjectSelfLocked(doc, id) || isEditorObjectCollectionLocked(doc, id);
385
+ }
386
+ /**
387
+ * True when an object carries `hidden: true` — omitted from editor viewport overlays and picking
388
+ * while remaining in the hierarchy and document.
389
+ * @internal
390
+ */
391
+ export function isEditorObjectHidden(doc, id) {
392
+ const marker = doc.markers.find((entry) => entry.id === id);
393
+ if (marker !== undefined)
394
+ return marker.hidden === true;
395
+ const volume = doc.volumes.find((entry) => entry.id === id);
396
+ if (volume !== undefined)
397
+ return volume.hidden === true;
398
+ const path = doc.paths.find((entry) => entry.id === id);
399
+ if (path !== undefined)
400
+ return path.hidden === true;
401
+ const note = doc.annotations.find((entry) => entry.id === id);
402
+ return note?.hidden === true;
403
+ }
282
404
  /**
283
405
  * Extracts the selected ids into a prefab fragment centered on their own bounds centroid, so the
284
406
  * same prefab reinserts consistently regardless of where in the scene (or which game) it lands.
@@ -312,6 +434,29 @@ export function createPrefabFragment(doc, ids) {
312
434
  export function findEditorMarker(doc, id) {
313
435
  return doc.markers.find((marker) => marker.id === id);
314
436
  }
437
+ /** Looks up a marker by id and throws when it is absent — for scene contracts where a
438
+ * missing marker is an authoring error to surface loudly, not a runtime branch to handle.
439
+ * @internal
440
+ */
441
+ export function requireEditorMarker(doc, id) {
442
+ const marker = findEditorMarker(doc, id);
443
+ if (marker === undefined) {
444
+ throw new Error(`editor document has no marker with id "${id}"`);
445
+ }
446
+ return marker;
447
+ }
448
+ /** A marker's world position as an `[x, y, z]` tuple — the shared shape gameplay tables read.
449
+ * @internal
450
+ */
451
+ export function editorMarkerPosition(marker) {
452
+ return [marker.position.x, marker.position.y, marker.position.z];
453
+ }
454
+ /** A marker's ground position as an `[x, z]` tuple, dropping the vertical axis.
455
+ * @internal
456
+ */
457
+ export function editorMarkerXZ(marker) {
458
+ return [marker.position.x, marker.position.z];
459
+ }
315
460
  /** Looks up a volume by id in an editor document.
316
461
  * @internal
317
462
  */
@@ -475,6 +620,10 @@ function decodeMarker(item, path, errors) {
475
620
  marker.catalogId = item.catalogId;
476
621
  if (typeof item.parentId === "string")
477
622
  marker.parentId = item.parentId;
623
+ if (typeof item.locked === "boolean")
624
+ marker.locked = item.locked;
625
+ if (typeof item.hidden === "boolean")
626
+ marker.hidden = item.hidden;
478
627
  const meta = decodeMeta(item.meta, `${path}.meta`, errors);
479
628
  if (meta !== undefined)
480
629
  marker.meta = meta;
@@ -512,6 +661,10 @@ function decodeVolume(item, path, errors) {
512
661
  volume.label = item.label;
513
662
  if (typeof item.parentId === "string")
514
663
  volume.parentId = item.parentId;
664
+ if (typeof item.locked === "boolean")
665
+ volume.locked = item.locked;
666
+ if (typeof item.hidden === "boolean")
667
+ volume.hidden = item.hidden;
515
668
  const meta = decodeMeta(item.meta, `${path}.meta`, errors);
516
669
  if (meta !== undefined)
517
670
  volume.meta = meta;
@@ -543,6 +696,10 @@ function decodePath(item, path, errors) {
543
696
  decodedPath.label = item.label;
544
697
  if (typeof item.parentId === "string")
545
698
  decodedPath.parentId = item.parentId;
699
+ if (typeof item.locked === "boolean")
700
+ decodedPath.locked = item.locked;
701
+ if (typeof item.hidden === "boolean")
702
+ decodedPath.hidden = item.hidden;
546
703
  const meta = decodeMeta(item.meta, `${path}.meta`, errors);
547
704
  if (meta !== undefined)
548
705
  decodedPath.meta = meta;
@@ -565,6 +722,10 @@ function decodeNote(item, path, errors) {
565
722
  note.color = item.color;
566
723
  if (typeof item.parentId === "string")
567
724
  note.parentId = item.parentId;
725
+ if (typeof item.locked === "boolean")
726
+ note.locked = item.locked;
727
+ if (typeof item.hidden === "boolean")
728
+ note.hidden = item.hidden;
568
729
  const meta = decodeMeta(item.meta, `${path}.meta`, errors);
569
730
  if (meta !== undefined)
570
731
  note.meta = meta;
@@ -645,6 +806,30 @@ function decodeCatalogEntry(item, path, errors) {
645
806
  entry.meta = meta;
646
807
  return entry;
647
808
  }
809
+ /**
810
+ * Decodes a document-carried `ParamSchema` (`{ fields: ParamField[] }`) — modeled on
811
+ * `loadGameCatalogs`'s `decodeSchema`. Any field that is not a plain object with string `key` and
812
+ * `type` is dropped with a diagnostic instead of poisoning the whole schema; a non-array `fields`
813
+ * value drops the schema entirely (undefined). Structural but permissive: field-shape validation
814
+ * beyond key/type is left to `parseParams`, which defaults anything it cannot read.
815
+ */
816
+ function decodeParamSchema(raw, path, errors) {
817
+ if (raw === undefined)
818
+ return undefined;
819
+ if (!isPlainObject(raw) || !Array.isArray(raw.fields)) {
820
+ errors.push({ path, message: "expected { fields: ParamField[] }" });
821
+ return undefined;
822
+ }
823
+ const fields = [];
824
+ raw.fields.forEach((field, index) => {
825
+ if (!isPlainObject(field) || typeof field.key !== "string" || typeof field.type !== "string") {
826
+ errors.push({ path: `${path}.fields[${index}]`, message: "expected a ParamField with string key + type" });
827
+ return;
828
+ }
829
+ fields.push(field);
830
+ });
831
+ return { fields, ...(Array.isArray(raw.groups) ? { groups: raw.groups } : {}) };
832
+ }
648
833
  function decodeCatalog(item, path, errors) {
649
834
  if (!isPlainObject(item)) {
650
835
  errors.push({ path, message: "expected an object" });
@@ -655,7 +840,203 @@ function decodeCatalog(item, path, errors) {
655
840
  return null;
656
841
  }
657
842
  const entries = decodeArray(item.entries, `${path}.entries`, decodeCatalogEntry, errors);
658
- return { id: item.id, entries };
843
+ const schema = decodeParamSchema(item.schema, `${path}.schema`, errors);
844
+ const catalog = { id: item.id, entries };
845
+ if (typeof item.label === "string")
846
+ catalog.label = item.label;
847
+ if (schema !== undefined)
848
+ catalog.schema = schema;
849
+ return catalog;
850
+ }
851
+ function decodeDirectiveArea(value, path, errors) {
852
+ if (value === undefined)
853
+ return undefined;
854
+ if (!isPlainObject(value)) {
855
+ errors.push({ path, message: "expected an object" });
856
+ return undefined;
857
+ }
858
+ const pair = (v, p) => {
859
+ if (!Array.isArray(v) || v.length !== 2 || typeof v[0] !== "number" || typeof v[1] !== "number") {
860
+ errors.push({ path: p, message: "expected [number, number]" });
861
+ return null;
862
+ }
863
+ return [v[0], v[1]];
864
+ };
865
+ const min = pair(value.min, `${path}.min`);
866
+ const max = pair(value.max, `${path}.max`);
867
+ if (min === null || max === null)
868
+ return undefined;
869
+ return { min, max };
870
+ }
871
+ function decodeSpecies(item, path, errors) {
872
+ if (!isPlainObject(item)) {
873
+ errors.push({ path, message: "expected an object" });
874
+ return null;
875
+ }
876
+ if (typeof item.id !== "string")
877
+ errors.push({ path: `${path}.id`, message: "expected a string" });
878
+ if (typeof item.cap !== "number")
879
+ errors.push({ path: `${path}.cap`, message: "expected a number" });
880
+ if (typeof item.id !== "string" || typeof item.cap !== "number")
881
+ return null;
882
+ const species = { id: item.id, cap: item.cap };
883
+ if (typeof item.weight === "number")
884
+ species.weight = item.weight;
885
+ return species;
886
+ }
887
+ function decodeDirective(item, path, errors) {
888
+ if (!isPlainObject(item)) {
889
+ errors.push({ path, message: "expected an object" });
890
+ return null;
891
+ }
892
+ if (typeof item.id !== "string")
893
+ errors.push({ path: `${path}.id`, message: "expected a string" });
894
+ const kindOk = item.kind === "scatter" || item.kind === "population";
895
+ if (!kindOk)
896
+ errors.push({ path: `${path}.kind`, message: "expected \"scatter\" | \"population\"" });
897
+ const area = decodeDirectiveArea(item.area, `${path}.area`, errors);
898
+ if (typeof item.id !== "string" || !kindOk)
899
+ return null;
900
+ const base = {
901
+ id: item.id,
902
+ ...(typeof item.region === "string" ? { region: item.region } : {}),
903
+ ...(area === undefined ? {} : { area }),
904
+ ...(typeof item.seed === "string" || typeof item.seed === "number" ? { seed: item.seed } : {}),
905
+ ...(isPlainObject(item.meta) ? { meta: item.meta } : {}),
906
+ };
907
+ if (item.kind === "scatter") {
908
+ if (typeof item.asset !== "string")
909
+ errors.push({ path: `${path}.asset`, message: "expected a string" });
910
+ if (typeof item.density !== "number")
911
+ errors.push({ path: `${path}.density`, message: "expected a number" });
912
+ if (typeof item.asset !== "string" || typeof item.density !== "number")
913
+ return null;
914
+ const scatter = { ...base, kind: "scatter", asset: item.asset, density: item.density };
915
+ for (const key of ["minSpacing", "jitter", "minScale", "maxScale", "minYaw", "maxYaw"]) {
916
+ if (typeof item[key] === "number")
917
+ scatter[key] = item[key];
918
+ }
919
+ return scatter;
920
+ }
921
+ const species = decodeArray(item.species, `${path}.species`, decodeSpecies, errors);
922
+ return { ...base, kind: "population", species };
923
+ }
924
+ /**
925
+ * Validates a baked minimap arriving from outside this process: `background` must be a
926
+ * `data:image/…` URI and `bounds` must be four finite numbers, so a malformed bake is rejected with a
927
+ * path-specific diagnostic instead of feeding a broken `<image href>` to the runtime minimap.
928
+ */
929
+ function decodeMinimapBake(value, path, errors) {
930
+ if (!isPlainObject(value)) {
931
+ errors.push({ path, message: "expected an object" });
932
+ return undefined;
933
+ }
934
+ const backgroundOk = typeof value.background === "string" && value.background.startsWith("data:image/");
935
+ if (!backgroundOk) {
936
+ errors.push({ path: `${path}.background`, message: "expected a data:image/… string" });
937
+ }
938
+ const bounds = value.bounds;
939
+ const boundsOk = isPlainObject(bounds) &&
940
+ ["minX", "minZ", "maxX", "maxZ"].every((key) => typeof bounds[key] === "number" && Number.isFinite(bounds[key]));
941
+ if (!boundsOk) {
942
+ errors.push({ path: `${path}.bounds`, message: "expected {minX,minZ,maxX,maxZ} finite numbers" });
943
+ }
944
+ if (!backgroundOk || !boundsOk)
945
+ return undefined;
946
+ const b = bounds;
947
+ return {
948
+ background: value.background,
949
+ bounds: { minX: b.minX, minZ: b.minZ, maxX: b.maxX, maxZ: b.maxZ },
950
+ };
951
+ }
952
+ const SKY_PRESETS = new Set(["day", "dusk", "night"]);
953
+ /**
954
+ * Validates scene-document environment/lighting. Unknown presets, non-finite intensities, and
955
+ * malformed fog objects produce path-specific diagnostics; empty valid objects are accepted so a
956
+ * game can seed `environment: {}` as "use engine defaults, ready to author".
957
+ */
958
+ function decodeEnvironment(value, path, errors) {
959
+ if (!isPlainObject(value)) {
960
+ errors.push({ path, message: "expected an object" });
961
+ return undefined;
962
+ }
963
+ let failed = false;
964
+ const env = {};
965
+ if (value.preset !== undefined) {
966
+ if (typeof value.preset === "string" && SKY_PRESETS.has(value.preset)) {
967
+ env.preset = value.preset;
968
+ }
969
+ else {
970
+ errors.push({ path: `${path}.preset`, message: 'expected "day" | "dusk" | "night"' });
971
+ failed = true;
972
+ }
973
+ }
974
+ if (value.timeOfDay !== undefined) {
975
+ if (typeof value.timeOfDay === "boolean")
976
+ env.timeOfDay = value.timeOfDay;
977
+ else {
978
+ errors.push({ path: `${path}.timeOfDay`, message: "expected a boolean" });
979
+ failed = true;
980
+ }
981
+ }
982
+ if (value.horizonColor !== undefined) {
983
+ if (typeof value.horizonColor === "string")
984
+ env.horizonColor = value.horizonColor;
985
+ else {
986
+ errors.push({ path: `${path}.horizonColor`, message: "expected a string" });
987
+ failed = true;
988
+ }
989
+ }
990
+ if (value.zenithColor !== undefined) {
991
+ if (typeof value.zenithColor === "string")
992
+ env.zenithColor = value.zenithColor;
993
+ else {
994
+ errors.push({ path: `${path}.zenithColor`, message: "expected a string" });
995
+ failed = true;
996
+ }
997
+ }
998
+ for (const key of ["sunIntensity", "ambientIntensity"]) {
999
+ if (value[key] === undefined)
1000
+ continue;
1001
+ if (typeof value[key] === "number" && Number.isFinite(value[key]))
1002
+ env[key] = value[key];
1003
+ else {
1004
+ errors.push({ path: `${path}.${key}`, message: "expected a finite number" });
1005
+ failed = true;
1006
+ }
1007
+ }
1008
+ if (value.fog !== undefined) {
1009
+ if (!isPlainObject(value.fog)) {
1010
+ errors.push({ path: `${path}.fog`, message: "expected an object" });
1011
+ failed = true;
1012
+ }
1013
+ else {
1014
+ const fog = {};
1015
+ if (value.fog.color !== undefined) {
1016
+ if (typeof value.fog.color === "string")
1017
+ fog.color = value.fog.color;
1018
+ else {
1019
+ errors.push({ path: `${path}.fog.color`, message: "expected a string" });
1020
+ failed = true;
1021
+ }
1022
+ }
1023
+ for (const key of ["near", "far"]) {
1024
+ if (value.fog[key] === undefined)
1025
+ continue;
1026
+ if (typeof value.fog[key] === "number" && Number.isFinite(value.fog[key]))
1027
+ fog[key] = value.fog[key];
1028
+ else {
1029
+ errors.push({ path: `${path}.fog.${key}`, message: "expected a finite number" });
1030
+ failed = true;
1031
+ }
1032
+ }
1033
+ if (Object.keys(fog).length > 0)
1034
+ env.fog = fog;
1035
+ }
1036
+ }
1037
+ if (failed)
1038
+ return undefined;
1039
+ return env;
659
1040
  }
660
1041
  const GRID_AXES = new Set(["xz", "xy"]);
661
1042
  function decodeGridPaletteEntry(item, path, errors) {
@@ -766,9 +1147,33 @@ export function decodeEditorDocument(raw) {
766
1147
  const collections = decodeArray(raw.collections, "$.collections", decodeCollection, errors);
767
1148
  const catalogs = decodeArray(raw.catalogs, "$.catalogs", decodeCatalog, errors);
768
1149
  const grids = raw.grids === undefined ? undefined : decodeArray(raw.grids, "$.grids", decodeGridLayer, errors);
1150
+ const directives = raw.directives === undefined ? undefined : decodeArray(raw.directives, "$.directives", decodeDirective, errors);
769
1151
  if (raw.terrain !== undefined && !isPlainObject(raw.terrain)) {
770
1152
  errors.push({ path: "$.terrain", message: "expected an object" });
771
1153
  }
1154
+ const minimap = raw.minimap === undefined ? undefined : decodeMinimapBake(raw.minimap, "$.minimap", errors);
1155
+ const environment = raw.environment === undefined ? undefined : decodeEnvironment(raw.environment, "$.environment", errors);
1156
+ // Placeable object ids form one document-global namespace (selection, parenting, and removal all
1157
+ // treat them that way), so a document that reuses an id — even across two different collections —
1158
+ // is malformed and rejected here with the offending path, rather than silently loading a scene
1159
+ // where one id shadows another. Combine paths (merge/duplicate/overlay) re-id instead; this is the
1160
+ // single-document decode boundary, where a collision is an authoring error, not something to paper over.
1161
+ const seenIds = new Set();
1162
+ for (const [collection, list] of [
1163
+ ["markers", markers],
1164
+ ["volumes", volumes],
1165
+ ["paths", paths],
1166
+ ["annotations", annotations],
1167
+ ]) {
1168
+ list.forEach((item, index) => {
1169
+ if (seenIds.has(item.id)) {
1170
+ errors.push({ path: `$.${collection}[${index}].id`, message: `duplicate id "${item.id}"` });
1171
+ }
1172
+ else {
1173
+ seenIds.add(item.id);
1174
+ }
1175
+ });
1176
+ }
772
1177
  if (errors.length > 0)
773
1178
  return { ok: false, errors };
774
1179
  const terrain = raw.terrain === undefined ? undefined : migrateTerrainSnapshot(raw.terrain);
@@ -787,6 +1192,9 @@ export function decodeEditorDocument(raw) {
787
1192
  ...(grids === undefined ? {} : { grids }),
788
1193
  ...(terrain === undefined ? {} : { terrain }),
789
1194
  ...(ui === undefined ? {} : { ui }),
1195
+ ...(directives === undefined ? {} : { directives }),
1196
+ ...(minimap === undefined ? {} : { minimap }),
1197
+ ...(environment === undefined ? {} : { environment }),
790
1198
  },
791
1199
  };
792
1200
  }
@@ -814,6 +1222,8 @@ function upsertById(base, overlay) {
814
1222
  */
815
1223
  export function applyEditorDocumentOverlay(base, overlay) {
816
1224
  const terrain = overlay.terrain ?? base.terrain;
1225
+ const minimap = overlay.minimap ?? base.minimap;
1226
+ const environment = overlay.environment ?? base.environment;
817
1227
  const ui = overlay.ui === undefined
818
1228
  ? cloneEditorUiDocument(base.ui)
819
1229
  : {
@@ -822,6 +1232,7 @@ export function applyEditorDocumentOverlay(base, overlay) {
822
1232
  ...Object.fromEntries(Object.entries(overlay.ui.panels).map(([id, panel]) => [id, { ...panel }])),
823
1233
  },
824
1234
  };
1235
+ const grids = upsertGrids(base.grids, overlay.grids);
825
1236
  return {
826
1237
  version: 1,
827
1238
  markers: upsertById(base.markers, overlay.markers),
@@ -831,9 +1242,14 @@ export function applyEditorDocumentOverlay(base, overlay) {
831
1242
  prefabs: upsertById(base.prefabs, overlay.prefabs),
832
1243
  collections: upsertById(base.collections, overlay.collections),
833
1244
  catalogs: upsertCatalogs(base.catalogs, overlay.catalogs),
834
- ...(upsertGrids(base.grids, overlay.grids) === undefined ? {} : { grids: upsertGrids(base.grids, overlay.grids) }),
1245
+ ...(grids === undefined ? {} : { grids }),
835
1246
  ...(terrain === undefined ? {} : { terrain }),
836
1247
  ...(ui === undefined ? {} : { ui }),
1248
+ ...(minimap === undefined ? {} : { minimap: cloneMinimapBake(minimap) }),
1249
+ ...(environment === undefined ? {} : { environment: cloneEnvironment(environment) }),
1250
+ ...(base.directives === undefined && overlay.directives === undefined
1251
+ ? {}
1252
+ : { directives: upsertById(base.directives ?? [], overlay.directives ?? []) }),
837
1253
  };
838
1254
  }
839
1255
  /** Computes the world-space min/max bounds spanning every object in a document, or null if empty.
@@ -0,0 +1,42 @@
1
+ import type { GameContextEntityEntry } from "../runtime/gameContext.js";
2
+ import { type ParamSchema } from "../scene/sceneKinds.js";
3
+ import type { EditorCatalogData, EditorCatalogDefinition } from "./types.js";
4
+ /**
5
+ * Well-known catalog id for authored entity definitions. A `mob`/`boss` marker's `catalogId`
6
+ * references an entry (row) in this catalog, and {@link entityEntryFromCatalog} turns that row into
7
+ * the runtime {@link GameContextEntityEntry} a spawn consumes — so an entity's stats and speed are
8
+ * authored in the editor Data tab, not hardcoded in game TS.
9
+ */
10
+ export declare const ENTITY_CATALOG_ID = "entities";
11
+ /**
12
+ * The default {@link ParamSchema} for an authored entity definition — role, health, movement speed,
13
+ * and visual scale. Drives the editor Data-tab inspector for the `entities` catalog and is the shape
14
+ * {@link entityEntryFromCatalog} reads. A game exports it as an `editorCatalogs` schema; the scaffold
15
+ * ships a starter one so the tab is never empty.
16
+ * @capability editor-entities Author entity stats/movement per catalog row instead of hardcoding TS.
17
+ */
18
+ export declare const entityDefinitionSchema: ParamSchema;
19
+ /** Minimal document shape {@link entityEntryFromCatalog} reads — any `EditorDocument` satisfies it. */
20
+ export interface EntityCatalogDocumentLike {
21
+ catalogs: readonly EditorCatalogData[];
22
+ }
23
+ /**
24
+ * Turns an authored entity row into the runtime {@link GameContextEntityEntry} a spawn consumes:
25
+ * looks up `entityId` in the document's {@link ENTITY_CATALOG_ID} catalog (falling back to the
26
+ * game-exported `definitions` defaults), parses it against {@link entityDefinitionSchema}, and maps
27
+ * the fields onto stats/movement/role. Returns `null` when no row defines the entity, so the default
28
+ * `content.ts` path can chain other resolvers. The single seam that makes "place a mob, tune its
29
+ * stats/speed in the editor, save — it spawns with those stats" work with zero game code.
30
+ * @capability editor-entities Consume authored entity definitions from the scene document at runtime.
31
+ */
32
+ export declare function entityEntryFromCatalog(document: EntityCatalogDocumentLike, entityId: string, definitions?: readonly EditorCatalogDefinition[]): GameContextEntityEntry | null;
33
+ /**
34
+ * Merges game-exported catalog definitions with editor-authored ones carried on the scene document.
35
+ * A document catalog that carries its own `schema` and has no matching game definition is synthesized
36
+ * into a full {@link EditorCatalogDefinition} (label falls back to the id) so the Data panel, the
37
+ * `list_catalogs` RPC, and the count all see it. A game definition always wins on id collision — its
38
+ * schema lives in code and is authoritative. Document catalogs without a schema (plain value bags for
39
+ * a game-declared catalog) are ignored here; they are addressed through their game definition.
40
+ * @capability editor-catalogs Merge game-exported and editor-authored catalogs into one addressable set.
41
+ */
42
+ export declare function resolveCatalogDefinitions(document: EntityCatalogDocumentLike, gameDefinitions: readonly EditorCatalogDefinition[]): EditorCatalogDefinition[];