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

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 (387) hide show
  1. package/dist/abilities-resolver/index.d.ts +9 -0
  2. package/dist/abilities-resolver/index.d.ts.map +1 -0
  3. package/dist/abilities-resolver/index.js +9 -0
  4. package/dist/abilities-resolver/index.js.map +1 -0
  5. package/dist/abilities-resolver/resolver.d.ts +64 -0
  6. package/dist/abilities-resolver/resolver.d.ts.map +1 -0
  7. package/dist/abilities-resolver/resolver.js +135 -0
  8. package/dist/abilities-resolver/resolver.js.map +1 -0
  9. package/dist/bundle-schemas.d.ts +1 -0
  10. package/dist/bundle-schemas.d.ts.map +1 -0
  11. package/dist/bundle-schemas.js +1 -0
  12. package/dist/bundle-schemas.js.map +1 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +2 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/codegen-data.d.ts +1 -0
  18. package/dist/codegen-data.d.ts.map +1 -0
  19. package/dist/codegen-data.js +2 -0
  20. package/dist/codegen-data.js.map +1 -0
  21. package/dist/commands/import.d.ts +1 -0
  22. package/dist/commands/import.d.ts.map +1 -0
  23. package/dist/commands/import.js +1 -0
  24. package/dist/commands/import.js.map +1 -0
  25. package/dist/commands/translate.d.ts +1 -0
  26. package/dist/commands/translate.d.ts.map +1 -0
  27. package/dist/commands/translate.js +1 -0
  28. package/dist/commands/translate.js.map +1 -0
  29. package/dist/commands/validate-all.d.ts +1 -0
  30. package/dist/commands/validate-all.d.ts.map +1 -0
  31. package/dist/commands/validate-all.js +1 -0
  32. package/dist/commands/validate-all.js.map +1 -0
  33. package/dist/commands/validate-core.d.ts +1 -0
  34. package/dist/commands/validate-core.d.ts.map +1 -0
  35. package/dist/commands/validate-core.js +1 -0
  36. package/dist/commands/validate-core.js.map +1 -0
  37. package/dist/commands/validate-enrichment.d.ts +1 -0
  38. package/dist/commands/validate-enrichment.d.ts.map +1 -0
  39. package/dist/commands/validate-enrichment.js +1 -0
  40. package/dist/commands/validate-enrichment.js.map +1 -0
  41. package/dist/convert-faction.d.ts +1 -0
  42. package/dist/convert-faction.d.ts.map +1 -0
  43. package/dist/convert-faction.js +1 -0
  44. package/dist/convert-faction.js.map +1 -0
  45. package/dist/converters/configs/adepta-sororitas.d.ts +1 -0
  46. package/dist/converters/configs/adepta-sororitas.d.ts.map +1 -0
  47. package/dist/converters/configs/adepta-sororitas.js +1 -0
  48. package/dist/converters/configs/adepta-sororitas.js.map +1 -0
  49. package/dist/converters/configs/adeptus-astartes.d.ts +1 -0
  50. package/dist/converters/configs/adeptus-astartes.d.ts.map +1 -0
  51. package/dist/converters/configs/adeptus-astartes.js +1 -0
  52. package/dist/converters/configs/adeptus-astartes.js.map +1 -0
  53. package/dist/converters/configs/adeptus-custodes.d.ts +1 -0
  54. package/dist/converters/configs/adeptus-custodes.d.ts.map +1 -0
  55. package/dist/converters/configs/adeptus-custodes.js +1 -0
  56. package/dist/converters/configs/adeptus-custodes.js.map +1 -0
  57. package/dist/converters/configs/adeptus-mechanicus.d.ts +1 -0
  58. package/dist/converters/configs/adeptus-mechanicus.d.ts.map +1 -0
  59. package/dist/converters/configs/adeptus-mechanicus.js +1 -0
  60. package/dist/converters/configs/adeptus-mechanicus.js.map +1 -0
  61. package/dist/converters/configs/aeldari.d.ts +1 -0
  62. package/dist/converters/configs/aeldari.d.ts.map +1 -0
  63. package/dist/converters/configs/aeldari.js +1 -0
  64. package/dist/converters/configs/aeldari.js.map +1 -0
  65. package/dist/converters/configs/agents-of-the-imperium.d.ts +1 -0
  66. package/dist/converters/configs/agents-of-the-imperium.d.ts.map +1 -0
  67. package/dist/converters/configs/agents-of-the-imperium.js +1 -0
  68. package/dist/converters/configs/agents-of-the-imperium.js.map +1 -0
  69. package/dist/converters/configs/astra-militarum.d.ts +1 -0
  70. package/dist/converters/configs/astra-militarum.d.ts.map +1 -0
  71. package/dist/converters/configs/astra-militarum.js +1 -0
  72. package/dist/converters/configs/astra-militarum.js.map +1 -0
  73. package/dist/converters/configs/black-templars.d.ts +1 -0
  74. package/dist/converters/configs/black-templars.d.ts.map +1 -0
  75. package/dist/converters/configs/black-templars.js +1 -0
  76. package/dist/converters/configs/black-templars.js.map +1 -0
  77. package/dist/converters/configs/blood-angels.d.ts +1 -0
  78. package/dist/converters/configs/blood-angels.d.ts.map +1 -0
  79. package/dist/converters/configs/blood-angels.js +1 -0
  80. package/dist/converters/configs/blood-angels.js.map +1 -0
  81. package/dist/converters/configs/chaos-daemons.d.ts +1 -0
  82. package/dist/converters/configs/chaos-daemons.d.ts.map +1 -0
  83. package/dist/converters/configs/chaos-daemons.js +1 -0
  84. package/dist/converters/configs/chaos-daemons.js.map +1 -0
  85. package/dist/converters/configs/chaos-knights.d.ts +1 -0
  86. package/dist/converters/configs/chaos-knights.d.ts.map +1 -0
  87. package/dist/converters/configs/chaos-knights.js +1 -0
  88. package/dist/converters/configs/chaos-knights.js.map +1 -0
  89. package/dist/converters/configs/chaos-space-marines.d.ts +1 -0
  90. package/dist/converters/configs/chaos-space-marines.d.ts.map +1 -0
  91. package/dist/converters/configs/chaos-space-marines.js +1 -0
  92. package/dist/converters/configs/chaos-space-marines.js.map +1 -0
  93. package/dist/converters/configs/crimson-fists.d.ts +1 -0
  94. package/dist/converters/configs/crimson-fists.d.ts.map +1 -0
  95. package/dist/converters/configs/crimson-fists.js +1 -0
  96. package/dist/converters/configs/crimson-fists.js.map +1 -0
  97. package/dist/converters/configs/dark-angels.d.ts +1 -0
  98. package/dist/converters/configs/dark-angels.d.ts.map +1 -0
  99. package/dist/converters/configs/dark-angels.js +1 -0
  100. package/dist/converters/configs/dark-angels.js.map +1 -0
  101. package/dist/converters/configs/death-guard.d.ts +1 -0
  102. package/dist/converters/configs/death-guard.d.ts.map +1 -0
  103. package/dist/converters/configs/death-guard.js +1 -0
  104. package/dist/converters/configs/death-guard.js.map +1 -0
  105. package/dist/converters/configs/deathwatch.d.ts +1 -0
  106. package/dist/converters/configs/deathwatch.d.ts.map +1 -0
  107. package/dist/converters/configs/deathwatch.js +1 -0
  108. package/dist/converters/configs/deathwatch.js.map +1 -0
  109. package/dist/converters/configs/drukhari.d.ts +1 -0
  110. package/dist/converters/configs/drukhari.d.ts.map +1 -0
  111. package/dist/converters/configs/drukhari.js +1 -0
  112. package/dist/converters/configs/drukhari.js.map +1 -0
  113. package/dist/converters/configs/emperors-children.d.ts +1 -0
  114. package/dist/converters/configs/emperors-children.d.ts.map +1 -0
  115. package/dist/converters/configs/emperors-children.js +1 -0
  116. package/dist/converters/configs/emperors-children.js.map +1 -0
  117. package/dist/converters/configs/genestealer-cults.d.ts +1 -0
  118. package/dist/converters/configs/genestealer-cults.d.ts.map +1 -0
  119. package/dist/converters/configs/genestealer-cults.js +1 -0
  120. package/dist/converters/configs/genestealer-cults.js.map +1 -0
  121. package/dist/converters/configs/grey-knights.d.ts +1 -0
  122. package/dist/converters/configs/grey-knights.d.ts.map +1 -0
  123. package/dist/converters/configs/grey-knights.js +1 -0
  124. package/dist/converters/configs/grey-knights.js.map +1 -0
  125. package/dist/converters/configs/imperial-fists.d.ts +1 -0
  126. package/dist/converters/configs/imperial-fists.d.ts.map +1 -0
  127. package/dist/converters/configs/imperial-fists.js +1 -0
  128. package/dist/converters/configs/imperial-fists.js.map +1 -0
  129. package/dist/converters/configs/imperial-knights.d.ts +1 -0
  130. package/dist/converters/configs/imperial-knights.d.ts.map +1 -0
  131. package/dist/converters/configs/imperial-knights.js +1 -0
  132. package/dist/converters/configs/imperial-knights.js.map +1 -0
  133. package/dist/converters/configs/iron-hands.d.ts +1 -0
  134. package/dist/converters/configs/iron-hands.d.ts.map +1 -0
  135. package/dist/converters/configs/iron-hands.js +1 -0
  136. package/dist/converters/configs/iron-hands.js.map +1 -0
  137. package/dist/converters/configs/leagues-of-votann.d.ts +1 -0
  138. package/dist/converters/configs/leagues-of-votann.d.ts.map +1 -0
  139. package/dist/converters/configs/leagues-of-votann.js +1 -0
  140. package/dist/converters/configs/leagues-of-votann.js.map +1 -0
  141. package/dist/converters/configs/necrons.d.ts +1 -0
  142. package/dist/converters/configs/necrons.d.ts.map +1 -0
  143. package/dist/converters/configs/necrons.js +1 -0
  144. package/dist/converters/configs/necrons.js.map +1 -0
  145. package/dist/converters/configs/orks.d.ts +1 -0
  146. package/dist/converters/configs/orks.d.ts.map +1 -0
  147. package/dist/converters/configs/orks.js +1 -0
  148. package/dist/converters/configs/orks.js.map +1 -0
  149. package/dist/converters/configs/raven-guard.d.ts +1 -0
  150. package/dist/converters/configs/raven-guard.d.ts.map +1 -0
  151. package/dist/converters/configs/raven-guard.js +1 -0
  152. package/dist/converters/configs/raven-guard.js.map +1 -0
  153. package/dist/converters/configs/salamanders.d.ts +1 -0
  154. package/dist/converters/configs/salamanders.d.ts.map +1 -0
  155. package/dist/converters/configs/salamanders.js +1 -0
  156. package/dist/converters/configs/salamanders.js.map +1 -0
  157. package/dist/converters/configs/space-wolves.d.ts +1 -0
  158. package/dist/converters/configs/space-wolves.d.ts.map +1 -0
  159. package/dist/converters/configs/space-wolves.js +1 -0
  160. package/dist/converters/configs/space-wolves.js.map +1 -0
  161. package/dist/converters/configs/tau-empire.d.ts +1 -0
  162. package/dist/converters/configs/tau-empire.d.ts.map +1 -0
  163. package/dist/converters/configs/tau-empire.js +1 -0
  164. package/dist/converters/configs/tau-empire.js.map +1 -0
  165. package/dist/converters/configs/thousand-sons.d.ts +1 -0
  166. package/dist/converters/configs/thousand-sons.d.ts.map +1 -0
  167. package/dist/converters/configs/thousand-sons.js +1 -0
  168. package/dist/converters/configs/thousand-sons.js.map +1 -0
  169. package/dist/converters/configs/tyranids.d.ts +1 -0
  170. package/dist/converters/configs/tyranids.d.ts.map +1 -0
  171. package/dist/converters/configs/tyranids.js +1 -0
  172. package/dist/converters/configs/tyranids.js.map +1 -0
  173. package/dist/converters/configs/ultramarines.d.ts +1 -0
  174. package/dist/converters/configs/ultramarines.d.ts.map +1 -0
  175. package/dist/converters/configs/ultramarines.js +1 -0
  176. package/dist/converters/configs/ultramarines.js.map +1 -0
  177. package/dist/converters/configs/white-scars.d.ts +1 -0
  178. package/dist/converters/configs/white-scars.d.ts.map +1 -0
  179. package/dist/converters/configs/white-scars.js +1 -0
  180. package/dist/converters/configs/white-scars.js.map +1 -0
  181. package/dist/converters/configs/world-eaters.d.ts +1 -0
  182. package/dist/converters/configs/world-eaters.d.ts.map +1 -0
  183. package/dist/converters/configs/world-eaters.js +1 -0
  184. package/dist/converters/configs/world-eaters.js.map +1 -0
  185. package/dist/converters/faction-config.d.ts +1 -0
  186. package/dist/converters/faction-config.d.ts.map +1 -0
  187. package/dist/converters/faction-config.js +1 -0
  188. package/dist/converters/faction-config.js.map +1 -0
  189. package/dist/converters/id-generator.d.ts +1 -0
  190. package/dist/converters/id-generator.d.ts.map +1 -0
  191. package/dist/converters/id-generator.js +1 -0
  192. package/dist/converters/id-generator.js.map +1 -0
  193. package/dist/converters/keyword-filter.d.ts +1 -0
  194. package/dist/converters/keyword-filter.d.ts.map +1 -0
  195. package/dist/converters/keyword-filter.js +1 -0
  196. package/dist/converters/keyword-filter.js.map +1 -0
  197. package/dist/converters/stat-parser.d.ts +1 -0
  198. package/dist/converters/stat-parser.d.ts.map +1 -0
  199. package/dist/converters/stat-parser.js +1 -0
  200. package/dist/converters/stat-parser.js.map +1 -0
  201. package/dist/converters/view-selector.d.ts +1 -0
  202. package/dist/converters/view-selector.d.ts.map +1 -0
  203. package/dist/converters/view-selector.js +1 -0
  204. package/dist/converters/view-selector.js.map +1 -0
  205. package/dist/converters/weapon-dedup.d.ts +1 -0
  206. package/dist/converters/weapon-dedup.d.ts.map +1 -0
  207. package/dist/converters/weapon-dedup.js +1 -0
  208. package/dist/converters/weapon-dedup.js.map +1 -0
  209. package/dist/cruncher/buffs.d.ts +184 -0
  210. package/dist/cruncher/buffs.d.ts.map +1 -0
  211. package/dist/cruncher/buffs.js +150 -0
  212. package/dist/cruncher/buffs.js.map +1 -0
  213. package/dist/cruncher/engine.d.ts +50 -0
  214. package/dist/cruncher/engine.d.ts.map +1 -0
  215. package/dist/cruncher/engine.js +312 -0
  216. package/dist/cruncher/engine.js.map +1 -0
  217. package/dist/cruncher/from-dsl.d.ts +69 -0
  218. package/dist/cruncher/from-dsl.d.ts.map +1 -0
  219. package/dist/cruncher/from-dsl.js +523 -0
  220. package/dist/cruncher/from-dsl.js.map +1 -0
  221. package/dist/cruncher/from-keyword.d.ts +35 -0
  222. package/dist/cruncher/from-keyword.d.ts.map +1 -0
  223. package/dist/cruncher/from-keyword.js +159 -0
  224. package/dist/cruncher/from-keyword.js.map +1 -0
  225. package/dist/cruncher/get-buffs.d.ts +12 -0
  226. package/dist/cruncher/get-buffs.d.ts.map +1 -0
  227. package/dist/cruncher/get-buffs.js +7 -0
  228. package/dist/cruncher/get-buffs.js.map +1 -0
  229. package/dist/cruncher/index.d.ts +11 -0
  230. package/dist/cruncher/index.d.ts.map +1 -0
  231. package/dist/cruncher/index.js +11 -0
  232. package/dist/cruncher/index.js.map +1 -0
  233. package/dist/data/bundle.generated.d.ts +1 -0
  234. package/dist/data/bundle.generated.d.ts.map +1 -0
  235. package/dist/data/bundle.generated.js +2 -1
  236. package/dist/data/bundle.generated.js.map +1 -0
  237. package/dist/data/collection.d.ts +1 -0
  238. package/dist/data/collection.d.ts.map +1 -0
  239. package/dist/data/collection.js +1 -0
  240. package/dist/data/collection.js.map +1 -0
  241. package/dist/data/dataset.d.ts +54 -2
  242. package/dist/data/dataset.d.ts.map +1 -0
  243. package/dist/data/dataset.js +111 -1
  244. package/dist/data/dataset.js.map +1 -0
  245. package/dist/data/entities.d.ts +70 -2
  246. package/dist/data/entities.d.ts.map +1 -0
  247. package/dist/data/entities.js +122 -0
  248. package/dist/data/entities.js.map +1 -0
  249. package/dist/data/index.d.ts +9 -1
  250. package/dist/data/index.d.ts.map +1 -0
  251. package/dist/data/index.js +14 -1
  252. package/dist/data/index.js.map +1 -0
  253. package/dist/data/normalize.d.ts +1 -0
  254. package/dist/data/normalize.d.ts.map +1 -0
  255. package/dist/data/normalize.js +1 -0
  256. package/dist/data/normalize.js.map +1 -0
  257. package/dist/data/roster-resolve.d.ts +33 -0
  258. package/dist/data/roster-resolve.d.ts.map +1 -0
  259. package/dist/data/roster-resolve.js +36 -0
  260. package/dist/data/roster-resolve.js.map +1 -0
  261. package/dist/data/types.d.ts +4 -1
  262. package/dist/data/types.d.ts.map +1 -0
  263. package/dist/data/types.js +2 -0
  264. package/dist/data/types.js.map +1 -0
  265. package/dist/export/helpers.d.ts +33 -0
  266. package/dist/export/helpers.d.ts.map +1 -0
  267. package/dist/export/helpers.js +57 -0
  268. package/dist/export/helpers.js.map +1 -0
  269. package/dist/export/index.d.ts +21 -0
  270. package/dist/export/index.d.ts.map +1 -0
  271. package/dist/export/index.js +25 -0
  272. package/dist/export/index.js.map +1 -0
  273. package/dist/export/newrecruit-json.d.ts +3 -0
  274. package/dist/export/newrecruit-json.d.ts.map +1 -0
  275. package/dist/export/newrecruit-json.js +140 -0
  276. package/dist/export/newrecruit-json.js.map +1 -0
  277. package/dist/export/newrecruit-simple.d.ts +3 -0
  278. package/dist/export/newrecruit-simple.d.ts.map +1 -0
  279. package/dist/export/newrecruit-simple.js +76 -0
  280. package/dist/export/newrecruit-simple.js.map +1 -0
  281. package/dist/export/newrecruit-wtc.d.ts +4 -0
  282. package/dist/export/newrecruit-wtc.d.ts.map +1 -0
  283. package/dist/export/newrecruit-wtc.js +142 -0
  284. package/dist/export/newrecruit-wtc.js.map +1 -0
  285. package/dist/export/roster-json.d.ts +3 -0
  286. package/dist/export/roster-json.d.ts.map +1 -0
  287. package/dist/export/roster-json.js +8 -0
  288. package/dist/export/roster-json.js.map +1 -0
  289. package/dist/export/serializer.d.ts +27 -0
  290. package/dist/export/serializer.d.ts.map +1 -0
  291. package/dist/export/serializer.js +2 -0
  292. package/dist/export/serializer.js.map +1 -0
  293. package/dist/gen-conformance.d.ts +1 -0
  294. package/dist/gen-conformance.d.ts.map +1 -0
  295. package/dist/gen-conformance.js +73 -12
  296. package/dist/gen-conformance.js.map +1 -0
  297. package/dist/generated.d.ts +194 -118
  298. package/dist/generated.d.ts.map +1 -0
  299. package/dist/generated.js +1 -0
  300. package/dist/generated.js.map +1 -0
  301. package/dist/import/adapter.d.ts +4 -3
  302. package/dist/import/adapter.d.ts.map +1 -0
  303. package/dist/import/adapter.js +1 -0
  304. package/dist/import/adapter.js.map +1 -0
  305. package/dist/import/decode.d.ts +1 -0
  306. package/dist/import/decode.d.ts.map +1 -0
  307. package/dist/import/decode.js +1 -0
  308. package/dist/import/decode.js.map +1 -0
  309. package/dist/import/import-roster.d.ts +35 -0
  310. package/dist/import/import-roster.d.ts.map +1 -0
  311. package/dist/import/import-roster.js +97 -0
  312. package/dist/import/import-roster.js.map +1 -0
  313. package/dist/import/index.d.ts +7 -3
  314. package/dist/import/index.d.ts.map +1 -0
  315. package/dist/import/index.js +5 -1
  316. package/dist/import/index.js.map +1 -0
  317. package/dist/import/listforge.d.ts +1 -0
  318. package/dist/import/listforge.d.ts.map +1 -0
  319. package/dist/import/listforge.js +7 -1
  320. package/dist/import/listforge.js.map +1 -0
  321. package/dist/import/newrecruit-json.d.ts +31 -0
  322. package/dist/import/newrecruit-json.d.ts.map +1 -0
  323. package/dist/import/newrecruit-json.js +224 -0
  324. package/dist/import/newrecruit-json.js.map +1 -0
  325. package/dist/import/newrecruit-simple.d.ts +29 -0
  326. package/dist/import/newrecruit-simple.d.ts.map +1 -0
  327. package/dist/import/newrecruit-simple.js +200 -0
  328. package/dist/import/newrecruit-simple.js.map +1 -0
  329. package/dist/import/newrecruit-text.d.ts +48 -0
  330. package/dist/import/newrecruit-text.d.ts.map +1 -0
  331. package/dist/import/newrecruit-text.js +96 -0
  332. package/dist/import/newrecruit-text.js.map +1 -0
  333. package/dist/import/newrecruit-wtc.d.ts +36 -0
  334. package/dist/import/newrecruit-wtc.d.ts.map +1 -0
  335. package/dist/import/newrecruit-wtc.js +334 -0
  336. package/dist/import/newrecruit-wtc.js.map +1 -0
  337. package/dist/import/resolve.d.ts +3 -2
  338. package/dist/import/resolve.d.ts.map +1 -0
  339. package/dist/import/resolve.js +5 -2
  340. package/dist/import/resolve.js.map +1 -0
  341. package/dist/import/types.d.ts +11 -1
  342. package/dist/import/types.d.ts.map +1 -0
  343. package/dist/import/types.js +1 -0
  344. package/dist/import/types.js.map +1 -0
  345. package/dist/index.d.ts +5 -2
  346. package/dist/index.d.ts.map +1 -0
  347. package/dist/index.js +4 -1
  348. package/dist/index.js.map +1 -0
  349. package/dist/known-support-10e.d.ts +1 -0
  350. package/dist/known-support-10e.d.ts.map +1 -0
  351. package/dist/known-support-10e.js +1 -0
  352. package/dist/known-support-10e.js.map +1 -0
  353. package/dist/link-abilities.d.ts +41 -0
  354. package/dist/link-abilities.d.ts.map +1 -0
  355. package/dist/link-abilities.js +159 -0
  356. package/dist/link-abilities.js.map +1 -0
  357. package/dist/migrations/2026-weapon-keywords.d.ts +2 -0
  358. package/dist/migrations/2026-weapon-keywords.d.ts.map +1 -0
  359. package/dist/migrations/2026-weapon-keywords.js +243 -0
  360. package/dist/migrations/2026-weapon-keywords.js.map +1 -0
  361. package/dist/port-10e-faction.d.ts +1 -0
  362. package/dist/port-10e-faction.d.ts.map +1 -0
  363. package/dist/port-10e-faction.js +1 -0
  364. package/dist/port-10e-faction.js.map +1 -0
  365. package/dist/report.d.ts +1 -0
  366. package/dist/report.d.ts.map +1 -0
  367. package/dist/report.js +1 -0
  368. package/dist/report.js.map +1 -0
  369. package/dist/rube-goldberg.d.ts +3 -0
  370. package/dist/rube-goldberg.d.ts.map +1 -0
  371. package/dist/rube-goldberg.js +109 -0
  372. package/dist/rube-goldberg.js.map +1 -0
  373. package/dist/schema-loader.d.ts +1 -0
  374. package/dist/schema-loader.d.ts.map +1 -0
  375. package/dist/schema-loader.js +1 -0
  376. package/dist/schema-loader.js.map +1 -0
  377. package/dist/validate.d.ts +1 -0
  378. package/dist/validate.d.ts.map +1 -0
  379. package/dist/validate.js +2 -0
  380. package/dist/validate.js.map +1 -0
  381. package/package.json +7 -2
  382. package/schemas/core/roster.schema.json +17 -4
  383. package/schemas/core/weapon-keyword.schema.json +31 -0
  384. package/schemas/core/weapon.schema.json +22 -1
  385. package/schemas/enrichment/ability-dsl/effect.schema.json +23 -1
  386. package/dist/import/import-listforge.d.ts +0 -23
  387. package/dist/import/import-listforge.js +0 -32
@@ -0,0 +1,523 @@
1
+ /** Targets that resolve to the buffed unit itself. */
2
+ const SELF_TARGETS = new Set([
3
+ "self",
4
+ "bearer",
5
+ "unit",
6
+ "attached-unit",
7
+ "friendly-within-aura",
8
+ "all-friendly",
9
+ ]);
10
+ /** Aliases the DSL uses when a node specifically calls out "the attacker". */
11
+ const ATTACKER_TARGET = "attacker";
12
+ /** Aliases the DSL uses when a node specifically calls out "the defender". */
13
+ const DEFENDER_TARGETS = new Set(["defender", "enemy-within-aura", "all-enemy"]);
14
+ /**
15
+ * Walk an ability DSL `effect` tree and produce the buff stack it contributes
16
+ * against `context` from the given `perspective`, plus an `unsupported` list
17
+ * naming any branches the buff layer can't express today.
18
+ */
19
+ export function effectToBuffs(effect, source, context, perspective = "attacker") {
20
+ const out = { applied: [], unsupported: [] };
21
+ walk(effect, source, { context, perspective }, out);
22
+ return out;
23
+ }
24
+ function walk(node, source, opts, out) {
25
+ if (!isObject(node))
26
+ return;
27
+ const type = node.type;
28
+ switch (type) {
29
+ case "re-roll":
30
+ translateReroll(node, source, opts, out);
31
+ return;
32
+ case "roll-modifier":
33
+ translateRollModifier(node, source, opts, out);
34
+ return;
35
+ case "stat-modifier":
36
+ translateStatModifier(node, source, opts, out);
37
+ return;
38
+ case "feel-no-pain":
39
+ translateFeelNoPain(node, source, opts, out);
40
+ return;
41
+ case "keyword-grant":
42
+ translateKeywordGrant(node, source, opts, out);
43
+ return;
44
+ case "bs-modifier":
45
+ translateBsModifier(node, source, opts, out);
46
+ return;
47
+ case "conditional":
48
+ translateConditional(node, source, opts, out);
49
+ return;
50
+ case "sequence":
51
+ for (const step of node.steps ?? [])
52
+ walk(step, source, opts, out);
53
+ return;
54
+ case "choice":
55
+ // Player decision — auto-applying every branch would double-count.
56
+ out.unsupported.push({
57
+ reason: "choice: player picks one option; the buff layer can't choose",
58
+ effectFragment: node,
59
+ });
60
+ return;
61
+ case "dice-gated":
62
+ // Probabilistic; the buff layer is deterministic.
63
+ out.unsupported.push({
64
+ reason: "dice-gated effect: stochastic; not expressible as a buff",
65
+ effectFragment: node,
66
+ });
67
+ return;
68
+ case "dice-pool-allocation":
69
+ out.unsupported.push({
70
+ reason: "dice-pool-allocation: player allocates dice at runtime",
71
+ effectFragment: node,
72
+ });
73
+ return;
74
+ default:
75
+ // Unknown effect — record it. Covers ability-grant, deep-strike,
76
+ // mortal-wounds, cp-gain, movement-modifier, etc.; the buff layer
77
+ // doesn't model these as deterministic mods to a single shot.
78
+ out.unsupported.push({
79
+ reason: `effect type "${String(type)}" is not modelled by the buff layer`,
80
+ effectFragment: node,
81
+ });
82
+ return;
83
+ }
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // Leaf translators
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * Classify a node's `target` field against the perspective we're translating
90
+ * for. Returns:
91
+ * - `"self"`: the node targets the buffed unit (apply attacker-side or
92
+ * defender-side translation, depending on perspective + stat).
93
+ * - `"attacker"` / `"defender"`: the node targets the other party explicitly.
94
+ * - `"unknown"`: missing/malformed target.
95
+ */
96
+ function classifyTarget(node) {
97
+ const target = node.target;
98
+ if (typeof target !== "string")
99
+ return "unknown";
100
+ if (target === ATTACKER_TARGET)
101
+ return "attacker";
102
+ if (DEFENDER_TARGETS.has(target))
103
+ return "defender";
104
+ if (SELF_TARGETS.has(target))
105
+ return "self";
106
+ return "unknown";
107
+ }
108
+ /**
109
+ * Does this node's target match the buffed unit under the current
110
+ * perspective? Used for symmetric roll/keyword translations where the same
111
+ * effect is "self" in either direction.
112
+ */
113
+ function appliesToBuffedUnit(node, perspective) {
114
+ const cls = classifyTarget(node);
115
+ if (cls === "self")
116
+ return true;
117
+ if (cls === "attacker")
118
+ return perspective === "attacker";
119
+ if (cls === "defender")
120
+ return perspective === "target";
121
+ return false;
122
+ }
123
+ function translateReroll(node, source, opts, out) {
124
+ // Rerolls are inherently attacker-side (you re-roll your own hit/wound/
125
+ // damage; save rerolls fire when *you* are the target). Apply only under
126
+ // the matching perspective so a target-perspective walk doesn't grab the
127
+ // attacker's reroll-failed-hits buff.
128
+ if (opts.perspective === "attacker" && !appliesToBuffedUnit(node, "attacker"))
129
+ return;
130
+ const modifier = node.modifier;
131
+ if (!isObject(modifier)) {
132
+ out.unsupported.push({ reason: "re-roll: missing modifier object", effectFragment: node });
133
+ return;
134
+ }
135
+ const roll = modifier.roll;
136
+ const subset = modifier.subset;
137
+ // Under target perspective, only "save" rerolls fire on the buffed unit.
138
+ if (opts.perspective === "target" && roll !== "save")
139
+ return;
140
+ if ((roll === "hit" || roll === "wound" || roll === "save" || roll === "damage") &&
141
+ (subset === "ones" || subset === "all-failures")) {
142
+ out.applied.push({ source, contribution: { type: "reroll", roll, subset } });
143
+ return;
144
+ }
145
+ out.unsupported.push({
146
+ reason: `re-roll on "${String(roll)}" (subset "${String(subset)}") is outside the damage path`,
147
+ effectFragment: node,
148
+ });
149
+ }
150
+ function translateRollModifier(node, source, opts, out) {
151
+ const modifier = node.modifier;
152
+ if (!isObject(modifier)) {
153
+ out.unsupported.push({
154
+ reason: "roll-modifier: missing modifier object",
155
+ effectFragment: node,
156
+ });
157
+ return;
158
+ }
159
+ const value = signedValue(modifier);
160
+ if (value === null) {
161
+ out.unsupported.push({
162
+ reason: `roll-modifier: operation "${String(modifier.operation)}" not supported`,
163
+ effectFragment: node,
164
+ });
165
+ return;
166
+ }
167
+ const roll = modifier.roll;
168
+ // Each roll type is intrinsically on one side. Hit / wound / damage are
169
+ // attacker-side; save is defender-side. The perspective decides whether the
170
+ // buffed unit's `target` is the right party for that roll type.
171
+ if (opts.perspective === "attacker") {
172
+ if (!appliesToBuffedUnit(node, "attacker"))
173
+ return;
174
+ if (roll === "save")
175
+ return; // saves apply to the defender, not the attacker.
176
+ }
177
+ else {
178
+ // target perspective: only `save` rolls on the buffed unit fire here.
179
+ if (roll !== "save")
180
+ return;
181
+ if (!appliesToBuffedUnit(node, "target"))
182
+ return;
183
+ }
184
+ switch (roll) {
185
+ case "hit":
186
+ out.applied.push({ source, contribution: { type: "hit-mod", value } });
187
+ return;
188
+ case "wound":
189
+ out.applied.push({ source, contribution: { type: "wound-mod", value } });
190
+ return;
191
+ case "save":
192
+ out.applied.push({ source, contribution: { type: "save-mod", value } });
193
+ return;
194
+ case "damage":
195
+ out.applied.push({ source, contribution: { type: "damage-mod", value } });
196
+ return;
197
+ default:
198
+ out.unsupported.push({
199
+ reason: `roll-modifier on "${String(roll)}" is outside the damage path`,
200
+ effectFragment: node,
201
+ });
202
+ }
203
+ }
204
+ function translateStatModifier(node, source, opts, out) {
205
+ const modifier = node.modifier;
206
+ if (!isObject(modifier)) {
207
+ out.unsupported.push({
208
+ reason: "stat-modifier: missing modifier object",
209
+ effectFragment: node,
210
+ });
211
+ return;
212
+ }
213
+ const value = signedValue(modifier);
214
+ if (value === null) {
215
+ out.unsupported.push({
216
+ reason: `stat-modifier: operation "${String(modifier.operation)}" not supported`,
217
+ effectFragment: node,
218
+ });
219
+ return;
220
+ }
221
+ const stat = modifier.stat;
222
+ const isOnBuffedUnit = appliesToBuffedUnit(node, opts.perspective);
223
+ switch (stat) {
224
+ case "A":
225
+ if (opts.perspective !== "attacker" || !isOnBuffedUnit)
226
+ return;
227
+ out.applied.push({ source, contribution: { type: "attacks-mod", value } });
228
+ return;
229
+ case "S":
230
+ if (opts.perspective !== "attacker" || !isOnBuffedUnit)
231
+ return;
232
+ out.applied.push({ source, contribution: { type: "strength-mod", value } });
233
+ return;
234
+ case "T":
235
+ // Defender stat. Only relevant under target perspective.
236
+ if (opts.perspective !== "target") {
237
+ out.unsupported.push({
238
+ reason: "stat-modifier T: defender-side stat; applies when the buffed unit is the target",
239
+ effectFragment: node,
240
+ });
241
+ return;
242
+ }
243
+ if (!isOnBuffedUnit)
244
+ return;
245
+ out.applied.push({ source, contribution: { type: "toughness-mod", value } });
246
+ return;
247
+ case "Sv":
248
+ // Saves improve when the *defender* gets +Sv. A +1 to Sv in printed
249
+ // rules means "improve the save by 1", which maps to a `save-mod` of
250
+ // `-value` since save-mod is signed against the *needed roll*.
251
+ // (Equivalent: a -1 Sv penalty is a +1 save-mod.) We translate
252
+ // "Sv add 1" → save-mod -1 to keep the resolver's sign convention.
253
+ if (opts.perspective !== "target") {
254
+ out.unsupported.push({
255
+ reason: "stat-modifier Sv: defender-side stat; applies when the buffed unit is the target",
256
+ effectFragment: node,
257
+ });
258
+ return;
259
+ }
260
+ if (!isOnBuffedUnit)
261
+ return;
262
+ out.applied.push({ source, contribution: { type: "save-mod", value: -value } });
263
+ return;
264
+ case "AP":
265
+ // AP rides on the attacker's weapon profile and is stored as a negative
266
+ // number in the data (e.g. AP -1). The data's `{operation:"add", value:-1}`
267
+ // form means "AP becomes one more negative" → more piercing. `signedValue`
268
+ // already returns that negative number directly, so pass it through.
269
+ if (opts.perspective !== "attacker" || !isOnBuffedUnit)
270
+ return;
271
+ out.applied.push({ source, contribution: { type: "ap-mod", value } });
272
+ return;
273
+ default:
274
+ out.unsupported.push({
275
+ reason: `stat-modifier on "${String(stat)}" is outside the damage path`,
276
+ effectFragment: node,
277
+ });
278
+ }
279
+ }
280
+ function translateFeelNoPain(node, source, opts, out) {
281
+ // FNP applies when the buffed unit is the *target* — it ablates incoming
282
+ // damage. Under attacker perspective the FNP is irrelevant (the unit is
283
+ // firing, not taking damage). Drop silently rather than as `unsupported`
284
+ // so attacker-perspective walks don't surface a spurious diagnostic for
285
+ // every unit that happens to have a FNP rule.
286
+ if (opts.perspective !== "target")
287
+ return;
288
+ const modifier = node.modifier;
289
+ if (!isObject(modifier)) {
290
+ out.unsupported.push({
291
+ reason: "feel-no-pain: missing modifier object",
292
+ effectFragment: node,
293
+ });
294
+ return;
295
+ }
296
+ const threshold = Number(modifier.threshold);
297
+ if (!Number.isFinite(threshold)) {
298
+ out.unsupported.push({
299
+ reason: "feel-no-pain: threshold not numeric",
300
+ effectFragment: node,
301
+ });
302
+ return;
303
+ }
304
+ out.applied.push({ source, contribution: { type: "feel-no-pain", threshold } });
305
+ }
306
+ function translateKeywordGrant(node, source, opts, out) {
307
+ // Weapon-keyword grants ride with the attacker's profile (e.g. "your
308
+ // weapons gain [Sustained Hits 1]"). Defender-perspective walks ignore
309
+ // them — the keyword applies when the buffed unit fires, not when it's
310
+ // shot at.
311
+ if (opts.perspective !== "attacker")
312
+ return;
313
+ if (!appliesToBuffedUnit(node, "attacker"))
314
+ return;
315
+ const modifier = node.modifier;
316
+ if (!isObject(modifier))
317
+ return;
318
+ const keywords = modifier.keywords;
319
+ if (!Array.isArray(keywords))
320
+ return;
321
+ for (const raw of keywords) {
322
+ if (typeof raw !== "string")
323
+ continue;
324
+ const ref = parseKeywordGrant(raw);
325
+ if (!ref) {
326
+ out.unsupported.push({
327
+ reason: `keyword-grant: cannot parse "${raw}" to a catalog keyword`,
328
+ effectFragment: { keyword: raw },
329
+ });
330
+ continue;
331
+ }
332
+ out.applied.push({ source, contribution: { type: "extra-keyword", keywordRef: ref } });
333
+ }
334
+ }
335
+ function translateBsModifier(node, source, opts, out) {
336
+ // A bs-modifier on `target: "attacker"` is a defender-side rule: it
337
+ // penalises *incoming* hit rolls (e.g. Benefit of Cover). Translate it
338
+ // as a `hit-mod` buff under target perspective so the resolver's ±1 cap
339
+ // composes with attacker-side mods.
340
+ if (opts.perspective !== "target")
341
+ return;
342
+ const cls = classifyTarget(node);
343
+ if (cls !== "attacker")
344
+ return; // a bs-modifier on self wouldn't make sense.
345
+ const modifier = node.modifier;
346
+ if (!isObject(modifier))
347
+ return;
348
+ const value = signedValue(modifier);
349
+ if (value === null)
350
+ return;
351
+ out.applied.push({ source, contribution: { type: "hit-mod", value } });
352
+ }
353
+ function translateConditional(node, source, opts, out) {
354
+ const condition = node.condition;
355
+ const effect = node.effect;
356
+ if (!isObject(condition))
357
+ return;
358
+ const negated = condition.negated === true;
359
+ const verdict = evaluateCondition(condition, opts.context);
360
+ if (verdict === "unknown") {
361
+ out.unsupported.push({
362
+ reason: `conditional: cannot evaluate condition "${String(condition.type)}" against current context`,
363
+ effectFragment: node,
364
+ });
365
+ return;
366
+ }
367
+ const active = negated ? !verdict : verdict;
368
+ if (!active)
369
+ return;
370
+ walk(effect, source, opts, out);
371
+ }
372
+ // ---------------------------------------------------------------------------
373
+ // Condition evaluator
374
+ // ---------------------------------------------------------------------------
375
+ function evaluateCondition(condition, ctx) {
376
+ // Compound conditions use {operator, operands} rather than {type, parameters}.
377
+ // The schema's `condition-node` oneOf doesn't guarantee discrimination by a
378
+ // single field, so dispatch on shape: presence of `operator` + `operands`
379
+ // wins over the simple-condition switch below.
380
+ if (typeof condition.operator === "string" &&
381
+ Array.isArray(condition.operands)) {
382
+ return evaluateCompound(condition.operator, condition.operands, ctx);
383
+ }
384
+ switch (condition.type) {
385
+ case "phase-is": {
386
+ const wanted = condition.parameters?.phase;
387
+ if (typeof wanted !== "string")
388
+ return "unknown";
389
+ return ctx.phase === wanted;
390
+ }
391
+ case "timing-is": {
392
+ const wanted = condition.parameters?.timing;
393
+ if (typeof wanted !== "string")
394
+ return "unknown";
395
+ if (ctx.timing === undefined)
396
+ return "unknown";
397
+ return ctx.timing === wanted;
398
+ }
399
+ case "remained-stationary":
400
+ return ctx.attackerStationary === true;
401
+ case "target-has-keyword": {
402
+ const kw = condition.parameters?.keyword;
403
+ if (typeof kw !== "string")
404
+ return "unknown";
405
+ return (ctx.targetKeywords ?? []).includes(kw.toLowerCase());
406
+ }
407
+ case "unit-has-keyword": {
408
+ const kw = condition.parameters?.keyword;
409
+ if (typeof kw !== "string")
410
+ return "unknown";
411
+ return (ctx.attackerKeywords ?? []).includes(kw.toLowerCase());
412
+ }
413
+ case "is-attached":
414
+ // The resolver knows whether a leader is attached; absent that signal
415
+ // here, treat as unknown so the SPA can surface the gap.
416
+ return "unknown";
417
+ default:
418
+ return "unknown";
419
+ }
420
+ }
421
+ /**
422
+ * Kleene three-valued evaluator for compound conditions. `and` short-circuits
423
+ * to `false` as soon as any operand is false (an unknown operand is then
424
+ * irrelevant); `or` short-circuits to `true` symmetrically. `not` flips its
425
+ * single operand and leaves `"unknown"` as `"unknown"`. Unknown operands that
426
+ * don't get short-circuited propagate as `"unknown"` so the SPA can surface
427
+ * the gap rather than collapsing it into a misleading false.
428
+ */
429
+ function evaluateCompound(operator, operands, ctx) {
430
+ if (operator === "not") {
431
+ const first = operands[0];
432
+ if (!isObject(first))
433
+ return "unknown";
434
+ const v = evaluateCondition(first, ctx);
435
+ if (v === "unknown")
436
+ return "unknown";
437
+ return !v;
438
+ }
439
+ if (operator !== "and" && operator !== "or")
440
+ return "unknown";
441
+ let sawUnknown = false;
442
+ for (const operand of operands) {
443
+ if (!isObject(operand)) {
444
+ sawUnknown = true;
445
+ continue;
446
+ }
447
+ const v = evaluateCondition(operand, ctx);
448
+ if (v === "unknown") {
449
+ sawUnknown = true;
450
+ continue;
451
+ }
452
+ if (operator === "and" && v === false)
453
+ return false;
454
+ if (operator === "or" && v === true)
455
+ return true;
456
+ }
457
+ if (sawUnknown)
458
+ return "unknown";
459
+ return operator === "and"; // all true for AND, all false for OR
460
+ }
461
+ // ---------------------------------------------------------------------------
462
+ // Helpers
463
+ // ---------------------------------------------------------------------------
464
+ /**
465
+ * Read a signed numeric value out of a modifier `{operation, value}` pair.
466
+ * "add"/"subtract" become the matching sign; "set" / "multiply" / etc. return
467
+ * `null` (translator surfaces them as unsupported).
468
+ */
469
+ function signedValue(modifier) {
470
+ const value = Number(modifier.value);
471
+ if (!Number.isFinite(value))
472
+ return null;
473
+ switch (modifier.operation) {
474
+ case "add":
475
+ return value;
476
+ case "subtract":
477
+ return -value;
478
+ default:
479
+ return null;
480
+ }
481
+ }
482
+ /**
483
+ * Parse a printed weapon-keyword string (e.g. `"Sustained Hits 1"`,
484
+ * `"Anti-INFANTRY 4+"`, `"Lethal Hits"`) into a `{keyword_id, parameters?}`
485
+ * catalog reference, or `null` if the form is unrecognised.
486
+ *
487
+ * Reverses the conventions baked into the M0 catalog: kebab-case ids,
488
+ * trailing number → `value`, embedded keyword + threshold → `target_keyword`
489
+ * + `threshold`.
490
+ */
491
+ export function parseKeywordGrant(raw) {
492
+ const trimmed = raw.trim();
493
+ if (trimmed === "")
494
+ return null;
495
+ // Anti-X N+ → { anti, target_keyword: X, threshold: N }
496
+ const antiMatch = /^anti-([A-Z][A-Z\s-]*)\s+(\d+)\+?$/i.exec(trimmed);
497
+ if (antiMatch) {
498
+ return {
499
+ keyword_id: "anti",
500
+ parameters: { target_keyword: antiMatch[1].trim(), threshold: Number(antiMatch[2]) },
501
+ };
502
+ }
503
+ // "Lethal Hits", "Twin-linked", "Heavy" → kebab-case lookup, no params.
504
+ // "Sustained Hits 1", "Rapid Fire 2", "Melta 2" → kebab-case + value.
505
+ const valueMatch = /^(.+?)\s+(\d+)$/.exec(trimmed);
506
+ if (valueMatch) {
507
+ return {
508
+ keyword_id: toKebabCase(valueMatch[1]),
509
+ parameters: { value: Number(valueMatch[2]) },
510
+ };
511
+ }
512
+ return { keyword_id: toKebabCase(trimmed) };
513
+ }
514
+ function toKebabCase(s) {
515
+ return s
516
+ .toLowerCase()
517
+ .replace(/[\s_]+/g, "-")
518
+ .replace(/[^a-z0-9-]/g, "");
519
+ }
520
+ function isObject(value) {
521
+ return typeof value === "object" && value !== null && !Array.isArray(value);
522
+ }
523
+ //# sourceMappingURL=from-dsl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"from-dsl.js","sourceRoot":"","sources":["../../src/cruncher/from-dsl.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Translate an Ability DSL `effect` tree into the {@link Buff} stack it\n * contributes (for an attacker-perspective crunch) along with a list of\n * effect fragments the translator could not auto-apply.\n *\n * The buff layer is intentionally a subset of the DSL: it covers the math the\n * cruncher's expected-value engine reads (rerolls, die-roll modifiers, S/A/T\n * stat shifts, FNP, granted weapon keywords, cover) and reports everything\n * else — choice nodes (player decisions), dice-gated effects (stochastic),\n * defender-side bs-modifier, attack-restrictions, ability grants, mortal\n * wound triggers — as `unsupported` so the SPA can surface \"this ability has\n * effects we can't auto-apply\" rather than silently dropping them.\n *\n * The walker classifies an effect's `target` against the attacker\n * perspective: `self`, `bearer`, `unit`, `attached-unit`, `attacker`, and\n * `friendly-within-aura` are all treated as \"applies to my unit\". `defender`,\n * `enemy-within-aura`, and `all-enemy` are dropped without being marked\n * unsupported — those are defender-side mods and would surface from the\n * target's perspective (M3 work), not the attacker's.\n *\n * @packageDocumentation\n */\nimport type { Buff, BuffSource, EngineContext, WeaponKeywordRef } from \"./buffs.js\";\n\n/** A fragment we couldn't translate. The SPA can render these as warnings. */\nexport type UnsupportedFragment = {\n reason: string;\n effectFragment: unknown;\n};\n\nexport type EffectTranslation = {\n applied: Buff[];\n unsupported: UnsupportedFragment[];\n};\n\n/**\n * Whose perspective the translation runs from.\n *\n * - `\"attacker\"`: the buffed unit is *firing*. `target: \"unit\"/\"self\"` etc.\n * become attacker-side mods (re-rolls, hit/wound mods, A/S shifts, granted\n * keywords). `target: \"defender\"` is silently dropped — that's incoming\n * penalty math relevant when the buffed unit is the *target*, surfaced via\n * the `\"target\"` perspective instead.\n *\n * - `\"target\"`: the buffed unit is *being shot at*. Defensive mods on the\n * buffed unit (`stat-modifier T`, `stat-modifier Sv`, `feel-no-pain`,\n * `roll-modifier save`) become defender-side buffs. Conversely, attacker-\n * only mods (re-rolls, hit/wound mods, A/S shifts) drop silently because\n * they describe what the buffed unit does when *attacking*.\n *\n * The bs-modifier effect (a -1 to incoming hit rolls, e.g. Benefit of Cover)\n * becomes a `hit-mod` buff under target perspective so it stacks correctly\n * with attacker-side modifiers in the resolver's ±1 cap.\n */\nexport type TranslationPerspective = \"attacker\" | \"target\";\n\n/** Targets that resolve to the buffed unit itself. */\nconst SELF_TARGETS = new Set([\n \"self\",\n \"bearer\",\n \"unit\",\n \"attached-unit\",\n \"friendly-within-aura\",\n \"all-friendly\",\n]);\n\n/** Aliases the DSL uses when a node specifically calls out \"the attacker\". */\nconst ATTACKER_TARGET = \"attacker\";\n/** Aliases the DSL uses when a node specifically calls out \"the defender\". */\nconst DEFENDER_TARGETS = new Set([\"defender\", \"enemy-within-aura\", \"all-enemy\"]);\n\n/**\n * Walk an ability DSL `effect` tree and produce the buff stack it contributes\n * against `context` from the given `perspective`, plus an `unsupported` list\n * naming any branches the buff layer can't express today.\n */\nexport function effectToBuffs(\n effect: unknown,\n source: BuffSource,\n context: EngineContext,\n perspective: TranslationPerspective = \"attacker\",\n): EffectTranslation {\n const out: EffectTranslation = { applied: [], unsupported: [] };\n walk(effect, source, { context, perspective }, out);\n return out;\n}\n\ntype WalkOpts = { context: EngineContext; perspective: TranslationPerspective };\n\nfunction walk(\n node: unknown,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n if (!isObject(node)) return;\n const type = node.type;\n switch (type) {\n case \"re-roll\":\n translateReroll(node, source, opts, out);\n return;\n case \"roll-modifier\":\n translateRollModifier(node, source, opts, out);\n return;\n case \"stat-modifier\":\n translateStatModifier(node, source, opts, out);\n return;\n case \"feel-no-pain\":\n translateFeelNoPain(node, source, opts, out);\n return;\n case \"keyword-grant\":\n translateKeywordGrant(node, source, opts, out);\n return;\n case \"bs-modifier\":\n translateBsModifier(node, source, opts, out);\n return;\n case \"conditional\":\n translateConditional(node, source, opts, out);\n return;\n case \"sequence\":\n for (const step of (node.steps as unknown[]) ?? []) walk(step, source, opts, out);\n return;\n case \"choice\":\n // Player decision — auto-applying every branch would double-count.\n out.unsupported.push({\n reason: \"choice: player picks one option; the buff layer can't choose\",\n effectFragment: node,\n });\n return;\n case \"dice-gated\":\n // Probabilistic; the buff layer is deterministic.\n out.unsupported.push({\n reason: \"dice-gated effect: stochastic; not expressible as a buff\",\n effectFragment: node,\n });\n return;\n case \"dice-pool-allocation\":\n out.unsupported.push({\n reason: \"dice-pool-allocation: player allocates dice at runtime\",\n effectFragment: node,\n });\n return;\n default:\n // Unknown effect — record it. Covers ability-grant, deep-strike,\n // mortal-wounds, cp-gain, movement-modifier, etc.; the buff layer\n // doesn't model these as deterministic mods to a single shot.\n out.unsupported.push({\n reason: `effect type \"${String(type)}\" is not modelled by the buff layer`,\n effectFragment: node,\n });\n return;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Leaf translators\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a node's `target` field against the perspective we're translating\n * for. Returns:\n * - `\"self\"`: the node targets the buffed unit (apply attacker-side or\n * defender-side translation, depending on perspective + stat).\n * - `\"attacker\"` / `\"defender\"`: the node targets the other party explicitly.\n * - `\"unknown\"`: missing/malformed target.\n */\nfunction classifyTarget(\n node: Record<string, unknown>,\n): \"self\" | \"attacker\" | \"defender\" | \"unknown\" {\n const target = node.target;\n if (typeof target !== \"string\") return \"unknown\";\n if (target === ATTACKER_TARGET) return \"attacker\";\n if (DEFENDER_TARGETS.has(target)) return \"defender\";\n if (SELF_TARGETS.has(target)) return \"self\";\n return \"unknown\";\n}\n\n/**\n * Does this node's target match the buffed unit under the current\n * perspective? Used for symmetric roll/keyword translations where the same\n * effect is \"self\" in either direction.\n */\nfunction appliesToBuffedUnit(\n node: Record<string, unknown>,\n perspective: TranslationPerspective,\n): boolean {\n const cls = classifyTarget(node);\n if (cls === \"self\") return true;\n if (cls === \"attacker\") return perspective === \"attacker\";\n if (cls === \"defender\") return perspective === \"target\";\n return false;\n}\n\nfunction translateReroll(\n node: Record<string, unknown>,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n // Rerolls are inherently attacker-side (you re-roll your own hit/wound/\n // damage; save rerolls fire when *you* are the target). Apply only under\n // the matching perspective so a target-perspective walk doesn't grab the\n // attacker's reroll-failed-hits buff.\n if (opts.perspective === \"attacker\" && !appliesToBuffedUnit(node, \"attacker\")) return;\n const modifier = node.modifier;\n if (!isObject(modifier)) {\n out.unsupported.push({ reason: \"re-roll: missing modifier object\", effectFragment: node });\n return;\n }\n const roll = modifier.roll;\n const subset = modifier.subset;\n // Under target perspective, only \"save\" rerolls fire on the buffed unit.\n if (opts.perspective === \"target\" && roll !== \"save\") return;\n if (\n (roll === \"hit\" || roll === \"wound\" || roll === \"save\" || roll === \"damage\") &&\n (subset === \"ones\" || subset === \"all-failures\")\n ) {\n out.applied.push({ source, contribution: { type: \"reroll\", roll, subset } });\n return;\n }\n out.unsupported.push({\n reason: `re-roll on \"${String(roll)}\" (subset \"${String(subset)}\") is outside the damage path`,\n effectFragment: node,\n });\n}\n\nfunction translateRollModifier(\n node: Record<string, unknown>,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n const modifier = node.modifier;\n if (!isObject(modifier)) {\n out.unsupported.push({\n reason: \"roll-modifier: missing modifier object\",\n effectFragment: node,\n });\n return;\n }\n const value = signedValue(modifier);\n if (value === null) {\n out.unsupported.push({\n reason: `roll-modifier: operation \"${String(modifier.operation)}\" not supported`,\n effectFragment: node,\n });\n return;\n }\n const roll = modifier.roll;\n // Each roll type is intrinsically on one side. Hit / wound / damage are\n // attacker-side; save is defender-side. The perspective decides whether the\n // buffed unit's `target` is the right party for that roll type.\n if (opts.perspective === \"attacker\") {\n if (!appliesToBuffedUnit(node, \"attacker\")) return;\n if (roll === \"save\") return; // saves apply to the defender, not the attacker.\n } else {\n // target perspective: only `save` rolls on the buffed unit fire here.\n if (roll !== \"save\") return;\n if (!appliesToBuffedUnit(node, \"target\")) return;\n }\n switch (roll) {\n case \"hit\":\n out.applied.push({ source, contribution: { type: \"hit-mod\", value } });\n return;\n case \"wound\":\n out.applied.push({ source, contribution: { type: \"wound-mod\", value } });\n return;\n case \"save\":\n out.applied.push({ source, contribution: { type: \"save-mod\", value } });\n return;\n case \"damage\":\n out.applied.push({ source, contribution: { type: \"damage-mod\", value } });\n return;\n default:\n out.unsupported.push({\n reason: `roll-modifier on \"${String(roll)}\" is outside the damage path`,\n effectFragment: node,\n });\n }\n}\n\nfunction translateStatModifier(\n node: Record<string, unknown>,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n const modifier = node.modifier;\n if (!isObject(modifier)) {\n out.unsupported.push({\n reason: \"stat-modifier: missing modifier object\",\n effectFragment: node,\n });\n return;\n }\n const value = signedValue(modifier);\n if (value === null) {\n out.unsupported.push({\n reason: `stat-modifier: operation \"${String(modifier.operation)}\" not supported`,\n effectFragment: node,\n });\n return;\n }\n const stat = modifier.stat;\n const isOnBuffedUnit = appliesToBuffedUnit(node, opts.perspective);\n switch (stat) {\n case \"A\":\n if (opts.perspective !== \"attacker\" || !isOnBuffedUnit) return;\n out.applied.push({ source, contribution: { type: \"attacks-mod\", value } });\n return;\n case \"S\":\n if (opts.perspective !== \"attacker\" || !isOnBuffedUnit) return;\n out.applied.push({ source, contribution: { type: \"strength-mod\", value } });\n return;\n case \"T\":\n // Defender stat. Only relevant under target perspective.\n if (opts.perspective !== \"target\") {\n out.unsupported.push({\n reason: \"stat-modifier T: defender-side stat; applies when the buffed unit is the target\",\n effectFragment: node,\n });\n return;\n }\n if (!isOnBuffedUnit) return;\n out.applied.push({ source, contribution: { type: \"toughness-mod\", value } });\n return;\n case \"Sv\":\n // Saves improve when the *defender* gets +Sv. A +1 to Sv in printed\n // rules means \"improve the save by 1\", which maps to a `save-mod` of\n // `-value` since save-mod is signed against the *needed roll*.\n // (Equivalent: a -1 Sv penalty is a +1 save-mod.) We translate\n // \"Sv add 1\" → save-mod -1 to keep the resolver's sign convention.\n if (opts.perspective !== \"target\") {\n out.unsupported.push({\n reason: \"stat-modifier Sv: defender-side stat; applies when the buffed unit is the target\",\n effectFragment: node,\n });\n return;\n }\n if (!isOnBuffedUnit) return;\n out.applied.push({ source, contribution: { type: \"save-mod\", value: -value } });\n return;\n case \"AP\":\n // AP rides on the attacker's weapon profile and is stored as a negative\n // number in the data (e.g. AP -1). The data's `{operation:\"add\", value:-1}`\n // form means \"AP becomes one more negative\" → more piercing. `signedValue`\n // already returns that negative number directly, so pass it through.\n if (opts.perspective !== \"attacker\" || !isOnBuffedUnit) return;\n out.applied.push({ source, contribution: { type: \"ap-mod\", value } });\n return;\n default:\n out.unsupported.push({\n reason: `stat-modifier on \"${String(stat)}\" is outside the damage path`,\n effectFragment: node,\n });\n }\n}\n\nfunction translateFeelNoPain(\n node: Record<string, unknown>,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n // FNP applies when the buffed unit is the *target* — it ablates incoming\n // damage. Under attacker perspective the FNP is irrelevant (the unit is\n // firing, not taking damage). Drop silently rather than as `unsupported`\n // so attacker-perspective walks don't surface a spurious diagnostic for\n // every unit that happens to have a FNP rule.\n if (opts.perspective !== \"target\") return;\n const modifier = node.modifier;\n if (!isObject(modifier)) {\n out.unsupported.push({\n reason: \"feel-no-pain: missing modifier object\",\n effectFragment: node,\n });\n return;\n }\n const threshold = Number(modifier.threshold);\n if (!Number.isFinite(threshold)) {\n out.unsupported.push({\n reason: \"feel-no-pain: threshold not numeric\",\n effectFragment: node,\n });\n return;\n }\n out.applied.push({ source, contribution: { type: \"feel-no-pain\", threshold } });\n}\n\nfunction translateKeywordGrant(\n node: Record<string, unknown>,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n // Weapon-keyword grants ride with the attacker's profile (e.g. \"your\n // weapons gain [Sustained Hits 1]\"). Defender-perspective walks ignore\n // them — the keyword applies when the buffed unit fires, not when it's\n // shot at.\n if (opts.perspective !== \"attacker\") return;\n if (!appliesToBuffedUnit(node, \"attacker\")) return;\n const modifier = node.modifier;\n if (!isObject(modifier)) return;\n const keywords = modifier.keywords;\n if (!Array.isArray(keywords)) return;\n for (const raw of keywords) {\n if (typeof raw !== \"string\") continue;\n const ref = parseKeywordGrant(raw);\n if (!ref) {\n out.unsupported.push({\n reason: `keyword-grant: cannot parse \"${raw}\" to a catalog keyword`,\n effectFragment: { keyword: raw },\n });\n continue;\n }\n out.applied.push({ source, contribution: { type: \"extra-keyword\", keywordRef: ref } });\n }\n}\n\nfunction translateBsModifier(\n node: Record<string, unknown>,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n // A bs-modifier on `target: \"attacker\"` is a defender-side rule: it\n // penalises *incoming* hit rolls (e.g. Benefit of Cover). Translate it\n // as a `hit-mod` buff under target perspective so the resolver's ±1 cap\n // composes with attacker-side mods.\n if (opts.perspective !== \"target\") return;\n const cls = classifyTarget(node);\n if (cls !== \"attacker\") return; // a bs-modifier on self wouldn't make sense.\n const modifier = node.modifier;\n if (!isObject(modifier)) return;\n const value = signedValue(modifier);\n if (value === null) return;\n out.applied.push({ source, contribution: { type: \"hit-mod\", value } });\n}\n\nfunction translateConditional(\n node: Record<string, unknown>,\n source: BuffSource,\n opts: WalkOpts,\n out: EffectTranslation,\n): void {\n const condition = node.condition;\n const effect = node.effect;\n if (!isObject(condition)) return;\n const negated = condition.negated === true;\n const verdict = evaluateCondition(condition, opts.context);\n if (verdict === \"unknown\") {\n out.unsupported.push({\n reason: `conditional: cannot evaluate condition \"${String(condition.type)}\" against current context`,\n effectFragment: node,\n });\n return;\n }\n const active = negated ? !verdict : verdict;\n if (!active) return;\n walk(effect, source, opts, out);\n}\n\n// ---------------------------------------------------------------------------\n// Condition evaluator\n// ---------------------------------------------------------------------------\n\nfunction evaluateCondition(\n condition: Record<string, unknown>,\n ctx: EngineContext,\n): boolean | \"unknown\" {\n // Compound conditions use {operator, operands} rather than {type, parameters}.\n // The schema's `condition-node` oneOf doesn't guarantee discrimination by a\n // single field, so dispatch on shape: presence of `operator` + `operands`\n // wins over the simple-condition switch below.\n if (\n typeof condition.operator === \"string\" &&\n Array.isArray(condition.operands)\n ) {\n return evaluateCompound(condition.operator, condition.operands, ctx);\n }\n switch (condition.type) {\n case \"phase-is\": {\n const wanted = (condition.parameters as Record<string, unknown> | undefined)?.phase;\n if (typeof wanted !== \"string\") return \"unknown\";\n return ctx.phase === wanted;\n }\n case \"timing-is\": {\n const wanted = (condition.parameters as Record<string, unknown> | undefined)?.timing;\n if (typeof wanted !== \"string\") return \"unknown\";\n if (ctx.timing === undefined) return \"unknown\";\n return ctx.timing === wanted;\n }\n case \"remained-stationary\":\n return ctx.attackerStationary === true;\n case \"target-has-keyword\": {\n const kw = (condition.parameters as Record<string, unknown> | undefined)?.keyword;\n if (typeof kw !== \"string\") return \"unknown\";\n return (ctx.targetKeywords ?? []).includes(kw.toLowerCase());\n }\n case \"unit-has-keyword\": {\n const kw = (condition.parameters as Record<string, unknown> | undefined)?.keyword;\n if (typeof kw !== \"string\") return \"unknown\";\n return (ctx.attackerKeywords ?? []).includes(kw.toLowerCase());\n }\n case \"is-attached\":\n // The resolver knows whether a leader is attached; absent that signal\n // here, treat as unknown so the SPA can surface the gap.\n return \"unknown\";\n default:\n return \"unknown\";\n }\n}\n\n/**\n * Kleene three-valued evaluator for compound conditions. `and` short-circuits\n * to `false` as soon as any operand is false (an unknown operand is then\n * irrelevant); `or` short-circuits to `true` symmetrically. `not` flips its\n * single operand and leaves `\"unknown\"` as `\"unknown\"`. Unknown operands that\n * don't get short-circuited propagate as `\"unknown\"` so the SPA can surface\n * the gap rather than collapsing it into a misleading false.\n */\nfunction evaluateCompound(\n operator: string,\n operands: unknown[],\n ctx: EngineContext,\n): boolean | \"unknown\" {\n if (operator === \"not\") {\n const first = operands[0];\n if (!isObject(first)) return \"unknown\";\n const v = evaluateCondition(first, ctx);\n if (v === \"unknown\") return \"unknown\";\n return !v;\n }\n if (operator !== \"and\" && operator !== \"or\") return \"unknown\";\n let sawUnknown = false;\n for (const operand of operands) {\n if (!isObject(operand)) {\n sawUnknown = true;\n continue;\n }\n const v = evaluateCondition(operand, ctx);\n if (v === \"unknown\") {\n sawUnknown = true;\n continue;\n }\n if (operator === \"and\" && v === false) return false;\n if (operator === \"or\" && v === true) return true;\n }\n if (sawUnknown) return \"unknown\";\n return operator === \"and\"; // all true for AND, all false for OR\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Read a signed numeric value out of a modifier `{operation, value}` pair.\n * \"add\"/\"subtract\" become the matching sign; \"set\" / \"multiply\" / etc. return\n * `null` (translator surfaces them as unsupported).\n */\nfunction signedValue(modifier: Record<string, unknown>): number | null {\n const value = Number(modifier.value);\n if (!Number.isFinite(value)) return null;\n switch (modifier.operation) {\n case \"add\":\n return value;\n case \"subtract\":\n return -value;\n default:\n return null;\n }\n}\n\n/**\n * Parse a printed weapon-keyword string (e.g. `\"Sustained Hits 1\"`,\n * `\"Anti-INFANTRY 4+\"`, `\"Lethal Hits\"`) into a `{keyword_id, parameters?}`\n * catalog reference, or `null` if the form is unrecognised.\n *\n * Reverses the conventions baked into the M0 catalog: kebab-case ids,\n * trailing number → `value`, embedded keyword + threshold → `target_keyword`\n * + `threshold`.\n */\nexport function parseKeywordGrant(raw: string): WeaponKeywordRef | null {\n const trimmed = raw.trim();\n if (trimmed === \"\") return null;\n\n // Anti-X N+ → { anti, target_keyword: X, threshold: N }\n const antiMatch = /^anti-([A-Z][A-Z\\s-]*)\\s+(\\d+)\\+?$/i.exec(trimmed);\n if (antiMatch) {\n return {\n keyword_id: \"anti\",\n parameters: { target_keyword: antiMatch[1].trim(), threshold: Number(antiMatch[2]) },\n };\n }\n\n // \"Lethal Hits\", \"Twin-linked\", \"Heavy\" → kebab-case lookup, no params.\n // \"Sustained Hits 1\", \"Rapid Fire 2\", \"Melta 2\" → kebab-case + value.\n const valueMatch = /^(.+?)\\s+(\\d+)$/.exec(trimmed);\n if (valueMatch) {\n return {\n keyword_id: toKebabCase(valueMatch[1]),\n parameters: { value: Number(valueMatch[2]) },\n };\n }\n return { keyword_id: toKebabCase(trimmed) };\n}\n\nfunction toKebabCase(s: string): string {\n return s\n .toLowerCase()\n .replace(/[\\s_]+/g, \"-\")\n .replace(/[^a-z0-9-]/g, \"\");\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"]}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Translate a weapon-keyword catalog entry into the Buff stack it contributes
3
+ * for a given reference-site parameter set and engine context.
4
+ *
5
+ * Two paths converge here:
6
+ *
7
+ * 1. **DSL walk**, for keywords whose catalog `effect` is non-null
8
+ * (`twin-linked`, `heavy`). The walker handles a deliberately small subset
9
+ * of nodes — `re-roll`, `roll-modifier`, `conditional`-on-the-conditions
10
+ * the engine knows about — and produces `Buff`s with
11
+ * `source.kind = "weapon-keyword"`.
12
+ *
13
+ * 2. **Id dispatch**, for the eight rules whose catalog `effect` is null
14
+ * because the DSL has no primitive for them yet — `lethal-hits`,
15
+ * `sustained-hits`, `devastating-wounds`, `anti`, `melta`, `rapid-fire`,
16
+ * `torrent`, `ignores-cover`. These are surfaced as `extra-keyword` buffs
17
+ * so the engine can read them out of `ResolvedModifiers.extraKeywords`
18
+ * and dispatch its math directly.
19
+ *
20
+ * Unrecognised nodes drop silently in M1 — diagnostic surfacing belongs to
21
+ * M2's broader ability translator.
22
+ */
23
+ import type { Buff, EngineContext } from "./buffs.js";
24
+ /**
25
+ * Convert a single weapon-keyword reference (catalog effect + reference-site
26
+ * parameters) into the buff contributions it makes against `context`.
27
+ */
28
+ export declare function buffsFromKeyword(args: {
29
+ keywordId: string;
30
+ weaponId: string;
31
+ effect: unknown;
32
+ parameters?: Record<string, unknown>;
33
+ context: EngineContext;
34
+ }): Buff[];
35
+ //# sourceMappingURL=from-keyword.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"from-keyword.d.ts","sourceRoot":"","sources":["../../src/cruncher/from-keyword.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAc,aAAa,EAAoB,MAAM,YAAY,CAAC;AAcpF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,aAAa,CAAC;CACxB,GAAG,IAAI,EAAE,CAiBT"}