@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/import/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,mFAAmF;IACnF,EAAE,EAAE,YAAY,CAAC;IACjB,kEAAkE;IAClE,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;IACnC,sEAAsE;IACtE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,CAAC;CACvC;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,EAAE,GACxB,aAAa,CASf"}
@@ -7,3 +7,4 @@ export function selectAdapter(decoded, adapters) {
7
7
  }
8
8
  return adapter;
9
9
  }
10
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../src/import/adapter.ts"],"names":[],"mappings":"AA0BA,0DAA0D;AAC1D,MAAM,UAAU,aAAa,CAC3B,OAAgB,EAChB,QAAyB;IAEzB,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,uDAAuD;YACrD,WAAW,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CAC/D,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["/**\n * The format-adapter seam.\n *\n * Each supported source format implements {@link FormatAdapter}: it recognises a\n * decoded payload ({@link FormatAdapter.matches}) and lowers it to the\n * format-agnostic {@link ParsedRoster} ({@link FormatAdapter.parse}). Resolution\n * onto 40kdc entity ids happens once, downstream, against any `ParsedRoster` —\n * so adding a new source format (New Recruit, WTC, …) means writing one adapter,\n * not touching the resolver.\n *\n * v1 registers only {@link listForgeAdapter}.\n *\n * @packageDocumentation\n */\nimport type { ParsedRoster, RosterFormat } from \"./types.js\";\n\n/** Recognises and parses one source list-export format. */\nexport interface FormatAdapter {\n /** Stable identifier for the format. Carries through to `Roster.source.format`. */\n id: RosterFormat;\n /** True when this adapter can parse the given decoded payload. */\n matches(decoded: unknown): boolean;\n /** Lower a recognised payload to the format-agnostic intermediate. */\n parse(decoded: unknown): ParsedRoster;\n}\n\n/** Pick the first adapter that recognises the payload. */\nexport function selectAdapter(\n decoded: unknown,\n adapters: FormatAdapter[],\n): FormatAdapter {\n const adapter = adapters.find((a) => a.matches(decoded));\n if (!adapter) {\n throw new Error(\n \"no registered import adapter recognises this payload \" +\n `(tried: ${adapters.map((a) => a.id).join(\", \") || \"none\"})`,\n );\n }\n return adapter;\n}\n"]}
@@ -4,3 +4,4 @@
4
4
  * @throws if the input is neither valid JSON nor a decodable gzip payload.
5
5
  */
6
6
  export declare function decodeListForge(input: string): unknown;
7
+ //# sourceMappingURL=decode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/import/decode.ts"],"names":[],"mappings":"AA6CA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CA+BtD"}
@@ -70,3 +70,4 @@ export function decodeListForge(input) {
70
70
  }
71
71
  return JSON.parse(json);
72
72
  }
73
+ //# sourceMappingURL=decode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.js","sourceRoot":"","sources":["../../src/import/decode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,kEAAkE;AAClE,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAEnC,2DAA2D;AAC3D,MAAM,gBAAgB,GAAG,aAAa,CAAC;AAEvC;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,KAAa;IACnC,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACpD,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC,KAAK,CAAC,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACzC,OAAO,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,CAAC;IAED,mCAAmC;IACnC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;IAExC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,wEAAwE;YACtE,4CAA4C,kBAAkB,KAAK,CACtE,CAAC;IACJ,CAAC;IAED,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC7C,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,kDAAkD,EAAE;YAClE,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * Decode a ListForge share payload into a JSON object.\n *\n * ListForge packs a roster as `base64( gzip( utf8(json) ) )` and embeds it in a\n * URL hash fragment: `https://app/#/listforge/<BASE64>`. The fragment is used\n * deliberately so browsers never send it to a server, preserving the payload\n * verbatim. A valid gzipped payload always base64-encodes to a string starting\n * with `H4sIAAAAAAAAA`.\n *\n * {@link decodeListForge} accepts any of three forms and returns the parsed JSON:\n * - a full URL (the segment after the last `/` is taken),\n * - a bare base64 segment,\n * - an already-decoded JSON string (passed straight to `JSON.parse`).\n *\n * Only `node:zlib` is used — no third-party dependency.\n *\n * @packageDocumentation\n */\nimport { gunzipSync } from \"node:zlib\";\n\n/** The base64 prefix every ListForge gzip payload begins with. */\nconst GZIP_BASE64_PREFIX = \"H4sIA\";\n\n/** The path marker ListForge uses ahead of the payload. */\nconst LISTFORGE_MARKER = \"/listforge/\";\n\n/**\n * Extract the payload segment from an input that may be a URL.\n *\n * The base64 alphabet includes `/`, so a bare base64 segment cannot be split on\n * `/`. We only treat the input as a URL when it carries the `/listforge/` marker\n * or an `http(s)://` scheme; otherwise it is returned unchanged.\n */\nfunction extractSegment(input: string): string {\n const markerIndex = input.indexOf(LISTFORGE_MARKER);\n if (markerIndex !== -1) {\n return input.slice(markerIndex + LISTFORGE_MARKER.length);\n }\n if (/^https?:\\/\\//i.test(input)) {\n const lastSlash = input.lastIndexOf(\"/\");\n return lastSlash === -1 ? input : input.slice(lastSlash + 1);\n }\n return input;\n}\n\n/**\n * Decode a ListForge payload (URL, bare base64, or raw JSON) into a JSON value.\n *\n * @throws if the input is neither valid JSON nor a decodable gzip payload.\n */\nexport function decodeListForge(input: string): unknown {\n const trimmed = input.trim();\n if (trimmed === \"\") {\n throw new Error(\"decodeListForge: empty input\");\n }\n\n // Raw JSON object passed directly.\n if (trimmed.startsWith(\"{\")) {\n return JSON.parse(trimmed);\n }\n\n const segment = extractSegment(trimmed);\n\n if (!segment.startsWith(GZIP_BASE64_PREFIX)) {\n throw new Error(\n \"decodeListForge: input is not a ListForge payload (expected raw JSON, \" +\n `or a gzip+base64 segment beginning with \"${GZIP_BASE64_PREFIX}…\")`,\n );\n }\n\n let json: string;\n try {\n const bytes = Buffer.from(segment, \"base64\");\n json = gunzipSync(bytes).toString(\"utf8\");\n } catch (cause) {\n throw new Error(\"decodeListForge: failed to gunzip base64 payload\", {\n cause,\n });\n }\n\n return JSON.parse(json);\n}\n"]}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * GW adapter: lower the Games Workshop 40K app's plain-text army-list export to
3
+ * a {@link ParsedRoster}.
4
+ *
5
+ * The format opens with the same `++++…++++` summary fence as the NewRecruit WTC
6
+ * formats (FACTION KEYWORD / DETACHMENT / TOTAL ARMY POINTS / WARLORD /
7
+ * ENHANCEMENT / NUMBER OF UNITS / SECONDARY), then lists units grouped under
8
+ * ALL-CAPS battlefield-role sections (`BATTLELINE`, `CHARACTERS`,
9
+ * `ALLIED UNITS`, …). Each unit is a header line `Name (N pts)` followed by
10
+ * `•`-bulleted entries:
11
+ *
12
+ * ```
13
+ * War Dog Executioner (130 pts)
14
+ * • 1x Armoured feet
15
+ * • 2x War Dog autocannon
16
+ * • Houndpack Lance Character, Warlord
17
+ *
18
+ * Nurglings (40 pts)
19
+ * • 3x Nurgling Swarm
20
+ * • 3x Diseased claws and teeth
21
+ * ```
22
+ *
23
+ * Bullet classification (the parsing crux):
24
+ * - A top-level `• Nx Thing` *with* further-indented child bullets is a **model
25
+ * group** — `N` adds to the model count and the children are that group's
26
+ * wargear (Nurglings, Beasts of Nurgle).
27
+ * - A top-level `• Nx Thing` *without* children is plain **wargear**.
28
+ * - A bullet *without* an `Nx` count is an **annotation**: `… Character` flags a
29
+ * character, `Warlord` flags the warlord, `Name (+N pts)` is the enhancement.
30
+ *
31
+ * **Disjointness from the WTC matchers**: the GW format always carries `•`
32
+ * bullets and never the WTC `N with` lines. wtc-full always has `N with` (so it
33
+ * never collides), and wtc-compact never has bullets (its matcher now excludes
34
+ * them). This adapter therefore matches on *bullets present* + *no `N with`*.
35
+ *
36
+ * The GW export carries no separate POINTS LIMIT line, so `declared_limit`
37
+ * falls back to TOTAL ARMY POINTS (the round-trippable battle-size signal).
38
+ *
39
+ * @packageDocumentation
40
+ */
41
+ import type { FormatAdapter } from "./adapter.js";
42
+ import type { ParsedUnit } from "./types.js";
43
+ /** Accept the input only when it carries the FACTION KEYWORD summary header,
44
+ * has `•` bullets, and lacks the WTC `N with` body lines. */
45
+ declare function isGwText(decoded: unknown): string | null;
46
+ interface GwHeader {
47
+ name: string;
48
+ faction_raw_name: string | null;
49
+ detachment_raw_name: string | null;
50
+ total_reported: number | null;
51
+ declared_limit: number | null;
52
+ battle_size_raw: string | null;
53
+ }
54
+ declare function parseHeader(lines: string[]): {
55
+ header: GwHeader;
56
+ bodyStart: number;
57
+ } | null;
58
+ declare function parseBody(lines: string[], bodyStart: number): {
59
+ units: ParsedUnit[];
60
+ multi_force: boolean;
61
+ };
62
+ export declare const gwAdapter: FormatAdapter;
63
+ export declare const _internals: {
64
+ isGwText: typeof isGwText;
65
+ parseHeader: typeof parseHeader;
66
+ parseBody: typeof parseBody;
67
+ };
68
+ export {};
69
+ //# sourceMappingURL=gw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gw.d.ts","sourceRoot":"","sources":["../../src/import/gw.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAgB,UAAU,EAAiB,MAAM,YAAY,CAAC;AA8B1E;6DAC6D;AAC7D,iBAAS,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMjD;AAED,UAAU,QAAQ;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,iBAAS,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA+CpF;AA8FD,iBAAS,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG;IACtD,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;CACtB,CAsDA;AAED,eAAO,MAAM,SAAS,EAAE,aAqCvB,CAAC;AAGF,eAAO,MAAM,UAAU;;;;CAItB,CAAC"}
@@ -0,0 +1,245 @@
1
+ import { factionFromKeyword, inferBattleSizeRaw, stripParenthetical, } from "./newrecruit-text.js";
2
+ const FACTION_KEYWORD_PREFIX = "+ FACTION KEYWORD:";
3
+ const HEADER_FIELDS = {
4
+ faction: /^\+\s*FACTION KEYWORD:\s*(.+?)\s*$/i,
5
+ detachment: /^\+\s*DETACHMENT:\s*(.+?)\s*$/i,
6
+ totalPoints: /^\+\s*TOTAL ARMY POINTS:\s*(\d+)\s*pts?\s*$/i,
7
+ };
8
+ const FENCE = /^\++\s*$/;
9
+ const HEADER_LINE = /^\+/;
10
+ const SECTION_HEADER = /^[A-Z][A-Z0-9 \-/&]+$/; // BATTLELINE, ALLIED UNITS, …
11
+ const UNIT_HEADER = /^(.+?)\s*\(\s*(\d+)\s*pts?\s*\)\s*$/i;
12
+ const BULLET_LINE = /^(\s*)•\s*(.+?)\s*$/u;
13
+ const NX_PREFIX = /^(\d+)x\s+(.+)$/;
14
+ const ENHANCEMENT_ANNOT = /^(.+?)\s*\(\+\s*(\d+)\s*pts?\s*\)\s*$/i;
15
+ const WITH_LINE = /^[\t ]*\d+\s+with\b/m;
16
+ const BULLET = /^[\t ]*•/mu;
17
+ const ALLIED_SECTION = "ALLIED UNITS";
18
+ const CHARACTERS_SECTION = "CHARACTERS";
19
+ const CHARACTER_SUFFIX = " Character";
20
+ const WARLORD_MARKER = "Warlord";
21
+ /** Accept the input only when it carries the FACTION KEYWORD summary header,
22
+ * has `•` bullets, and lacks the WTC `N with` body lines. */
23
+ function isGwText(decoded) {
24
+ if (typeof decoded !== "string")
25
+ return null;
26
+ if (!decoded.includes(FACTION_KEYWORD_PREFIX))
27
+ return null;
28
+ if (!BULLET.test(decoded))
29
+ return null;
30
+ if (WITH_LINE.test(decoded))
31
+ return null; // that's wtc-full
32
+ return decoded;
33
+ }
34
+ function parseHeader(lines) {
35
+ let faction_raw_name = null;
36
+ let detachment_raw_name = null;
37
+ let total_reported = null;
38
+ const fenceIndices = [];
39
+ for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {
40
+ if (FENCE.test(lines[i]))
41
+ fenceIndices.push(i);
42
+ }
43
+ let sawFactionKeyword = false;
44
+ for (const line of lines) {
45
+ if (!line.startsWith("+"))
46
+ continue;
47
+ const factionMatch = HEADER_FIELDS.faction.exec(line);
48
+ if (factionMatch) {
49
+ faction_raw_name = factionFromKeyword(factionMatch[1]);
50
+ sawFactionKeyword = true;
51
+ continue;
52
+ }
53
+ const detMatch = HEADER_FIELDS.detachment.exec(line);
54
+ if (detMatch) {
55
+ detachment_raw_name = stripParenthetical(detMatch[1]);
56
+ continue;
57
+ }
58
+ const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);
59
+ if (ptsMatch) {
60
+ total_reported = Number.parseInt(ptsMatch[1], 10);
61
+ }
62
+ }
63
+ if (!sawFactionKeyword)
64
+ return null;
65
+ const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;
66
+ // The GW export has no POINTS LIMIT line — only TOTAL ARMY POINTS. Use it as
67
+ // the declared limit so the inferred battle size stays round-trippable.
68
+ const declared_limit = total_reported;
69
+ return {
70
+ header: {
71
+ name: "Imported roster",
72
+ faction_raw_name,
73
+ detachment_raw_name,
74
+ total_reported,
75
+ declared_limit,
76
+ battle_size_raw: inferBattleSizeRaw(declared_limit),
77
+ },
78
+ bodyStart,
79
+ };
80
+ }
81
+ function finishUnit(acc) {
82
+ const topIndent = acc.bullets.length
83
+ ? Math.min(...acc.bullets.map((b) => b.indent))
84
+ : 0;
85
+ const wargear = new Map();
86
+ let model_count = 0;
87
+ let is_warlord = false;
88
+ let is_character = acc.section === CHARACTERS_SECTION;
89
+ let enhancement_raw_name = null;
90
+ let enhancement_points = null;
91
+ const addWargear = (raw_name, count) => {
92
+ wargear.set(raw_name, (wargear.get(raw_name) ?? 0) + count);
93
+ };
94
+ for (let i = 0; i < acc.bullets.length; i += 1) {
95
+ const b = acc.bullets[i];
96
+ // A child bullet (deeper than the unit's top level) is a model group's
97
+ // weapon — its `Nx` count is already the squad-wide total.
98
+ if (b.indent > topIndent) {
99
+ if (b.count !== null)
100
+ addWargear(b.text, b.count);
101
+ continue;
102
+ }
103
+ // Top-level annotation (no `Nx` count): enhancement / character / warlord.
104
+ if (b.count === null) {
105
+ const enh = ENHANCEMENT_ANNOT.exec(b.text);
106
+ if (enh) {
107
+ if (enhancement_raw_name === null) {
108
+ enhancement_raw_name = enh[1].trim();
109
+ enhancement_points = Number.parseInt(enh[2], 10);
110
+ }
111
+ continue;
112
+ }
113
+ for (const token of b.text.split(",").map((s) => s.trim()).filter(Boolean)) {
114
+ if (token === WARLORD_MARKER)
115
+ is_warlord = true;
116
+ else if (token.endsWith(CHARACTER_SUFFIX))
117
+ is_character = true;
118
+ }
119
+ continue;
120
+ }
121
+ // Top-level `Nx` bullet: a model group when it has child bullets beneath
122
+ // it, otherwise plain wargear.
123
+ const next = acc.bullets[i + 1];
124
+ if (next && next.indent > topIndent) {
125
+ model_count += b.count;
126
+ }
127
+ else {
128
+ addWargear(b.text, b.count);
129
+ }
130
+ }
131
+ if (model_count === 0)
132
+ model_count = 1;
133
+ // The GW unit header points include the enhancement; back it out to the base.
134
+ const displayed = acc.displayed_pts;
135
+ const points = displayed === null
136
+ ? null
137
+ : enhancement_points !== null
138
+ ? displayed - enhancement_points
139
+ : displayed;
140
+ const wargearList = [];
141
+ for (const [raw_name, count] of wargear)
142
+ wargearList.push({ raw_name, count });
143
+ return {
144
+ raw_name: acc.raw_name,
145
+ is_character,
146
+ model_count,
147
+ points,
148
+ is_warlord,
149
+ enhancement_raw_name,
150
+ enhancement_points,
151
+ wargear: wargearList,
152
+ };
153
+ }
154
+ function parseBody(lines, bodyStart) {
155
+ const units = [];
156
+ let current = null;
157
+ let section = null;
158
+ let alliedUnits = 0;
159
+ const finalize = () => {
160
+ if (current) {
161
+ units.push(finishUnit(current));
162
+ current = null;
163
+ }
164
+ };
165
+ for (let i = bodyStart; i < lines.length; i += 1) {
166
+ const raw = lines[i];
167
+ const line = raw.trim();
168
+ if (!line || FENCE.test(line) || HEADER_LINE.test(line))
169
+ continue;
170
+ const bulletMatch = BULLET_LINE.exec(raw);
171
+ if (bulletMatch) {
172
+ if (current) {
173
+ const indent = bulletMatch[1].length;
174
+ const rest = bulletMatch[2];
175
+ const nx = NX_PREFIX.exec(rest);
176
+ current.bullets.push({
177
+ indent,
178
+ count: nx ? Number.parseInt(nx[1], 10) : null,
179
+ text: (nx ? nx[2] : rest).trim(),
180
+ });
181
+ }
182
+ continue;
183
+ }
184
+ const unitMatch = UNIT_HEADER.exec(line);
185
+ if (unitMatch) {
186
+ finalize();
187
+ current = {
188
+ raw_name: unitMatch[1].trim(),
189
+ displayed_pts: Number.parseInt(unitMatch[2], 10),
190
+ section,
191
+ bullets: [],
192
+ };
193
+ if (section === ALLIED_SECTION)
194
+ alliedUnits += 1;
195
+ continue;
196
+ }
197
+ if (SECTION_HEADER.test(line)) {
198
+ finalize();
199
+ section = line;
200
+ }
201
+ }
202
+ finalize();
203
+ return { units, multi_force: alliedUnits > 0 };
204
+ }
205
+ export const gwAdapter = {
206
+ id: "gw",
207
+ matches(decoded) {
208
+ return isGwText(decoded) !== null;
209
+ },
210
+ parse(decoded) {
211
+ const text = isGwText(decoded);
212
+ if (text === null)
213
+ throw new Error("gw: input is not a GW app text export");
214
+ const lines = text.split(/\r?\n/);
215
+ const parsed = parseHeader(lines);
216
+ if (!parsed)
217
+ throw new Error('gw: missing "+ FACTION KEYWORD:" header');
218
+ const { header, bodyStart } = parsed;
219
+ const { units, multi_force } = parseBody(lines, bodyStart);
220
+ let total_computed = 0;
221
+ for (const u of units) {
222
+ total_computed += u.points ?? 0;
223
+ total_computed += u.enhancement_points ?? 0;
224
+ }
225
+ return {
226
+ name: header.name,
227
+ generated_by: null,
228
+ faction_raw_name: header.faction_raw_name,
229
+ detachment_raw_name: header.detachment_raw_name,
230
+ battle_size_raw: header.battle_size_raw,
231
+ declared_limit: header.declared_limit,
232
+ total_reported: header.total_reported,
233
+ total_computed,
234
+ units,
235
+ multi_force,
236
+ };
237
+ },
238
+ };
239
+ // Internals re-exported for unit tests.
240
+ export const _internals = {
241
+ isGwText,
242
+ parseHeader,
243
+ parseBody,
244
+ };
245
+ //# sourceMappingURL=gw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gw.js","sourceRoot":"","sources":["../../src/import/gw.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GW adapter: lower the Games Workshop 40K app's plain-text army-list export to\n * a {@link ParsedRoster}.\n *\n * The format opens with the same `++++…++++` summary fence as the NewRecruit WTC\n * formats (FACTION KEYWORD / DETACHMENT / TOTAL ARMY POINTS / WARLORD /\n * ENHANCEMENT / NUMBER OF UNITS / SECONDARY), then lists units grouped under\n * ALL-CAPS battlefield-role sections (`BATTLELINE`, `CHARACTERS`,\n * `ALLIED UNITS`, …). Each unit is a header line `Name (N pts)` followed by\n * `•`-bulleted entries:\n *\n * ```\n * War Dog Executioner (130 pts)\n * • 1x Armoured feet\n * • 2x War Dog autocannon\n * • Houndpack Lance Character, Warlord\n *\n * Nurglings (40 pts)\n * • 3x Nurgling Swarm\n * • 3x Diseased claws and teeth\n * ```\n *\n * Bullet classification (the parsing crux):\n * - A top-level `• Nx Thing` *with* further-indented child bullets is a **model\n * group** — `N` adds to the model count and the children are that group's\n * wargear (Nurglings, Beasts of Nurgle).\n * - A top-level `• Nx Thing` *without* children is plain **wargear**.\n * - A bullet *without* an `Nx` count is an **annotation**: `… Character` flags a\n * character, `Warlord` flags the warlord, `Name (+N pts)` is the enhancement.\n *\n * **Disjointness from the WTC matchers**: the GW format always carries `•`\n * bullets and never the WTC `N with` lines. wtc-full always has `N with` (so it\n * never collides), and wtc-compact never has bullets (its matcher now excludes\n * them). This adapter therefore matches on *bullets present* + *no `N with`*.\n *\n * The GW export carries no separate POINTS LIMIT line, so `declared_limit`\n * falls back to TOTAL ARMY POINTS (the round-trippable battle-size signal).\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport {\n factionFromKeyword,\n inferBattleSizeRaw,\n stripParenthetical,\n} from \"./newrecruit-text.js\";\n\nconst FACTION_KEYWORD_PREFIX = \"+ FACTION KEYWORD:\";\n\nconst HEADER_FIELDS = {\n faction: /^\\+\\s*FACTION KEYWORD:\\s*(.+?)\\s*$/i,\n detachment: /^\\+\\s*DETACHMENT:\\s*(.+?)\\s*$/i,\n totalPoints: /^\\+\\s*TOTAL ARMY POINTS:\\s*(\\d+)\\s*pts?\\s*$/i,\n} as const;\n\nconst FENCE = /^\\++\\s*$/;\nconst HEADER_LINE = /^\\+/;\nconst SECTION_HEADER = /^[A-Z][A-Z0-9 \\-/&]+$/; // BATTLELINE, ALLIED UNITS, …\nconst UNIT_HEADER = /^(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst BULLET_LINE = /^(\\s*)•\\s*(.+?)\\s*$/u;\nconst NX_PREFIX = /^(\\d+)x\\s+(.+)$/;\nconst ENHANCEMENT_ANNOT = /^(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst WITH_LINE = /^[\\t ]*\\d+\\s+with\\b/m;\nconst BULLET = /^[\\t ]*•/mu;\n\nconst ALLIED_SECTION = \"ALLIED UNITS\";\nconst CHARACTERS_SECTION = \"CHARACTERS\";\nconst CHARACTER_SUFFIX = \" Character\";\nconst WARLORD_MARKER = \"Warlord\";\n\n/** Accept the input only when it carries the FACTION KEYWORD summary header,\n * has `•` bullets, and lacks the WTC `N with` body lines. */\nfunction isGwText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n if (!decoded.includes(FACTION_KEYWORD_PREFIX)) return null;\n if (!BULLET.test(decoded)) return null;\n if (WITH_LINE.test(decoded)) return null; // that's wtc-full\n return decoded;\n}\n\ninterface GwHeader {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n total_reported: number | null;\n declared_limit: number | null;\n battle_size_raw: string | null;\n}\n\nfunction parseHeader(lines: string[]): { header: GwHeader; bodyStart: number } | null {\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let total_reported: number | null = null;\n\n const fenceIndices: number[] = [];\n for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {\n if (FENCE.test(lines[i])) fenceIndices.push(i);\n }\n\n let sawFactionKeyword = false;\n for (const line of lines) {\n if (!line.startsWith(\"+\")) continue;\n const factionMatch = HEADER_FIELDS.faction.exec(line);\n if (factionMatch) {\n faction_raw_name = factionFromKeyword(factionMatch[1]);\n sawFactionKeyword = true;\n continue;\n }\n const detMatch = HEADER_FIELDS.detachment.exec(line);\n if (detMatch) {\n detachment_raw_name = stripParenthetical(detMatch[1]);\n continue;\n }\n const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);\n if (ptsMatch) {\n total_reported = Number.parseInt(ptsMatch[1], 10);\n }\n }\n\n if (!sawFactionKeyword) return null;\n\n const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;\n // The GW export has no POINTS LIMIT line — only TOTAL ARMY POINTS. Use it as\n // the declared limit so the inferred battle size stays round-trippable.\n const declared_limit = total_reported;\n return {\n header: {\n name: \"Imported roster\",\n faction_raw_name,\n detachment_raw_name,\n total_reported,\n declared_limit,\n battle_size_raw: inferBattleSizeRaw(declared_limit),\n },\n bodyStart,\n };\n}\n\ninterface Bullet {\n indent: number;\n count: number | null;\n text: string;\n}\n\ninterface UnitAcc {\n raw_name: string;\n displayed_pts: number | null;\n section: string | null;\n bullets: Bullet[];\n}\n\nfunction finishUnit(acc: UnitAcc): ParsedUnit {\n const topIndent = acc.bullets.length\n ? Math.min(...acc.bullets.map((b) => b.indent))\n : 0;\n\n const wargear = new Map<string, number>();\n let model_count = 0;\n let is_warlord = false;\n let is_character = acc.section === CHARACTERS_SECTION;\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n\n const addWargear = (raw_name: string, count: number): void => {\n wargear.set(raw_name, (wargear.get(raw_name) ?? 0) + count);\n };\n\n for (let i = 0; i < acc.bullets.length; i += 1) {\n const b = acc.bullets[i];\n\n // A child bullet (deeper than the unit's top level) is a model group's\n // weapon — its `Nx` count is already the squad-wide total.\n if (b.indent > topIndent) {\n if (b.count !== null) addWargear(b.text, b.count);\n continue;\n }\n\n // Top-level annotation (no `Nx` count): enhancement / character / warlord.\n if (b.count === null) {\n const enh = ENHANCEMENT_ANNOT.exec(b.text);\n if (enh) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = enh[1].trim();\n enhancement_points = Number.parseInt(enh[2], 10);\n }\n continue;\n }\n for (const token of b.text.split(\",\").map((s) => s.trim()).filter(Boolean)) {\n if (token === WARLORD_MARKER) is_warlord = true;\n else if (token.endsWith(CHARACTER_SUFFIX)) is_character = true;\n }\n continue;\n }\n\n // Top-level `Nx` bullet: a model group when it has child bullets beneath\n // it, otherwise plain wargear.\n const next = acc.bullets[i + 1];\n if (next && next.indent > topIndent) {\n model_count += b.count;\n } else {\n addWargear(b.text, b.count);\n }\n }\n\n if (model_count === 0) model_count = 1;\n\n // The GW unit header points include the enhancement; back it out to the base.\n const displayed = acc.displayed_pts;\n const points =\n displayed === null\n ? null\n : enhancement_points !== null\n ? displayed - enhancement_points\n : displayed;\n\n const wargearList: ParsedWargear[] = [];\n for (const [raw_name, count] of wargear) wargearList.push({ raw_name, count });\n\n return {\n raw_name: acc.raw_name,\n is_character,\n model_count,\n points,\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear: wargearList,\n };\n}\n\nfunction parseBody(lines: string[], bodyStart: number): {\n units: ParsedUnit[];\n multi_force: boolean;\n} {\n const units: ParsedUnit[] = [];\n let current: UnitAcc | null = null;\n let section: string | null = null;\n let alliedUnits = 0;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (let i = bodyStart; i < lines.length; i += 1) {\n const raw = lines[i];\n const line = raw.trim();\n if (!line || FENCE.test(line) || HEADER_LINE.test(line)) continue;\n\n const bulletMatch = BULLET_LINE.exec(raw);\n if (bulletMatch) {\n if (current) {\n const indent = bulletMatch[1].length;\n const rest = bulletMatch[2];\n const nx = NX_PREFIX.exec(rest);\n current.bullets.push({\n indent,\n count: nx ? Number.parseInt(nx[1], 10) : null,\n text: (nx ? nx[2] : rest).trim(),\n });\n }\n continue;\n }\n\n const unitMatch = UNIT_HEADER.exec(line);\n if (unitMatch) {\n finalize();\n current = {\n raw_name: unitMatch[1].trim(),\n displayed_pts: Number.parseInt(unitMatch[2], 10),\n section,\n bullets: [],\n };\n if (section === ALLIED_SECTION) alliedUnits += 1;\n continue;\n }\n\n if (SECTION_HEADER.test(line)) {\n finalize();\n section = line;\n }\n }\n\n finalize();\n return { units, multi_force: alliedUnits > 0 };\n}\n\nexport const gwAdapter: FormatAdapter = {\n id: \"gw\",\n\n matches(decoded: unknown): boolean {\n return isGwText(decoded) !== null;\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isGwText(decoded);\n if (text === null) throw new Error(\"gw: input is not a GW app text export\");\n\n const lines = text.split(/\\r?\\n/);\n const parsed = parseHeader(lines);\n if (!parsed) throw new Error('gw: missing \"+ FACTION KEYWORD:\" header');\n const { header, bodyStart } = parsed;\n\n const { units, multi_force } = parseBody(lines, bodyStart);\n\n let total_computed = 0;\n for (const u of units) {\n total_computed += u.points ?? 0;\n total_computed += u.enhancement_points ?? 0;\n }\n\n return {\n name: header.name,\n generated_by: null,\n faction_raw_name: header.faction_raw_name,\n detachment_raw_name: header.detachment_raw_name,\n battle_size_raw: header.battle_size_raw,\n declared_limit: header.declared_limit,\n total_reported: header.total_reported,\n total_computed,\n units,\n multi_force,\n };\n },\n};\n\n// Internals re-exported for unit tests.\nexport const _internals = {\n isGwText,\n parseHeader,\n parseBody,\n};\n"]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Orchestrates an army-list import: decode → parse → resolve.
3
+ *
4
+ * The adapter seam ({@link FormatAdapter}) lets every supported source format
5
+ * plug in here without touching {@link decode} or {@link resolve}. Adapters are
6
+ * registered in priority order, and every adapter's `matches()` predicate is
7
+ * tight enough that **at most one** matches any given decoded payload —
8
+ * {@link tryImportRoster} relies on that disjointness to short-circuit on the
9
+ * first match.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ import { Dataset } from "../data/dataset.js";
14
+ import type { FormatAdapter } from "./adapter.js";
15
+ import type { Roster, RosterFormat } from "./types.js";
16
+ export interface ImportOptions {
17
+ /** Dataset to resolve against. Defaults to the package's embedded dataset. */
18
+ dataset?: Dataset;
19
+ }
20
+ /**
21
+ * Import a ListForge share payload into a resolved 40kdc {@link Roster}.
22
+ *
23
+ * `input` may be a full ListForge URL, a bare base64 segment, or an
24
+ * already-decoded JSON string — all are handled transparently. For NewRecruit
25
+ * sources, use {@link importNewRecruit} (no base64/gzip decode).
26
+ */
27
+ export declare function importListForge(input: string, opts?: ImportOptions): Roster;
28
+ /**
29
+ * Import a NewRecruit export (any of the four formats — JSON, wtc-compact,
30
+ * wtc-full, simple) into a resolved 40kdc {@link Roster}.
31
+ *
32
+ * The JSON form is parsed when `input` is valid JSON; the text forms are
33
+ * dispatched on string content. No base64/gzip decoding is attempted —
34
+ * NewRecruit exports are not encoded.
35
+ */
36
+ export declare function importNewRecruit(input: string, opts?: ImportOptions): Roster;
37
+ export declare function importRoster(decoded: unknown, opts?: ImportOptions): Roster;
38
+ /** Why a {@link tryImportRoster} call did not produce a roster. */
39
+ export type ImportFailureReason = "empty-input" | "decode-failed" | "no-adapter-matched" | "parse-failed";
40
+ /** Per-adapter outcome from a {@link tryImportRoster} dispatch. */
41
+ export interface AdapterTrial {
42
+ id: RosterFormat;
43
+ /** True iff this adapter's `matches()` predicate accepted the decoded input. */
44
+ matched: boolean;
45
+ /** Present when {@link matched} is true and `parse()` then threw — the matcher
46
+ * violated its contract. Absent for clean rejections. */
47
+ reason?: string;
48
+ }
49
+ /** Discriminated result returned by {@link tryImportRoster}. */
50
+ export type ImportResult = {
51
+ ok: true;
52
+ roster: Roster;
53
+ format: RosterFormat;
54
+ } | {
55
+ ok: false;
56
+ reason: ImportFailureReason;
57
+ message: string;
58
+ trials: AdapterTrial[];
59
+ };
60
+ /**
61
+ * Auto-detect and import any supported roster format from a single string.
62
+ *
63
+ * Pipeline:
64
+ * 1. Empty input → `empty-input`.
65
+ * 2. Looks like a ListForge URL / base64 payload → decode (base64 + gunzip + JSON.parse).
66
+ * 3. Looks like raw JSON (starts with `{`/`[`) → JSON.parse.
67
+ * 4. Otherwise treat as text.
68
+ * 5. Greedy first-match adapter dispatch. The first adapter whose `matches()`
69
+ * accepts the decoded value wins; subsequent adapters are not tried.
70
+ * 6. If the matched adapter's `parse()` throws, that's a matcher contract
71
+ * violation — surfaced as `parse-failed`, not silently retried.
72
+ *
73
+ * Caller never sees an exception; the discriminated {@link ImportResult} carries
74
+ * either the resolved {@link Roster} (with the detected {@link RosterFormat})
75
+ * or a typed failure plus per-adapter trial info for diagnostics.
76
+ *
77
+ * Prefer this over {@link importListForge} / {@link importNewRecruit} when the
78
+ * caller doesn't know which format the user pasted.
79
+ */
80
+ export declare function tryImportRoster(input: string, opts?: ImportOptions): ImportResult;
81
+ /** The adapter list, exposed for tests that need to walk every matcher (e.g.
82
+ * the disjointness invariant test). */
83
+ export declare const REGISTERED_ADAPTERS: readonly FormatAdapter[];
84
+ //# sourceMappingURL=import-roster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-roster.d.ts","sourceRoot":"","sources":["../../src/import/import-roster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAalD,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA2BvD,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAG/E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAUhF;AAyBD,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,GAAE,aAAkB,GAAG,MAAM,CAM/E;AAMD,mEAAmE;AACnE,MAAM,MAAM,mBAAmB,GAC3B,aAAa,GACb,eAAe,GACf,oBAAoB,GACpB,cAAc,CAAC;AAEnB,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,YAAY,CAAC;IACjB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;IACjB;6DACyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,gEAAgE;AAChE,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAClD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,mBAAmB,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB,CAAC;AAWN;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,aAAkB,GACvB,YAAY,CA+Dd;AAED;uCACuC;AACvC,eAAO,MAAM,mBAAmB,EAAE,SAAS,aAAa,EAAa,CAAC"}