@jgengine/core 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/CHANGELOG.md +265 -2
  2. package/dist/ai/decisionGraph.d.ts +96 -0
  3. package/dist/ai/decisionGraph.js +212 -0
  4. package/dist/ai/groupAssist.d.ts +10 -0
  5. package/dist/ai/groupAssist.js +11 -0
  6. package/dist/ai/mobBrainGraph.d.ts +3 -0
  7. package/dist/ai/mobBrainGraph.js +25 -0
  8. package/dist/ai/pursuit.d.ts +1 -1
  9. package/dist/ai/pursuit.js +1 -1
  10. package/dist/ai/tacticalQueries.d.ts +30 -0
  11. package/dist/ai/tacticalQueries.js +45 -0
  12. package/dist/ai/threat.d.ts +8 -0
  13. package/dist/ai/threat.js +10 -0
  14. package/dist/anim/animGraph.d.ts +155 -0
  15. package/dist/anim/animGraph.js +414 -0
  16. package/dist/anim/boneTexture.d.ts +55 -0
  17. package/dist/anim/boneTexture.js +43 -0
  18. package/dist/anim/footPlacement.d.ts +57 -0
  19. package/dist/anim/footPlacement.js +67 -0
  20. package/dist/anim/ikSolver.d.ts +51 -0
  21. package/dist/anim/ikSolver.js +124 -0
  22. package/dist/anim/locomotionGraph.d.ts +35 -0
  23. package/dist/anim/locomotionGraph.js +62 -0
  24. package/dist/assets/spriteAtlas.d.ts +21 -0
  25. package/dist/assets/spriteAtlas.js +103 -0
  26. package/dist/audio/audioFalloff.d.ts +12 -0
  27. package/dist/audio/doppler.d.ts +21 -0
  28. package/dist/audio/doppler.js +32 -0
  29. package/dist/audio/engineLayers.d.ts +83 -0
  30. package/dist/audio/engineLayers.js +115 -0
  31. package/dist/combat/abilityKit.d.ts +1 -0
  32. package/dist/combat/abilityKit.js +1 -0
  33. package/dist/combat/death.d.ts +7 -0
  34. package/dist/combat/shotOrigin.d.ts +6 -0
  35. package/dist/combat/shotOrigin.js +6 -0
  36. package/dist/combat/weaponFeedback.d.ts +36 -0
  37. package/dist/combat/weaponFeedback.js +42 -0
  38. package/dist/combat/weaponHandling.d.ts +161 -0
  39. package/dist/combat/weaponHandling.js +202 -0
  40. package/dist/combat/weaponPresentation.d.ts +115 -0
  41. package/dist/combat/weaponPresentation.js +96 -0
  42. package/dist/devtools/warnOnce.d.ts +8 -0
  43. package/dist/devtools/warnOnce.js +18 -0
  44. package/dist/economy/currency.d.ts +16 -2
  45. package/dist/economy/currency.js +55 -14
  46. package/dist/economy/wallet.d.ts +5 -4
  47. package/dist/economy/wallet.js +8 -4
  48. package/dist/editor/commandApply.js +1 -0
  49. package/dist/editor/commands.d.ts +4 -1
  50. package/dist/editor/document.d.ts +2 -1
  51. package/dist/editor/document.js +92 -1
  52. package/dist/editor/environment.d.ts +3 -0
  53. package/dist/editor/environment.js +24 -0
  54. package/dist/editor/index.d.ts +4 -2
  55. package/dist/editor/index.js +1 -1
  56. package/dist/editor/types.d.ts +20 -0
  57. package/dist/faction/reputation.d.ts +6 -0
  58. package/dist/faction/reputation.js +11 -0
  59. package/dist/game/cameraConfig.d.ts +59 -1
  60. package/dist/game/chat.d.ts +22 -1
  61. package/dist/game/chat.js +48 -19
  62. package/dist/game/controlGate.d.ts +6 -0
  63. package/dist/game/controlGate.js +30 -1
  64. package/dist/game/defineGame.d.ts +10 -1
  65. package/dist/game/events.d.ts +14 -1
  66. package/dist/game/lootTable.d.ts +4 -0
  67. package/dist/game/lootTable.js +16 -0
  68. package/dist/game/pixelPerfect.d.ts +25 -0
  69. package/dist/game/pixelPerfect.js +29 -0
  70. package/dist/game/playableGame.d.ts +167 -6
  71. package/dist/game/systemRuntime.js +6 -1
  72. package/dist/game/toasts.d.ts +8 -0
  73. package/dist/game/toasts.js +7 -0
  74. package/dist/game/vfxInstance.d.ts +9 -0
  75. package/dist/game/vfxInstance.js +12 -1
  76. package/dist/game/viewports.d.ts +43 -0
  77. package/dist/game/viewports.js +46 -0
  78. package/dist/game/worldItem.d.ts +5 -1
  79. package/dist/game/worldItem.js +0 -1
  80. package/dist/input/actionBindings.d.ts +17 -2
  81. package/dist/input/actionBindings.js +24 -11
  82. package/dist/input/actionContexts.d.ts +45 -0
  83. package/dist/input/actionContexts.js +113 -0
  84. package/dist/input/axisInput.d.ts +4 -0
  85. package/dist/input/axisInput.js +4 -0
  86. package/dist/input/axisShaper.d.ts +73 -0
  87. package/dist/input/axisShaper.js +105 -0
  88. package/dist/input/gamepadModel.d.ts +79 -0
  89. package/dist/input/gamepadModel.js +108 -0
  90. package/dist/input/haptics.d.ts +50 -0
  91. package/dist/input/haptics.js +74 -0
  92. package/dist/input/inputBuffer.d.ts +40 -0
  93. package/dist/input/inputBuffer.js +59 -0
  94. package/dist/input/lookChannel.d.ts +10 -0
  95. package/dist/input/lookChannel.js +6 -0
  96. package/dist/input/pointer.d.ts +2 -0
  97. package/dist/input/pointer.js +4 -0
  98. package/dist/item/affix.d.ts +6 -0
  99. package/dist/item/affix.js +6 -0
  100. package/dist/meta/changelog.d.ts +1 -1
  101. package/dist/meta/changelog.js +196 -1
  102. package/dist/movement/characterController.d.ts +76 -0
  103. package/dist/movement/characterController.js +216 -0
  104. package/dist/movement/freeFlight.d.ts +119 -0
  105. package/dist/movement/freeFlight.js +294 -0
  106. package/dist/movement/glideModel.d.ts +3 -0
  107. package/dist/movement/glideModel.js +6 -0
  108. package/dist/movement/grappleSwing.d.ts +2 -0
  109. package/dist/movement/grappleSwing.js +5 -1
  110. package/dist/movement/movementModel.d.ts +39 -0
  111. package/dist/movement/movementModel.js +62 -19
  112. package/dist/movement/movementProbe.d.ts +51 -0
  113. package/dist/movement/movementProbe.js +119 -0
  114. package/dist/movement/playerMovement.d.ts +30 -3
  115. package/dist/movement/playerMovement.js +304 -7
  116. package/dist/movement/serverStep.d.ts +5 -0
  117. package/dist/movement/serverStep.js +6 -0
  118. package/dist/movement/solidObstacles.d.ts +39 -8
  119. package/dist/movement/solidObstacles.js +71 -6
  120. package/dist/movement/voxelController.js +7 -7
  121. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  122. package/dist/multiplayer/poseSyncGate.js +9 -1
  123. package/dist/multiplayer/presenceContract.d.ts +6 -0
  124. package/dist/multiplayer/presenceModel.d.ts +2 -0
  125. package/dist/multiplayer/presenceModel.js +11 -0
  126. package/dist/nav/navFromEnvironment.d.ts +9 -3
  127. package/dist/nav/navFromEnvironment.js +18 -12
  128. package/dist/nav/navMesh.d.ts +74 -0
  129. package/dist/nav/navMesh.js +570 -0
  130. package/dist/physics/aircraftDynamics.d.ts +390 -0
  131. package/dist/physics/aircraftDynamics.js +704 -0
  132. package/dist/physics/boatDynamics.d.ts +116 -0
  133. package/dist/physics/boatDynamics.js +160 -0
  134. package/dist/physics/drivableVehicle.d.ts +22 -8
  135. package/dist/physics/drivableVehicle.js +4 -2
  136. package/dist/physics/flightDynamics.d.ts +29 -4
  137. package/dist/physics/flightDynamics.js +28 -5
  138. package/dist/physics/handlingProbe.d.ts +249 -0
  139. package/dist/physics/handlingProbe.js +606 -0
  140. package/dist/physics/kinematicVehicle.d.ts +37 -2
  141. package/dist/physics/kinematicVehicle.js +15 -0
  142. package/dist/physics/physicsBackend.d.ts +203 -0
  143. package/dist/physics/physicsBackend.js +49 -0
  144. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  145. package/dist/physics/physicsBackendConformance.js +151 -0
  146. package/dist/physics/physicsWorldBackend.d.ts +45 -0
  147. package/dist/physics/physicsWorldBackend.js +518 -0
  148. package/dist/physics/vehicleBackendLink.d.ts +49 -0
  149. package/dist/physics/vehicleBackendLink.js +102 -0
  150. package/dist/physics/vehicleDynamics.d.ts +374 -0
  151. package/dist/physics/vehicleDynamics.js +737 -0
  152. package/dist/physics/worldColliders.d.ts +13 -0
  153. package/dist/physics/worldColliders.js +128 -0
  154. package/dist/puzzle/cellGrid.d.ts +15 -9
  155. package/dist/puzzle/cellGrid.js +13 -9
  156. package/dist/random/rng.d.ts +12 -1
  157. package/dist/random/rng.js +20 -3
  158. package/dist/render/environment.d.ts +23 -0
  159. package/dist/render/environment.js +1 -0
  160. package/dist/render/lookPreset.d.ts +19 -6
  161. package/dist/render/lookPreset.js +56 -4
  162. package/dist/render/postProcessing.d.ts +40 -1
  163. package/dist/render/postProcessing.js +1 -1
  164. package/dist/render/sprite2d.d.ts +38 -0
  165. package/dist/render/sprite2d.js +40 -0
  166. package/dist/runtime/adapter.d.ts +20 -1
  167. package/dist/runtime/adapter.js +8 -1
  168. package/dist/runtime/cameraDirector.d.ts +17 -3
  169. package/dist/runtime/cameraDirector.js +25 -0
  170. package/dist/runtime/commandInput.d.ts +52 -0
  171. package/dist/runtime/commandInput.js +71 -0
  172. package/dist/runtime/commandRunner.d.ts +39 -7
  173. package/dist/runtime/commandRunner.js +35 -5
  174. package/dist/runtime/context/combat.d.ts +1 -0
  175. package/dist/runtime/context/combat.js +2 -1
  176. package/dist/runtime/context/deathLoot.d.ts +2 -0
  177. package/dist/runtime/context/deathLoot.js +1 -0
  178. package/dist/runtime/gameContext.js +18 -1
  179. package/dist/runtime/gameContextTypes.d.ts +33 -7
  180. package/dist/runtime/gameRuntime.d.ts +23 -5
  181. package/dist/runtime/gameRuntime.js +21 -4
  182. package/dist/runtime/headlessRunner.d.ts +10 -0
  183. package/dist/runtime/headlessRunner.js +26 -8
  184. package/dist/runtime/hostPolicy.d.ts +3 -2
  185. package/dist/runtime/hostPolicy.js +8 -10
  186. package/dist/runtime/hostedGameRunner.js +34 -6
  187. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  188. package/dist/runtime/hostedWorldSession.js +29 -6
  189. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  190. package/dist/runtime/hostedWorldStore.js +1 -0
  191. package/dist/runtime/inputRecorder.d.ts +30 -0
  192. package/dist/runtime/inputRecorder.js +59 -0
  193. package/dist/runtime/inputSnapshot.d.ts +14 -0
  194. package/dist/runtime/inputSnapshot.js +11 -0
  195. package/dist/runtime/localPlayers.d.ts +73 -0
  196. package/dist/runtime/localPlayers.js +119 -0
  197. package/dist/runtime/poseInterpolation.d.ts +36 -0
  198. package/dist/runtime/poseInterpolation.js +91 -0
  199. package/dist/runtime/prediction.d.ts +45 -0
  200. package/dist/runtime/prediction.js +62 -0
  201. package/dist/runtime/simContext.d.ts +45 -0
  202. package/dist/runtime/simContext.js +65 -0
  203. package/dist/runtime/simLoop.d.ts +52 -0
  204. package/dist/runtime/simLoop.js +72 -0
  205. package/dist/runtime/simSnapshot.d.ts +32 -0
  206. package/dist/runtime/simSnapshot.js +60 -0
  207. package/dist/runtime/snapshot.d.ts +8 -0
  208. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  209. package/dist/runtime/snapshotBuffer.js +58 -0
  210. package/dist/runtime/transport.d.ts +41 -2
  211. package/dist/runtime/transport.js +7 -1
  212. package/dist/runtime/worldChunks.d.ts +2 -0
  213. package/dist/runtime/worldChunks.js +14 -0
  214. package/dist/runtime/worldProjection.d.ts +9 -0
  215. package/dist/runtime/worldProjection.js +17 -1
  216. package/dist/runtime/worldSnapshot.d.ts +6 -0
  217. package/dist/runtime/worldSnapshot.js +3 -0
  218. package/dist/scene/behaviorRuntime.d.ts +29 -1
  219. package/dist/scene/behaviorRuntime.js +71 -4
  220. package/dist/scene/behaviors.d.ts +12 -1
  221. package/dist/scene/builtinSceneKinds.js +10 -0
  222. package/dist/scene/sceneKinds.d.ts +9 -0
  223. package/dist/scene/vehicleSeat.d.ts +5 -1
  224. package/dist/scene/vehicleSeat.js +5 -1
  225. package/dist/sensor/concealment.d.ts +6 -0
  226. package/dist/sensor/concealment.js +8 -0
  227. package/dist/sensor/freezeMonitor.d.ts +6 -0
  228. package/dist/sensor/freezeMonitor.js +8 -0
  229. package/dist/sensor/perception.d.ts +70 -0
  230. package/dist/sensor/perception.js +189 -0
  231. package/dist/sensor/recordingBuffer.d.ts +3 -0
  232. package/dist/sensor/recordingBuffer.js +8 -0
  233. package/dist/session/roundState.d.ts +4 -0
  234. package/dist/session/roundState.js +11 -0
  235. package/dist/settings/frameRateLimit.d.ts +16 -0
  236. package/dist/settings/frameRateLimit.js +37 -0
  237. package/dist/settings/graphicsProfile.d.ts +21 -0
  238. package/dist/settings/graphicsProfile.js +48 -0
  239. package/dist/settings/graphicsSettings.d.ts +40 -0
  240. package/dist/settings/graphicsSettings.js +81 -0
  241. package/dist/settings/settingsModel.d.ts +9 -1
  242. package/dist/settings/settingsModel.js +10 -0
  243. package/dist/stats/eventMeter.d.ts +1 -1
  244. package/dist/stats/eventMeter.js +1 -1
  245. package/dist/time/accrueSince.d.ts +10 -0
  246. package/dist/time/accrueSince.js +9 -0
  247. package/dist/time/rateWindow.d.ts +16 -0
  248. package/dist/time/rateWindow.js +14 -0
  249. package/dist/time/serverTick.d.ts +21 -3
  250. package/dist/time/serverTick.js +14 -13
  251. package/dist/vfx/feedbackMixer.d.ts +61 -0
  252. package/dist/vfx/feedbackMixer.js +106 -0
  253. package/dist/vfx/particleDirector.d.ts +68 -0
  254. package/dist/vfx/particleDirector.js +81 -0
  255. package/dist/visibility/simulationCulling.d.ts +8 -1
  256. package/dist/visibility/simulationCulling.js +7 -0
  257. package/dist/visibility/spatialIndex.d.ts +21 -0
  258. package/dist/visibility/spatialIndex.js +16 -0
  259. package/dist/world/authoredSolids.d.ts +46 -0
  260. package/dist/world/authoredSolids.js +75 -0
  261. package/dist/world/buildingGenerator.js +2 -2
  262. package/dist/world/buildingKit.d.ts +3 -1
  263. package/dist/world/buildings.d.ts +46 -2
  264. package/dist/world/buildings.js +53 -3
  265. package/dist/world/cityContent.js +103 -25
  266. package/dist/world/cityKind.d.ts +139 -0
  267. package/dist/world/cityKind.js +367 -16
  268. package/dist/world/features.d.ts +56 -3
  269. package/dist/world/features.js +13 -2
  270. package/dist/world/footprintGrid.d.ts +7 -0
  271. package/dist/world/footprintGrid.js +16 -0
  272. package/dist/world/interiors.d.ts +1 -0
  273. package/dist/world/interiors.js +5 -3
  274. package/dist/world/lod.d.ts +6 -0
  275. package/dist/world/lod.js +8 -0
  276. package/dist/world/placement.d.ts +7 -0
  277. package/dist/world/placement.js +2 -0
  278. package/dist/world/placementController.d.ts +1 -1
  279. package/dist/world/placementController.js +7 -0
  280. package/dist/world/raceCircuit.d.ts +168 -0
  281. package/dist/world/raceCircuit.js +797 -0
  282. package/dist/world/terraform.d.ts +8 -0
  283. package/dist/world/terraform.js +7 -0
  284. package/dist/world/terrain.d.ts +5 -1
  285. package/dist/world/terrain.js +5 -0
  286. package/dist/world/territory.d.ts +97 -0
  287. package/dist/world/territory.js +182 -0
  288. package/dist/world/walls.d.ts +7 -0
  289. package/dist/world/walls.js +7 -0
  290. package/dist/world/worldSolids.d.ts +84 -0
  291. package/dist/world/worldSolids.js +240 -0
  292. package/dist/world.d.ts +5 -3
  293. package/dist/world.js +4 -2
  294. package/package.json +1 -1
  295. package/skills/ce-handoff/SKILL.md +2 -2
  296. package/skills/game-design/SKILL.md +11 -1
  297. package/skills/jgengine/SKILL.md +9 -5
  298. package/skills/jgengine/api.md +205 -101
  299. package/skills/jgengine/capabilities.md +56 -0
  300. package/skills/jgengine/recipes/minimal-game.md +3 -0
  301. package/skills/jgengine-combat/SKILL.md +5 -0
  302. package/skills/jgengine-combat/api.md +138 -103
  303. package/skills/jgengine-combat/capabilities.md +29 -1
  304. package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
  305. package/skills/jgengine-gameplay/SKILL.md +10 -0
  306. package/skills/jgengine-gameplay/api.md +501 -453
  307. package/skills/jgengine-gameplay/capabilities.md +36 -0
  308. package/skills/jgengine-gameplay/reference-systems.md +4 -0
  309. package/skills/jgengine-world/SKILL.md +21 -3
  310. package/skills/jgengine-world/api.md +1172 -625
  311. package/skills/jgengine-world/capabilities.md +229 -3
  312. package/skills/jgengine-world/recipes/character-animation.md +39 -0
  313. package/skills/jgengine-world/recipes/controls.md +44 -0
  314. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  315. package/skills/jgengine-world/recipes/npc-senses.md +43 -0
  316. package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
  317. package/skills/jgengine-world/reference.md +31 -2
  318. package/skills/jgengine-world/visibility.md +112 -0
@@ -0,0 +1,40 @@
1
+ import { type GraphicsProfile } from "./graphicsProfile.js";
2
+ import { type GraphicsQuality, type SettingsStore } from "./settingsModel.js";
3
+ /** The post-processing stages a player can toggle individually on top of the quality tier. */
4
+ export type GraphicsPostStage = keyof GraphicsProfile["postStages"];
5
+ /** Ordered stage list used to build settings rows. */
6
+ export declare const GRAPHICS_POST_STAGES: readonly GraphicsPostStage[];
7
+ /** Player-facing labels for each post stage. */
8
+ export declare const GRAPHICS_POST_STAGE_LABELS: Record<GraphicsPostStage, string>;
9
+ /** Setting id that stores one stage toggle (`graphics.post.<stage>`). */
10
+ export declare function graphicsPostStageSettingId(stage: GraphicsPostStage): string;
11
+ /** Lowest player render scale; the value is the device-pixel-ratio cap handed to the canvas. */
12
+ export declare const RENDER_SCALE_MIN = 0.5;
13
+ /** Highest player render scale. */
14
+ export declare const RENDER_SCALE_MAX = 2;
15
+ /** Render-scale slider increment. */
16
+ export declare const RENDER_SCALE_STEP = 0.05;
17
+ /** Texture-filtering choices offered to the player; the value is the anisotropy sample count. */
18
+ export declare const ANISOTROPY_OPTIONS: readonly {
19
+ value: string;
20
+ label: string;
21
+ }[];
22
+ /** Per-tier profile overrides a game authors with `defineGame({ graphics })`. */
23
+ export type GraphicsProfileOverrides = Partial<Record<GraphicsQuality, Partial<GraphicsProfile>>>;
24
+ /** The resolved player graphics choice: the tier plus the profile after per-stage and render-scale overrides. */
25
+ export interface GraphicsSettingsState {
26
+ quality: GraphicsQuality;
27
+ profile: GraphicsProfile;
28
+ }
29
+ /** The stored quality tier, falling back to the default when the value is missing or unknown. */
30
+ export declare function readGraphicsQuality(store: Pick<SettingsStore, "get">): GraphicsQuality;
31
+ /**
32
+ * Resolve the player's graphics state from a settings store. The tier profile (with the game's
33
+ * overrides) supplies every default; a stored render scale or stage toggle replaces its field.
34
+ */
35
+ export declare function readGraphicsSettings(store: Pick<SettingsStore, "get">, overrides?: GraphicsProfileOverrides): GraphicsSettingsState;
36
+ /**
37
+ * Select a quality tier and re-apply its render scale, texture filtering and stage defaults, so picking a preset
38
+ * resets the individual overrides instead of leaving stale toggles behind.
39
+ */
40
+ export declare function applyGraphicsQuality(store: Pick<SettingsStore, "set">, quality: GraphicsQuality, overrides?: GraphicsProfileOverrides): GraphicsProfile;
@@ -0,0 +1,81 @@
1
+ import { resolveGraphicsProfile } from "./graphicsProfile.js";
2
+ import { DEFAULT_GRAPHICS_QUALITY, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, } from "./settingsModel.js";
3
+ /** Ordered stage list used to build settings rows. */
4
+ export const GRAPHICS_POST_STAGES = ["ao", "bloom", "dof", "smaa"];
5
+ /** Player-facing labels for each post stage. */
6
+ export const GRAPHICS_POST_STAGE_LABELS = {
7
+ ao: "Ambient occlusion",
8
+ bloom: "Bloom",
9
+ dof: "Depth of field",
10
+ smaa: "Anti-aliasing (SMAA)",
11
+ };
12
+ /** Setting id that stores one stage toggle (`graphics.post.<stage>`). */
13
+ export function graphicsPostStageSettingId(stage) {
14
+ return `graphics.post.${stage}`;
15
+ }
16
+ /** Lowest player render scale; the value is the device-pixel-ratio cap handed to the canvas. */
17
+ export const RENDER_SCALE_MIN = 0.5;
18
+ /** Highest player render scale. */
19
+ export const RENDER_SCALE_MAX = 2;
20
+ /** Render-scale slider increment. */
21
+ export const RENDER_SCALE_STEP = 0.05;
22
+ /** Texture-filtering choices offered to the player; the value is the anisotropy sample count. */
23
+ export const ANISOTROPY_OPTIONS = [
24
+ { value: "1", label: "Off" },
25
+ { value: "2", label: "2x" },
26
+ { value: "4", label: "4x" },
27
+ { value: "8", label: "8x" },
28
+ { value: "16", label: "16x" },
29
+ ];
30
+ const ANISOTROPY_VALUES = [1, 2, 4, 8, 16];
31
+ function toAnisotropy(raw, fallback) {
32
+ const value = Number(raw);
33
+ return ANISOTROPY_VALUES.find((option) => option === value) ?? fallback;
34
+ }
35
+ const QUALITY_VALUES = new Set(GRAPHICS_QUALITY_OPTIONS.map((option) => option.value));
36
+ function isGraphicsQuality(value) {
37
+ return typeof value === "string" && QUALITY_VALUES.has(value);
38
+ }
39
+ function clampRenderScale(value) {
40
+ return Math.min(RENDER_SCALE_MAX, Math.max(RENDER_SCALE_MIN, value));
41
+ }
42
+ /** The stored quality tier, falling back to the default when the value is missing or unknown. */
43
+ export function readGraphicsQuality(store) {
44
+ const raw = store.get(SETTING_IDS.graphicsQuality, DEFAULT_GRAPHICS_QUALITY);
45
+ return isGraphicsQuality(raw) ? raw : DEFAULT_GRAPHICS_QUALITY;
46
+ }
47
+ /**
48
+ * Resolve the player's graphics state from a settings store. The tier profile (with the game's
49
+ * overrides) supplies every default; a stored render scale or stage toggle replaces its field.
50
+ */
51
+ export function readGraphicsSettings(store, overrides) {
52
+ const quality = readGraphicsQuality(store);
53
+ const tier = resolveGraphicsProfile(quality, overrides?.[quality]);
54
+ const postStages = { ...tier.postStages };
55
+ for (const stage of GRAPHICS_POST_STAGES) {
56
+ postStages[stage] = store.get(graphicsPostStageSettingId(stage), tier.postStages[stage]);
57
+ }
58
+ return {
59
+ quality,
60
+ profile: {
61
+ ...tier,
62
+ renderScale: clampRenderScale(store.get(SETTING_IDS.graphicsRenderScale, tier.renderScale)),
63
+ anisotropy: toAnisotropy(store.get(SETTING_IDS.graphicsAnisotropy, String(tier.anisotropy)), tier.anisotropy),
64
+ postStages,
65
+ },
66
+ };
67
+ }
68
+ /**
69
+ * Select a quality tier and re-apply its render scale, texture filtering and stage defaults, so picking a preset
70
+ * resets the individual overrides instead of leaving stale toggles behind.
71
+ */
72
+ export function applyGraphicsQuality(store, quality, overrides) {
73
+ const tier = resolveGraphicsProfile(quality, overrides?.[quality]);
74
+ store.set(SETTING_IDS.graphicsQuality, quality);
75
+ store.set(SETTING_IDS.graphicsRenderScale, tier.renderScale);
76
+ store.set(SETTING_IDS.graphicsAnisotropy, String(tier.anisotropy));
77
+ for (const stage of GRAPHICS_POST_STAGES) {
78
+ store.set(graphicsPostStageSettingId(stage), tier.postStages[stage]);
79
+ }
80
+ return tier;
81
+ }
@@ -67,10 +67,18 @@ export declare const SETTING_IDS: {
67
67
  readonly graphicsQuality: "graphics.quality";
68
68
  readonly graphicsShadows: "graphics.shadows";
69
69
  readonly graphicsUiScale: "graphics.uiScale";
70
+ readonly graphicsRenderScale: "graphics.renderScale";
71
+ readonly graphicsPostAo: "graphics.post.ao";
72
+ readonly graphicsPostBloom: "graphics.post.bloom";
73
+ readonly graphicsPostDof: "graphics.post.dof";
74
+ readonly graphicsPostSmaa: "graphics.post.smaa";
75
+ readonly graphicsFrameRateLimit: "graphics.frameRateLimit";
76
+ readonly graphicsAnisotropy: "graphics.anisotropy";
77
+ readonly graphicsShowFps: "graphics.showFps";
70
78
  readonly touchStyle: "controls.touchStyle";
71
79
  readonly touchJoystick: "controls.touchJoystick";
72
80
  };
73
- export type GraphicsQuality = "low" | "medium" | "high";
81
+ export type GraphicsQuality = "low" | "medium" | "high" | "ultra";
74
82
  export declare const GRAPHICS_QUALITY_OPTIONS: readonly SettingOption[];
75
83
  export declare const DEFAULT_MASTER_VOLUME = 1;
76
84
  export declare const DEFAULT_GRAPHICS_QUALITY: GraphicsQuality;
@@ -11,6 +11,14 @@ export const SETTING_IDS = {
11
11
  graphicsQuality: "graphics.quality",
12
12
  graphicsShadows: "graphics.shadows",
13
13
  graphicsUiScale: "graphics.uiScale",
14
+ graphicsRenderScale: "graphics.renderScale",
15
+ graphicsPostAo: "graphics.post.ao",
16
+ graphicsPostBloom: "graphics.post.bloom",
17
+ graphicsPostDof: "graphics.post.dof",
18
+ graphicsPostSmaa: "graphics.post.smaa",
19
+ graphicsFrameRateLimit: "graphics.frameRateLimit",
20
+ graphicsAnisotropy: "graphics.anisotropy",
21
+ graphicsShowFps: "graphics.showFps",
14
22
  touchStyle: "controls.touchStyle",
15
23
  touchJoystick: "controls.touchJoystick",
16
24
  };
@@ -18,6 +26,7 @@ export const GRAPHICS_QUALITY_OPTIONS = [
18
26
  { value: "low", label: "Low" },
19
27
  { value: "medium", label: "Medium" },
20
28
  { value: "high", label: "High" },
29
+ { value: "ultra", label: "Ultra" },
21
30
  ];
22
31
  export const DEFAULT_MASTER_VOLUME = 1;
23
32
  export const DEFAULT_GRAPHICS_QUALITY = "high";
@@ -31,6 +40,7 @@ export const GRAPHICS_QUALITY_DPR = {
31
40
  low: 1,
32
41
  medium: 1.5,
33
42
  high: 2,
43
+ ultra: 2,
34
44
  };
35
45
  export function busVolumeSettingId(busId) {
36
46
  return `sound.bus.${busId}`;
@@ -34,6 +34,6 @@ export interface EventMeter {
34
34
  /**
35
35
  * A heat/hype gauge that rises as tagged events land and cools between them, firing when it fills or breaks — the streak/overdrive meter shooters and fighters hand-roll.
36
36
  *
37
- * @capability event-meter a heat/streak gauge that builds from repeated hits and cools down over time
37
+ * @capability event-meter a heat/streak/alert gauge (wanted level, rage, combo) that builds from repeated events and cools down over time
38
38
  */
39
39
  export declare function createEventMeter(config: EventMeterConfig): EventMeter;
@@ -5,7 +5,7 @@ function readyOf(meter, mode) {
5
5
  /**
6
6
  * A heat/hype gauge that rises as tagged events land and cools between them, firing when it fills or breaks — the streak/overdrive meter shooters and fighters hand-roll.
7
7
  *
8
- * @capability event-meter a heat/streak gauge that builds from repeated hits and cools down over time
8
+ * @capability event-meter a heat/streak/alert gauge (wanted level, rage, combo) that builds from repeated events and cools down over time
9
9
  */
10
10
  export function createEventMeter(config) {
11
11
  const mode = config.mode ?? "hold";
@@ -0,0 +1,10 @@
1
+ /** Elapsed duration and the next anchor to persist with accrued effects. */
2
+ export interface Accrual {
3
+ elapsedMs: number;
4
+ elapsedSeconds: number;
5
+ anchorMs: number;
6
+ }
7
+ /** Elapsed time since a persisted anchor, capped once; persist anchorMs with the accrued result. */
8
+ export declare function accrueSince(anchorMs: number, nowMs: number, options?: {
9
+ capMs?: number;
10
+ }): Accrual;
@@ -0,0 +1,9 @@
1
+ /** Elapsed time since a persisted anchor, capped once; persist anchorMs with the accrued result. */
2
+ export function accrueSince(anchorMs, nowMs, options = {}) {
3
+ const cap = options.capMs ?? Infinity;
4
+ if (!Number.isFinite(anchorMs) || !Number.isFinite(nowMs) || Number.isNaN(cap) || cap < 0) {
5
+ throw new RangeError("Accrual requires finite anchors and a nonnegative cap");
6
+ }
7
+ const elapsedMs = Math.min(cap, Math.max(0, nowMs - anchorMs));
8
+ return { elapsedMs, elapsedSeconds: elapsedMs / 1000, anchorMs: Math.max(anchorMs, nowMs) };
9
+ }
@@ -0,0 +1,16 @@
1
+ /** Maximum accepted operations in a sliding duration. */
2
+ export interface RateWindowPolicy {
3
+ windowMs: number;
4
+ max: number;
5
+ }
6
+ /** Admission result, next stored timestamps, and wait before retrying. */
7
+ export interface RateWindowDecision {
8
+ allowed: boolean;
9
+ timestamps: number[];
10
+ retryAfterMs: number;
11
+ }
12
+ /**
13
+ * A sliding rate window; persist timestamps only alongside the accepted operation.
14
+ * @capability rate-window-policy Evaluate deterministic request windows with retry timing.
15
+ */
16
+ export declare function decideRateWindow(timestamps: readonly number[], nowMs: number, policy: RateWindowPolicy): RateWindowDecision;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A sliding rate window; persist timestamps only alongside the accepted operation.
3
+ * @capability rate-window-policy Evaluate deterministic request windows with retry timing.
4
+ */
5
+ export function decideRateWindow(timestamps, nowMs, policy) {
6
+ if (!Number.isFinite(nowMs) || !Number.isFinite(policy.windowMs) || policy.windowMs <= 0 ||
7
+ !Number.isSafeInteger(policy.max) || policy.max < 1)
8
+ throw new RangeError("Invalid rate window policy");
9
+ const active = timestamps.filter((at) => Number.isFinite(at) && at > nowMs - policy.windowMs).sort((a, b) => a - b);
10
+ if (active.length >= policy.max) {
11
+ return { allowed: false, timestamps: active, retryAfterMs: Math.max(0, active[active.length - policy.max] + policy.windowMs - nowMs) };
12
+ }
13
+ return { allowed: true, timestamps: [...active, nowMs], retryAfterMs: 0 };
14
+ }
@@ -2,11 +2,26 @@ export interface TickSystemDefinition<TSystemId extends string = string> {
2
2
  id: TSystemId;
3
3
  /** Minimum real time between runs; the heartbeat fires the system on the first tick at or past it. */
4
4
  intervalMs: number;
5
+ scope?: "onlinePlayers" | "allServers";
6
+ batchSize?: number;
5
7
  }
6
8
  /** Last-run timestamp per system id. */
7
9
  export type TickAnchors = Record<string, number>;
10
+ /** One due system and how many times this heartbeat owes it. */
11
+ export interface TickSystemRun<TSystemId extends string = string> {
12
+ id: TSystemId;
13
+ /** Times to run the system now; above 1 when wall time stalled past several intervals. */
14
+ runs: number;
15
+ scope?: "onlinePlayers" | "allServers";
16
+ batchSize?: number;
17
+ }
8
18
  export interface ServerTickPlan<TSystemId extends string = string> {
9
- due: TSystemId[];
19
+ /**
20
+ * Due systems, at most one entry per id. `runs` carries catch-up multiplicity — the anchors were
21
+ * advanced by that many intervals, so a host that runs the handler once per entry silently drops
22
+ * the rest.
23
+ */
24
+ due: TickSystemRun<TSystemId>[];
10
25
  anchors: TickAnchors;
11
26
  }
12
27
  export type PlanServerTickOptions = {
@@ -15,8 +30,11 @@ export type PlanServerTickOptions = {
15
30
  };
16
31
  /**
17
32
  * Decides which systems a heartbeat should run. A system with no anchor runs immediately.
18
- * When wall time stalls past multiple intervals, the system id is repeated (bounded by
19
- * maxCatchUp). Excess lag past the bound resyncs the anchor to `now`.
33
+ * When wall time stalls past multiple intervals the system's `runs` climbs (bounded by
34
+ * maxCatchUp), and the returned anchor moves by that many intervals — so a host that ignores
35
+ * `runs` loses the catch-up work. Excess lag past the bound resyncs the anchor to `now`.
20
36
  * Returned anchors carry only systems present in the pipeline, so removed systems clean up.
21
37
  */
22
38
  export declare function planServerTick<TSystemId extends string>(systems: readonly TickSystemDefinition<TSystemId>[], anchors: TickAnchors, now: number, options?: PlanServerTickOptions): ServerTickPlan<TSystemId>;
39
+ /** Times this plan owes `id`, or 0 when it is not due. Saves a host a lookup over {@link ServerTickPlan.due}. */
40
+ export declare function tickRunCount<TSystemId extends string>(plan: ServerTickPlan<TSystemId>, id: TSystemId): number;
@@ -1,8 +1,9 @@
1
1
  const DEFAULT_MAX_CATCH_UP = 3;
2
2
  /**
3
3
  * Decides which systems a heartbeat should run. A system with no anchor runs immediately.
4
- * When wall time stalls past multiple intervals, the system id is repeated (bounded by
5
- * maxCatchUp). Excess lag past the bound resyncs the anchor to `now`.
4
+ * When wall time stalls past multiple intervals the system's `runs` climbs (bounded by
5
+ * maxCatchUp), and the returned anchor moves by that many intervals — so a host that ignores
6
+ * `runs` loses the catch-up work. Excess lag past the bound resyncs the anchor to `now`.
6
7
  * Returned anchors carry only systems present in the pipeline, so removed systems clean up.
7
8
  */
8
9
  export function planServerTick(systems, anchors, now, options) {
@@ -10,14 +11,12 @@ export function planServerTick(systems, anchors, now, options) {
10
11
  const due = [];
11
12
  const nextAnchors = {};
12
13
  for (const system of systems) {
14
+ if (system.batchSize !== undefined && (!Number.isSafeInteger(system.batchSize) || system.batchSize < 1 || system.batchSize > 1000))
15
+ throw new RangeError("batchSize must be an integer from 1 to 1000");
16
+ const dispatch = { ...(system.scope === undefined ? {} : { scope: system.scope }), ...(system.batchSize === undefined ? {} : { batchSize: system.batchSize }) };
13
17
  const lastRunAt = anchors[system.id];
14
- if (lastRunAt === undefined) {
15
- due.push(system.id);
16
- nextAnchors[system.id] = now;
17
- continue;
18
- }
19
- if (system.intervalMs <= 0) {
20
- due.push(system.id);
18
+ if (lastRunAt === undefined || system.intervalMs <= 0) {
19
+ due.push({ id: system.id, runs: 1, ...dispatch });
21
20
  nextAnchors[system.id] = now;
22
21
  continue;
23
22
  }
@@ -28,14 +27,16 @@ export function planServerTick(systems, anchors, now, options) {
28
27
  }
29
28
  const missed = Math.floor(elapsed / system.intervalMs);
30
29
  if (missed > maxCatchUp) {
31
- for (let i = 0; i < maxCatchUp; i += 1)
32
- due.push(system.id);
30
+ due.push({ id: system.id, runs: maxCatchUp, ...dispatch });
33
31
  nextAnchors[system.id] = now;
34
32
  continue;
35
33
  }
36
- for (let i = 0; i < missed; i += 1)
37
- due.push(system.id);
34
+ due.push({ id: system.id, runs: missed, ...dispatch });
38
35
  nextAnchors[system.id] = lastRunAt + missed * system.intervalMs;
39
36
  }
40
37
  return { due, anchors: nextAnchors };
41
38
  }
39
+ /** Times this plan owes `id`, or 0 when it is not due. Saves a host a lookup over {@link ServerTickPlan.due}. */
40
+ export function tickRunCount(plan, id) {
41
+ return plan.due.find((entry) => entry.id === id)?.runs ?? 0;
42
+ }
@@ -0,0 +1,61 @@
1
+ /** Piecewise-linear map from a signal value to an output value; points in ascending input order, ends clamp. */
2
+ export type FeedbackCurve = readonly (readonly [number, number])[];
3
+ /** One wire from a sim signal to a named output. */
4
+ export interface FeedbackRoute<TSignal extends string, TTarget extends string> {
5
+ signal: TSignal;
6
+ target: TTarget;
7
+ /** Signal → output mapping (default identity). */
8
+ curve?: FeedbackCurve;
9
+ /** Units/s the routed value may rise (default `Infinity`: follows instantly). */
10
+ attack?: number;
11
+ /** Units/s the routed value may fall (default `attack`). */
12
+ release?: number;
13
+ }
14
+ /** A one-shot fired when a signal crosses a threshold (landing thud, gear-change clunk, impact rumble). */
15
+ export interface FeedbackEvent<TSignal extends string> {
16
+ id: string;
17
+ signal: TSignal;
18
+ /** Fires when the signal rises to or above this value from below it. */
19
+ threshold: number;
20
+ /** Seconds before the same event may fire again (default `0`). */
21
+ cooldown?: number;
22
+ }
23
+ /** Config for {@link createFeedbackMixer}. */
24
+ export interface FeedbackMixerConfig<TSignal extends string, TTarget extends string> {
25
+ routes: readonly FeedbackRoute<TSignal, TTarget>[];
26
+ /** How routes landing on the same target combine (default `"sum"`). */
27
+ combine?: Partial<Record<TTarget, "sum" | "max">>;
28
+ /** Value a target starts from before routes add to it (default `0`). */
29
+ base?: Partial<Record<TTarget, number>>;
30
+ events?: readonly FeedbackEvent<TSignal>[];
31
+ }
32
+ /** Serializable mixer state: smoothed route values, last signal per event, and event cooldowns. */
33
+ export interface FeedbackMixerState {
34
+ routes: number[];
35
+ eventSignals: number[];
36
+ cooldowns: number[];
37
+ }
38
+ /** Maps sim telemetry to presentation parameters through declared routes, curves and smoothing. */
39
+ export interface FeedbackMixer<TSignal extends string, TTarget extends string> {
40
+ /**
41
+ * Advance by `dt` with this tick's signals and return every target's value. The returned record is reused
42
+ * between calls, so read it before the next update. Missing signals read as `0`.
43
+ */
44
+ update(dt: number, signals: Readonly<Partial<Record<TSignal, number>>>): Readonly<Record<TTarget, number>>;
45
+ /** True when event `id` fired on the last update. */
46
+ fired(id: string): boolean;
47
+ value(): Readonly<Record<TTarget, number>>;
48
+ retune(next: FeedbackMixerConfig<TSignal, TTarget>): void;
49
+ snapshot(): FeedbackMixerState;
50
+ restore(state: FeedbackMixerState): void;
51
+ reset(): void;
52
+ }
53
+ /** Sample a {@link FeedbackCurve}; an empty curve is the identity. */
54
+ export declare function sampleFeedbackCurve(curve: FeedbackCurve | undefined, x: number): number;
55
+ /**
56
+ * Creates a {@link FeedbackMixer}: the one place a game declares how sim telemetry drives presentation —
57
+ * rpm to engine pitch, tire saturation to squeal and rumble, speed to FOV, landing speed to a thud — instead
58
+ * of hand-writing that glue per vehicle, aircraft or weapon. Deterministic, allocation-free per update.
59
+ * @capability feedback-mixer map sim telemetry to camera, audio and haptic parameters through curves, smoothing and threshold events
60
+ */
61
+ export declare function createFeedbackMixer<TSignal extends string, TTarget extends string>(initial: FeedbackMixerConfig<TSignal, TTarget>): FeedbackMixer<TSignal, TTarget>;
@@ -0,0 +1,106 @@
1
+ /** Sample a {@link FeedbackCurve}; an empty curve is the identity. */
2
+ export function sampleFeedbackCurve(curve, x) {
3
+ if (curve === undefined || curve.length === 0)
4
+ return x;
5
+ const first = curve[0];
6
+ if (x <= first[0])
7
+ return first[1];
8
+ for (let i = 1; i < curve.length; i += 1) {
9
+ const [x1, y1] = curve[i];
10
+ if (x <= x1) {
11
+ const [x0, y0] = curve[i - 1];
12
+ const span = x1 - x0;
13
+ return span <= 0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / span;
14
+ }
15
+ }
16
+ return curve[curve.length - 1][1];
17
+ }
18
+ function approach(current, target, attack, release, dt) {
19
+ const rate = target > current ? attack : release;
20
+ if (!Number.isFinite(rate))
21
+ return target;
22
+ const step = Math.max(0, rate) * dt;
23
+ return Math.abs(target - current) <= step ? target : current + Math.sign(target - current) * step;
24
+ }
25
+ /**
26
+ * Creates a {@link FeedbackMixer}: the one place a game declares how sim telemetry drives presentation —
27
+ * rpm to engine pitch, tire saturation to squeal and rumble, speed to FOV, landing speed to a thud — instead
28
+ * of hand-writing that glue per vehicle, aircraft or weapon. Deterministic, allocation-free per update.
29
+ * @capability feedback-mixer map sim telemetry to camera, audio and haptic parameters through curves, smoothing and threshold events
30
+ */
31
+ export function createFeedbackMixer(initial) {
32
+ let config = initial;
33
+ let routeValues = [];
34
+ let eventSignals = [];
35
+ let cooldowns = [];
36
+ let firedNow = new Set();
37
+ const output = {};
38
+ const touched = new Set();
39
+ function shapeState() {
40
+ routeValues = config.routes.map((_, i) => routeValues[i] ?? 0);
41
+ eventSignals = (config.events ?? []).map((_, i) => eventSignals[i] ?? 0);
42
+ cooldowns = (config.events ?? []).map((_, i) => cooldowns[i] ?? 0);
43
+ for (const route of config.routes)
44
+ output[route.target] ??= 0;
45
+ for (const key of Object.keys(config.base ?? {}))
46
+ output[key] ??= config.base?.[key] ?? 0;
47
+ }
48
+ shapeState();
49
+ return {
50
+ update(dt, signals) {
51
+ const step = Math.max(0, dt);
52
+ for (const key of Object.keys(output))
53
+ output[key] = config.base?.[key] ?? 0;
54
+ touched.clear();
55
+ for (let i = 0; i < config.routes.length; i += 1) {
56
+ const route = config.routes[i];
57
+ const target = sampleFeedbackCurve(route.curve, signals[route.signal] ?? 0);
58
+ const attack = route.attack ?? Number.POSITIVE_INFINITY;
59
+ const value = approach(routeValues[i], target, attack, route.release ?? attack, step);
60
+ routeValues[i] = value;
61
+ const mode = config.combine?.[route.target] ?? "sum";
62
+ if (mode === "max") {
63
+ const current = touched.has(route.target) ? output[route.target] : Number.NEGATIVE_INFINITY;
64
+ output[route.target] = Math.max(current, value);
65
+ }
66
+ else
67
+ output[route.target] += value;
68
+ touched.add(route.target);
69
+ }
70
+ firedNow.clear();
71
+ const events = config.events ?? [];
72
+ for (let i = 0; i < events.length; i += 1) {
73
+ const event = events[i];
74
+ const value = signals[event.signal] ?? 0;
75
+ cooldowns[i] = Math.max(0, cooldowns[i] - step);
76
+ if (value >= event.threshold && eventSignals[i] < event.threshold && cooldowns[i] === 0) {
77
+ firedNow.add(event.id);
78
+ cooldowns[i] = event.cooldown ?? 0;
79
+ }
80
+ eventSignals[i] = value;
81
+ }
82
+ return output;
83
+ },
84
+ fired: (id) => firedNow.has(id),
85
+ value: () => output,
86
+ retune(next) {
87
+ config = next;
88
+ shapeState();
89
+ },
90
+ snapshot: () => ({ routes: [...routeValues], eventSignals: [...eventSignals], cooldowns: [...cooldowns] }),
91
+ restore(state) {
92
+ routeValues = [...state.routes];
93
+ eventSignals = [...state.eventSignals];
94
+ cooldowns = [...state.cooldowns];
95
+ shapeState();
96
+ },
97
+ reset() {
98
+ routeValues = routeValues.map(() => 0);
99
+ eventSignals = eventSignals.map(() => 0);
100
+ cooldowns = cooldowns.map(() => 0);
101
+ firedNow = new Set();
102
+ for (const key of Object.keys(output))
103
+ output[key] = config.base?.[key] ?? 0;
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,68 @@
1
+ import type { EmitterConfig, Vec3 } from "./particles.js";
2
+ /** How a particle effect composites on screen: `additive` for sparks/fire/glow, `normal` for smoke/dust. */
3
+ export type ParticleBlendHint = "additive" | "normal";
4
+ /** A one-shot burst request — consumed once by the renderer, never serialized. */
5
+ export interface ParticleBurst {
6
+ /** Monotonic sequence id so a renderer can consume each burst exactly once. */
7
+ seq: number;
8
+ config: EmitterConfig;
9
+ /** Particles to emit immediately. */
10
+ count: number;
11
+ blending?: ParticleBlendHint;
12
+ }
13
+ /** Options for {@link ParticleDirector.attach}. */
14
+ export interface ParticleAttachOptions {
15
+ /** Scene entity id the emitter origin tracks each frame (plus `offset`). */
16
+ follow?: string;
17
+ /** World offset from the followed entity (or from the config position when unfollowed). */
18
+ offset?: Vec3;
19
+ blending?: ParticleBlendHint;
20
+ }
21
+ /** A keyed continuous emitter owned by the director until detached. */
22
+ export interface ParticleEmitterSpec {
23
+ id: string;
24
+ config: EmitterConfig;
25
+ follow?: string;
26
+ offset?: Vec3;
27
+ blending?: ParticleBlendHint;
28
+ }
29
+ /** Serializable director state: the standing emitters (bursts are transient by nature). */
30
+ export interface ParticleDirectorState {
31
+ nextSeq: number;
32
+ emitters: ParticleEmitterSpec[];
33
+ }
34
+ /**
35
+ * The game-side seam for particle effects. Game logic requests one-shot bursts and
36
+ * standing emitters as plain data; the shell renders them through
37
+ * `createParticleSystem`/`ParticleField`, applies the graphics-quality particle cap,
38
+ * and tracks `follow` entities. Nothing here touches a renderer, so commands and
39
+ * `onTick` systems can drive VFX headlessly and tests can assert the requested effects.
40
+ */
41
+ export interface ParticleDirector {
42
+ /** Queue a one-shot burst (impact puff, debris, muzzle smoke). */
43
+ burst(config: EmitterConfig, count: number, blending?: ParticleBlendHint): void;
44
+ /** Create or replace a standing emitter under `id` (exhaust, dust plume, torch smoke). */
45
+ attach(id: string, config: EmitterConfig, options?: ParticleAttachOptions): void;
46
+ /** Patch a standing emitter's config in place (e.g. scale exhaust rate with speed). */
47
+ retune(id: string, patch: Partial<EmitterConfig>): void;
48
+ /** Remove a standing emitter. Unknown ids are ignored. */
49
+ detach(id: string): void;
50
+ /** Drop every standing emitter and any unconsumed bursts. */
51
+ clear(): void;
52
+ /** The standing emitters, for renderers and assertions. */
53
+ emitters(): readonly ParticleEmitterSpec[];
54
+ /** Return and forget the queued bursts — the renderer's consume-once read. */
55
+ drainBursts(): ParticleBurst[];
56
+ subscribe(listener: () => void): () => void;
57
+ snapshot(): ParticleDirectorState;
58
+ restore(state: ParticleDirectorState): void;
59
+ }
60
+ /**
61
+ * Create the particle intent registry a `GameContext` exposes as `ctx.particles`.
62
+ * State is data-only and serializable: standing emitters survive `snapshot`/`restore`,
63
+ * queued bursts are transient. The renderer subscribes, drains bursts, and mirrors
64
+ * the emitter list — the director never allocates particle pools itself.
65
+ *
66
+ * @capability particle-director game-reachable VFX seam: queue one-shot particle bursts and keyed follow-capable standing emitters as serializable data for the shell to render
67
+ */
68
+ export declare function createParticleDirector(): ParticleDirector;