@jgengine/core 0.10.0 → 0.11.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 (607) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/ai/crowd.d.ts +55 -0
  5. package/dist/ai/crowd.js +114 -0
  6. package/dist/ai/flock.d.ts +5 -2
  7. package/dist/ai/flock.js +5 -2
  8. package/dist/ai/groupAssist.d.ts +1 -0
  9. package/dist/ai/groupAssist.js +1 -0
  10. package/dist/ai/heatSystem.d.ts +76 -0
  11. package/dist/ai/heatSystem.js +97 -0
  12. package/dist/ai/interestScheduler.d.ts +107 -0
  13. package/dist/ai/interestScheduler.js +118 -0
  14. package/dist/ai/jobBoard.d.ts +1 -0
  15. package/dist/ai/jobBoard.js +1 -0
  16. package/dist/ai/laneSelect.d.ts +5 -2
  17. package/dist/ai/laneSelect.js +5 -2
  18. package/dist/ai/mobBrain.d.ts +1 -0
  19. package/dist/ai/mobBrain.js +1 -0
  20. package/dist/ai/spawnDirector.d.ts +7 -2
  21. package/dist/ai/spawnDirector.js +18 -15
  22. package/dist/ai/spawnPoint.d.ts +3 -1
  23. package/dist/ai/spawnPoint.js +3 -1
  24. package/dist/ai/targetAcquisition.d.ts +84 -0
  25. package/dist/ai/targetAcquisition.js +92 -0
  26. package/dist/anim/easing.d.ts +19 -0
  27. package/dist/anim/easing.js +19 -0
  28. package/dist/anim/oscillator.d.ts +12 -0
  29. package/dist/anim/oscillator.js +25 -0
  30. package/dist/area/areaEffectField.d.ts +127 -0
  31. package/dist/area/areaEffectField.js +175 -0
  32. package/dist/area/stackPolicy.d.ts +47 -0
  33. package/dist/area/stackPolicy.js +99 -0
  34. package/dist/authoring.d.ts +1 -0
  35. package/dist/authoring.js +1 -0
  36. package/dist/board/laneBoard.d.ts +13 -0
  37. package/dist/board/laneBoard.js +13 -0
  38. package/dist/board/timelineBoard.d.ts +10 -0
  39. package/dist/board/timelineBoard.js +10 -0
  40. package/dist/combat/abilityKit.d.ts +18 -0
  41. package/dist/combat/abilityKit.js +24 -9
  42. package/dist/combat/animationState.d.ts +5 -0
  43. package/dist/combat/animationState.js +5 -0
  44. package/dist/combat/breakMeters.d.ts +10 -0
  45. package/dist/combat/breakMeters.js +10 -0
  46. package/dist/combat/castRunner.d.ts +5 -0
  47. package/dist/combat/castRunner.js +5 -0
  48. package/dist/combat/comboPoints.d.ts +5 -0
  49. package/dist/combat/comboPoints.js +5 -0
  50. package/dist/combat/comboString.d.ts +5 -0
  51. package/dist/combat/comboString.js +5 -0
  52. package/dist/combat/damageInterceptors.d.ts +192 -0
  53. package/dist/combat/damageInterceptors.js +251 -0
  54. package/dist/combat/damageMatchup.d.ts +68 -0
  55. package/dist/combat/damageMatchup.js +42 -0
  56. package/dist/combat/damageResolution.d.ts +63 -0
  57. package/dist/combat/damageResolution.js +50 -0
  58. package/dist/combat/death.d.ts +7 -1
  59. package/dist/combat/death.js +5 -0
  60. package/dist/combat/deathReason.d.ts +14 -0
  61. package/dist/combat/deathReason.js +1 -0
  62. package/dist/combat/defensiveWindow.d.ts +5 -0
  63. package/dist/combat/defensiveWindow.js +5 -0
  64. package/dist/combat/dotField.d.ts +5 -1
  65. package/dist/combat/dotField.js +5 -1
  66. package/dist/combat/downed.d.ts +5 -0
  67. package/dist/combat/downed.js +5 -0
  68. package/dist/combat/effects.d.ts +5 -0
  69. package/dist/combat/effects.js +5 -0
  70. package/dist/combat/encounterSequence.d.ts +258 -0
  71. package/dist/combat/encounterSequence.js +426 -0
  72. package/dist/combat/hitReaction.d.ts +57 -1
  73. package/dist/combat/hitReaction.js +45 -5
  74. package/dist/combat/magazine.d.ts +54 -0
  75. package/dist/combat/magazine.js +96 -0
  76. package/dist/combat/projectiles.d.ts +5 -0
  77. package/dist/combat/projectiles.js +32 -14
  78. package/dist/combat/receivedDamage.d.ts +117 -0
  79. package/dist/combat/receivedDamage.js +92 -0
  80. package/dist/combat/regenShield.d.ts +5 -1
  81. package/dist/combat/regenShield.js +5 -1
  82. package/dist/combat/renderCues.d.ts +51 -0
  83. package/dist/combat/renderCues.js +44 -0
  84. package/dist/combat/resourcePool.d.ts +5 -0
  85. package/dist/combat/resourcePool.js +5 -0
  86. package/dist/combat/rewardAllocation.d.ts +121 -0
  87. package/dist/combat/rewardAllocation.js +251 -0
  88. package/dist/combat/shotOrigin.d.ts +18 -1
  89. package/dist/combat/shotOrigin.js +39 -0
  90. package/dist/combat/statusApplication.d.ts +96 -0
  91. package/dist/combat/statusApplication.js +81 -0
  92. package/dist/combat/telegraph.d.ts +11 -2
  93. package/dist/combat/telegraph.js +11 -2
  94. package/dist/combat/thresholdCrossings.d.ts +85 -0
  95. package/dist/combat/thresholdCrossings.js +116 -0
  96. package/dist/combat.d.ts +34 -0
  97. package/dist/combat.js +34 -0
  98. package/dist/commands/commandRegistry.d.ts +11 -0
  99. package/dist/commands/commandRegistry.js +9 -2
  100. package/dist/crafting/crop.d.ts +18 -0
  101. package/dist/crafting/crop.js +18 -0
  102. package/dist/crafting/production.d.ts +5 -0
  103. package/dist/crafting/production.js +5 -0
  104. package/dist/devtools/rewriteTunables.d.ts +17 -0
  105. package/dist/devtools/rewriteTunables.js +276 -0
  106. package/dist/devtools/saveEndpoint.d.ts +46 -0
  107. package/dist/devtools/saveEndpoint.js +48 -0
  108. package/dist/devtools/transformTunables.d.ts +3 -0
  109. package/dist/devtools/transformTunables.js +3 -0
  110. package/dist/devtools/tunableSchema.d.ts +31 -0
  111. package/dist/devtools/tunableSchema.js +31 -0
  112. package/dist/economy/currency.d.ts +12 -1
  113. package/dist/economy/currency.js +12 -1
  114. package/dist/economy/listingBook.d.ts +91 -0
  115. package/dist/economy/listingBook.js +144 -0
  116. package/dist/economy/resourceLedger.d.ts +222 -0
  117. package/dist/economy/resourceLedger.js +353 -0
  118. package/dist/economy/sharedWallet.d.ts +14 -0
  119. package/dist/economy/sharedWallet.js +14 -0
  120. package/dist/economy/techTree.d.ts +12 -0
  121. package/dist/economy/techTree.js +12 -0
  122. package/dist/economy/wallet.d.ts +26 -2
  123. package/dist/economy/wallet.js +35 -6
  124. package/dist/editor/commands.d.ts +190 -7
  125. package/dist/editor/commands.js +727 -55
  126. package/dist/editor/document.d.ts +159 -13
  127. package/dist/editor/document.js +750 -19
  128. package/dist/editor/grid.d.ts +229 -0
  129. package/dist/editor/grid.js +366 -0
  130. package/dist/editor/gridAdapters.d.ts +80 -0
  131. package/dist/editor/gridAdapters.js +134 -0
  132. package/dist/editor/index.d.ts +73 -6
  133. package/dist/editor/index.js +6 -2
  134. package/dist/editor/liveSync.d.ts +127 -0
  135. package/dist/editor/liveSync.js +249 -0
  136. package/dist/editor/runtimeInspector.d.ts +88 -0
  137. package/dist/editor/runtimeInspector.js +239 -0
  138. package/dist/editor/types.d.ts +107 -1
  139. package/dist/faction/reputation.d.ts +5 -0
  140. package/dist/faction/reputation.js +5 -0
  141. package/dist/format/distance.d.ts +17 -0
  142. package/dist/format/distance.js +18 -0
  143. package/dist/format/duration.d.ts +27 -0
  144. package/dist/format/duration.js +45 -0
  145. package/dist/format/speed.d.ts +17 -0
  146. package/dist/format/speed.js +25 -0
  147. package/dist/game/connectedPlayers.d.ts +6 -4
  148. package/dist/game/connectedPlayers.js +2 -2
  149. package/dist/game/cosmetics.d.ts +9 -0
  150. package/dist/game/cosmetics.js +16 -0
  151. package/dist/game/defineGame.d.ts +100 -2
  152. package/dist/game/defineGame.js +39 -2
  153. package/dist/game/defineSystem.d.ts +88 -0
  154. package/dist/game/defineSystem.js +45 -0
  155. package/dist/game/dialogue.d.ts +33 -0
  156. package/dist/game/dialogue.js +21 -0
  157. package/dist/game/events.d.ts +35 -13
  158. package/dist/game/events.js +5 -0
  159. package/dist/game/feed.d.ts +33 -0
  160. package/dist/game/feed.js +35 -2
  161. package/dist/game/gamePhase.d.ts +11 -0
  162. package/dist/game/gamePhase.js +16 -0
  163. package/dist/game/leaderboard.d.ts +5 -0
  164. package/dist/game/leaderboard.js +5 -0
  165. package/dist/game/loadout.d.ts +5 -0
  166. package/dist/game/loadout.js +5 -0
  167. package/dist/game/lootFilter.d.ts +2 -0
  168. package/dist/game/lootFilter.js +2 -0
  169. package/dist/game/lootPipeline.d.ts +164 -0
  170. package/dist/game/lootPipeline.js +216 -0
  171. package/dist/game/lootTable.d.ts +19 -2
  172. package/dist/game/lootTable.js +15 -5
  173. package/dist/game/objectives.d.ts +43 -0
  174. package/dist/game/objectives.js +36 -0
  175. package/dist/game/paramLayers.d.ts +148 -0
  176. package/dist/game/paramLayers.js +185 -0
  177. package/dist/game/ping.d.ts +5 -0
  178. package/dist/game/ping.js +5 -0
  179. package/dist/game/playableGame.d.ts +92 -7
  180. package/dist/game/quest.d.ts +8 -0
  181. package/dist/game/quest.js +29 -0
  182. package/dist/game/race.d.ts +141 -0
  183. package/dist/game/race.js +169 -0
  184. package/dist/game/recordBook.d.ts +3 -1
  185. package/dist/game/recordBook.js +4 -1
  186. package/dist/game/ruleSelection.d.ts +75 -0
  187. package/dist/game/ruleSelection.js +127 -0
  188. package/dist/game/runDraft.d.ts +5 -0
  189. package/dist/game/runDraft.js +5 -0
  190. package/dist/game/saveStore.d.ts +111 -0
  191. package/dist/game/saveStore.js +312 -0
  192. package/dist/game/snapshotHistory.d.ts +67 -0
  193. package/dist/game/snapshotHistory.js +95 -0
  194. package/dist/game/social.d.ts +38 -1
  195. package/dist/game/social.js +71 -0
  196. package/dist/game/spawnPoints.d.ts +5 -0
  197. package/dist/game/spawnPoints.js +5 -0
  198. package/dist/game/systemRuntime.d.ts +50 -0
  199. package/dist/game/systemRuntime.js +157 -0
  200. package/dist/game/systemSchedule.d.ts +72 -0
  201. package/dist/game/systemSchedule.js +242 -0
  202. package/dist/game/toasts.d.ts +43 -0
  203. package/dist/game/toasts.js +44 -0
  204. package/dist/game/trade.d.ts +10 -0
  205. package/dist/game/trade.js +10 -0
  206. package/dist/game/unlocks.d.ts +8 -0
  207. package/dist/game/unlocks.js +16 -0
  208. package/dist/game/vfxInstance.d.ts +138 -0
  209. package/dist/game/vfxInstance.js +96 -0
  210. package/dist/game/worldItem.d.ts +17 -4
  211. package/dist/game/worldItem.js +17 -4
  212. package/dist/gameplay.d.ts +91 -0
  213. package/dist/gameplay.js +91 -0
  214. package/dist/index.d.ts +5 -14
  215. package/dist/index.js +5 -14
  216. package/dist/input/actionBindings.d.ts +23 -6
  217. package/dist/input/actionBindings.js +23 -6
  218. package/dist/input/axisInput.d.ts +7 -2
  219. package/dist/input/axisInput.js +7 -2
  220. package/dist/input/controlGroups.d.ts +67 -0
  221. package/dist/input/controlGroups.js +41 -0
  222. package/dist/input/touchScheme.d.ts +82 -6
  223. package/dist/input/touchScheme.js +123 -10
  224. package/dist/interaction/lockpick.d.ts +61 -0
  225. package/dist/interaction/lockpick.js +168 -0
  226. package/dist/interaction/proximityPrompt.d.ts +2 -0
  227. package/dist/interaction/proximityPrompt.js +2 -0
  228. package/dist/interaction/qte.d.ts +5 -0
  229. package/dist/interaction/qte.js +5 -0
  230. package/dist/interaction/skillCheck.d.ts +5 -0
  231. package/dist/interaction/skillCheck.js +5 -0
  232. package/dist/inventory/inventoryModel.d.ts +12 -0
  233. package/dist/inventory/inventoryModel.js +12 -0
  234. package/dist/inventory/shapedGrid.d.ts +16 -0
  235. package/dist/inventory/shapedGrid.js +16 -0
  236. package/dist/inventory/slotModel.d.ts +10 -0
  237. package/dist/inventory/slotModel.js +10 -0
  238. package/dist/item/durability.d.ts +5 -0
  239. package/dist/item/durability.js +5 -0
  240. package/dist/item/itemIdentity.d.ts +171 -0
  241. package/dist/item/itemIdentity.js +138 -0
  242. package/dist/item/itemInstanceRegistry.d.ts +32 -0
  243. package/dist/item/itemInstanceRegistry.js +34 -0
  244. package/dist/item/itemgen.d.ts +163 -0
  245. package/dist/item/itemgen.js +151 -0
  246. package/dist/item/modularItem.d.ts +5 -0
  247. package/dist/item/modularItem.js +5 -0
  248. package/dist/item/use.d.ts +5 -0
  249. package/dist/item/use.js +5 -0
  250. package/dist/item/useBehavior.d.ts +142 -0
  251. package/dist/item/useBehavior.js +105 -0
  252. package/dist/item/weapon.d.ts +5 -0
  253. package/dist/item/weapon.js +5 -0
  254. package/dist/math/scalar.d.ts +25 -0
  255. package/dist/math/scalar.js +47 -0
  256. package/dist/meta/changelog.d.ts +4 -1
  257. package/dist/meta/changelog.js +16 -2
  258. package/dist/movement/avoidance.d.ts +45 -0
  259. package/dist/movement/avoidance.js +123 -0
  260. package/dist/movement/cameraRig.d.ts +8 -0
  261. package/dist/movement/cameraRig.js +8 -0
  262. package/dist/movement/dash.d.ts +11 -0
  263. package/dist/movement/dash.js +11 -0
  264. package/dist/movement/formation.d.ts +135 -0
  265. package/dist/movement/formation.js +194 -0
  266. package/dist/movement/glideModel.d.ts +5 -0
  267. package/dist/movement/glideModel.js +5 -0
  268. package/dist/movement/grappleSwing.d.ts +5 -0
  269. package/dist/movement/grappleSwing.js +5 -0
  270. package/dist/movement/leaderTrail.d.ts +5 -0
  271. package/dist/movement/leaderTrail.js +5 -0
  272. package/dist/movement/movementModel.d.ts +35 -8
  273. package/dist/movement/movementModel.js +42 -14
  274. package/dist/movement/playerMovement.js +65 -10
  275. package/dist/movement/poseState.d.ts +13 -0
  276. package/dist/movement/poseState.js +16 -0
  277. package/dist/movement/steering.d.ts +8 -0
  278. package/dist/movement/steering.js +18 -0
  279. package/dist/multiplayer/combatSnapshot.d.ts +3 -0
  280. package/dist/multiplayer/combatSnapshot.js +3 -0
  281. package/dist/multiplayer/lagCompensation.d.ts +5 -0
  282. package/dist/multiplayer/lagCompensation.js +5 -0
  283. package/dist/multiplayer/presenceModel.d.ts +9 -1
  284. package/dist/multiplayer/presenceModel.js +9 -1
  285. package/dist/multiplayer/simultaneousCommit.d.ts +3 -0
  286. package/dist/multiplayer/simultaneousCommit.js +3 -0
  287. package/dist/multiplayer.d.ts +11 -0
  288. package/dist/multiplayer.js +11 -0
  289. package/dist/nav/pathFollow.d.ts +57 -0
  290. package/dist/nav/pathFollow.js +109 -0
  291. package/dist/orders/orderKinds.d.ts +156 -0
  292. package/dist/orders/orderKinds.js +197 -0
  293. package/dist/orders/orderQueue.d.ts +208 -0
  294. package/dist/orders/orderQueue.js +211 -0
  295. package/dist/physics/drivableVehicle.d.ts +29 -0
  296. package/dist/physics/drivableVehicle.js +16 -0
  297. package/dist/physics/kinematicVehicle.d.ts +21 -1
  298. package/dist/physics/kinematicVehicle.js +23 -8
  299. package/dist/physics/physicsWorld.d.ts +24 -12
  300. package/dist/physics/physicsWorld.js +11 -5
  301. package/dist/physics/radialImpulse.d.ts +25 -0
  302. package/dist/physics/radialImpulse.js +27 -0
  303. package/dist/procedural.d.ts +3 -0
  304. package/dist/procedural.js +3 -0
  305. package/dist/progression/statGraph.d.ts +116 -0
  306. package/dist/progression/statGraph.js +318 -0
  307. package/dist/puzzle/cellGrid.d.ts +9 -0
  308. package/dist/puzzle/cellGrid.js +9 -0
  309. package/dist/puzzle/fallingPiece.d.ts +9 -0
  310. package/dist/puzzle/fallingPiece.js +9 -0
  311. package/dist/puzzle/nonogram.d.ts +39 -0
  312. package/dist/puzzle/nonogram.js +136 -0
  313. package/dist/random/nameGen.d.ts +5 -0
  314. package/dist/random/nameGen.js +5 -0
  315. package/dist/random/pick.d.ts +8 -0
  316. package/dist/random/pick.js +31 -0
  317. package/dist/random/rng.d.ts +22 -0
  318. package/dist/random/rng.js +28 -8
  319. package/dist/random/seedLink.d.ts +4 -0
  320. package/dist/random/seedLink.js +4 -0
  321. package/dist/relation/keyedValues.d.ts +54 -0
  322. package/dist/relation/keyedValues.js +115 -0
  323. package/dist/relation/thresholds.d.ts +62 -0
  324. package/dist/relation/thresholds.js +62 -0
  325. package/dist/render/color.d.ts +12 -0
  326. package/dist/render/color.js +26 -0
  327. package/dist/render/lookPreset.d.ts +41 -0
  328. package/dist/render/lookPreset.js +29 -0
  329. package/dist/render/postProcessing.d.ts +20 -0
  330. package/dist/render/postProcessing.js +21 -1
  331. package/dist/rules/predicate.d.ts +53 -0
  332. package/dist/rules/predicate.js +64 -0
  333. package/dist/rules/ruleEffects.d.ts +38 -0
  334. package/dist/rules/ruleEffects.js +37 -0
  335. package/dist/rules/triggeredRules.d.ts +160 -0
  336. package/dist/rules/triggeredRules.js +241 -0
  337. package/dist/runtime/adapter.d.ts +42 -3
  338. package/dist/runtime/adapter.js +44 -1
  339. package/dist/runtime/cameraDirector.d.ts +1 -0
  340. package/dist/runtime/cameraDirector.js +1 -0
  341. package/dist/runtime/commandRunner.d.ts +1 -0
  342. package/dist/runtime/commandRunner.js +1 -0
  343. package/dist/runtime/context/combatFx.d.ts +23 -0
  344. package/dist/runtime/context/combatFx.js +161 -0
  345. package/dist/runtime/context/registries.d.ts +13 -0
  346. package/dist/runtime/context/registries.js +84 -0
  347. package/dist/runtime/context/worldItems.d.ts +21 -0
  348. package/dist/runtime/context/worldItems.js +37 -0
  349. package/dist/runtime/descriptors/baseline.d.ts +48 -0
  350. package/dist/runtime/descriptors/baseline.js +134 -0
  351. package/dist/runtime/descriptors/features.d.ts +74 -0
  352. package/dist/runtime/descriptors/features.js +281 -0
  353. package/dist/runtime/gameContext.d.ts +114 -13
  354. package/dist/runtime/gameContext.js +303 -395
  355. package/dist/runtime/gameRuntime.d.ts +1 -0
  356. package/dist/runtime/gameRuntime.js +1 -0
  357. package/dist/runtime/headlessRunner.d.ts +60 -0
  358. package/dist/runtime/headlessRunner.js +44 -0
  359. package/dist/runtime/hostPersistence.d.ts +12 -0
  360. package/dist/runtime/hostPersistence.js +12 -0
  361. package/dist/runtime/hostedGameRunner.d.ts +8 -3
  362. package/dist/runtime/hostedGameRunner.js +26 -3
  363. package/dist/runtime/hostedWorldSession.d.ts +12 -4
  364. package/dist/runtime/hostedWorldSession.js +11 -4
  365. package/dist/runtime/inputSnapshot.d.ts +8 -3
  366. package/dist/runtime/inputSnapshot.js +6 -2
  367. package/dist/runtime/motionIntents.d.ts +11 -2
  368. package/dist/runtime/motionIntents.js +16 -2
  369. package/dist/runtime/perContext.d.ts +15 -0
  370. package/dist/runtime/perContext.js +21 -0
  371. package/dist/runtime/runtimeSave.d.ts +79 -0
  372. package/dist/runtime/runtimeSave.js +105 -0
  373. package/dist/runtime/save.d.ts +4 -0
  374. package/dist/runtime/save.js +4 -0
  375. package/dist/runtime/snapshot.d.ts +7 -0
  376. package/dist/runtime/snapshot.js +7 -0
  377. package/dist/runtime/visibility.d.ts +2 -1
  378. package/dist/runtime/visibility.js +2 -1
  379. package/dist/runtime/worldChannel.d.ts +7 -3
  380. package/dist/runtime/worldChannel.js +51 -6
  381. package/dist/runtime/worldMirror.d.ts +14 -2
  382. package/dist/runtime/worldMirror.js +15 -2
  383. package/dist/runtime/worldProjection.d.ts +47 -0
  384. package/dist/runtime/worldProjection.js +52 -0
  385. package/dist/runtime/worldReplication.d.ts +30 -7
  386. package/dist/runtime/worldReplication.js +41 -9
  387. package/dist/runtime/worldSnapshot.d.ts +30 -3
  388. package/dist/runtime/worldSnapshot.js +20 -5
  389. package/dist/scene/assetCatalog.d.ts +21 -0
  390. package/dist/scene/assetCatalog.js +4 -0
  391. package/dist/scene/assetDiagnostics.d.ts +69 -0
  392. package/dist/scene/assetDiagnostics.js +110 -0
  393. package/dist/scene/assetGenerator.d.ts +65 -0
  394. package/dist/scene/assetGenerator.js +64 -0
  395. package/dist/scene/assetSpace.d.ts +137 -0
  396. package/dist/scene/assetSpace.js +259 -0
  397. package/dist/scene/authoredTriggers.d.ts +169 -0
  398. package/dist/scene/authoredTriggers.js +340 -0
  399. package/dist/scene/behaviorRuntime.d.ts +75 -0
  400. package/dist/scene/behaviorRuntime.js +217 -0
  401. package/dist/scene/behaviors.d.ts +14 -2
  402. package/dist/scene/behaviors.js +7 -2
  403. package/dist/scene/bodyBind.d.ts +60 -0
  404. package/dist/scene/bodyBind.js +51 -0
  405. package/dist/scene/builtinSceneKinds.d.ts +5 -0
  406. package/dist/scene/builtinSceneKinds.js +62 -0
  407. package/dist/scene/colliders.d.ts +13 -1
  408. package/dist/scene/colliders.js +37 -1
  409. package/dist/scene/entityStats.d.ts +11 -2
  410. package/dist/scene/entityStats.js +11 -2
  411. package/dist/scene/entityStore.d.ts +12 -1
  412. package/dist/scene/entityStore.js +15 -0
  413. package/dist/scene/facing.d.ts +18 -0
  414. package/dist/scene/facing.js +26 -0
  415. package/dist/scene/modelSockets.d.ts +33 -0
  416. package/dist/scene/modelSockets.js +29 -0
  417. package/dist/scene/objectQuery.d.ts +2 -0
  418. package/dist/scene/objectQuery.js +2 -0
  419. package/dist/scene/objectStore.d.ts +4 -0
  420. package/dist/scene/objectStore.js +6 -0
  421. package/dist/scene/possession.d.ts +8 -0
  422. package/dist/scene/possession.js +14 -0
  423. package/dist/scene/roster.d.ts +3 -0
  424. package/dist/scene/roster.js +12 -0
  425. package/dist/scene/sceneKinds.d.ts +240 -0
  426. package/dist/scene/sceneKinds.js +214 -0
  427. package/dist/scene/sceneOwnership.d.ts +169 -0
  428. package/dist/scene/sceneOwnership.js +179 -0
  429. package/dist/scene/selectionBookmarks.d.ts +93 -0
  430. package/dist/scene/selectionBookmarks.js +122 -0
  431. package/dist/scene/vehicleSeat.d.ts +68 -0
  432. package/dist/scene/vehicleSeat.js +59 -0
  433. package/dist/sensor/concealment.d.ts +3 -0
  434. package/dist/sensor/concealment.js +3 -0
  435. package/dist/sensor/freezeMonitor.d.ts +1 -0
  436. package/dist/sensor/freezeMonitor.js +1 -0
  437. package/dist/sensor/frustumSensor.d.ts +7 -3
  438. package/dist/sensor/frustumSensor.js +7 -3
  439. package/dist/sensor/hiddenStateProbe.d.ts +5 -2
  440. package/dist/sensor/hiddenStateProbe.js +5 -2
  441. package/dist/sensor/recordingBuffer.d.ts +2 -1
  442. package/dist/sensor/recordingBuffer.js +2 -1
  443. package/dist/sensor/replayLoop.d.ts +6 -2
  444. package/dist/sensor/replayLoop.js +6 -2
  445. package/dist/sensor/revealQuery.d.ts +2 -1
  446. package/dist/sensor/revealQuery.js +2 -1
  447. package/dist/sensor/visionCone.d.ts +10 -3
  448. package/dist/sensor/visionCone.js +10 -3
  449. package/dist/session/contestedChannel.d.ts +1 -0
  450. package/dist/session/contestedChannel.js +1 -0
  451. package/dist/session/extraction.d.ts +1 -0
  452. package/dist/session/extraction.js +1 -0
  453. package/dist/session/roles.d.ts +6 -0
  454. package/dist/session/roles.js +6 -0
  455. package/dist/session/roundState.d.ts +8 -0
  456. package/dist/session/roundState.js +2 -0
  457. package/dist/settings/settingsModel.d.ts +1 -0
  458. package/dist/settings/settingsModel.js +1 -0
  459. package/dist/stats/accumulatorMeter.d.ts +6 -0
  460. package/dist/stats/accumulatorMeter.js +6 -0
  461. package/dist/stats/eventMeter.d.ts +5 -0
  462. package/dist/stats/eventMeter.js +5 -0
  463. package/dist/stats/rollCheck.d.ts +5 -0
  464. package/dist/stats/rollCheck.js +5 -0
  465. package/dist/stats/statModifiers.d.ts +5 -0
  466. package/dist/stats/statModifiers.js +5 -0
  467. package/dist/store/defineKeyedStore.d.ts +38 -0
  468. package/dist/store/defineKeyedStore.js +42 -0
  469. package/dist/store/defineStore.d.ts +33 -0
  470. package/dist/store/defineStore.js +35 -0
  471. package/dist/store/observableKeyedStore.d.ts +5 -0
  472. package/dist/store/observableKeyedStore.js +34 -2
  473. package/dist/survival/decayMeter.d.ts +55 -0
  474. package/dist/survival/decayMeter.js +130 -55
  475. package/dist/survival/moodle.d.ts +2 -0
  476. package/dist/survival/regionHealth.d.ts +6 -0
  477. package/dist/survival/regionHealth.js +5 -0
  478. package/dist/tactics/fallingGrid.d.ts +4 -2
  479. package/dist/tactics/fallingGrid.js +4 -2
  480. package/dist/tactics/predictiveQuery.d.ts +3 -0
  481. package/dist/tactics/predictiveQuery.js +3 -0
  482. package/dist/tactics/snapshot.d.ts +2 -0
  483. package/dist/tactics/snapshot.js +2 -0
  484. package/dist/tactics/surface.d.ts +1 -0
  485. package/dist/tactics/surface.js +1 -0
  486. package/dist/tactics/tacticalGrid.d.ts +1 -0
  487. package/dist/tactics/tacticalGrid.js +1 -0
  488. package/dist/time/beatClock.d.ts +14 -4
  489. package/dist/time/beatClock.js +14 -4
  490. package/dist/time/calendarClock.d.ts +3 -0
  491. package/dist/time/calendarClock.js +3 -0
  492. package/dist/time/idleProgress.d.ts +4 -0
  493. package/dist/time/idleProgress.js +4 -0
  494. package/dist/time/simClock.d.ts +2 -0
  495. package/dist/time/simClock.js +20 -5
  496. package/dist/ui/actionModel.d.ts +110 -0
  497. package/dist/ui/actionModel.js +159 -0
  498. package/dist/ui/gameLayout.d.ts +30 -11
  499. package/dist/ui/gameLayout.js +30 -11
  500. package/dist/ui/hudDocument.d.ts +114 -0
  501. package/dist/ui/hudDocument.js +220 -0
  502. package/dist/ui/hudLayout.d.ts +33 -1
  503. package/dist/ui/hudLayout.js +204 -17
  504. package/dist/ui/selectionModel.d.ts +92 -0
  505. package/dist/ui/selectionModel.js +88 -0
  506. package/dist/ui/tooltipModel.d.ts +63 -0
  507. package/dist/ui/tooltipModel.js +82 -0
  508. package/dist/ui.d.ts +15 -0
  509. package/dist/ui.js +15 -0
  510. package/dist/visibility/bounds.d.ts +8 -1
  511. package/dist/visibility/bounds.js +8 -1
  512. package/dist/visibility/frustum.d.ts +12 -3
  513. package/dist/visibility/frustum.js +12 -3
  514. package/dist/work/jobQueue.d.ts +201 -0
  515. package/dist/work/jobQueue.js +253 -0
  516. package/dist/work/unitTraining.d.ts +58 -0
  517. package/dist/work/unitTraining.js +52 -0
  518. package/dist/world/authoredObjects.d.ts +58 -0
  519. package/dist/world/authoredObjects.js +69 -0
  520. package/dist/world/buildingGenerator.d.ts +18 -0
  521. package/dist/world/buildingGenerator.js +62 -0
  522. package/dist/world/buildings.d.ts +5 -0
  523. package/dist/world/buildings.js +5 -0
  524. package/dist/world/catenary.d.ts +22 -0
  525. package/dist/world/catenary.js +69 -0
  526. package/dist/world/features.d.ts +115 -7
  527. package/dist/world/features.js +56 -5
  528. package/dist/world/footprintGrid.d.ts +59 -0
  529. package/dist/world/footprintGrid.js +141 -0
  530. package/dist/world/geometry.d.ts +64 -0
  531. package/dist/world/geometry.js +94 -0
  532. package/dist/world/grassKind.d.ts +51 -0
  533. package/dist/world/grassKind.js +101 -0
  534. package/dist/world/pathInstances.d.ts +27 -0
  535. package/dist/world/pathInstances.js +27 -0
  536. package/dist/world/pathTerrain.d.ts +80 -0
  537. package/dist/world/pathTerrain.js +142 -0
  538. package/dist/world/placeAsset.d.ts +81 -0
  539. package/dist/world/placeAsset.js +77 -0
  540. package/dist/world/placement.d.ts +4 -0
  541. package/dist/world/placement.js +4 -0
  542. package/dist/world/placementController.d.ts +6 -0
  543. package/dist/world/placementController.js +6 -0
  544. package/dist/world/polyline.d.ts +34 -0
  545. package/dist/world/polyline.js +81 -0
  546. package/dist/world/roads.d.ts +8 -0
  547. package/dist/world/roads.js +38 -0
  548. package/dist/world/scatterRegion.d.ts +164 -0
  549. package/dist/world/scatterRegion.js +409 -0
  550. package/dist/world/sceneShapes.d.ts +43 -0
  551. package/dist/world/sceneShapes.js +1 -0
  552. package/dist/world/segment.d.ts +27 -0
  553. package/dist/world/segment.js +43 -0
  554. package/dist/world/soilKind.d.ts +48 -0
  555. package/dist/world/soilKind.js +92 -0
  556. package/dist/world/spatialSample.d.ts +241 -0
  557. package/dist/world/spatialSample.js +465 -0
  558. package/dist/world/terraform.d.ts +213 -1
  559. package/dist/world/terraform.js +655 -20
  560. package/dist/world/terrain.d.ts +119 -20
  561. package/dist/world/terrain.js +247 -41
  562. package/dist/world/terrainGuides.d.ts +182 -0
  563. package/dist/world/terrainGuides.js +343 -0
  564. package/dist/world/vec2.d.ts +81 -0
  565. package/dist/world/vec2.js +128 -0
  566. package/dist/world/vec3.d.ts +49 -0
  567. package/dist/world/vec3.js +76 -0
  568. package/dist/world/vegetation.d.ts +71 -0
  569. package/dist/world/vegetation.js +151 -0
  570. package/dist/world/visibilityField.d.ts +154 -0
  571. package/dist/world/visibilityField.js +271 -0
  572. package/dist/world/volumetricClouds.d.ts +52 -0
  573. package/dist/world/volumetricClouds.js +43 -0
  574. package/dist/world/walls.d.ts +9 -0
  575. package/dist/world/walls.js +9 -0
  576. package/dist/world/waterKind.d.ts +55 -0
  577. package/dist/world/waterKind.js +99 -0
  578. package/dist/world.d.ts +132 -0
  579. package/dist/world.js +132 -0
  580. package/package.json +32 -8
  581. package/skills/jgengine/SKILL.md +51 -0
  582. package/skills/jgengine/api.md +434 -0
  583. package/skills/jgengine/capabilities.md +29 -0
  584. package/skills/jgengine/reference-cartridge.md +72 -0
  585. package/skills/jgengine-combat/SKILL.md +34 -0
  586. package/skills/jgengine-combat/api.md +549 -0
  587. package/skills/jgengine-combat/capabilities.md +169 -0
  588. package/skills/jgengine-combat/reference.md +98 -0
  589. package/skills/jgengine-gameplay/SKILL.md +39 -0
  590. package/skills/jgengine-gameplay/api.md +1794 -0
  591. package/skills/jgengine-gameplay/capabilities.md +378 -0
  592. package/skills/jgengine-gameplay/reference-systems.md +60 -0
  593. package/skills/jgengine-world/SKILL.md +38 -0
  594. package/skills/jgengine-world/api.md +2652 -0
  595. package/skills/jgengine-world/capabilities.md +370 -0
  596. package/skills/jgengine-world/reference.md +222 -0
  597. package/dist/cartridge/runtime.d.ts +0 -15
  598. package/dist/cartridge/runtime.js +0 -556
  599. package/dist/cartridge/spec.d.ts +0 -188
  600. package/dist/cartridge/spec.js +0 -19
  601. package/dist/cartridge/validate.d.ts +0 -2
  602. package/dist/cartridge/validate.js +0 -78
  603. package/dist/store/engineStore.d.ts +0 -22
  604. package/dist/store/engineStore.js +0 -50
  605. package/dist/store/features.d.ts +0 -10
  606. package/dist/store/features.js +0 -23
  607. package/llms.txt +0 -5389
@@ -1,36 +1,42 @@
1
- import { createCardPile } from "../cards/cardPile.js";
1
+ import {} from "../cards/cardPile.js";
2
2
  import { createDeathSystem, deathReasonFromEffect, normalizeOnDeath } from "../combat/death.js";
3
3
  import { createEffectSystem, } from "../combat/effects.js";
4
4
  import { createProjectileSystem } from "../combat/projectiles.js";
5
- import { resolveHitReaction, } from "../combat/hitReaction.js";
6
- import { pointInTelegraph, } from "../combat/telegraph.js";
5
+ import {} from "../combat/hitReaction.js";
6
+ import {} from "../combat/telegraph.js";
7
7
  import { createCommandRegistry, } from "../commands/commandRegistry.js";
8
- import { balance as walletBalance, canAfford as walletCanAfford, charge as walletCharge, chargeAll as walletChargeAll, createEmptyWallet, grant as walletGrant, } from "../economy/wallet.js";
9
- import { createCosmetics } from "../game/cosmetics.js";
8
+ import { balance as walletBalance, charge as walletCharge, createEmptyWallet, grant as walletGrant, isOverdrawn as walletIsOverdrawn, } from "../economy/wallet.js";
9
+ import {} from "../game/cosmetics.js";
10
10
  import { groundFieldFor } from "../world/terrain.js";
11
11
  import { createGameEvents } from "../game/events.js";
12
+ import { createVfxInstanceStore } from "../game/vfxInstance.js";
12
13
  import { createGameFeed } from "../game/feed.js";
13
- import { createLeaderboard } from "../game/leaderboard.js";
14
+ import { setGamePhase } from "../game/gamePhase.js";
15
+ import {} from "../game/leaderboard.js";
14
16
  import { createLoadouts } from "../game/loadout.js";
15
17
  import { createLootRegistry, grantDrops } from "../game/lootTable.js";
16
- import { createQuestJournal } from "../game/quest.js";
17
- import { createWorldItemStore, resolveDeathDrops, DEFAULT_RARITY, WORLD_ITEM_ENTITY_NAME, } from "../game/worldItem.js";
18
- import { createChat } from "../game/chat.js";
18
+ import {} from "../game/dialogue.js";
19
+ import {} from "../game/quest.js";
20
+ import { resolveDeathDrops, DEFAULT_RARITY, } from "../game/worldItem.js";
21
+ import {} from "../game/chat.js";
19
22
  import { createSocial } from "../game/social.js";
20
- import { createTradeSystem } from "../game/trade.js";
21
- import { createUnlocks } from "../game/unlocks.js";
23
+ import {} from "../game/trade.js";
24
+ import {} from "../game/unlocks.js";
22
25
  import { createInventorySet, putItem, } from "../inventory/inventoryModel.js";
23
26
  import { createItemUse, } from "../item/use.js";
24
27
  import { createWeaponStats } from "../item/weapon.js";
25
28
  import { createPoseState } from "../movement/poseState.js";
29
+ import { createBodyBind } from "../scene/bodyBind.js";
26
30
  import { createPaintLayer } from "../scene/paintLayer.js";
27
- import { createEntityStatsApi, hydrateEntityStats, seedStatValues, setStatValue, snapshotEntityStats, } from "../scene/entityStats.js";
31
+ import { createEntityStatsApi, seedStatValues, setStatValue, } from "../scene/entityStats.js";
32
+ import { createEntityStore, } from "../scene/entityStore.js";
28
33
  import { createForms } from "../scene/form.js";
29
- import { scaledObjectColliders } from "../scene/colliders.js";
34
+ import { scaledEntityColliders, scaledObjectColliders } from "../scene/colliders.js";
30
35
  import { raycastObjects, raycastObjectsAll } from "../scene/objectQuery.js";
31
36
  import { createObjectStore, objectVisualScale } from "../scene/objectStore.js";
32
- import { createRoster } from "../scene/roster.js";
33
- import { createConnectedPlayers } from "../game/connectedPlayers.js";
37
+ import {} from "../scene/roster.js";
38
+ import { createSelectionSet } from "../scene/selection.js";
39
+ import {} from "../game/connectedPlayers.js";
34
40
  import { createPossession } from "../scene/possession.js";
35
41
  import { createSceneRaycast, } from "../scene/sceneRaycast.js";
36
42
  import { createSpatialApi } from "../scene/spatial.js";
@@ -39,12 +45,21 @@ import { createStats } from "../stats/statModifiers.js";
39
45
  import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
40
46
  import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
41
47
  import { applyWorldSnapshot, composeWorldSnapshot, } from "./worldSnapshot.js";
48
+ import { policyProjectsViewers, projectByVisibleIds, projectEntitiesForViewer, projectPerUserForViewer, visibleEntityIds, } from "./worldProjection.js";
49
+ import { createRuntimeSave } from "./runtimeSave.js";
50
+ import { isOffline } from "./adapter.js";
51
+ import { localSaveBackend, memorySaveBackend } from "../game/saveStore.js";
42
52
  import { createSimClock } from "../time/simClock.js";
43
- import { createTurnLoop } from "../turn/turnLoop.js";
53
+ import {} from "../turn/turnLoop.js";
44
54
  import { RaceState } from "../game/race.js";
45
55
  import { createCameraDirector } from "./cameraDirector.js";
46
56
  import { createInputSnapshot } from "./inputSnapshot.js";
47
57
  import { createMotionIntents } from "./motionIntents.js";
58
+ import { baselineDescriptors } from "./descriptors/baseline.js";
59
+ import { featureDescriptors } from "./descriptors/features.js";
60
+ import { createCombatFx } from "./context/combatFx.js";
61
+ import { createContextRegistries } from "./context/registries.js";
62
+ import { createWorldItemContext } from "./context/worldItems.js";
48
63
  export function createGameContext(options) {
49
64
  const { definition, content, player } = options;
50
65
  const now = options.now ?? Date.now;
@@ -54,7 +69,7 @@ export function createGameContext(options) {
54
69
  const activeUserId = () => actingUserId ?? player.userId;
55
70
  const time = createSimClock({ config: definition.time, onChange: signal.notify });
56
71
  const ground = groundFieldFor(definition.world);
57
- const entities = definition.scene;
72
+ const entities = createEntityStore();
58
73
  const objects = createObjectStore();
59
74
  entities.subscribe(signal.notify);
60
75
  objects.subscribe(signal.notify);
@@ -106,7 +121,16 @@ export function createGameContext(options) {
106
121
  const override = entityColliders.get(instanceId);
107
122
  if (override !== undefined)
108
123
  return override;
109
- return catalogEntry(instanceId)?.colliders ?? null;
124
+ const entry = catalogEntry(instanceId);
125
+ if (entry?.colliders !== undefined)
126
+ return entry.colliders;
127
+ const scale = entry?.scale;
128
+ if (scale !== undefined && scale !== 1)
129
+ return scaledEntityColliders(scale);
130
+ return null;
131
+ }
132
+ function entityVisualScaleOf(instanceId) {
133
+ return catalogEntry(instanceId)?.scale ?? 1;
110
134
  }
111
135
  function objectCollidersOf(instanceId) {
112
136
  const override = objectColliders.get(instanceId);
@@ -181,47 +205,23 @@ export function createGameContext(options) {
181
205
  };
182
206
  const feed = createGameFeed(definition.feed);
183
207
  const lootRegistry = createLootRegistry();
184
- const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], signal.notify);
185
208
  const features = definition.features ?? {};
186
- const rawSocial = features.social || features.chat
187
- ? createSocial({
188
- events,
189
- now,
190
- emotes: {
191
- entities: { get: (id) => entities.get(id) },
192
- spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
193
- },
194
- })
195
- : null;
196
- let social;
197
- if (features.social && rawSocial !== null) {
198
- social = {
199
- friends: notifyAfter(rawSocial.friends, ["request", "accept", "decline", "remove", "block", "hydrate"], signal.notify),
200
- party: notifyAfter(rawSocial.party, ["invite", "accept", "decline", "kick", "leave", "promote"], signal.notify),
201
- presence: rawSocial.presence,
202
- emotes: rawSocial.emotes,
203
- worldInvites: notifyAfter(rawSocial.worldInvites, ["invite", "accept", "decline"], signal.notify),
204
- };
205
- }
206
- let chat;
207
- if (features.chat && rawSocial !== null) {
208
- chat = notifyAfter(createChat({
209
- events,
210
- now,
211
- party: rawSocial.party,
212
- proximity: {
213
- entities: { get: (id) => entities.get(id) },
214
- spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
215
- },
216
- blockedBy: (userId) => rawSocial.friends.snapshot(userId).blocked,
217
- }), ["register", "send", "whisper", "hydrate"], signal.notify);
218
- }
219
- const leaderboard = features.leaderboard
220
- ? notifyAfter(createLeaderboard(), ["increment", "hydrate"], signal.notify)
221
- : undefined;
222
- const roster = features.roster
223
- ? notifyAfter(createRoster({ now }), ["capture", "release", "setEquipped", "hydrate"], signal.notify)
224
- : undefined;
209
+ const featureRegistry = new Map();
210
+ const featureValue = (key) => featureRegistry.get(key);
211
+ let rawSocial = null;
212
+ const sharedSocial = () => {
213
+ if (rawSocial === null) {
214
+ rawSocial = createSocial({
215
+ events,
216
+ now,
217
+ emotes: {
218
+ entities: { get: (id) => entities.get(id) },
219
+ spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
220
+ },
221
+ });
222
+ }
223
+ return rawSocial;
224
+ };
225
225
  const playerStatsByUser = new Map();
226
226
  function playerStatsFor(userId) {
227
227
  let stats = playerStatsByUser.get(userId);
@@ -233,10 +233,26 @@ export function createGameContext(options) {
233
233
  }
234
234
  const pose = createPoseState((instanceId) => catalogEntry(instanceId)?.movement);
235
235
  const commandRegistry = createCommandRegistry();
236
+ if (definition.lifecycle !== undefined) {
237
+ const lifecycle = definition.lifecycle;
238
+ commandRegistry.define(lifecycle.commands?.start ?? "start", {
239
+ apply(state, input) {
240
+ const next = lifecycle.start(lifecycle.store.read(state), state, input);
241
+ lifecycle.store.write(state, next);
242
+ setGamePhase(state, lifecycle.phaseOf(next));
243
+ },
244
+ });
245
+ commandRegistry.define(lifecycle.commands?.restart ?? "restart", {
246
+ apply(state) {
247
+ const next = lifecycle.restart(lifecycle.store.read(state), state);
248
+ lifecycle.store.write(state, next);
249
+ setGamePhase(state, lifecycle.phaseOf(next));
250
+ },
251
+ });
252
+ }
236
253
  const itemUse = createItemUse((itemId) => content.itemById?.(itemId)?.use);
237
254
  const possession = notifyAfter(createPossession({ entities, events }), ["possess", "own", "disown"], signal.notify);
238
255
  const forms = notifyAfter(createForms({ entities, time, events }), ["shapeshift", "revert"], signal.notify);
239
- const cosmetics = notifyAfter(createCosmetics({ events }), ["apply", "equip", "hydrate"], signal.notify);
240
256
  const paintLayer = notifyAfter(createPaintLayer(), ["paint", "clear"], signal.notify);
241
257
  const inventoryDeclarations = definition.inventories ?? {};
242
258
  const inventoryIds = Object.keys(inventoryDeclarations);
@@ -262,14 +278,15 @@ export function createGameContext(options) {
262
278
  wallets.set(userId, walletGrant(walletOf(userId), currencyId, amount));
263
279
  signal.notify();
264
280
  },
265
- charge(userId, currencyId, amount) {
266
- const result = walletCharge(walletOf(userId), currencyId, amount);
281
+ charge(userId, currencyId, amount, options) {
282
+ const result = walletCharge(walletOf(userId), currencyId, amount, options);
267
283
  if (result.status === "rejected")
268
284
  return { reason: result.reason };
269
285
  wallets.set(userId, result.state);
270
286
  signal.notify();
271
287
  return null;
272
288
  },
289
+ isOverdrawn: (userId, currencyId) => walletIsOverdrawn(walletOf(userId), currencyId),
273
290
  };
274
291
  function putIntoAnyInventory(userId, itemId, count) {
275
292
  const inventory = inventoryFor(userId);
@@ -293,64 +310,11 @@ export function createGameContext(options) {
293
310
  events.emit("loot.granted", event);
294
311
  },
295
312
  };
296
- const trade = createTradeSystem({
297
- resolveTrade: (itemId) => content.itemById?.(itemId)?.trade,
298
- wallet: {
299
- canAfford: (costs) => (walletCanAfford(walletOf(activeUserId()), costs) ? null : "insufficient-funds"),
300
- charge(costs) {
301
- const result = walletChargeAll(walletOf(activeUserId()), costs);
302
- if (result.status === "ok") {
303
- wallets.set(activeUserId(), result.state);
304
- signal.notify();
305
- }
306
- },
307
- grant(gains) {
308
- for (const [currencyId, amount] of Object.entries(gains)) {
309
- economy.grant(activeUserId(), currencyId, amount);
310
- }
311
- },
312
- },
313
- inventory: {
314
- put(inventoryId, itemId, count) {
315
- if (layouts[inventoryId] === undefined)
316
- return { reason: `unknown inventory "${inventoryId}"` };
317
- const result = inventoryFor(activeUserId()).put(inventoryId, itemId, count);
318
- return result.status === "ok" ? null : { reason: result.reason };
319
- },
320
- take(inventoryId, itemId, count) {
321
- if (layouts[inventoryId] === undefined)
322
- return { reason: `unknown inventory "${inventoryId}"` };
323
- const result = inventoryFor(activeUserId()).take(inventoryId, itemId, count);
324
- return result.status === "ok" ? null : { reason: result.reason };
325
- },
326
- count: (inventoryId, itemId) => inventoryFor(activeUserId()).count(inventoryId, itemId),
327
- },
328
- });
329
313
  function seedUserPool(userId, statId, pool) {
330
314
  const map = ensureInstanceStats(userId);
331
315
  const next = setStatValue(map, statId, pool);
332
316
  map[statId] = next[statId];
333
317
  }
334
- const rawQuest = createQuestJournal({
335
- events,
336
- rewards: {
337
- grantXp(userId, amount) {
338
- const existing = ensureInstanceStats(userId)["xp"];
339
- const current = (existing?.current ?? 0) + amount;
340
- seedUserPool(userId, "xp", { current, max: Math.max(existing?.max ?? 0, current) });
341
- },
342
- grantEconomy: (userId, currencyId, amount) => economy.grant(userId, currencyId, amount),
343
- grantItem(userId, inventoryId, itemId, count) {
344
- if (layouts[inventoryId] === undefined)
345
- return { reason: `unknown inventory "${inventoryId}"` };
346
- const result = inventoryFor(userId).put(inventoryId, itemId, count);
347
- return result.status === "ok" ? null : { reason: result.reason };
348
- },
349
- grantUnlock: (userId, unlockId) => unlocks.grant(userId, unlockId),
350
- },
351
- hasUnlock: (userId, id) => unlocks.has(userId, id),
352
- });
353
- const quest = notifyAfter(rawQuest, ["accept", "abandon", "progress", "turnIn", "grant", "revoke", "hydrate"], signal.notify);
354
318
  const loadouts = notifyAfter(createLoadouts({
355
319
  inventory: {
356
320
  begin(userId) {
@@ -377,7 +341,7 @@ export function createGameContext(options) {
377
341
  },
378
342
  stats: { seed: seedUserPool },
379
343
  economy: { grant: economy.grant },
380
- unlocks: { grant: unlocks.grant },
344
+ unlocks: { grant: (userId, unlockId) => featureValue("unlocks")?.grant(userId, unlockId) },
381
345
  }), ["applyLoadout"], signal.notify);
382
346
  function spawnEntity(name, spawnOptions) {
383
347
  const entry = content.entityById?.(name);
@@ -398,6 +362,23 @@ export function createGameContext(options) {
398
362
  entityColliders.delete(instanceId);
399
363
  return existed;
400
364
  }
365
+ function moveEntityTowardCommit(instanceId, target, options) {
366
+ const next = spatial.moveToward(instanceId, target, options);
367
+ if (next === null)
368
+ return null;
369
+ const current = entities.get(instanceId);
370
+ if (current === null)
371
+ return null;
372
+ let rotationY = current.rotationY;
373
+ if (options.face === true) {
374
+ const dx = next[0] - current.position[0];
375
+ const dz = next[2] - current.position[2];
376
+ if (Math.hypot(dx, dz) > 1e-9)
377
+ rotationY = Math.atan2(dx, dz);
378
+ }
379
+ entities.setPose(instanceId, { position: next, rotationY });
380
+ return next;
381
+ }
401
382
  function resetAllToSpawn(filter) {
402
383
  let count = 0;
403
384
  for (const entity of entities.list()) {
@@ -408,37 +389,28 @@ export function createGameContext(options) {
408
389
  }
409
390
  return count;
410
391
  }
411
- const worldItems = notifyAfter(createWorldItemStore({
412
- spawnEntity: (position) => entities.spawn(WORLD_ITEM_ENTITY_NAME, { position, role: "prop" }),
413
- despawnEntity,
414
- resolvePosition: (instanceId) => entities.get(instanceId)?.position,
415
- }), ["spawn", "take", "remove"], signal.notify);
416
- function spawnWorldItem(input) {
417
- const record = worldItems.spawn(input);
418
- events.emit("worldItem.dropped", {
419
- instanceId: record.instanceId,
420
- itemId: record.itemId,
421
- rarity: record.rarity,
422
- count: record.count,
423
- position: [input.position[0], input.position[1], input.position[2]],
424
- ...(record.source !== undefined ? { source: record.source } : {}),
425
- });
426
- return record;
427
- }
428
- function pickupWorldItem(instanceId, userId) {
429
- const record = worldItems.take(instanceId);
430
- if (record === null)
431
- return { status: "rejected", reason: "not-found" };
432
- loot.grantToPlayer(userId, [{ item: record.itemId, count: record.count }], "worldItem.pickup");
433
- events.emit("worldItem.picked_up", {
434
- instanceId,
435
- userId,
436
- itemId: record.itemId,
437
- rarity: record.rarity,
438
- count: record.count,
392
+ const bodyBinds = new Map();
393
+ function bind(key) {
394
+ const existing = bodyBinds.get(key);
395
+ if (existing !== undefined)
396
+ return existing;
397
+ const created = createBodyBind({
398
+ has: (id) => entities.get(id) !== null,
399
+ spawn: spawnEntity,
400
+ despawn: despawnEntity,
401
+ setPose: entities.setPose,
402
+ update: entities.update,
439
403
  });
440
- return { status: "ok", record };
404
+ bodyBinds.set(key, created);
405
+ return created;
441
406
  }
407
+ const { worldItems, spawnWorldItem, pickupWorldItem } = createWorldItemContext({
408
+ entities,
409
+ events,
410
+ despawnEntity,
411
+ grantToPlayer: loot.grantToPlayer,
412
+ signalNotify: signal.notify,
413
+ });
442
414
  const death = createDeathSystem({
443
415
  resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
444
416
  resolveIdentity(instanceId) {
@@ -498,121 +470,16 @@ export function createGameContext(options) {
498
470
  }
499
471
  },
500
472
  }), ["applyEffect"], signal.notify);
501
- function emitFloatText(input) {
502
- const position = input.position ??
503
- (input.instanceId === undefined ? undefined : entities.get(input.instanceId)?.position);
504
- if (position === undefined)
505
- return;
506
- const text = input.text ?? (input.amount === undefined ? "" : String(Math.round(input.amount)));
507
- const event = {
508
- position: [position[0], position[1], position[2]],
509
- text,
510
- kind: input.kind ?? "info",
511
- };
512
- if (input.instanceId !== undefined)
513
- event.instanceId = input.instanceId;
514
- if (input.amount !== undefined)
515
- event.amount = input.amount;
516
- if (input.hitType !== undefined)
517
- event.hitType = input.hitType;
518
- if (input.element !== undefined)
519
- event.element = input.element;
520
- if (input.crit !== undefined)
521
- event.crit = input.crit;
522
- if (input.scale !== undefined)
523
- event.scale = input.scale;
524
- events.emit("entity.floatText", event);
525
- }
526
- let telegraphSeq = 0;
527
- function fireTelegraph(input) {
528
- const id = telegraphSeq++;
529
- const telegraphEvent = {
530
- id,
531
- shape: input.shape,
532
- position: [input.at[0], input.at[1], input.at[2]],
533
- windupMs: input.windupMs,
534
- kind: input.kind ?? "danger",
535
- };
536
- if (input.dir !== undefined)
537
- telegraphEvent.dir = input.dir;
538
- events.emit("combat.telegraph", telegraphEvent);
539
- const cancelVisual = () => events.emit("combat.telegraphCancelled", { id });
540
- const bound = input.effect;
541
- if (bound === undefined)
542
- return cancelVisual;
543
- const config = { shape: input.shape, at: input.at, windupMs: input.windupMs };
544
- if (input.dir !== undefined)
545
- config.dir = input.dir;
546
- const cancelEffect = time.after(input.windupMs / 1000, () => {
547
- const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position));
548
- for (const target of targets) {
549
- applyEffectAndFloat({
550
- from: input.from,
551
- to: target.id,
552
- effect: bound.effect,
553
- ...(bound.via === undefined ? {} : { via: bound.via }),
554
- });
555
- }
556
- });
557
- return () => {
558
- cancelEffect();
559
- cancelVisual();
560
- };
561
- }
562
- function applyHitReaction(input) {
563
- const attacker = entities.get(input.from);
564
- const target = entities.get(input.to);
565
- if (target === null)
566
- return null;
567
- const attackerPos = attacker?.position ?? target.position;
568
- const reaction = resolveHitReaction(input.config, {
569
- attackerPos,
570
- targetPos: target.position,
571
- ...(input.power === undefined ? {} : { power: input.power }),
572
- });
573
- entities.setPose(input.to, {
574
- position: [
575
- target.position[0] + reaction.impulse[0],
576
- target.position[1] + reaction.impulse[1],
577
- target.position[2] + reaction.impulse[2],
578
- ],
579
- rotationY: target.rotationY,
580
- });
581
- const reactionEvent = {
582
- instanceId: input.to,
583
- position: [target.position[0], target.position[1], target.position[2]],
584
- hitstopMs: reaction.hitstopMs,
585
- };
586
- if (reaction.shake !== null)
587
- reactionEvent.shake = reaction.shake;
588
- events.emit("combat.hitReaction", reactionEvent);
589
- return reaction;
590
- }
591
- function applyEffectAndFloat(input) {
592
- const positionsBefore = new Map();
593
- for (const entity of entities.list())
594
- positionsBefore.set(entity.id, entity.position);
595
- const results = effects.applyEffect(input);
596
- for (const result of results) {
597
- let total = 0;
598
- for (const delta of result.applied)
599
- total += delta.delta;
600
- if (total === 0)
601
- continue;
602
- const position = entities.get(result.instanceId)?.position ?? positionsBefore.get(result.instanceId);
603
- if (position === undefined)
604
- continue;
605
- const magnitude = Math.abs(total);
606
- emitFloatText({
607
- instanceId: result.instanceId,
608
- position: [position[0], position[1], position[2]],
609
- text: String(Math.round(magnitude)),
610
- kind: total < 0 ? "damage" : "heal",
611
- amount: magnitude,
612
- });
613
- }
614
- return results;
615
- }
473
+ const { emitFloatText, emitVfx, fireTelegraph, applyHitReaction, applyEffectAndFloat } = createCombatFx({
474
+ entities,
475
+ events,
476
+ time,
477
+ applyEffect: (input) => effects.applyEffect(input),
478
+ });
479
+ const vfxInstances = createVfxInstanceStore({
480
+ onOp: (op) => events.emit("combat.vfxInstance", op),
481
+ now: () => time.now() * 1000,
482
+ });
616
483
  const floatingEffects = {
617
484
  canReceive: effects.canReceive,
618
485
  preview: effects.preview,
@@ -674,8 +541,15 @@ export function createGameContext(options) {
674
541
  });
675
542
  },
676
543
  }), ["fireProjectile", "settleProjectile"], signal.notify);
544
+ const objectSelection = notifyAfter(createSelectionSet(), ["add", "remove", "toggle", "replace", "clear"], signal.notify);
677
545
  const sceneObjects = {
678
546
  ...objects,
547
+ remove(instanceId) {
548
+ const existed = objects.remove(instanceId);
549
+ if (existed)
550
+ objectSelection.remove(instanceId);
551
+ return existed;
552
+ },
679
553
  catalog: (instanceId) => catalogObject(instanceId) ?? null,
680
554
  raycast: (input) => raycastObjects(objects, input),
681
555
  raycastAll: (input) => raycastObjectsAll(objects, input),
@@ -687,87 +561,10 @@ export function createGameContext(options) {
687
561
  signal.notify();
688
562
  },
689
563
  collidersOf: objectCollidersOf,
564
+ selection: objectSelection,
690
565
  };
691
566
  const store = notifyAfter(createObservableKeyedStore(), ["set", "delete", "hydrate"], signal.notify);
692
- const players = features.players
693
- ? notifyAfter(createConnectedPlayers(), ["join", "leave"], signal.notify)
694
- : undefined;
695
- const cardPiles = new Map();
696
- function pile(id, config) {
697
- const existing = cardPiles.get(id);
698
- if (existing !== undefined)
699
- return existing;
700
- if (config === undefined) {
701
- throw new Error(`cardPile "${id}" has not been created yet; pass a config on first access`);
702
- }
703
- const created = notifyAfter(createCardPile(config), ["shuffle", "draw", "discard", "exhaust", "move", "reset"], signal.notify);
704
- cardPiles.set(id, created);
705
- return created;
706
- }
707
- const turnLoops = new Map();
708
- function loop(id, config) {
709
- const existing = turnLoops.get(id);
710
- if (existing !== undefined)
711
- return existing;
712
- if (config === undefined) {
713
- throw new Error(`turn loop "${id}" has not been created yet; pass a config on first access`);
714
- }
715
- const raw = createTurnLoop(config);
716
- const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "discard", "clear"], signal.notify);
717
- const wrapped = {
718
- ...notifyAfter(raw, [
719
- "setOrder",
720
- "addParticipant",
721
- "removeParticipant",
722
- "advancePhase",
723
- "advanceTurn",
724
- "advanceRound",
725
- "spend",
726
- "gain",
727
- "refill",
728
- "restore",
729
- ], signal.notify),
730
- commit: wrappedCommit,
731
- };
732
- turnLoops.set(id, wrapped);
733
- return wrapped;
734
- }
735
- class NotifyingRaceState extends RaceState {
736
- addRacer(racerId, startTime) {
737
- super.addRacer(racerId, startTime);
738
- signal.notify();
739
- }
740
- removeRacer(racerId) {
741
- super.removeRacer(racerId);
742
- signal.notify();
743
- }
744
- reset() {
745
- super.reset();
746
- signal.notify();
747
- }
748
- eliminate(racerId) {
749
- super.eliminate(racerId);
750
- signal.notify();
751
- }
752
- update(now, positions) {
753
- const raceEvents = super.update(now, positions);
754
- if (raceEvents.length > 0)
755
- signal.notify();
756
- return raceEvents;
757
- }
758
- }
759
- const raceStates = new Map();
760
- function raceState(id, config) {
761
- const existing = raceStates.get(id);
762
- if (existing !== undefined)
763
- return existing;
764
- if (config === undefined) {
765
- throw new Error(`race "${id}" has not been created yet; pass a config on first access`);
766
- }
767
- const created = new NotifyingRaceState(config);
768
- raceStates.set(id, created);
769
- return created;
770
- }
567
+ const { pile, loop, raceState, cardPiles, turnLoops } = createContextRegistries(signal.notify);
771
568
  const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
772
569
  const input = createInputSnapshot();
773
570
  const motionByUser = new Map();
@@ -779,53 +576,116 @@ export function createGameContext(options) {
779
576
  }
780
577
  return queue;
781
578
  }
579
+ const featureDeps = {
580
+ features,
581
+ signalNotify: signal.notify,
582
+ events,
583
+ now,
584
+ entities,
585
+ spatial,
586
+ store,
587
+ commandRegistry,
588
+ economy,
589
+ content,
590
+ activeUserId,
591
+ walletOf,
592
+ setWallet: (userId, state) => wallets.set(userId, state),
593
+ layouts,
594
+ inventoryFor,
595
+ ensureInstanceStats,
596
+ seedUserPool,
597
+ sharedSocial,
598
+ pile,
599
+ loop,
600
+ cardPiles,
601
+ turnLoops,
602
+ raceState,
603
+ feature: featureValue,
604
+ };
605
+ const featureReplicateModules = [];
606
+ const featureSaveModules = [];
607
+ for (const descriptor of featureDescriptors) {
608
+ if (!descriptor.enabled(features))
609
+ continue;
610
+ const build = descriptor.create(featureDeps);
611
+ featureRegistry.set(descriptor.key, build.value);
612
+ if (build.replicate !== undefined)
613
+ featureReplicateModules.push(build.replicate);
614
+ if (build.save !== undefined)
615
+ featureSaveModules.push(build.save);
616
+ }
617
+ const baselineDeps = {
618
+ signalNotify: signal.notify,
619
+ entities,
620
+ statsByInstance,
621
+ store,
622
+ feed,
623
+ inventoryIds,
624
+ inventoryByUser,
625
+ inventoryFor,
626
+ wallets,
627
+ time,
628
+ pose,
629
+ possession,
630
+ motionByUser,
631
+ motionFor,
632
+ };
633
+ const baselineBuilds = baselineDescriptors.map((descriptor) => descriptor.create(baselineDeps));
782
634
  const snapshotModules = [
783
- { key: "entities", snapshot: () => entities.snapshot(), hydrate: (data) => entities.hydrate(data) },
784
- {
785
- key: "stats",
786
- snapshot: () => snapshotEntityStats(statsByInstance),
787
- hydrate: (data) => hydrateEntityStats(statsByInstance, data),
788
- },
789
- {
790
- key: "store",
791
- snapshot: () => store.snapshot(),
792
- hydrate: (data) => store.hydrate(data),
793
- },
794
- { key: "feed", snapshot: () => feed.snapshot(), hydrate: (data) => feed.hydrate(data) },
795
- {
796
- key: "inventory",
797
- snapshot: () => {
798
- const byUser = {};
799
- for (const [userId, set] of inventoryByUser) {
800
- const states = {};
801
- for (const inventoryId of inventoryIds)
802
- states[inventoryId] = set.state(inventoryId);
803
- byUser[userId] = states;
804
- }
805
- return byUser;
806
- },
807
- hydrate: (data) => {
808
- for (const [userId, states] of Object.entries(data)) {
809
- const set = inventoryFor(userId);
810
- for (const [inventoryId, state] of Object.entries(states))
811
- set.replaceState(inventoryId, state);
812
- }
813
- },
814
- },
635
+ ...baselineBuilds.flatMap((build) => (build.replicate === undefined ? [] : [build.replicate])),
636
+ ...featureReplicateModules,
815
637
  ];
816
- if (leaderboard) {
817
- snapshotModules.push({
818
- key: "leaderboard",
819
- snapshot: () => leaderboard.snapshot(),
820
- hydrate: (data) => leaderboard.hydrate(data),
821
- });
638
+ const replication = options.replication;
639
+ const projectsViewers = policyProjectsViewers(replication);
640
+ const aoiRadius = replication?.aoiRadius;
641
+ const projectorFor = (key) => {
642
+ if (aoiRadius !== undefined && key === "entities") {
643
+ return (data, viewer) => projectEntitiesForViewer(data, viewer, aoiRadius);
644
+ }
645
+ if (aoiRadius !== undefined && key === "stats") {
646
+ return (data, viewer, world) => projectByVisibleIds(data, visibleEntityIds((world["entities"] ?? []), viewer, aoiRadius));
647
+ }
648
+ if (replication?.privatePerUser === true && key === "inventory") {
649
+ return (data, viewer) => projectPerUserForViewer(data, viewer);
650
+ }
651
+ return undefined;
652
+ };
653
+ const replicationModules = snapshotModules.map((module) => {
654
+ const project = projectorFor(module.key);
655
+ return {
656
+ ...module,
657
+ version: () => signal.version(),
658
+ ...(project === undefined ? {} : { project }),
659
+ };
660
+ });
661
+ const replicationVersion = () => replicationModules.reduce((sum, module) => sum + (module.version?.() ?? 0), 0);
662
+ /**
663
+ * The whole-world *save* set is a superset of the *replication* set (`snapshotModules`): it adds the
664
+ * always-on baseline subsystems (economy, time, pose, possession, motion) plus every opted-in
665
+ * feature's `save` module (unlocks, roster, quest, cosmetics, cards, turn) — the persistent state a
666
+ * single-player save must restore but a host does not replicate to clients. Coverage is owned per
667
+ * subsystem, not hand-listed against a drifting feature manifest: each feature descriptor emits its
668
+ * own `save`, so a new persistent feature can't silently fall out of the save. Keeping the two sets
669
+ * distinct leaves `ctx.snapshot()`/`ctx.hydrate()` — the host→client payload — byte-identical for
670
+ * every multiplayer game, while `ctx.game.save` still persists everything.
671
+ * Mutable so game systems can register save modules at install time via `ctx.game.registerSave`.
672
+ */
673
+ const saveModules = [
674
+ ...snapshotModules,
675
+ ...baselineBuilds.flatMap((build) => (build.save === undefined ? [] : [build.save])),
676
+ ...featureSaveModules,
677
+ ];
678
+ function registerSave(module) {
679
+ saveModules.push(module);
822
680
  }
823
- if (chat) {
824
- snapshotModules.push({
825
- key: "chat",
826
- snapshot: () => chat.snapshot(),
827
- hydrate: (data) => chat.hydrate(data),
681
+ function registerReplicate(module) {
682
+ const project = projectorFor(module.key);
683
+ replicationModules.push({
684
+ ...module,
685
+ version: () => signal.version(),
686
+ ...(project === undefined ? {} : { project }),
828
687
  });
688
+ saveModules.push(module);
829
689
  }
830
690
  const ctx = {
831
691
  scene: {
@@ -838,12 +698,16 @@ export function createGameContext(options) {
838
698
  setPoseConstraint: entities.setPoseConstraint,
839
699
  get: entities.get,
840
700
  list: entities.list,
701
+ ids: entities.ids,
702
+ subscribeMembership: entities.subscribeMembership,
841
703
  spawnPoseOf: entities.spawnPoseOf,
842
704
  resetToSpawn: entities.resetToSpawn,
843
705
  resetAllToSpawn,
844
706
  blackboard: entities.blackboard,
845
707
  stats: entityStats,
846
708
  floatText: emitFloatText,
709
+ vfx: emitVfx,
710
+ vfxInstance: vfxInstances,
847
711
  telegraph: fireTelegraph,
848
712
  hitReaction: applyHitReaction,
849
713
  setTarget: targeting.setTarget,
@@ -860,6 +724,7 @@ export function createGameContext(options) {
860
724
  hasLineOfSight: spatial.hasLineOfSight,
861
725
  queryArc: spatial.queryArc,
862
726
  moveToward: spatial.moveToward,
727
+ moveTowardCommit: moveEntityTowardCommit,
863
728
  invalidateSpatial: spatial.invalidate,
864
729
  setColliders(instanceId, colliders) {
865
730
  if (colliders === null)
@@ -869,8 +734,10 @@ export function createGameContext(options) {
869
734
  signal.notify();
870
735
  },
871
736
  collidersOf: entityCollidersOf,
737
+ visualScaleOf: entityVisualScaleOf,
872
738
  form: forms,
873
739
  paint: paintLayer,
740
+ bind,
874
741
  },
875
742
  worldItem: {
876
743
  spawn: spawnWorldItem,
@@ -933,19 +800,22 @@ export function createGameContext(options) {
933
800
  },
934
801
  },
935
802
  loot,
936
- trade,
937
- quest,
938
- social,
939
- chat,
940
- unlocks,
803
+ trade: featureValue("trade"),
804
+ quest: featureValue("quest"),
805
+ social: featureValue("social"),
806
+ chat: featureValue("chat"),
807
+ unlocks: featureValue("unlocks"),
941
808
  economy,
942
- leaderboard,
943
- roster,
809
+ leaderboard: featureValue("leaderboard"),
810
+ roster: featureValue("roster"),
944
811
  store,
945
- cards: features.cards ? { pile } : undefined,
946
- turn: features.turn ? { loop } : undefined,
947
- race: features.race ? { state: raceState } : undefined,
948
- players,
812
+ dialogue: featureValue("dialogue"),
813
+ cards: featureValue("cards"),
814
+ turn: featureValue("turn"),
815
+ race: featureValue("race"),
816
+ players: featureValue("players"),
817
+ registerSave,
818
+ registerReplicate,
949
819
  },
950
820
  player: {
951
821
  get userId() {
@@ -964,7 +834,7 @@ export function createGameContext(options) {
964
834
  applyLoadout: loadouts.applyLoadout,
965
835
  movement: pose,
966
836
  possession,
967
- cosmetics,
837
+ cosmetics: featureValue("cosmetics"),
968
838
  get motion() {
969
839
  return motionFor(activeUserId());
970
840
  },
@@ -988,11 +858,49 @@ export function createGameContext(options) {
988
858
  input,
989
859
  subscribe: signal.subscribe,
990
860
  version: signal.version,
991
- snapshot: () => composeWorldSnapshot(snapshotModules),
861
+ touch: signal.notify,
862
+ snapshot: (viewer) => composeWorldSnapshot(replicationModules, viewer),
992
863
  hydrate(snapshot) {
993
- applyWorldSnapshot(snapshotModules, snapshot);
864
+ applyWorldSnapshot(replicationModules, snapshot);
994
865
  signal.notify();
995
866
  },
867
+ replicationVersion,
868
+ replicatesPerViewer: () => projectsViewers,
996
869
  };
870
+ const saveOptions = resolveSaveOptions(definition, options);
871
+ if (saveOptions !== undefined) {
872
+ const saveTarget = {
873
+ snapshot: () => composeWorldSnapshot(saveModules),
874
+ hydrate: (snapshot) => {
875
+ applyWorldSnapshot(saveModules, snapshot);
876
+ signal.notify();
877
+ },
878
+ subscribe: signal.subscribe,
879
+ };
880
+ ctx.game.save = createRuntimeSave({ target: saveTarget, ...saveOptions });
881
+ }
997
882
  return ctx;
998
883
  }
884
+ function saveKey(name) {
885
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
886
+ return `jgengine:save:${slug === "" ? "game" : slug}`;
887
+ }
888
+ /** Explicit `options.save` wins (the shell can inject any backend, e.g. cloud); otherwise `defineGame({ persist })` auto-wires a localStorage save for offline/single-player worlds only. */
889
+ function resolveSaveOptions(definition, options) {
890
+ if (options.save !== undefined)
891
+ return options.save;
892
+ const persist = definition.persist;
893
+ if (persist === undefined || persist === false)
894
+ return undefined;
895
+ if (!isOffline(definition.multiplayer))
896
+ return undefined;
897
+ const config = persist === true ? {} : persist;
898
+ return {
899
+ backend: config.storage === "memory" ? memorySaveBackend() : localSaveBackend(),
900
+ mode: config.mode,
901
+ key: saveKey(definition.name),
902
+ version: config.version,
903
+ autosaveMs: config.autosaveMs,
904
+ now: options.now,
905
+ };
906
+ }