@jgengine/core 0.19.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/ai/decisionGraph.d.ts +2 -0
  3. package/dist/ai/decisionGraph.js +6 -0
  4. package/dist/ai/flock.d.ts +1 -1
  5. package/dist/ai/flock.js +7 -4
  6. package/dist/anim/animGraph.d.ts +33 -2
  7. package/dist/anim/animGraph.js +200 -12
  8. package/dist/anim/locomotionGraph.d.ts +11 -3
  9. package/dist/anim/locomotionGraph.js +25 -15
  10. package/dist/audio/audioFalloff.d.ts +4 -0
  11. package/dist/audio/voiceAllocator.d.ts +46 -0
  12. package/dist/audio/voiceAllocator.js +76 -0
  13. package/dist/combat/abilityKit.d.ts +19 -0
  14. package/dist/combat/abilityKit.js +40 -4
  15. package/dist/combat/death.d.ts +4 -3
  16. package/dist/combat/death.js +3 -3
  17. package/dist/combat/magazine.d.ts +9 -0
  18. package/dist/combat/magazine.js +24 -2
  19. package/dist/combat/projectiles.d.ts +94 -2
  20. package/dist/combat/projectiles.js +282 -61
  21. package/dist/combat/regenShield.d.ts +14 -0
  22. package/dist/combat/regenShield.js +23 -7
  23. package/dist/crafting/recipeCatalog.d.ts +16 -0
  24. package/dist/crafting/recipeCatalog.js +55 -0
  25. package/dist/economy/auctionBook.d.ts +1 -1
  26. package/dist/economy/auctionBook.js +11 -1
  27. package/dist/economy/listingBook.d.ts +1 -1
  28. package/dist/economy/listingBook.js +11 -1
  29. package/dist/economy/resourceLedger.js +3 -3
  30. package/dist/economy/shopStock.js +7 -6
  31. package/dist/economy/techTree.js +7 -6
  32. package/dist/economy/unlockPoints.js +3 -6
  33. package/dist/editor/commandApply.js +89 -16
  34. package/dist/editor/commands.d.ts +38 -1
  35. package/dist/editor/commands.js +320 -3
  36. package/dist/editor/creatorStorage.d.ts +59 -0
  37. package/dist/editor/creatorStorage.js +164 -0
  38. package/dist/editor/document.d.ts +4 -0
  39. package/dist/editor/document.js +116 -17
  40. package/dist/editor/index.d.ts +3 -0
  41. package/dist/editor/index.js +1 -0
  42. package/dist/editor/liveSync.d.ts +4 -1
  43. package/dist/editor/liveSync.js +26 -10
  44. package/dist/editor/materialAuthoring.d.ts +14 -0
  45. package/dist/editor/materialAuthoring.js +38 -0
  46. package/dist/editor/modelNodeVisibility.d.ts +11 -0
  47. package/dist/editor/modelNodeVisibility.js +29 -0
  48. package/dist/editor/movementCatalog.d.ts +90 -0
  49. package/dist/editor/movementCatalog.js +144 -0
  50. package/dist/editor/simulation.d.ts +60 -0
  51. package/dist/editor/simulation.js +202 -0
  52. package/dist/editor/staticPrefab.d.ts +17 -0
  53. package/dist/editor/staticPrefab.js +51 -0
  54. package/dist/editor/terrainMaterials.d.ts +10 -0
  55. package/dist/editor/terrainMaterials.js +13 -0
  56. package/dist/editor/types.d.ts +11 -0
  57. package/dist/game/cameraConfig.d.ts +3 -1
  58. package/dist/game/clipRoles.d.ts +2 -1
  59. package/dist/game/clipRoles.js +7 -3
  60. package/dist/game/contentValidation.d.ts +69 -0
  61. package/dist/game/contentValidation.js +118 -0
  62. package/dist/game/controlGate.d.ts +8 -0
  63. package/dist/game/controlGate.js +43 -6
  64. package/dist/game/defineGame.d.ts +32 -3
  65. package/dist/game/dialogueGraph.d.ts +20 -0
  66. package/dist/game/dialogueGraph.js +107 -0
  67. package/dist/game/events.d.ts +5 -0
  68. package/dist/game/keyValueStore.d.ts +17 -4
  69. package/dist/game/keyValueStore.js +35 -23
  70. package/dist/game/lootModifiers.d.ts +17 -0
  71. package/dist/game/lootModifiers.js +26 -0
  72. package/dist/game/narrativeAuthoring.d.ts +67 -0
  73. package/dist/game/narrativeAuthoring.js +122 -0
  74. package/dist/game/playableGame.d.ts +52 -20
  75. package/dist/game/quest.d.ts +15 -0
  76. package/dist/game/quest.js +209 -63
  77. package/dist/game/questCatalog.d.ts +50 -0
  78. package/dist/game/questCatalog.js +205 -0
  79. package/dist/game/saveStore.d.ts +6 -5
  80. package/dist/game/saveStore.js +57 -57
  81. package/dist/game/systemRuntime.d.ts +2 -2
  82. package/dist/game/systemRuntime.js +84 -33
  83. package/dist/gameplay.d.ts +3 -0
  84. package/dist/gameplay.js +3 -0
  85. package/dist/interaction/promptRegistry.d.ts +5 -4
  86. package/dist/interaction/promptRegistry.js +8 -2
  87. package/dist/inventory/inventoryModel.js +9 -6
  88. package/dist/inventory/shapedGrid.d.ts +48 -9
  89. package/dist/inventory/shapedGrid.js +69 -10
  90. package/dist/item/durability.js +4 -0
  91. package/dist/item/itemInstanceRegistry.d.ts +32 -1
  92. package/dist/item/itemInstanceRegistry.js +111 -7
  93. package/dist/item/itemgen.d.ts +1 -1
  94. package/dist/item/itemgen.js +1 -1
  95. package/dist/item/modularItem.js +7 -1
  96. package/dist/material/appearanceSignals.d.ts +53 -0
  97. package/dist/material/appearanceSignals.js +47 -0
  98. package/dist/material/hairCards.d.ts +47 -0
  99. package/dist/material/hairCards.js +136 -0
  100. package/dist/material/materialAsset.d.ts +162 -0
  101. package/dist/material/materialAsset.js +382 -0
  102. package/dist/meta/changelog.d.ts +1 -1
  103. package/dist/meta/changelog.js +348 -1
  104. package/dist/movement/characterController.d.ts +5 -2
  105. package/dist/movement/characterController.js +50 -23
  106. package/dist/movement/freeFlight.d.ts +2 -2
  107. package/dist/movement/freeFlight.js +3 -3
  108. package/dist/movement/movementModel.d.ts +24 -1
  109. package/dist/movement/movementModel.js +74 -7
  110. package/dist/movement/playerMovement.d.ts +28 -1
  111. package/dist/movement/playerMovement.js +425 -102
  112. package/dist/movement/poseState.d.ts +2 -0
  113. package/dist/movement/poseState.js +29 -1
  114. package/dist/movement/solidObstacles.d.ts +3 -1
  115. package/dist/movement/solidObstacles.js +8 -5
  116. package/dist/movement/terrainGrade.d.ts +13 -0
  117. package/dist/movement/terrainGrade.js +33 -0
  118. package/dist/movement/voxelController.d.ts +2 -2
  119. package/dist/movement/voxelController.js +3 -3
  120. package/dist/nav/navGrid.d.ts +19 -1
  121. package/dist/nav/navGrid.js +26 -10
  122. package/dist/nav/solidRoute.d.ts +50 -0
  123. package/dist/nav/solidRoute.js +144 -0
  124. package/dist/physics/ballisticSweep.d.ts +10 -3
  125. package/dist/physics/ballisticSweep.js +53 -29
  126. package/dist/physics/forceVolume.d.ts +41 -0
  127. package/dist/physics/forceVolume.js +88 -0
  128. package/dist/physics/kinematicVehicle.d.ts +8 -3
  129. package/dist/physics/kinematicVehicle.js +2 -2
  130. package/dist/physics/physicsWorldBackend.js +19 -3
  131. package/dist/physics/spatialGrid.d.ts +1 -1
  132. package/dist/physics/spatialGrid.js +8 -9
  133. package/dist/physics/vehicleDynamics.d.ts +3 -2
  134. package/dist/physics/vehicleDynamics.js +2 -2
  135. package/dist/physics/vehicleObstacles.d.ts +3 -2
  136. package/dist/physics/vehicleObstacles.js +29 -5
  137. package/dist/runtime/cameraDirector.d.ts +39 -2
  138. package/dist/runtime/cameraDirector.js +162 -0
  139. package/dist/runtime/commandDispatch.d.ts +14 -0
  140. package/dist/runtime/commandDispatch.js +19 -0
  141. package/dist/runtime/context/combat.d.ts +11 -4
  142. package/dist/runtime/context/combat.js +178 -11
  143. package/dist/runtime/context/deathLoot.d.ts +9 -8
  144. package/dist/runtime/context/deathLoot.js +16 -16
  145. package/dist/runtime/context/scene.d.ts +2 -0
  146. package/dist/runtime/context/scene.js +13 -21
  147. package/dist/runtime/descriptors/baseline.js +34 -3
  148. package/dist/runtime/descriptors/features.d.ts +2 -1
  149. package/dist/runtime/descriptors/features.js +18 -0
  150. package/dist/runtime/gameContext.js +79 -13
  151. package/dist/runtime/gameContextTypes.d.ts +14 -0
  152. package/dist/runtime/headlessRunner.d.ts +3 -1
  153. package/dist/runtime/headlessRunner.js +11 -11
  154. package/dist/runtime/hostedGameRunner.d.ts +24 -1
  155. package/dist/runtime/hostedGameRunner.js +156 -10
  156. package/dist/runtime/hostedWorldSession.d.ts +17 -5
  157. package/dist/runtime/hostedWorldSession.js +154 -16
  158. package/dist/runtime/hostedWorldStore.d.ts +2 -2
  159. package/dist/runtime/inputRecorder.d.ts +1 -1
  160. package/dist/runtime/inputRecorder.js +21 -1
  161. package/dist/runtime/inputSnapshot.d.ts +16 -4
  162. package/dist/runtime/inputSnapshot.js +31 -3
  163. package/dist/runtime/runtimeSave.d.ts +10 -6
  164. package/dist/runtime/runtimeSave.js +11 -5
  165. package/dist/runtime/simContext.d.ts +8 -3
  166. package/dist/runtime/simContext.js +7 -3
  167. package/dist/runtime/simSnapshot.js +2 -0
  168. package/dist/runtime/worldReplication.d.ts +2 -0
  169. package/dist/runtime/worldReplication.js +1 -1
  170. package/dist/scene/assetCatalog.d.ts +2 -0
  171. package/dist/scene/behaviorRuntime.d.ts +26 -5
  172. package/dist/scene/behaviorRuntime.js +263 -16
  173. package/dist/scene/behaviors.d.ts +30 -1
  174. package/dist/scene/behaviors.js +29 -0
  175. package/dist/scene/entityStore.d.ts +14 -3
  176. package/dist/scene/entityStore.js +45 -9
  177. package/dist/scene/mount.d.ts +9 -0
  178. package/dist/scene/mount.js +35 -0
  179. package/dist/scene/possession.d.ts +2 -0
  180. package/dist/scene/possession.js +21 -1
  181. package/dist/scene/sceneRaycast.d.ts +7 -0
  182. package/dist/scene/sceneRaycast.js +55 -0
  183. package/dist/scene/spatial.d.ts +20 -1
  184. package/dist/scene/spatial.js +94 -2
  185. package/dist/scene/vehicleSeat.d.ts +36 -2
  186. package/dist/scene/vehicleSeat.js +89 -2
  187. package/dist/stats/accumulatorMeter.d.ts +10 -0
  188. package/dist/stats/accumulatorMeter.js +6 -0
  189. package/dist/stats/eventMeter.d.ts +9 -2
  190. package/dist/stats/eventMeter.js +5 -2
  191. package/dist/store/defineStore.d.ts +3 -1
  192. package/dist/store/defineStore.js +3 -1
  193. package/dist/store/observableKeyedStore.d.ts +1 -1
  194. package/dist/store/observableKeyedStore.js +8 -6
  195. package/dist/time/simClock.d.ts +6 -2
  196. package/dist/time/simClock.js +5 -0
  197. package/dist/vfx/particleDirector.d.ts +58 -6
  198. package/dist/vfx/particleDirector.js +113 -22
  199. package/dist/vfx/particles.d.ts +68 -5
  200. package/dist/vfx/particles.js +284 -32
  201. package/dist/visibility/assetStreaming.d.ts +21 -3
  202. package/dist/visibility/assetStreaming.js +108 -24
  203. package/dist/visibility/settings.d.ts +8 -2
  204. package/dist/visibility/settings.js +3 -0
  205. package/dist/visibility/visibilitySystem.js +1 -0
  206. package/dist/work/expedition.d.ts +110 -0
  207. package/dist/work/expedition.js +195 -0
  208. package/dist/work/staffedStation.d.ts +29 -0
  209. package/dist/work/staffedStation.js +27 -0
  210. package/dist/world/authoredAnimation.d.ts +38 -0
  211. package/dist/world/authoredAnimation.js +99 -0
  212. package/dist/world/authoredObjects.d.ts +24 -4
  213. package/dist/world/authoredObjects.js +43 -16
  214. package/dist/world/authoredSimulation.d.ts +74 -0
  215. package/dist/world/authoredSimulation.js +302 -0
  216. package/dist/world/authoredWeather.d.ts +120 -0
  217. package/dist/world/authoredWeather.js +314 -0
  218. package/dist/world/buildingKit.d.ts +8 -3
  219. package/dist/world/buildingKit.js +21 -8
  220. package/dist/world/environmentForces.d.ts +21 -0
  221. package/dist/world/environmentForces.js +42 -0
  222. package/dist/world/features.d.ts +3 -3
  223. package/dist/world/footprintGrid.d.ts +55 -0
  224. package/dist/world/footprintGrid.js +201 -0
  225. package/dist/world/grassKind.d.ts +10 -0
  226. package/dist/world/grassKind.js +22 -0
  227. package/dist/world/pathTerrain.d.ts +1 -1
  228. package/dist/world/terraform.d.ts +2 -2
  229. package/dist/world/terraform.js +46 -18
  230. package/dist/world/weather.d.ts +17 -0
  231. package/dist/world/weather.js +116 -20
  232. package/dist/world/worldSolids.d.ts +11 -0
  233. package/dist/world/worldSolids.js +112 -1
  234. package/dist/world.d.ts +5 -5
  235. package/dist/world.js +4 -4
  236. package/package.json +1 -1
  237. package/skills/ce-handoff/SKILL.md +1 -1
  238. package/skills/game-design/SKILL.md +2 -0
  239. package/skills/jgengine/SKILL.md +23 -2
  240. package/skills/jgengine/api.md +28 -17
  241. package/skills/jgengine/recipes/minimal-game.md +7 -2
  242. package/skills/jgengine-combat/SKILL.md +8 -2
  243. package/skills/jgengine-combat/api.md +22 -10
  244. package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
  245. package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
  246. package/skills/jgengine-combat/reference.md +24 -2
  247. package/skills/jgengine-gameplay/SKILL.md +63 -0
  248. package/skills/jgengine-gameplay/api.md +131 -37
  249. package/skills/jgengine-gameplay/capabilities.md +68 -0
  250. package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
  251. package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
  252. package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
  253. package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
  254. package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
  255. package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
  256. package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
  257. package/skills/jgengine-gameplay/reference-systems.md +119 -0
  258. package/skills/jgengine-world/SKILL.md +12 -3
  259. package/skills/jgengine-world/api.md +201 -58
  260. package/skills/jgengine-world/capabilities.md +116 -3
  261. package/skills/jgengine-world/recipes/character-animation.md +31 -8
  262. package/skills/jgengine-world/recipes/controls.md +4 -0
  263. package/skills/jgengine-world/recipes/npc-senses.md +12 -0
  264. package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
  265. package/skills/jgengine-world/reference.md +167 -5
  266. package/skills/jgengine-world/visibility.md +28 -1
@@ -1,3 +1,4 @@
1
+ import { type ForceFieldConfig } from "../physics/forceVolume.js";
1
2
  /** A 3D vector `[x, y, z]`. */
2
3
  export type Vec3 = readonly [number, number, number];
3
4
  /** A `[min, max]` range a spawned particle draws uniformly from. */
@@ -10,6 +11,41 @@ export interface Curve {
10
11
  start: number;
11
12
  end: number;
12
13
  }
14
+ /** Serializable spawn volume in emitter coordinates; cone widens from its base along +Y. */
15
+ export interface ParticleSpawnShape {
16
+ kind: "disc" | "sphere" | "cone";
17
+ radius: number;
18
+ height?: number;
19
+ /** Spawn on the outer surface instead of filling the volume. */
20
+ surface?: boolean;
21
+ }
22
+ /** Bounded particle contact policy; a query adapter can replace the fallback horizontal plane. */
23
+ export interface ParticleCollisionConfig {
24
+ planeY?: number;
25
+ response?: "kill" | "bounce";
26
+ restitution?: number;
27
+ maxQueries?: number;
28
+ maxEvents?: number;
29
+ }
30
+ /** One contact from an injected scene query, in simulation coordinates. */
31
+ export interface ParticleCollisionHit {
32
+ position: Vec3;
33
+ normal: Vec3;
34
+ }
35
+ /** Presentation events never authorize gameplay damage or physical impulses. */
36
+ export interface ParticleEvent {
37
+ type: "collision" | "death";
38
+ particleId: number;
39
+ position: Vec3;
40
+ velocity: Vec3;
41
+ normal?: Vec3;
42
+ }
43
+ /** Optional geometry adapter; queries are capped independently of rendering. */
44
+ export interface ParticleSystemOptions {
45
+ collision?: (from: Vec3, to: Vec3) => ParticleCollisionHit | null;
46
+ /** External cosmetic acceleration; return `out` to reuse its allocation. */
47
+ acceleration?: (position: Vec3, velocity: Vec3, out: [number, number, number]) => Vec3;
48
+ }
13
49
  /**
14
50
  * A particle emitter: how particles spawn and how each one evolves over its life.
15
51
  * Every field is data — no functions — so an emitter is fully serializable and an
@@ -24,6 +60,7 @@ export interface EmitterConfig {
24
60
  position?: Vec3;
25
61
  /** Half-extents of a box the spawn point is jittered within. Default `[0, 0, 0]`. */
26
62
  spawnJitter?: Vec3;
63
+ spawnShape?: ParticleSpawnShape;
27
64
  /** Particle lifetime in seconds. Default `{ min: 1, max: 1 }`. */
28
65
  lifetime?: Range;
29
66
  /** Initial speed along the emit direction, in units/second. Default `{ min: 1, max: 1 }`. */
@@ -44,8 +81,13 @@ export interface EmitterConfig {
44
81
  colorEnd?: number;
45
82
  /** Opacity over life, each `0..1`. Default `{ start: 1, end: 0 }` (fade out). */
46
83
  alpha?: Curve;
47
- /** Injected clock — unused by the sim (it is dt-driven) but reserved; determinism comes from `seed`. */
84
+ /** Seed for deterministic births; retuning it resets future randomness. */
48
85
  seed?: string | number;
86
+ /** Shared physical field descriptors, sampled as cosmetic acceleration; at most eight per particle. */
87
+ forces?: readonly ForceFieldConfig[];
88
+ collision?: ParticleCollisionConfig;
89
+ /** Enable bounded death events. Collision events use `collision.maxEvents`. */
90
+ deathEvents?: boolean;
49
91
  }
50
92
  /**
51
93
  * Read-only packed buffers of the live particles, laid out for a renderer to
@@ -62,6 +104,11 @@ export interface ParticleBuffers {
62
104
  colors: Float32Array;
63
105
  /** `count` floats: opacity `0..1` per particle. */
64
106
  alphas: Float32Array;
107
+ /** Stable birth ids, retained through pool compaction. */
108
+ ids: Float64Array;
109
+ /** Packed velocity and prior integration positions for streak/ribbon outputs. */
110
+ velocities: Float32Array;
111
+ previousPositions: Float32Array;
65
112
  }
66
113
  /** Serializable simulation state for save/restore and deterministic replay. */
67
114
  export interface ParticleSnapshot {
@@ -69,6 +116,11 @@ export interface ParticleSnapshot {
69
116
  accumulator: number;
70
117
  count: number;
71
118
  data: number[];
119
+ running?: boolean;
120
+ nextId?: number;
121
+ ids?: number[];
122
+ previousPositions?: number[];
123
+ config?: EmitterConfig;
72
124
  }
73
125
  /** A live, dt-driven particle simulation. */
74
126
  export interface ParticleSystem {
@@ -76,14 +128,20 @@ export interface ParticleSystem {
76
128
  update(dt: number): void;
77
129
  /** Spawn `n` particles immediately (a one-shot puff/explosion), independent of `rate`. */
78
130
  emit(n: number): void;
131
+ /** Resume continuous emission without replacing the live pool. */
132
+ start(): void;
133
+ /** Stop spawning while existing particles continue aging. */
134
+ stop(): void;
135
+ /** Consume bounded presentation collision/death events exactly once. */
136
+ drainEvents(): ParticleEvent[];
79
137
  /** Live particle count. */
80
138
  count(): number;
81
139
  /** The packed buffers for rendering (valid until the next `update`/`emit`). */
82
140
  buffers(): ParticleBuffers;
83
141
  /** Kill all particles and reset the emission accumulator (keeps the seed stream). */
84
142
  clear(): void;
85
- /** Patch the emitter config in place (e.g. move the origin, change the rate). */
86
- configure(patch: Partial<EmitterConfig>): void;
143
+ /** Patch tuning in place; `replace` resets omitted fields to defaults while retaining live particles. */
144
+ configure(patch: Partial<EmitterConfig>, replace?: boolean): void;
87
145
  subscribe(listener: () => void): () => void;
88
146
  snapshot(): ParticleSnapshot;
89
147
  restore(snapshot: ParticleSnapshot): void;
@@ -98,6 +156,11 @@ export interface ParticleSystem {
98
156
  * embers, magic, or confetti. Travel/gameplay stays elsewhere; this owns only
99
157
  * the spawn-integrate-fade lifecycle.
100
158
  *
101
- * @capability particle-system deterministic pooled particle emitter with SoA render buffers, burst/continuous emission, gravity/drag, size/color/alpha-over-life, and serializable state
159
+ * @capability particle-system deterministic pooled particle emitter with spawn volumes, force fields, bounded collision/death events, live tuning, SoA render buffers, and serializable state
160
+ */
161
+ export declare function createParticleSystem(config?: EmitterConfig, options?: ParticleSystemOptions): ParticleSystem;
162
+ /**
163
+ * Validate the serializable spawn/update descriptor before authoring or loading it.
164
+ * @capability particle-emitter-validation validate finite spawn volumes, force fields, lifetimes, appearance curves, and bounded particle collision policies
102
165
  */
103
- export declare function createParticleSystem(config?: EmitterConfig): ParticleSystem;
166
+ export declare function validateEmitterConfig(value: unknown): string[];
@@ -1,25 +1,15 @@
1
+ import { sampleForceField, validateForceField } from "../physics/forceVolume.js";
1
2
  import { hashString, randomSeedFrom, stepRandomSeed } from "../random/rng.js";
2
3
  function clamp01(v) {
3
4
  return v < 0 ? 0 : v > 1 ? 1 : v;
4
5
  }
5
- /**
6
- * A generic, allocation-aware particle system: one emitter, a fixed pool, and
7
- * Structure-of-Arrays buffers a renderer uploads straight to the GPU. It is
8
- * dt-driven (call `update(dt)` each frame) and deterministic — all randomness
9
- * flows from an injected `seed`, so the same seed and dt sequence reproduce the
10
- * same frames, and `snapshot`/`restore` round-trips the live pool. Nothing here
11
- * is combat- or genre-specific: configure it for smoke, sparks, rain, dust,
12
- * embers, magic, or confetti. Travel/gameplay stays elsewhere; this owns only
13
- * the spawn-integrate-fade lifecycle.
14
- *
15
- * @capability particle-system deterministic pooled particle emitter with SoA render buffers, burst/continuous emission, gravity/drag, size/color/alpha-over-life, and serializable state
16
- */
17
- export function createParticleSystem(config = {}) {
18
- const cfg = {
6
+ function emitterDefaults(config) {
7
+ return {
19
8
  max: config.max ?? 512,
20
9
  rate: config.rate ?? 0,
21
10
  position: config.position ?? [0, 0, 0],
22
11
  spawnJitter: config.spawnJitter ?? [0, 0, 0],
12
+ spawnShape: config.spawnShape,
23
13
  lifetime: config.lifetime ?? { min: 1, max: 1 },
24
14
  speed: config.speed ?? { min: 1, max: 1 },
25
15
  direction: config.direction ?? [0, 1, 0],
@@ -30,8 +20,26 @@ export function createParticleSystem(config = {}) {
30
20
  colorStart: config.colorStart ?? 0xffffff,
31
21
  colorEnd: config.colorEnd ?? config.colorStart ?? 0xffffff,
32
22
  alpha: config.alpha ?? { start: 1, end: 0 },
23
+ forces: config.forces,
24
+ collision: config.collision,
25
+ deathEvents: config.deathEvents ?? false,
33
26
  };
34
- const max = Math.max(1, Math.floor(cfg.max));
27
+ }
28
+ /**
29
+ * A generic, allocation-aware particle system: one emitter, a fixed pool, and
30
+ * Structure-of-Arrays buffers a renderer uploads straight to the GPU. It is
31
+ * dt-driven (call `update(dt)` each frame) and deterministic — all randomness
32
+ * flows from an injected `seed`, so the same seed and dt sequence reproduce the
33
+ * same frames, and `snapshot`/`restore` round-trips the live pool. Nothing here
34
+ * is combat- or genre-specific: configure it for smoke, sparks, rain, dust,
35
+ * embers, magic, or confetti. Travel/gameplay stays elsewhere; this owns only
36
+ * the spawn-integrate-fade lifecycle.
37
+ *
38
+ * @capability particle-system deterministic pooled particle emitter with spawn volumes, force fields, bounded collision/death events, live tuning, SoA render buffers, and serializable state
39
+ */
40
+ export function createParticleSystem(config = {}, options = {}) {
41
+ const cfg = emitterDefaults(config);
42
+ const max = Number.isFinite(cfg.max) ? Math.max(1, Math.min(65536, Math.floor(cfg.max))) : 512;
35
43
  // SoA live pool.
36
44
  const px = new Float32Array(max);
37
45
  const py = new Float32Array(max);
@@ -41,6 +49,8 @@ export function createParticleSystem(config = {}) {
41
49
  const vz = new Float32Array(max);
42
50
  const age = new Float32Array(max);
43
51
  const life = new Float32Array(max);
52
+ const ids = new Float64Array(max);
53
+ const previous = new Float32Array(max * 3);
44
54
  // Per-particle baked endpoints (so mid-flight config changes don't recolor existing particles).
45
55
  const sizeS = new Float32Array(max);
46
56
  const sizeE = new Float32Array(max);
@@ -57,8 +67,19 @@ export function createParticleSystem(config = {}) {
57
67
  const outSize = new Float32Array(max);
58
68
  const outColor = new Float32Array(max * 3);
59
69
  const outAlpha = new Float32Array(max);
70
+ const outVelocity = new Float32Array(max * 3);
71
+ const renderBuffers = { count: 0, positions: outPos, sizes: outSize, colors: outColor,
72
+ alphas: outAlpha, ids, velocities: outVelocity, previousPositions: previous };
60
73
  let live = 0;
61
74
  let accumulator = 0;
75
+ let running = true;
76
+ let nextId = 1;
77
+ let events = [];
78
+ let configuredSeed = config.seed;
79
+ const queryFrom = [0, 0, 0];
80
+ const queryTo = [0, 0, 0];
81
+ const sampledForce = [0, 0, 0];
82
+ const queryVelocity = [0, 0, 0];
62
83
  let seed = randomSeedFrom(typeof config.seed === "number" ? config.seed : hashString(String(config.seed ?? "particles")));
63
84
  const listeners = new Set();
64
85
  function notify() {
@@ -80,11 +101,35 @@ export function createParticleSystem(config = {}) {
80
101
  if (live >= max)
81
102
  return;
82
103
  const i = live++;
104
+ ids[i] = nextId++;
83
105
  const [cr0, cg0, cb0] = unpackColor(cfg.colorStart);
84
106
  const [cr1, cg1, cb1] = unpackColor(cfg.colorEnd);
85
107
  px[i] = cfg.position[0] + (rand() * 2 - 1) * cfg.spawnJitter[0];
86
108
  py[i] = cfg.position[1] + (rand() * 2 - 1) * cfg.spawnJitter[1];
87
109
  pz[i] = cfg.position[2] + (rand() * 2 - 1) * cfg.spawnJitter[2];
110
+ const shape = cfg.spawnShape;
111
+ if (shape !== undefined) {
112
+ const theta = rand() * Math.PI * 2;
113
+ if (shape.kind === "sphere") {
114
+ const y = rand() * 2 - 1;
115
+ const radius = shape.radius * (shape.surface ? 1 : Math.cbrt(rand()));
116
+ const horizontal = Math.sqrt(Math.max(0, 1 - y * y)) * radius;
117
+ px[i] += Math.cos(theta) * horizontal;
118
+ py[i] += y * radius;
119
+ pz[i] += Math.sin(theta) * horizontal;
120
+ }
121
+ else {
122
+ const heightFraction = shape.kind === "cone" ? (shape.surface ? Math.sqrt(rand()) : Math.cbrt(rand())) : 1;
123
+ const radius = shape.radius * heightFraction * (shape.surface ? 1 : Math.sqrt(rand()));
124
+ px[i] += Math.cos(theta) * radius;
125
+ pz[i] += Math.sin(theta) * radius;
126
+ if (shape.kind === "cone")
127
+ py[i] += (shape.height ?? shape.radius) * heightFraction;
128
+ }
129
+ }
130
+ previous[i * 3] = px[i];
131
+ previous[i * 3 + 1] = py[i];
132
+ previous[i * 3 + 2] = pz[i];
88
133
  // Direction: base dir perturbed within a cone of half-angle `spread`.
89
134
  let dx = cfg.direction[0];
90
135
  let dy = cfg.direction[1];
@@ -138,6 +183,10 @@ export function createParticleSystem(config = {}) {
138
183
  vz[i] = vz[last];
139
184
  age[i] = age[last];
140
185
  life[i] = life[last];
186
+ ids[i] = ids[last];
187
+ previous[i * 3] = previous[last * 3];
188
+ previous[i * 3 + 1] = previous[last * 3 + 1];
189
+ previous[i * 3 + 2] = previous[last * 3 + 2];
141
190
  sizeS[i] = sizeS[last];
142
191
  sizeE[i] = sizeE[last];
143
192
  r0[i] = r0[last];
@@ -151,47 +200,128 @@ export function createParticleSystem(config = {}) {
151
200
  }
152
201
  live--;
153
202
  }
203
+ function bounded(value, fallback, cap) {
204
+ return Number.isFinite(value) ? Math.max(0, Math.min(cap, Math.floor(value))) : fallback;
205
+ }
206
+ function recordEvent(type, i, normal) {
207
+ events.push({ type, particleId: ids[i], position: [px[i], py[i], pz[i]], velocity: [vx[i], vy[i], vz[i]],
208
+ ...(normal === undefined ? {} : { normal: [normal[0], normal[1], normal[2]] }) });
209
+ }
154
210
  const system = {
155
211
  update(dt) {
156
- if (dt <= 0)
212
+ if (!Number.isFinite(dt) || dt <= 0)
157
213
  return;
158
214
  const damp = cfg.drag > 0 ? Math.max(0, 1 - cfg.drag * dt) : 1;
159
- // Integrate + reap (iterate backwards because we swap-remove).
215
+ const contact = cfg.collision;
216
+ const queryLimit = Math.min(max, bounded(contact?.maxQueries, 64, 4096));
217
+ const eventLimit = bounded(contact?.maxEvents, 64, 1024);
218
+ let queries = 0;
160
219
  for (let i = live - 1; i >= 0; i--) {
161
220
  age[i] += dt;
162
221
  if (age[i] >= life[i]) {
222
+ if (cfg.deathEvents && events.length < eventLimit)
223
+ recordEvent("death", i);
163
224
  swapRemove(i);
164
225
  continue;
165
226
  }
166
- vx[i] = (vx[i] + cfg.gravity[0] * dt) * damp;
167
- vy[i] = (vy[i] + cfg.gravity[1] * dt) * damp;
168
- vz[i] = (vz[i] + cfg.gravity[2] * dt) * damp;
227
+ previous[i * 3] = px[i];
228
+ previous[i * 3 + 1] = py[i];
229
+ previous[i * 3 + 2] = pz[i];
230
+ let ax = cfg.gravity[0], ay = cfg.gravity[1], az = cfg.gravity[2];
231
+ queryFrom[0] = px[i];
232
+ queryFrom[1] = py[i];
233
+ queryFrom[2] = pz[i];
234
+ for (let f = 0; f < Math.min(8, cfg.forces?.length ?? 0); f++) {
235
+ const force = sampleForceField(cfg.forces[f], queryFrom, 0xffffffff, sampledForce);
236
+ ax += force[0];
237
+ ay += force[1];
238
+ az += force[2];
239
+ }
240
+ if (options.acceleration !== undefined) {
241
+ queryVelocity[0] = vx[i];
242
+ queryVelocity[1] = vy[i];
243
+ queryVelocity[2] = vz[i];
244
+ const force = options.acceleration(queryFrom, queryVelocity, sampledForce);
245
+ ax += force[0];
246
+ ay += force[1];
247
+ az += force[2];
248
+ }
249
+ vx[i] = (vx[i] + ax * dt) * damp;
250
+ vy[i] = (vy[i] + ay * dt) * damp;
251
+ vz[i] = (vz[i] + az * dt) * damp;
169
252
  px[i] += vx[i] * dt;
170
253
  py[i] += vy[i] * dt;
171
254
  pz[i] += vz[i] * dt;
255
+ if (contact === undefined)
256
+ continue;
257
+ queryTo[0] = px[i];
258
+ queryTo[1] = py[i];
259
+ queryTo[2] = pz[i];
260
+ let hit = null;
261
+ if (options.collision !== undefined && queries < queryLimit) {
262
+ queries++;
263
+ hit = options.collision(queryFrom, queryTo);
264
+ }
265
+ if (hit === null && contact.planeY !== undefined && queryFrom[1] >= contact.planeY && py[i] <= contact.planeY && vy[i] < 0) {
266
+ const t = (queryFrom[1] - contact.planeY) / (queryFrom[1] - py[i]);
267
+ hit = { position: [queryFrom[0] + (px[i] - queryFrom[0]) * t, contact.planeY, queryFrom[2] + (pz[i] - queryFrom[2]) * t], normal: [0, 1, 0] };
268
+ }
269
+ if (hit === null)
270
+ continue;
271
+ px[i] = hit.position[0];
272
+ py[i] = hit.position[1];
273
+ pz[i] = hit.position[2];
274
+ if (events.length < eventLimit)
275
+ recordEvent("collision", i, hit.normal);
276
+ if (contact.response !== "bounce") {
277
+ swapRemove(i);
278
+ continue;
279
+ }
280
+ const length = Math.hypot(...hit.normal);
281
+ if (length === 0)
282
+ continue;
283
+ const nx = hit.normal[0] / length, ny = hit.normal[1] / length, nz = hit.normal[2] / length;
284
+ const dot = vx[i] * nx + vy[i] * ny + vz[i] * nz;
285
+ if (dot < 0) {
286
+ const impulse = (1 + clamp01(contact.restitution ?? 0.5)) * dot;
287
+ vx[i] -= impulse * nx;
288
+ vy[i] -= impulse * ny;
289
+ vz[i] -= impulse * nz;
290
+ }
172
291
  }
173
- // Continuous emission.
174
- if (cfg.rate > 0) {
175
- accumulator += cfg.rate * dt;
176
- while (accumulator >= 1) {
177
- accumulator -= 1;
292
+ if (running && Number.isFinite(cfg.rate) && cfg.rate > 0) {
293
+ accumulator = Math.min(Number.MAX_SAFE_INTEGER, accumulator + cfg.rate * dt);
294
+ const wanted = Math.floor(accumulator);
295
+ accumulator -= wanted;
296
+ for (let k = 0, n = Math.min(wanted, max - live); k < n; k++)
178
297
  spawnOne();
179
- }
180
298
  }
181
299
  notify();
182
300
  },
183
301
  emit(n) {
184
- const wanted = Math.max(0, Math.floor(n));
302
+ const wanted = Number.isFinite(n) ? Math.min(max - live, Math.max(0, Math.floor(n))) : 0;
185
303
  for (let k = 0; k < wanted; k++)
186
304
  spawnOne();
187
305
  notify();
188
306
  },
307
+ start() { if (!running) {
308
+ running = true;
309
+ notify();
310
+ } },
311
+ stop() { if (running) {
312
+ running = false;
313
+ notify();
314
+ } },
315
+ drainEvents() { const drained = events; events = []; return drained; },
189
316
  count() {
190
317
  return live;
191
318
  },
192
319
  buffers() {
193
320
  for (let i = 0; i < live; i++) {
194
321
  const t = clamp01(age[i] / life[i]);
322
+ outVelocity[i * 3] = vx[i];
323
+ outVelocity[i * 3 + 1] = vy[i];
324
+ outVelocity[i * 3 + 2] = vz[i];
195
325
  outPos[i * 3] = px[i];
196
326
  outPos[i * 3 + 1] = py[i];
197
327
  outPos[i * 3 + 2] = pz[i];
@@ -201,15 +331,21 @@ export function createParticleSystem(config = {}) {
201
331
  outColor[i * 3 + 2] = b0[i] + (b1[i] - b0[i]) * t;
202
332
  outAlpha[i] = a0[i] + (a1[i] - a0[i]) * t;
203
333
  }
204
- return { count: live, positions: outPos, sizes: outSize, colors: outColor, alphas: outAlpha };
334
+ renderBuffers.count = live;
335
+ return renderBuffers;
205
336
  },
206
337
  clear() {
207
338
  live = 0;
208
339
  accumulator = 0;
340
+ events = [];
209
341
  notify();
210
342
  },
211
- configure(patch) {
212
- Object.assign(cfg, patch);
343
+ configure(patch, replace = false) {
344
+ Object.assign(cfg, replace ? emitterDefaults(patch) : patch);
345
+ if (patch.seed !== undefined && patch.seed !== configuredSeed) {
346
+ configuredSeed = patch.seed;
347
+ seed = randomSeedFrom(typeof patch.seed === "number" ? patch.seed : hashString(patch.seed));
348
+ }
213
349
  notify();
214
350
  },
215
351
  subscribe(listener) {
@@ -221,12 +357,20 @@ export function createParticleSystem(config = {}) {
221
357
  for (let i = 0; i < live; i++) {
222
358
  data.push(px[i], py[i], pz[i], vx[i], vy[i], vz[i], age[i], life[i], sizeS[i], sizeE[i], r0[i], g0[i], b0[i], r1[i], g1[i], b1[i], a0[i], a1[i]);
223
359
  }
224
- return { seed: seed, accumulator, count: live, data };
360
+ return { seed: seed, accumulator, count: live, data, running, nextId,
361
+ ids: Array.from(ids.subarray(0, live)), previousPositions: Array.from(previous.subarray(0, live * 3)), config: structuredClone({ ...cfg, seed: configuredSeed }) };
225
362
  },
226
363
  restore(snapshot) {
364
+ if (snapshot.config !== undefined) {
365
+ Object.assign(cfg, structuredClone(snapshot.config));
366
+ configuredSeed = snapshot.config.seed;
367
+ }
227
368
  seed = randomSeedFrom(snapshot.seed);
228
369
  accumulator = snapshot.accumulator;
229
- live = Math.min(max, snapshot.count);
370
+ live = Math.min(max, Math.max(0, Math.floor(snapshot.count)), Math.floor(snapshot.data.length / 18));
371
+ running = snapshot.running ?? true;
372
+ nextId = snapshot.nextId ?? live + 1;
373
+ events = [];
230
374
  const stride = 18;
231
375
  for (let i = 0; i < live; i++) {
232
376
  const o = i * stride;
@@ -248,9 +392,117 @@ export function createParticleSystem(config = {}) {
248
392
  b1[i] = snapshot.data[o + 15];
249
393
  a0[i] = snapshot.data[o + 16];
250
394
  a1[i] = snapshot.data[o + 17];
395
+ ids[i] = snapshot.ids?.[i] ?? i + 1;
396
+ previous[i * 3] = snapshot.previousPositions?.[i * 3] ?? px[i];
397
+ previous[i * 3 + 1] = snapshot.previousPositions?.[i * 3 + 1] ?? py[i];
398
+ previous[i * 3 + 2] = snapshot.previousPositions?.[i * 3 + 2] ?? pz[i];
251
399
  }
252
400
  notify();
253
401
  },
254
402
  };
255
403
  return system;
256
404
  }
405
+ /**
406
+ * Validate the serializable spawn/update descriptor before authoring or loading it.
407
+ * @capability particle-emitter-validation validate finite spawn volumes, force fields, lifetimes, appearance curves, and bounded particle collision policies
408
+ */
409
+ export function validateEmitterConfig(value) {
410
+ const errors = [];
411
+ if (value === null || typeof value !== "object" || Array.isArray(value))
412
+ return ["emitter must be an object"];
413
+ const config = value;
414
+ const finite = (v) => typeof v === "number" && Number.isFinite(v);
415
+ const number = (name, min, max = Infinity, integer = false) => {
416
+ const v = config[name];
417
+ if (v !== undefined && (!finite(v) || v < min || v > max || (integer && !Number.isInteger(v))))
418
+ errors.push(`${name} must be ${integer ? "an integer" : "finite"} in [${min}, ${max}]`);
419
+ };
420
+ const vector = (name, nonnegative = false) => {
421
+ const v = config[name];
422
+ if (v !== undefined && (!Array.isArray(v) || v.length !== 3 || v.some((n) => !finite(n) || (nonnegative && n < 0))))
423
+ errors.push(`${name} must contain three finite${nonnegative ? " nonnegative" : ""} numbers`);
424
+ };
425
+ number("max", 1, 65536, true);
426
+ number("rate", 0);
427
+ number("spread", 0, Math.PI);
428
+ number("drag", 0, 1);
429
+ number("colorStart", 0, 0xffffff, true);
430
+ number("colorEnd", 0, 0xffffff, true);
431
+ vector("position");
432
+ vector("direction");
433
+ vector("gravity");
434
+ vector("spawnJitter", true);
435
+ for (const [name, lower] of [["lifetime", Number.MIN_VALUE], ["speed", 0]]) {
436
+ const v = config[name];
437
+ if (v === undefined)
438
+ continue;
439
+ if (v === null || typeof v !== "object") {
440
+ errors.push(`${name} must be a min/max range`);
441
+ continue;
442
+ }
443
+ const range = v;
444
+ if (!finite(range.min) || !finite(range.max) || range.min < lower || range.max < range.min)
445
+ errors.push(`${name} must have finite ordered min/max values ${name === "lifetime" ? "above zero" : "at least zero"}`);
446
+ }
447
+ for (const name of ["size", "alpha"]) {
448
+ const v = config[name];
449
+ if (v === undefined)
450
+ continue;
451
+ if (v === null || typeof v !== "object") {
452
+ errors.push(`${name} must be a start/end curve`);
453
+ continue;
454
+ }
455
+ const curve = v;
456
+ if (!finite(curve.start) || !finite(curve.end) || curve.start < 0 || curve.end < 0 || (name === "alpha" && (curve.start > 1 || curve.end > 1)))
457
+ errors.push(`${name} must have finite nonnegative start/end values${name === "alpha" ? " at most one" : ""}`);
458
+ }
459
+ if (config.spawnShape !== undefined) {
460
+ const shape = config.spawnShape;
461
+ if (shape === null || typeof shape !== "object" || Array.isArray(shape))
462
+ errors.push("spawnShape must be a volume descriptor");
463
+ else {
464
+ if (shape.kind !== "disc" && shape.kind !== "sphere" && shape.kind !== "cone")
465
+ errors.push("spawnShape.kind must be disc, sphere, or cone");
466
+ if (!finite(shape.radius) || shape.radius <= 0)
467
+ errors.push("spawnShape.radius must be finite and positive");
468
+ if (shape.height !== undefined && (!finite(shape.height) || shape.height <= 0))
469
+ errors.push("spawnShape.height must be finite and positive");
470
+ if (shape.surface !== undefined && typeof shape.surface !== "boolean")
471
+ errors.push("spawnShape.surface must be boolean");
472
+ }
473
+ }
474
+ if (config.seed !== undefined && typeof config.seed !== "string" && !finite(config.seed))
475
+ errors.push("seed must be a string or finite number");
476
+ if (config.deathEvents !== undefined && typeof config.deathEvents !== "boolean")
477
+ errors.push("deathEvents must be boolean");
478
+ if (config.forces !== undefined) {
479
+ if (!Array.isArray(config.forces) || config.forces.length > 8)
480
+ errors.push("forces must be an array of at most eight fields");
481
+ else
482
+ config.forces.forEach((value, i) => {
483
+ try {
484
+ validateForceField(value);
485
+ }
486
+ catch (error) {
487
+ errors.push(`forces[${i}]: ${error instanceof Error ? error.message : String(error)}`);
488
+ }
489
+ });
490
+ }
491
+ if (config.collision !== undefined) {
492
+ const contact = config.collision;
493
+ if (contact === null || typeof contact !== "object" || Array.isArray(contact))
494
+ errors.push("collision must be a contact policy");
495
+ else {
496
+ if (contact.planeY !== undefined && !finite(contact.planeY))
497
+ errors.push("collision.planeY must be finite");
498
+ if (contact.response !== undefined && contact.response !== "kill" && contact.response !== "bounce")
499
+ errors.push("collision.response must be kill or bounce");
500
+ if (contact.restitution !== undefined && (!finite(contact.restitution) || contact.restitution < 0 || contact.restitution > 1))
501
+ errors.push("collision.restitution must be in [0, 1]");
502
+ for (const [name, cap] of [["maxQueries", 4096], ["maxEvents", 1024]])
503
+ if (contact[name] !== undefined && (!finite(contact[name]) || !Number.isInteger(contact[name]) || contact[name] < 0 || contact[name] > cap))
504
+ errors.push(`collision.${name} must be an integer in [0, ${cap}]`);
505
+ }
506
+ }
507
+ return errors;
508
+ }
@@ -11,7 +11,7 @@ export interface CancelSignal {
11
11
  readonly cancelled: boolean;
12
12
  }
13
13
  export interface AssetStreamingOptions {
14
- /** Async loader. Must honor `signal.cancelled` and avoid committing work when it flips true. */
14
+ /** Async loader. Honor `signal.cancelled` and always settle; cancelled successful loads are released through `unload`. */
15
15
  readonly load: (assetId: string, signal: CancelSignal) => Promise<AssetLoadResult>;
16
16
  /** Release GPU/CPU resources for an asset. */
17
17
  readonly unload?: (assetId: string) => void;
@@ -28,6 +28,7 @@ export interface AssetRecord {
28
28
  lastActiveMs: number;
29
29
  value: unknown;
30
30
  }
31
+ /** Current demand/residency gauges and cumulative unload/cancellation counters; clear resets totals. */
31
32
  export interface StreamingStats {
32
33
  queued: number;
33
34
  loading: number;
@@ -36,7 +37,18 @@ export interface StreamingStats {
36
37
  errored: number;
37
38
  inFlight: number;
38
39
  cancelled: number;
40
+ /** Known resident bytes; missing or invalid loader sizes count as zero. */
39
41
  bytes: number;
42
+ /** Current tracked records, including inactive history until forget or clear. */
43
+ records: number;
44
+ /** Known bytes protected by pins or active references. */
45
+ protectedBytes: number;
46
+ /** Loaded assets whose resident size is unknown. */
47
+ unknownSizeLoaded: number;
48
+ /** Known resident bytes above the configured target, including protected bytes. */
49
+ overBudgetBytes: number;
50
+ /** Cumulative resources evicted by resident-byte pressure since clear. */
51
+ budgetEvicted: number;
40
52
  }
41
53
  export interface AssetStreamingSystem {
42
54
  /** Demand an asset (near/in view). Deduplicated — an already loaded/loading/queued asset is never re-fetched concurrently. */
@@ -49,18 +61,24 @@ export interface AssetStreamingSystem {
49
61
  unpin(assetId: string): void;
50
62
  /** Refresh the "last needed" timestamp used by the grace-period unload. */
51
63
  markActive(assetId: string): void;
52
- /** Advance one frame: start up to the load budget, evict past-grace assets up to the unload budget. */
64
+ /** Advance one frame: bounded load starts and unloads; resident-byte pressure precedes idle unloading. */
53
65
  tick(dt: number): void;
54
- /** Cancel an in-flight or queued request that is no longer needed. */
66
+ /** Cancel demand. Unresolved loads still occupy concurrency slots and block same-id retries until settlement. */
55
67
  cancel(assetId: string): void;
68
+ /** Merge policy. Zero concurrency pauses starts; resident-byte target/order changes apply on the next tick. */
69
+ retune(settings: Partial<StreamingSettings>): void;
56
70
  stateOf(assetId: string): AssetLoadState | undefined;
57
71
  isLoaded(assetId: string): boolean;
58
72
  record(assetId: string): AssetRecord | undefined;
73
+ /** Drop inactive unloaded/error history. Returns false for missing, pinned, retained, queued, loaded or unresolved assets. Old record handles remain detached; future demand creates a new record. */
74
+ forget(assetId: string): boolean;
59
75
  stats(): StreamingStats;
60
76
  /** Merge asset counters into a per-frame VisibilityStats. */
61
77
  applyTo(stats: VisibilityStats): void;
62
78
  /** Resolve once every in-flight load settles — for deterministic tests. */
63
79
  settle(): Promise<void>;
80
+ /** Release loaded resources and cancel demand; outstanding loads remain tracked until settlement. */
64
81
  clear(): void;
65
82
  }
83
+ /** Stream shared resources with bounded load concurrency, per-tick resident-byte eviction, and explicit inactive-history cleanup. */
66
84
  export declare function createAssetStreamingSystem(options: AssetStreamingOptions): AssetStreamingSystem;