@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
@@ -122,9 +122,28 @@ export declare function offline(): MultiplayerAdapterConfig;
122
122
  export declare function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null;
123
123
  export declare function multiplayerAdapterKind(multiplayer: unknown): string | null;
124
124
  export type ServersPoolConfig = {
125
+ minPlayersToStart?: number;
126
+ adapter: MultiplayerAdapterConfig;
127
+ } & ({
128
+ topology: "shared";
129
+ maxServers?: number;
130
+ slotsPerServer?: number;
131
+ } | {
132
+ topology?: "rooms";
133
+ maxServers: number;
134
+ slotsPerServer: number;
135
+ });
136
+ /** Resolve the shared-world pool to one unbounded world; room pools keep explicit capacities. */
137
+ export declare function servers(config: ServersPoolConfig): {
138
+ minPlayersToStart?: number;
139
+ adapter: MultiplayerAdapterConfig;
140
+ topology: "rooms" | "shared";
125
141
  maxServers: number;
126
142
  slotsPerServer: number;
143
+ } | {
127
144
  minPlayersToStart?: number;
128
145
  adapter: MultiplayerAdapterConfig;
146
+ topology: "rooms" | "shared";
147
+ maxServers: number;
148
+ slotsPerServer: number;
129
149
  };
130
- export declare function servers(config: ServersPoolConfig): ServersPoolConfig;
@@ -105,6 +105,13 @@ export function adapterOf(multiplayer) {
105
105
  export function multiplayerAdapterKind(multiplayer) {
106
106
  return adapterOf(multiplayer)?.kind ?? null;
107
107
  }
108
+ /** Resolve the shared-world pool to one unbounded world; room pools keep explicit capacities. */
108
109
  export function servers(config) {
109
- return config;
110
+ const shared = config.topology === "shared";
111
+ const maxServers = shared ? 1 : config.maxServers;
112
+ const slotsPerServer = shared ? Number.MAX_SAFE_INTEGER : config.slotsPerServer;
113
+ if (!Number.isSafeInteger(maxServers) || maxServers < 1 || !Number.isSafeInteger(slotsPerServer) || slotsPerServer < 1) {
114
+ throw new RangeError("Server pool capacities must be positive safe integers");
115
+ }
116
+ return { ...config, topology: config.topology ?? "rooms", maxServers, slotsPerServer };
110
117
  }
@@ -1,6 +1,16 @@
1
- import type { ChaseCameraConfig, CinematicCameraConfig } from "../game/playableGame.js";
2
- /** Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11), or a whole driving-feel overlay (speed→FOV, lead, bank, speed shake, drift-lag) applied only while a vehicle is piloted (#1299). */
3
- export type ChaseCameraTuning = Partial<Pick<ChaseCameraConfig, "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse">>;
1
+ import type { ChaseCameraConfig, ChaseView, CinematicCameraConfig } from "../game/playableGame.js";
2
+ /** Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11), a whole driving-feel overlay applied only while a vehicle is piloted (#1299), or a `view` switch between chase and seat cameras. */
3
+ export type ChaseCameraTuning = Partial<Pick<ChaseCameraConfig, "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse" | "distanceBySpeed" | "pitchFollow" | "fovKick" | "lookBackAction" | "collision" | "view">>;
4
+ /** Default order {@link nextChaseView} cycles through. */
5
+ export declare const CHASE_VIEWS: readonly ChaseView[];
6
+ /**
7
+ * The view after `current` in `views`, wrapping at the end; a view missing from the list starts the cycle over.
8
+ * Pair with `setChaseTuning` to bind a "change camera" key:
9
+ * `ctx.camera.setChaseTuning({ ...ctx.camera.chaseTuning(), view: nextChaseView(view) })`.
10
+ *
11
+ * @capability chase-view-cycle cycle a vehicle camera between chase, hood and cockpit views at runtime
12
+ */
13
+ export declare function nextChaseView(current: ChaseView, views?: readonly ChaseView[]): ChaseView;
4
14
  export interface CameraDirector {
5
15
  follow(entityId: string | null): void;
6
16
  /** `undefined` means no runtime override — the shell falls back to the static `playable.camera.followEntityId`. `null` means explicitly follow nothing. */
@@ -10,6 +20,10 @@ export interface CameraDirector {
10
20
  /** Overlay a runtime patch on the chase rig's config — a boss-intro pull-back, drift zoom-out; `null` restores the static config. */
11
21
  setChaseTuning(tuning: ChaseCameraTuning | null): void;
12
22
  chaseTuning(): ChaseCameraTuning | null;
23
+ /** Add a transient FOV impulse in degrees (a landing, a hit, a boost) that the chase rig layers on and decays per `camera.chase.fovKick`. Kicks from one frame sum. */
24
+ kickFov(degrees: number): void;
25
+ /** Drain the FOV impulses queued since the last call; the shell's chase rig calls this once per frame. */
26
+ takeFovKick(): number;
13
27
  subscribe(listener: () => void): () => void;
14
28
  }
15
29
  /** @internal */
@@ -1,9 +1,25 @@
1
+ /** Default order {@link nextChaseView} cycles through. */
2
+ export const CHASE_VIEWS = ["chase", "hood", "cockpit"];
3
+ /**
4
+ * The view after `current` in `views`, wrapping at the end; a view missing from the list starts the cycle over.
5
+ * Pair with `setChaseTuning` to bind a "change camera" key:
6
+ * `ctx.camera.setChaseTuning({ ...ctx.camera.chaseTuning(), view: nextChaseView(view) })`.
7
+ *
8
+ * @capability chase-view-cycle cycle a vehicle camera between chase, hood and cockpit views at runtime
9
+ */
10
+ export function nextChaseView(current, views = CHASE_VIEWS) {
11
+ if (views.length === 0)
12
+ return current;
13
+ const index = views.indexOf(current);
14
+ return views[(index + 1) % views.length];
15
+ }
1
16
  /** @internal */
2
17
  export function createCameraDirector() {
3
18
  const listeners = new Set();
4
19
  let followEntity = undefined;
5
20
  let cinematicConfig = null;
6
21
  let chaseTuningPatch = null;
22
+ let pendingFovKick = 0;
7
23
  function notify() {
8
24
  for (const listener of listeners)
9
25
  listener();
@@ -24,6 +40,15 @@ export function createCameraDirector() {
24
40
  notify();
25
41
  },
26
42
  chaseTuning: () => chaseTuningPatch,
43
+ kickFov(degrees) {
44
+ if (Number.isFinite(degrees))
45
+ pendingFovKick += degrees;
46
+ },
47
+ takeFovKick() {
48
+ const kick = pendingFovKick;
49
+ pendingFovKick = 0;
50
+ return kick;
51
+ },
27
52
  subscribe(listener) {
28
53
  listeners.add(listener);
29
54
  return () => listeners.delete(listener);
@@ -0,0 +1,52 @@
1
+ /** Validated integer quantity or a stable input rejection. */
2
+ export type QuantityResult = {
3
+ ok: true;
4
+ quantity: number;
5
+ } | {
6
+ ok: false;
7
+ reason: "invalid-quantity";
8
+ };
9
+ /** Read an untrusted whole-item quantity without coercion, truncation, or clamping.
10
+ * @capability command-quantity validate bounded whole-item counts at command boundaries
11
+ */
12
+ export declare function readQuantity(value: unknown, options?: {
13
+ min?: number;
14
+ max?: number;
15
+ }): QuantityResult;
16
+ /** Narrow an untrusted value to a plain object, rejecting arrays and null. */
17
+ export declare function isInputRecord(value: unknown): value is Record<string, unknown>;
18
+ /** Read a non-empty string of at most `maxLength` characters, or `null`. */
19
+ export declare function readInputString(value: unknown, options?: {
20
+ maxLength?: number;
21
+ }): string | null;
22
+ /**
23
+ * Read a finite number inside `[min, max]`, or `null`. `NaN` and `±Infinity` are always rejected, so a
24
+ * coordinate can never drive an unbounded loop; `integer` also rejects fractions instead of flooring them.
25
+ * @capability command-number validate finite, bounded numbers and integers at command boundaries
26
+ */
27
+ export declare function readInputNumber(value: unknown, options?: {
28
+ min?: number;
29
+ max?: number;
30
+ integer?: boolean;
31
+ }): number | null;
32
+ /** Read one of a fixed set of values, or `null`. */
33
+ export declare function readInputOneOf<const T extends string | number>(value: unknown, allowed: readonly T[]): T | null;
34
+ /** Read an `{ x, y, z }` point whose axes are finite and within `maxAbs`, or `null`. */
35
+ export declare function readInputPoint3(value: unknown, options?: {
36
+ maxAbs?: number;
37
+ }): {
38
+ x: number;
39
+ y: number;
40
+ z: number;
41
+ } | null;
42
+ /** Read an `{ x, z }` ground point whose axes are finite and within `maxAbs`, or `null`. */
43
+ export declare function readInputPoint2(value: unknown, options?: {
44
+ maxAbs?: number;
45
+ }): {
46
+ x: number;
47
+ z: number;
48
+ } | null;
49
+ /** Read an array of at most `maxItems` entries, each accepted by `readItem`; any rejected entry rejects the whole array. */
50
+ export declare function readInputArray<T>(value: unknown, readItem: (item: unknown) => T | null, options?: {
51
+ maxItems?: number;
52
+ }): T[] | null;
@@ -0,0 +1,71 @@
1
+ /** Read an untrusted whole-item quantity without coercion, truncation, or clamping.
2
+ * @capability command-quantity validate bounded whole-item counts at command boundaries
3
+ */
4
+ export function readQuantity(value, options = {}) {
5
+ const min = options.min ?? 1;
6
+ const max = options.max ?? Number.MAX_SAFE_INTEGER;
7
+ if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max) || min < 0 || max < min) {
8
+ throw new RangeError("Quantity bounds must be nonnegative safe integers in order");
9
+ }
10
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max
11
+ ? { ok: true, quantity: value }
12
+ : { ok: false, reason: "invalid-quantity" };
13
+ }
14
+ /** Narrow an untrusted value to a plain object, rejecting arrays and null. */
15
+ export function isInputRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ /** Read a non-empty string of at most `maxLength` characters, or `null`. */
19
+ export function readInputString(value, options = {}) {
20
+ return typeof value === "string" && value.length > 0 && value.length <= (options.maxLength ?? 256) ? value : null;
21
+ }
22
+ /**
23
+ * Read a finite number inside `[min, max]`, or `null`. `NaN` and `±Infinity` are always rejected, so a
24
+ * coordinate can never drive an unbounded loop; `integer` also rejects fractions instead of flooring them.
25
+ * @capability command-number validate finite, bounded numbers and integers at command boundaries
26
+ */
27
+ export function readInputNumber(value, options = {}) {
28
+ if (typeof value !== "number" || !Number.isFinite(value))
29
+ return null;
30
+ if (options.integer === true && !Number.isSafeInteger(value))
31
+ return null;
32
+ if (value < (options.min ?? -Number.MAX_VALUE) || value > (options.max ?? Number.MAX_VALUE))
33
+ return null;
34
+ return value;
35
+ }
36
+ /** Read one of a fixed set of values, or `null`. */
37
+ export function readInputOneOf(value, allowed) {
38
+ return allowed.includes(value) ? value : null;
39
+ }
40
+ /** Read an `{ x, y, z }` point whose axes are finite and within `maxAbs`, or `null`. */
41
+ export function readInputPoint3(value, options = {}) {
42
+ if (!isInputRecord(value))
43
+ return null;
44
+ const bound = { min: -(options.maxAbs ?? Number.MAX_VALUE), max: options.maxAbs ?? Number.MAX_VALUE };
45
+ const x = readInputNumber(value.x, bound);
46
+ const y = readInputNumber(value.y, bound);
47
+ const z = readInputNumber(value.z, bound);
48
+ return x === null || y === null || z === null ? null : { x, y, z };
49
+ }
50
+ /** Read an `{ x, z }` ground point whose axes are finite and within `maxAbs`, or `null`. */
51
+ export function readInputPoint2(value, options = {}) {
52
+ if (!isInputRecord(value))
53
+ return null;
54
+ const bound = { min: -(options.maxAbs ?? Number.MAX_VALUE), max: options.maxAbs ?? Number.MAX_VALUE };
55
+ const x = readInputNumber(value.x, bound);
56
+ const z = readInputNumber(value.z, bound);
57
+ return x === null || z === null ? null : { x, z };
58
+ }
59
+ /** Read an array of at most `maxItems` entries, each accepted by `readItem`; any rejected entry rejects the whole array. */
60
+ export function readInputArray(value, readItem, options = {}) {
61
+ if (!Array.isArray(value) || value.length > (options.maxItems ?? 256))
62
+ return null;
63
+ const items = [];
64
+ for (const entry of value) {
65
+ const item = readItem(entry);
66
+ if (item === null)
67
+ return null;
68
+ items.push(item);
69
+ }
70
+ return items;
71
+ }
@@ -12,23 +12,47 @@ export type CommandScope = {
12
12
  /** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */
13
13
  chunkKeys?: readonly string[];
14
14
  };
15
+ /** Declarative read scope; actor is resolved before host hydration. */
16
+ export type CommandScopeDefinition = Omit<CommandScope, "players"> & {
17
+ players?: readonly string[] | "actor";
18
+ };
19
+ /** Who may send a command: any client through the public transport, or only trusted host code. */
20
+ export type CommandAccess = "client" | "server";
21
+ /** Reason every host returns when a command's `parse` rejects its input. */
22
+ export declare const MALFORMED_COMMAND_INPUT_REASON = "Malformed command input";
23
+ /** Reason a public transport returns for a {@link CommandDef} marked `access: "server"`. */
24
+ export declare const SERVER_ONLY_COMMAND_REASON = "Command is server-only";
15
25
  export type CommandDef<TInput = unknown> = {
26
+ /**
27
+ * `"server"` when `input` carries facts only the host may decide (unlock state, prices, multipliers):
28
+ * public transports then refuse it and only trusted host calls such as `helpers.runCommand` run it.
29
+ * Defaults to `"client"`, so every field of `input` must be validated as untrusted.
30
+ */
31
+ access?: CommandAccess;
32
+ /**
33
+ * Turns the untrusted wire value into the command's input, or `null` to refuse it with
34
+ * {@link MALFORMED_COMMAND_INPUT_REASON} before `scope`, `validate` or `apply` run. Those then only ever see
35
+ * parsed input, so a field checked here can't be skipped in one of them. Build it from the readers in
36
+ * `runtime/commandInput`; declare typed commands with {@link defineCommand}.
37
+ */
38
+ parse?: (input: unknown) => TInput | null;
16
39
  /**
17
40
  * What this command reads and writes, derived from its own input. A host that hydrates through a
18
41
  * scope loads only this slice instead of the whole world, and refuses the command if `apply` then
19
42
  * dirties a player or chunk the scope did not name — writing an unhydrated chunk would overwrite
20
- * the stored one with a blank. Omit it to keep the whole-world default.
43
+ * the stored one with a blank. Omit it to load only the actor and no chunks; return `{}` to request the whole world.
21
44
  */
22
- scope?: (input: TInput, actorUserId: string) => CommandScope;
23
- validate: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string) => CommandValidationError | null;
24
- apply: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string) => import("./snapshot.js").GameRuntimeSnapshot;
45
+ scope?: (input: TInput, actorUserId: string) => CommandScopeDefinition;
46
+ /** `nowMs` is the host wall clock, in ms — read it rather than a `now` the client put in `input`. */
47
+ validate: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string, nowMs: number) => CommandValidationError | null;
48
+ apply: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string, nowMs: number) => import("./snapshot.js").GameRuntimeSnapshot;
25
49
  };
26
50
  /**
27
- * The scope a command declares for this input, or `undefined` when it declares none (hydrate everything).
51
+ * The resolved scope, defaulting to the actor and no chunks.
28
52
  * Evaluate it before hydration: the actor and the command's own input are both known by then.
29
53
  * @internal
30
54
  */
31
- export declare function resolveCommandScope<TInput>(commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): CommandScope | undefined;
55
+ export declare function resolveCommandScope<TInput>(commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): CommandScope;
32
56
  /**
33
57
  * Widen a scope so the actor is always hydrated. A command's own player row is dirtied by
34
58
  * {@link runCommand} whatever the command did, and an unhydrated player is dropped from the persist.
@@ -49,4 +73,12 @@ export type RunCommandResult = {
49
73
  reason: string;
50
74
  };
51
75
  /** @internal */
52
- export declare function runCommand<TInput>(snapshot: import("./snapshot.js").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): RunCommandResult;
76
+ export declare function runCommand<TInput>(snapshot: import("./snapshot.js").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string, nowMs?: number): RunCommandResult;
77
+ /**
78
+ * Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and
79
+ * `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
80
+ * @capability command-parse parse untrusted command input once so every command stage sees typed, bounded values
81
+ */
82
+ export declare function defineCommand<TInput>(definition: CommandDef<TInput> & {
83
+ parse: (input: unknown) => TInput | null;
84
+ }): CommandDef;
@@ -1,10 +1,25 @@
1
+ /** Reason every host returns when a command's `parse` rejects its input. */
2
+ export const MALFORMED_COMMAND_INPUT_REASON = "Malformed command input";
3
+ /** Reason a public transport returns for a {@link CommandDef} marked `access: "server"`. */
4
+ export const SERVER_ONLY_COMMAND_REASON = "Command is server-only";
1
5
  /**
2
- * The scope a command declares for this input, or `undefined` when it declares none (hydrate everything).
6
+ * The resolved scope, defaulting to the actor and no chunks.
3
7
  * Evaluate it before hydration: the actor and the command's own input are both known by then.
4
8
  * @internal
5
9
  */
6
10
  export function resolveCommandScope(commands, commandName, input, actorUserId) {
7
- return commands[commandName]?.scope?.(input, actorUserId);
11
+ const command = commands[commandName];
12
+ let declared;
13
+ if (command?.parse === undefined) {
14
+ declared = command?.scope?.(input, actorUserId);
15
+ }
16
+ else {
17
+ const parsed = command.parse(input);
18
+ declared = parsed === null ? undefined : command.scope?.(parsed, actorUserId);
19
+ }
20
+ const scope = declared ?? { players: "actor", chunkKeys: [] };
21
+ const { players, ...rest } = scope;
22
+ return players === undefined ? rest : { ...rest, players: players === "actor" ? [actorUserId] : players };
8
23
  }
9
24
  /**
10
25
  * Widen a scope so the actor is always hydrated. A command's own player row is dirtied by
@@ -35,16 +50,23 @@ export function commandScopeEscape(scope, dirty, commandName) {
35
50
  return `Command "${commandName}" wrote outside its declared scope: ${parts.join("; ")}`;
36
51
  }
37
52
  /** @internal */
38
- export function runCommand(snapshot, commands, commandName, input, actorUserId) {
53
+ export function runCommand(snapshot, commands, commandName, input, actorUserId, nowMs = Date.now()) {
39
54
  const command = commands[commandName];
40
55
  if (!command) {
41
56
  return { ok: false, reason: `Unknown command: ${commandName}` };
42
57
  }
43
- const validationError = command.validate(snapshot, input, actorUserId);
58
+ let parsed = input;
59
+ if (command.parse !== undefined) {
60
+ const result = command.parse(input);
61
+ if (result === null)
62
+ return { ok: false, reason: MALFORMED_COMMAND_INPUT_REASON };
63
+ parsed = result;
64
+ }
65
+ const validationError = command.validate(snapshot, parsed, actorUserId, nowMs);
44
66
  if (validationError) {
45
67
  return { ok: false, reason: validationError.reason };
46
68
  }
47
- const next = command.apply(snapshot, input, actorUserId);
69
+ const next = command.apply(snapshot, parsed, actorUserId, nowMs);
48
70
  return {
49
71
  ok: true,
50
72
  snapshot: {
@@ -60,3 +82,11 @@ export function runCommand(snapshot, commands, commandName, input, actorUserId)
60
82
  },
61
83
  };
62
84
  }
85
+ /**
86
+ * Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and
87
+ * `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
88
+ * @capability command-parse parse untrusted command input once so every command stage sees typed, bounded values
89
+ */
90
+ export function defineCommand(definition) {
91
+ return definition;
92
+ }
@@ -36,6 +36,7 @@ export interface CombatSubsystemDeps {
36
36
  despawnEntity: (instanceId: string) => boolean;
37
37
  runCommand: (name: string, args: unknown) => void;
38
38
  localUserId: string;
39
+ rng: () => number;
39
40
  physics?: PhysicsConfig;
40
41
  }
41
42
  /** @internal Effects, projectiles, death, and combat presentation surface. */
@@ -8,7 +8,7 @@ import { createCombatFx } from "./combatFx.js";
8
8
  import { applyLethalLoot, isLocalPlayerKill } from "./deathLoot.js";
9
9
  /** @internal */
10
10
  export function createCombatSubsystem(d) {
11
- const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand, localUserId, } = d;
11
+ const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand, localUserId, rng, } = d;
12
12
  const death = createDeathSystem({
13
13
  resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
14
14
  resolveIdentity(instanceId) {
@@ -64,6 +64,7 @@ export function createCombatSubsystem(d) {
64
64
  spawnWorldItem,
65
65
  grantToPlayer: loot.grantToPlayer,
66
66
  localUserId,
67
+ rng,
67
68
  });
68
69
  },
69
70
  }), ["applyEffect"], signalNotify);
@@ -18,6 +18,8 @@ export interface LethalLootInput {
18
18
  spawnWorldItem: (input: WorldItemSpawnInput) => void;
19
19
  grantToPlayer: (userId: string, drops: Drop[], source?: string) => void;
20
20
  localUserId: string;
21
+ /** The world's seeded stream — scatter positions must replay with the rest of the simulation. */
22
+ rng: () => number;
21
23
  }
22
24
  /**
23
25
  * Pure death→loot policy: when a kill yields drops for the local player, either scatter them as
@@ -14,6 +14,7 @@ export function applyLethalLoot(input) {
14
14
  const resolved = resolveDeathDrops([...input.drops], {
15
15
  mode: "world",
16
16
  origin: input.position,
17
+ rng: input.rng,
17
18
  resolveRarity: (itemId) => input.content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
18
19
  resolveBaseType: (itemId) => input.content.itemById?.(itemId)?.baseType ?? itemId,
19
20
  scatter: normalizedOnDeath.scatter,
@@ -1,6 +1,7 @@
1
1
  import { createCommandRegistry } from "../commands/commandRegistry.js";
2
2
  import {} from "../game/cosmetics.js";
3
3
  import { groundFieldFor } from "../world/terrain.js";
4
+ import { createWorldSolids, structureSolids } from "../world/worldSolids.js";
4
5
  import { createGameEvents } from "../game/events.js";
5
6
  import { createGameFeed } from "../game/feed.js";
6
7
  import { setGamePhase } from "../game/gamePhase.js";
@@ -19,8 +20,10 @@ import { createRuntimeSave } from "./runtimeSave.js";
19
20
  import { isOffline } from "./adapter.js";
20
21
  import { localSaveBackend, memorySaveBackend } from "../game/saveStore.js";
21
22
  import { createSimClock } from "../time/simClock.js";
23
+ import { createSimContext } from "./simContext.js";
22
24
  import { seededRng } from "../random/rng.js";
23
25
  import { createCameraDirector } from "./cameraDirector.js";
26
+ import { createParticleDirector } from "../vfx/particleDirector.js";
24
27
  import { createInputSnapshot } from "./inputSnapshot.js";
25
28
  import { baselineDescriptors } from "./descriptors/baseline.js";
26
29
  import { featureDescriptors } from "./descriptors/features.js";
@@ -41,6 +44,10 @@ export function createGameContext(options) {
41
44
  const activeUserId = () => actingUserId ?? player.userId;
42
45
  const time = createSimClock({ config: definition.time, onChange: signal.notify });
43
46
  const ground = groundFieldFor(definition.world);
47
+ const solids = createWorldSolids();
48
+ if (definition.world?.kind === "environment") {
49
+ solids.set("environment:structures", structureSolids(definition.world, ground.sampleHeight));
50
+ }
44
51
  const rawEvents = createGameEvents();
45
52
  const events = {
46
53
  on: rawEvents.on,
@@ -213,6 +220,7 @@ export function createGameContext(options) {
213
220
  commandRegistry.run(ctxRef, name, args);
214
221
  },
215
222
  localUserId: player.userId,
223
+ rng,
216
224
  ...(definition.physics !== undefined ? { physics: definition.physics } : {}),
217
225
  });
218
226
  scene.setOnAfterSpawn((instanceId) => combat.death.revive(instanceId));
@@ -220,6 +228,7 @@ export function createGameContext(options) {
220
228
  const store = notifyAfter(createObservableKeyedStore(), ["set", "delete", "hydrate"], signal.notify);
221
229
  const { pile, loop, raceState, cardPiles, turnLoops } = createContextRegistries(signal.notify);
222
230
  const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
231
+ const particles = createParticleDirector();
223
232
  const input = createInputSnapshot();
224
233
  // --- Descriptor install (single feature enable path) ---
225
234
  const featureDeps = {
@@ -289,7 +298,7 @@ export function createGameContext(options) {
289
298
  ...featureReplicateModules,
290
299
  ];
291
300
  const replication = options.replication;
292
- const projectsViewers = policyProjectsViewers(replication);
301
+ const projectsViewers = policyProjectsViewers(replication) || snapshotModules.some((module) => module.priority !== undefined);
293
302
  const aoiRadius = replication?.aoiRadius;
294
303
  const typedProject = (project) => (data, viewer, world) => project(data, viewer, world);
295
304
  const projectorFor = (key) => {
@@ -403,8 +412,10 @@ export function createGameContext(options) {
403
412
  world: {
404
413
  ground,
405
414
  groundHeightAt: ground.sampleHeight,
415
+ solids,
406
416
  },
407
417
  game: {
418
+ territory: options.territory,
408
419
  commands: {
409
420
  define: commandRegistry.define,
410
421
  has: commandRegistry.has,
@@ -496,7 +507,9 @@ export function createGameContext(options) {
496
507
  weapon,
497
508
  },
498
509
  time,
510
+ sim: createSimContext({ config: definition.simulation, entities }),
499
511
  camera,
512
+ particles,
500
513
  input,
501
514
  subscribe: signal.subscribe,
502
515
  version: signal.version,
@@ -510,6 +523,10 @@ export function createGameContext(options) {
510
523
  replicatesPerViewer: () => projectsViewers,
511
524
  };
512
525
  ctxRef = ctx;
526
+ const physicsBackend = definition.physics?.backend;
527
+ if (physicsBackend !== undefined) {
528
+ ctx.sim.addStage({ id: "physics", phase: "afterMovement", run: (dt) => physicsBackend.step(dt) });
529
+ }
513
530
  const saveOptions = resolveSaveOptions(definition, options);
514
531
  if (saveOptions !== undefined) {
515
532
  const saveTarget = {
@@ -1,3 +1,5 @@
1
+ import type { Territory } from "../world/territory.js";
2
+ import type { CurrencyDefinition } from "../economy/currency.js";
1
3
  import type { CardPile, CardPileConfig } from "../cards/cardPile.js";
2
4
  import type { EffectInput, EffectResult, EffectVia, ReceiveMap, SingleTargetEffectInput } from "../combat/effects.js";
3
5
  import type { ProjectileSystem } from "../combat/projectiles.js";
@@ -9,6 +11,7 @@ import type { ChargeOptions as WalletChargeOptions } from "../economy/wallet.js"
9
11
  import type { Cosmetics } from "../game/cosmetics.js";
10
12
  import type { GameDefinition } from "../game/defineGame.js";
11
13
  import type { TerrainField } from "../world/terrain.js";
14
+ import type { WorldSolids } from "../world/worldSolids.js";
12
15
  import type { GameEventMap, GameEvents, VfxKind } from "../game/events.js";
13
16
  import type { VfxInstanceStore } from "../game/vfxInstance.js";
14
17
  import type { GameFeed } from "../game/feed.js";
@@ -52,9 +55,11 @@ import type { SnapshotModule, SnapshotViewer, WorldSnapshot } from "./worldSnaps
52
55
  import type { ReplicationPolicy } from "./worldProjection.js";
53
56
  import type { RuntimeSave, RuntimeSaveOptions } from "./runtimeSave.js";
54
57
  import type { SimClock } from "../time/simClock.js";
58
+ import type { SimContext } from "./simContext.js";
55
59
  import type { TurnLoop, TurnLoopConfig } from "../turn/turnLoop.js";
56
60
  import type { RaceState, RaceStateConfig } from "../game/race.js";
57
61
  import type { CameraDirector } from "./cameraDirector.js";
62
+ import type { ParticleDirector } from "../vfx/particleDirector.js";
58
63
  import type { InputSnapshot } from "./inputSnapshot.js";
59
64
  import type { MotionIntents } from "./motionIntents.js";
60
65
  import type { OnDeathSpec } from "../combat/death.js";
@@ -111,6 +116,7 @@ export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAsset
111
116
  userId: string;
112
117
  isNew: boolean;
113
118
  };
119
+ territory?: Territory;
114
120
  now?: () => number;
115
121
  occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
116
122
  /**
@@ -339,6 +345,11 @@ export type WorldItemPickupResult = {
339
345
  status: "rejected";
340
346
  reason: string;
341
347
  };
348
+ /**
349
+ * `ctx.scene.worldItem`: items lying in the world — spawn, list, find the nearest, and pick up into a player's bag.
350
+ *
351
+ * @capability world-item-pickup pick up dropped items: ctx.scene.worldItem nearestInRadius → pickup into the player's inventory
352
+ */
342
353
  export interface SceneWorldItemContext {
343
354
  spawn(input: WorldItemSpawnInput): WorldItemRecord;
344
355
  get(instanceId: string): WorldItemRecord | null;
@@ -367,14 +378,14 @@ export interface GameContextLoot {
367
378
  grantToPlayer(userId: string, drops: Drop[], source?: string): void;
368
379
  }
369
380
  export interface GameContextEconomy {
370
- balance(userId: string, currencyId: string): number;
371
- grant(userId: string, currencyId: string, amount: number): void;
381
+ balance(userId: string, currencyId: string | CurrencyDefinition): number;
382
+ grant(userId: string, currencyId: string | CurrencyDefinition, amount: number): void;
372
383
  /** `options.overdraft` opts this charge into carrying a negative balance (`true` unlimited, `{ max }` capped) — omitted keeps the strict no-debt default. */
373
- charge(userId: string, currencyId: string, amount: number, options?: WalletChargeOptions): {
384
+ charge(userId: string, currencyId: string | CurrencyDefinition, amount: number, options?: WalletChargeOptions): {
374
385
  reason: string;
375
386
  } | null;
376
387
  /** True once `balance(userId, currencyId)` has gone negative under an overdraft-enabled charge. */
377
- isOverdrawn(userId: string, currencyId: string): boolean;
388
+ isOverdrawn(userId: string, currencyId: string | CurrencyDefinition): boolean;
378
389
  }
379
390
  export interface GameContextItemUse {
380
391
  register(handlers: Record<string, ItemUseHandler<GameContext>>): void;
@@ -385,6 +396,11 @@ export interface GameContextItemUse {
385
396
  export interface GameContextWorld {
386
397
  ground: TerrainField;
387
398
  groundHeightAt(x: number, z: number): number;
399
+ /**
400
+ * Static collision for world geometry that is not a scene object. `environment({ structures })`
401
+ * fills the `environment:structures` layer; `syncAuthoredSolids` fills one layer per studio object.
402
+ */
403
+ solids: WorldSolids;
388
404
  }
389
405
  export interface GameContextCards {
390
406
  /** Lazily creates (on first call, `config` required) or returns the existing notify-wrapped pile for `id`. */
@@ -415,14 +431,19 @@ export interface GameAudio {
415
431
  }): void;
416
432
  /**
417
433
  * Live-update retained loop `id` via `audio.loopSet`: `rate` re-pitches it (1 = authored pitch, clamped
418
- * 0.25–4 by the shell), `gain` rescales volume (0–1), `at` repositions its emitter. Cheap to call every
419
- * tick (~60 Hz) — the shell ramps rate/gain over ~20 ms to avoid zipper noise. A no-op when `id` is not a
420
- * live loop (an update may race a stop) (#1051).
434
+ * 0.25–4 by the shell), `gain` rescales volume (0–1), `at` repositions its emitter. `lowpass`/`highpass`
435
+ * set filter cutoffs in Hz (muffle an engine off-throttle or behind a wall); `velocity` is the emitter's
436
+ * world velocity, which pitches the loop by doppler when its sound declares `doppler`. Cheap to call every
437
+ * tick (~60 Hz) — the shell ramps rate/gain/cutoffs over ~20 ms to avoid zipper noise. A no-op when `id`
438
+ * is not a live loop (an update may race a stop) (#1051).
421
439
  */
422
440
  setLoop(id: string, params: {
423
441
  rate?: number;
424
442
  gain?: number;
425
443
  at?: readonly [number, number, number];
444
+ velocity?: readonly [number, number, number];
445
+ lowpass?: number;
446
+ highpass?: number;
426
447
  }): void;
427
448
  /** Stop and dispose retained loop `id` (emits `audio.loopStop`); an unknown `id` is ignored (#1051). */
428
449
  stopLoop(id: string): void;
@@ -446,6 +467,7 @@ export interface GameContext {
446
467
  */
447
468
  rng: () => number;
448
469
  game: {
470
+ territory?: Territory;
449
471
  commands: GameContextCommands;
450
472
  events: GameEvents;
451
473
  audio: GameAudio;
@@ -523,8 +545,12 @@ export interface GameContext {
523
545
  weapon: WeaponStats;
524
546
  };
525
547
  time: SimClock;
548
+ /** The simulation stepper (fixed or variable) every driver advances through, plus render-side pose interpolation and per-step stages. */
549
+ sim: SimContext;
526
550
  /** Runtime camera-follow/cinematic override (#196.2); the shell reads `followedEntityId()` each frame. */
527
551
  camera: CameraDirector;
552
+ /** Particle VFX seam (#1659): queue bursts / standing emitters as data; the shell renders them quality-capped. */
553
+ particles: ParticleDirector;
528
554
  /** Held-input snapshot published by the shell each frame before `onTick` (#164.1). */
529
555
  input: InputSnapshot;
530
556
  subscribe(listener: () => void): () => void;