@alpaca-software/40kdc-data 0.1.0 → 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 (385) 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 +12 -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 +10 -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 +7 -0
  22. package/dist/commands/import.d.ts.map +1 -0
  23. package/dist/commands/import.js +103 -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 +2 -0
  294. package/dist/gen-conformance.d.ts.map +1 -0
  295. package/dist/gen-conformance.js +131 -0
  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 +27 -0
  302. package/dist/import/adapter.d.ts.map +1 -0
  303. package/dist/import/adapter.js +10 -0
  304. package/dist/import/adapter.js.map +1 -0
  305. package/dist/import/decode.d.ts +7 -0
  306. package/dist/import/decode.d.ts.map +1 -0
  307. package/dist/import/decode.js +73 -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 +22 -0
  314. package/dist/import/index.d.ts.map +1 -0
  315. package/dist/import/index.js +19 -0
  316. package/dist/import/index.js.map +1 -0
  317. package/dist/import/listforge.d.ts +24 -0
  318. package/dist/import/listforge.d.ts.map +1 -0
  319. package/dist/import/listforge.js +201 -0
  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 +20 -0
  338. package/dist/import/resolve.d.ts.map +1 -0
  339. package/dist/import/resolve.js +190 -0
  340. package/dist/import/resolve.js.map +1 -0
  341. package/dist/import/types.d.ts +153 -0
  342. package/dist/import/types.d.ts.map +1 -0
  343. package/dist/import/types.js +20 -0
  344. package/dist/import/types.js.map +1 -0
  345. package/dist/index.d.ts +6 -0
  346. package/dist/index.d.ts.map +1 -0
  347. package/dist/index.js +7 -0
  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 +8 -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
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Decode a ListForge share payload into a JSON object.
3
+ *
4
+ * ListForge packs a roster as `base64( gzip( utf8(json) ) )` and embeds it in a
5
+ * URL hash fragment: `https://app/#/listforge/<BASE64>`. The fragment is used
6
+ * deliberately so browsers never send it to a server, preserving the payload
7
+ * verbatim. A valid gzipped payload always base64-encodes to a string starting
8
+ * with `H4sIAAAAAAAAA`.
9
+ *
10
+ * {@link decodeListForge} accepts any of three forms and returns the parsed JSON:
11
+ * - a full URL (the segment after the last `/` is taken),
12
+ * - a bare base64 segment,
13
+ * - an already-decoded JSON string (passed straight to `JSON.parse`).
14
+ *
15
+ * Only `node:zlib` is used — no third-party dependency.
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ import { gunzipSync } from "node:zlib";
20
+ /** The base64 prefix every ListForge gzip payload begins with. */
21
+ const GZIP_BASE64_PREFIX = "H4sIA";
22
+ /** The path marker ListForge uses ahead of the payload. */
23
+ const LISTFORGE_MARKER = "/listforge/";
24
+ /**
25
+ * Extract the payload segment from an input that may be a URL.
26
+ *
27
+ * The base64 alphabet includes `/`, so a bare base64 segment cannot be split on
28
+ * `/`. We only treat the input as a URL when it carries the `/listforge/` marker
29
+ * or an `http(s)://` scheme; otherwise it is returned unchanged.
30
+ */
31
+ function extractSegment(input) {
32
+ const markerIndex = input.indexOf(LISTFORGE_MARKER);
33
+ if (markerIndex !== -1) {
34
+ return input.slice(markerIndex + LISTFORGE_MARKER.length);
35
+ }
36
+ if (/^https?:\/\//i.test(input)) {
37
+ const lastSlash = input.lastIndexOf("/");
38
+ return lastSlash === -1 ? input : input.slice(lastSlash + 1);
39
+ }
40
+ return input;
41
+ }
42
+ /**
43
+ * Decode a ListForge payload (URL, bare base64, or raw JSON) into a JSON value.
44
+ *
45
+ * @throws if the input is neither valid JSON nor a decodable gzip payload.
46
+ */
47
+ export function decodeListForge(input) {
48
+ const trimmed = input.trim();
49
+ if (trimmed === "") {
50
+ throw new Error("decodeListForge: empty input");
51
+ }
52
+ // Raw JSON object passed directly.
53
+ if (trimmed.startsWith("{")) {
54
+ return JSON.parse(trimmed);
55
+ }
56
+ const segment = extractSegment(trimmed);
57
+ if (!segment.startsWith(GZIP_BASE64_PREFIX)) {
58
+ throw new Error("decodeListForge: input is not a ListForge payload (expected raw JSON, " +
59
+ `or a gzip+base64 segment beginning with "${GZIP_BASE64_PREFIX}…")`);
60
+ }
61
+ let json;
62
+ try {
63
+ const bytes = Buffer.from(segment, "base64");
64
+ json = gunzipSync(bytes).toString("utf8");
65
+ }
66
+ catch (cause) {
67
+ throw new Error("decodeListForge: failed to gunzip base64 payload", {
68
+ cause,
69
+ });
70
+ }
71
+ return JSON.parse(json);
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,35 @@
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 — NewRecruit's tighter matchers run first so
7
+ * the ListForge fallback only catches generic BattleScribe JSON.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import { Dataset } from "../data/dataset.js";
12
+ import type { Roster } from "./types.js";
13
+ export interface ImportOptions {
14
+ /** Dataset to resolve against. Defaults to the package's embedded dataset. */
15
+ dataset?: Dataset;
16
+ }
17
+ /**
18
+ * Import a ListForge share payload into a resolved 40kdc {@link Roster}.
19
+ *
20
+ * `input` may be a full ListForge URL, a bare base64 segment, or an
21
+ * already-decoded JSON string — all are handled transparently. For NewRecruit
22
+ * sources, use {@link importNewRecruit} (no base64/gzip decode).
23
+ */
24
+ export declare function importListForge(input: string, opts?: ImportOptions): Roster;
25
+ /**
26
+ * Import a NewRecruit export (any of the four formats — JSON, wtc-compact,
27
+ * wtc-full, simple) into a resolved 40kdc {@link Roster}.
28
+ *
29
+ * The JSON form is parsed when `input` is valid JSON; the text forms are
30
+ * dispatched on string content. No base64/gzip decoding is attempted —
31
+ * NewRecruit exports are not encoded.
32
+ */
33
+ export declare function importNewRecruit(input: string, opts?: ImportOptions): Roster;
34
+ export declare function importRoster(decoded: unknown, opts?: ImportOptions): Roster;
35
+ //# 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;;;;;;;;;GASG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAY7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAqBzC,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"}
@@ -0,0 +1,97 @@
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 — NewRecruit's tighter matchers run first so
7
+ * the ListForge fallback only catches generic BattleScribe JSON.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import { Dataset } from "../data/dataset.js";
12
+ import { selectAdapter } from "./adapter.js";
13
+ import { decodeListForge } from "./decode.js";
14
+ import { listForgeAdapter } from "./listforge.js";
15
+ import { newRecruitJsonAdapter } from "./newrecruit-json.js";
16
+ import { newRecruitSimpleAdapter } from "./newrecruit-simple.js";
17
+ import { newRecruitWtcCompactAdapter, newRecruitWtcFullAdapter, } from "./newrecruit-wtc.js";
18
+ import { resolve } from "./resolve.js";
19
+ /**
20
+ * Adapters available to {@link importRoster}, in match-priority order.
21
+ *
22
+ * NewRecruit-JSON runs ahead of ListForge because both recognise a
23
+ * `roster.forces` BattleScribe payload, and the NewRecruit signature is more
24
+ * specific (`xmlns: rosterSchema` or `generatedBy: newrecruit.eu`). The text
25
+ * adapters (`wtc-compact` / `wtc-full` / `simple`) only match strings and
26
+ * disambiguate among themselves via structural cues, so their order amongst
27
+ * each other doesn't matter; wtc-full goes before wtc-compact because its
28
+ * matcher is the more specific of the two.
29
+ */
30
+ const ADAPTERS = [
31
+ newRecruitJsonAdapter,
32
+ newRecruitWtcFullAdapter,
33
+ newRecruitWtcCompactAdapter,
34
+ newRecruitSimpleAdapter,
35
+ listForgeAdapter,
36
+ ];
37
+ /**
38
+ * Import a ListForge share payload into a resolved 40kdc {@link Roster}.
39
+ *
40
+ * `input` may be a full ListForge URL, a bare base64 segment, or an
41
+ * already-decoded JSON string — all are handled transparently. For NewRecruit
42
+ * sources, use {@link importNewRecruit} (no base64/gzip decode).
43
+ */
44
+ export function importListForge(input, opts = {}) {
45
+ const decoded = decodeListForge(input);
46
+ return importRoster(decoded, opts);
47
+ }
48
+ /**
49
+ * Import a NewRecruit export (any of the four formats — JSON, wtc-compact,
50
+ * wtc-full, simple) into a resolved 40kdc {@link Roster}.
51
+ *
52
+ * The JSON form is parsed when `input` is valid JSON; the text forms are
53
+ * dispatched on string content. No base64/gzip decoding is attempted —
54
+ * NewRecruit exports are not encoded.
55
+ */
56
+ export function importNewRecruit(input, opts = {}) {
57
+ const trimmed = input.trimStart();
58
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
59
+ try {
60
+ return importRoster(JSON.parse(input), opts);
61
+ }
62
+ catch {
63
+ // Fall through to treating the input as raw text.
64
+ }
65
+ }
66
+ return importRoster(input, opts);
67
+ }
68
+ /**
69
+ * Import an already-decoded payload. Selects the matching format adapter and
70
+ * resolves the result against the dataset. Accepts either a parsed JSON object
71
+ * (NewRecruit JSON / ListForge) or a string (the three NewRecruit text formats).
72
+ */
73
+ /**
74
+ * Detect an already-resolved canonical {@link Roster} (the JSON shape produced
75
+ * by `rosterJsonSerializer`). Lets a downstream consumer round-trip canonical
76
+ * Roster JSON through `importRoster` without going through an adapter.
77
+ */
78
+ function isCanonicalRoster(decoded) {
79
+ if (typeof decoded !== "object" || decoded === null)
80
+ return false;
81
+ const r = decoded;
82
+ const source = r.source;
83
+ return (typeof source === "object" &&
84
+ source !== null &&
85
+ typeof source.format === "string" &&
86
+ Array.isArray(r.units) &&
87
+ "diagnostics" in r);
88
+ }
89
+ export function importRoster(decoded, opts = {}) {
90
+ if (isCanonicalRoster(decoded))
91
+ return decoded;
92
+ const ds = opts.dataset ?? Dataset.embedded();
93
+ const adapter = selectAdapter(decoded, [...ADAPTERS]);
94
+ const parsed = adapter.parse(decoded);
95
+ return resolve(parsed, ds, adapter.id);
96
+ }
97
+ //# sourceMappingURL=import-roster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-roster.js","sourceRoot":"","sources":["../../src/import/import-roster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC;;;;;;;;;;GAUG;AACH,MAAM,QAAQ,GAA6B;IACzC,qBAAqB;IACrB,wBAAwB;IACxB,2BAA2B;IAC3B,uBAAuB;IACvB,gBAAgB;CACjB,CAAC;AAOF;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,OAAsB,EAAE;IACrE,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACvC,OAAO,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,OAAsB,EAAE;IACtE,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;IAClC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC;YACP,kDAAkD;QACpD,CAAC;IACH,CAAC;IACD,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACnC,CAAC;AAED;;;;GAIG;AACH;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,OAAgB;IACzC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAClE,MAAM,CAAC,GAAG,OAAkC,CAAC;IAC7C,MAAM,MAAM,GAAG,CAAC,CAAC,MAA6C,CAAC;IAC/D,OAAO,CACL,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,KAAK,IAAI;QACf,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;QACjC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QACtB,aAAa,IAAI,CAAC,CACnB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAgB,EAAE,OAAsB,EAAE;IACrE,IAAI,iBAAiB,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC/C,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAC9C,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,OAAO,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;AACzC,CAAC","sourcesContent":["/**\n * Orchestrates an army-list import: decode → parse → resolve.\n *\n * The adapter seam ({@link FormatAdapter}) lets every supported source format\n * plug in here without touching {@link decode} or {@link resolve}. Adapters are\n * registered in priority order — NewRecruit's tighter matchers run first so\n * the ListForge fallback only catches generic BattleScribe JSON.\n *\n * @packageDocumentation\n */\nimport { Dataset } from \"../data/dataset.js\";\nimport type { FormatAdapter } from \"./adapter.js\";\nimport { selectAdapter } from \"./adapter.js\";\nimport { decodeListForge } from \"./decode.js\";\nimport { listForgeAdapter } from \"./listforge.js\";\nimport { newRecruitJsonAdapter } from \"./newrecruit-json.js\";\nimport { newRecruitSimpleAdapter } from \"./newrecruit-simple.js\";\nimport {\n newRecruitWtcCompactAdapter,\n newRecruitWtcFullAdapter,\n} from \"./newrecruit-wtc.js\";\nimport { resolve } from \"./resolve.js\";\nimport type { Roster } from \"./types.js\";\n\n/**\n * Adapters available to {@link importRoster}, in match-priority order.\n *\n * NewRecruit-JSON runs ahead of ListForge because both recognise a\n * `roster.forces` BattleScribe payload, and the NewRecruit signature is more\n * specific (`xmlns: rosterSchema` or `generatedBy: newrecruit.eu`). The text\n * adapters (`wtc-compact` / `wtc-full` / `simple`) only match strings and\n * disambiguate among themselves via structural cues, so their order amongst\n * each other doesn't matter; wtc-full goes before wtc-compact because its\n * matcher is the more specific of the two.\n */\nconst ADAPTERS: readonly FormatAdapter[] = [\n newRecruitJsonAdapter,\n newRecruitWtcFullAdapter,\n newRecruitWtcCompactAdapter,\n newRecruitSimpleAdapter,\n listForgeAdapter,\n];\n\nexport interface ImportOptions {\n /** Dataset to resolve against. Defaults to the package's embedded dataset. */\n dataset?: Dataset;\n}\n\n/**\n * Import a ListForge share payload into a resolved 40kdc {@link Roster}.\n *\n * `input` may be a full ListForge URL, a bare base64 segment, or an\n * already-decoded JSON string — all are handled transparently. For NewRecruit\n * sources, use {@link importNewRecruit} (no base64/gzip decode).\n */\nexport function importListForge(input: string, opts: ImportOptions = {}): Roster {\n const decoded = decodeListForge(input);\n return importRoster(decoded, opts);\n}\n\n/**\n * Import a NewRecruit export (any of the four formats — JSON, wtc-compact,\n * wtc-full, simple) into a resolved 40kdc {@link Roster}.\n *\n * The JSON form is parsed when `input` is valid JSON; the text forms are\n * dispatched on string content. No base64/gzip decoding is attempted —\n * NewRecruit exports are not encoded.\n */\nexport function importNewRecruit(input: string, opts: ImportOptions = {}): Roster {\n const trimmed = input.trimStart();\n if (trimmed.startsWith(\"{\") || trimmed.startsWith(\"[\")) {\n try {\n return importRoster(JSON.parse(input), opts);\n } catch {\n // Fall through to treating the input as raw text.\n }\n }\n return importRoster(input, opts);\n}\n\n/**\n * Import an already-decoded payload. Selects the matching format adapter and\n * resolves the result against the dataset. Accepts either a parsed JSON object\n * (NewRecruit JSON / ListForge) or a string (the three NewRecruit text formats).\n */\n/**\n * Detect an already-resolved canonical {@link Roster} (the JSON shape produced\n * by `rosterJsonSerializer`). Lets a downstream consumer round-trip canonical\n * Roster JSON through `importRoster` without going through an adapter.\n */\nfunction isCanonicalRoster(decoded: unknown): decoded is Roster {\n if (typeof decoded !== \"object\" || decoded === null) return false;\n const r = decoded as Record<string, unknown>;\n const source = r.source as Record<string, unknown> | undefined;\n return (\n typeof source === \"object\" &&\n source !== null &&\n typeof source.format === \"string\" &&\n Array.isArray(r.units) &&\n \"diagnostics\" in r\n );\n}\n\nexport function importRoster(decoded: unknown, opts: ImportOptions = {}): Roster {\n if (isCanonicalRoster(decoded)) return decoded;\n const ds = opts.dataset ?? Dataset.embedded();\n const adapter = selectAdapter(decoded, [...ADAPTERS]);\n const parsed = adapter.parse(decoded);\n return resolve(parsed, ds, adapter.id);\n}\n"]}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Army-list importer: turn an external list-builder export into a resolved
3
+ * 40kdc roster.
4
+ *
5
+ * v1 supports ListForge's "share JSON" payload. The output is a {@link Roster}
6
+ * keyed on 40kdc entity ids and validatable against
7
+ * `schemas/core/roster.schema.json`. Resolution is lenient — unmatched names are
8
+ * retained with candidate suggestions and summarised in diagnostics.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export { importListForge, importNewRecruit, importRoster } from "./import-roster.js";
13
+ export type { ImportOptions } from "./import-roster.js";
14
+ export { decodeListForge } from "./decode.js";
15
+ export { resolve } from "./resolve.js";
16
+ export { listForgeAdapter } from "./listforge.js";
17
+ export { newRecruitJsonAdapter } from "./newrecruit-json.js";
18
+ export { newRecruitSimpleAdapter } from "./newrecruit-simple.js";
19
+ export { newRecruitWtcCompactAdapter, newRecruitWtcFullAdapter, } from "./newrecruit-wtc.js";
20
+ export type { FormatAdapter } from "./adapter.js";
21
+ export type { Roster, RosterUnit, RosterWargear, RosterSource, RosterFormat, RosterPoints, ResolvedRef, Candidate, RosterLeaderAttachment, Diagnostics, Warning, WarningCode, BattleSize, GameVersionRef, ParsedRoster, ParsedUnit, ParsedWargear, } from "./types.js";
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/import/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrF,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EACV,MAAM,EACN,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,SAAS,EACT,sBAAsB,EACtB,WAAW,EACX,OAAO,EACP,WAAW,EACX,UAAU,EACV,cAAc,EACd,YAAY,EACZ,UAAU,EACV,aAAa,GACd,MAAM,YAAY,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Army-list importer: turn an external list-builder export into a resolved
3
+ * 40kdc roster.
4
+ *
5
+ * v1 supports ListForge's "share JSON" payload. The output is a {@link Roster}
6
+ * keyed on 40kdc entity ids and validatable against
7
+ * `schemas/core/roster.schema.json`. Resolution is lenient — unmatched names are
8
+ * retained with candidate suggestions and summarised in diagnostics.
9
+ *
10
+ * @packageDocumentation
11
+ */
12
+ export { importListForge, importNewRecruit, importRoster } from "./import-roster.js";
13
+ export { decodeListForge } from "./decode.js";
14
+ export { resolve } from "./resolve.js";
15
+ export { listForgeAdapter } from "./listforge.js";
16
+ export { newRecruitJsonAdapter } from "./newrecruit-json.js";
17
+ export { newRecruitSimpleAdapter } from "./newrecruit-simple.js";
18
+ export { newRecruitWtcCompactAdapter, newRecruitWtcFullAdapter, } from "./newrecruit-wtc.js";
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/import/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAErF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,2BAA2B,EAC3B,wBAAwB,GACzB,MAAM,qBAAqB,CAAC","sourcesContent":["/**\n * Army-list importer: turn an external list-builder export into a resolved\n * 40kdc roster.\n *\n * v1 supports ListForge's \"share JSON\" payload. The output is a {@link Roster}\n * keyed on 40kdc entity ids and validatable against\n * `schemas/core/roster.schema.json`. Resolution is lenient — unmatched names are\n * retained with candidate suggestions and summarised in diagnostics.\n *\n * @packageDocumentation\n */\nexport { importListForge, importNewRecruit, importRoster } from \"./import-roster.js\";\nexport type { ImportOptions } from \"./import-roster.js\";\nexport { decodeListForge } from \"./decode.js\";\nexport { resolve } from \"./resolve.js\";\nexport { listForgeAdapter } from \"./listforge.js\";\nexport { newRecruitJsonAdapter } from \"./newrecruit-json.js\";\nexport { newRecruitSimpleAdapter } from \"./newrecruit-simple.js\";\nexport {\n newRecruitWtcCompactAdapter,\n newRecruitWtcFullAdapter,\n} from \"./newrecruit-wtc.js\";\nexport type { FormatAdapter } from \"./adapter.js\";\nexport type {\n Roster,\n RosterUnit,\n RosterWargear,\n RosterSource,\n RosterFormat,\n RosterPoints,\n ResolvedRef,\n Candidate,\n RosterLeaderAttachment,\n Diagnostics,\n Warning,\n WarningCode,\n BattleSize,\n GameVersionRef,\n ParsedRoster,\n ParsedUnit,\n ParsedWargear,\n} from \"./types.js\";\n"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ListForge adapter: lower a decoded ListForge "share JSON" payload (a
3
+ * BattleScribe-derived roster tree) to a {@link ParsedRoster}.
4
+ *
5
+ * The walk reads an ALLOWLIST of fields only — `name`, `number`, `type`,
6
+ * `categories[].name`, `group`, and `costs` point values — and never touches
7
+ * `rules[].description` or ability `profiles[].characteristics[].$text`, which
8
+ * carry reproduced rules text. This keeps the importer's output free of
9
+ * copyrighted prose by construction.
10
+ *
11
+ * Selection-tree shape (recursive `selections`):
12
+ * - Configuration nodes (`type: "upgrade"`) named "Detachment" / "Battle Size"
13
+ * carry the chosen value as their first child selection.
14
+ * - Unit nodes (`type: "model" | "unit"`) carry role categories, a points cost,
15
+ * and — nested anywhere beneath them — their wargear (weapon-category
16
+ * selections), enhancement (a selection whose `group` starts "Enhancements"),
17
+ * the "Warlord" marker, and model sub-selections.
18
+ * - Every unit carries a `"Faction: <Name>"` category.
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ import type { FormatAdapter } from "./adapter.js";
23
+ export declare const listForgeAdapter: FormatAdapter;
24
+ //# sourceMappingURL=listforge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listforge.d.ts","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAmMlD,eAAO,MAAM,gBAAgB,EAAE,aA0D9B,CAAC"}
@@ -0,0 +1,201 @@
1
+ const PTS_COST_NAME = "pts";
2
+ const FACTION_CATEGORY = /^Faction:\s*(.+)$/;
3
+ const POINTS_LIMIT = /(\d[\d,]*)\s*Point/i;
4
+ const ENHANCEMENT_GROUP_PREFIX = "Enhancements";
5
+ const CHARACTER_CATEGORIES = new Set(["Character", "Epic Hero"]);
6
+ const WEAPON_CATEGORY_SUFFIX = " Weapon"; // "Ranged Weapon", "Melee Weapon", "Psychic Weapon"
7
+ function asArray(value) {
8
+ return Array.isArray(value) ? value : [];
9
+ }
10
+ function asString(value) {
11
+ return typeof value === "string" ? value : null;
12
+ }
13
+ function selectionName(sel) {
14
+ return asString(sel.name) ?? "";
15
+ }
16
+ function selectionType(sel) {
17
+ return asString(sel.type) ?? "";
18
+ }
19
+ /** A selection's multiplicity (`number`), defaulting to 1. */
20
+ function selectionCount(sel) {
21
+ return typeof sel.number === "number" && sel.number > 0 ? sel.number : 1;
22
+ }
23
+ /** Point value from a selection's cost block, or null when absent. */
24
+ function pointsOf(sel) {
25
+ for (const raw of asArray(sel.costs)) {
26
+ const cost = raw;
27
+ if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === "number") {
28
+ return cost.value;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function categoryNames(sel) {
34
+ return asArray(sel.categories)
35
+ .map((c) => asString(c.name))
36
+ .filter((n) => n !== null);
37
+ }
38
+ function childSelections(sel) {
39
+ return asArray(sel.selections);
40
+ }
41
+ /** Depth-first visit of a selection and everything beneath it. */
42
+ function walk(sel, visit) {
43
+ visit(sel);
44
+ for (const child of childSelections(sel))
45
+ walk(child, visit);
46
+ }
47
+ function isUnitSelection(sel) {
48
+ const type = selectionType(sel);
49
+ return type === "model" || type === "unit";
50
+ }
51
+ function isCharacter(sel) {
52
+ return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));
53
+ }
54
+ function isWeaponSelection(sel) {
55
+ return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));
56
+ }
57
+ function isEnhancementSelection(sel) {
58
+ const group = asString(sel.group);
59
+ return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);
60
+ }
61
+ /** Sum the model count of a unit from its nested model selections. */
62
+ function modelCount(unit) {
63
+ let total = 0;
64
+ walk(unit, (s) => {
65
+ if (selectionType(s) === "model")
66
+ total += selectionCount(s);
67
+ });
68
+ return total > 0 ? total : selectionCount(unit);
69
+ }
70
+ /** Build a parsed unit from a top-level unit selection. */
71
+ function parseUnit(unit) {
72
+ const wargear = [];
73
+ let enhancement_raw_name = null;
74
+ let enhancement_points = null;
75
+ let is_warlord = false;
76
+ for (const node of childSelections(unit)) {
77
+ walk(node, (s) => {
78
+ if (isEnhancementSelection(s)) {
79
+ if (enhancement_raw_name === null) {
80
+ enhancement_raw_name = selectionName(s);
81
+ enhancement_points = pointsOf(s);
82
+ }
83
+ return;
84
+ }
85
+ if (selectionName(s) === "Warlord") {
86
+ is_warlord = true;
87
+ return;
88
+ }
89
+ if (isWeaponSelection(s)) {
90
+ wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });
91
+ }
92
+ });
93
+ }
94
+ return {
95
+ raw_name: selectionName(unit),
96
+ is_character: isCharacter(unit),
97
+ model_count: modelCount(unit),
98
+ points: pointsOf(unit),
99
+ is_warlord,
100
+ enhancement_raw_name,
101
+ enhancement_points,
102
+ wargear,
103
+ };
104
+ }
105
+ /** Value carried as the first child of a named configuration selection. */
106
+ function configValue(selections, configName) {
107
+ const node = selections.find((s) => selectionName(s) === configName);
108
+ if (!node)
109
+ return null;
110
+ const child = childSelections(node)[0];
111
+ return child ? selectionName(child) : null;
112
+ }
113
+ function parseLimit(label) {
114
+ if (!label)
115
+ return null;
116
+ const match = POINTS_LIMIT.exec(label);
117
+ if (!match)
118
+ return null;
119
+ return Number.parseInt(match[1].replace(/,/g, ""), 10);
120
+ }
121
+ /** First `"Faction: X"` category found anywhere; reports all distinct names. */
122
+ function collectFactions(forces) {
123
+ const seen = new Set();
124
+ for (const force of forces) {
125
+ for (const sel of childSelections(force)) {
126
+ walk(sel, (s) => {
127
+ for (const name of categoryNames(s)) {
128
+ const match = FACTION_CATEGORY.exec(name);
129
+ if (match)
130
+ seen.add(match[1].trim());
131
+ }
132
+ });
133
+ }
134
+ }
135
+ return [...seen];
136
+ }
137
+ function rosterOf(decoded) {
138
+ if (!decoded || typeof decoded !== "object")
139
+ return null;
140
+ const roster = decoded.roster;
141
+ if (!roster || typeof roster !== "object")
142
+ return null;
143
+ if (!Array.isArray(roster.forces))
144
+ return null;
145
+ return roster;
146
+ }
147
+ export const listForgeAdapter = {
148
+ id: "listforge",
149
+ matches(decoded) {
150
+ return rosterOf(decoded) !== null;
151
+ },
152
+ parse(decoded) {
153
+ const payload = decoded;
154
+ const roster = rosterOf(decoded);
155
+ if (!roster) {
156
+ throw new Error("listforge: payload has no roster.forces array");
157
+ }
158
+ const forces = asArray(roster.forces);
159
+ // Configuration lives among each force's top-level selections.
160
+ let detachment_raw_name = null;
161
+ let battle_size_raw = null;
162
+ const units = [];
163
+ for (const force of forces) {
164
+ const top = childSelections(force);
165
+ detachment_raw_name ??= configValue(top, "Detachment");
166
+ battle_size_raw ??= configValue(top, "Battle Size");
167
+ for (const sel of top) {
168
+ if (isUnitSelection(sel))
169
+ units.push(parseUnit(sel));
170
+ }
171
+ }
172
+ const factions = collectFactions(forces);
173
+ const total_reported = pointsOf(roster);
174
+ // Honest computed total: sum every cost line in the tree. A unit's own cost
175
+ // and its nested enhancement's cost are distinct lines that together make up
176
+ // the unit's army contribution, so a full walk reproduces the army total.
177
+ let total_computed = 0;
178
+ for (const force of forces) {
179
+ for (const sel of childSelections(force)) {
180
+ walk(sel, (s) => {
181
+ const pts = pointsOf(s);
182
+ if (pts)
183
+ total_computed += pts;
184
+ });
185
+ }
186
+ }
187
+ return {
188
+ name: asString(payload.name) ?? asString(roster.name) ?? "Imported roster",
189
+ generated_by: asString(payload.generatedBy),
190
+ faction_raw_name: factions[0] ?? null,
191
+ detachment_raw_name,
192
+ battle_size_raw,
193
+ declared_limit: parseLimit(battle_size_raw),
194
+ total_reported,
195
+ total_computed,
196
+ units,
197
+ multi_force: factions.length > 1,
198
+ };
199
+ },
200
+ };
201
+ //# sourceMappingURL=listforge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listforge.js","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ListForge adapter: lower a decoded ListForge \"share JSON\" payload (a\n * BattleScribe-derived roster tree) to a {@link ParsedRoster}.\n *\n * The walk reads an ALLOWLIST of fields only — `name`, `number`, `type`,\n * `categories[].name`, `group`, and `costs` point values — and never touches\n * `rules[].description` or ability `profiles[].characteristics[].$text`, which\n * carry reproduced rules text. This keeps the importer's output free of\n * copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`):\n * - Configuration nodes (`type: \"upgrade\"`) named \"Detachment\" / \"Battle Size\"\n * carry the chosen value as their first child selection.\n * - Unit nodes (`type: \"model\" | \"unit\"`) carry role categories, a points cost,\n * and — nested anywhere beneath them — their wargear (weapon-category\n * selections), enhancement (a selection whose `group` starts \"Enhancements\"),\n * the \"Warlord\" marker, and model sub-selections.\n * - Every unit carries a `\"Faction: <Name>\"` category.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\nconst PTS_COST_NAME = \"pts\";\nconst FACTION_CATEGORY = /^Faction:\\s*(.+)$/;\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\nconst ENHANCEMENT_GROUP_PREFIX = \"Enhancements\";\nconst CHARACTER_CATEGORIES = new Set([\"Character\", \"Epic Hero\"]);\nconst WEAPON_CATEGORY_SUFFIX = \" Weapon\"; // \"Ranged Weapon\", \"Melee Weapon\", \"Psychic Weapon\"\n\n// --- Minimal structural views of the parts of the payload we read. ----------\n\ninterface RawCategory {\n name?: unknown;\n}\ninterface RawCost {\n name?: unknown;\n value?: unknown;\n}\ninterface RawSelection {\n name?: unknown;\n type?: unknown;\n number?: unknown;\n group?: unknown;\n categories?: unknown;\n costs?: unknown;\n selections?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction selectionName(sel: RawSelection): string {\n return asString(sel.name) ?? \"\";\n}\n\nfunction selectionType(sel: RawSelection): string {\n return asString(sel.type) ?? \"\";\n}\n\n/** A selection's multiplicity (`number`), defaulting to 1. */\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\n/** Point value from a selection's cost block, or null when absent. */\nfunction pointsOf(sel: RawSelection): number | null {\n for (const raw of asArray(sel.costs)) {\n const cost = raw as RawCost;\n if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === \"number\") {\n return cost.value;\n }\n }\n return null;\n}\n\nfunction categoryNames(sel: RawSelection): string[] {\n return asArray(sel.categories)\n .map((c) => asString((c as RawCategory).name))\n .filter((n): n is string => n !== null);\n}\n\nfunction childSelections(sel: RawSelection): RawSelection[] {\n return asArray(sel.selections) as RawSelection[];\n}\n\n/** Depth-first visit of a selection and everything beneath it. */\nfunction walk(sel: RawSelection, visit: (s: RawSelection) => void): void {\n visit(sel);\n for (const child of childSelections(sel)) walk(child, visit);\n}\n\nfunction isUnitSelection(sel: RawSelection): boolean {\n const type = selectionType(sel);\n return type === \"model\" || type === \"unit\";\n}\n\nfunction isCharacter(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));\n}\n\nfunction isWeaponSelection(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));\n}\n\nfunction isEnhancementSelection(sel: RawSelection): boolean {\n const group = asString(sel.group);\n return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);\n}\n\n/** Sum the model count of a unit from its nested model selections. */\nfunction modelCount(unit: RawSelection): number {\n let total = 0;\n walk(unit, (s) => {\n if (selectionType(s) === \"model\") total += selectionCount(s);\n });\n return total > 0 ? total : selectionCount(unit);\n}\n\n/** Build a parsed unit from a top-level unit selection. */\nfunction parseUnit(unit: RawSelection): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const node of childSelections(unit)) {\n walk(node, (s) => {\n if (isEnhancementSelection(s)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = selectionName(s);\n enhancement_points = pointsOf(s);\n }\n return;\n }\n if (selectionName(s) === \"Warlord\") {\n is_warlord = true;\n return;\n }\n if (isWeaponSelection(s)) {\n wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });\n }\n });\n }\n\n return {\n raw_name: selectionName(unit),\n is_character: isCharacter(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\n/** Value carried as the first child of a named configuration selection. */\nfunction configValue(\n selections: RawSelection[],\n configName: string,\n): string | null {\n const node = selections.find((s) => selectionName(s) === configName);\n if (!node) return null;\n const child = childSelections(node)[0];\n return child ? selectionName(child) : null;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\n/** First `\"Faction: X\"` category found anywhere; reports all distinct names. */\nfunction collectFactions(forces: RawSelection[]): string[] {\n const seen = new Set<string>();\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n for (const name of categoryNames(s)) {\n const match = FACTION_CATEGORY.exec(name);\n if (match) seen.add(match[1].trim());\n }\n });\n }\n }\n return [...seen];\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n}\ninterface RawPayload {\n name?: unknown;\n generatedBy?: unknown;\n roster?: unknown;\n}\n\nfunction rosterOf(decoded: unknown): RawRoster | null {\n if (!decoded || typeof decoded !== \"object\") return null;\n const roster = (decoded as RawPayload).roster;\n if (!roster || typeof roster !== \"object\") return null;\n if (!Array.isArray((roster as RawRoster).forces)) return null;\n return roster as RawRoster;\n}\n\nexport const listForgeAdapter: FormatAdapter = {\n id: \"listforge\",\n\n matches(decoded: unknown): boolean {\n return rosterOf(decoded) !== null;\n },\n\n parse(decoded: unknown): ParsedRoster {\n const payload = decoded as RawPayload;\n const roster = rosterOf(decoded);\n if (!roster) {\n throw new Error(\"listforge: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n // Configuration lives among each force's top-level selections.\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n for (const force of forces) {\n const top = childSelections(force);\n detachment_raw_name ??= configValue(top, \"Detachment\");\n battle_size_raw ??= configValue(top, \"Battle Size\");\n for (const sel of top) {\n if (isUnitSelection(sel)) units.push(parseUnit(sel));\n }\n }\n\n const factions = collectFactions(forces);\n const total_reported = pointsOf(roster as RawSelection);\n\n // Honest computed total: sum every cost line in the tree. A unit's own cost\n // and its nested enhancement's cost are distinct lines that together make up\n // the unit's army contribution, so a full walk reproduces the army total.\n let total_computed = 0;\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n const pts = pointsOf(s);\n if (pts) total_computed += pts;\n });\n }\n }\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by: asString(payload.generatedBy),\n faction_raw_name: factions[0] ?? null,\n detachment_raw_name,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * NewRecruit JSON adapter: lower a decoded NewRecruit roster export (a
3
+ * BattleScribe-derived tree, same outer shape as ListForge) to a {@link ParsedRoster}.
4
+ *
5
+ * NewRecruit-specific signals used to detect the format:
6
+ * - `generatedBy` reports the NewRecruit URL ("https://newrecruit.eu"), and/or
7
+ * - `roster.xmlns` is set to the BattleScribe rosterSchema namespace.
8
+ *
9
+ * The primary faction surfaces in `forces[].catalogueName` (e.g.
10
+ * "Chaos - Chaos Knights") — we take the segment after the final " - ". Falls
11
+ * back to the first `"Faction: X"` category if no catalogueName is present.
12
+ *
13
+ * The walk reads the same ALLOWLIST as the ListForge adapter — `name`,
14
+ * `number`, `type`, `categories[].name`, `group`, `costs` point values, and
15
+ * `catalogueName`. `rules[].description`, ability `profiles[].characteristics[].$text`,
16
+ * and every other prose field are never touched, so the importer's output is
17
+ * free of copyrighted prose by construction.
18
+ *
19
+ * Selection-tree shape (recursive `selections`) is identical to ListForge:
20
+ * - Configuration nodes (`type: "upgrade"`) named "Detachment" / "Battle Size"
21
+ * carry the chosen value as their first child selection.
22
+ * - Unit nodes (`type: "model" | "unit"`) carry role categories, a points cost,
23
+ * and — nested anywhere beneath them — their wargear (weapon-category
24
+ * selections), enhancement (a selection whose `group` starts "Enhancements"),
25
+ * the "Warlord" marker, and model sub-selections.
26
+ *
27
+ * @packageDocumentation
28
+ */
29
+ import type { FormatAdapter } from "./adapter.js";
30
+ export declare const newRecruitJsonAdapter: FormatAdapter;
31
+ //# sourceMappingURL=newrecruit-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"newrecruit-json.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA4NlD,eAAO,MAAM,qBAAqB,EAAE,aA4DnC,CAAC"}