@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,314 @@
1
+ import { createEnvironmentField } from "./envField.js";
2
+ import { windField } from "./wind.js";
3
+ const MAX_ENTRIES = 256;
4
+ const MAX_MAGNITUDE = 1_000_000;
5
+ const MAX_DOMAIN = 1_000_000_000_000;
6
+ const MODES = ["clear", "rain", "snow", "mixed", "dust"];
7
+ const CLEAR = { mode: "clear", intensity: 0 };
8
+ function identifier(value, label) {
9
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 128)
10
+ throw new Error(`${label} must be a nonempty string of at most 128 characters`);
11
+ }
12
+ function finite(value, label, min = -Infinity, max = Infinity) {
13
+ if (!Number.isFinite(value) || value < min || value > max)
14
+ throw new Error(`${label} must be finite and within ${min}..${max}`);
15
+ }
16
+ function objectKeys(value, allowed, label) {
17
+ if (value === null || typeof value !== "object" || Array.isArray(value))
18
+ throw new Error(`${label} must be an object`);
19
+ for (const key of Object.keys(value))
20
+ if (!allowed.includes(key))
21
+ throw new Error(`${label}.${key}: unknown field`);
22
+ }
23
+ function conditions(value, profile = false) {
24
+ objectKeys(value, ["mode", "intensity", "temperatureOffset", "lightning", ...(profile ? ["id"] : [])], "weather conditions");
25
+ if (!MODES.includes(value.mode))
26
+ throw new Error(`unknown weather mode: ${value.mode}`);
27
+ finite(value.intensity, "weather intensity", 0);
28
+ if (value.intensity > 1)
29
+ throw new Error("weather intensity must be <= 1");
30
+ if (value.temperatureOffset !== undefined)
31
+ finite(value.temperatureOffset, "weather temperatureOffset", -MAX_MAGNITUDE, MAX_MAGNITUDE);
32
+ if (value.lightning !== undefined) {
33
+ finite(value.lightning, "weather lightning", 0);
34
+ if (value.lightning > 1)
35
+ throw new Error("weather lightning must be <= 1");
36
+ }
37
+ }
38
+ function validateWind(value) {
39
+ if (value === undefined)
40
+ return;
41
+ objectKeys(value, ["direction", "speed", "gust", "gustFrequency", "turbulence", "seed"], "wind");
42
+ if (value.direction !== undefined) {
43
+ if (!Array.isArray(value.direction) || value.direction.length !== 2)
44
+ throw new Error("wind direction requires world X,Z");
45
+ for (const axis of value.direction)
46
+ finite(axis, "wind direction", -MAX_MAGNITUDE, MAX_MAGNITUDE);
47
+ }
48
+ for (const key of ["speed", "gust", "gustFrequency", "turbulence"]) {
49
+ if (value[key] !== undefined)
50
+ finite(value[key], `wind.${key}`, 0, key === "gustFrequency" ? 1000 : MAX_MAGNITUDE);
51
+ }
52
+ if (value.seed !== undefined && typeof value.seed !== "string")
53
+ finite(value.seed, "wind seed");
54
+ }
55
+ /** Validate authoring references and work bounds before persisting a weather document.
56
+ * @capability authored-weather-validation Reject invalid weather profiles, schedules, zones and wind before authoring mutations.
57
+ */
58
+ export function validateAuthoredWeather(config) {
59
+ objectKeys(config, ["ambient", "profiles", "initialProfileId", "schedule", "wind", "zones"], "weather");
60
+ for (const list of [config.profiles, config.schedule, config.zones]) {
61
+ if (list !== undefined && !Array.isArray(list))
62
+ throw new Error("weather profiles, schedule and zones must be arrays");
63
+ if (list !== undefined && list.length > MAX_ENTRIES)
64
+ throw new Error(`weather lists are limited to ${MAX_ENTRIES} entries`);
65
+ }
66
+ if (config.ambient !== undefined)
67
+ conditions(config.ambient);
68
+ validateWind(config.wind);
69
+ const ids = new Set();
70
+ for (const profile of config.profiles ?? []) {
71
+ identifier(profile.id, "weather profile id");
72
+ if (!profile.id || ids.has(profile.id))
73
+ throw new Error(`invalid or duplicate weather profile: ${profile.id}`);
74
+ ids.add(profile.id);
75
+ conditions(profile, true);
76
+ }
77
+ if (config.initialProfileId !== undefined)
78
+ identifier(config.initialProfileId, "initial weather profile id");
79
+ if (config.initialProfileId !== undefined && !ids.has(config.initialProfileId))
80
+ throw new Error(`unknown initial weather profile: ${config.initialProfileId}`);
81
+ let previous = -1;
82
+ for (const entry of config.schedule ?? []) {
83
+ objectKeys(entry, ["atSeconds", "profileId", "transitionSeconds"], "weather schedule");
84
+ identifier(entry.profileId, "scheduled weather profile id");
85
+ finite(entry.atSeconds, "weather schedule time", 0, MAX_DOMAIN);
86
+ if (entry.atSeconds <= previous)
87
+ throw new Error("weather schedule times must be strictly increasing");
88
+ previous = entry.atSeconds;
89
+ if (!ids.has(entry.profileId))
90
+ throw new Error(`unknown weather profile: ${entry.profileId}`);
91
+ if (entry.transitionSeconds !== undefined)
92
+ finite(entry.transitionSeconds, "weather transition duration", 0, MAX_DOMAIN);
93
+ }
94
+ ids.clear();
95
+ for (const zone of config.zones ?? []) {
96
+ objectKeys(zone, ["id", "center", "radius", "falloff", "weather", "wind", "radial"], "weather zone");
97
+ identifier(zone.id, "weather zone id");
98
+ if (zone.radial !== undefined && typeof zone.radial !== "boolean")
99
+ throw new Error("weather zone radial must be boolean");
100
+ if (!zone.id || ids.has(zone.id))
101
+ throw new Error(`invalid or duplicate weather zone: ${zone.id}`);
102
+ ids.add(zone.id);
103
+ if (!Array.isArray(zone.center) || zone.center.length !== 2)
104
+ throw new Error("weather zone center requires world X,Z");
105
+ for (const axis of zone.center)
106
+ finite(axis, "weather zone center", -MAX_DOMAIN, MAX_DOMAIN);
107
+ finite(zone.radius, "weather zone radius", Number.MIN_VALUE, MAX_DOMAIN);
108
+ if (zone.falloff !== undefined)
109
+ finite(zone.falloff, "weather zone falloff", 0, MAX_DOMAIN);
110
+ if (zone.weather !== undefined)
111
+ conditions(zone.weather);
112
+ validateWind(zone.wind);
113
+ }
114
+ }
115
+ function amounts(value) {
116
+ return [
117
+ value.mode === "rain" ? value.intensity : value.mode === "mixed" ? value.intensity * 0.5 : 0,
118
+ value.mode === "snow" ? value.intensity : value.mode === "mixed" ? value.intensity * 0.5 : 0,
119
+ value.mode === "dust" ? value.intensity : 0,
120
+ ];
121
+ }
122
+ function modeOf(rain, snow, dust) {
123
+ if (rain > 0 && snow > 0)
124
+ return "mixed";
125
+ if (rain > 0)
126
+ return "rain";
127
+ if (snow > 0)
128
+ return "snow";
129
+ return dust > 0 ? "dust" : "clear";
130
+ }
131
+ function blendOf(value) {
132
+ const [rain, snow, dust] = amounts(value);
133
+ return { rain, snow, dust, temperatureOffset: value.temperatureOffset ?? 0, lightning: value.lightning ?? 0 };
134
+ }
135
+ function blend(a, b, weight) {
136
+ const out = { ...a };
137
+ for (const key of ["rain", "snow", "dust", "temperatureOffset", "lightning"])
138
+ out[key] += (b[key] - a[key]) * weight;
139
+ return out;
140
+ }
141
+ function scheduledWeather(initial, transitions, time) {
142
+ let current = initial;
143
+ for (const entry of transitions) {
144
+ if (time < entry.at)
145
+ break;
146
+ current = blend(entry.from, entry.to, entry.duration === 0 ? 1 : Math.min(1, (time - entry.at) / entry.duration));
147
+ }
148
+ return current;
149
+ }
150
+ function compileWeather(source) {
151
+ validateAuthoredWeather(source);
152
+ const config = JSON.parse(JSON.stringify(source));
153
+ const profiles = new Map((config.profiles ?? []).map((profile) => [profile.id, Object.freeze(blendOf(profile))]));
154
+ const initial = config.initialProfileId === undefined ? Object.freeze(blendOf(config.ambient ?? CLEAR)) : profiles.get(config.initialProfileId);
155
+ const ambientWind = config.wind === undefined ? null : windField(config.wind);
156
+ const zones = (config.zones ?? []).map((zone) => Object.freeze({ zone, field: zone.wind === undefined ? null : windField(zone.wind) }));
157
+ const transitions = [];
158
+ for (const entry of config.schedule ?? []) {
159
+ transitions.push(Object.freeze({ at: entry.atSeconds, duration: entry.transitionSeconds ?? 0, from: Object.freeze(scheduledWeather(initial, transitions, entry.atSeconds)), to: profiles.get(entry.profileId) }));
160
+ }
161
+ return Object.freeze({ initial, profiles, ambientWind, zones: Object.freeze(zones), transitions: Object.freeze(transitions) });
162
+ }
163
+ /** Pure authored weather sampling; the caller's simulation clock owns pause, replay and restore.
164
+ * @capability authored-weather Sample scheduled precipitation and local wind from one authored document and simulation clock.
165
+ */
166
+ export function createAuthoredWeather(source = {}) {
167
+ const compiled = compileWeather(source);
168
+ return {
169
+ sample(x, z, timeSeconds) {
170
+ finite(x, "weather sample x", -MAX_DOMAIN, MAX_DOMAIN);
171
+ finite(z, "weather sample z", -MAX_DOMAIN, MAX_DOMAIN);
172
+ finite(timeSeconds, "weather sample time", 0, MAX_DOMAIN);
173
+ let value = scheduledWeather(compiled.initial, compiled.transitions, timeSeconds);
174
+ const ambient = compiled.ambientWind?.atPoint(x, z, timeSeconds) ?? [0, 0];
175
+ let wx = ambient[0];
176
+ let wz = ambient[1];
177
+ for (const { zone, field } of compiled.zones) {
178
+ const dx = x - zone.center[0];
179
+ const dz = z - zone.center[1];
180
+ const distance = Math.hypot(dx, dz);
181
+ const falloff = zone.falloff ?? 0;
182
+ if (distance > zone.radius + falloff)
183
+ continue;
184
+ const t = distance <= zone.radius ? 1 : 1 - (distance - zone.radius) / falloff;
185
+ const weight = t * t * (3 - 2 * t);
186
+ if (zone.weather !== undefined)
187
+ value = blend(value, blendOf(zone.weather), weight);
188
+ if (field !== null) {
189
+ const vector = zoneWind(field, zone.radial ?? false, dx, dz, distance, timeSeconds);
190
+ wx += vector[0] * weight;
191
+ wz += vector[1] * weight;
192
+ }
193
+ }
194
+ const precipitation = value.rain + value.snow;
195
+ return { ...value, mode: modeOf(value.rain, value.snow, value.dust), intensity: Math.min(1, precipitation + value.dust), timeSeconds, wind: [wx, wz], precipitation };
196
+ },
197
+ };
198
+ }
199
+ function zoneWind(field, radial, dx, dz, distance, time) {
200
+ if (!radial)
201
+ return field.atPoint(dx, dz, time);
202
+ if (distance === 0)
203
+ return [0, 0];
204
+ const speed = field.strengthAt(dx, dz, time);
205
+ return [dx / distance * speed, dz / distance * speed];
206
+ }
207
+ function exposureConfig(config) {
208
+ const out = { maxSurfaces: config.maxSurfaces ?? 1024, wettingRate: config.wettingRate ?? 0.2, dryingRate: config.dryingRate ?? 0.05, snowRate: config.snowRate ?? 0.1, meltRate: config.meltRate ?? 0.02 };
209
+ if (!Number.isInteger(out.maxSurfaces) || out.maxSurfaces < 1 || out.maxSurfaces > 65_536)
210
+ throw new Error("weather exposure maxSurfaces must be an integer in 1..65536");
211
+ for (const key of ["wettingRate", "dryingRate", "snowRate", "meltRate"])
212
+ finite(out[key], `weather exposure ${key}`, 0);
213
+ return out;
214
+ }
215
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
216
+ /** Bounded surface accumulation shared by gameplay and material adapters, with injected persistence.
217
+ * @capability weather-surface-accumulation Persist and retune bounded surface wetness, snow, temperature and exposure independently of rendering.
218
+ */
219
+ export function createWeatherExposure(config = {}, storage) {
220
+ let tuning = exposureConfig(config);
221
+ const values = new Map();
222
+ const snapshot = () => ({ surfaces: Array.from(values, ([id, value]) => ({ id, value: { ...value } })) });
223
+ const persist = () => storage?.write(snapshot());
224
+ function restore(state) {
225
+ if (state.surfaces.length > tuning.maxSurfaces)
226
+ throw new Error("weather exposure snapshot exceeds capacity");
227
+ const next = new Map();
228
+ for (const entry of state.surfaces) {
229
+ if (!entry.id || next.has(entry.id))
230
+ throw new Error("weather exposure snapshot has invalid surface ids");
231
+ for (const key of ["wetness", "snow", "heat", "exposure"])
232
+ finite(entry.value[key], `weather exposure ${key}`);
233
+ for (const key of ["wetness", "snow", "exposure"])
234
+ if (entry.value[key] < 0 || entry.value[key] > 1)
235
+ throw new Error(`weather exposure ${key} must be in 0..1`);
236
+ next.set(entry.id, { ...entry.value });
237
+ }
238
+ values.clear();
239
+ for (const [id, value] of next)
240
+ values.set(id, value);
241
+ persist();
242
+ }
243
+ const saved = storage?.read();
244
+ if (saved !== undefined)
245
+ restore(saved);
246
+ return {
247
+ step(dt, timeSeconds, surfaces, weather) {
248
+ finite(dt, "weather exposure dt", 0);
249
+ finite(timeSeconds, "weather exposure time", 0);
250
+ if (dt === 0)
251
+ return;
252
+ if (surfaces.length > tuning.maxSurfaces)
253
+ throw new Error("weather exposure step exceeds capacity");
254
+ const ids = new Set();
255
+ let added = 0;
256
+ for (const surface of surfaces) {
257
+ if (!surface.id || ids.has(surface.id))
258
+ throw new Error("weather exposure step has invalid surface ids");
259
+ ids.add(surface.id);
260
+ finite(surface.x, "weather exposure x");
261
+ finite(surface.z, "weather exposure z");
262
+ if (surface.exposure !== undefined)
263
+ finite(surface.exposure, "weather exposure exposure", 0);
264
+ if ((surface.exposure ?? 1) > 1)
265
+ throw new Error("weather exposure exposure must be <= 1");
266
+ if (surface.temperature !== undefined)
267
+ finite(surface.temperature, "weather exposure temperature");
268
+ if (!values.has(surface.id))
269
+ added += 1;
270
+ }
271
+ if (values.size + added > tuning.maxSurfaces)
272
+ throw new Error("weather exposure capacity exhausted; forget unused surfaces");
273
+ for (const surface of surfaces) {
274
+ const sample = weather.sample(surface.x, surface.z, timeSeconds);
275
+ const current = values.get(surface.id) ?? { wetness: 0, snow: 0, heat: 0, exposure: 1 };
276
+ const exposure = surface.exposure ?? 1;
277
+ const heat = surface.temperature ?? sample.temperatureOffset ?? 0;
278
+ const rain = sample.rain * exposure;
279
+ const snowfall = sample.snow * exposure * tuning.snowRate * dt;
280
+ const melt = Math.min(current.snow + snowfall, Math.max(0, heat) * tuning.meltRate * dt);
281
+ values.set(surface.id, {
282
+ wetness: clamp01(current.wetness + (rain * tuning.wettingRate - (1 - rain) * tuning.dryingRate) * dt + melt),
283
+ snow: clamp01(current.snow + snowfall - melt),
284
+ heat,
285
+ exposure,
286
+ });
287
+ }
288
+ persist();
289
+ },
290
+ sample(id) { const value = values.get(id); return value === undefined ? null : { ...value }; },
291
+ forget(id) { if (values.delete(id))
292
+ persist(); },
293
+ snapshot,
294
+ restore,
295
+ retune(next) {
296
+ const resolved = exposureConfig(next);
297
+ if (values.size > resolved.maxSurfaces)
298
+ throw new Error("weather exposure retune capacity is below retained surface count");
299
+ tuning = resolved;
300
+ },
301
+ };
302
+ }
303
+ /** Environment temperature, sunlight and shelter read the same authored precipitation as rendering.
304
+ * @capability weather-environment-field Connect authored precipitation and temperature changes to spatial environment queries.
305
+ */
306
+ export function createWeatherEnvironmentField(weather, config = {}) {
307
+ const base = createEnvironmentField({ ...config, rain: (x, z, time) => weather.sample(x, z, time).rain });
308
+ const temperature = (x, z, time, y) => base.temperature(x, z, time, y) + (weather.sample(x, z, time).temperatureOffset ?? 0);
309
+ return {
310
+ ...base,
311
+ temperature,
312
+ sample(x, z, time, y) { return { ...base.sample(x, z, time, y), temperature: temperature(x, z, time, y) }; },
313
+ };
314
+ }
@@ -3,6 +3,7 @@
3
3
  * as an instanced asset kit instead of untextured blocks. Pure serializable data — core resolves
4
4
  * bindings, the renderer loads them.
5
5
  */
6
+ import { type MaterialAsset, type MaterialAssignment } from "../material/materialAsset.js";
6
7
  import type { BuildingKitSlot, BuildingPartKind, BuildingSurfaceMaterial, BuildingVariantCounts, Vec3 } from "./buildings.js";
7
8
  /**
8
9
  * How a kit model's native bounds are mapped onto the slot box `BuildingPartPlacement.scale`
@@ -34,6 +35,8 @@ export interface BuildingKitPart {
34
35
  tint?: string;
35
36
  /** Tiled PBR maps applied over the model's own materials; `color` tints, `repeat` sets the tiling. */
36
37
  material?: BuildingSurfaceMaterial;
38
+ /** Editor-authored mesh/slot selectors applied after the broad tint and surface. */
39
+ materialAssignments?: readonly MaterialAssignment[];
37
40
  /**
38
41
  * Quarter-turn the model when its long horizontal axis disagrees with the slot's, so a panel
39
42
  * authored running along Z still tiles a bay that runs along X. Default true — modular kits do not
@@ -49,6 +52,8 @@ export interface BuildingKitPart {
49
52
  */
50
53
  export interface BuildingKit {
51
54
  id: string;
55
+ /** The caller's existing editor-document material assets; only assigned IDs are loaded. */
56
+ materialAssets?: readonly MaterialAsset[];
52
57
  /** Variants per part kind; `BuildingKitSlot.variant` indexes this list, wrapping. */
53
58
  parts: Readonly<Partial<Record<BuildingPartKind, readonly BuildingKitPart[]>>>;
54
59
  /** Overrides keyed by `BuildingKitSlot.key` (`${facade}.${kind}`); beats `parts` for that facade. */
@@ -63,6 +68,7 @@ export type BuildingKitPartInput = string | BuildingKitPart;
63
68
  /** The authoring shape {@link defineBuildingKit} normalizes into a {@link BuildingKit}. */
64
69
  export interface BuildingKitInput {
65
70
  id?: string;
71
+ materialAssets?: readonly MaterialAsset[];
66
72
  parts: Readonly<Partial<Record<BuildingPartKind, readonly BuildingKitPartInput[]>>>;
67
73
  slots?: Readonly<Record<string, readonly BuildingKitPartInput[]>>;
68
74
  omit?: readonly BuildingPartKind[];
@@ -85,10 +91,9 @@ export type BuildingKitBinding = {
85
91
  };
86
92
  /**
87
93
  * Validates and normalizes a kit description, accepting bare model strings as single-field variants.
88
- * Throws on empty variant lists so a typo fails at authoring time rather than silently falling back
89
- * to untextured boxes.
94
+ * Throws on empty variants or invalid editor material references/selectors before loading models.
90
95
  *
91
- * @capability building-kit bind generated facade part slots to real models instead of untextured blocks
96
+ * @capability building-kit bind generated facade part slots to caller models and editor-authored named materials
92
97
  */
93
98
  export declare function defineBuildingKit(input: BuildingKitInput): BuildingKit;
94
99
  /**
@@ -1,36 +1,49 @@
1
+ /**
2
+ * Binds the part slots `world/buildings` generates to real models, so a procedural building renders
3
+ * as an instanced asset kit instead of untextured blocks. Pure serializable data — core resolves
4
+ * bindings, the renderer loads them.
5
+ */
6
+ import { parseMaterialAssignments, validateMaterialAssignments } from "../material/materialAsset.js";
1
7
  const BOX = { type: "box" };
2
8
  const OMIT = { type: "omit" };
3
9
  const DEFAULT_FIT = "stretch";
4
- function normalizePart(input, where) {
10
+ function normalizePart(input, where, assets) {
5
11
  const part = typeof input === "string" ? { model: input } : input;
6
12
  if (typeof part.model !== "string" || part.model.length === 0) {
7
13
  throw new Error(`Building kit ${where} has a variant with no model reference.`);
8
14
  }
15
+ if (part.materialAssignments !== undefined) {
16
+ const assignments = parseMaterialAssignments(part.materialAssignments);
17
+ const errors = validateMaterialAssignments(assignments, assets, { disallowedTextureRoles: ["height"] }).filter(item => item.severity === "error");
18
+ if (errors.length > 0)
19
+ throw new Error(`Building kit ${where}: ${errors.map(item => `${item.path}: ${item.message}`).join("; ")}`);
20
+ return { ...part, materialAssignments: assignments };
21
+ }
9
22
  return part;
10
23
  }
11
- function normalizeList(list, where) {
24
+ function normalizeList(list, where, assets) {
12
25
  if (list.length === 0) {
13
26
  throw new Error(`Building kit ${where} binds an empty variant list. Bind at least one model, or add the kind to \`omit\` to drop it.`);
14
27
  }
15
- return list.map((entry) => normalizePart(entry, where));
28
+ return list.map((entry) => normalizePart(entry, where, assets));
16
29
  }
17
30
  /**
18
31
  * Validates and normalizes a kit description, accepting bare model strings as single-field variants.
19
- * Throws on empty variant lists so a typo fails at authoring time rather than silently falling back
20
- * to untextured boxes.
32
+ * Throws on empty variants or invalid editor material references/selectors before loading models.
21
33
  *
22
- * @capability building-kit bind generated facade part slots to real models instead of untextured blocks
34
+ * @capability building-kit bind generated facade part slots to caller models and editor-authored named materials
23
35
  */
24
36
  export function defineBuildingKit(input) {
25
37
  const parts = {};
26
38
  for (const [kind, list] of Object.entries(input.parts)) {
27
39
  if (list === undefined)
28
40
  continue;
29
- parts[kind] = normalizeList(list, `"${input.id ?? "kit"}" part "${kind}"`);
41
+ parts[kind] = normalizeList(list, `"${input.id ?? "kit"}" part "${kind}"`, input.materialAssets ?? []);
30
42
  }
31
43
  const kit = {
32
44
  id: input.id ?? "building-kit",
33
45
  parts,
46
+ ...(input.materialAssets === undefined ? {} : { materialAssets: input.materialAssets }),
34
47
  ...(input.omit === undefined ? {} : { omit: input.omit }),
35
48
  ...(input.fit === undefined ? {} : { fit: input.fit }),
36
49
  };
@@ -38,7 +51,7 @@ export function defineBuildingKit(input) {
38
51
  return kit;
39
52
  const slots = {};
40
53
  for (const [key, list] of Object.entries(input.slots)) {
41
- slots[key] = normalizeList(list, `"${kit.id}" slot "${key}"`);
54
+ slots[key] = normalizeList(list, `"${kit.id}" slot "${key}"`, input.materialAssets ?? []);
42
55
  }
43
56
  return { ...kit, slots };
44
57
  }
@@ -0,0 +1,21 @@
1
+ import type { GameContext } from "../runtime/gameContextTypes.js";
2
+ import type { MotionIntents } from "../runtime/motionIntents.js";
3
+ /** The game selects affected players, coupling, target bits and its acceleration limit. */
4
+ export interface EnvironmentMotionTarget {
5
+ entityId: string;
6
+ motion: Pick<MotionIntents, "impulse" | "pushHorizontal">;
7
+ /** Wind coupling in 1/second; supported range 0..1e12. */
8
+ windResponse: number;
9
+ /** Maximum combined acceleration in metres/second²; supported range 0..1e12. */
10
+ maxAcceleration: number;
11
+ mask?: number;
12
+ /** Opt into authored shelter exposure scaling for wind and localized forces. */
13
+ sheltered?: boolean;
14
+ }
15
+ /** Inject authoritative spatial forces before existing movement consumes its motion intents.
16
+ * @capability environment-motion apply capped spatial wind and vortex forces to game-selected actors through shared motion intents
17
+ */
18
+ export declare function installEnvironmentMotion(ctx: GameContext, options: {
19
+ targets: () => readonly EnvironmentMotionTarget[];
20
+ maxTargets?: number;
21
+ }): () => void;
@@ -0,0 +1,42 @@
1
+ /** Inject authoritative spatial forces before existing movement consumes its motion intents.
2
+ * @capability environment-motion apply capped spatial wind and vortex forces to game-selected actors through shared motion intents
3
+ */
4
+ export function installEnvironmentMotion(ctx, options) {
5
+ const limit = options.maxTargets ?? 64;
6
+ if (!Number.isInteger(limit) || limit < 1 || limit > 4096)
7
+ throw new Error("environment motion target budget must be an integer in 1..4096");
8
+ const seen = new Set();
9
+ const pending = [];
10
+ return ctx.sim.addStage({
11
+ id: "environment-motion",
12
+ phase: "beforeMovement",
13
+ run(_realDt, _tick, gameDt) {
14
+ if (gameDt <= 0)
15
+ return;
16
+ const targets = options.targets();
17
+ if (targets.length > limit)
18
+ throw new Error("environment motion target budget exceeded");
19
+ seen.clear();
20
+ pending.length = 0;
21
+ for (const target of targets) {
22
+ if (!target || typeof target.entityId !== "string" || target.entityId.length === 0 || typeof target.motion?.pushHorizontal !== "function" || typeof target.motion?.impulse !== "function")
23
+ throw new Error("environment motion target requires an entity id and motion sink");
24
+ if (seen.has(target.entityId))
25
+ throw new Error(`duplicate environment motion target ${target.entityId}`);
26
+ seen.add(target.entityId);
27
+ const entity = ctx.scene.entity.get(target.entityId);
28
+ if (entity === null)
29
+ continue;
30
+ const acceleration = ctx.environment.accelerationAt(entity.position, ctx.time.now(), target.windResponse, target.maxAcceleration, target.mask ?? 0);
31
+ const exposure = target.sheltered === true ? ctx.environment.exposureAt(...entity.position) : 1;
32
+ pending.push({ target, acceleration, exposure });
33
+ }
34
+ for (const { target, acceleration, exposure } of pending) {
35
+ if (acceleration[0] !== 0 || acceleration[2] !== 0)
36
+ target.motion.pushHorizontal(acceleration[0] * gameDt * exposure, acceleration[2] * gameDt * exposure);
37
+ if (acceleration[1] !== 0)
38
+ target.motion.impulse(acceleration[1] * gameDt * exposure);
39
+ }
40
+ },
41
+ });
42
+ }
@@ -66,13 +66,13 @@ export interface TerrainDetailConfig {
66
66
  * PBR map URLs for a real ground texture — the same shape
67
67
  * `buildMaterialCatalog({ basePath }).resolve(id)!.maps` from `@jgengine/assets` returns. Kept
68
68
  * dependency-free here so `core` never imports the assets package; any URLs (pulled maps, a CDN, a
69
- * data URI) satisfy it.
69
+ * data URI) satisfy it. Omitted roughness keeps the terrain scalar; omitted AO skips occlusion sampling.
70
70
  */
71
71
  export interface TerrainMaterialMaps {
72
72
  color: string;
73
73
  normal: string;
74
- roughness: string;
75
- ao: string;
74
+ roughness?: string;
75
+ ao?: string;
76
76
  displacement: string;
77
77
  }
78
78
  /**
@@ -62,5 +62,60 @@ export declare function boundaryNeighbors(grid: FootprintGrid, cells: readonly G
62
62
  * (no occupied neighbors at all) passes unless `requireConnection` demands one.
63
63
  */
64
64
  export declare function hasValidAdjacency(grid: FootprintGrid, cells: readonly GridCell[], accepts: (neighborKind: string) => boolean, requireConnection?: boolean): boolean;
65
+ /** Caller-owned logical footprint. Cells must be unique, safe integer addresses and cardinally connected. */
66
+ export interface RegionFootprint {
67
+ readonly id: string;
68
+ readonly kind: string;
69
+ readonly tier: number;
70
+ readonly cells: readonly GridCell[];
71
+ /** Nonnegative finite contribution; independent of occupied area. */
72
+ readonly capacity: number;
73
+ }
74
+ /** Original footprint identity and capacity retained when regions pool. */
75
+ export interface RegionContribution {
76
+ readonly id: string;
77
+ readonly cells: readonly GridCell[];
78
+ readonly capacity: number;
79
+ }
80
+ /** Plain JSON pooled region. Contributions partition its cells and sum to its capacity. */
81
+ export interface MergedRegion extends RegionFootprint {
82
+ readonly contributions: readonly RegionContribution[];
83
+ }
84
+ /** Optional caller work limits, checked before cell traversal. Omitted limits follow input size. */
85
+ export interface RegionWorkBudget {
86
+ readonly maxCells?: number;
87
+ readonly maxContributions?: number;
88
+ }
89
+ /** Identities for split sides containing multiple contributions; singletons retain their original id. */
90
+ export interface RegionSplitIds {
91
+ readonly left?: string;
92
+ readonly right?: string;
93
+ }
94
+ /** A standalone footprint or a pooled region carrying its original contributions. */
95
+ export type FootprintRegion = RegionFootprint | MergedRegion;
96
+ /** Matching kind/tier, disjoint valid source identities/cells, and cardinal contact. Linear in input size.
97
+ * @capability mergeable-footprint-regions check compatible adjacent regions within caller work budgets
98
+ */
99
+ export declare function canMerge(a: FootprintRegion, b: FootprintRegion, budget?: RegionWorkBudget): boolean;
100
+ /**
101
+ * Pool compatible footprints under caller-owned `id`, retaining original capacities and identities.
102
+ * Returns null for invalid data, kind/tier mismatch, overlap, no cardinal contact or exhausted budget.
103
+ * Does not mutate inputs or grid reservations; costs, merge width and upgrade policy remain caller-owned.
104
+ * Capacity is the ordered JavaScript-number sum of original contributions, independent of cell area.
105
+ *
106
+ * @capability merge-footprint-regions pool adjacent compatible footprints with serializable capacity provenance
107
+ */
108
+ export declare function mergeFootprints(a: FootprintRegion, b: FootprintRegion, id: string, budget?: RegionWorkBudget): MergedRegion | null;
109
+ /**
110
+ * Partition whole original contributions by `leftIds`; both sides must remain cardinally connected.
111
+ * Singletons restore their original id and capacity; larger sides require explicit output identities.
112
+ * Returns null for invalid provenance/budget, unknown or duplicate ids, empty/disconnected sides, or
113
+ * conflicting output identities (including identities from the opposite side's contributions).
114
+ * An output may reuse its own side's source identity. Cell cuts/capacity repricing require caller policy.
115
+ * Both sides inherit the current region kind/tier. Inputs and grid reservations remain unchanged.
116
+ *
117
+ * @capability split-footprint-regions split pooled regions along retained contributions without area-based capacity loss
118
+ */
119
+ export declare function splitRegion(region: MergedRegion, leftIds: readonly string[], ids?: RegionSplitIds, budget?: RegionWorkBudget): [FootprintRegion, FootprintRegion] | null;
65
120
  /** Bridges live reservations into `world/placement`'s `PlacementRules.obstacles` so `validatePlacement`/`createPlacementController` see the grid's committed footprints unchanged. */
66
121
  export declare function footprintObstacles(grid: FootprintGrid): PlacementObstacle[];