@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,212 @@
1
+ function compare(left, op, right) {
2
+ switch (op) {
3
+ case "=":
4
+ case "==":
5
+ case "eq":
6
+ return left === right;
7
+ case "!=":
8
+ case "ne":
9
+ return left !== right;
10
+ case "<":
11
+ case "lt":
12
+ return typeof left === "number" && typeof right === "number" && left < right;
13
+ case "<=":
14
+ case "lte":
15
+ return typeof left === "number" && typeof right === "number" && left <= right;
16
+ case ">":
17
+ case "gt":
18
+ return typeof left === "number" && typeof right === "number" && left > right;
19
+ case ">=":
20
+ case "gte":
21
+ return typeof left === "number" && typeof right === "number" && left >= right;
22
+ }
23
+ }
24
+ function childrenOf(node) {
25
+ switch (node.kind) {
26
+ case "selector":
27
+ case "sequence":
28
+ case "random":
29
+ return node.children;
30
+ case "utility":
31
+ return node.options.map((option) => option.node);
32
+ case "invert":
33
+ case "cooldown":
34
+ return [node.child];
35
+ default:
36
+ return [];
37
+ }
38
+ }
39
+ function compile(root) {
40
+ const byId = [];
41
+ const visit = (node, path) => {
42
+ const entry = { node, id: byId.length, path, children: [] };
43
+ byId.push(entry);
44
+ childrenOf(node).forEach((child, index) => entry.children.push(visit(child, [...path, index])));
45
+ return entry;
46
+ };
47
+ visit(root, []);
48
+ return byId;
49
+ }
50
+ /** Creates a deterministic runtime for a serializable decision graph.
51
+ * @capability ai-decision-graph Evaluate serializable selector, sequence, condition, action, utility, timer, and random AI decisions with interrupt hooks.
52
+ */
53
+ export function createDecisionGraphRuntime(graph, actions, options = {}) {
54
+ const nodes = compile(graph);
55
+ const root = nodes[0];
56
+ let runningId = -1;
57
+ let tickCount = 0;
58
+ let clock = 0;
59
+ const memory = new Map();
60
+ const cooldowns = new Map();
61
+ const lastReached = new Float64Array(nodes.length).fill(-1);
62
+ function carried(id) {
63
+ const entry = memory.get(id);
64
+ return entry !== undefined && entry.tick === tickCount - 1 ? entry : undefined;
65
+ }
66
+ function hold(id, index, elapsed) {
67
+ memory.set(id, { index, elapsed, tick: tickCount });
68
+ }
69
+ function run(entry, ctx, blackboard, dt) {
70
+ const node = entry.node;
71
+ switch (node.kind) {
72
+ case "condition":
73
+ return compare(blackboard[node.key], node.op, node.value) ? "done" : "failed";
74
+ case "action": {
75
+ const action = actions[node.action];
76
+ if (action === undefined)
77
+ throw new Error(`Decision action '${node.action}' is not registered`);
78
+ lastReached[entry.id] = tickCount;
79
+ const status = action(ctx, node.params, blackboard);
80
+ if (status === "running")
81
+ runningId = entry.id;
82
+ return status;
83
+ }
84
+ case "selector":
85
+ case "sequence": {
86
+ const stopOn = node.kind === "selector" ? "failed" : "done";
87
+ const start = node.memory === true ? (carried(entry.id)?.index ?? 0) : 0;
88
+ memory.delete(entry.id);
89
+ for (let index = start; index < entry.children.length; index += 1) {
90
+ const status = run(entry.children[index], ctx, blackboard, dt);
91
+ if (status === stopOn)
92
+ continue;
93
+ if (status === "running" && node.memory === true)
94
+ hold(entry.id, index, 0);
95
+ return status;
96
+ }
97
+ return node.kind === "selector" ? "failed" : "done";
98
+ }
99
+ case "utility": {
100
+ let best = -Infinity;
101
+ let bestIndex = -1;
102
+ for (let index = 0; index < node.options.length; index += 1) {
103
+ let score = 0;
104
+ for (const term of node.options[index].score) {
105
+ const value = blackboard[term.key];
106
+ if (typeof value === "number")
107
+ score += value * term.weight;
108
+ }
109
+ if (score > best) {
110
+ best = score;
111
+ bestIndex = index;
112
+ }
113
+ }
114
+ return bestIndex < 0 ? "failed" : run(entry.children[bestIndex], ctx, blackboard, dt);
115
+ }
116
+ case "invert": {
117
+ const status = run(entry.children[0], ctx, blackboard, dt);
118
+ return status === "running" ? status : status === "done" ? "failed" : "done";
119
+ }
120
+ case "cooldown": {
121
+ if (clock < (cooldowns.get(entry.id) ?? -Infinity))
122
+ return "failed";
123
+ const status = run(entry.children[0], ctx, blackboard, dt);
124
+ if (status === "done")
125
+ cooldowns.set(entry.id, clock + Math.max(0, node.seconds));
126
+ return status;
127
+ }
128
+ case "wait": {
129
+ const elapsed = (carried(entry.id)?.elapsed ?? 0) + dt;
130
+ if (elapsed >= node.seconds) {
131
+ memory.delete(entry.id);
132
+ return "done";
133
+ }
134
+ hold(entry.id, 0, elapsed);
135
+ runningId = entry.id;
136
+ return "running";
137
+ }
138
+ case "random": {
139
+ if (entry.children.length === 0)
140
+ return "failed";
141
+ let index = carried(entry.id)?.index ?? -1;
142
+ if (index < 0) {
143
+ if (options.rng === undefined)
144
+ throw new Error("A random decision node needs options.rng (pass ctx.rng)");
145
+ const weights = entry.children.map((_, i) => Math.max(0, node.weights?.[i] ?? 1));
146
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
147
+ if (total <= 0)
148
+ return "failed";
149
+ let roll = options.rng() * total;
150
+ index = 0;
151
+ while (index < weights.length - 1 && roll >= weights[index])
152
+ roll -= weights[index++];
153
+ }
154
+ memory.delete(entry.id);
155
+ const status = run(entry.children[index], ctx, blackboard, dt);
156
+ if (status === "running")
157
+ hold(entry.id, index, 0);
158
+ return status;
159
+ }
160
+ }
161
+ }
162
+ function idFromPath(path) {
163
+ let entry = root;
164
+ for (const index of path)
165
+ entry = entry?.children[index];
166
+ return entry?.id ?? -1;
167
+ }
168
+ return {
169
+ tick(ctx, blackboard, dt) {
170
+ const previous = runningId;
171
+ tickCount += 1;
172
+ clock += Math.max(0, dt);
173
+ runningId = -1;
174
+ const status = run(root, ctx, blackboard, Math.max(0, dt));
175
+ const abandoned = previous >= 0 ? nodes[previous].node : null;
176
+ if (abandoned?.kind === "action" && lastReached[previous] !== tickCount) {
177
+ options.onAbort?.(abandoned.action, ctx, abandoned.params, blackboard);
178
+ }
179
+ return status;
180
+ },
181
+ running() {
182
+ const node = runningId >= 0 ? nodes[runningId].node : null;
183
+ return node?.kind === "action" ? node.action : null;
184
+ },
185
+ snapshot() {
186
+ const nodeState = {};
187
+ for (const [id, entry] of memory)
188
+ nodeState[id] = { ...entry };
189
+ const cooldownState = {};
190
+ for (const [id, readyAt] of cooldowns)
191
+ cooldownState[id] = readyAt;
192
+ return {
193
+ runningPath: runningId >= 0 ? [...nodes[runningId].path] : null,
194
+ tick: tickCount,
195
+ clock,
196
+ nodes: nodeState,
197
+ cooldowns: cooldownState,
198
+ };
199
+ },
200
+ restore(next) {
201
+ runningId = next.runningPath === null ? -1 : idFromPath(next.runningPath);
202
+ tickCount = next.tick ?? 0;
203
+ clock = next.clock ?? 0;
204
+ memory.clear();
205
+ cooldowns.clear();
206
+ for (const [id, entry] of Object.entries(next.nodes ?? {}))
207
+ memory.set(Number(id), { ...entry });
208
+ for (const [id, readyAt] of Object.entries(next.cooldowns ?? {}))
209
+ cooldowns.set(Number(id), readyAt);
210
+ },
211
+ };
212
+ }
@@ -9,12 +9,22 @@ export interface AssistNetworkConfig {
9
9
  shareFraction?: number;
10
10
  distanceBetween?: (a: string, b: string) => number;
11
11
  }
12
+ /** Plain JSON membership of an {@link AssistNetwork}, in registration order; threat lives in each member's own table. */
13
+ export interface AssistNetworkState {
14
+ members: {
15
+ id: string;
16
+ groupId: string;
17
+ }[];
18
+ }
12
19
  export interface AssistNetwork {
13
20
  register(member: AssistMember): void;
14
21
  remove(memberId: string): void;
15
22
  memberIds(groupId?: string): string[];
16
23
  assistersOf(memberId: string): string[];
17
24
  addThreat(memberId: string, sourceId: string, amount: number): string[];
25
+ snapshot(): AssistNetworkState;
26
+ /** Replaces the membership; `tableOf` supplies each member's (separately restored) threat table. */
27
+ restore(next: AssistNetworkState, tableOf: (memberId: string) => ThreatTable): void;
18
28
  }
19
29
  /** @internal */
20
30
  export declare function createAssistNetwork(config?: AssistNetworkConfig): AssistNetwork;
@@ -56,5 +56,16 @@ export function createAssistNetwork(config = {}) {
56
56
  members.get(id).table.add(sourceId, amount * shareFraction);
57
57
  return assisters;
58
58
  },
59
+ snapshot() {
60
+ return { members: order.map((id) => ({ id, groupId: members.get(id).groupId })) };
61
+ },
62
+ restore(next, tableOf) {
63
+ members.clear();
64
+ order.length = 0;
65
+ for (const { id, groupId } of next.members) {
66
+ order.push(id);
67
+ members.set(id, { id, groupId, table: tableOf(id) });
68
+ }
69
+ },
59
70
  };
60
71
  }
@@ -0,0 +1,3 @@
1
+ import type { DecisionGraph } from "./decisionGraph.js";
2
+ /** Graph-shaped description of the mob brain's idle, wander, chase, engage, and evade decisions. */
3
+ export declare const mobBrainGraph: DecisionGraph;
@@ -0,0 +1,25 @@
1
+ /** Graph-shaped description of the mob brain's idle, wander, chase, engage, and evade decisions. */
2
+ export const mobBrainGraph = {
3
+ kind: "selector",
4
+ children: [
5
+ { kind: "sequence", children: [
6
+ { kind: "condition", key: "evading", op: "=", value: true },
7
+ { kind: "action", action: "evade" },
8
+ ] },
9
+ { kind: "sequence", children: [
10
+ { kind: "condition", key: "targetId", op: "!=", value: "" },
11
+ { kind: "selector", children: [
12
+ { kind: "sequence", children: [
13
+ { kind: "condition", key: "leashExceeded", op: "=", value: true },
14
+ { kind: "action", action: "evade" },
15
+ ] },
16
+ { kind: "sequence", children: [
17
+ { kind: "condition", key: "inAttackRange", op: "=", value: true },
18
+ { kind: "action", action: "engage" },
19
+ ] },
20
+ { kind: "action", action: "chase" },
21
+ ] },
22
+ ] },
23
+ { kind: "action", action: "wanderOrIdle" },
24
+ ],
25
+ };
@@ -51,6 +51,6 @@ export declare function armPursuit(state: PursuitState, intervalSeconds: number)
51
51
  *
52
52
  * On `"attack"` the caller applies the hit and calls {@link armPursuit} with the next interval.
53
53
  *
54
- * @capability pursuit chase-to-reach cooldown-gated attack with optional leash-to-home; owns the attack cooldown
54
+ * @capability pursuit enemy/mob aggro chase-to-reach with a cooldown-gated attack and optional leash-to-home; owns the attack cooldown
55
55
  */
56
56
  export declare function advancePursuit(state: PursuitState, dt: number, targetDistance: number | null, stopDistance: number, cooldownMode?: CooldownMode, homeDistance?: number, leashRange?: number): PursuitAction;
@@ -36,7 +36,7 @@ function tickCooldown(state, dt) {
36
36
  *
37
37
  * On `"attack"` the caller applies the hit and calls {@link armPursuit} with the next interval.
38
38
  *
39
- * @capability pursuit chase-to-reach cooldown-gated attack with optional leash-to-home; owns the attack cooldown
39
+ * @capability pursuit enemy/mob aggro chase-to-reach with a cooldown-gated attack and optional leash-to-home; owns the attack cooldown
40
40
  */
41
41
  export function advancePursuit(state, dt, targetDistance, stopDistance, cooldownMode = "always", homeDistance = Number.NEGATIVE_INFINITY, leashRange = Number.POSITIVE_INFINITY) {
42
42
  if (cooldownMode === "always")
@@ -0,0 +1,30 @@
1
+ import type { NavMeshData } from "../nav/navMesh.js";
2
+ import type { Vec3 } from "../world/geometry.js";
3
+ /**
4
+ * Returns boundary locations that are hidden from a threat.
5
+ * @capability tactical-cover-points derive cover candidates from navigation-mesh boundaries
6
+ */
7
+ export declare function coverPoints(mesh: NavMeshData, threatPos: Vec3, losBlocked: (a: Vec3, b: Vec3) => boolean): Vec3[];
8
+ /** Functions that score one candidate position; higher values are preferred. */
9
+ export interface TacticalPositionEvaluators {
10
+ distanceTo?: (point: Vec3) => number;
11
+ flankOf?: (point: Vec3) => number;
12
+ retreatFrom?: (point: Vec3) => number;
13
+ }
14
+ /** Weights and a hard bound for tactical candidate scoring. */
15
+ export interface TacticalPositionWeights {
16
+ distanceTo?: number;
17
+ flankOf?: number;
18
+ retreatFrom?: number;
19
+ maxCandidates?: number;
20
+ }
21
+ /** A candidate position with its deterministic weighted tactical score. */
22
+ export interface ScoredPosition {
23
+ point: Vec3;
24
+ score: number;
25
+ }
26
+ /**
27
+ * Score positions and retain at most `maxCandidates`, ordered highest score first.
28
+ * @capability tactical-position-score rank bounded tactical positions with caller-defined weights
29
+ */
30
+ export declare function scorePositions(points: readonly Vec3[], evaluators: TacticalPositionEvaluators, weights?: TacticalPositionWeights): ScoredPosition[];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Returns boundary locations that are hidden from a threat.
3
+ * @capability tactical-cover-points derive cover candidates from navigation-mesh boundaries
4
+ */
5
+ export function coverPoints(mesh, threatPos, losBlocked) {
6
+ const edges = new Map();
7
+ const shared = new Set();
8
+ for (const poly of mesh.polys) {
9
+ for (let i = 0; i < poly.length; i += 1) {
10
+ const a = poly[i];
11
+ const b = poly[(i + 1) % poly.length];
12
+ const key = a < b ? `${a}:${b}` : `${b}:${a}`;
13
+ if (edges.has(key))
14
+ shared.add(key);
15
+ else
16
+ edges.set(key, { a, b });
17
+ }
18
+ }
19
+ const result = [];
20
+ for (const [key, edge] of edges) {
21
+ if (shared.has(key))
22
+ continue;
23
+ const a = [mesh.verts[edge.a * 3] ?? 0, mesh.verts[edge.a * 3 + 1] ?? 0, mesh.verts[edge.a * 3 + 2] ?? 0];
24
+ const b = [mesh.verts[edge.b * 3] ?? 0, mesh.verts[edge.b * 3 + 1] ?? 0, mesh.verts[edge.b * 3 + 2] ?? 0];
25
+ const midpoint = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
26
+ if (losBlocked(midpoint, threatPos))
27
+ result.push(midpoint);
28
+ }
29
+ return result;
30
+ }
31
+ /**
32
+ * Score positions and retain at most `maxCandidates`, ordered highest score first.
33
+ * @capability tactical-position-score rank bounded tactical positions with caller-defined weights
34
+ */
35
+ export function scorePositions(points, evaluators, weights = {}) {
36
+ const scored = points.map((point) => ({
37
+ point,
38
+ score: (evaluators.distanceTo?.(point) ?? 0) * (weights.distanceTo ?? 0)
39
+ + (evaluators.flankOf?.(point) ?? 0) * (weights.flankOf ?? 0)
40
+ + (evaluators.retreatFrom?.(point) ?? 0) * (weights.retreatFrom ?? 0),
41
+ }));
42
+ scored.sort((a, b) => b.score - a.score);
43
+ const max = Math.max(0, Math.floor(weights.maxCandidates ?? scored.length));
44
+ return scored.slice(0, max);
45
+ }
@@ -11,6 +11,12 @@ export interface HighestThreatOptions {
11
11
  current?: string | null;
12
12
  stickiness?: number;
13
13
  }
14
+ /** Plain JSON state of a {@link ThreatTable}; `entries` keeps insertion order, which breaks ties in `highest`. */
15
+ export interface ThreatTableState {
16
+ entries: [sourceId: string, threat: number][];
17
+ forcedSource: string | null;
18
+ forcedRemaining: number;
19
+ }
14
20
  export interface ThreatTable {
15
21
  add(sourceId: string, amount: number): number;
16
22
  set(sourceId: string, amount: number): void;
@@ -23,5 +29,7 @@ export interface ThreatTable {
23
29
  remove(sourceId: string): void;
24
30
  clear(): void;
25
31
  size(): number;
32
+ snapshot(): ThreatTableState;
33
+ restore(next: ThreatTableState): void;
26
34
  }
27
35
  export declare function createThreatTable(config?: ThreatTableConfig): ThreatTable;
package/dist/ai/threat.js CHANGED
@@ -97,5 +97,15 @@ export function createThreatTable(config = {}) {
97
97
  size() {
98
98
  return table.size;
99
99
  },
100
+ snapshot() {
101
+ return { entries: [...table], forcedSource, forcedRemaining };
102
+ },
103
+ restore(next) {
104
+ table.clear();
105
+ for (const [sourceId, threat] of next.entries)
106
+ table.set(sourceId, threat);
107
+ forcedSource = next.forcedSource;
108
+ forcedRemaining = next.forcedRemaining;
109
+ },
100
110
  };
101
111
  }
@@ -0,0 +1,155 @@
1
+ /** Parameter value a graph reads: floats for blends and comparisons, booleans for gates. */
2
+ export type AnimParamValue = number | boolean;
3
+ /** The parameter set a graph evaluates against each advance. */
4
+ export type AnimParams = Readonly<Record<string, AnimParamValue>>;
5
+ /** A state plays one clip, or blends clips by one or two parameters. */
6
+ export type AnimState = {
7
+ kind: "clip";
8
+ clip: string;
9
+ speed?: number;
10
+ loop?: boolean;
11
+ rootMotion?: boolean;
12
+ } | {
13
+ kind: "blend1D";
14
+ param: string;
15
+ points: readonly {
16
+ at: number;
17
+ clip: string;
18
+ }[];
19
+ speed?: number;
20
+ loop?: boolean;
21
+ rootMotion?: boolean;
22
+ } | {
23
+ kind: "blend2D";
24
+ params: readonly [string, string];
25
+ points: readonly {
26
+ at: readonly [number, number];
27
+ clip: string;
28
+ }[];
29
+ speed?: number;
30
+ loop?: boolean;
31
+ rootMotion?: boolean;
32
+ };
33
+ /** Comparison operators an {@link AnimCondition} supports. */
34
+ export type AnimCompare = ">" | "<" | ">=" | "<=" | "==" | "!=";
35
+ /** A parameter comparison; every condition on a transition must hold. */
36
+ export interface AnimCondition {
37
+ param: string;
38
+ op: AnimCompare;
39
+ value: AnimParamValue;
40
+ }
41
+ /** Edge between states. `from: "*"` matches any state except `to`. */
42
+ export interface AnimTransition {
43
+ from: string | "*";
44
+ to: string;
45
+ /** All must hold. */
46
+ when?: readonly AnimCondition[];
47
+ /** Fires once when this trigger was set since the last advance; consumed on use. */
48
+ trigger?: string;
49
+ /** Crossfade seconds. Default 0.2. */
50
+ duration?: number;
51
+ /** Only leave `from` once its normalized time reaches this (one-shots returning to locomotion). */
52
+ exitTime?: number;
53
+ }
54
+ /** A blend layer with its own state machine. Masked layers apply only to bones whose track names start with a prefix. */
55
+ export interface AnimLayer {
56
+ id: string;
57
+ entry: string;
58
+ states: Readonly<Record<string, AnimState>>;
59
+ transitions: readonly AnimTransition[];
60
+ /** Bone name prefixes this layer drives; absent means the whole rig. */
61
+ mask?: readonly string[];
62
+ additive?: boolean;
63
+ /** Layer influence 0..1. Default 1. */
64
+ weight?: number;
65
+ }
66
+ /** A named moment inside a clip (foot plant, hit frame, reload point). */
67
+ export interface AnimEvent {
68
+ clip: string;
69
+ atSec: number;
70
+ name: string;
71
+ }
72
+ /** Serializable animation graph. Clip durations come from the rig at runtime (see {@link AnimGraphClipInfo}). */
73
+ export interface AnimGraph {
74
+ layers: readonly AnimLayer[];
75
+ events?: readonly AnimEvent[];
76
+ }
77
+ /** Per-clip duration in seconds, read from the loaded rig. */
78
+ export type AnimGraphClipInfo = Readonly<Record<string, number | {
79
+ duration: number;
80
+ rootTrack?: {
81
+ times: Float32Array;
82
+ values: Float32Array;
83
+ };
84
+ }>>;
85
+ interface LayerTransitionState {
86
+ to: string;
87
+ elapsed: number;
88
+ duration: number;
89
+ fromWeights: Record<string, number>;
90
+ fromTimes: Record<string, number>;
91
+ }
92
+ interface LayerState {
93
+ current: string;
94
+ time: number;
95
+ transition: LayerTransitionState | null;
96
+ }
97
+ /** Serializable evaluator state. */
98
+ export interface AnimGraphState {
99
+ layers: Record<string, LayerState>;
100
+ triggers: string[];
101
+ }
102
+ /** One clip's contribution: weight 0..1 and the time to seek it to. */
103
+ export interface AnimClipOutput {
104
+ clip: string;
105
+ weight: number;
106
+ time: number;
107
+ layer: string;
108
+ }
109
+ /** What one advance asks the rig to show. */
110
+ export interface AnimGraphOutput {
111
+ clips: AnimClipOutput[];
112
+ events: {
113
+ name: string;
114
+ clip: string;
115
+ }[];
116
+ /** Root-bone travel over this advance from `rootMotion` states, in the rig's local units. */
117
+ rootDelta?: [number, number, number];
118
+ /** `true` while any layer's current state has `rootMotion`, even on an advance with no travel. */
119
+ rootMotion?: true;
120
+ }
121
+ /** The evaluator handle: arm triggers, advance, inspect, snapshot and restore. */
122
+ export interface AnimGraphRuntime {
123
+ graph(): AnimGraph;
124
+ retune(graph: AnimGraph): void;
125
+ state(): AnimGraphState;
126
+ snapshot(): AnimGraphState;
127
+ restore(state: AnimGraphState): void;
128
+ /** Arm a trigger for the next advance. */
129
+ trigger(name: string): void;
130
+ /** Current state id of a layer. */
131
+ stateOf(layerId: string): string | null;
132
+ advance(dt: number, params: AnimParams, clips: AnimGraphClipInfo): AnimGraphOutput;
133
+ }
134
+ /** Entity blackboard key the shell reads extra graph parameters from: `ctx.scene.entity.blackboard.set(id, ANIM_PARAMS_KEY, { aiming: true })`. */
135
+ export declare const ANIM_PARAMS_KEY = "anim.params";
136
+ /** Static clip weights of a state at `params`, before any crossfade. */
137
+ export declare function stateClipWeights(state: AnimState, params: AnimParams): Record<string, number>;
138
+ /**
139
+ * Headless animation state machine and blend evaluator. It owns every clip's playback time and weight, so the
140
+ * renderer only seeks and weights actions on a mixer, and headless hosts, replays, and tests advance the same
141
+ * graph without three.js. Transitions are data (parameter comparisons and consumed triggers), layers can be
142
+ * masked or additive, and events fire by clip time, including across loop wraps.
143
+ *
144
+ * @capability anim-graph data-first animation state machine with blend trees, crossfades, layers, and clip events
145
+ */
146
+ export declare function createAnimGraphRuntime(initial: AnimGraph): AnimGraphRuntime;
147
+ /**
148
+ * Validates untrusted JSON (a saved scene document, a network payload) as an {@link AnimGraph}.
149
+ * Malformed states, transitions to unknown states, and bad conditions are dropped; a layer whose
150
+ * entry state is missing is dropped; the result is `undefined` when no layer survives.
151
+ *
152
+ * @capability anim-graph load an animation graph from saved JSON without trusting its shape
153
+ */
154
+ export declare function parseAnimGraph(value: unknown): AnimGraph | undefined;
155
+ export {};