@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,72 @@
1
+ const DEFAULT_MAX_CATCH_UP = 5;
2
+ const DEFAULT_SNAP_DISTANCE = 8;
3
+ function resolveConfig(config) {
4
+ const hz = config?.hz;
5
+ return {
6
+ hz: typeof hz === "number" && Number.isFinite(hz) && hz > 0 ? hz : "variable",
7
+ maxCatchUpSteps: Math.max(1, Math.floor(config?.maxCatchUpSteps ?? DEFAULT_MAX_CATCH_UP)),
8
+ snapDistance: config?.snapDistance ?? DEFAULT_SNAP_DISTANCE,
9
+ };
10
+ }
11
+ /**
12
+ * The simulation stepper every driver (shell frame, headless runner, authoritative host) advances through, so
13
+ * the sim sees the same dt sequence regardless of who drives it. Fixed mode accumulates real time and steps in
14
+ * equal slices with a catch-up cap; variable mode steps once per advance.
15
+ *
16
+ * @capability sim-loop fixed-step or variable simulation stepping with tick count, catch-up cap, and interpolation alpha
17
+ */
18
+ export function createSimLoop(initial) {
19
+ let config = resolveConfig(initial);
20
+ let tick = 0;
21
+ let accumulator = 0;
22
+ let alpha = 1;
23
+ function stepSeconds() {
24
+ return config.hz === "variable" ? null : 1 / config.hz;
25
+ }
26
+ return {
27
+ advance(realDt, step) {
28
+ if (!(realDt > 0))
29
+ return { steps: 0, alpha, tick, dropped: 0 };
30
+ const stepDt = stepSeconds();
31
+ if (stepDt === null) {
32
+ tick += 1;
33
+ alpha = 1;
34
+ step(realDt, tick);
35
+ return { steps: 1, alpha, tick, dropped: 0 };
36
+ }
37
+ accumulator += realDt;
38
+ let steps = 0;
39
+ while (accumulator >= stepDt && steps < config.maxCatchUpSteps) {
40
+ tick += 1;
41
+ step(stepDt, tick);
42
+ accumulator -= stepDt;
43
+ steps += 1;
44
+ }
45
+ let dropped = 0;
46
+ if (accumulator >= stepDt) {
47
+ dropped = Math.floor(accumulator / stepDt);
48
+ accumulator -= dropped * stepDt;
49
+ }
50
+ alpha = accumulator / stepDt;
51
+ return { steps, alpha, tick, dropped };
52
+ },
53
+ tick: () => tick,
54
+ alpha: () => alpha,
55
+ stepSeconds,
56
+ isFixed: () => config.hz !== "variable",
57
+ retune(next) {
58
+ config = resolveConfig({ ...config, ...next });
59
+ if (config.hz === "variable") {
60
+ accumulator = 0;
61
+ alpha = 1;
62
+ }
63
+ },
64
+ config: () => config,
65
+ snapshot: () => ({ tick, accumulator }),
66
+ restore(state) {
67
+ tick = state.tick;
68
+ accumulator = state.accumulator;
69
+ alpha = config.hz === "variable" ? 1 : accumulator * config.hz;
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,32 @@
1
+ import type { GameContext } from "./gameContext.js";
2
+ /** One piece of simulation state that can be captured and restored by id. */
3
+ export interface SimSnapshotContributor<T = unknown> {
4
+ id: string;
5
+ capture(): T;
6
+ restore(data: T): void;
7
+ }
8
+ /** Captured contributor data keyed by contributor id. */
9
+ export type SimSnapshot = Record<string, unknown>;
10
+ /** Registry of {@link SimSnapshotContributor}s that captures and restores them together. */
11
+ export interface SimSnapshotRegistry {
12
+ register<T>(contributor: SimSnapshotContributor<T>): () => void;
13
+ capture(): SimSnapshot;
14
+ /** Alias of {@link capture}, matching the engine's snapshot/restore naming. */
15
+ snapshot(): SimSnapshot;
16
+ /** Restore every contributor present in `snapshot`; contributors missing from it are left untouched. */
17
+ restore(snapshot: SimSnapshot): void;
18
+ ids(): string[];
19
+ }
20
+ /**
21
+ * A registry of everything that must round-trip for a simulation to resume bit-for-bit: world state, the
22
+ * clock, the rng cursor, the loop accumulator, and whatever a physics backend or game adds. Pair with
23
+ * {@link createInputRecorder} for replay.
24
+ *
25
+ * @capability sim-snapshot capture and restore whole-simulation state for replay, rollback, and regression tests
26
+ */
27
+ export declare function createSimSnapshotRegistry(): SimSnapshotRegistry;
28
+ /**
29
+ * The engine's own contributors for a context: replicated world state, the sim clock, the loop and pose
30
+ * history, and the rng cursor when `ctx.rng` is a seeded stream. Games register theirs on the same registry.
31
+ */
32
+ export declare function createContextSimSnapshot(ctx: GameContext): SimSnapshotRegistry;
@@ -0,0 +1,60 @@
1
+ import { rngStateOf, restoreRng } from "../random/rng.js";
2
+ /**
3
+ * A registry of everything that must round-trip for a simulation to resume bit-for-bit: world state, the
4
+ * clock, the rng cursor, the loop accumulator, and whatever a physics backend or game adds. Pair with
5
+ * {@link createInputRecorder} for replay.
6
+ *
7
+ * @capability sim-snapshot capture and restore whole-simulation state for replay, rollback, and regression tests
8
+ */
9
+ export function createSimSnapshotRegistry() {
10
+ const contributors = new Map();
11
+ function capture() {
12
+ const out = {};
13
+ for (const [id, contributor] of contributors)
14
+ out[id] = contributor.capture();
15
+ return out;
16
+ }
17
+ return {
18
+ register(contributor) {
19
+ if (contributors.has(contributor.id)) {
20
+ throw new Error(`sim snapshot contributor "${contributor.id}" is already registered`);
21
+ }
22
+ contributors.set(contributor.id, contributor);
23
+ return () => {
24
+ contributors.delete(contributor.id);
25
+ };
26
+ },
27
+ capture,
28
+ snapshot: capture,
29
+ restore(snapshot) {
30
+ for (const [id, contributor] of contributors) {
31
+ if (id in snapshot)
32
+ contributor.restore(snapshot[id]);
33
+ }
34
+ },
35
+ ids: () => Array.from(contributors.keys()),
36
+ };
37
+ }
38
+ /**
39
+ * The engine's own contributors for a context: replicated world state, the sim clock, the loop and pose
40
+ * history, and the rng cursor when `ctx.rng` is a seeded stream. Games register theirs on the same registry.
41
+ */
42
+ export function createContextSimSnapshot(ctx) {
43
+ const registry = createSimSnapshotRegistry();
44
+ // ctx.snapshot() hands back live rows; clone both ways so a captured snapshot stays frozen while the sim keeps running.
45
+ registry.register({
46
+ id: "world",
47
+ capture: () => structuredClone(ctx.snapshot()),
48
+ restore: (data) => ctx.hydrate(structuredClone(data)),
49
+ });
50
+ registry.register({ id: "clock", capture: () => ctx.time.snapshot(), restore: (data) => ctx.time.hydrate(data) });
51
+ registry.register({ id: "sim", capture: () => ctx.sim.snapshot(), restore: (data) => ctx.sim.restore(data) });
52
+ if (rngStateOf(ctx.rng) !== null) {
53
+ registry.register({
54
+ id: "rng",
55
+ capture: () => rngStateOf(ctx.rng) ?? 0,
56
+ restore: (cursor) => restoreRng(ctx.rng, cursor),
57
+ });
58
+ }
59
+ return registry;
60
+ }
@@ -36,6 +36,8 @@ export type RuntimeInventorySlot = {
36
36
  slot?: number;
37
37
  };
38
38
  export type RuntimePlayerRow = {
39
+ territoryOwnedCount?: number;
40
+ ownedTerritoryChunkKeys?: string[];
39
41
  userId: string;
40
42
  inventories: Record<string, RuntimeInventorySlot[]>;
41
43
  economy: Record<string, number>;
@@ -46,6 +48,12 @@ export type RuntimePlayerRow = {
46
48
  session?: Record<string, unknown>;
47
49
  };
48
50
  export type RuntimeChunkRow = {
51
+ /** Cell key to owner id, persisted with its spatial chunk. */
52
+ territory?: Record<string, string>;
53
+ territoryReceipts?: Record<string, {
54
+ claimedAt: number;
55
+ costPaid: number;
56
+ }>;
49
57
  chunkKey: string;
50
58
  objects: RuntimeObjectRow[];
51
59
  entities: RuntimeEntityRow[];
@@ -0,0 +1,33 @@
1
+ /** A sampled pair of snapshots and interpolation factor. */
2
+ export type SnapshotBufferSample<T> = {
3
+ before: T;
4
+ after: T;
5
+ alpha: number;
6
+ };
7
+ /** Serializable snapshot-buffer state. */
8
+ export type SnapshotBufferState<T> = {
9
+ delayMs: number;
10
+ capacity: number;
11
+ entries: Array<{
12
+ atMs: number;
13
+ value: T;
14
+ }>;
15
+ };
16
+ /** Handle for a timestamped delayed snapshot buffer. */
17
+ export type SnapshotBuffer<T> = {
18
+ push(atMs: number, value: T): void;
19
+ sampleAt(nowMs: number): SnapshotBufferSample<T> | null;
20
+ snapshot(): SnapshotBufferState<T>;
21
+ restore(next: SnapshotBufferState<T>): void;
22
+ retune(config: {
23
+ delayMs?: number;
24
+ capacity?: number;
25
+ }): void;
26
+ };
27
+ /** Stores timestamped snapshots and samples a delayed, interpolatable pair.
28
+ * @capability runtime snapshot interpolation
29
+ */
30
+ export declare function createSnapshotBuffer<T>(config: {
31
+ delayMs: number;
32
+ capacity: number;
33
+ }): SnapshotBuffer<T>;
@@ -0,0 +1,58 @@
1
+ /** Stores timestamped snapshots and samples a delayed, interpolatable pair.
2
+ * @capability runtime snapshot interpolation
3
+ */
4
+ export function createSnapshotBuffer(config) {
5
+ let delayMs = Math.max(0, config.delayMs);
6
+ let capacity = Math.max(1, Math.floor(config.capacity));
7
+ let entries = [];
8
+ const trim = () => {
9
+ if (entries.length > capacity)
10
+ entries = entries.slice(-capacity);
11
+ };
12
+ return {
13
+ push(atMs, value) {
14
+ const entry = { atMs, value };
15
+ const index = entries.findIndex((item) => item.atMs > atMs);
16
+ if (index < 0)
17
+ entries.push(entry);
18
+ else
19
+ entries.splice(index, 0, entry);
20
+ trim();
21
+ },
22
+ sampleAt(nowMs) {
23
+ if (entries.length === 0)
24
+ return null;
25
+ const target = nowMs - delayMs;
26
+ if (target <= entries[0].atMs)
27
+ return { before: entries[0].value, after: entries[0].value, alpha: 0 };
28
+ const last = entries[entries.length - 1];
29
+ if (target >= last.atMs)
30
+ return { before: last.value, after: last.value, alpha: 1 };
31
+ for (let i = 1; i < entries.length; i += 1) {
32
+ const after = entries[i];
33
+ if (target <= after.atMs) {
34
+ const before = entries[i - 1];
35
+ const span = after.atMs - before.atMs;
36
+ return { before: before.value, after: after.value, alpha: span === 0 ? 1 : (target - before.atMs) / span };
37
+ }
38
+ }
39
+ return null;
40
+ },
41
+ snapshot() {
42
+ return { delayMs, capacity, entries: entries.map((entry) => ({ ...entry })) };
43
+ },
44
+ restore(next) {
45
+ delayMs = Math.max(0, next.delayMs);
46
+ capacity = Math.max(1, Math.floor(next.capacity));
47
+ entries = next.entries.map((entry) => ({ ...entry })).sort((a, b) => a.atMs - b.atMs);
48
+ trim();
49
+ },
50
+ retune(config) {
51
+ if (config.delayMs !== undefined)
52
+ delayMs = Math.max(0, config.delayMs);
53
+ if (config.capacity !== undefined)
54
+ capacity = Math.max(1, Math.floor(config.capacity));
55
+ trim();
56
+ },
57
+ };
58
+ }
@@ -1,10 +1,38 @@
1
1
  import type { ChatSync } from "../multiplayer/chatContract.js";
2
2
  import type { PlayerPose } from "../multiplayer/poseSyncGate.js";
3
3
  import type { PresenceTransport } from "../multiplayer/presenceContract.js";
4
+ import type { WorldDiff } from "./worldReplication.js";
5
+ import type { WorldSnapshot } from "./worldSnapshot.js";
6
+ /** A revision-stamped world baseline or compact diff sent to a server-authoritative client. */
7
+ export type WorldSyncFrame = {
8
+ kind: "baseline";
9
+ revision: number;
10
+ snapshot: WorldSnapshot;
11
+ } | {
12
+ kind: "diff";
13
+ revision: number;
14
+ diff: WorldDiff;
15
+ };
16
+ /** A short-lived credential allowing a disconnected client to reclaim one server membership. */
17
+ export type ResumeTicket = {
18
+ userId: string;
19
+ serverId: string;
20
+ token: string;
21
+ };
4
22
  export type JoinServerResult = {
5
23
  serverId: string;
6
24
  isNew: boolean;
25
+ resumeTicket?: ResumeTicket;
26
+ };
27
+ /** Join failures are returned to the caller so the triggering surface can offer retry. */
28
+ export type JoinServerOutcome = (JoinServerResult & {
29
+ ok: true;
30
+ }) | {
31
+ ok: false;
32
+ reason: "full" | "closed" | "unauthorized";
7
33
  };
34
+ /** Connection role used for authoritative world access; spectators are read-only. */
35
+ export type MultiplayerRole = "player" | "spectator";
8
36
  export type RunCommandArgs = {
9
37
  serverId: string;
10
38
  command: string;
@@ -21,7 +49,7 @@ export type GameRuntimeServerView = {
21
49
  gameId: string;
22
50
  revision: number;
23
51
  memberUserIds: string[];
24
- serverState: unknown;
52
+ serverState: unknown | WorldSyncFrame;
25
53
  updatedAt: number;
26
54
  };
27
55
  export type GameRuntimePlayerView = {
@@ -35,15 +63,25 @@ export type GameRuntimeFeedView = {
35
63
  entries: unknown[];
36
64
  };
37
65
  export type GameRuntimeTransport = {
66
+ /**
67
+ * Join a server. `sessionId` names this client session (one tab, one mounted view) so the host keeps
68
+ * the user a member while any of their sessions is still live.
69
+ */
38
70
  joinServer: (args: {
39
71
  gameId: string;
40
72
  serverId?: string;
41
- }) => Promise<JoinServerResult>;
73
+ role?: MultiplayerRole;
74
+ sessionId?: string;
75
+ }) => Promise<JoinServerOutcome>;
76
+ /** Leave a server. With `sessionId` only that session ends; the user leaves once no session remains. Without it the user leaves outright. */
42
77
  leaveServer: (args: {
43
78
  serverId: string;
79
+ sessionId?: string;
44
80
  }) => Promise<void>;
45
81
  runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>;
46
82
  };
83
+ /** Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}). */
84
+ export declare function createTransportSessionId(): string;
47
85
  export type FeedUnsubscribe = () => void;
48
86
  export type GameRuntimeFeeds = {
49
87
  subscribeServer: (serverId: string, onChange: (view: GameRuntimeServerView | null) => void) => FeedUnsubscribe;
@@ -54,6 +92,7 @@ export type GameRuntimeFeeds = {
54
92
  serverId: string;
55
93
  action: string;
56
94
  }, onChange: (view: GameRuntimeFeedView) => void) => FeedUnsubscribe;
95
+ requestServerBaseline?: (serverId: string) => void;
57
96
  };
58
97
  export type GameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = {
59
98
  transport: GameRuntimeTransport;
@@ -1 +1,7 @@
1
- export {};
1
+ /** Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}). */
2
+ export function createTransportSessionId() {
3
+ const cryptoApi = globalThis.crypto;
4
+ if (cryptoApi?.randomUUID)
5
+ return cryptoApi.randomUUID();
6
+ return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
7
+ }
@@ -37,3 +37,5 @@ export declare function setChunk(snapshot: GameRuntimeSnapshot, chunkKey: string
37
37
  export declare function updateChunk(snapshot: GameRuntimeSnapshot, chunkKey: string, update: (chunk: RuntimeChunkRow) => RuntimeChunkRow): GameRuntimeSnapshot;
38
38
  /** Drop a chunk from a snapshot and mark the key dirty so the host deletes its stored row. */
39
39
  export declare function deleteChunk(snapshot: GameRuntimeSnapshot, chunkKey: string): GameRuntimeSnapshot;
40
+ /** Square chunk neighborhood around a world position, including its own chunk. */
41
+ export declare function chunkKeysAround(position: readonly [number, number, number], rings?: number, size?: number): string[];
@@ -81,3 +81,17 @@ export function deleteChunk(snapshot, chunkKey) {
81
81
  dirty: { ...snapshot.dirty, server: true, chunks: withChunkDirty(snapshot, chunkKey) },
82
82
  };
83
83
  }
84
+ /** Square chunk neighborhood around a world position, including its own chunk. */
85
+ export function chunkKeysAround(position, rings = 1, size = DEFAULT_CHUNK_SIZE) {
86
+ if (!Number.isSafeInteger(rings) || rings < 0 || rings > 128)
87
+ throw new RangeError("rings must be an integer from 0 to 128");
88
+ if (!Number.isFinite(size) || size <= 0 || !position.every(Number.isFinite))
89
+ throw new RangeError("Invalid chunk position or size");
90
+ const center = chunkCoordAt(position, size);
91
+ const keys = [];
92
+ for (let cx = center.cx - rings; cx <= center.cx + rings; cx++) {
93
+ for (let cz = center.cz - rings; cz <= center.cz + rings; cz++)
94
+ keys.push(chunkKeyOf({ cx, cz }));
95
+ }
96
+ return keys;
97
+ }
@@ -8,6 +8,11 @@ import type { SnapshotViewer, WorldSnapshot } from "./worldSnapshot.js";
8
8
  * growing a per-feature branch.
9
9
  */
10
10
  export interface ReplicationPolicy {
11
+ /** Decimal places used for replicated positions and angles. */
12
+ quantize?: {
13
+ position?: number;
14
+ angle?: number;
15
+ };
11
16
  /**
12
17
  * Replicate each player's private per-user state (inventory) only to that player — other viewers
13
18
  * receive none of it. Off by default so existing games keep broadcasting every player's state.
@@ -24,6 +29,10 @@ export interface ReplicationPolicy {
24
29
  * @internal host projection plumbing — games set ReplicationPolicy; the runtime applies it.
25
30
  */
26
31
  export declare function policyProjectsViewers(policy: ReplicationPolicy | undefined): boolean;
32
+ /** Quantize replicated entity poses according to a host projection policy.
33
+ * @internal host projection plumbing
34
+ */
35
+ export declare function quantizeWorldForReplication(world: WorldSnapshot, policy: ReplicationPolicy | undefined): WorldSnapshot;
27
36
  /**
28
37
  * Cull an entity list to a viewer's area of interest: keep the viewer's own entity plus every entity
29
38
  * within `radius` of it. When the viewer has no locatable entity the full list is returned (fail-open —
@@ -4,7 +4,23 @@
4
4
  export function policyProjectsViewers(policy) {
5
5
  if (policy === undefined)
6
6
  return false;
7
- return policy.privatePerUser === true || policy.aoiRadius !== undefined;
7
+ return policy.privatePerUser === true || policy.aoiRadius !== undefined || policy.quantize !== undefined;
8
+ }
9
+ /** Quantize replicated entity poses according to a host projection policy.
10
+ * @internal host projection plumbing
11
+ */
12
+ export function quantizeWorldForReplication(world, policy) {
13
+ if (policy?.quantize?.position === undefined && policy?.quantize?.angle === undefined)
14
+ return world;
15
+ const digits = (value, places) => places === undefined ? value : Number(value.toFixed(Math.max(0, places)));
16
+ const entities = world["entities"]?.map((entity) => ({
17
+ ...entity,
18
+ position: entity.position.map((value) => digits(value, policy.quantize?.position)),
19
+ rotationY: digits(entity.rotationY, policy.quantize?.angle),
20
+ rotationX: digits(entity.rotationX, policy.quantize?.angle),
21
+ rotationZ: digits(entity.rotationZ, policy.quantize?.angle),
22
+ }));
23
+ return entities === undefined ? world : { ...world, entities };
8
24
  }
9
25
  function distanceSquared(a, b) {
10
26
  const dx = a[0] - b[0];
@@ -1,6 +1,10 @@
1
1
  /** Who a host→client snapshot is being projected for — the identity a {@link SnapshotModule.project} filters against. */
2
2
  export interface SnapshotViewer {
3
3
  readonly userId: string;
4
+ /** A read-only connection role; spectators receive snapshots but cannot mutate the world. */
5
+ readonly role?: "player" | "spectator";
6
+ /** Host tick used by {@link SnapshotModule.priority} to throttle low-priority modules. */
7
+ readonly tick?: number;
4
8
  }
5
9
  /**
6
10
  * The replication seam for host-authoritative shared worlds: the opt-in feature manifest *is* the
@@ -37,6 +41,8 @@ export interface SnapshotModule<T = unknown> {
37
41
  project?(data: T, viewer: SnapshotViewer, world: WorldSnapshot): T;
38
42
  /** Monotone change counter; unchanged between host commits means this module didn't mutate and need not re-serialize. */
39
43
  version?(): number;
44
+ /** Return the host tick interval for this module; values greater than one omit it on intervening ticks. */
45
+ priority?(data: T, viewer: SnapshotViewer): number;
40
46
  }
41
47
  /** Full world baseline keyed by {@link SnapshotModule.key} — one entry per opted-in subsystem. */
42
48
  export type WorldSnapshot = Record<string, unknown>;
@@ -13,6 +13,9 @@ export function composeWorldSnapshot(modules, viewer) {
13
13
  const snapshot = {};
14
14
  for (const module of modules) {
15
15
  const value = raw[module.key];
16
+ const interval = viewer?.tick === undefined ? 1 : Math.max(1, Math.floor(module.priority?.(value, viewer) ?? 1));
17
+ if (viewer?.tick !== undefined && interval > 1 && viewer.tick % interval !== 0)
18
+ continue;
16
19
  snapshot[module.key] =
17
20
  module.project !== undefined ? module.project(value, viewer, raw) : value;
18
21
  }
@@ -1,8 +1,27 @@
1
+ import type { Blackboard, DecisionAbort, DecisionAction, DecisionGraphRuntime } from "../ai/decisionGraph.js";
2
+ import { type InterestGateState } from "../ai/interestScheduler.js";
1
3
  import { type PathFollowState, type PathProgress, type Waypoint } from "../nav/pathFollow.js";
2
4
  import type { GameContext } from "../runtime/gameContext.js";
3
5
  /** Whether a behavior instance advances and writes pose (`active`), is temporarily suspended retaining
4
6
  * state (`paused`), or is held off until re-enabled (`disabled`). */
5
7
  export type BehaviorStatus = "active" | "paused" | "disabled";
8
+ /** Context supplied to a registered decision graph action. */
9
+ export interface BehaviorActionContext {
10
+ ctx: GameContext;
11
+ entityId: string;
12
+ dt: number;
13
+ }
14
+ /** Callback used by a registered decision graph behavior action. */
15
+ export type BehaviorAction = DecisionAction<BehaviorActionContext>;
16
+ /** Optional hooks for a registered behavior action set. */
17
+ export interface BehaviorActionOptions {
18
+ /** Called when an action running last think is pre-empted by another branch, to stop movement or release claims. */
19
+ onAbort?: DecisionAbort<BehaviorActionContext>;
20
+ }
21
+ /** Register the named actions used by decision graph behavior descriptors.
22
+ * @capability behavior-actions register named callbacks for decision graph behavior descriptors
23
+ */
24
+ export declare function registerBehaviorActions(id: string, actions: Record<string, BehaviorAction>, options?: BehaviorActionOptions): () => void;
6
25
  /**
7
26
  * Catch-up policy applied when a paused instance resumes. `freeze` (default, deterministic) discards
8
27
  * the time spent paused; `advance` silently fast-forwards the instance by the paused duration without
@@ -18,11 +37,18 @@ export type BehaviorSnapshot = {
18
37
  readonly kind: "wander";
19
38
  readonly origin: Waypoint;
20
39
  readonly target: Waypoint | null;
40
+ } | {
41
+ readonly kind: "decisionGraph";
42
+ readonly graph: ReturnType<DecisionGraphRuntime["snapshot"]>;
43
+ readonly gate: InterestGateState;
44
+ readonly blackboard?: Blackboard;
45
+ /** Seconds accrued since the last think. */
46
+ readonly pending?: number;
21
47
  };
22
48
  /** Inspection readout for editor/debug tooling, from {@link BehaviorControl.inspect}/{@link BehaviorControl.list}. */
23
49
  export interface BehaviorInspection {
24
50
  id: string;
25
- kind: "patrol" | "wander";
51
+ kind: "patrol" | "wander" | "decisionGraph";
26
52
  status: BehaviorStatus;
27
53
  reason: string | null;
28
54
  }
@@ -54,6 +80,8 @@ export interface BehaviorControl {
54
80
  inspect(id: string): BehaviorInspection | null;
55
81
  /** Inspect every live instance (bounded by spawned behavior entities). */
56
82
  list(): BehaviorInspection[];
83
+ /** The live, persistent blackboard of a decision graph instance for perception and game systems to write facts into, or `null`. */
84
+ blackboard(id: string): Blackboard | null;
57
85
  }
58
86
  /**
59
87
  * Advance every spawned entity carrying a `patrol` or `wander` {@link BehaviorDescriptor} one tick — the
@@ -1,4 +1,5 @@
1
- import { seededRng } from "../random/rng.js";
1
+ import { createDecisionGraphRuntime } from "../ai/decisionGraph.js";
2
+ import { advanceInterestGate, createInterestGateState } from "../ai/interestScheduler.js";
2
3
  import { advancePathFollow, createPathFollow, pathFollowSeek, } from "../nav/pathFollow.js";
3
4
  import { resolveWalkerStep } from "../movement/solidObstacles.js";
4
5
  import { perContext } from "../runtime/perContext.js";
@@ -6,12 +7,30 @@ const DEFAULT_WANDER_SPEED = 1.5;
6
7
  const WANDER_ARRIVAL = 0.6;
7
8
  /** Below this share of its intended step a wanderer counts as blocked and re-rolls its target. */
8
9
  const WANDER_BLOCKED_FRACTION = 0.25;
10
+ const behaviorActions = new Map();
11
+ /** Register the named actions used by decision graph behavior descriptors.
12
+ * @capability behavior-actions register named callbacks for decision graph behavior descriptors
13
+ */
14
+ export function registerBehaviorActions(id, actions, options = {}) {
15
+ const entry = { actions, options };
16
+ behaviorActions.set(id, entry);
17
+ return () => {
18
+ if (behaviorActions.get(id) === entry)
19
+ behaviorActions.delete(id);
20
+ };
21
+ }
22
+ function thinkCadence(behavior) {
23
+ return { wakeRadius: Number.POSITIVE_INFINITY, activeInterval: Math.max(0, behavior.thinkInterval ?? 0) };
24
+ }
9
25
  function patrolOf(entity) {
10
26
  return entity.behaviors.find((b) => b.kind === "patrol") ?? null;
11
27
  }
12
28
  function wanderOf(entity) {
13
29
  return entity.behaviors.find((b) => b.kind === "wander") ?? null;
14
30
  }
31
+ function decisionGraphOf(entity) {
32
+ return entity.behaviors.find((b) => b.kind === "decisionGraph") ?? null;
33
+ }
15
34
  const runtimeOf = perContext((ctx) => {
16
35
  const nav = new Map();
17
36
  const refresh = () => {
@@ -41,7 +60,30 @@ const runtimeOf = perContext((ctx) => {
41
60
  radius: wander.radius,
42
61
  origin: entity.position,
43
62
  target: null,
44
- roll: seededRng(`wander:${entity.id}`),
63
+ roll: () => ctx.rng(),
64
+ status: "active",
65
+ reason: null,
66
+ pausedElapsed: 0,
67
+ });
68
+ continue;
69
+ }
70
+ const decisionGraph = decisionGraphOf(entity);
71
+ if (decisionGraph !== null) {
72
+ const registered = behaviorActions.get(decisionGraph.actions);
73
+ if (registered === undefined)
74
+ continue;
75
+ const cadence = thinkCadence(decisionGraph);
76
+ nav.set(entity.id, {
77
+ kind: "decisionGraph",
78
+ behavior: decisionGraph,
79
+ runtime: createDecisionGraphRuntime(decisionGraph.graph, registered.actions, {
80
+ rng: () => ctx.rng(),
81
+ ...(registered.options.onAbort === undefined ? {} : { onAbort: registered.options.onAbort }),
82
+ }),
83
+ gate: createInterestGateState(cadence, ctx.rng()),
84
+ cadence,
85
+ blackboard: { ...(decisionGraph.blackboard ?? {}) },
86
+ pending: 0,
45
87
  status: "active",
46
88
  reason: null,
47
89
  pausedElapsed: 0,
@@ -135,9 +177,21 @@ export function advanceBehaviors(ctx, dt) {
135
177
  entry.state = advancePathFollow(entry.config, entry.state, dt);
136
178
  posePatrol(ctx, id, entry, dt);
137
179
  }
138
- else {
180
+ else if (entry.kind === "wander") {
139
181
  stepWander(ctx, id, entry, dt);
140
182
  }
183
+ else {
184
+ const entity = ctx.scene.entity.get(id);
185
+ if (entity === null)
186
+ continue;
187
+ entry.pending += dt;
188
+ const step = advanceInterestGate(entry.gate, entry.cadence, dt, { proximity: 0 });
189
+ if (!step.active)
190
+ continue;
191
+ const elapsed = entry.pending;
192
+ entry.pending = 0;
193
+ entry.runtime.tick({ ctx, entityId: id, dt: elapsed }, entry.blackboard, elapsed);
194
+ }
141
195
  }
142
196
  }
143
197
  /**
@@ -206,7 +260,9 @@ export function behaviorControl(ctx) {
206
260
  return null;
207
261
  if (entry.kind === "patrol")
208
262
  return { kind: "patrol", state: { ...entry.state } };
209
- return { kind: "wander", origin: [...entry.origin], target: entry.target === null ? null : [...entry.target] };
263
+ if (entry.kind === "wander")
264
+ return { kind: "wander", origin: [...entry.origin], target: entry.target === null ? null : [...entry.target] };
265
+ return { kind: "decisionGraph", graph: entry.runtime.snapshot(), gate: { ...entry.gate }, blackboard: { ...entry.blackboard }, pending: entry.pending };
210
266
  },
211
267
  restore: (id, snapshot) => {
212
268
  const entry = nav.get(id);
@@ -216,6 +272,13 @@ export function behaviorControl(ctx) {
216
272
  entry.state = { ...snapshot.state };
217
273
  return true;
218
274
  }
275
+ if (entry.kind === "decisionGraph" && snapshot.kind === "decisionGraph") {
276
+ entry.runtime.restore(snapshot.graph);
277
+ entry.gate = { ...snapshot.gate };
278
+ entry.blackboard = { ...(snapshot.blackboard ?? entry.behavior.blackboard ?? {}) };
279
+ entry.pending = snapshot.pending ?? 0;
280
+ return true;
281
+ }
219
282
  if (entry.kind === "wander" && snapshot.kind === "wander") {
220
283
  entry.origin = [...snapshot.origin];
221
284
  entry.target = snapshot.target === null ? null : [...snapshot.target];
@@ -235,5 +298,9 @@ export function behaviorControl(ctx) {
235
298
  out.push({ id, kind: entry.kind, status: entry.status, reason: entry.reason });
236
299
  return out;
237
300
  },
301
+ blackboard: (id) => {
302
+ const entry = nav.get(id);
303
+ return entry?.kind === "decisionGraph" ? entry.blackboard : null;
304
+ },
238
305
  };
239
306
  }