@jgengine/core 0.18.0 → 0.19.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 (318) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/ai/decisionGraph.d.ts +96 -0
  3. package/dist/ai/decisionGraph.js +212 -0
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/mobBrainGraph.d.ts +3 -0
  7. package/dist/ai/mobBrainGraph.js +25 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/tacticalQueries.d.ts +30 -0
  11. package/dist/ai/tacticalQueries.js +45 -0
  12. package/dist/ai/threat.d.ts +8 -0
  13. package/dist/ai/threat.js +10 -0
  14. package/dist/anim/animGraph.d.ts +155 -0
  15. package/dist/anim/animGraph.js +414 -0
  16. package/dist/anim/boneTexture.d.ts +55 -0
  17. package/dist/anim/boneTexture.js +43 -0
  18. package/dist/anim/footPlacement.d.ts +57 -0
  19. package/dist/anim/footPlacement.js +67 -0
  20. package/dist/anim/ikSolver.d.ts +51 -0
  21. package/dist/anim/ikSolver.js +124 -0
  22. package/dist/anim/locomotionGraph.d.ts +35 -0
  23. package/dist/anim/locomotionGraph.js +62 -0
  24. package/dist/assets/spriteAtlas.d.ts +21 -0
  25. package/dist/assets/spriteAtlas.js +103 -0
  26. package/dist/audio/audioFalloff.d.ts +12 -0
  27. package/dist/audio/doppler.d.ts +21 -0
  28. package/dist/audio/doppler.js +32 -0
  29. package/dist/audio/engineLayers.d.ts +83 -0
  30. package/dist/audio/engineLayers.js +115 -0
  31. package/dist/combat/abilityKit.d.ts +1 -0
  32. package/dist/combat/abilityKit.js +1 -0
  33. package/dist/combat/death.d.ts +7 -0
  34. package/dist/combat/shotOrigin.d.ts +6 -0
  35. package/dist/combat/shotOrigin.js +6 -0
  36. package/dist/combat/weaponFeedback.d.ts +36 -0
  37. package/dist/combat/weaponFeedback.js +42 -0
  38. package/dist/combat/weaponHandling.d.ts +161 -0
  39. package/dist/combat/weaponHandling.js +202 -0
  40. package/dist/combat/weaponPresentation.d.ts +115 -0
  41. package/dist/combat/weaponPresentation.js +96 -0
  42. package/dist/devtools/warnOnce.d.ts +8 -0
  43. package/dist/devtools/warnOnce.js +18 -0
  44. package/dist/economy/currency.d.ts +16 -2
  45. package/dist/economy/currency.js +55 -14
  46. package/dist/economy/wallet.d.ts +5 -4
  47. package/dist/economy/wallet.js +8 -4
  48. package/dist/editor/commandApply.js +1 -0
  49. package/dist/editor/commands.d.ts +4 -1
  50. package/dist/editor/document.d.ts +2 -1
  51. package/dist/editor/document.js +92 -1
  52. package/dist/editor/environment.d.ts +3 -0
  53. package/dist/editor/environment.js +24 -0
  54. package/dist/editor/index.d.ts +4 -2
  55. package/dist/editor/index.js +1 -1
  56. package/dist/editor/types.d.ts +20 -0
  57. package/dist/faction/reputation.d.ts +6 -0
  58. package/dist/faction/reputation.js +11 -0
  59. package/dist/game/cameraConfig.d.ts +59 -1
  60. package/dist/game/chat.d.ts +22 -1
  61. package/dist/game/chat.js +48 -19
  62. package/dist/game/controlGate.d.ts +6 -0
  63. package/dist/game/controlGate.js +30 -1
  64. package/dist/game/defineGame.d.ts +10 -1
  65. package/dist/game/events.d.ts +14 -1
  66. package/dist/game/lootTable.d.ts +4 -0
  67. package/dist/game/lootTable.js +16 -0
  68. package/dist/game/pixelPerfect.d.ts +25 -0
  69. package/dist/game/pixelPerfect.js +29 -0
  70. package/dist/game/playableGame.d.ts +167 -6
  71. package/dist/game/systemRuntime.js +6 -1
  72. package/dist/game/toasts.d.ts +8 -0
  73. package/dist/game/toasts.js +7 -0
  74. package/dist/game/vfxInstance.d.ts +9 -0
  75. package/dist/game/vfxInstance.js +12 -1
  76. package/dist/game/viewports.d.ts +43 -0
  77. package/dist/game/viewports.js +46 -0
  78. package/dist/game/worldItem.d.ts +5 -1
  79. package/dist/game/worldItem.js +0 -1
  80. package/dist/input/actionBindings.d.ts +17 -2
  81. package/dist/input/actionBindings.js +24 -11
  82. package/dist/input/actionContexts.d.ts +45 -0
  83. package/dist/input/actionContexts.js +113 -0
  84. package/dist/input/axisInput.d.ts +4 -0
  85. package/dist/input/axisInput.js +4 -0
  86. package/dist/input/axisShaper.d.ts +73 -0
  87. package/dist/input/axisShaper.js +105 -0
  88. package/dist/input/gamepadModel.d.ts +79 -0
  89. package/dist/input/gamepadModel.js +108 -0
  90. package/dist/input/haptics.d.ts +50 -0
  91. package/dist/input/haptics.js +74 -0
  92. package/dist/input/inputBuffer.d.ts +40 -0
  93. package/dist/input/inputBuffer.js +59 -0
  94. package/dist/input/lookChannel.d.ts +10 -0
  95. package/dist/input/lookChannel.js +6 -0
  96. package/dist/input/pointer.d.ts +2 -0
  97. package/dist/input/pointer.js +4 -0
  98. package/dist/item/affix.d.ts +6 -0
  99. package/dist/item/affix.js +6 -0
  100. package/dist/meta/changelog.d.ts +1 -1
  101. package/dist/meta/changelog.js +196 -1
  102. package/dist/movement/characterController.d.ts +76 -0
  103. package/dist/movement/characterController.js +216 -0
  104. package/dist/movement/freeFlight.d.ts +119 -0
  105. package/dist/movement/freeFlight.js +294 -0
  106. package/dist/movement/glideModel.d.ts +3 -0
  107. package/dist/movement/glideModel.js +6 -0
  108. package/dist/movement/grappleSwing.d.ts +2 -0
  109. package/dist/movement/grappleSwing.js +5 -1
  110. package/dist/movement/movementModel.d.ts +39 -0
  111. package/dist/movement/movementModel.js +62 -19
  112. package/dist/movement/movementProbe.d.ts +51 -0
  113. package/dist/movement/movementProbe.js +119 -0
  114. package/dist/movement/playerMovement.d.ts +30 -3
  115. package/dist/movement/playerMovement.js +304 -7
  116. package/dist/movement/serverStep.d.ts +5 -0
  117. package/dist/movement/serverStep.js +6 -0
  118. package/dist/movement/solidObstacles.d.ts +39 -8
  119. package/dist/movement/solidObstacles.js +71 -6
  120. package/dist/movement/voxelController.js +7 -7
  121. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  122. package/dist/multiplayer/poseSyncGate.js +9 -1
  123. package/dist/multiplayer/presenceContract.d.ts +6 -0
  124. package/dist/multiplayer/presenceModel.d.ts +2 -0
  125. package/dist/multiplayer/presenceModel.js +11 -0
  126. package/dist/nav/navFromEnvironment.d.ts +9 -3
  127. package/dist/nav/navFromEnvironment.js +18 -12
  128. package/dist/nav/navMesh.d.ts +74 -0
  129. package/dist/nav/navMesh.js +570 -0
  130. package/dist/physics/aircraftDynamics.d.ts +390 -0
  131. package/dist/physics/aircraftDynamics.js +704 -0
  132. package/dist/physics/boatDynamics.d.ts +116 -0
  133. package/dist/physics/boatDynamics.js +160 -0
  134. package/dist/physics/drivableVehicle.d.ts +22 -8
  135. package/dist/physics/drivableVehicle.js +4 -2
  136. package/dist/physics/flightDynamics.d.ts +29 -4
  137. package/dist/physics/flightDynamics.js +28 -5
  138. package/dist/physics/handlingProbe.d.ts +249 -0
  139. package/dist/physics/handlingProbe.js +606 -0
  140. package/dist/physics/kinematicVehicle.d.ts +37 -2
  141. package/dist/physics/kinematicVehicle.js +15 -0
  142. package/dist/physics/physicsBackend.d.ts +203 -0
  143. package/dist/physics/physicsBackend.js +49 -0
  144. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  145. package/dist/physics/physicsBackendConformance.js +151 -0
  146. package/dist/physics/physicsWorldBackend.d.ts +45 -0
  147. package/dist/physics/physicsWorldBackend.js +518 -0
  148. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  149. package/dist/physics/vehicleBackendLink.js +102 -0
  150. package/dist/physics/vehicleDynamics.d.ts +374 -0
  151. package/dist/physics/vehicleDynamics.js +737 -0
  152. package/dist/physics/worldColliders.d.ts +13 -0
  153. package/dist/physics/worldColliders.js +128 -0
  154. package/dist/puzzle/cellGrid.d.ts +15 -9
  155. package/dist/puzzle/cellGrid.js +13 -9
  156. package/dist/random/rng.d.ts +12 -1
  157. package/dist/random/rng.js +20 -3
  158. package/dist/render/environment.d.ts +23 -0
  159. package/dist/render/environment.js +1 -0
  160. package/dist/render/lookPreset.d.ts +19 -6
  161. package/dist/render/lookPreset.js +56 -4
  162. package/dist/render/postProcessing.d.ts +40 -1
  163. package/dist/render/postProcessing.js +1 -1
  164. package/dist/render/sprite2d.d.ts +38 -0
  165. package/dist/render/sprite2d.js +40 -0
  166. package/dist/runtime/adapter.d.ts +20 -1
  167. package/dist/runtime/adapter.js +8 -1
  168. package/dist/runtime/cameraDirector.d.ts +17 -3
  169. package/dist/runtime/cameraDirector.js +25 -0
  170. package/dist/runtime/commandInput.d.ts +52 -0
  171. package/dist/runtime/commandInput.js +71 -0
  172. package/dist/runtime/commandRunner.d.ts +39 -7
  173. package/dist/runtime/commandRunner.js +35 -5
  174. package/dist/runtime/context/combat.d.ts +1 -0
  175. package/dist/runtime/context/combat.js +2 -1
  176. package/dist/runtime/context/deathLoot.d.ts +2 -0
  177. package/dist/runtime/context/deathLoot.js +1 -0
  178. package/dist/runtime/gameContext.js +18 -1
  179. package/dist/runtime/gameContextTypes.d.ts +33 -7
  180. package/dist/runtime/gameRuntime.d.ts +23 -5
  181. package/dist/runtime/gameRuntime.js +21 -4
  182. package/dist/runtime/headlessRunner.d.ts +10 -0
  183. package/dist/runtime/headlessRunner.js +26 -8
  184. package/dist/runtime/hostPolicy.d.ts +3 -2
  185. package/dist/runtime/hostPolicy.js +8 -10
  186. package/dist/runtime/hostedGameRunner.js +34 -6
  187. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  188. package/dist/runtime/hostedWorldSession.js +29 -6
  189. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  190. package/dist/runtime/hostedWorldStore.js +1 -0
  191. package/dist/runtime/inputRecorder.d.ts +30 -0
  192. package/dist/runtime/inputRecorder.js +59 -0
  193. package/dist/runtime/inputSnapshot.d.ts +14 -0
  194. package/dist/runtime/inputSnapshot.js +11 -0
  195. package/dist/runtime/localPlayers.d.ts +73 -0
  196. package/dist/runtime/localPlayers.js +119 -0
  197. package/dist/runtime/poseInterpolation.d.ts +36 -0
  198. package/dist/runtime/poseInterpolation.js +91 -0
  199. package/dist/runtime/prediction.d.ts +45 -0
  200. package/dist/runtime/prediction.js +62 -0
  201. package/dist/runtime/simContext.d.ts +45 -0
  202. package/dist/runtime/simContext.js +65 -0
  203. package/dist/runtime/simLoop.d.ts +52 -0
  204. package/dist/runtime/simLoop.js +72 -0
  205. package/dist/runtime/simSnapshot.d.ts +32 -0
  206. package/dist/runtime/simSnapshot.js +60 -0
  207. package/dist/runtime/snapshot.d.ts +8 -0
  208. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  209. package/dist/runtime/snapshotBuffer.js +58 -0
  210. package/dist/runtime/transport.d.ts +41 -2
  211. package/dist/runtime/transport.js +7 -1
  212. package/dist/runtime/worldChunks.d.ts +2 -0
  213. package/dist/runtime/worldChunks.js +14 -0
  214. package/dist/runtime/worldProjection.d.ts +9 -0
  215. package/dist/runtime/worldProjection.js +17 -1
  216. package/dist/runtime/worldSnapshot.d.ts +6 -0
  217. package/dist/runtime/worldSnapshot.js +3 -0
  218. package/dist/scene/behaviorRuntime.d.ts +29 -1
  219. package/dist/scene/behaviorRuntime.js +71 -4
  220. package/dist/scene/behaviors.d.ts +12 -1
  221. package/dist/scene/builtinSceneKinds.js +10 -0
  222. package/dist/scene/sceneKinds.d.ts +9 -0
  223. package/dist/scene/vehicleSeat.d.ts +5 -1
  224. package/dist/scene/vehicleSeat.js +5 -1
  225. package/dist/sensor/concealment.d.ts +6 -0
  226. package/dist/sensor/concealment.js +8 -0
  227. package/dist/sensor/freezeMonitor.d.ts +6 -0
  228. package/dist/sensor/freezeMonitor.js +8 -0
  229. package/dist/sensor/perception.d.ts +70 -0
  230. package/dist/sensor/perception.js +189 -0
  231. package/dist/sensor/recordingBuffer.d.ts +3 -0
  232. package/dist/sensor/recordingBuffer.js +8 -0
  233. package/dist/session/roundState.d.ts +4 -0
  234. package/dist/session/roundState.js +11 -0
  235. package/dist/settings/frameRateLimit.d.ts +16 -0
  236. package/dist/settings/frameRateLimit.js +37 -0
  237. package/dist/settings/graphicsProfile.d.ts +21 -0
  238. package/dist/settings/graphicsProfile.js +48 -0
  239. package/dist/settings/graphicsSettings.d.ts +40 -0
  240. package/dist/settings/graphicsSettings.js +81 -0
  241. package/dist/settings/settingsModel.d.ts +9 -1
  242. package/dist/settings/settingsModel.js +10 -0
  243. package/dist/stats/eventMeter.d.ts +1 -1
  244. package/dist/stats/eventMeter.js +1 -1
  245. package/dist/time/accrueSince.d.ts +10 -0
  246. package/dist/time/accrueSince.js +9 -0
  247. package/dist/time/rateWindow.d.ts +16 -0
  248. package/dist/time/rateWindow.js +14 -0
  249. package/dist/time/serverTick.d.ts +21 -3
  250. package/dist/time/serverTick.js +14 -13
  251. package/dist/vfx/feedbackMixer.d.ts +61 -0
  252. package/dist/vfx/feedbackMixer.js +106 -0
  253. package/dist/vfx/particleDirector.d.ts +68 -0
  254. package/dist/vfx/particleDirector.js +81 -0
  255. package/dist/visibility/simulationCulling.d.ts +8 -1
  256. package/dist/visibility/simulationCulling.js +7 -0
  257. package/dist/visibility/spatialIndex.d.ts +21 -0
  258. package/dist/visibility/spatialIndex.js +16 -0
  259. package/dist/world/authoredSolids.d.ts +46 -0
  260. package/dist/world/authoredSolids.js +75 -0
  261. package/dist/world/buildingGenerator.js +2 -2
  262. package/dist/world/buildingKit.d.ts +3 -1
  263. package/dist/world/buildings.d.ts +46 -2
  264. package/dist/world/buildings.js +53 -3
  265. package/dist/world/cityContent.js +103 -25
  266. package/dist/world/cityKind.d.ts +139 -0
  267. package/dist/world/cityKind.js +367 -16
  268. package/dist/world/features.d.ts +56 -3
  269. package/dist/world/features.js +13 -2
  270. package/dist/world/footprintGrid.d.ts +7 -0
  271. package/dist/world/footprintGrid.js +16 -0
  272. package/dist/world/interiors.d.ts +1 -0
  273. package/dist/world/interiors.js +5 -3
  274. package/dist/world/lod.d.ts +6 -0
  275. package/dist/world/lod.js +8 -0
  276. package/dist/world/placement.d.ts +7 -0
  277. package/dist/world/placement.js +2 -0
  278. package/dist/world/placementController.d.ts +1 -1
  279. package/dist/world/placementController.js +7 -0
  280. package/dist/world/raceCircuit.d.ts +168 -0
  281. package/dist/world/raceCircuit.js +797 -0
  282. package/dist/world/terraform.d.ts +8 -0
  283. package/dist/world/terraform.js +7 -0
  284. package/dist/world/terrain.d.ts +5 -1
  285. package/dist/world/terrain.js +5 -0
  286. package/dist/world/territory.d.ts +97 -0
  287. package/dist/world/territory.js +182 -0
  288. package/dist/world/walls.d.ts +7 -0
  289. package/dist/world/walls.js +7 -0
  290. package/dist/world/worldSolids.d.ts +84 -0
  291. package/dist/world/worldSolids.js +240 -0
  292. package/dist/world.d.ts +5 -3
  293. package/dist/world.js +4 -2
  294. package/package.json +1 -1
  295. package/skills/ce-handoff/SKILL.md +2 -2
  296. package/skills/game-design/SKILL.md +11 -1
  297. package/skills/jgengine/SKILL.md +9 -5
  298. package/skills/jgengine/api.md +205 -101
  299. package/skills/jgengine/capabilities.md +56 -0
  300. package/skills/jgengine/recipes/minimal-game.md +3 -0
  301. package/skills/jgengine-combat/SKILL.md +5 -0
  302. package/skills/jgengine-combat/api.md +138 -103
  303. package/skills/jgengine-combat/capabilities.md +29 -1
  304. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  305. package/skills/jgengine-gameplay/SKILL.md +10 -0
  306. package/skills/jgengine-gameplay/api.md +501 -453
  307. package/skills/jgengine-gameplay/capabilities.md +36 -0
  308. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  309. package/skills/jgengine-world/SKILL.md +21 -3
  310. package/skills/jgengine-world/api.md +1172 -625
  311. package/skills/jgengine-world/capabilities.md +229 -3
  312. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  313. package/skills/jgengine-world/recipes/controls.md +44 -0
  314. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  315. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  316. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  317. package/skills/jgengine-world/reference.md +31 -2
  318. package/skills/jgengine-world/visibility.md +112 -0
@@ -0,0 +1,115 @@
1
+ import type { WeaponHandlingFrame } from "./weaponHandling.js";
2
+ /** Camera-space position `[x, y, z]` in metres: `+x` right, `+y` up, `-z` forward. */
3
+ export type ViewmodelOffset = readonly [number, number, number];
4
+ /**
5
+ * How one weapon sits in first person. Offsets are camera-space metres, angles are radians, FOVs are
6
+ * degrees. Every field is optional; two weapons differ by the ones they set.
7
+ */
8
+ export interface WeaponPresentationTuning {
9
+ /** Viewmodel anchor at the hip (default `[0.34, -0.26, -0.72]`). */
10
+ hip?: ViewmodelOffset;
11
+ /** Viewmodel anchor fully aimed; put the sight on the camera axis (default `[0, -0.07, -0.55]`). */
12
+ ads?: ViewmodelOffset;
13
+ /** Viewmodel FOV in degrees, independent of the world FOV; omit to draw the viewmodel at the world FOV. */
14
+ viewmodelFov?: {
15
+ hip?: number;
16
+ ads?: number;
17
+ };
18
+ /** World FOV multiplier at full ADS, e.g. `0.8` for a light zoom (default `1`). */
19
+ adsZoom?: number;
20
+ /** Viewmodel lag behind look motion. */
21
+ sway?: {
22
+ /** Radians of lag per rad/s of look speed (default `0.02`). */
23
+ perRadPerSec?: number;
24
+ /** Largest lag, rad (default `0.08`). */
25
+ max?: number;
26
+ /** How fast lag follows its target, 1/s (default `12`). */
27
+ response?: number;
28
+ /** Lag multiplier at full ADS (default `0.3`). */
29
+ adsScale?: number;
30
+ };
31
+ /** Walk bob driven by the movement probe's speed and `bobPhase`. */
32
+ bob?: {
33
+ /** Side and vertical amplitude in metres at `fullSpeed` (default `[0.012, 0.01]`). */
34
+ amplitude?: readonly [number, number];
35
+ /** Ground speed that gives the full amplitude, m/s (default `4`). */
36
+ fullSpeed?: number;
37
+ /** Bob multiplier at full ADS (default `0.25`). */
38
+ adsScale?: number;
39
+ };
40
+ /** Viewmodel reaction to the handling's recoil offset. */
41
+ kick?: {
42
+ /** Metres pushed back per radian of recoil (default `1.2`). */
43
+ back?: number;
44
+ /** Viewmodel muzzle rise per radian of recoil (default `1.5`). */
45
+ rise?: number;
46
+ };
47
+ }
48
+ /** One frame of inputs: look motion, the movement probe, and the weapon's handling readout. */
49
+ export interface WeaponPresentationInput {
50
+ /** Look yaw speed, rad/s, positive turning left (the same sign as a camera yaw that grows left). */
51
+ lookYawRate: number;
52
+ /** Look pitch speed, rad/s, positive looking up. */
53
+ lookPitchRate: number;
54
+ /** Horizontal ground speed, m/s (e.g. `EntityRenderCues.speed`). */
55
+ speed: number;
56
+ /** `0..1` walk-cycle phase (e.g. `EntityRenderCues.bobPhase`). */
57
+ bobPhase: number;
58
+ handling: WeaponHandlingFrame;
59
+ }
60
+ /**
61
+ * Where the viewmodel and camera go this frame. First person applies all of it; third person and
62
+ * over-the-shoulder read `lookPitch`/`lookYaw`, `aimPitch`/`aimYaw` and `adsProgress` from the same frame.
63
+ */
64
+ export interface WeaponPose {
65
+ /** Viewmodel anchor, camera-space metres. */
66
+ offset: [number, number, number];
67
+ /** Viewmodel rotation about its anchor, rad: `pitch > 0` muzzle up, `yaw > 0` muzzle left, `roll > 0` tilts left. */
68
+ pitch: number;
69
+ yaw: number;
70
+ roll: number;
71
+ /** Viewmodel FOV in degrees, or `null` to use the world FOV. */
72
+ viewmodelFov: number | null;
73
+ /** Multiplier for the world FOV (ADS zoom). */
74
+ fovScale: number;
75
+ /** Offset to add to the camera look, rad: aim kick plus view punch. `lookYaw > 0` pulls right. */
76
+ lookPitch: number;
77
+ lookYaw: number;
78
+ /** Offset shots follow, rad; a third-person rig or upper-body aim reads this. */
79
+ aimPitch: number;
80
+ aimYaw: number;
81
+ /** `0` hip … `1` fully aimed. */
82
+ adsProgress: number;
83
+ }
84
+ /** Serializable presentation state: the current sway lag. */
85
+ export interface WeaponPresentationState {
86
+ swayPitch: number;
87
+ swayYaw: number;
88
+ }
89
+ /** Turns a {@link WeaponHandlingFrame} plus look and movement into a viewmodel and camera pose. */
90
+ export interface WeaponPresentation {
91
+ /** Advance sway by `dt` and return the pose. The returned object is reused between calls. */
92
+ update(dt: number, input: WeaponPresentationInput): WeaponPose;
93
+ pose(): WeaponPose;
94
+ retune(next: WeaponPresentationTuning): void;
95
+ snapshot(): WeaponPresentationState;
96
+ restore(state: WeaponPresentationState): void;
97
+ reset(): void;
98
+ }
99
+ /** Default hip anchor, matching the shell's built-in viewmodel. */
100
+ export declare const DEFAULT_VIEWMODEL_HIP: ViewmodelOffset;
101
+ /** Default ADS anchor: the built-in viewmodel's top rail on the camera axis. */
102
+ export declare const DEFAULT_VIEWMODEL_ADS: ViewmodelOffset;
103
+ /**
104
+ * Creates a {@link WeaponPresentation}: per-weapon viewmodel offset, ADS pose and viewmodel FOV, sway
105
+ * and inertia from look speed, bob from the movement probe, and a recoil kick, all driven by the same
106
+ * handling frame third-person and over-the-shoulder rigs read. Deterministic and allocation-free per update.
107
+ * @capability weapon-presentation per-weapon viewmodel offset, ADS pose, viewmodel FOV, sway, bob and recoil kick from a weapon handling frame
108
+ */
109
+ export declare function createWeaponPresentation(initial?: WeaponPresentationTuning): WeaponPresentation;
110
+ /**
111
+ * Scale for a viewmodel drawn at `viewmodelFov` through a camera at `worldFov` (both degrees): multiply
112
+ * the viewmodel's camera-space x and y by this and it projects as if rendered with its own FOV, without
113
+ * a second camera or render pass.
114
+ */
115
+ export declare function viewmodelFovScale(worldFov: number, viewmodelFov: number): number;
@@ -0,0 +1,96 @@
1
+ /** Default hip anchor, matching the shell's built-in viewmodel. */
2
+ export const DEFAULT_VIEWMODEL_HIP = [0.34, -0.26, -0.72];
3
+ /** Default ADS anchor: the built-in viewmodel's top rail on the camera axis. */
4
+ export const DEFAULT_VIEWMODEL_ADS = [0, -0.07, -0.55];
5
+ const TAU = Math.PI * 2;
6
+ const DEFAULT_BOB = [0.012, 0.01];
7
+ function lerp(hip, aimed, ads) {
8
+ return hip * (1 - ads) + aimed * ads;
9
+ }
10
+ function clamp(value, limit) {
11
+ return Math.max(-limit, Math.min(limit, value));
12
+ }
13
+ /**
14
+ * Creates a {@link WeaponPresentation}: per-weapon viewmodel offset, ADS pose and viewmodel FOV, sway
15
+ * and inertia from look speed, bob from the movement probe, and a recoil kick, all driven by the same
16
+ * handling frame third-person and over-the-shoulder rigs read. Deterministic and allocation-free per update.
17
+ * @capability weapon-presentation per-weapon viewmodel offset, ADS pose, viewmodel FOV, sway, bob and recoil kick from a weapon handling frame
18
+ */
19
+ export function createWeaponPresentation(initial = {}) {
20
+ let tuning = initial;
21
+ const state = { swayPitch: 0, swayYaw: 0 };
22
+ const out = {
23
+ offset: [...(tuning.hip ?? DEFAULT_VIEWMODEL_HIP)],
24
+ pitch: 0,
25
+ yaw: 0,
26
+ roll: 0,
27
+ viewmodelFov: tuning.viewmodelFov?.hip ?? null,
28
+ fovScale: 1,
29
+ lookPitch: 0,
30
+ lookYaw: 0,
31
+ aimPitch: 0,
32
+ aimYaw: 0,
33
+ adsProgress: 0,
34
+ };
35
+ return {
36
+ update(dt, input) {
37
+ const step = Math.max(0, dt);
38
+ const handling = input.handling;
39
+ const ads = Math.max(0, Math.min(1, handling.adsProgress));
40
+ const sway = tuning.sway;
41
+ const swayMax = sway?.max ?? 0.08;
42
+ const swayScale = lerp(1, sway?.adsScale ?? 0.3, ads);
43
+ const per = sway?.perRadPerSec ?? 0.02;
44
+ const targetYaw = clamp(-input.lookYawRate * per, swayMax) * swayScale;
45
+ const targetPitch = clamp(-input.lookPitchRate * per, swayMax) * swayScale;
46
+ const follow = 1 - Math.exp(-(sway?.response ?? 12) * step);
47
+ state.swayYaw += (targetYaw - state.swayYaw) * follow;
48
+ state.swayPitch += (targetPitch - state.swayPitch) * follow;
49
+ const bob = tuning.bob;
50
+ const [bobX, bobY] = bob?.amplitude ?? DEFAULT_BOB;
51
+ const bobScale = Math.min(1, Math.max(0, input.speed) / Math.max(1e-6, bob?.fullSpeed ?? 4)) * lerp(1, bob?.adsScale ?? 0.25, ads);
52
+ const phase = input.bobPhase * TAU;
53
+ const recoil = Math.hypot(handling.aimPitch + handling.cameraPitch, handling.aimYaw + handling.cameraYaw);
54
+ const hip = tuning.hip ?? DEFAULT_VIEWMODEL_HIP;
55
+ const aimed = tuning.ads ?? DEFAULT_VIEWMODEL_ADS;
56
+ out.offset[0] = lerp(hip[0], aimed[0], ads) + Math.sin(phase) * bobX * bobScale;
57
+ out.offset[1] = lerp(hip[1], aimed[1], ads) - Math.abs(Math.sin(phase)) * bobY * bobScale;
58
+ out.offset[2] = lerp(hip[2], aimed[2], ads) + recoil * (tuning.kick?.back ?? 1.2);
59
+ out.pitch = state.swayPitch + recoil * (tuning.kick?.rise ?? 1.5);
60
+ out.yaw = state.swayYaw;
61
+ out.roll = state.swayYaw * 0.5;
62
+ const hipFov = tuning.viewmodelFov?.hip ?? tuning.viewmodelFov?.ads;
63
+ const adsFov = tuning.viewmodelFov?.ads ?? hipFov;
64
+ out.viewmodelFov = hipFov === undefined || adsFov === undefined ? null : lerp(hipFov, adsFov, ads);
65
+ out.fovScale = lerp(1, tuning.adsZoom ?? 1, ads);
66
+ out.lookPitch = handling.aimPitch + handling.cameraPitch;
67
+ out.lookYaw = handling.aimYaw + handling.cameraYaw;
68
+ out.aimPitch = handling.aimPitch;
69
+ out.aimYaw = handling.aimYaw;
70
+ out.adsProgress = ads;
71
+ return out;
72
+ },
73
+ pose: () => out,
74
+ retune(next) {
75
+ tuning = next;
76
+ },
77
+ snapshot: () => ({ ...state }),
78
+ restore(next) {
79
+ state.swayPitch = next.swayPitch;
80
+ state.swayYaw = next.swayYaw;
81
+ },
82
+ reset() {
83
+ state.swayPitch = 0;
84
+ state.swayYaw = 0;
85
+ },
86
+ };
87
+ }
88
+ /**
89
+ * Scale for a viewmodel drawn at `viewmodelFov` through a camera at `worldFov` (both degrees): multiply
90
+ * the viewmodel's camera-space x and y by this and it projects as if rendered with its own FOV, without
91
+ * a second camera or render pass.
92
+ */
93
+ export function viewmodelFovScale(worldFov, viewmodelFov) {
94
+ const toHalf = (deg) => Math.tan((Math.max(1, Math.min(179, deg)) * Math.PI) / 360);
95
+ return toHalf(worldFov) / toHalf(viewmodelFov);
96
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Logs a dev-mode `console.warn` once per `site`, and never in a production build. Reach for it
3
+ * where a permissive config would otherwise fail silently — a clipped sky dome, an ignored tint.
4
+ * @capability warn-once emit a dev-only warning a single time per call site
5
+ */
6
+ export declare function warnOnce(site: string, message: string): void;
7
+ /** Clears the once-per-site record so a test can observe the warning again. @internal */
8
+ export declare function resetWarnOnce(): void;
@@ -0,0 +1,18 @@
1
+ import { isProductionEnvironment } from "./saveEndpoint.js";
2
+ const warned = new Set();
3
+ /**
4
+ * Logs a dev-mode `console.warn` once per `site`, and never in a production build. Reach for it
5
+ * where a permissive config would otherwise fail silently — a clipped sky dome, an ignored tint.
6
+ * @capability warn-once emit a dev-only warning a single time per call site
7
+ */
8
+ export function warnOnce(site, message) {
9
+ if (warned.has(site) || isProductionEnvironment())
10
+ return;
11
+ warned.add(site);
12
+ const host = globalThis;
13
+ host.console?.warn?.(message);
14
+ }
15
+ /** Clears the once-per-site record so a test can observe the warning again. @internal */
16
+ export function resetWarnOnce() {
17
+ warned.clear();
18
+ }
@@ -1,6 +1,8 @@
1
1
  export interface CurrencyDefinition<TCurrencyId extends string = string> {
2
2
  id: TCurrencyId;
3
3
  name: string;
4
+ /** Fractional digits in major-unit amounts; defaults to 0. */
5
+ decimals?: number;
4
6
  /** Prepended when formatting amounts (e.g. "$"). */
5
7
  symbol?: string;
6
8
  /** Appended when formatting amounts (e.g. "tokens"). */
@@ -16,7 +18,19 @@ export type CurrencyAdjustment = {
16
18
  reason: string;
17
19
  };
18
20
  /** @internal */
19
- export declare function sanitizeCurrencyAmount(amount: number): number;
21
+ export declare function sanitizeCurrencyAmount(amount: number, currency?: CurrencyDefinition): number;
22
+ /** Convert major units to integer minor units, rounding once at the write boundary.
23
+ * @capability currency-minor-units round decimal currency into safe integer minor units
24
+ */
25
+ export declare function toMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number;
26
+ /** Convert stored integer minor units to major units for display or existing balance records.
27
+ * @capability currency-major-units convert safe minor-unit integers into currency amounts
28
+ */
29
+ export declare function fromMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number;
30
+ /** Format a major-unit value using the currency's declared precision.
31
+ * @capability currency-precision-format display a currency using its declared decimal precision
32
+ */
33
+ export declare function formatCurrency(currency: CurrencyDefinition, value: number): string;
20
34
  /**
21
35
  * Format a currency amount with its symbol and grouping for HUD display.
22
36
  *
@@ -31,7 +45,7 @@ export declare function applyCurrencyOperation(currency: CurrencyDefinition, cur
31
45
  /** Deducts clamp to the available balance (a delta can never overdraw); adds apply in full.
32
46
  * @internal
33
47
  */
34
- export declare function resolveCurrencyDelta(current: number, delta: number): {
48
+ export declare function resolveCurrencyDelta(current: number, delta: number, currency?: CurrencyDefinition): {
35
49
  operation: CurrencyOperation;
36
50
  amount: number;
37
51
  };
@@ -1,6 +1,43 @@
1
1
  /** @internal */
2
- export function sanitizeCurrencyAmount(amount) {
3
- return Math.max(0, Math.floor(amount));
2
+ export function sanitizeCurrencyAmount(amount, currency) {
3
+ if (!Number.isFinite(amount))
4
+ throw new RangeError("Currency amount must be finite");
5
+ return fromMinorUnits(currency, toMinorUnits(currency, Math.max(0, amount)));
6
+ }
7
+ function currencyScale(currency) {
8
+ const decimals = currency?.decimals ?? 0;
9
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 9) {
10
+ throw new RangeError("Currency decimals must be an integer between 0 and 9");
11
+ }
12
+ return 10 ** decimals;
13
+ }
14
+ /** Convert major units to integer minor units, rounding once at the write boundary.
15
+ * @capability currency-minor-units round decimal currency into safe integer minor units
16
+ */
17
+ export function toMinorUnits(currency, value) {
18
+ if (!Number.isFinite(value))
19
+ throw new RangeError("Currency amount must be finite");
20
+ const scaled = value * currencyScale(currency);
21
+ const result = Math.sign(scaled) * Math.round(Math.abs(scaled) + Math.min(1e-7, Number.EPSILON * Math.abs(scaled)));
22
+ if (!Number.isSafeInteger(result))
23
+ throw new RangeError("Currency amount exceeds safe integer precision");
24
+ return result;
25
+ }
26
+ /** Convert stored integer minor units to major units for display or existing balance records.
27
+ * @capability currency-major-units convert safe minor-unit integers into currency amounts
28
+ */
29
+ export function fromMinorUnits(currency, value) {
30
+ if (!Number.isSafeInteger(value))
31
+ throw new RangeError("Minor units must be a safe integer");
32
+ return value / currencyScale(currency);
33
+ }
34
+ /** Format a major-unit value using the currency's declared precision.
35
+ * @capability currency-precision-format display a currency using its declared decimal precision
36
+ */
37
+ export function formatCurrency(currency, value) {
38
+ const decimals = currency.decimals ?? 0;
39
+ const rounded = fromMinorUnits(currency, toMinorUnits(currency, value));
40
+ return `${currency.symbol ?? ""}${rounded.toFixed(decimals)}${currency.unit ? ` ${currency.unit}` : ""}`;
4
41
  }
5
42
  /**
6
43
  * Format a currency amount with its symbol and grouping for HUD display.
@@ -9,9 +46,7 @@ export function sanitizeCurrencyAmount(amount) {
9
46
  * @internal
10
47
  */
11
48
  export function formatCurrencyAmount(currency, amount) {
12
- const prefix = currency.symbol ?? "";
13
- const suffix = currency.unit ? ` ${currency.unit}` : "";
14
- return `${prefix}${amount}${suffix}`;
49
+ return formatCurrency(currency, amount);
15
50
  }
16
51
  /** @internal */
17
52
  export function insufficientCurrencyReason(currency, needed, current) {
@@ -19,20 +54,26 @@ export function insufficientCurrencyReason(currency, needed, current) {
19
54
  }
20
55
  /** @internal */
21
56
  export function applyCurrencyOperation(currency, current, operation, amount) {
22
- const safeAmount = sanitizeCurrencyAmount(amount);
23
- if (safeAmount === 0)
24
- return { success: true, newBalance: current, appliedDelta: 0 };
25
- if (operation === "deduct" && current < safeAmount) {
26
- return { success: false, reason: insufficientCurrencyReason(currency, safeAmount, current) };
57
+ if (!Number.isFinite(amount) || amount < 0 || !Number.isFinite(current)) {
58
+ return { success: false, reason: "Invalid currency amount" };
59
+ }
60
+ const currentMinor = toMinorUnits(currency, current);
61
+ const amountMinor = toMinorUnits(currency, amount);
62
+ if (operation === "deduct" && currentMinor < amountMinor) {
63
+ return { success: false, reason: insufficientCurrencyReason(currency, fromMinorUnits(currency, amountMinor), current) };
27
64
  }
28
- const appliedDelta = operation === "add" ? safeAmount : -safeAmount;
29
- return { success: true, newBalance: current + appliedDelta, appliedDelta };
65
+ const deltaMinor = operation === "add" ? amountMinor : -amountMinor;
66
+ return {
67
+ success: true,
68
+ newBalance: fromMinorUnits(currency, currentMinor + deltaMinor),
69
+ appliedDelta: fromMinorUnits(currency, deltaMinor),
70
+ };
30
71
  }
31
72
  /** Deducts clamp to the available balance (a delta can never overdraw); adds apply in full.
32
73
  * @internal
33
74
  */
34
- export function resolveCurrencyDelta(current, delta) {
35
- const safeDelta = Math.floor(delta);
75
+ export function resolveCurrencyDelta(current, delta, currency) {
76
+ const safeDelta = fromMinorUnits(currency, toMinorUnits(currency, delta));
36
77
  if (safeDelta > 0)
37
78
  return { operation: "add", amount: safeDelta };
38
79
  return { operation: "deduct", amount: Math.min(Math.abs(safeDelta), current) };
@@ -1,3 +1,4 @@
1
+ import { type CurrencyDefinition } from "./currency.js";
1
2
  export interface WalletState {
2
3
  balances: Readonly<Record<string, number>>;
3
4
  }
@@ -31,16 +32,16 @@ export interface ChargeOptions {
31
32
  * @capability wallet hold currency balances with charge and affordability checks
32
33
  */
33
34
  export declare function createEmptyWallet(): WalletState;
34
- export declare function balance(state: WalletState, currency: string): number;
35
- export declare function grant(state: WalletState, currency: string, amount: number): WalletState;
35
+ export declare function balance(state: WalletState, currency: string | CurrencyDefinition): number;
36
+ export declare function grant(state: WalletState, currency: string | CurrencyDefinition, amount: number): WalletState;
36
37
  /**
37
38
  * Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft`
38
39
  * opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability
39
40
  * check stays the default with `options` omitted.
40
41
  */
41
- export declare function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult;
42
+ export declare function charge(state: WalletState, currency: string | CurrencyDefinition, amount: number, options?: ChargeOptions): ChargeResult;
42
43
  /** True once `balance(state, currency)` has gone negative under an overdraft-enabled charge. */
43
- export declare function isOverdrawn(state: WalletState, currency: string): boolean;
44
+ export declare function isOverdrawn(state: WalletState, currency: string | CurrencyDefinition): boolean;
44
45
  /** True when every currency in `costs` has at least that much balance (a pure, non-mutating check). */
45
46
  export declare function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean;
46
47
  export declare function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>, options?: ChargeOptions): ChargeResult;
@@ -1,3 +1,4 @@
1
+ import { fromMinorUnits, toMinorUnits } from "./currency.js";
1
2
  /**
2
3
  * Hold per-currency balances with affordability checks and charge/grant operations.
3
4
  *
@@ -12,14 +13,16 @@ function assertValidAmount(amount) {
12
13
  }
13
14
  }
14
15
  export function balance(state, currency) {
15
- return state.balances[currency] ?? 0;
16
+ return state.balances[typeof currency === "string" ? currency : currency.id] ?? 0;
16
17
  }
17
18
  export function grant(state, currency, amount) {
18
19
  assertValidAmount(amount);
19
20
  return {
20
21
  balances: {
21
22
  ...state.balances,
22
- [currency]: balance(state, currency) + amount,
23
+ [typeof currency === "string" ? currency : currency.id]: typeof currency === "string"
24
+ ? balance(state, currency) + amount
25
+ : fromMinorUnits(currency, toMinorUnits(currency, balance(state, currency)) + toMinorUnits(currency, amount)),
23
26
  },
24
27
  };
25
28
  }
@@ -40,7 +43,8 @@ function withinOverdraft(nextBalance, overdraft) {
40
43
  export function charge(state, currency, amount, options) {
41
44
  assertValidAmount(amount);
42
45
  const current = balance(state, currency);
43
- const next = current - amount;
46
+ const next = typeof currency === "string" ? current - amount
47
+ : fromMinorUnits(currency, toMinorUnits(currency, current) - toMinorUnits(currency, amount));
44
48
  if (!withinOverdraft(next, options?.overdraft)) {
45
49
  return { status: "rejected", reason: "insufficient-funds" };
46
50
  }
@@ -49,7 +53,7 @@ export function charge(state, currency, amount, options) {
49
53
  state: {
50
54
  balances: {
51
55
  ...state.balances,
52
- [currency]: next,
56
+ [typeof currency === "string" ? currency : currency.id]: next,
53
57
  },
54
58
  },
55
59
  };
@@ -447,6 +447,7 @@ const mutationHandlers = {
447
447
  }),
448
448
  setTerrain: (state, command) => ({ ...state, document: { ...state.document, terrain: command.terrain } }),
449
449
  setMinimapBake: (state, command) => ({ ...state, document: { ...state.document, minimap: command.minimap } }),
450
+ setBake: (state, command) => ({ ...state, document: { ...state.document, bakes: [...(state.document.bakes ?? []).filter((bake) => bake.id !== command.bake.id), command.bake] } }),
450
451
  setEnvironment: (state, command) => {
451
452
  if (command.environment === undefined) {
452
453
  const { environment: _removed, ...rest } = state.document;
@@ -2,7 +2,7 @@ import { type SurfaceDelta, type TerraformDelta, type TerrainMaterialLayer, type
2
2
  import type { EditorUiDocument, EditorUiPanelLayout } from "../ui/hudDocument.js";
3
3
  import type { EditorGridCellEdit, EditorGridLayer } from "./grid.js";
4
4
  import type { ParamSchema } from "../scene/sceneKinds.js";
5
- import type { EditorCatalogEntry, EditorDocument, EditorEnvironment, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorTerrain, EditorVec3, EditorVolume } from "./types.js";
5
+ import type { EditorCatalogEntry, EditorBake, EditorDocument, EditorEnvironment, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorTerrain, EditorVec3, EditorVolume } from "./types.js";
6
6
  /** A single editor mutation — select, move, add, remove, undo/redo — dispatched to a session. */
7
7
  export type EditorCommand = {
8
8
  type: "select";
@@ -122,6 +122,9 @@ export type EditorCommand = {
122
122
  } | {
123
123
  type: "setMinimapBake";
124
124
  minimap: EditorMinimapBake;
125
+ } | {
126
+ type: "setBake";
127
+ bake: EditorBake;
125
128
  }
126
129
  /**
127
130
  * Replace the scene-document environment/lighting bag. Pass `undefined` to clear authored sky/fog
@@ -1,5 +1,5 @@
1
1
  import { type EditorGridLayer } from "./grid.js";
2
- import type { EditorCatalogData, EditorCatalogDefinition, EditorCatalogEntry, EditorCollection, EditorDirective, EditorDocument, EditorEnvironment, EditorFragmentContent, EditorLayersInput, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorPrefab, EditorTerrain, EditorVolume } from "./types.js";
2
+ import type { EditorCatalogData, EditorBake, EditorCatalogDefinition, EditorCatalogEntry, EditorCollection, EditorDirective, EditorDocument, EditorEnvironment, EditorFragmentContent, EditorLayersInput, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorPrefab, EditorTerrain, EditorVolume } from "./types.js";
3
3
  /** Spread-ready non-placeable fields preserved across document rebuilds. @internal */
4
4
  export declare function editorDocumentExtras(doc: EditorDocument): {
5
5
  prefabs: EditorPrefab[];
@@ -10,6 +10,7 @@ export declare function editorDocumentExtras(doc: EditorDocument): {
10
10
  ui?: EditorDocument["ui"];
11
11
  directives?: EditorDirective[];
12
12
  minimap?: EditorMinimapBake;
13
+ bakes?: EditorBake[];
13
14
  environment?: EditorEnvironment;
14
15
  };
15
16
  /** Builds a fresh, empty editor document to start authoring a scene from scratch.
@@ -5,18 +5,28 @@ import { cloneGridLayer, migrateGridLayer, } from "./grid.js";
5
5
  function cloneMinimapBake(minimap) {
6
6
  return { background: minimap.background, bounds: { ...minimap.bounds } };
7
7
  }
8
+ function cloneBakes(bakes) {
9
+ return bakes === undefined ? undefined : bakes.map((bake) => ({ ...bake, data: structuredClone(bake.data) }));
10
+ }
8
11
  /** Deep-copies scene-document environment/lighting so edits never mutate history snapshots. */
9
12
  function cloneEnvironment(environment) {
10
13
  return {
14
+ ...(environment.source === undefined ? {} : { source: cloneEnvironmentSource(environment.source) }),
15
+ ...(environment.pointLights === undefined ? {} : { pointLights: environment.pointLights.map((light) => ({ ...light, position: [...light.position] })) }),
11
16
  ...(environment.preset === undefined ? {} : { preset: environment.preset }),
12
17
  ...(environment.timeOfDay === undefined ? {} : { timeOfDay: environment.timeOfDay }),
13
18
  ...(environment.horizonColor === undefined ? {} : { horizonColor: environment.horizonColor }),
14
19
  ...(environment.zenithColor === undefined ? {} : { zenithColor: environment.zenithColor }),
15
20
  ...(environment.sunIntensity === undefined ? {} : { sunIntensity: environment.sunIntensity }),
16
21
  ...(environment.ambientIntensity === undefined ? {} : { ambientIntensity: environment.ambientIntensity }),
22
+ ...(environment.sunAzimuth === undefined ? {} : { sunAzimuth: environment.sunAzimuth }),
23
+ ...(environment.sunElevation === undefined ? {} : { sunElevation: environment.sunElevation }),
17
24
  ...(environment.fog === undefined ? {} : { fog: { ...environment.fog } }),
18
25
  };
19
26
  }
27
+ function cloneEnvironmentSource(source) {
28
+ return source.kind === "cube" ? { ...source, urls: [...source.urls] } : { ...source };
29
+ }
20
30
  function cloneDirective(directive) {
21
31
  const area = directive.area === undefined
22
32
  ? undefined
@@ -66,6 +76,7 @@ export function editorDocumentExtras(doc) {
66
76
  ...(doc.ui === undefined ? {} : { ui: doc.ui }),
67
77
  ...(doc.directives === undefined ? {} : { directives: doc.directives }),
68
78
  ...(doc.minimap === undefined ? {} : { minimap: doc.minimap }),
79
+ ...(doc.bakes === undefined ? {} : { bakes: cloneBakes(doc.bakes) }),
69
80
  ...(doc.environment === undefined ? {} : { environment: doc.environment }),
70
81
  };
71
82
  }
@@ -147,6 +158,7 @@ export function cloneEditorDocument(doc) {
147
158
  ...(ui === undefined ? {} : { ui }),
148
159
  ...(directives === undefined ? {} : { directives }),
149
160
  ...(doc.minimap === undefined ? {} : { minimap: cloneMinimapBake(doc.minimap) }),
161
+ ...(doc.bakes === undefined ? {} : { bakes: cloneBakes(doc.bakes) }),
150
162
  ...(doc.environment === undefined ? {} : { environment: cloneEnvironment(doc.environment) }),
151
163
  };
152
164
  }
@@ -204,6 +216,7 @@ export function normalizeEditorLayers(input) {
204
216
  ...(ui === undefined ? {} : { ui }),
205
217
  ...(directives === undefined ? {} : { directives }),
206
218
  ...(resolved.minimap === undefined ? {} : { minimap: cloneMinimapBake(resolved.minimap) }),
219
+ ...(resolved.bakes === undefined ? {} : { bakes: cloneBakes(resolved.bakes) }),
207
220
  ...(resolved.environment === undefined ? {} : { environment: cloneEnvironment(resolved.environment) }),
208
221
  };
209
222
  }
@@ -949,6 +962,21 @@ function decodeMinimapBake(value, path, errors) {
949
962
  bounds: { minX: b.minX, minZ: b.minZ, maxX: b.maxX, maxZ: b.maxZ },
950
963
  };
951
964
  }
965
+ function decodeBakes(value, path, errors) {
966
+ if (!Array.isArray(value)) {
967
+ errors.push({ path, message: "expected an array" });
968
+ return undefined;
969
+ }
970
+ const bakes = [];
971
+ value.forEach((item, index) => {
972
+ if (!isPlainObject(item) || (item.kind !== "nav" && item.kind !== "minimap") || typeof item.id !== "string" || item.data === undefined) {
973
+ errors.push({ path: `${path}[${index}]`, message: 'expected {kind: "nav" | "minimap", id, data}' });
974
+ return;
975
+ }
976
+ bakes.push({ kind: item.kind, id: item.id, data: structuredClone(item.data) });
977
+ });
978
+ return bakes;
979
+ }
952
980
  const SKY_PRESETS = new Set(["day", "dusk", "night"]);
953
981
  /**
954
982
  * Validates scene-document environment/lighting. Unknown presets, non-finite intensities, and
@@ -962,6 +990,67 @@ function decodeEnvironment(value, path, errors) {
962
990
  }
963
991
  let failed = false;
964
992
  const env = {};
993
+ if (value.source !== undefined) {
994
+ const source = value.source;
995
+ if (!isPlainObject(source) || typeof source.kind !== "string") {
996
+ errors.push({ path: `${path}.source`, message: "expected an environment source object" });
997
+ failed = true;
998
+ }
999
+ else if (source.kind === "gradient") {
1000
+ if (typeof source.sky !== "string") {
1001
+ errors.push({ path: `${path}.source.sky`, message: "expected a string" });
1002
+ failed = true;
1003
+ }
1004
+ if (typeof source.ground !== "string") {
1005
+ errors.push({ path: `${path}.source.ground`, message: "expected a string" });
1006
+ failed = true;
1007
+ }
1008
+ if (source.sun !== undefined && typeof source.sun !== "string") {
1009
+ errors.push({ path: `${path}.source.sun`, message: "expected a string" });
1010
+ failed = true;
1011
+ }
1012
+ if (source.intensity !== undefined && (typeof source.intensity !== "number" || !Number.isFinite(source.intensity))) {
1013
+ errors.push({ path: `${path}.source.intensity`, message: "expected a finite number" });
1014
+ failed = true;
1015
+ }
1016
+ if (!failed)
1017
+ env.source = { kind: "gradient", sky: source.sky, ground: source.ground, ...(source.sun === undefined ? {} : { sun: source.sun }), ...(source.intensity === undefined ? {} : { intensity: source.intensity }) };
1018
+ }
1019
+ else if (source.kind === "hdri" && typeof source.url === "string") {
1020
+ env.source = { kind: "hdri", url: source.url, ...(typeof source.rotation === "number" ? { rotation: source.rotation } : {}), ...(typeof source.intensity === "number" ? { intensity: source.intensity } : {}) };
1021
+ }
1022
+ else if (source.kind === "cube" && Array.isArray(source.urls) && source.urls.length === 6 && source.urls.every((url) => typeof url === "string")) {
1023
+ env.source = { kind: "cube", urls: [...source.urls], ...(typeof source.intensity === "number" ? { intensity: source.intensity } : {}) };
1024
+ }
1025
+ else {
1026
+ errors.push({ path: `${path}.source`, message: 'expected kind "gradient", "hdri", or "cube" with valid fields' });
1027
+ failed = true;
1028
+ }
1029
+ }
1030
+ if (value.pointLights !== undefined) {
1031
+ if (!Array.isArray(value.pointLights)) {
1032
+ errors.push({ path: `${path}.pointLights`, message: "expected an array" });
1033
+ failed = true;
1034
+ }
1035
+ else if (value.pointLights.length > 64 || value.pointLights.some((light) => {
1036
+ if (!isPlainObject(light) || !Array.isArray(light.position) || light.position.length !== 3 || light.position.some((n) => typeof n !== "number" || !Number.isFinite(n)))
1037
+ return true;
1038
+ return light.intensity !== undefined && (typeof light.intensity !== "number" || !Number.isFinite(light.intensity));
1039
+ })) {
1040
+ errors.push({ path: `${path}.pointLights`, message: "expected point lights with finite positions and intensities" });
1041
+ failed = true;
1042
+ }
1043
+ else {
1044
+ env.pointLights = value.pointLights.map((light) => ({
1045
+ position: [...light.position],
1046
+ ...(typeof light.color === "string" ? { color: light.color } : {}),
1047
+ ...(typeof light.intensity === "number" ? { intensity: light.intensity } : {}),
1048
+ ...(typeof light.distance === "number" ? { distance: light.distance } : {}),
1049
+ ...(typeof light.decay === "number" ? { decay: light.decay } : {}),
1050
+ ...(typeof light.castShadow === "boolean" ? { castShadow: light.castShadow } : {}),
1051
+ }));
1052
+ }
1053
+ }
965
1054
  if (value.preset !== undefined) {
966
1055
  if (typeof value.preset === "string" && SKY_PRESETS.has(value.preset)) {
967
1056
  env.preset = value.preset;
@@ -995,7 +1084,7 @@ function decodeEnvironment(value, path, errors) {
995
1084
  failed = true;
996
1085
  }
997
1086
  }
998
- for (const key of ["sunIntensity", "ambientIntensity"]) {
1087
+ for (const key of ["sunIntensity", "ambientIntensity", "sunAzimuth", "sunElevation"]) {
999
1088
  if (value[key] === undefined)
1000
1089
  continue;
1001
1090
  if (typeof value[key] === "number" && Number.isFinite(value[key]))
@@ -1152,6 +1241,7 @@ export function decodeEditorDocument(raw) {
1152
1241
  errors.push({ path: "$.terrain", message: "expected an object" });
1153
1242
  }
1154
1243
  const minimap = raw.minimap === undefined ? undefined : decodeMinimapBake(raw.minimap, "$.minimap", errors);
1244
+ const bakes = raw.bakes === undefined ? undefined : decodeBakes(raw.bakes, "$.bakes", errors);
1155
1245
  const environment = raw.environment === undefined ? undefined : decodeEnvironment(raw.environment, "$.environment", errors);
1156
1246
  // Placeable object ids form one document-global namespace (selection, parenting, and removal all
1157
1247
  // treat them that way), so a document that reuses an id — even across two different collections —
@@ -1194,6 +1284,7 @@ export function decodeEditorDocument(raw) {
1194
1284
  ...(ui === undefined ? {} : { ui }),
1195
1285
  ...(directives === undefined ? {} : { directives }),
1196
1286
  ...(minimap === undefined ? {} : { minimap }),
1287
+ ...(bakes === undefined && minimap === undefined ? {} : { bakes: bakes ?? [{ kind: "minimap", id: "minimap", data: minimap }] }),
1197
1288
  ...(environment === undefined ? {} : { environment }),
1198
1289
  },
1199
1290
  };