@alpaca-software/40kdc-data 0.1.1 → 0.1.3

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 (424) hide show
  1. package/README.md +2 -0
  2. package/dist/abilities-resolver/index.d.ts +9 -0
  3. package/dist/abilities-resolver/index.d.ts.map +1 -0
  4. package/dist/abilities-resolver/index.js +9 -0
  5. package/dist/abilities-resolver/index.js.map +1 -0
  6. package/dist/abilities-resolver/resolver.d.ts +73 -0
  7. package/dist/abilities-resolver/resolver.d.ts.map +1 -0
  8. package/dist/abilities-resolver/resolver.js +142 -0
  9. package/dist/abilities-resolver/resolver.js.map +1 -0
  10. package/dist/audit-coverage.d.ts +78 -0
  11. package/dist/audit-coverage.d.ts.map +1 -0
  12. package/dist/audit-coverage.js +341 -0
  13. package/dist/audit-coverage.js.map +1 -0
  14. package/dist/author-batch.d.ts +147 -0
  15. package/dist/author-batch.d.ts.map +1 -0
  16. package/dist/author-batch.js +675 -0
  17. package/dist/author-batch.js.map +1 -0
  18. package/dist/author-input.d.ts +37 -0
  19. package/dist/author-input.d.ts.map +1 -0
  20. package/dist/author-input.js +162 -0
  21. package/dist/author-input.js.map +1 -0
  22. package/dist/bundle-schemas.d.ts +1 -0
  23. package/dist/bundle-schemas.d.ts.map +1 -0
  24. package/dist/bundle-schemas.js +1 -0
  25. package/dist/bundle-schemas.js.map +1 -0
  26. package/dist/cli.d.ts +2 -0
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +9 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/codegen-data.d.ts +1 -0
  31. package/dist/codegen-data.d.ts.map +1 -0
  32. package/dist/codegen-data.js +2 -0
  33. package/dist/codegen-data.js.map +1 -0
  34. package/dist/commands/import.d.ts +1 -0
  35. package/dist/commands/import.d.ts.map +1 -0
  36. package/dist/commands/import.js +1 -0
  37. package/dist/commands/import.js.map +1 -0
  38. package/dist/commands/translate.d.ts +1 -0
  39. package/dist/commands/translate.d.ts.map +1 -0
  40. package/dist/commands/translate.js +10 -4
  41. package/dist/commands/translate.js.map +1 -0
  42. package/dist/commands/validate-all.d.ts +1 -0
  43. package/dist/commands/validate-all.d.ts.map +1 -0
  44. package/dist/commands/validate-all.js +1 -0
  45. package/dist/commands/validate-all.js.map +1 -0
  46. package/dist/commands/validate-core.d.ts +1 -0
  47. package/dist/commands/validate-core.d.ts.map +1 -0
  48. package/dist/commands/validate-core.js +1 -0
  49. package/dist/commands/validate-core.js.map +1 -0
  50. package/dist/commands/validate-enrichment.d.ts +1 -0
  51. package/dist/commands/validate-enrichment.d.ts.map +1 -0
  52. package/dist/commands/validate-enrichment.js +1 -0
  53. package/dist/commands/validate-enrichment.js.map +1 -0
  54. package/dist/convert-faction.d.ts +1 -0
  55. package/dist/convert-faction.d.ts.map +1 -0
  56. package/dist/convert-faction.js +1 -0
  57. package/dist/convert-faction.js.map +1 -0
  58. package/dist/converters/configs/adepta-sororitas.d.ts +1 -0
  59. package/dist/converters/configs/adepta-sororitas.d.ts.map +1 -0
  60. package/dist/converters/configs/adepta-sororitas.js +1 -0
  61. package/dist/converters/configs/adepta-sororitas.js.map +1 -0
  62. package/dist/converters/configs/adeptus-astartes.d.ts +1 -0
  63. package/dist/converters/configs/adeptus-astartes.d.ts.map +1 -0
  64. package/dist/converters/configs/adeptus-astartes.js +1 -0
  65. package/dist/converters/configs/adeptus-astartes.js.map +1 -0
  66. package/dist/converters/configs/adeptus-custodes.d.ts +1 -0
  67. package/dist/converters/configs/adeptus-custodes.d.ts.map +1 -0
  68. package/dist/converters/configs/adeptus-custodes.js +1 -0
  69. package/dist/converters/configs/adeptus-custodes.js.map +1 -0
  70. package/dist/converters/configs/adeptus-mechanicus.d.ts +1 -0
  71. package/dist/converters/configs/adeptus-mechanicus.d.ts.map +1 -0
  72. package/dist/converters/configs/adeptus-mechanicus.js +1 -0
  73. package/dist/converters/configs/adeptus-mechanicus.js.map +1 -0
  74. package/dist/converters/configs/aeldari.d.ts +1 -0
  75. package/dist/converters/configs/aeldari.d.ts.map +1 -0
  76. package/dist/converters/configs/aeldari.js +1 -0
  77. package/dist/converters/configs/aeldari.js.map +1 -0
  78. package/dist/converters/configs/agents-of-the-imperium.d.ts +1 -0
  79. package/dist/converters/configs/agents-of-the-imperium.d.ts.map +1 -0
  80. package/dist/converters/configs/agents-of-the-imperium.js +1 -0
  81. package/dist/converters/configs/agents-of-the-imperium.js.map +1 -0
  82. package/dist/converters/configs/astra-militarum.d.ts +1 -0
  83. package/dist/converters/configs/astra-militarum.d.ts.map +1 -0
  84. package/dist/converters/configs/astra-militarum.js +1 -0
  85. package/dist/converters/configs/astra-militarum.js.map +1 -0
  86. package/dist/converters/configs/black-templars.d.ts +1 -0
  87. package/dist/converters/configs/black-templars.d.ts.map +1 -0
  88. package/dist/converters/configs/black-templars.js +1 -0
  89. package/dist/converters/configs/black-templars.js.map +1 -0
  90. package/dist/converters/configs/blood-angels.d.ts +1 -0
  91. package/dist/converters/configs/blood-angels.d.ts.map +1 -0
  92. package/dist/converters/configs/blood-angels.js +1 -0
  93. package/dist/converters/configs/blood-angels.js.map +1 -0
  94. package/dist/converters/configs/chaos-daemons.d.ts +1 -0
  95. package/dist/converters/configs/chaos-daemons.d.ts.map +1 -0
  96. package/dist/converters/configs/chaos-daemons.js +1 -0
  97. package/dist/converters/configs/chaos-daemons.js.map +1 -0
  98. package/dist/converters/configs/chaos-knights.d.ts +1 -0
  99. package/dist/converters/configs/chaos-knights.d.ts.map +1 -0
  100. package/dist/converters/configs/chaos-knights.js +1 -0
  101. package/dist/converters/configs/chaos-knights.js.map +1 -0
  102. package/dist/converters/configs/chaos-space-marines.d.ts +1 -0
  103. package/dist/converters/configs/chaos-space-marines.d.ts.map +1 -0
  104. package/dist/converters/configs/chaos-space-marines.js +1 -0
  105. package/dist/converters/configs/chaos-space-marines.js.map +1 -0
  106. package/dist/converters/configs/crimson-fists.d.ts +1 -0
  107. package/dist/converters/configs/crimson-fists.d.ts.map +1 -0
  108. package/dist/converters/configs/crimson-fists.js +1 -0
  109. package/dist/converters/configs/crimson-fists.js.map +1 -0
  110. package/dist/converters/configs/dark-angels.d.ts +1 -0
  111. package/dist/converters/configs/dark-angels.d.ts.map +1 -0
  112. package/dist/converters/configs/dark-angels.js +1 -0
  113. package/dist/converters/configs/dark-angels.js.map +1 -0
  114. package/dist/converters/configs/death-guard.d.ts +1 -0
  115. package/dist/converters/configs/death-guard.d.ts.map +1 -0
  116. package/dist/converters/configs/death-guard.js +1 -0
  117. package/dist/converters/configs/death-guard.js.map +1 -0
  118. package/dist/converters/configs/deathwatch.d.ts +1 -0
  119. package/dist/converters/configs/deathwatch.d.ts.map +1 -0
  120. package/dist/converters/configs/deathwatch.js +1 -0
  121. package/dist/converters/configs/deathwatch.js.map +1 -0
  122. package/dist/converters/configs/drukhari.d.ts +1 -0
  123. package/dist/converters/configs/drukhari.d.ts.map +1 -0
  124. package/dist/converters/configs/drukhari.js +1 -0
  125. package/dist/converters/configs/drukhari.js.map +1 -0
  126. package/dist/converters/configs/emperors-children.d.ts +1 -0
  127. package/dist/converters/configs/emperors-children.d.ts.map +1 -0
  128. package/dist/converters/configs/emperors-children.js +1 -0
  129. package/dist/converters/configs/emperors-children.js.map +1 -0
  130. package/dist/converters/configs/genestealer-cults.d.ts +1 -0
  131. package/dist/converters/configs/genestealer-cults.d.ts.map +1 -0
  132. package/dist/converters/configs/genestealer-cults.js +1 -0
  133. package/dist/converters/configs/genestealer-cults.js.map +1 -0
  134. package/dist/converters/configs/grey-knights.d.ts +1 -0
  135. package/dist/converters/configs/grey-knights.d.ts.map +1 -0
  136. package/dist/converters/configs/grey-knights.js +1 -0
  137. package/dist/converters/configs/grey-knights.js.map +1 -0
  138. package/dist/converters/configs/imperial-fists.d.ts +1 -0
  139. package/dist/converters/configs/imperial-fists.d.ts.map +1 -0
  140. package/dist/converters/configs/imperial-fists.js +1 -0
  141. package/dist/converters/configs/imperial-fists.js.map +1 -0
  142. package/dist/converters/configs/imperial-knights.d.ts +1 -0
  143. package/dist/converters/configs/imperial-knights.d.ts.map +1 -0
  144. package/dist/converters/configs/imperial-knights.js +1 -0
  145. package/dist/converters/configs/imperial-knights.js.map +1 -0
  146. package/dist/converters/configs/iron-hands.d.ts +1 -0
  147. package/dist/converters/configs/iron-hands.d.ts.map +1 -0
  148. package/dist/converters/configs/iron-hands.js +1 -0
  149. package/dist/converters/configs/iron-hands.js.map +1 -0
  150. package/dist/converters/configs/leagues-of-votann.d.ts +1 -0
  151. package/dist/converters/configs/leagues-of-votann.d.ts.map +1 -0
  152. package/dist/converters/configs/leagues-of-votann.js +1 -0
  153. package/dist/converters/configs/leagues-of-votann.js.map +1 -0
  154. package/dist/converters/configs/necrons.d.ts +1 -0
  155. package/dist/converters/configs/necrons.d.ts.map +1 -0
  156. package/dist/converters/configs/necrons.js +1 -0
  157. package/dist/converters/configs/necrons.js.map +1 -0
  158. package/dist/converters/configs/orks.d.ts +1 -0
  159. package/dist/converters/configs/orks.d.ts.map +1 -0
  160. package/dist/converters/configs/orks.js +1 -0
  161. package/dist/converters/configs/orks.js.map +1 -0
  162. package/dist/converters/configs/raven-guard.d.ts +1 -0
  163. package/dist/converters/configs/raven-guard.d.ts.map +1 -0
  164. package/dist/converters/configs/raven-guard.js +1 -0
  165. package/dist/converters/configs/raven-guard.js.map +1 -0
  166. package/dist/converters/configs/salamanders.d.ts +1 -0
  167. package/dist/converters/configs/salamanders.d.ts.map +1 -0
  168. package/dist/converters/configs/salamanders.js +1 -0
  169. package/dist/converters/configs/salamanders.js.map +1 -0
  170. package/dist/converters/configs/space-wolves.d.ts +1 -0
  171. package/dist/converters/configs/space-wolves.d.ts.map +1 -0
  172. package/dist/converters/configs/space-wolves.js +1 -0
  173. package/dist/converters/configs/space-wolves.js.map +1 -0
  174. package/dist/converters/configs/tau-empire.d.ts +1 -0
  175. package/dist/converters/configs/tau-empire.d.ts.map +1 -0
  176. package/dist/converters/configs/tau-empire.js +1 -0
  177. package/dist/converters/configs/tau-empire.js.map +1 -0
  178. package/dist/converters/configs/thousand-sons.d.ts +1 -0
  179. package/dist/converters/configs/thousand-sons.d.ts.map +1 -0
  180. package/dist/converters/configs/thousand-sons.js +1 -0
  181. package/dist/converters/configs/thousand-sons.js.map +1 -0
  182. package/dist/converters/configs/tyranids.d.ts +1 -0
  183. package/dist/converters/configs/tyranids.d.ts.map +1 -0
  184. package/dist/converters/configs/tyranids.js +1 -0
  185. package/dist/converters/configs/tyranids.js.map +1 -0
  186. package/dist/converters/configs/ultramarines.d.ts +1 -0
  187. package/dist/converters/configs/ultramarines.d.ts.map +1 -0
  188. package/dist/converters/configs/ultramarines.js +1 -0
  189. package/dist/converters/configs/ultramarines.js.map +1 -0
  190. package/dist/converters/configs/white-scars.d.ts +1 -0
  191. package/dist/converters/configs/white-scars.d.ts.map +1 -0
  192. package/dist/converters/configs/white-scars.js +1 -0
  193. package/dist/converters/configs/white-scars.js.map +1 -0
  194. package/dist/converters/configs/world-eaters.d.ts +1 -0
  195. package/dist/converters/configs/world-eaters.d.ts.map +1 -0
  196. package/dist/converters/configs/world-eaters.js +1 -0
  197. package/dist/converters/configs/world-eaters.js.map +1 -0
  198. package/dist/converters/faction-config.d.ts +1 -0
  199. package/dist/converters/faction-config.d.ts.map +1 -0
  200. package/dist/converters/faction-config.js +1 -0
  201. package/dist/converters/faction-config.js.map +1 -0
  202. package/dist/converters/id-generator.d.ts +1 -0
  203. package/dist/converters/id-generator.d.ts.map +1 -0
  204. package/dist/converters/id-generator.js +1 -0
  205. package/dist/converters/id-generator.js.map +1 -0
  206. package/dist/converters/keyword-filter.d.ts +1 -0
  207. package/dist/converters/keyword-filter.d.ts.map +1 -0
  208. package/dist/converters/keyword-filter.js +1 -0
  209. package/dist/converters/keyword-filter.js.map +1 -0
  210. package/dist/converters/stat-parser.d.ts +1 -0
  211. package/dist/converters/stat-parser.d.ts.map +1 -0
  212. package/dist/converters/stat-parser.js +1 -0
  213. package/dist/converters/stat-parser.js.map +1 -0
  214. package/dist/converters/view-selector.d.ts +1 -0
  215. package/dist/converters/view-selector.d.ts.map +1 -0
  216. package/dist/converters/view-selector.js +1 -0
  217. package/dist/converters/view-selector.js.map +1 -0
  218. package/dist/converters/weapon-dedup.d.ts +1 -0
  219. package/dist/converters/weapon-dedup.d.ts.map +1 -0
  220. package/dist/converters/weapon-dedup.js +1 -0
  221. package/dist/converters/weapon-dedup.js.map +1 -0
  222. package/dist/cruncher/attribution.d.ts +66 -0
  223. package/dist/cruncher/attribution.d.ts.map +1 -0
  224. package/dist/cruncher/attribution.js +88 -0
  225. package/dist/cruncher/attribution.js.map +1 -0
  226. package/dist/cruncher/buffs.d.ts +206 -0
  227. package/dist/cruncher/buffs.d.ts.map +1 -0
  228. package/dist/cruncher/buffs.js +150 -0
  229. package/dist/cruncher/buffs.js.map +1 -0
  230. package/dist/cruncher/engine.d.ts +50 -0
  231. package/dist/cruncher/engine.d.ts.map +1 -0
  232. package/dist/cruncher/engine.js +312 -0
  233. package/dist/cruncher/engine.js.map +1 -0
  234. package/dist/cruncher/from-dsl.d.ts +101 -0
  235. package/dist/cruncher/from-dsl.d.ts.map +1 -0
  236. package/dist/cruncher/from-dsl.js +968 -0
  237. package/dist/cruncher/from-dsl.js.map +1 -0
  238. package/dist/cruncher/from-keyword.d.ts +35 -0
  239. package/dist/cruncher/from-keyword.d.ts.map +1 -0
  240. package/dist/cruncher/from-keyword.js +159 -0
  241. package/dist/cruncher/from-keyword.js.map +1 -0
  242. package/dist/cruncher/get-buffs.d.ts +12 -0
  243. package/dist/cruncher/get-buffs.d.ts.map +1 -0
  244. package/dist/cruncher/get-buffs.js +7 -0
  245. package/dist/cruncher/get-buffs.js.map +1 -0
  246. package/dist/cruncher/index.d.ts +12 -0
  247. package/dist/cruncher/index.d.ts.map +1 -0
  248. package/dist/cruncher/index.js +12 -0
  249. package/dist/cruncher/index.js.map +1 -0
  250. package/dist/data/bundle.generated.d.ts +1 -0
  251. package/dist/data/bundle.generated.d.ts.map +1 -0
  252. package/dist/data/bundle.generated.js +2 -1
  253. package/dist/data/bundle.generated.js.map +1 -0
  254. package/dist/data/collection.d.ts +10 -0
  255. package/dist/data/collection.d.ts.map +1 -0
  256. package/dist/data/collection.js +15 -0
  257. package/dist/data/collection.js.map +1 -0
  258. package/dist/data/dataset.d.ts +132 -2
  259. package/dist/data/dataset.d.ts.map +1 -0
  260. package/dist/data/dataset.js +248 -1
  261. package/dist/data/dataset.js.map +1 -0
  262. package/dist/data/entities.d.ts +67 -2
  263. package/dist/data/entities.d.ts.map +1 -0
  264. package/dist/data/entities.js +122 -0
  265. package/dist/data/entities.js.map +1 -0
  266. package/dist/data/index.d.ts +10 -1
  267. package/dist/data/index.d.ts.map +1 -0
  268. package/dist/data/index.js +14 -1
  269. package/dist/data/index.js.map +1 -0
  270. package/dist/data/normalize.d.ts +1 -0
  271. package/dist/data/normalize.d.ts.map +1 -0
  272. package/dist/data/normalize.js +1 -0
  273. package/dist/data/normalize.js.map +1 -0
  274. package/dist/data/roster-resolve.d.ts +58 -0
  275. package/dist/data/roster-resolve.d.ts.map +1 -0
  276. package/dist/data/roster-resolve.js +82 -0
  277. package/dist/data/roster-resolve.js.map +1 -0
  278. package/dist/data/types.d.ts +4 -1
  279. package/dist/data/types.d.ts.map +1 -0
  280. package/dist/data/types.js +2 -0
  281. package/dist/data/types.js.map +1 -0
  282. package/dist/export/helpers.d.ts +33 -0
  283. package/dist/export/helpers.d.ts.map +1 -0
  284. package/dist/export/helpers.js +57 -0
  285. package/dist/export/helpers.js.map +1 -0
  286. package/dist/export/index.d.ts +22 -0
  287. package/dist/export/index.d.ts.map +1 -0
  288. package/dist/export/index.js +28 -0
  289. package/dist/export/index.js.map +1 -0
  290. package/dist/export/newrecruit-json.d.ts +3 -0
  291. package/dist/export/newrecruit-json.d.ts.map +1 -0
  292. package/dist/export/newrecruit-json.js +140 -0
  293. package/dist/export/newrecruit-json.js.map +1 -0
  294. package/dist/export/newrecruit-simple.d.ts +3 -0
  295. package/dist/export/newrecruit-simple.d.ts.map +1 -0
  296. package/dist/export/newrecruit-simple.js +76 -0
  297. package/dist/export/newrecruit-simple.js.map +1 -0
  298. package/dist/export/newrecruit-wtc.d.ts +4 -0
  299. package/dist/export/newrecruit-wtc.d.ts.map +1 -0
  300. package/dist/export/newrecruit-wtc.js +142 -0
  301. package/dist/export/newrecruit-wtc.js.map +1 -0
  302. package/dist/export/roster-json.d.ts +3 -0
  303. package/dist/export/roster-json.d.ts.map +1 -0
  304. package/dist/export/roster-json.js +8 -0
  305. package/dist/export/roster-json.js.map +1 -0
  306. package/dist/export/rosterizer.d.ts +3 -0
  307. package/dist/export/rosterizer.d.ts.map +1 -0
  308. package/dist/export/rosterizer.js +144 -0
  309. package/dist/export/rosterizer.js.map +1 -0
  310. package/dist/export/serializer.d.ts +27 -0
  311. package/dist/export/serializer.d.ts.map +1 -0
  312. package/dist/export/serializer.js +2 -0
  313. package/dist/export/serializer.js.map +1 -0
  314. package/dist/gen-conformance.d.ts +1 -0
  315. package/dist/gen-conformance.d.ts.map +1 -0
  316. package/dist/gen-conformance.js +274 -12
  317. package/dist/gen-conformance.js.map +1 -0
  318. package/dist/generated.d.ts +194 -118
  319. package/dist/generated.d.ts.map +1 -0
  320. package/dist/generated.js +1 -0
  321. package/dist/generated.js.map +1 -0
  322. package/dist/import/adapter.d.ts +4 -3
  323. package/dist/import/adapter.d.ts.map +1 -0
  324. package/dist/import/adapter.js +1 -0
  325. package/dist/import/adapter.js.map +1 -0
  326. package/dist/import/decode.d.ts +1 -0
  327. package/dist/import/decode.d.ts.map +1 -0
  328. package/dist/import/decode.js +1 -0
  329. package/dist/import/decode.js.map +1 -0
  330. package/dist/import/gw.d.ts +69 -0
  331. package/dist/import/gw.d.ts.map +1 -0
  332. package/dist/import/gw.js +245 -0
  333. package/dist/import/gw.js.map +1 -0
  334. package/dist/import/import-roster.d.ts +84 -0
  335. package/dist/import/import-roster.d.ts.map +1 -0
  336. package/dist/import/import-roster.js +207 -0
  337. package/dist/import/import-roster.js.map +1 -0
  338. package/dist/import/index.d.ts +7 -3
  339. package/dist/import/index.d.ts.map +1 -0
  340. package/dist/import/index.js +5 -1
  341. package/dist/import/index.js.map +1 -0
  342. package/dist/import/listforge.d.ts +1 -0
  343. package/dist/import/listforge.d.ts.map +1 -0
  344. package/dist/import/listforge.js +22 -2
  345. package/dist/import/listforge.js.map +1 -0
  346. package/dist/import/newrecruit-json.d.ts +31 -0
  347. package/dist/import/newrecruit-json.d.ts.map +1 -0
  348. package/dist/import/newrecruit-json.js +224 -0
  349. package/dist/import/newrecruit-json.js.map +1 -0
  350. package/dist/import/newrecruit-simple.d.ts +29 -0
  351. package/dist/import/newrecruit-simple.d.ts.map +1 -0
  352. package/dist/import/newrecruit-simple.js +200 -0
  353. package/dist/import/newrecruit-simple.js.map +1 -0
  354. package/dist/import/newrecruit-text.d.ts +51 -0
  355. package/dist/import/newrecruit-text.d.ts.map +1 -0
  356. package/dist/import/newrecruit-text.js +102 -0
  357. package/dist/import/newrecruit-text.js.map +1 -0
  358. package/dist/import/newrecruit-wtc.d.ts +36 -0
  359. package/dist/import/newrecruit-wtc.d.ts.map +1 -0
  360. package/dist/import/newrecruit-wtc.js +337 -0
  361. package/dist/import/newrecruit-wtc.js.map +1 -0
  362. package/dist/import/resolve.d.ts +3 -2
  363. package/dist/import/resolve.d.ts.map +1 -0
  364. package/dist/import/resolve.js +5 -2
  365. package/dist/import/resolve.js.map +1 -0
  366. package/dist/import/rosterizer.d.ts +70 -0
  367. package/dist/import/rosterizer.d.ts.map +1 -0
  368. package/dist/import/rosterizer.js +348 -0
  369. package/dist/import/rosterizer.js.map +1 -0
  370. package/dist/import/types.d.ts +11 -1
  371. package/dist/import/types.d.ts.map +1 -0
  372. package/dist/import/types.js +1 -0
  373. package/dist/import/types.js.map +1 -0
  374. package/dist/index.d.ts +5 -2
  375. package/dist/index.d.ts.map +1 -0
  376. package/dist/index.js +4 -1
  377. package/dist/index.js.map +1 -0
  378. package/dist/known-support-10e.d.ts +1 -0
  379. package/dist/known-support-10e.d.ts.map +1 -0
  380. package/dist/known-support-10e.js +1 -0
  381. package/dist/known-support-10e.js.map +1 -0
  382. package/dist/link-abilities.d.ts +41 -0
  383. package/dist/link-abilities.d.ts.map +1 -0
  384. package/dist/link-abilities.js +159 -0
  385. package/dist/link-abilities.js.map +1 -0
  386. package/dist/migrations/2026-weapon-keywords.d.ts +2 -0
  387. package/dist/migrations/2026-weapon-keywords.d.ts.map +1 -0
  388. package/dist/migrations/2026-weapon-keywords.js +247 -0
  389. package/dist/migrations/2026-weapon-keywords.js.map +1 -0
  390. package/dist/port-10e-faction.d.ts +1 -0
  391. package/dist/port-10e-faction.d.ts.map +1 -0
  392. package/dist/port-10e-faction.js +1 -0
  393. package/dist/port-10e-faction.js.map +1 -0
  394. package/dist/report.d.ts +1 -0
  395. package/dist/report.d.ts.map +1 -0
  396. package/dist/report.js +1 -0
  397. package/dist/report.js.map +1 -0
  398. package/dist/rube-goldberg.d.ts +3 -0
  399. package/dist/rube-goldberg.d.ts.map +1 -0
  400. package/dist/rube-goldberg.js +109 -0
  401. package/dist/rube-goldberg.js.map +1 -0
  402. package/dist/runner.d.ts +38 -0
  403. package/dist/runner.d.ts.map +1 -0
  404. package/dist/runner.js +492 -0
  405. package/dist/runner.js.map +1 -0
  406. package/dist/schema-loader.d.ts +1 -0
  407. package/dist/schema-loader.d.ts.map +1 -0
  408. package/dist/schema-loader.js +1 -0
  409. package/dist/schema-loader.js.map +1 -0
  410. package/dist/scrub-ip.d.ts +14 -0
  411. package/dist/scrub-ip.d.ts.map +1 -0
  412. package/dist/scrub-ip.js +88 -0
  413. package/dist/scrub-ip.js.map +1 -0
  414. package/dist/validate.d.ts +1 -0
  415. package/dist/validate.d.ts.map +1 -0
  416. package/dist/validate.js +2 -0
  417. package/dist/validate.js.map +1 -0
  418. package/package.json +15 -3
  419. package/schemas/core/roster.schema.json +19 -4
  420. package/schemas/core/weapon-keyword.schema.json +31 -0
  421. package/schemas/core/weapon.schema.json +22 -1
  422. package/schemas/enrichment/ability-dsl/effect.schema.json +23 -1
  423. package/dist/import/import-listforge.d.ts +0 -23
  424. package/dist/import/import-listforge.js +0 -32
@@ -29,6 +29,7 @@ export declare class Collection<T, V> implements Iterable<V> {
29
29
  private readonly byId;
30
30
  private readonly byNorm;
31
31
  private readonly byFactionId;
32
+ private readonly idOf;
32
33
  private readonly nameOf?;
33
34
  private readonly wrapFn;
34
35
  constructor(cfg: CollectionConfig<T, V>);
@@ -38,6 +39,14 @@ export declare class Collection<T, V> implements Iterable<V> {
38
39
  get size(): number;
39
40
  /** Look up by exact id. */
40
41
  get(id: string): V | undefined;
42
+ /**
43
+ * Look up by exact id *within a faction*. Use this when an id is shared
44
+ * across factions (e.g. `chaos-land-raider` lives under five Chaos factions)
45
+ * and a faction context is known — {@link get} would return whichever copy
46
+ * was registered first, which may belong to the wrong faction. Returns
47
+ * `undefined` when no record with that id belongs to `factionId`.
48
+ */
49
+ getInFaction(id: string, factionId: string): V | undefined;
41
50
  /** Whether a record with this exact id exists. */
42
51
  has(id: string): boolean;
43
52
  /**
@@ -62,3 +71,4 @@ export declare class Collection<T, V> implements Iterable<V> {
62
71
  byFaction(factionId: string): V[];
63
72
  [Symbol.iterator](): Iterator<V>;
64
73
  }
74
+ //# sourceMappingURL=collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/data/collection.ts"],"names":[],"mappings":"AAiBA,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB,CAAC,CAAC,EAAE,CAAC;IACpC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAClC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IACzC,8EAA8E;IAC9E,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACnD,4CAA4C;IAC5C,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU,CAAC,CAAC,EAAE,CAAC,CAAE,YAAW,QAAQ,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0B;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;gBAE5B,GAAG,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC;IAuBvC,6DAA6D;IAC7D,IAAI,GAAG,IAAI,CAAC,EAAE,CAEb;IAED,kCAAkC;IAClC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,2BAA2B;IAC3B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAK9B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAM1D,kDAAkD;IAClD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAIlC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE;IAc3B,gFAAgF;IAChF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,EAAE;IAIjC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC;CAGjC"}
@@ -27,9 +27,11 @@ export class Collection {
27
27
  byId = new Map();
28
28
  byNorm = new Map();
29
29
  byFactionId = new Map();
30
+ idOf;
30
31
  nameOf;
31
32
  wrapFn;
32
33
  constructor(cfg) {
34
+ this.idOf = cfg.idOf;
33
35
  this.nameOf = cfg.nameOf;
34
36
  this.wrapFn = cfg.wrap;
35
37
  const dedupeKeyOf = cfg.dedupeKeyOf ?? cfg.idOf;
@@ -64,6 +66,18 @@ export class Collection {
64
66
  const item = this.byId.get(id);
65
67
  return item ? this.wrapFn(item) : undefined;
66
68
  }
69
+ /**
70
+ * Look up by exact id *within a faction*. Use this when an id is shared
71
+ * across factions (e.g. `chaos-land-raider` lives under five Chaos factions)
72
+ * and a faction context is known — {@link get} would return whichever copy
73
+ * was registered first, which may belong to the wrong faction. Returns
74
+ * `undefined` when no record with that id belongs to `factionId`.
75
+ */
76
+ getInFaction(id, factionId) {
77
+ const list = this.byFactionId.get(factionId);
78
+ const item = list?.find((i) => this.idOf(i) === id);
79
+ return item ? this.wrapFn(item) : undefined;
80
+ }
67
81
  /** Whether a record with this exact id exists. */
68
82
  has(id) {
69
83
  return this.byId.has(id);
@@ -116,3 +130,4 @@ function push(map, key, value) {
116
130
  else
117
131
  map.set(key, [value]);
118
132
  }
133
+ //# sourceMappingURL=collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/data/collection.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A queryable, iterable view over one entity collection.\n *\n * Indexes (by id, by normalized name, by faction) are built once at construction.\n * Records are deduplicated by {@link CollectionConfig.dedupeKeyOf} (default: id,\n * first occurrence wins). Some records are intentionally shared: the same unit\n * id (e.g. `ministorum-priest`) appears under several factions, so units dedupe\n * on `(faction_id, id)` to keep each faction's copy; identical core abilities\n * (e.g. `leader`) copied into many faction files dedupe away on `ability_id`.\n *\n * `get(id)`/`find` return the first match when an id is shared across factions;\n * use {@link Collection.byFaction} or {@link Collection.findAll} to disambiguate.\n *\n * @packageDocumentation\n */\nimport { normalizeName } from \"./normalize.js\";\n\n/** How a {@link Collection} reads keys and builds views from raw records. */\nexport interface CollectionConfig<T, V> {\n items: T[];\n /** Primary id of a record (e.g. `u => u.id`, `a => a.ability_id`). */\n idOf: (item: T) => string;\n /**\n * Uniqueness key used for deduplication. Defaults to {@link idOf}. Set to a\n * composite (e.g. `(faction_id, id)`) for records that share an id across\n * factions, so distinct copies are preserved rather than collapsed.\n */\n dedupeKeyOf?: (item: T) => string;\n /** Display name, if the record has one — drives {@link Collection.find}. */\n nameOf?: (item: T) => string | undefined;\n /** Owning faction id, if applicable — drives {@link Collection.byFaction}. */\n factionOf?: (item: T) => string | null | undefined;\n /** Wrap a raw record in its linked view. */\n wrap: (item: T) => V;\n}\n\n/**\n * A collection of one entity type, exposing id/name/faction lookups.\n *\n * Iterable: `for (const unit of units) { … }`.\n *\n * @typeParam T - the raw (generated) record type\n * @typeParam V - the linked view type returned to callers\n */\nexport class Collection<T, V> implements Iterable<V> {\n private readonly items: T[] = [];\n private readonly byId = new Map<string, T>();\n private readonly byNorm = new Map<string, T[]>();\n private readonly byFactionId = new Map<string, T[]>();\n private readonly idOf: (item: T) => string;\n private readonly nameOf?: (item: T) => string | undefined;\n private readonly wrapFn: (item: T) => V;\n\n constructor(cfg: CollectionConfig<T, V>) {\n this.idOf = cfg.idOf;\n this.nameOf = cfg.nameOf;\n this.wrapFn = cfg.wrap;\n const dedupeKeyOf = cfg.dedupeKeyOf ?? cfg.idOf;\n const seen = new Set<string>();\n for (const item of cfg.items) {\n const dedupeKey = dedupeKeyOf(item);\n if (seen.has(dedupeKey)) continue; // first-wins dedup\n seen.add(dedupeKey);\n this.items.push(item);\n\n const id = cfg.idOf(item);\n if (!this.byId.has(id)) this.byId.set(id, item); // first-wins for shared ids\n\n const name = cfg.nameOf?.(item);\n if (name) push(this.byNorm, normalizeName(name), item);\n\n const faction = cfg.factionOf?.(item);\n if (faction) push(this.byFactionId, faction, item);\n }\n }\n\n /** Every record, deduplicated by id, in first-seen order. */\n get all(): V[] {\n return this.items.map((item) => this.wrapFn(item));\n }\n\n /** Number of distinct records. */\n get size(): number {\n return this.items.length;\n }\n\n /** Look up by exact id. */\n get(id: string): V | undefined {\n const item = this.byId.get(id);\n return item ? this.wrapFn(item) : undefined;\n }\n\n /**\n * Look up by exact id *within a faction*. Use this when an id is shared\n * across factions (e.g. `chaos-land-raider` lives under five Chaos factions)\n * and a faction context is known — {@link get} would return whichever copy\n * was registered first, which may belong to the wrong faction. Returns\n * `undefined` when no record with that id belongs to `factionId`.\n */\n getInFaction(id: string, factionId: string): V | undefined {\n const list = this.byFactionId.get(factionId);\n const item = list?.find((i) => this.idOf(i) === id);\n return item ? this.wrapFn(item) : undefined;\n }\n\n /** Whether a record with this exact id exists. */\n has(id: string): boolean {\n return this.byId.has(id);\n }\n\n /**\n * Find one record by id or name. Name matching is diacritic- and\n * punctuation-insensitive (see {@link normalizeName}), trying, in order:\n * exact id → exact normalized name → normalized-name substring. Returns the\n * first match; names can repeat across factions, so use {@link findAll} or\n * {@link byFaction} when a query may be ambiguous.\n *\n * @example\n * units.find(\"Kharn\"); // resolves \"Khârn the Betrayer\"\n */\n find(query: string): V | undefined {\n return this.findAll(query)[0];\n }\n\n /**\n * All records matching a query, by the same rules as {@link find}. An exact id\n * match returns just that record; otherwise every normalized-name-exact match\n * is returned, falling back to every normalized-name-substring match. Useful\n * to surface (rather than silently collapse) names shared across factions.\n */\n findAll(query: string): V[] {\n const byId = this.byId.get(query);\n if (byId) return [this.wrapFn(byId)];\n\n const key = normalizeName(query);\n const exact = this.byNorm.get(key);\n if (exact && exact.length > 0) return exact.map((i) => this.wrapFn(i));\n\n if (!this.nameOf || key === \"\") return [];\n return this.items\n .filter((item) => normalizeName(this.nameOf!(item) ?? \"\").includes(key))\n .map((item) => this.wrapFn(item));\n }\n\n /** All records belonging to a faction id (empty if the type has no faction). */\n byFaction(factionId: string): V[] {\n return (this.byFactionId.get(factionId) ?? []).map((i) => this.wrapFn(i));\n }\n\n [Symbol.iterator](): Iterator<V> {\n return this.items.map((item) => this.wrapFn(item))[Symbol.iterator]();\n }\n}\n\nfunction push<K, T>(map: Map<K, T[]>, key: K, value: T): void {\n const existing = map.get(key);\n if (existing) existing.push(value);\n else map.set(key, [value]);\n}\n"]}
@@ -5,14 +5,47 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
- import type { DeploymentPattern, Detachment, Enhancement, ForceDisposition, GameVersion, InteractionFlag, LeaderAttachment, Mission, MissionMatchup, Phase, ResourcePool, SecondaryCard, Stratagem, TimingFlag, Unit, UnitComposition, WargearOption } from "../generated.js";
8
+ import type { DeploymentPattern, Detachment, Enhancement, ForceDisposition, GameVersion, InteractionFlag, LeaderAttachment, Mission, MissionMatchup, Phase, ResourcePool, SecondaryCard, Stratagem, TimingFlag, Unit, UnitComposition, WargearOption, WeaponKeyword } from "../generated.js";
9
9
  import { Collection } from "./collection.js";
10
- import { AbilityView, FactionView, UnitView, WeaponView } from "./entities.js";
10
+ import { AbilityView, FactionView, UnitView, WeaponKeywordView, WeaponView } from "./entities.js";
11
11
  import { type RawData } from "./types.js";
12
+ import type { Buff, BuffSource, EngineContext } from "../cruncher/buffs.js";
13
+ import { type EligibilityInput, type EligibleAbility } from "../abilities-resolver/index.js";
14
+ /**
15
+ * One toggleable buff lever for damage analysis: the contributions it adds and
16
+ * whether it's on by default. `enabled` is `true` for buffs that always apply
17
+ * (intrinsic keywords, unconditional abilities) and `false` for player
18
+ * decisions — stratagems (CP cost) and activatable gates (dice-pool options,
19
+ * `choice` branches, timing-gated activations). A consumer flips `enabled`,
20
+ * then crunches the enabled subset; an optimizer searches it.
21
+ *
22
+ * @see {@link Dataset.stackableBuffsFor}
23
+ */
24
+ export type StackableBuff = {
25
+ /** Stable toggle id (stable across re-enumeration of the same input). */
26
+ id: string;
27
+ /** Human label for the lever. */
28
+ label: string;
29
+ /** Contributions this lever adds when enabled (≥1). */
30
+ buffs: Buff[];
31
+ /** Default selection state. */
32
+ enabled: boolean;
33
+ /** Where the lever came from. */
34
+ source: BuffSource;
35
+ /** Id of the mutually-limited {@link StackableBuffGroup} this belongs to, if any. */
36
+ group?: string;
37
+ };
38
+ /** A pool of {@link StackableBuff} levers limited to `maxActivations` at once. */
39
+ export type StackableBuffGroup = {
40
+ id: string;
41
+ label: string;
42
+ maxActivations: number;
43
+ };
12
44
  /** The whole dataset, with linked accessors over every entity collection. */
13
45
  export declare class Dataset {
14
46
  readonly units: Collection<Unit, UnitView>;
15
47
  readonly weapons: Collection<RawData["weapons"][number], WeaponView>;
48
+ readonly weaponKeywords: Collection<WeaponKeyword, WeaponKeywordView>;
16
49
  readonly factions: Collection<RawData["factions"][number], FactionView>;
17
50
  readonly abilities: Collection<RawData["abilities"][number], AbilityView>;
18
51
  readonly detachments: Collection<Detachment, Detachment>;
@@ -37,6 +70,8 @@ export declare class Dataset {
37
70
  private readonly unitsByAbility;
38
71
  /** weapon id → units that list it. */
39
72
  private readonly unitsByWeapon;
73
+ /** weapon-keyword id → weapons whose profiles reference it. */
74
+ private readonly weaponsByKeyword;
40
75
  constructor(raw?: RawData);
41
76
  /** The dataset built from the package's embedded data. */
42
77
  static embedded(): Dataset;
@@ -46,5 +81,100 @@ export declare class Dataset {
46
81
  unitsWithAbility(abilityId: string): UnitView[];
47
82
  /** Units that list the given weapon id. */
48
83
  unitsWithWeapon(weaponId: string): UnitView[];
84
+ /** Weapons whose profiles reference the given weapon-keyword id. */
85
+ weaponsWithKeyword(keywordId: string): WeaponView[];
86
+ /**
87
+ * Leaders whose leader-attachment data lists `bodyguardUnitId` among its
88
+ * eligible body units, sorted by name. The attachment is stored on the
89
+ * leader pointing down to its bodyguards, so answering "which leaders can
90
+ * attach to this unit?" means scanning the attachment list. Returns an empty
91
+ * array for a unit that no leader can attach to (including leader units).
92
+ */
93
+ leadersAttachableTo(bodyguardUnitId: string): UnitView[];
94
+ /**
95
+ * The inverse of {@link leadersAttachableTo}: the body units the given
96
+ * leader can attach to, sorted by name. Scans the same leader-attachment
97
+ * data from the leader's side (`leader_id` matches; resolve each
98
+ * `eligible_bodyguard_ids` entry), deduped by id. Empty for a non-leader
99
+ * unit. Together the two queries give the bidirectional attachment graph the
100
+ * SPA needs to offer a partner dropdown from either end.
101
+ */
102
+ bodyguardsAttachableFrom(leaderUnitId: string): UnitView[];
103
+ /**
104
+ * Enumerate every ability that could apply to the given unit in `phase`,
105
+ * grouped by source. The SPA uses this to render the abilities pane.
106
+ */
107
+ eligibleAbilities(input: EligibilityInput, phase: Phase): EligibleAbility[];
108
+ /**
109
+ * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:
110
+ * intrinsic weapon-profile keywords plus every eligible ability whose DSL
111
+ * effect translates to an attacker-side buff (army, detachment, unit,
112
+ * attached members, support, plus any stratagems the caller has opted into).
113
+ *
114
+ * The result includes only buffs the buff layer can express today — the
115
+ * `unsupported` half of the DSL→Buff translation is dropped here so callers
116
+ * who just want the stack don't need to thread diagnostics through. Use
117
+ * {@link AbilityView.describeBuffs} when you need the diagnostics for an
118
+ * individual ability. Symmetric to {@link defensiveBuffsFor}, which walks
119
+ * the same eligibility set under target perspective.
120
+ */
121
+ buffsFor(input: EligibilityInput & {
122
+ weaponProfiles?: {
123
+ weaponId: string;
124
+ profileIndex: number;
125
+ }[];
126
+ /** Stratagem ids the caller has opted into spending CP on. */
127
+ optedInStratagemIds?: string[];
128
+ }, context: EngineContext): Buff[];
129
+ /**
130
+ * Defender-perspective buff stack for the chosen unit: walks the same
131
+ * eligible-abilities set as {@link buffsFor} but translates each ability's
132
+ * DSL effect as defensive (FNP, save mods from `stat-modifier Sv`,
133
+ * toughness mods from `stat-modifier T`, save rerolls, incoming hit
134
+ * penalties from `bs-modifier`). Use this when the chosen unit is being
135
+ * crunched as the *target* — the engine reads `feelNoPain`/`saveMod`/
136
+ * `toughnessMod` out of `resolveBuffs` so wiring the result into `crunch`
137
+ * just means concatenating onto the existing `buffs` array.
138
+ *
139
+ * `weaponProfiles` are ignored under target perspective — weapon-keyword
140
+ * effects ride with the firing weapon, not the receiving unit.
141
+ */
142
+ defensiveBuffsFor(input: EligibilityInput & {
143
+ optedInStratagemIds?: string[];
144
+ }, context: EngineContext): Buff[];
145
+ /**
146
+ * Enumerate every attacker-side buff a unit could stack in `context` as a
147
+ * list of toggleable levers, plus the activation groups that limit them.
148
+ *
149
+ * Unlike {@link buffsFor} — which returns only the buffs that auto-apply —
150
+ * this surfaces the *player decisions* too: stratagems, and the activatable
151
+ * gates the DSL models as dice-pool options, `choice` branches, or
152
+ * timing-gated activations (e.g. Blessings of Khorne's three keyword grants).
153
+ * Each lever carries `enabled` (its default state) and, where it's part of a
154
+ * limited pool, a `group` id whose {@link StackableBuffGroup} caps how many
155
+ * can fire at once. The intended loop:
156
+ *
157
+ * ```ts
158
+ * const { buffs } = ds.stackableBuffsFor(input, ctx);
159
+ * const chosen = buffs.filter(b => b.enabled).flatMap(b => b.buffs);
160
+ * crunch({ ...profiles, buffs: chosen, context: ctx }, ds);
161
+ * ```
162
+ *
163
+ * Target/phase conditions a lever still carries (e.g. "vs Infantry") ride on
164
+ * each buff's `applicableWhen`, so toggling it on is always safe — the
165
+ * resolver gates it per-target.
166
+ */
167
+ stackableBuffsFor(input: EligibilityInput & {
168
+ weaponProfiles?: {
169
+ weaponId: string;
170
+ profileIndex: number;
171
+ }[];
172
+ }, context: EngineContext): {
173
+ buffs: StackableBuff[];
174
+ groups: StackableBuffGroup[];
175
+ };
176
+ /** Shared implementation for buffsFor / defensiveBuffsFor. */
177
+ private collectBuffs;
49
178
  private buildIndexes;
50
179
  }
180
+ //# sourceMappingURL=dataset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset.d.ts","sourceRoot":"","sources":["../../src/data/dataset.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,14 @@
1
1
  import { Collection } from "./collection.js";
2
- import { AbilityView, FactionView, UnitView, WeaponView } from "./entities.js";
2
+ import { AbilityView, FactionView, UnitView, WeaponKeywordView, WeaponView, } from "./entities.js";
3
3
  import { emptyRawData } from "./types.js";
4
4
  import { RAW_DATA } from "./bundle.generated.js";
5
+ import { resolveEligibleAbilities, } from "../abilities-resolver/index.js";
5
6
  /** The whole dataset, with linked accessors over every entity collection. */
6
7
  export class Dataset {
7
8
  // Richly-linked collections.
8
9
  units;
9
10
  weapons;
11
+ weaponKeywords;
10
12
  factions;
11
13
  abilities;
12
14
  // Id-bearing collections without bespoke views (records returned as-is).
@@ -33,6 +35,8 @@ export class Dataset {
33
35
  unitsByAbility = new Map();
34
36
  /** weapon id → units that list it. */
35
37
  unitsByWeapon = new Map();
38
+ /** weapon-keyword id → weapons whose profiles reference it. */
39
+ weaponsByKeyword = new Map();
36
40
  constructor(raw = emptyRawData()) {
37
41
  this.units = new Collection({
38
42
  items: raw.units,
@@ -50,6 +54,12 @@ export class Dataset {
50
54
  nameOf: (w) => w.name,
51
55
  wrap: (w) => new WeaponView(w, this),
52
56
  });
57
+ this.weaponKeywords = new Collection({
58
+ items: raw.weaponKeywords,
59
+ idOf: (k) => k.id,
60
+ nameOf: (k) => k.name,
61
+ wrap: (k) => new WeaponKeywordView(k, this),
62
+ });
53
63
  this.factions = new Collection({
54
64
  items: raw.factions,
55
65
  idOf: (f) => f.id,
@@ -97,6 +107,203 @@ export class Dataset {
97
107
  unitsWithWeapon(weaponId) {
98
108
  return (this.unitsByWeapon.get(weaponId) ?? []).map((u) => new UnitView(u, this));
99
109
  }
110
+ /** Weapons whose profiles reference the given weapon-keyword id. */
111
+ weaponsWithKeyword(keywordId) {
112
+ return (this.weaponsByKeyword.get(keywordId) ?? []).map((w) => new WeaponView(w, this));
113
+ }
114
+ /**
115
+ * Leaders whose leader-attachment data lists `bodyguardUnitId` among its
116
+ * eligible body units, sorted by name. The attachment is stored on the
117
+ * leader pointing down to its bodyguards, so answering "which leaders can
118
+ * attach to this unit?" means scanning the attachment list. Returns an empty
119
+ * array for a unit that no leader can attach to (including leader units).
120
+ */
121
+ leadersAttachableTo(bodyguardUnitId) {
122
+ return this.leaderAttachments
123
+ .filter((la) => la.eligible_bodyguard_ids.includes(bodyguardUnitId))
124
+ .map((la) => this.units.get(la.leader_id))
125
+ .filter((u) => u !== undefined)
126
+ .sort((a, b) => a.name.localeCompare(b.name));
127
+ }
128
+ /**
129
+ * The inverse of {@link leadersAttachableTo}: the body units the given
130
+ * leader can attach to, sorted by name. Scans the same leader-attachment
131
+ * data from the leader's side (`leader_id` matches; resolve each
132
+ * `eligible_bodyguard_ids` entry), deduped by id. Empty for a non-leader
133
+ * unit. Together the two queries give the bidirectional attachment graph the
134
+ * SPA needs to offer a partner dropdown from either end.
135
+ */
136
+ bodyguardsAttachableFrom(leaderUnitId) {
137
+ const seen = new Set();
138
+ const out = [];
139
+ for (const la of this.leaderAttachments) {
140
+ if (la.leader_id !== leaderUnitId)
141
+ continue;
142
+ for (const bodyguardId of la.eligible_bodyguard_ids) {
143
+ if (seen.has(bodyguardId))
144
+ continue;
145
+ const unit = this.units.get(bodyguardId);
146
+ if (!unit)
147
+ continue;
148
+ seen.add(bodyguardId);
149
+ out.push(unit);
150
+ }
151
+ }
152
+ return out.sort((a, b) => a.name.localeCompare(b.name));
153
+ }
154
+ /**
155
+ * Enumerate every ability that could apply to the given unit in `phase`,
156
+ * grouped by source. The SPA uses this to render the abilities pane.
157
+ */
158
+ eligibleAbilities(input, phase) {
159
+ return resolveEligibleAbilities(this, input, phase);
160
+ }
161
+ /**
162
+ * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:
163
+ * intrinsic weapon-profile keywords plus every eligible ability whose DSL
164
+ * effect translates to an attacker-side buff (army, detachment, unit,
165
+ * attached members, support, plus any stratagems the caller has opted into).
166
+ *
167
+ * The result includes only buffs the buff layer can express today — the
168
+ * `unsupported` half of the DSL→Buff translation is dropped here so callers
169
+ * who just want the stack don't need to thread diagnostics through. Use
170
+ * {@link AbilityView.describeBuffs} when you need the diagnostics for an
171
+ * individual ability. Symmetric to {@link defensiveBuffsFor}, which walks
172
+ * the same eligibility set under target perspective.
173
+ */
174
+ buffsFor(input, context) {
175
+ return this.collectBuffs(input, context, "attacker");
176
+ }
177
+ /**
178
+ * Defender-perspective buff stack for the chosen unit: walks the same
179
+ * eligible-abilities set as {@link buffsFor} but translates each ability's
180
+ * DSL effect as defensive (FNP, save mods from `stat-modifier Sv`,
181
+ * toughness mods from `stat-modifier T`, save rerolls, incoming hit
182
+ * penalties from `bs-modifier`). Use this when the chosen unit is being
183
+ * crunched as the *target* — the engine reads `feelNoPain`/`saveMod`/
184
+ * `toughnessMod` out of `resolveBuffs` so wiring the result into `crunch`
185
+ * just means concatenating onto the existing `buffs` array.
186
+ *
187
+ * `weaponProfiles` are ignored under target perspective — weapon-keyword
188
+ * effects ride with the firing weapon, not the receiving unit.
189
+ */
190
+ defensiveBuffsFor(input, context) {
191
+ return this.collectBuffs(input, context, "target");
192
+ }
193
+ /**
194
+ * Enumerate every attacker-side buff a unit could stack in `context` as a
195
+ * list of toggleable levers, plus the activation groups that limit them.
196
+ *
197
+ * Unlike {@link buffsFor} — which returns only the buffs that auto-apply —
198
+ * this surfaces the *player decisions* too: stratagems, and the activatable
199
+ * gates the DSL models as dice-pool options, `choice` branches, or
200
+ * timing-gated activations (e.g. Blessings of Khorne's three keyword grants).
201
+ * Each lever carries `enabled` (its default state) and, where it's part of a
202
+ * limited pool, a `group` id whose {@link StackableBuffGroup} caps how many
203
+ * can fire at once. The intended loop:
204
+ *
205
+ * ```ts
206
+ * const { buffs } = ds.stackableBuffsFor(input, ctx);
207
+ * const chosen = buffs.filter(b => b.enabled).flatMap(b => b.buffs);
208
+ * crunch({ ...profiles, buffs: chosen, context: ctx }, ds);
209
+ * ```
210
+ *
211
+ * Target/phase conditions a lever still carries (e.g. "vs Infantry") ride on
212
+ * each buff's `applicableWhen`, so toggling it on is always safe — the
213
+ * resolver gates it per-target.
214
+ */
215
+ stackableBuffsFor(input, context) {
216
+ const buffs = [];
217
+ const groups = new Map();
218
+ // Surface the attachment fact to the DSL translator so `is-attached` /
219
+ // `model-is-leader` conditions can evaluate. Clone — never mutate the
220
+ // caller's context. An explicitly-set flag wins over the derivation.
221
+ const ctx = {
222
+ ...context,
223
+ attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,
224
+ };
225
+ // Intrinsic weapon-profile keywords — always on.
226
+ for (const ref of input.weaponProfiles ?? []) {
227
+ const weapon = this.weapons.get(ref.weaponId);
228
+ if (!weapon)
229
+ continue;
230
+ const wk = weapon.profileBuffs(ref.profileIndex, ctx);
231
+ if (wk.length === 0)
232
+ continue;
233
+ buffs.push({
234
+ id: `weapon:${ref.weaponId}:${ref.profileIndex}`,
235
+ label: `${weapon.name} keywords`,
236
+ buffs: wk,
237
+ enabled: true,
238
+ source: wk[0].source,
239
+ });
240
+ }
241
+ for (const entry of this.eligibleAbilities(input, ctx.phase)) {
242
+ const source = bufferSourceFromEligible(entry);
243
+ const { applied, activatable } = entry.ability.describeBuffs(source, ctx, "attacker");
244
+ // Stratagems cost CP — opt-in, not on by default.
245
+ const isStratagem = entry.source.kind === "detachment-stratagem";
246
+ if (applied.length > 0) {
247
+ buffs.push({
248
+ id: `${entry.source.kind}:${entry.ability.id}`,
249
+ label: entry.ability.name,
250
+ buffs: applied,
251
+ enabled: !isStratagem,
252
+ source,
253
+ });
254
+ }
255
+ for (const act of activatable) {
256
+ let groupId;
257
+ if (act.group) {
258
+ groupId = act.group.id;
259
+ if (!groups.has(groupId)) {
260
+ groups.set(groupId, {
261
+ id: groupId,
262
+ label: entry.ability.name,
263
+ maxActivations: act.group.maxActivations,
264
+ });
265
+ }
266
+ }
267
+ buffs.push({
268
+ id: act.id,
269
+ label: `${entry.ability.name} — ${act.label}`,
270
+ buffs: act.buffs,
271
+ enabled: false,
272
+ source,
273
+ group: groupId,
274
+ });
275
+ }
276
+ }
277
+ return { buffs, groups: [...groups.values()] };
278
+ }
279
+ /** Shared implementation for buffsFor / defensiveBuffsFor. */
280
+ collectBuffs(input, context, perspective) {
281
+ const out = [];
282
+ // Surface the attachment fact to the DSL translator (see stackableBuffsFor).
283
+ // Clone — never mutate the caller's context; explicit flag wins.
284
+ const ctx = {
285
+ ...context,
286
+ attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,
287
+ };
288
+ // Weapon-profile keywords are attacker-only.
289
+ if (perspective === "attacker") {
290
+ for (const ref of input.weaponProfiles ?? []) {
291
+ const weapon = this.weapons.get(ref.weaponId);
292
+ if (!weapon)
293
+ continue;
294
+ out.push(...weapon.profileBuffs(ref.profileIndex, ctx));
295
+ }
296
+ }
297
+ const optedIn = new Set(input.optedInStratagemIds ?? []);
298
+ for (const entry of this.eligibleAbilities(input, ctx.phase)) {
299
+ if (entry.source.kind === "detachment-stratagem" && !optedIn.has(entry.source.stratagemId)) {
300
+ continue;
301
+ }
302
+ const source = bufferSourceFromEligible(entry);
303
+ out.push(...entry.ability.getBuffs(source, ctx, perspective));
304
+ }
305
+ return out;
306
+ }
100
307
  buildIndexes(raw) {
101
308
  for (const pm of raw.phaseMappings) {
102
309
  const key = `${pm.source_type}:${pm.source_id}`;
@@ -113,6 +320,22 @@ export class Dataset {
113
320
  for (const weaponId of unit.weapon_ids ?? [])
114
321
  push(this.unitsByWeapon, weaponId, unit);
115
322
  }
323
+ const seenByKeyword = new Map();
324
+ for (const weapon of raw.weapons) {
325
+ for (const profile of weapon.profiles) {
326
+ for (const ref of profile.keywords ?? []) {
327
+ let seen = seenByKeyword.get(ref.keyword_id);
328
+ if (!seen) {
329
+ seen = new Set();
330
+ seenByKeyword.set(ref.keyword_id, seen);
331
+ }
332
+ if (seen.has(weapon.id))
333
+ continue;
334
+ seen.add(weapon.id);
335
+ push(this.weaponsByKeyword, ref.keyword_id, weapon);
336
+ }
337
+ }
338
+ }
116
339
  }
117
340
  }
118
341
  /** Build a passthrough collection for an id-bearing record type. */
@@ -132,3 +355,27 @@ function push(map, key, value) {
132
355
  else
133
356
  map.set(key, [value]);
134
357
  }
358
+ /** Map an EligibleAbility back to the BuffSource the translator expects. */
359
+ function bufferSourceFromEligible(entry) {
360
+ const abilityId = entry.ability.id;
361
+ switch (entry.source.kind) {
362
+ case "army":
363
+ return { kind: "ability", abilityId, abilityKind: "army" };
364
+ case "detachment":
365
+ return { kind: "ability", abilityId, abilityKind: "detachment" };
366
+ case "detachment-stratagem":
367
+ return { kind: "ability", abilityId, abilityKind: "detachment-stratagem" };
368
+ case "unit":
369
+ return { kind: "ability", abilityId, abilityKind: "unit" };
370
+ case "attached":
371
+ return {
372
+ kind: "ability",
373
+ abilityId,
374
+ abilityKind: "attached",
375
+ sourceUnitId: entry.source.unitId,
376
+ };
377
+ case "support":
378
+ return { kind: "ability", abilityId, abilityKind: "support" };
379
+ }
380
+ }
381
+ //# sourceMappingURL=dataset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset.js","sourceRoot":"","sources":["../../src/data/dataset.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * {@link Dataset} ties the embedded records together: it owns every\n * {@link Collection}, builds the cross-entity indexes once, and is the `this`\n * the linked views resolve against.\n *\n * @packageDocumentation\n */\nimport type {\n DeploymentPattern,\n Detachment,\n Enhancement,\n ForceDisposition,\n GameVersion,\n InteractionFlag,\n LeaderAttachment,\n Mission,\n MissionMatchup,\n Phase,\n ResourcePool,\n SecondaryCard,\n Stratagem,\n TimingFlag,\n Unit,\n UnitComposition,\n WargearOption,\n WeaponKeyword,\n} from \"../generated.js\";\nimport { Collection } from \"./collection.js\";\nimport {\n AbilityView,\n FactionView,\n UnitView,\n WeaponKeywordView,\n WeaponView,\n} from \"./entities.js\";\nimport { emptyRawData, type RawData } from \"./types.js\";\nimport { RAW_DATA } from \"./bundle.generated.js\";\nimport type { Buff, BuffSource, EngineContext } from \"../cruncher/buffs.js\";\nimport {\n resolveEligibleAbilities,\n type EligibilityInput,\n type EligibleAbility,\n} from \"../abilities-resolver/index.js\";\n\n/**\n * One toggleable buff lever for damage analysis: the contributions it adds and\n * whether it's on by default. `enabled` is `true` for buffs that always apply\n * (intrinsic keywords, unconditional abilities) and `false` for player\n * decisions — stratagems (CP cost) and activatable gates (dice-pool options,\n * `choice` branches, timing-gated activations). A consumer flips `enabled`,\n * then crunches the enabled subset; an optimizer searches it.\n *\n * @see {@link Dataset.stackableBuffsFor}\n */\nexport type StackableBuff = {\n /** Stable toggle id (stable across re-enumeration of the same input). */\n id: string;\n /** Human label for the lever. */\n label: string;\n /** Contributions this lever adds when enabled (≥1). */\n buffs: Buff[];\n /** Default selection state. */\n enabled: boolean;\n /** Where the lever came from. */\n source: BuffSource;\n /** Id of the mutually-limited {@link StackableBuffGroup} this belongs to, if any. */\n group?: string;\n};\n\n/** A pool of {@link StackableBuff} levers limited to `maxActivations` at once. */\nexport type StackableBuffGroup = {\n id: string;\n label: string;\n maxActivations: number;\n};\n\n/** The whole dataset, with linked accessors over every entity collection. */\nexport class Dataset {\n // Richly-linked collections.\n readonly units: Collection<Unit, UnitView>;\n readonly weapons: Collection<RawData[\"weapons\"][number], WeaponView>;\n readonly weaponKeywords: Collection<WeaponKeyword, WeaponKeywordView>;\n readonly factions: Collection<RawData[\"factions\"][number], FactionView>;\n readonly abilities: Collection<RawData[\"abilities\"][number], AbilityView>;\n\n // Id-bearing collections without bespoke views (records returned as-is).\n readonly detachments: Collection<Detachment, Detachment>;\n readonly enhancements: Collection<Enhancement, Enhancement>;\n readonly stratagems: Collection<Stratagem, Stratagem>;\n readonly wargearOptions: Collection<WargearOption, WargearOption>;\n readonly missions: Collection<Mission, Mission>;\n readonly missionMatchups: Collection<MissionMatchup, MissionMatchup>;\n readonly secondaryCards: Collection<SecondaryCard, SecondaryCard>;\n readonly deploymentPatterns: Collection<DeploymentPattern, DeploymentPattern>;\n readonly forceDispositions: Collection<ForceDisposition, ForceDisposition>;\n readonly resourcePools: Collection<ResourcePool, ResourcePool>;\n\n // Id-less collections, exposed as plain arrays.\n readonly leaderAttachments: readonly LeaderAttachment[];\n readonly unitCompositions: readonly UnitComposition[];\n readonly gameVersions: readonly GameVersion[];\n readonly timingFlags: readonly TimingFlag[];\n readonly interactionFlags: readonly InteractionFlag[];\n readonly phaseMappings: readonly RawData[\"phaseMappings\"][number][];\n\n /** `source_type:source_id` → unioned phases. */\n private readonly phaseIndex = new Map<string, Phase[]>();\n /** ability id → units that list it. */\n private readonly unitsByAbility = new Map<string, Unit[]>();\n /** weapon id → units that list it. */\n private readonly unitsByWeapon = new Map<string, Unit[]>();\n /** weapon-keyword id → weapons whose profiles reference it. */\n private readonly weaponsByKeyword = new Map<string, RawData[\"weapons\"][number][]>();\n\n constructor(raw: RawData = emptyRawData()) {\n this.units = new Collection({\n items: raw.units,\n idOf: (u) => u.id,\n // The same unit id is shared across factions (e.g. ministorum-priest);\n // keep each faction's copy, collapse only true within-faction duplicates.\n dedupeKeyOf: (u) => `${u.faction_id}::${u.id}`,\n nameOf: (u) => u.name,\n factionOf: (u) => u.faction_id,\n wrap: (u) => new UnitView(u, this),\n });\n this.weapons = new Collection({\n items: raw.weapons,\n idOf: (w) => w.id,\n nameOf: (w) => w.name,\n wrap: (w) => new WeaponView(w, this),\n });\n this.weaponKeywords = new Collection({\n items: raw.weaponKeywords,\n idOf: (k) => k.id,\n nameOf: (k) => k.name,\n wrap: (k) => new WeaponKeywordView(k, this),\n });\n this.factions = new Collection({\n items: raw.factions,\n idOf: (f) => f.id,\n nameOf: (f) => f.name,\n wrap: (f) => new FactionView(f, this),\n });\n this.abilities = new Collection({\n items: raw.abilities,\n idOf: (a) => a.ability_id,\n nameOf: (a) => a.name,\n factionOf: (a) => a.faction_id,\n wrap: (a) => new AbilityView(a, this),\n });\n\n this.detachments = idCollection(raw.detachments, (d) => d.faction_id);\n this.enhancements = idCollection(raw.enhancements);\n this.stratagems = idCollection(raw.stratagems);\n this.wargearOptions = idCollection(raw.wargearOptions);\n this.missions = idCollection(raw.missions);\n this.missionMatchups = idCollection(raw.missionMatchups);\n this.secondaryCards = idCollection(raw.secondaryCards);\n this.deploymentPatterns = idCollection(raw.deploymentPatterns);\n this.forceDispositions = idCollection(raw.forceDispositions);\n this.resourcePools = idCollection(raw.resourcePools);\n\n this.leaderAttachments = raw.leaderAttachments;\n this.unitCompositions = raw.unitCompositions;\n this.gameVersions = raw.gameVersions;\n this.timingFlags = raw.timingFlags;\n this.interactionFlags = raw.interactionFlags;\n this.phaseMappings = raw.phaseMappings;\n\n this.buildIndexes(raw);\n }\n\n /** The dataset built from the package's embedded data. */\n static embedded(): Dataset {\n return new Dataset(RAW_DATA);\n }\n\n /** Phases a source acts in, unioned across its phase-mappings. */\n phasesFor(sourceType: string, sourceId: string): Phase[] {\n return this.phaseIndex.get(`${sourceType}:${sourceId}`) ?? [];\n }\n\n /** Units that list the given ability id. */\n unitsWithAbility(abilityId: string): UnitView[] {\n return (this.unitsByAbility.get(abilityId) ?? []).map((u) => new UnitView(u, this));\n }\n\n /** Units that list the given weapon id. */\n unitsWithWeapon(weaponId: string): UnitView[] {\n return (this.unitsByWeapon.get(weaponId) ?? []).map((u) => new UnitView(u, this));\n }\n\n /** Weapons whose profiles reference the given weapon-keyword id. */\n weaponsWithKeyword(keywordId: string): WeaponView[] {\n return (this.weaponsByKeyword.get(keywordId) ?? []).map((w) => new WeaponView(w, this));\n }\n\n /**\n * Leaders whose leader-attachment data lists `bodyguardUnitId` among its\n * eligible body units, sorted by name. The attachment is stored on the\n * leader pointing down to its bodyguards, so answering \"which leaders can\n * attach to this unit?\" means scanning the attachment list. Returns an empty\n * array for a unit that no leader can attach to (including leader units).\n */\n leadersAttachableTo(bodyguardUnitId: string): UnitView[] {\n return this.leaderAttachments\n .filter((la) => la.eligible_bodyguard_ids.includes(bodyguardUnitId))\n .map((la) => this.units.get(la.leader_id))\n .filter((u): u is UnitView => u !== undefined)\n .sort((a, b) => a.name.localeCompare(b.name));\n }\n\n /**\n * The inverse of {@link leadersAttachableTo}: the body units the given\n * leader can attach to, sorted by name. Scans the same leader-attachment\n * data from the leader's side (`leader_id` matches; resolve each\n * `eligible_bodyguard_ids` entry), deduped by id. Empty for a non-leader\n * unit. Together the two queries give the bidirectional attachment graph the\n * SPA needs to offer a partner dropdown from either end.\n */\n bodyguardsAttachableFrom(leaderUnitId: string): UnitView[] {\n const seen = new Set<string>();\n const out: UnitView[] = [];\n for (const la of this.leaderAttachments) {\n if (la.leader_id !== leaderUnitId) continue;\n for (const bodyguardId of la.eligible_bodyguard_ids) {\n if (seen.has(bodyguardId)) continue;\n const unit = this.units.get(bodyguardId);\n if (!unit) continue;\n seen.add(bodyguardId);\n out.push(unit);\n }\n }\n return out.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n /**\n * Enumerate every ability that could apply to the given unit in `phase`,\n * grouped by source. The SPA uses this to render the abilities pane.\n */\n eligibleAbilities(input: EligibilityInput, phase: Phase): EligibleAbility[] {\n return resolveEligibleAbilities(this, input, phase);\n }\n\n /**\n * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:\n * intrinsic weapon-profile keywords plus every eligible ability whose DSL\n * effect translates to an attacker-side buff (army, detachment, unit,\n * attached members, support, plus any stratagems the caller has opted into).\n *\n * The result includes only buffs the buff layer can express today — the\n * `unsupported` half of the DSL→Buff translation is dropped here so callers\n * who just want the stack don't need to thread diagnostics through. Use\n * {@link AbilityView.describeBuffs} when you need the diagnostics for an\n * individual ability. Symmetric to {@link defensiveBuffsFor}, which walks\n * the same eligibility set under target perspective.\n */\n buffsFor(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n /** Stratagem ids the caller has opted into spending CP on. */\n optedInStratagemIds?: string[];\n },\n context: EngineContext,\n ): Buff[] {\n return this.collectBuffs(input, context, \"attacker\");\n }\n\n /**\n * Defender-perspective buff stack for the chosen unit: walks the same\n * eligible-abilities set as {@link buffsFor} but translates each ability's\n * DSL effect as defensive (FNP, save mods from `stat-modifier Sv`,\n * toughness mods from `stat-modifier T`, save rerolls, incoming hit\n * penalties from `bs-modifier`). Use this when the chosen unit is being\n * crunched as the *target* — the engine reads `feelNoPain`/`saveMod`/\n * `toughnessMod` out of `resolveBuffs` so wiring the result into `crunch`\n * just means concatenating onto the existing `buffs` array.\n *\n * `weaponProfiles` are ignored under target perspective — weapon-keyword\n * effects ride with the firing weapon, not the receiving unit.\n */\n defensiveBuffsFor(\n input: EligibilityInput & { optedInStratagemIds?: string[] },\n context: EngineContext,\n ): Buff[] {\n return this.collectBuffs(input, context, \"target\");\n }\n\n /**\n * Enumerate every attacker-side buff a unit could stack in `context` as a\n * list of toggleable levers, plus the activation groups that limit them.\n *\n * Unlike {@link buffsFor} — which returns only the buffs that auto-apply —\n * this surfaces the *player decisions* too: stratagems, and the activatable\n * gates the DSL models as dice-pool options, `choice` branches, or\n * timing-gated activations (e.g. Blessings of Khorne's three keyword grants).\n * Each lever carries `enabled` (its default state) and, where it's part of a\n * limited pool, a `group` id whose {@link StackableBuffGroup} caps how many\n * can fire at once. The intended loop:\n *\n * ```ts\n * const { buffs } = ds.stackableBuffsFor(input, ctx);\n * const chosen = buffs.filter(b => b.enabled).flatMap(b => b.buffs);\n * crunch({ ...profiles, buffs: chosen, context: ctx }, ds);\n * ```\n *\n * Target/phase conditions a lever still carries (e.g. \"vs Infantry\") ride on\n * each buff's `applicableWhen`, so toggling it on is always safe — the\n * resolver gates it per-target.\n */\n stackableBuffsFor(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n },\n context: EngineContext,\n ): { buffs: StackableBuff[]; groups: StackableBuffGroup[] } {\n const buffs: StackableBuff[] = [];\n const groups = new Map<string, StackableBuffGroup>();\n\n // Surface the attachment fact to the DSL translator so `is-attached` /\n // `model-is-leader` conditions can evaluate. Clone — never mutate the\n // caller's context. An explicitly-set flag wins over the derivation.\n const ctx: EngineContext = {\n ...context,\n attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,\n };\n\n // Intrinsic weapon-profile keywords — always on.\n for (const ref of input.weaponProfiles ?? []) {\n const weapon = this.weapons.get(ref.weaponId);\n if (!weapon) continue;\n const wk = weapon.profileBuffs(ref.profileIndex, ctx);\n if (wk.length === 0) continue;\n buffs.push({\n id: `weapon:${ref.weaponId}:${ref.profileIndex}`,\n label: `${weapon.name} keywords`,\n buffs: wk,\n enabled: true,\n source: wk[0].source,\n });\n }\n\n for (const entry of this.eligibleAbilities(input, ctx.phase)) {\n const source = bufferSourceFromEligible(entry);\n const { applied, activatable } = entry.ability.describeBuffs(source, ctx, \"attacker\");\n // Stratagems cost CP — opt-in, not on by default.\n const isStratagem = entry.source.kind === \"detachment-stratagem\";\n\n if (applied.length > 0) {\n buffs.push({\n id: `${entry.source.kind}:${entry.ability.id}`,\n label: entry.ability.name,\n buffs: applied,\n enabled: !isStratagem,\n source,\n });\n }\n\n for (const act of activatable) {\n let groupId: string | undefined;\n if (act.group) {\n groupId = act.group.id;\n if (!groups.has(groupId)) {\n groups.set(groupId, {\n id: groupId,\n label: entry.ability.name,\n maxActivations: act.group.maxActivations,\n });\n }\n }\n buffs.push({\n id: act.id,\n label: `${entry.ability.name} — ${act.label}`,\n buffs: act.buffs,\n enabled: false,\n source,\n group: groupId,\n });\n }\n }\n\n return { buffs, groups: [...groups.values()] };\n }\n\n /** Shared implementation for buffsFor / defensiveBuffsFor. */\n private collectBuffs(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n optedInStratagemIds?: string[];\n },\n context: EngineContext,\n perspective: \"attacker\" | \"target\",\n ): Buff[] {\n const out: Buff[] = [];\n\n // Surface the attachment fact to the DSL translator (see stackableBuffsFor).\n // Clone — never mutate the caller's context; explicit flag wins.\n const ctx: EngineContext = {\n ...context,\n attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,\n };\n\n // Weapon-profile keywords are attacker-only.\n if (perspective === \"attacker\") {\n for (const ref of input.weaponProfiles ?? []) {\n const weapon = this.weapons.get(ref.weaponId);\n if (!weapon) continue;\n out.push(...weapon.profileBuffs(ref.profileIndex, ctx));\n }\n }\n\n const optedIn = new Set(input.optedInStratagemIds ?? []);\n for (const entry of this.eligibleAbilities(input, ctx.phase)) {\n if (entry.source.kind === \"detachment-stratagem\" && !optedIn.has(entry.source.stratagemId)) {\n continue;\n }\n const source = bufferSourceFromEligible(entry);\n out.push(...entry.ability.getBuffs(source, ctx, perspective));\n }\n\n return out;\n }\n\n private buildIndexes(raw: RawData): void {\n for (const pm of raw.phaseMappings) {\n const key = `${pm.source_type}:${pm.source_id}`;\n const existing = this.phaseIndex.get(key) ?? [];\n for (const phase of pm.phases) {\n if (!existing.includes(phase)) existing.push(phase);\n }\n this.phaseIndex.set(key, existing);\n }\n for (const unit of raw.units) {\n for (const abilityId of unit.ability_ids ?? []) push(this.unitsByAbility, abilityId, unit);\n for (const weaponId of unit.weapon_ids ?? []) push(this.unitsByWeapon, weaponId, unit);\n }\n const seenByKeyword = new Map<string, Set<string>>();\n for (const weapon of raw.weapons) {\n for (const profile of weapon.profiles) {\n for (const ref of profile.keywords ?? []) {\n let seen = seenByKeyword.get(ref.keyword_id);\n if (!seen) {\n seen = new Set();\n seenByKeyword.set(ref.keyword_id, seen);\n }\n if (seen.has(weapon.id)) continue;\n seen.add(weapon.id);\n push(this.weaponsByKeyword, ref.keyword_id, weapon);\n }\n }\n }\n }\n}\n\n/** Build a passthrough collection for an id-bearing record type. */\nfunction idCollection<T extends { id: string }>(\n items: T[],\n factionOf?: (item: T) => string | null | undefined,\n): Collection<T, T> {\n return new Collection<T, T>({\n items,\n idOf: (i) => i.id,\n nameOf: (i) => (i as { name?: string }).name,\n factionOf,\n wrap: (i) => i,\n });\n}\n\nfunction push<T>(map: Map<string, T[]>, key: string, value: T): void {\n const existing = map.get(key);\n if (existing) existing.push(value);\n else map.set(key, [value]);\n}\n\n/** Map an EligibleAbility back to the BuffSource the translator expects. */\nfunction bufferSourceFromEligible(entry: EligibleAbility): BuffSource {\n const abilityId = entry.ability.id;\n switch (entry.source.kind) {\n case \"army\":\n return { kind: \"ability\", abilityId, abilityKind: \"army\" };\n case \"detachment\":\n return { kind: \"ability\", abilityId, abilityKind: \"detachment\" };\n case \"detachment-stratagem\":\n return { kind: \"ability\", abilityId, abilityKind: \"detachment-stratagem\" };\n case \"unit\":\n return { kind: \"ability\", abilityId, abilityKind: \"unit\" };\n case \"attached\":\n return {\n kind: \"ability\",\n abilityId,\n abilityKind: \"attached\",\n sourceUnitId: entry.source.unitId,\n };\n case \"support\":\n return { kind: \"ability\", abilityId, abilityKind: \"support\" };\n }\n}\n"]}