@jgengine/core 0.19.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/ai/decisionGraph.d.ts +2 -0
  3. package/dist/ai/decisionGraph.js +6 -0
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/anim/animGraph.d.ts +33 -2
  7. package/dist/anim/animGraph.js +200 -12
  8. package/dist/anim/locomotionGraph.d.ts +11 -3
  9. package/dist/anim/locomotionGraph.js +25 -15
  10. package/dist/audio/audioFalloff.d.ts +4 -0
  11. package/dist/audio/voiceAllocator.d.ts +46 -0
  12. package/dist/audio/voiceAllocator.js +76 -0
  13. package/dist/combat/abilityKit.d.ts +19 -0
  14. package/dist/combat/abilityKit.js +40 -4
  15. package/dist/combat/death.d.ts +4 -3
  16. package/dist/combat/death.js +3 -3
  17. package/dist/combat/magazine.d.ts +9 -0
  18. package/dist/combat/magazine.js +24 -2
  19. package/dist/combat/projectiles.d.ts +94 -2
  20. package/dist/combat/projectiles.js +282 -61
  21. package/dist/combat/regenShield.d.ts +14 -0
  22. package/dist/combat/regenShield.js +23 -7
  23. package/dist/crafting/recipeCatalog.d.ts +16 -0
  24. package/dist/crafting/recipeCatalog.js +55 -0
  25. package/dist/economy/auctionBook.d.ts +1 -1
  26. package/dist/economy/auctionBook.js +11 -1
  27. package/dist/economy/listingBook.d.ts +1 -1
  28. package/dist/economy/listingBook.js +11 -1
  29. package/dist/economy/resourceLedger.js +3 -3
  30. package/dist/economy/shopStock.js +7 -6
  31. package/dist/economy/techTree.js +7 -6
  32. package/dist/economy/unlockPoints.js +3 -6
  33. package/dist/editor/commandApply.js +89 -16
  34. package/dist/editor/commands.d.ts +38 -1
  35. package/dist/editor/commands.js +320 -3
  36. package/dist/editor/creatorStorage.d.ts +59 -0
  37. package/dist/editor/creatorStorage.js +164 -0
  38. package/dist/editor/document.d.ts +4 -0
  39. package/dist/editor/document.js +116 -17
  40. package/dist/editor/index.d.ts +3 -0
  41. package/dist/editor/index.js +1 -0
  42. package/dist/editor/liveSync.d.ts +4 -1
  43. package/dist/editor/liveSync.js +26 -10
  44. package/dist/editor/materialAuthoring.d.ts +14 -0
  45. package/dist/editor/materialAuthoring.js +38 -0
  46. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  47. package/dist/editor/modelNodeVisibility.js +29 -0
  48. package/dist/editor/movementCatalog.d.ts +90 -0
  49. package/dist/editor/movementCatalog.js +144 -0
  50. package/dist/editor/simulation.d.ts +60 -0
  51. package/dist/editor/simulation.js +202 -0
  52. package/dist/editor/staticPrefab.d.ts +17 -0
  53. package/dist/editor/staticPrefab.js +51 -0
  54. package/dist/editor/terrainMaterials.d.ts +10 -0
  55. package/dist/editor/terrainMaterials.js +13 -0
  56. package/dist/editor/types.d.ts +11 -0
  57. package/dist/game/cameraConfig.d.ts +3 -1
  58. package/dist/game/clipRoles.d.ts +2 -1
  59. package/dist/game/clipRoles.js +7 -3
  60. package/dist/game/contentValidation.d.ts +69 -0
  61. package/dist/game/contentValidation.js +118 -0
  62. package/dist/game/controlGate.d.ts +8 -0
  63. package/dist/game/controlGate.js +43 -6
  64. package/dist/game/defineGame.d.ts +32 -3
  65. package/dist/game/dialogueGraph.d.ts +20 -0
  66. package/dist/game/dialogueGraph.js +107 -0
  67. package/dist/game/events.d.ts +5 -0
  68. package/dist/game/keyValueStore.d.ts +17 -4
  69. package/dist/game/keyValueStore.js +35 -23
  70. package/dist/game/lootModifiers.d.ts +17 -0
  71. package/dist/game/lootModifiers.js +26 -0
  72. package/dist/game/narrativeAuthoring.d.ts +67 -0
  73. package/dist/game/narrativeAuthoring.js +122 -0
  74. package/dist/game/playableGame.d.ts +52 -20
  75. package/dist/game/quest.d.ts +15 -0
  76. package/dist/game/quest.js +209 -63
  77. package/dist/game/questCatalog.d.ts +50 -0
  78. package/dist/game/questCatalog.js +205 -0
  79. package/dist/game/saveStore.d.ts +6 -5
  80. package/dist/game/saveStore.js +57 -57
  81. package/dist/game/systemRuntime.d.ts +2 -2
  82. package/dist/game/systemRuntime.js +84 -33
  83. package/dist/gameplay.d.ts +3 -0
  84. package/dist/gameplay.js +3 -0
  85. package/dist/interaction/promptRegistry.d.ts +5 -4
  86. package/dist/interaction/promptRegistry.js +8 -2
  87. package/dist/inventory/inventoryModel.js +9 -6
  88. package/dist/inventory/shapedGrid.d.ts +48 -9
  89. package/dist/inventory/shapedGrid.js +69 -10
  90. package/dist/item/durability.js +4 -0
  91. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  92. package/dist/item/itemInstanceRegistry.js +111 -7
  93. package/dist/item/itemgen.d.ts +1 -1
  94. package/dist/item/itemgen.js +1 -1
  95. package/dist/item/modularItem.js +7 -1
  96. package/dist/material/appearanceSignals.d.ts +53 -0
  97. package/dist/material/appearanceSignals.js +47 -0
  98. package/dist/material/hairCards.d.ts +47 -0
  99. package/dist/material/hairCards.js +136 -0
  100. package/dist/material/materialAsset.d.ts +162 -0
  101. package/dist/material/materialAsset.js +382 -0
  102. package/dist/meta/changelog.d.ts +1 -1
  103. package/dist/meta/changelog.js +348 -1
  104. package/dist/movement/characterController.d.ts +5 -2
  105. package/dist/movement/characterController.js +50 -23
  106. package/dist/movement/freeFlight.d.ts +2 -2
  107. package/dist/movement/freeFlight.js +3 -3
  108. package/dist/movement/movementModel.d.ts +24 -1
  109. package/dist/movement/movementModel.js +74 -7
  110. package/dist/movement/playerMovement.d.ts +28 -1
  111. package/dist/movement/playerMovement.js +425 -102
  112. package/dist/movement/poseState.d.ts +2 -0
  113. package/dist/movement/poseState.js +29 -1
  114. package/dist/movement/solidObstacles.d.ts +3 -1
  115. package/dist/movement/solidObstacles.js +8 -5
  116. package/dist/movement/terrainGrade.d.ts +13 -0
  117. package/dist/movement/terrainGrade.js +33 -0
  118. package/dist/movement/voxelController.d.ts +2 -2
  119. package/dist/movement/voxelController.js +3 -3
  120. package/dist/nav/navGrid.d.ts +19 -1
  121. package/dist/nav/navGrid.js +26 -10
  122. package/dist/nav/solidRoute.d.ts +50 -0
  123. package/dist/nav/solidRoute.js +144 -0
  124. package/dist/physics/ballisticSweep.d.ts +10 -3
  125. package/dist/physics/ballisticSweep.js +53 -29
  126. package/dist/physics/forceVolume.d.ts +41 -0
  127. package/dist/physics/forceVolume.js +88 -0
  128. package/dist/physics/kinematicVehicle.d.ts +8 -3
  129. package/dist/physics/kinematicVehicle.js +2 -2
  130. package/dist/physics/physicsWorldBackend.js +19 -3
  131. package/dist/physics/spatialGrid.d.ts +1 -1
  132. package/dist/physics/spatialGrid.js +8 -9
  133. package/dist/physics/vehicleDynamics.d.ts +3 -2
  134. package/dist/physics/vehicleDynamics.js +2 -2
  135. package/dist/physics/vehicleObstacles.d.ts +3 -2
  136. package/dist/physics/vehicleObstacles.js +29 -5
  137. package/dist/runtime/cameraDirector.d.ts +39 -2
  138. package/dist/runtime/cameraDirector.js +162 -0
  139. package/dist/runtime/commandDispatch.d.ts +14 -0
  140. package/dist/runtime/commandDispatch.js +19 -0
  141. package/dist/runtime/context/combat.d.ts +11 -4
  142. package/dist/runtime/context/combat.js +178 -11
  143. package/dist/runtime/context/deathLoot.d.ts +9 -8
  144. package/dist/runtime/context/deathLoot.js +16 -16
  145. package/dist/runtime/context/scene.d.ts +2 -0
  146. package/dist/runtime/context/scene.js +13 -21
  147. package/dist/runtime/descriptors/baseline.js +34 -3
  148. package/dist/runtime/descriptors/features.d.ts +2 -1
  149. package/dist/runtime/descriptors/features.js +18 -0
  150. package/dist/runtime/gameContext.js +79 -13
  151. package/dist/runtime/gameContextTypes.d.ts +14 -0
  152. package/dist/runtime/headlessRunner.d.ts +3 -1
  153. package/dist/runtime/headlessRunner.js +11 -11
  154. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  155. package/dist/runtime/hostedGameRunner.js +156 -10
  156. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  157. package/dist/runtime/hostedWorldSession.js +154 -16
  158. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  159. package/dist/runtime/inputRecorder.d.ts +1 -1
  160. package/dist/runtime/inputRecorder.js +21 -1
  161. package/dist/runtime/inputSnapshot.d.ts +16 -4
  162. package/dist/runtime/inputSnapshot.js +31 -3
  163. package/dist/runtime/runtimeSave.d.ts +10 -6
  164. package/dist/runtime/runtimeSave.js +11 -5
  165. package/dist/runtime/simContext.d.ts +8 -3
  166. package/dist/runtime/simContext.js +7 -3
  167. package/dist/runtime/simSnapshot.js +2 -0
  168. package/dist/runtime/worldReplication.d.ts +2 -0
  169. package/dist/runtime/worldReplication.js +1 -1
  170. package/dist/scene/assetCatalog.d.ts +2 -0
  171. package/dist/scene/behaviorRuntime.d.ts +26 -5
  172. package/dist/scene/behaviorRuntime.js +263 -16
  173. package/dist/scene/behaviors.d.ts +30 -1
  174. package/dist/scene/behaviors.js +29 -0
  175. package/dist/scene/entityStore.d.ts +14 -3
  176. package/dist/scene/entityStore.js +45 -9
  177. package/dist/scene/mount.d.ts +9 -0
  178. package/dist/scene/mount.js +35 -0
  179. package/dist/scene/possession.d.ts +2 -0
  180. package/dist/scene/possession.js +21 -1
  181. package/dist/scene/sceneRaycast.d.ts +7 -0
  182. package/dist/scene/sceneRaycast.js +55 -0
  183. package/dist/scene/spatial.d.ts +20 -1
  184. package/dist/scene/spatial.js +94 -2
  185. package/dist/scene/vehicleSeat.d.ts +36 -2
  186. package/dist/scene/vehicleSeat.js +89 -2
  187. package/dist/stats/accumulatorMeter.d.ts +10 -0
  188. package/dist/stats/accumulatorMeter.js +6 -0
  189. package/dist/stats/eventMeter.d.ts +9 -2
  190. package/dist/stats/eventMeter.js +5 -2
  191. package/dist/store/defineStore.d.ts +3 -1
  192. package/dist/store/defineStore.js +3 -1
  193. package/dist/store/observableKeyedStore.d.ts +1 -1
  194. package/dist/store/observableKeyedStore.js +8 -6
  195. package/dist/time/simClock.d.ts +6 -2
  196. package/dist/time/simClock.js +5 -0
  197. package/dist/vfx/particleDirector.d.ts +58 -6
  198. package/dist/vfx/particleDirector.js +113 -22
  199. package/dist/vfx/particles.d.ts +68 -5
  200. package/dist/vfx/particles.js +284 -32
  201. package/dist/visibility/assetStreaming.d.ts +21 -3
  202. package/dist/visibility/assetStreaming.js +108 -24
  203. package/dist/visibility/settings.d.ts +8 -2
  204. package/dist/visibility/settings.js +3 -0
  205. package/dist/visibility/visibilitySystem.js +1 -0
  206. package/dist/work/expedition.d.ts +110 -0
  207. package/dist/work/expedition.js +195 -0
  208. package/dist/work/staffedStation.d.ts +29 -0
  209. package/dist/work/staffedStation.js +27 -0
  210. package/dist/world/authoredAnimation.d.ts +38 -0
  211. package/dist/world/authoredAnimation.js +99 -0
  212. package/dist/world/authoredObjects.d.ts +24 -4
  213. package/dist/world/authoredObjects.js +43 -16
  214. package/dist/world/authoredSimulation.d.ts +74 -0
  215. package/dist/world/authoredSimulation.js +302 -0
  216. package/dist/world/authoredWeather.d.ts +120 -0
  217. package/dist/world/authoredWeather.js +314 -0
  218. package/dist/world/buildingKit.d.ts +8 -3
  219. package/dist/world/buildingKit.js +21 -8
  220. package/dist/world/environmentForces.d.ts +21 -0
  221. package/dist/world/environmentForces.js +42 -0
  222. package/dist/world/features.d.ts +3 -3
  223. package/dist/world/footprintGrid.d.ts +55 -0
  224. package/dist/world/footprintGrid.js +201 -0
  225. package/dist/world/grassKind.d.ts +10 -0
  226. package/dist/world/grassKind.js +22 -0
  227. package/dist/world/pathTerrain.d.ts +1 -1
  228. package/dist/world/terraform.d.ts +2 -2
  229. package/dist/world/terraform.js +46 -18
  230. package/dist/world/weather.d.ts +17 -0
  231. package/dist/world/weather.js +116 -20
  232. package/dist/world/worldSolids.d.ts +11 -0
  233. package/dist/world/worldSolids.js +112 -1
  234. package/dist/world.d.ts +5 -5
  235. package/dist/world.js +4 -4
  236. package/package.json +1 -1
  237. package/skills/ce-handoff/SKILL.md +1 -1
  238. package/skills/game-design/SKILL.md +2 -0
  239. package/skills/jgengine/SKILL.md +23 -2
  240. package/skills/jgengine/api.md +28 -17
  241. package/skills/jgengine/recipes/minimal-game.md +7 -2
  242. package/skills/jgengine-combat/SKILL.md +8 -2
  243. package/skills/jgengine-combat/api.md +22 -10
  244. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  245. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  246. package/skills/jgengine-combat/reference.md +24 -2
  247. package/skills/jgengine-gameplay/SKILL.md +63 -0
  248. package/skills/jgengine-gameplay/api.md +131 -37
  249. package/skills/jgengine-gameplay/capabilities.md +68 -0
  250. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  251. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  252. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  253. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  254. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  255. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  256. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  257. package/skills/jgengine-gameplay/reference-systems.md +119 -0
  258. package/skills/jgengine-world/SKILL.md +12 -3
  259. package/skills/jgengine-world/api.md +201 -58
  260. package/skills/jgengine-world/capabilities.md +116 -3
  261. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  262. package/skills/jgengine-world/recipes/controls.md +4 -0
  263. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  264. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  265. package/skills/jgengine-world/reference.md +167 -5
  266. package/skills/jgengine-world/visibility.md +28 -1
@@ -0,0 +1,302 @@
1
+ import { stepFlock } from "../ai/flock.js";
2
+ import { seededRng } from "../random/rng.js";
3
+ import { sampleForceField } from "../physics/forceVolume.js";
4
+ import { decodeEditorSimulation } from "../editor/simulation.js";
5
+ import { createEmptyEditorDocument } from "../editor/document.js";
6
+ import { createAuthoredWeather, createWeatherExposure } from "./authoredWeather.js";
7
+ import { createFireGrid } from "./weather.js";
8
+ function finite(value, label, min = -Infinity, max = Infinity) {
9
+ if (!Number.isFinite(value) || value < min || value > max)
10
+ throw new Error(`${label} must be finite in ${min}..${max}`);
11
+ }
12
+ function inside(shelter, x, y, z) {
13
+ const dx = x - shelter.center.x, dy = y - shelter.center.y, dz = z - shelter.center.z;
14
+ if (shelter.shape === "box") {
15
+ const extents = shelter.halfExtents;
16
+ return extents !== undefined && Math.abs(dx) <= extents.x && Math.abs(dy) <= extents.y && Math.abs(dz) <= extents.z;
17
+ }
18
+ const radius = shelter.radius ?? 0;
19
+ return shelter.shape === "sphere" ? dx * dx + dy * dy + dz * dz <= radius * radius : dx * dx + dz * dz <= radius * radius && Math.abs(dy) <= (shelter.height ?? 0) / 2;
20
+ }
21
+ /** Compose authored weather, shelter, accumulation, named effect intents and fire with one clock.
22
+ * @capability authored-environment compose authored weather, shelter, accumulation, fire, flocks and effects using one simulation clock
23
+ */
24
+ export function createAuthoredSimulation(options) {
25
+ let document = options.document ?? createEmptyEditorDocument();
26
+ let config = decodeEditorSimulation(document.simulation ?? {}, document);
27
+ let weather = createAuthoredWeather(config.weather ?? {});
28
+ let shelters = [];
29
+ const surfaces = createWeatherExposure({ maxSurfaces: options.maxSurfaces ?? 1024 });
30
+ const watched = new Map();
31
+ let fires = new Map();
32
+ let emitterIds = [];
33
+ let flocks = [];
34
+ let disposed = false;
35
+ let authoredSignature;
36
+ const forceScratch = [0, 0, 0];
37
+ function install(next) {
38
+ const nextConfig = decodeEditorSimulation(next.simulation ?? {}, next);
39
+ const routeIds = new Set((nextConfig.habitats ?? []).map((habitat) => habitat.routeId));
40
+ const signature = JSON.stringify({ config: nextConfig, shelters: next.volumes.filter((volume) => volume.kind === "shelter"), routes: next.paths.filter((route) => routeIds.has(route.id)).map(({ id, points }) => ({ id, points })) });
41
+ if (signature === authoredSignature)
42
+ return;
43
+ const nextDocument = structuredClone(next);
44
+ const nextEmitterIds = (nextConfig.emitters ?? []).map((emitter) => `authored:${emitter.id}`);
45
+ if (options.particles !== undefined) {
46
+ const existing = options.particles.emitters();
47
+ const owned = new Set(emitterIds);
48
+ const projected = existing.filter((emitter) => !owned.has(emitter.id)).length + nextEmitterIds.length;
49
+ if (projected > options.particles.limits().maxEmitters)
50
+ throw new Error("authored emitters exceed the shared particle director capacity");
51
+ for (const id of nextEmitterIds)
52
+ if (!owned.has(id) && existing.some((emitter) => emitter.id === id))
53
+ throw new Error(`authored emitter id is already owned: ${id}`);
54
+ }
55
+ const nextShelters = next.volumes.filter((volume) => volume.kind === "shelter");
56
+ if (nextShelters.length > 256)
57
+ throw new Error("authored shelters are limited to 256 volumes");
58
+ const nextWeather = createAuthoredWeather(nextConfig.weather ?? {});
59
+ const nextFires = new Map();
60
+ for (const area of nextConfig.fires ?? []) {
61
+ const grid = createFireGrid({ ...area.config, origin: [area.position.x, area.position.z], ...(area.fuel === undefined ? {} : { fuelAt: (col, row) => area.fuel[row * area.config.cols + col] }) });
62
+ for (const ignition of area.ignitions ?? [])
63
+ grid.igniteCell(ignition.col, ignition.row);
64
+ const previousArea = config.fires?.find((previous) => previous.id === area.id);
65
+ const previousGrid = fires.get(area.id);
66
+ if (previousGrid !== undefined && previousArea !== undefined && previousArea.config.cols === area.config.cols && previousArea.config.rows === area.config.rows && previousArea.config.cellSize === area.config.cellSize && JSON.stringify(previousArea.position) === JSON.stringify(area.position) && JSON.stringify(previousArea.fuel) === JSON.stringify(area.fuel) && JSON.stringify(previousArea.ignitions) === JSON.stringify(area.ignitions))
67
+ grid.restore(previousGrid.snapshot());
68
+ nextFires.set(area.id, grid);
69
+ }
70
+ const nextFlocks = (nextConfig.habitats ?? []).map((habitat) => {
71
+ const previous = flocks.find((state) => state.id === habitat.id);
72
+ const previousConfig = config.habitats?.find((state) => state.id === habitat.id);
73
+ if (previous !== undefined && previousConfig !== undefined && ["species", "role", "count", "seed", "position", "radius"].every((key) => JSON.stringify(previousConfig[key]) === JSON.stringify(habitat[key]))) {
74
+ const route = habitat.routeId === undefined ? undefined : next.paths.find((entry) => entry.id === habitat.routeId);
75
+ return { ...previous, routeIndex: previousConfig.routeId !== habitat.routeId ? 0 : previous.routeIndex % (route?.points.length ?? 1) };
76
+ }
77
+ const rng = seededRng(habitat.seed);
78
+ const agents = Array.from({ length: habitat.count }, () => {
79
+ const angle = rng() * Math.PI * 2, radius = Math.sqrt(rng()) * habitat.radius;
80
+ return { position: [habitat.position.x + Math.cos(angle) * radius, habitat.position.y, habitat.position.z + Math.sin(angle) * radius], velocity: [0, 0, 0] };
81
+ });
82
+ return { id: habitat.id, species: habitat.species, role: habitat.role, agents, routeIndex: 0 };
83
+ });
84
+ for (const id of emitterIds)
85
+ if (!nextEmitterIds.includes(id))
86
+ options.particles?.detach(id);
87
+ for (const emitter of nextConfig.emitters ?? []) {
88
+ const id = `authored:${emitter.id}`;
89
+ const previous = config.emitters?.find((entry) => entry.id === emitter.id);
90
+ if (emitterIds.includes(id) && JSON.stringify(previous) === JSON.stringify(emitter))
91
+ continue;
92
+ options.particles?.attach(id, { ...emitter.config, position: [emitter.position.x, emitter.position.y, emitter.position.z] }, { ...emitter.options, ...(emitter.fireAreaId === undefined ? {} : { active: emitter.options?.active !== false && nextFires.get(emitter.fireAreaId).burning > 0 }) });
93
+ }
94
+ emitterIds = nextEmitterIds;
95
+ document = nextDocument;
96
+ config = nextConfig;
97
+ weather = nextWeather;
98
+ shelters = structuredClone(nextShelters);
99
+ fires = nextFires;
100
+ flocks = nextFlocks;
101
+ authoredSignature = signature;
102
+ syncFireEmitters();
103
+ }
104
+ function exposureAt(x, y, z) {
105
+ finite(x, "exposure x");
106
+ finite(y, "exposure y");
107
+ finite(z, "exposure z");
108
+ let exposure = 1;
109
+ for (const shelter of shelters) {
110
+ if (!inside(shelter, x, y, z))
111
+ continue;
112
+ const authored = shelter.meta?.exposure;
113
+ const amount = typeof authored === "number" && Number.isFinite(authored) ? Math.max(0, Math.min(1, authored)) : 0;
114
+ exposure = Math.min(exposure, amount);
115
+ }
116
+ return exposure;
117
+ }
118
+ function accelerationAt(position, timeSeconds, windResponse, maxAcceleration, mask = 0xffffffff, out = [0, 0, 0]) {
119
+ if (position.length !== 3)
120
+ throw new Error("acceleration position must contain three components");
121
+ for (const axis of position)
122
+ finite(axis, "acceleration position");
123
+ finite(timeSeconds, "acceleration time", 0);
124
+ if (!Number.isInteger(mask) || mask < 0 || mask > 0xffffffff)
125
+ throw new Error("acceleration mask must be a uint32");
126
+ finite(windResponse, "wind response", 0, 1e12);
127
+ finite(maxAcceleration, "acceleration cap", 0, 1e12);
128
+ const wind = weather.sample(position[0], position[2], timeSeconds).wind;
129
+ let x = wind[0] * windResponse, y = 0, z = wind[1] * windResponse;
130
+ for (const field of config.forces ?? []) {
131
+ sampleForceField(field, position, mask, forceScratch);
132
+ x += forceScratch[0];
133
+ y += forceScratch[1];
134
+ z += forceScratch[2];
135
+ }
136
+ const magnitude = Math.hypot(x, y, z), scale = magnitude > maxAcceleration && magnitude > 0 ? maxAcceleration / magnitude : 1;
137
+ out[0] = x * scale;
138
+ out[1] = y * scale;
139
+ out[2] = z * scale;
140
+ return out;
141
+ }
142
+ function syncFireEmitters() {
143
+ for (const emitter of config.emitters ?? []) {
144
+ if (emitter.fireAreaId === undefined)
145
+ continue;
146
+ if (emitter.options?.active !== false && fires.get(emitter.fireAreaId).burning > 0)
147
+ options.particles?.start(`authored:${emitter.id}`);
148
+ else
149
+ options.particles?.stop(`authored:${emitter.id}`);
150
+ }
151
+ }
152
+ install(document);
153
+ return {
154
+ sample: (x, z) => weather.sample(x, z, options.timeSeconds()),
155
+ appearanceAt(surfaceId, position) {
156
+ const exposure = exposureAt(...position);
157
+ const sample = weather.sample(position[0], position[2], options.timeSeconds());
158
+ const accumulated = surfaces.sample(surfaceId);
159
+ let heat = 0;
160
+ for (const area of config.fires ?? []) {
161
+ if (Math.abs(position[1] - area.position.y) > area.config.cellSize)
162
+ continue;
163
+ const cell = fires.get(area.id).cellAt(position[0], position[2]);
164
+ if (cell !== null)
165
+ heat = Math.max(heat, Math.min(1, cell.heat / (area.config.ignitionThreshold ?? 1)));
166
+ }
167
+ return { timeSeconds: sample.timeSeconds, wind: [sample.wind[0], 0, sample.wind[1]], wetness: accumulated?.wetness ?? 0, snow: accumulated?.snow ?? 0, exposure, heat, temperatureOffsetC: sample.temperatureOffset ?? 0 };
168
+ },
169
+ exposureAt,
170
+ surfaces,
171
+ watch(surface) {
172
+ if (disposed)
173
+ throw new Error("authored simulation is disposed");
174
+ if (!surface.id || surface.id.length > 128)
175
+ throw new Error("surface id must be nonempty and at most 128 characters");
176
+ finite(surface.x, "surface x");
177
+ finite(surface.z, "surface z");
178
+ finite(surface.y ?? 0, "surface y");
179
+ if (surface.exposure !== undefined) {
180
+ finite(surface.exposure, "surface exposure", 0);
181
+ if (surface.exposure > 1)
182
+ throw new Error("surface exposure must be <= 1");
183
+ }
184
+ if (surface.temperature !== undefined)
185
+ finite(surface.temperature, "surface temperature");
186
+ if (!watched.has(surface.id) && watched.size >= (options.maxSurfaces ?? 1024))
187
+ throw new Error("authored surface budget exceeded");
188
+ watched.set(surface.id, { ...surface });
189
+ },
190
+ forget(id) { watched.delete(id); surfaces.forget(id); },
191
+ windAcceleration(position, response, maxAcceleration) {
192
+ for (const axis of position)
193
+ finite(axis, "wind position");
194
+ if (position.length !== 3)
195
+ throw new Error("wind position must contain three components");
196
+ finite(response, "wind response", 0, 1e12);
197
+ finite(maxAcceleration, "wind acceleration cap", 0, 1e12);
198
+ const wind = weather.sample(position[0], position[2], options.timeSeconds()).wind;
199
+ const x = wind[0] * response, z = wind[1] * response;
200
+ const magnitude = Math.hypot(x, z), scale = magnitude > maxAcceleration && magnitude > 0 ? maxAcceleration / magnitude : 1;
201
+ return [x * scale, 0, z * scale];
202
+ },
203
+ accelerationAt,
204
+ fire: (id) => fires.get(id),
205
+ flocks: () => flocks,
206
+ step(gameDt) {
207
+ finite(gameDt, "authored simulation dt", 0);
208
+ if (disposed || gameDt === 0)
209
+ return;
210
+ const time = options.timeSeconds();
211
+ const samples = Array.from(watched.values(), (surface) => ({ ...surface, exposure: (surface.exposure ?? 1) * exposureAt(surface.x, surface.y ?? 0, surface.z) }));
212
+ surfaces.step(gameDt, time, samples, weather);
213
+ for (const state of flocks) {
214
+ const habitat = config.habitats.find((entry) => entry.id === state.id);
215
+ const route = habitat.routeId === undefined ? undefined : document.paths.find((entry) => entry.id === habitat.routeId);
216
+ const point = route?.points[state.routeIndex % route.points.length];
217
+ const target = point === undefined ? [habitat.position.x, habitat.position.y, habitat.position.z] : [point.x, point.y, point.z];
218
+ const acceleration = (habitat.maxAcceleration ?? 0) === 0 ? undefined : (agent) => accelerationAt(agent.position, time, habitat.windResponse ?? 0, habitat.maxAcceleration, habitat.forceMask ?? 0xffffffff);
219
+ stepFlock(state.agents, habitat.steering, gameDt, target, acceleration);
220
+ const lead = state.agents[0];
221
+ if (route !== undefined && lead !== undefined && Math.hypot(lead.position[0] - target[0], lead.position[1] - target[1], lead.position[2] - target[2]) < Math.max(1, habitat.steering.maxSpeed * gameDt))
222
+ state.routeIndex = (state.routeIndex + 1) % route.points.length;
223
+ }
224
+ for (const area of config.fires ?? []) {
225
+ const grid = fires.get(area.id);
226
+ const local = weather.sample(area.position.x, area.position.z, time);
227
+ grid.step(gameDt, { spread: 1 - local.precipitation, wind: local.wind, extinguishRate: area.extinguishRate ?? 0, wetnessAt: (col, row) => {
228
+ const x = area.position.x + col * area.config.cellSize, z = area.position.z + row * area.config.cellSize;
229
+ return weather.sample(x, z, time).rain * exposureAt(x, area.position.y, z);
230
+ } });
231
+ }
232
+ syncFireEmitters();
233
+ },
234
+ retune(next) { if (disposed)
235
+ throw new Error("authored simulation is disposed"); install(next); },
236
+ snapshot: () => ({ surfaces: surfaces.snapshot(), watched: Array.from(watched.values(), (surface) => ({ ...surface })), fires: Array.from(fires, ([id, grid]) => ({ id, cells: grid.snapshot() })), flocks: structuredClone(flocks.map(({ id, agents, routeIndex }) => ({ id, agents, routeIndex }))) }),
237
+ restore(state) {
238
+ if (disposed)
239
+ throw new Error("authored simulation is disposed");
240
+ if (!Array.isArray(state.watched) || state.watched.length > (options.maxSurfaces ?? 1024))
241
+ throw new Error("authored surface restore budget exceeded");
242
+ if (!Array.isArray(state.fires) || state.fires.length !== fires.size)
243
+ throw new Error("restored fire cardinality differs from authored areas");
244
+ if (!Array.isArray(state.flocks) || state.flocks.length !== flocks.length)
245
+ throw new Error("restored flock cardinality differs from authored habitats");
246
+ const watchedIds = new Set();
247
+ const fireIds = new Set();
248
+ const flockIds = new Set();
249
+ const next = createAuthoredSimulation({ ...options, document, particles: undefined });
250
+ try {
251
+ next.surfaces.restore(state.surfaces);
252
+ for (const surface of state.watched) {
253
+ if (watchedIds.has(surface.id))
254
+ throw new Error(`duplicate restored surface id: ${surface.id}`);
255
+ watchedIds.add(surface.id);
256
+ next.watch(surface);
257
+ }
258
+ for (const saved of state.fires) {
259
+ if (fireIds.has(saved.id))
260
+ throw new Error(`duplicate restored fire id: ${saved.id}`);
261
+ fireIds.add(saved.id);
262
+ const grid = next.fire(saved.id);
263
+ if (grid === undefined)
264
+ throw new Error(`unknown restored fire area: ${saved.id}`);
265
+ grid.restore(saved.cells);
266
+ }
267
+ const restoredFlocks = state.flocks.map((saved) => {
268
+ if (flockIds.has(saved.id))
269
+ throw new Error(`duplicate restored flock id: ${saved.id}`);
270
+ flockIds.add(saved.id);
271
+ const current = flocks.find((entry) => entry.id === saved.id);
272
+ if (current === undefined || !Array.isArray(saved.agents) || current.agents.length !== saved.agents.length)
273
+ throw new Error(`invalid restored flock ${saved.id}`);
274
+ const habitat = config.habitats.find((entry) => entry.id === saved.id);
275
+ const route = habitat.routeId === undefined ? undefined : document.paths.find((entry) => entry.id === habitat.routeId);
276
+ if (!Number.isSafeInteger(saved.routeIndex) || saved.routeIndex < 0 || saved.routeIndex >= (route?.points.length ?? 1))
277
+ throw new Error(`invalid restored flock route ${saved.id}`);
278
+ for (const agent of saved.agents) {
279
+ if (!Array.isArray(agent.position) || !Array.isArray(agent.velocity) || agent.position.length !== 3 || agent.velocity.length !== 3)
280
+ throw new Error(`invalid restored flock vector ${saved.id}`);
281
+ for (const axis of [...agent.position, ...agent.velocity])
282
+ finite(axis, "restored flock vector");
283
+ }
284
+ return { ...current, agents: structuredClone(saved.agents), routeIndex: saved.routeIndex };
285
+ });
286
+ surfaces.restore(next.surfaces.snapshot());
287
+ watched.clear();
288
+ for (const surface of state.watched)
289
+ watched.set(surface.id, { ...surface });
290
+ for (const saved of state.fires)
291
+ fires.get(saved.id).restore(saved.cells);
292
+ flocks = restoredFlocks;
293
+ syncFireEmitters();
294
+ }
295
+ finally {
296
+ next.dispose();
297
+ }
298
+ },
299
+ dispose() { for (const id of emitterIds)
300
+ options.particles?.detach(id); emitterIds = []; watched.clear(); surfaces.restore({ surfaces: [] }); fires.clear(); flocks = []; disposed = true; },
301
+ };
302
+ }
@@ -0,0 +1,120 @@
1
+ import { type EnvironmentField, type EnvironmentFieldConfig } from "./envField.js";
2
+ import { type WindFieldConfig, type WindVector } from "./wind.js";
3
+ /** Wind is metres/second along world X,Z; world Y points up. Times are simulation seconds. */
4
+ export type AuthoredWeatherMode = "clear" | "rain" | "snow" | "mixed" | "dust";
5
+ /** Precipitation intensity, temperature offset in degrees, and normalized lightning brightness. */
6
+ export interface WeatherConditions {
7
+ mode: AuthoredWeatherMode;
8
+ intensity: number;
9
+ temperatureOffset?: number;
10
+ lightning?: number;
11
+ }
12
+ /** A named set of conditions referenced by the authored schedule. */
13
+ export interface AuthoredWeatherProfile extends WeatherConditions {
14
+ id: string;
15
+ }
16
+ /** An absolute simulation-time profile change with an optional continuous blend. */
17
+ export interface AuthoredWeatherTransition {
18
+ atSeconds: number;
19
+ profileId: string;
20
+ transitionSeconds?: number;
21
+ }
22
+ /** A local weather override and additive wind source centered in world X,Z metres. */
23
+ export interface AuthoredWeatherZone {
24
+ id: string;
25
+ center: readonly [number, number];
26
+ radius: number;
27
+ /** Blend distance outside the radius, in metres. */
28
+ falloff?: number;
29
+ weather?: WeatherConditions;
30
+ wind?: WindFieldConfig;
31
+ /** Local wind points outward from the center; it adds to ambient wind. */
32
+ radial?: boolean;
33
+ }
34
+ /** Serializable weather lists capped at 256 entries. Wind amplitudes and direction components are bounded to 1e6, gust frequency to 1000 Hz; coordinates and times to 1e12 metres/seconds. */
35
+ export interface AuthoredWeatherConfig {
36
+ ambient?: WeatherConditions;
37
+ profiles?: readonly AuthoredWeatherProfile[];
38
+ initialProfileId?: string;
39
+ /** Ordered absolute times, relative to the simulation clock. */
40
+ schedule?: readonly AuthoredWeatherTransition[];
41
+ wind?: WindFieldConfig;
42
+ /** Later weather zones blend over earlier ones; wind zones add to ambient wind. */
43
+ zones?: readonly AuthoredWeatherZone[];
44
+ }
45
+ /** Resolved conditions and wind at a world point and absolute simulation time. */
46
+ export interface AuthoredWeatherSample extends WeatherConditions {
47
+ timeSeconds: number;
48
+ wind: WindVector;
49
+ precipitation: number;
50
+ rain: number;
51
+ snow: number;
52
+ dust: number;
53
+ }
54
+ /** Pure spatial weather queries using the caller’s authoritative simulation clock. */
55
+ export interface AuthoredWeather {
56
+ sample(x: number, z: number, timeSeconds: number): AuthoredWeatherSample;
57
+ }
58
+ /** Validate authoring references and work bounds before persisting a weather document.
59
+ * @capability authored-weather-validation Reject invalid weather profiles, schedules, zones and wind before authoring mutations.
60
+ */
61
+ export declare function validateAuthoredWeather(config: AuthoredWeatherConfig): void;
62
+ /** Pure authored weather sampling; the caller's simulation clock owns pause, replay and restore.
63
+ * @capability authored-weather Sample scheduled precipitation and local wind from one authored document and simulation clock.
64
+ */
65
+ export declare function createAuthoredWeather(source?: AuthoredWeatherConfig): AuthoredWeather;
66
+ /** Surface capacity and normalized wetting, drying, snowfall and melting rates per second. */
67
+ export interface WeatherExposureConfig {
68
+ maxSurfaces?: number;
69
+ /** Accumulation and drying rates in normalized coverage/second. */
70
+ wettingRate?: number;
71
+ dryingRate?: number;
72
+ snowRate?: number;
73
+ meltRate?: number;
74
+ }
75
+ /** A stable surface id and world X,Z position with explicit sky exposure and temperature. */
76
+ export interface WeatherExposureSurface {
77
+ id: string;
78
+ x: number;
79
+ z: number;
80
+ /** Fraction of open sky; 0 means fully sheltered. */
81
+ exposure?: number;
82
+ /** Degrees above zero melt snow; omitted uses the weather temperature offset. */
83
+ temperature?: number;
84
+ }
85
+ /** Retained normalized wetness and snow coverage, temperature, and sky exposure. */
86
+ export interface WeatherExposureSample {
87
+ wetness: number;
88
+ snow: number;
89
+ heat: number;
90
+ exposure: number;
91
+ }
92
+ /** Detached accumulation values keyed by retained surface id. */
93
+ export interface WeatherExposureSnapshot {
94
+ surfaces: {
95
+ id: string;
96
+ value: WeatherExposureSample;
97
+ }[];
98
+ }
99
+ /** Injected persistence for detached surface accumulation snapshots. */
100
+ export interface WeatherExposureStorage {
101
+ read(): WeatherExposureSnapshot | undefined;
102
+ write(snapshot: WeatherExposureSnapshot): void;
103
+ }
104
+ /** Bounded surface accumulation with restoration, live rate tuning and explicit release. */
105
+ export interface WeatherExposure {
106
+ step(dt: number, timeSeconds: number, surfaces: readonly WeatherExposureSurface[], weather: AuthoredWeather): void;
107
+ sample(id: string): WeatherExposureSample | null;
108
+ forget(id: string): void;
109
+ snapshot(): WeatherExposureSnapshot;
110
+ restore(state: WeatherExposureSnapshot): void;
111
+ retune(config: WeatherExposureConfig): void;
112
+ }
113
+ /** Bounded surface accumulation shared by gameplay and material adapters, with injected persistence.
114
+ * @capability weather-surface-accumulation Persist and retune bounded surface wetness, snow, temperature and exposure independently of rendering.
115
+ */
116
+ export declare function createWeatherExposure(config?: WeatherExposureConfig, storage?: WeatherExposureStorage): WeatherExposure;
117
+ /** Environment temperature, sunlight and shelter read the same authored precipitation as rendering.
118
+ * @capability weather-environment-field Connect authored precipitation and temperature changes to spatial environment queries.
119
+ */
120
+ export declare function createWeatherEnvironmentField(weather: AuthoredWeather, config?: Omit<EnvironmentFieldConfig, "rain">): EnvironmentField;