@alpaca-software/40kdc-data 0.5.11 → 0.5.12
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.
- package/README.md +10 -0
- package/dist/codegen-data.js +1 -0
- package/dist/codegen-data.js.map +1 -1
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +3 -1
- package/dist/commands/import.js.map +1 -1
- package/dist/convert-faction.d.ts.map +1 -1
- package/dist/convert-faction.js +22 -2
- package/dist/convert-faction.js.map +1 -1
- package/dist/data/bundle.generated.js +1 -1
- package/dist/data/bundle.generated.js.map +1 -1
- package/dist/data/dataset.d.ts +2 -1
- package/dist/data/dataset.d.ts.map +1 -1
- package/dist/data/dataset.js +2 -0
- package/dist/data/dataset.js.map +1 -1
- package/dist/data/entities.d.ts +22 -0
- package/dist/data/entities.d.ts.map +1 -1
- package/dist/data/entities.js +28 -0
- package/dist/data/entities.js.map +1 -1
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +2 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/types.d.ts +3 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/data/types.js +1 -0
- package/dist/data/types.js.map +1 -1
- package/dist/export/newrecruit-json.d.ts.map +1 -1
- package/dist/export/newrecruit-json.js +4 -3
- package/dist/export/newrecruit-json.js.map +1 -1
- package/dist/export/newrecruit-simple.js +2 -2
- package/dist/export/newrecruit-simple.js.map +1 -1
- package/dist/export/newrecruit-wtc.d.ts.map +1 -1
- package/dist/export/newrecruit-wtc.js +3 -1
- package/dist/export/newrecruit-wtc.js.map +1 -1
- package/dist/export/rosterizer.d.ts.map +1 -1
- package/dist/export/rosterizer.js +6 -7
- package/dist/export/rosterizer.js.map +1 -1
- package/dist/extract-faction-pack.d.ts +56 -0
- package/dist/extract-faction-pack.d.ts.map +1 -0
- package/dist/extract-faction-pack.js +374 -0
- package/dist/extract-faction-pack.js.map +1 -0
- package/dist/gen-conformance.js +37 -4
- package/dist/gen-conformance.js.map +1 -1
- package/dist/generated.d.ts +42 -2
- package/dist/generated.d.ts.map +1 -1
- package/dist/generated.js.map +1 -1
- package/dist/import/gw.js +1 -1
- package/dist/import/gw.js.map +1 -1
- package/dist/import/listforge-text.js +1 -1
- package/dist/import/listforge-text.js.map +1 -1
- package/dist/import/listforge.d.ts.map +1 -1
- package/dist/import/listforge.js +18 -3
- package/dist/import/listforge.js.map +1 -1
- package/dist/import/newrecruit-json.d.ts.map +1 -1
- package/dist/import/newrecruit-json.js +21 -3
- package/dist/import/newrecruit-json.js.map +1 -1
- package/dist/import/newrecruit-simple.js +1 -1
- package/dist/import/newrecruit-simple.js.map +1 -1
- package/dist/import/newrecruit-wtc.js +1 -1
- package/dist/import/newrecruit-wtc.js.map +1 -1
- package/dist/import/resolve.d.ts.map +1 -1
- package/dist/import/resolve.js +46 -18
- package/dist/import/resolve.js.map +1 -1
- package/dist/import/roster-json.d.ts +5 -3
- package/dist/import/roster-json.d.ts.map +1 -1
- package/dist/import/roster-json.js +8 -4
- package/dist/import/roster-json.js.map +1 -1
- package/dist/import/rosterizer.js +3 -3
- package/dist/import/rosterizer.js.map +1 -1
- package/dist/import/types.d.ts +15 -4
- package/dist/import/types.d.ts.map +1 -1
- package/dist/import/types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/rube-goldberg.js +1 -1
- package/dist/rube-goldberg.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +33 -1
- package/dist/runner.js.map +1 -1
- package/dist/scope.d.ts +31 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +34 -0
- package/dist/scope.js.map +1 -0
- package/dist/translate/effect.d.ts +17 -3
- package/dist/translate/effect.d.ts.map +1 -1
- package/dist/translate/effect.js +23 -4
- package/dist/translate/effect.js.map +1 -1
- package/dist/translate/index.d.ts +1 -1
- package/dist/translate/index.d.ts.map +1 -1
- package/dist/translate/index.js +1 -1
- package/dist/translate/index.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +1 -0
- package/dist/validate.js.map +1 -1
- package/package.json +2 -1
- package/schemas/core/hull-shape.schema.json +30 -0
- package/schemas/core/roster.schema.json +24 -8
- package/schemas/core/stratagem.schema.json +2 -2
- package/schemas/core/unit-composition.schema.json +7 -0
- package/schemas/enrichment/ability-dsl/ability.schema.json +14 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listforge.js","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"AAwBA,MAAM,aAAa,GAAG,KAAK,CAAC;AAC5B,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAC7C,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAC3C,MAAM,wBAAwB,GAAG,cAAc,CAAC;AAChD,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;AACjE,MAAM,sBAAsB,GAAG,SAAS,CAAC,CAAC,oDAAoD;AAC9F,MAAM,gBAAgB,GAAG,iDAAiD,CAAC;AAC3E,MAAM,sBAAsB,GAAG,oBAAoB,CAAC;AAqBpD,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC;AAED,SAAS,aAAa,CAAC,GAAiB;IACtC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AAClC,CAAC;AAED,SAAS,aAAa,CAAC,GAAiB;IACtC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AAClC,CAAC;AAED,8DAA8D;AAC9D,SAAS,cAAc,CAAC,GAAiB;IACvC,OAAO,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,sEAAsE;AACtE,SAAS,QAAQ,CAAC,GAAiB;IACjC,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,GAAc,CAAC;QAC5B,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,aAAa,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5E,OAAO,IAAI,CAAC,KAAK,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,aAAa,CAAC,GAAiB;IACtC,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAE,CAAiB,CAAC,IAAI,CAAC,CAAC;SAC7C,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,GAAiB;IACxC,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAmB,CAAC;AACnD,CAAC;AAED,kEAAkE;AAClE,SAAS,IAAI,CAAC,GAAiB,EAAE,KAAgC;IAC/D,KAAK,CAAC,GAAG,CAAC,CAAC;IACX,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,GAAG,CAAC;QAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAC/D,CAAC;AAED,SAAS,eAAe,CAAC,GAAiB;IACxC,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,MAAM,CAAC;AAC7C,CAAC;AAED,SAAS,WAAW,CAAC,GAAiB;IACpC,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AACrE,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAiB;IAC1C,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,sBAAsB,CAAC,GAAiB;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,wBAAwB,CAAC,CAAC;AACtE,CAAC;AAED,sEAAsE;AACtE,SAAS,UAAU,CAAC,IAAkB;IACpC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE;QACf,IAAI,aAAa,CAAC,CAAC,CAAC,KAAK,OAAO;YAAE,KAAK,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;AAClD,CAAC;AAED,2DAA2D;AAC3D,SAAS,SAAS,CAAC,IAAkB;IACnC,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,IAAI,oBAAoB,GAAkB,IAAI,CAAC;IAC/C,IAAI,kBAAkB,GAAkB,IAAI,CAAC;IAC7C,IAAI,UAAU,GAAG,KAAK,CAAC;IAEvB,KAAK,MAAM,IAAI,IAAI,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE;YACf,IAAI,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC9B,IAAI,oBAAoB,KAAK,IAAI,EAAE,CAAC;oBAClC,oBAAoB,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;oBACxC,kBAAkB,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;gBACnC,CAAC;gBACD,OAAO;YACT,CAAC;YACD,IAAI,aAAa,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;gBACnC,UAAU,GAAG,IAAI,CAAC;gBAClB,OAAO;YACT,CAAC;YACD,IAAI,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,aAAa,CAAC,IAAI,CAAC;QAC7B,YAAY,EAAE,WAAW,CAAC,IAAI,CAAC;QAC/B,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC;QAC7B,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC;QACtB,UAAU;QACV,oBAAoB;QACpB,kBAAkB;QAClB,OAAO;KACR,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,SAAS,WAAW,CAClB,UAA0B,EAC1B,UAAkB;IAElB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,CAAC;IACrE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,SAAS,UAAU,CAAC,KAAoB;IACtC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,gFAAgF;AAChF,SAAS,eAAe,CAAC,MAAsB;IAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,KAAK,MAAM,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE;gBACd,KAAK,MAAM,IAAI,IAAI,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;oBACpC,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC1C,IAAI,KAAK;wBAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBACvC,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;AACnB,CAAC;AAeD,SAAS,QAAQ,CAAC,OAAgB;IAChC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzD,MAAM,MAAM,GAAI,OAAsB,CAAC,MAAM,CAAC;IAC9C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAE,MAAoB,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9D,OAAO,MAAmB,CAAC;AAC7B,CAAC;AAED;;yDAEyD;AACzD,SAAS,sBAAsB,CAAC,OAAgB,EAAE,MAAiB;IACjE,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,gBAAgB;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,KAAK,GACT,QAAQ,CAAE,OAAsB,CAAC,WAAW,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAChF,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAkB;IAC7C,EAAE,EAAE,WAAW;IAEf,OAAO,CAAC,OAAgB;QACtB,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC1B,OAAO,CAAC,sBAAsB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,OAAgB;QACpB,MAAM,OAAO,GAAG,OAAqB,CAAC;QACtC,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;QACnE,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAmB,CAAC;QAExD,+DAA+D;QAC/D,IAAI,mBAAmB,GAAkB,IAAI,CAAC;QAC9C,IAAI,eAAe,GAAkB,IAAI,CAAC;QAC1C,MAAM,KAAK,GAAiB,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;YACnC,mBAAmB,KAAK,WAAW,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;YACvD,eAAe,KAAK,WAAW,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;YACpD,KAAK,MAAM,GAAG,IAAI,GAAG,EAAE,CAAC;gBACtB,IAAI,eAAe,CAAC,GAAG,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;YACvD,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAsB,CAAC,CAAC;QAExD,4EAA4E;QAC5E,6EAA6E;QAC7E,0EAA0E;QAC1E,IAAI,cAAc,GAAG,CAAC,CAAC;QACvB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,KAAK,MAAM,GAAG,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE;oBACd,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;oBACxB,IAAI,GAAG;wBAAE,cAAc,IAAI,GAAG,CAAC;gBACjC,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO;YACL,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,iBAAiB;YAC1E,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC;YAC3C,gBAAgB,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI;YACrC,mBAAmB;YACnB,eAAe;YACf,cAAc,EAAE,UAAU,CAAC,eAAe,CAAC;YAC3C,cAAc;YACd,cAAc;YACd,KAAK;YACL,WAAW,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC;SACjC,CAAC;IACJ,CAAC;CACF,CAAC","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\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\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 xmlns?: unknown;\n generatedBy?: 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\n/** Detect a NewRecruit-flavoured BattleScribe payload. ListForge's matcher\n * excludes these so the greedy first-match dispatcher routes them to the\n * NewRecruit adapter without falling through to here. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n if (asString(roster.xmlns) === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString((decoded as RawPayload).generatedBy) ?? asString(roster.generatedBy);\n return genBy !== null && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX);\n}\n\nexport const listForgeAdapter: FormatAdapter = {\n id: \"listforge\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return !hasNewRecruitSignature(decoded, roster);\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"]}
|
|
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\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\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\n/** Every value under a named config, across repeated blocks and multiple\n * children, in source order. Used for multi-detachment 11e lists. */\nfunction configValues(selections: RawSelection[], configName: string): string[] {\n const out: string[] = [];\n for (const node of selections) {\n if (selectionName(node) !== configName) continue;\n for (const child of childSelections(node)) {\n const name = selectionName(child);\n if (name) out.push(name);\n }\n }\n return out;\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 xmlns?: unknown;\n generatedBy?: 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\n/** Detect a NewRecruit-flavoured BattleScribe payload. ListForge's matcher\n * excludes these so the greedy first-match dispatcher routes them to the\n * NewRecruit adapter without falling through to here. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n if (asString(roster.xmlns) === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString((decoded as RawPayload).generatedBy) ?? asString(roster.generatedBy);\n return genBy !== null && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX);\n}\n\nexport const listForgeAdapter: FormatAdapter = {\n id: \"listforge\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return !hasNewRecruitSignature(decoded, roster);\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 const detachment_raw_names: string[] = [];\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_names.push(...configValues(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_names,\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"]}
|
|
@@ -1 +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;
|
|
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;AA6OlD,eAAO,MAAM,qBAAqB,EAAE,aA4DnC,CAAC"}
|
|
@@ -106,6 +106,24 @@ function configValue(selections, configName) {
|
|
|
106
106
|
const child = childSelections(node)[0];
|
|
107
107
|
return child ? selectionName(child) : null;
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Every value selected under a named config — across repeated config nodes
|
|
111
|
+
* (one `Detachment` block per detachment) and multiple children of a single
|
|
112
|
+
* block. Source order is preserved. Used for multi-detachment 11e lists.
|
|
113
|
+
*/
|
|
114
|
+
function configValues(selections, configName) {
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const node of selections) {
|
|
117
|
+
if (selectionName(node) !== configName)
|
|
118
|
+
continue;
|
|
119
|
+
for (const child of childSelections(node)) {
|
|
120
|
+
const name = selectionName(child);
|
|
121
|
+
if (name)
|
|
122
|
+
out.push(name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
109
127
|
function parseLimit(label) {
|
|
110
128
|
if (!label)
|
|
111
129
|
return null;
|
|
@@ -181,12 +199,12 @@ export const newRecruitJsonAdapter = {
|
|
|
181
199
|
throw new Error("newrecruit-json: payload has no roster.forces array");
|
|
182
200
|
}
|
|
183
201
|
const forces = asArray(roster.forces);
|
|
184
|
-
|
|
202
|
+
const detachment_raw_names = [];
|
|
185
203
|
let battle_size_raw = null;
|
|
186
204
|
const units = [];
|
|
187
205
|
for (const force of forces) {
|
|
188
206
|
const top = childSelections(force);
|
|
189
|
-
|
|
207
|
+
detachment_raw_names.push(...configValues(top, "Detachment"));
|
|
190
208
|
battle_size_raw ??= configValue(top, "Battle Size");
|
|
191
209
|
for (const sel of top) {
|
|
192
210
|
if (isUnitSelection(sel))
|
|
@@ -211,7 +229,7 @@ export const newRecruitJsonAdapter = {
|
|
|
211
229
|
name: asString(payload.name) ?? asString(roster.name) ?? "Imported roster",
|
|
212
230
|
generated_by,
|
|
213
231
|
faction_raw_name: primaryFaction,
|
|
214
|
-
|
|
232
|
+
detachment_raw_names,
|
|
215
233
|
battle_size_raw,
|
|
216
234
|
declared_limit: parseLimit(battle_size_raw),
|
|
217
235
|
total_reported,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newrecruit-json.js","sourceRoot":"","sources":["../../src/import/newrecruit-json.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit JSON adapter: lower a decoded NewRecruit roster export (a\n * BattleScribe-derived tree, same outer shape as ListForge) to a {@link ParsedRoster}.\n *\n * NewRecruit-specific signals used to detect the format:\n * - `generatedBy` reports the NewRecruit URL (\"https://newrecruit.eu\"), and/or\n * - `roster.xmlns` is set to the BattleScribe rosterSchema namespace.\n *\n * The primary faction surfaces in `forces[].catalogueName` (e.g.\n * \"Chaos - Chaos Knights\") — we take the segment after the final \" - \". Falls\n * back to the first `\"Faction: X\"` category if no catalogueName is present.\n *\n * The walk reads the same ALLOWLIST as the ListForge adapter — `name`,\n * `number`, `type`, `categories[].name`, `group`, `costs` point values, and\n * `catalogueName`. `rules[].description`, ability `profiles[].characteristics[].$text`,\n * and every other prose field are never touched, so the importer's output is\n * free of copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`) is identical to ListForge:\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 *\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\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\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 catalogueName?: 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\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\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\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\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\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\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\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\n/** Primary faction from a force's `catalogueName` (e.g. \"Chaos - Chaos Knights\"\n * → \"Chaos Knights\"). Returns null when no force has a catalogueName. */\nfunction primaryFactionFromCatalogue(forces: RawSelection[]): string | null {\n for (const force of forces) {\n const name = asString(force.catalogueName);\n if (!name) continue;\n const parts = name.split(\" - \");\n const last = parts[parts.length - 1]?.trim();\n if (last) return last;\n }\n return null;\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n xmlns?: unknown;\n generatedBy?: 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\n/** Detect a NewRecruit payload: BattleScribe `rosterSchema` xmlns or a\n * `generatedBy` URL pointing at newrecruit.eu. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n const payload = decoded as RawPayload;\n const xmlns = asString(roster.xmlns);\n if (xmlns === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n if (genBy && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX)) {\n return true;\n }\n return false;\n}\n\nexport const newRecruitJsonAdapter: FormatAdapter = {\n id: \"newrecruit-json\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return hasNewRecruitSignature(decoded, roster);\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(\"newrecruit-json: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\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 primaryFaction = primaryFactionFromCatalogue(forces) ?? factions[0] ?? null;\n const total_reported = pointsOf(roster as RawSelection);\n\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 const generated_by =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by,\n faction_raw_name: primaryFaction,\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"]}
|
|
1
|
+
{"version":3,"file":"newrecruit-json.js","sourceRoot":"","sources":["../../src/import/newrecruit-json.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit JSON adapter: lower a decoded NewRecruit roster export (a\n * BattleScribe-derived tree, same outer shape as ListForge) to a {@link ParsedRoster}.\n *\n * NewRecruit-specific signals used to detect the format:\n * - `generatedBy` reports the NewRecruit URL (\"https://newrecruit.eu\"), and/or\n * - `roster.xmlns` is set to the BattleScribe rosterSchema namespace.\n *\n * The primary faction surfaces in `forces[].catalogueName` (e.g.\n * \"Chaos - Chaos Knights\") — we take the segment after the final \" - \". Falls\n * back to the first `\"Faction: X\"` category if no catalogueName is present.\n *\n * The walk reads the same ALLOWLIST as the ListForge adapter — `name`,\n * `number`, `type`, `categories[].name`, `group`, `costs` point values, and\n * `catalogueName`. `rules[].description`, ability `profiles[].characteristics[].$text`,\n * and every other prose field are never touched, so the importer's output is\n * free of copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`) is identical to ListForge:\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 *\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\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\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 catalogueName?: 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\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\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\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\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\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\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\n/**\n * Every value selected under a named config — across repeated config nodes\n * (one `Detachment` block per detachment) and multiple children of a single\n * block. Source order is preserved. Used for multi-detachment 11e lists.\n */\nfunction configValues(selections: RawSelection[], configName: string): string[] {\n const out: string[] = [];\n for (const node of selections) {\n if (selectionName(node) !== configName) continue;\n for (const child of childSelections(node)) {\n const name = selectionName(child);\n if (name) out.push(name);\n }\n }\n return out;\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\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\n/** Primary faction from a force's `catalogueName` (e.g. \"Chaos - Chaos Knights\"\n * → \"Chaos Knights\"). Returns null when no force has a catalogueName. */\nfunction primaryFactionFromCatalogue(forces: RawSelection[]): string | null {\n for (const force of forces) {\n const name = asString(force.catalogueName);\n if (!name) continue;\n const parts = name.split(\" - \");\n const last = parts[parts.length - 1]?.trim();\n if (last) return last;\n }\n return null;\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n xmlns?: unknown;\n generatedBy?: 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\n/** Detect a NewRecruit payload: BattleScribe `rosterSchema` xmlns or a\n * `generatedBy` URL pointing at newrecruit.eu. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n const payload = decoded as RawPayload;\n const xmlns = asString(roster.xmlns);\n if (xmlns === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n if (genBy && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX)) {\n return true;\n }\n return false;\n}\n\nexport const newRecruitJsonAdapter: FormatAdapter = {\n id: \"newrecruit-json\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return hasNewRecruitSignature(decoded, roster);\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(\"newrecruit-json: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n const detachment_raw_names: string[] = [];\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_names.push(...configValues(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 primaryFaction = primaryFactionFromCatalogue(forces) ?? factions[0] ?? null;\n const total_reported = pointsOf(roster as RawSelection);\n\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 const generated_by =\n asString(payload.generatedBy) ?? asString(roster.generatedBy);\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by,\n faction_raw_name: primaryFaction,\n detachment_raw_names,\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"]}
|
|
@@ -200,7 +200,7 @@ export const newRecruitSimpleAdapter = {
|
|
|
200
200
|
name,
|
|
201
201
|
generated_by: null,
|
|
202
202
|
faction_raw_name,
|
|
203
|
-
detachment_raw_name,
|
|
203
|
+
detachment_raw_names: detachment_raw_name ? [detachment_raw_name] : [],
|
|
204
204
|
battle_size_raw,
|
|
205
205
|
declared_limit,
|
|
206
206
|
total_reported,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newrecruit-simple.js","sourceRoot":"","sources":["../../src/import/newrecruit-simple.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"simple\" markdown-ish text adapter.\n *\n * Shape:\n * ```\n * <breadcrumb> - <faction> - <list name> - [N pts]\n *\n * # ++ Army Roster ++ [N pts]\n * ## Configuration\n * Battle Size: <Label>\n * Detachment: <Name>\n * Show/Hide Options: ...\n *\n * ## <Section> [N pts]\n * <Unit> [N pts]: <wargear>\n * <Unit> [N pts]:\n * • <count>x <ModelType>[ [N pts]]: <wargear>\n * ```\n *\n * Enhancements are inlined in the wargear list as `<Name> [N pts]` — the only\n * wargear token wearing a `[…]` pts suffix. `Warlord` and the detachment\n * \"<X> Character\" keyword are also stripped from the list and set as flags.\n * Per-model-type breakdowns under `•` lines are collapsed onto the parent unit.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport { classifyWargearList, splitWargearList } from \"./newrecruit-text.js\";\n\n// Point brackets may carry comma-separated faction resources after the pts\n// figure (e.g. `[4485pts, 29Cabal Points]`); the tail is recognized and\n// discarded — only the pts figure is consumed.\nconst FIRST_LINE = /^(.+)\\s-\\s\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst ROSTER_HEADER =\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst SECTION_HEADER = /^##\\s*(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?\\s*$/;\nconst UNIT_LINE = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\](?:\\s*:\\s*(.*))?$/i;\nconst BULLET =\n /^\\s*•\\s*(\\d+)x\\s+(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?(?:\\s*:\\s*(.*))?\\s*$/u;\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n enhancement_pts: number;\n displayed_pts: number | null;\n model_count: number;\n /** Aggregated wargear, keyed by name. Counts sum across `• Nx ModelType` breakdowns. */\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number | null): UnitBuilder {\n return {\n raw_name: name,\n is_character: false,\n is_warlord: false,\n enhancement_raw_name: null,\n enhancement_pts: 0,\n displayed_pts,\n model_count: 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyTokens(unit: UnitBuilder, tokensCsv: string, multiplier = 1): void {\n const tokens = splitWargearList(tokensCsv);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n if (cls.enhancement_raw_name && unit.enhancement_raw_name === null) {\n unit.enhancement_raw_name = cls.enhancement_raw_name;\n unit.enhancement_pts = cls.enhancement_points ?? 0;\n }\n const scaled = cls.wargear.map((w) => ({\n raw_name: w.raw_name,\n count: w.count * multiplier,\n }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const points =\n unit.displayed_pts === null ? null : unit.displayed_pts - unit.enhancement_pts;\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear: [...unit.wargear].map(([raw_name, count]) => ({ raw_name, count })),\n };\n}\n\nfunction parseFirstLine(line: string): { name: string; faction: string | null; declared_limit: number | null } | null {\n const m = FIRST_LINE.exec(line);\n if (!m) return null;\n const declared_limit = Number.parseInt(m[2], 10);\n const parts = m[1].split(\" - \").map((s) => s.trim()).filter((s) => s.length > 0);\n if (parts.length === 0) return null;\n const list_name = parts[parts.length - 1];\n const faction = parts.length >= 2 ? parts[parts.length - 2] : null;\n return { name: list_name, faction, declared_limit };\n}\n\ntype Section = \"preamble\" | \"configuration\" | \"units\";\n\nexport const newRecruitSimpleAdapter: FormatAdapter = {\n id: \"newrecruit-simple\",\n\n matches(decoded: unknown): boolean {\n if (typeof decoded !== \"string\") return false;\n const lines = decoded.split(/\\r?\\n/);\n const firstNonBlank = lines.find((l) => l.trim().length > 0);\n if (!firstNonBlank) return false;\n if (!FIRST_LINE.test(firstNonBlank)) return false;\n // Some exports omit the `# ++ Army Roster ++` line and open straight with\n // a `## Section` heading — accept either marker.\n return (\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+/m.test(decoded) ||\n /^##\\s+/m.test(decoded)\n );\n },\n\n parse(decoded: unknown): ParsedRoster {\n if (typeof decoded !== \"string\") {\n throw new Error(\"newrecruit-simple: input is not a string\");\n }\n const lines = decoded.split(/\\r?\\n/);\n\n let name = \"Imported roster\";\n let faction_raw_name: string | null = null;\n let declared_limit: number | null = null;\n let total_reported: number | null = null;\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitBuilder | null = null;\n let multi_force = false;\n let section: Section = \"preamble\";\n const enhancementPts: number[] = [];\n\n const finalize = (): void => {\n if (current) {\n enhancementPts.push(current.enhancement_pts);\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n const raw = lines[i];\n const line = raw.trim();\n if (!line) continue;\n\n // First non-blank line carries `<breadcrumb> - <faction> - <list> - [N pts]`.\n if (section === \"preamble\" && name === \"Imported roster\") {\n const first = parseFirstLine(line);\n if (first) {\n name = first.name;\n faction_raw_name = first.faction;\n declared_limit = first.declared_limit;\n continue;\n }\n }\n\n const rosterMatch = ROSTER_HEADER.exec(line);\n if (rosterMatch) {\n total_reported = Number.parseInt(rosterMatch[1], 10);\n continue;\n }\n\n const sectionMatch = SECTION_HEADER.exec(line);\n if (sectionMatch) {\n finalize();\n const heading = sectionMatch[1].trim().toLowerCase();\n if (heading === \"configuration\") {\n section = \"configuration\";\n } else {\n section = \"units\";\n if (heading.includes(\"allied\")) multi_force = true;\n }\n continue;\n }\n\n if (section === \"configuration\") {\n // Some exports list units directly after Configuration with no units\n // section heading; a `Name [N pts]` line ends the configuration block.\n if (UNIT_LINE.test(line)) {\n section = \"units\";\n } else {\n const idx = line.indexOf(\":\");\n if (idx > 0) {\n const key = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n if (key === \"battle size\") battle_size_raw = value;\n else if (key === \"detachment\") detachment_raw_name = value;\n }\n continue;\n }\n }\n\n // Unit section. A bullet line extends the *current* unit.\n const bulletMatch = BULLET.exec(raw);\n if (bulletMatch && current) {\n const count = Number.parseInt(bulletMatch[1], 10);\n // Bullets may add to the unit's model count beyond the implicit 1 we\n // set when we created it from the unit header.\n if (current.wargear.size === 0 && current.model_count === 1) {\n // First bullet: replace the implicit single-model assumption.\n current.model_count = count;\n } else {\n current.model_count += count;\n }\n if (bulletMatch[4]) applyTokens(current, bulletMatch[4], count);\n continue;\n }\n\n const unitMatch = UNIT_LINE.exec(line);\n if (unitMatch) {\n finalize();\n const unitName = unitMatch[1].trim();\n const pts = Number.parseInt(unitMatch[2], 10);\n current = newUnit(unitName, pts);\n const inlineWargear = unitMatch[3]?.trim() ?? \"\";\n if (inlineWargear.length > 0) {\n applyTokens(current, inlineWargear, 1);\n }\n // Leave model_count at the default 1. If `•` bullet lines follow, the\n // bullet handler resets model_count to the (summed) bullet counts.\n continue;\n }\n }\n finalize();\n\n let total_computed = 0;\n for (let i = 0; i < units.length; i += 1) {\n total_computed += units[i].points ?? 0;\n total_computed += enhancementPts[i] ?? 0;\n }\n\n return {\n name,\n generated_by: null,\n faction_raw_name,\n detachment_raw_name,\n battle_size_raw,\n declared_limit,\n total_reported,\n total_computed,\n units,\n multi_force,\n };\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"newrecruit-simple.js","sourceRoot":"","sources":["../../src/import/newrecruit-simple.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"simple\" markdown-ish text adapter.\n *\n * Shape:\n * ```\n * <breadcrumb> - <faction> - <list name> - [N pts]\n *\n * # ++ Army Roster ++ [N pts]\n * ## Configuration\n * Battle Size: <Label>\n * Detachment: <Name>\n * Show/Hide Options: ...\n *\n * ## <Section> [N pts]\n * <Unit> [N pts]: <wargear>\n * <Unit> [N pts]:\n * • <count>x <ModelType>[ [N pts]]: <wargear>\n * ```\n *\n * Enhancements are inlined in the wargear list as `<Name> [N pts]` — the only\n * wargear token wearing a `[…]` pts suffix. `Warlord` and the detachment\n * \"<X> Character\" keyword are also stripped from the list and set as flags.\n * Per-model-type breakdowns under `•` lines are collapsed onto the parent unit.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport { classifyWargearList, splitWargearList } from \"./newrecruit-text.js\";\n\n// Point brackets may carry comma-separated faction resources after the pts\n// figure (e.g. `[4485pts, 29Cabal Points]`); the tail is recognized and\n// discarded — only the pts figure is consumed.\nconst FIRST_LINE = /^(.+)\\s-\\s\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst ROSTER_HEADER =\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\]\\s*$/i;\nconst SECTION_HEADER = /^##\\s*(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?\\s*$/;\nconst UNIT_LINE = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\](?:\\s*:\\s*(.*))?$/i;\nconst BULLET =\n /^\\s*•\\s*(\\d+)x\\s+(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*(?:,[^\\]]*)?\\])?(?:\\s*:\\s*(.*))?\\s*$/u;\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n enhancement_pts: number;\n displayed_pts: number | null;\n model_count: number;\n /** Aggregated wargear, keyed by name. Counts sum across `• Nx ModelType` breakdowns. */\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number | null): UnitBuilder {\n return {\n raw_name: name,\n is_character: false,\n is_warlord: false,\n enhancement_raw_name: null,\n enhancement_pts: 0,\n displayed_pts,\n model_count: 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyTokens(unit: UnitBuilder, tokensCsv: string, multiplier = 1): void {\n const tokens = splitWargearList(tokensCsv);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n if (cls.enhancement_raw_name && unit.enhancement_raw_name === null) {\n unit.enhancement_raw_name = cls.enhancement_raw_name;\n unit.enhancement_pts = cls.enhancement_points ?? 0;\n }\n const scaled = cls.wargear.map((w) => ({\n raw_name: w.raw_name,\n count: w.count * multiplier,\n }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const points =\n unit.displayed_pts === null ? null : unit.displayed_pts - unit.enhancement_pts;\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear: [...unit.wargear].map(([raw_name, count]) => ({ raw_name, count })),\n };\n}\n\nfunction parseFirstLine(line: string): { name: string; faction: string | null; declared_limit: number | null } | null {\n const m = FIRST_LINE.exec(line);\n if (!m) return null;\n const declared_limit = Number.parseInt(m[2], 10);\n const parts = m[1].split(\" - \").map((s) => s.trim()).filter((s) => s.length > 0);\n if (parts.length === 0) return null;\n const list_name = parts[parts.length - 1];\n const faction = parts.length >= 2 ? parts[parts.length - 2] : null;\n return { name: list_name, faction, declared_limit };\n}\n\ntype Section = \"preamble\" | \"configuration\" | \"units\";\n\nexport const newRecruitSimpleAdapter: FormatAdapter = {\n id: \"newrecruit-simple\",\n\n matches(decoded: unknown): boolean {\n if (typeof decoded !== \"string\") return false;\n const lines = decoded.split(/\\r?\\n/);\n const firstNonBlank = lines.find((l) => l.trim().length > 0);\n if (!firstNonBlank) return false;\n if (!FIRST_LINE.test(firstNonBlank)) return false;\n // Some exports omit the `# ++ Army Roster ++` line and open straight with\n // a `## Section` heading — accept either marker.\n return (\n /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+/m.test(decoded) ||\n /^##\\s+/m.test(decoded)\n );\n },\n\n parse(decoded: unknown): ParsedRoster {\n if (typeof decoded !== \"string\") {\n throw new Error(\"newrecruit-simple: input is not a string\");\n }\n const lines = decoded.split(/\\r?\\n/);\n\n let name = \"Imported roster\";\n let faction_raw_name: string | null = null;\n let declared_limit: number | null = null;\n let total_reported: number | null = null;\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitBuilder | null = null;\n let multi_force = false;\n let section: Section = \"preamble\";\n const enhancementPts: number[] = [];\n\n const finalize = (): void => {\n if (current) {\n enhancementPts.push(current.enhancement_pts);\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n const raw = lines[i];\n const line = raw.trim();\n if (!line) continue;\n\n // First non-blank line carries `<breadcrumb> - <faction> - <list> - [N pts]`.\n if (section === \"preamble\" && name === \"Imported roster\") {\n const first = parseFirstLine(line);\n if (first) {\n name = first.name;\n faction_raw_name = first.faction;\n declared_limit = first.declared_limit;\n continue;\n }\n }\n\n const rosterMatch = ROSTER_HEADER.exec(line);\n if (rosterMatch) {\n total_reported = Number.parseInt(rosterMatch[1], 10);\n continue;\n }\n\n const sectionMatch = SECTION_HEADER.exec(line);\n if (sectionMatch) {\n finalize();\n const heading = sectionMatch[1].trim().toLowerCase();\n if (heading === \"configuration\") {\n section = \"configuration\";\n } else {\n section = \"units\";\n if (heading.includes(\"allied\")) multi_force = true;\n }\n continue;\n }\n\n if (section === \"configuration\") {\n // Some exports list units directly after Configuration with no units\n // section heading; a `Name [N pts]` line ends the configuration block.\n if (UNIT_LINE.test(line)) {\n section = \"units\";\n } else {\n const idx = line.indexOf(\":\");\n if (idx > 0) {\n const key = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n if (key === \"battle size\") battle_size_raw = value;\n else if (key === \"detachment\") detachment_raw_name = value;\n }\n continue;\n }\n }\n\n // Unit section. A bullet line extends the *current* unit.\n const bulletMatch = BULLET.exec(raw);\n if (bulletMatch && current) {\n const count = Number.parseInt(bulletMatch[1], 10);\n // Bullets may add to the unit's model count beyond the implicit 1 we\n // set when we created it from the unit header.\n if (current.wargear.size === 0 && current.model_count === 1) {\n // First bullet: replace the implicit single-model assumption.\n current.model_count = count;\n } else {\n current.model_count += count;\n }\n if (bulletMatch[4]) applyTokens(current, bulletMatch[4], count);\n continue;\n }\n\n const unitMatch = UNIT_LINE.exec(line);\n if (unitMatch) {\n finalize();\n const unitName = unitMatch[1].trim();\n const pts = Number.parseInt(unitMatch[2], 10);\n current = newUnit(unitName, pts);\n const inlineWargear = unitMatch[3]?.trim() ?? \"\";\n if (inlineWargear.length > 0) {\n applyTokens(current, inlineWargear, 1);\n }\n // Leave model_count at the default 1. If `•` bullet lines follow, the\n // bullet handler resets model_count to the (summed) bullet counts.\n continue;\n }\n }\n finalize();\n\n let total_computed = 0;\n for (let i = 0; i < units.length; i += 1) {\n total_computed += units[i].points ?? 0;\n total_computed += enhancementPts[i] ?? 0;\n }\n\n return {\n name,\n generated_by: null,\n faction_raw_name,\n detachment_raw_names: detachment_raw_name ? [detachment_raw_name] : [],\n battle_size_raw,\n declared_limit,\n total_reported,\n total_computed,\n units,\n multi_force,\n };\n },\n};\n"]}
|
|
@@ -314,7 +314,7 @@ function parseWith(text, format) {
|
|
|
314
314
|
name: header.name,
|
|
315
315
|
generated_by: null,
|
|
316
316
|
faction_raw_name: header.faction_raw_name,
|
|
317
|
-
|
|
317
|
+
detachment_raw_names: header.detachment_raw_name ? [header.detachment_raw_name] : [],
|
|
318
318
|
battle_size_raw: header.battle_size_raw,
|
|
319
319
|
declared_limit: header.declared_limit,
|
|
320
320
|
total_reported: header.total_reported,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"wtc-compact\" and \"wtc-full\" text adapters.\n *\n * Both formats open with a `++++++++` summary header carrying FACTION KEYWORD,\n * DETACHMENT, TOTAL ARMY POINTS, WARLORD, ENHANCEMENT(s), NUMBER OF UNITS, and\n * SECONDARY tournament-objective shorthand. The body diverges:\n *\n * - **wtc-compact** — one unit per line:\n * `[CharN: ]Nx <Unit> (P pts): <comma-separated wargear>`\n * followed optionally by `Enhancement: <Name> (+P pts)` on the next line.\n *\n * - **wtc-full** — uppercase section headers (`BATTLELINE`, `ALLIED UNITS`),\n * two-line unit blocks (`[CharN: ]Nx <Unit> (P pts)` then `N with <wargear>`),\n * `Enhancement: <Name> (+P pts)` on its own line, and per-model-type\n * breakdowns with `• Nx <ModelType>` + indented `N with <wargear>` lines.\n * Real exports also mix in compact-style lines: single-model units arrive as\n * `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line, and a model-type\n * bullet may inline its loadout after a colon (`• 1x Champion: Chainblades`,\n * `• 5x Eightbound: 5 with Chainblades`). The full-body parser accepts all\n * of these.\n *\n * The {@link Roster} pivot stores units at unit granularity — per-model-type\n * wargear breakdowns and `CharN:` slot numbers aren't modelled, so this adapter\n * collapses them: the parsed unit's `model_count` is summed from the breakdown\n * and its `wargear` is the union of every loadout under it. The `WARLORD` /\n * `Houndpack Lance Character` tokens are stripped from the wargear list (and\n * set `is_warlord`/`is_character` instead) so resolution doesn't try to look\n * them up as weapons. Round-trips are at Roster level, not byte-for-byte.\n *\n * Enhancement points (`+15 pts`) are subtracted from the displayed unit total\n * so `ParsedUnit.points` is the *base* unit cost — matching the ListForge\n * convention where the unit's own cost line is base and the enhancement is a\n * sibling cost line. `total_computed` walks every cost line just like ListForge\n * (base unit pts + each enhancement pts).\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport {\n classifyWargearList,\n factionFromKeyword,\n inferBattleSizeRaw,\n splitWargearList,\n stripParenthetical,\n} from \"./newrecruit-text.js\";\n\nconst WTC_HEADER_PREFIX = \"+ FACTION KEYWORD:\";\n\n// --- header parsing ---------------------------------------------------------\n\ninterface WtcHeader {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n declared_limit: number | null;\n total_reported: number | null;\n battle_size_raw: string | null;\n}\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 pointsLimit: /^\\+\\s*POINTS LIMIT:\\s*(\\d+)\\s*pts?\\s*$/i,\n listName: /^\\+\\s*LIST NAME:\\s*(.+?)\\s*$/i,\n} as const;\n\n/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */\nfunction parseWtcHeader(text: string): { header: WtcHeader; bodyStart: number } | null {\n const lines = text.split(/\\r?\\n/);\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let totalReported: number | null = null;\n let pointsLimit: number | null = null;\n let listName: string | null = null;\n\n // Two `+++++…` fence lines wrap the header. Find them.\n const fenceIndices: number[] = [];\n for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {\n if (/^\\++\\s*$/.test(lines[i])) fenceIndices.push(i);\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 totalReported = Number.parseInt(ptsMatch[1], 10);\n continue;\n }\n const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);\n if (limitMatch) {\n pointsLimit = Number.parseInt(limitMatch[1], 10);\n continue;\n }\n const nameMatch = HEADER_FIELDS.listName.exec(line);\n if (nameMatch) {\n listName = nameMatch[1];\n }\n }\n\n if (!sawFactionKeyword) return null;\n\n const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;\n // POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —\n // is the army's points ceiling. When the source carries only a single\n // figure (the tournament default), fall back to it.\n const declared_limit = pointsLimit ?? totalReported;\n const battle_size_raw = inferBattleSizeRaw(declared_limit);\n\n return {\n header: {\n name: listName ?? \"Imported roster\",\n faction_raw_name,\n detachment_raw_name,\n declared_limit,\n total_reported: totalReported,\n battle_size_raw,\n },\n bodyStart,\n };\n}\n\n// --- shared body helpers ----------------------------------------------------\n\nconst UNIT_HEADER_COMPACT =\n /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*:\\s*(.*)$/i;\nconst UNIT_HEADER_FULL = /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst ENHANCEMENT_LINE =\n /^Enhancement:\\s*(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst WITH_PREFIX = /^(\\d+)\\s+with\\s+(.*)$/i;\n// Optional trailing `: <wargear>` — NewRecruit inlines a model group's loadout\n// after the model type (`• 1x Champion: Chainblades`, `• 5x Eightbound: 5 with\n// Chainblades`) instead of always breaking it onto `N with` continuation lines.\nconst MODEL_BREAKDOWN =\n /^\\s*•\\s*(\\d+)x\\s+([^:]+?)(?:\\s*\\[[^\\]]*\\])?\\s*(?::\\s*(.+))?$/u;\nconst SECTION_HEADER = /^[A-Z][A-Z0-9 \\-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.\nconst HEADER_LINE = /^\\+/;\n\n/**\n * `N with X, Y, Z` means each of `N` models carries the same list — the weapon\n * counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when\n * the line has no `with` prefix.\n */\nfunction parseWithGroup(text: string): { multiplier: number; list: string } {\n const m = WITH_PREFIX.exec(text);\n if (m) {\n const n = Number.parseInt(m[1], 10);\n return { multiplier: n > 0 ? n : 1, list: m[2] };\n }\n return { multiplier: 1, list: text };\n}\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Total displayed pts from the header line; base computed once an enhancement is known. */\n displayed_pts: number | null;\n enhancement_pts: number;\n model_count: number;\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number, leading_count: number, is_character_prefix: boolean): UnitBuilder {\n return {\n raw_name: name,\n is_character: is_character_prefix,\n is_warlord: false,\n enhancement_raw_name: null,\n displayed_pts,\n enhancement_pts: 0,\n model_count: leading_count > 0 ? leading_count : 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyWithGroup(unit: UnitBuilder, listText: string): void {\n const { multiplier, list } = parseWithGroup(listText);\n const tokens = splitWargearList(list);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n // wtc never inlines the enhancement points in the wargear list (that's the\n // simple format) but classifyWargearList silently absorbs it if it shows up;\n // wtc's enhancement is always parsed off the explicit \"Enhancement:\" line.\n const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const displayed = unit.displayed_pts;\n const points = displayed === null ? null : displayed - unit.enhancement_pts;\n const wargear: ParsedWargear[] = [];\n for (const [raw_name, count] of unit.wargear) {\n wargear.push({ raw_name, count });\n }\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear,\n };\n}\n\n/** Compute total_computed by walking every parsed unit cost line. */\nfunction computeTotal(units: ParsedUnit[], enhancementPtsByIndex: number[]): number {\n let total = 0;\n for (let i = 0; i < units.length; i += 1) {\n total += units[i].points ?? 0;\n total += enhancementPtsByIndex[i] ?? 0;\n }\n return total;\n}\n\nfunction attachEnhancement(unit: UnitBuilder, raw_name: string, pts: number): void {\n unit.enhancement_raw_name = raw_name.trim();\n unit.enhancement_pts = pts;\n}\n\n// --- compact body parser ----------------------------------------------------\n\nfunction parseCompactBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n // Emit immediately so subsequent unit lines start fresh.\n finalize();\n continue;\n }\n\n const unitMatch = UNIT_HEADER_COMPACT.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, unitMatch[4]);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- full body parser -------------------------------------------------------\n\nfunction parseFullBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n let breakdownModels = 0;\n\n const finalize = (): void => {\n if (current) {\n if (breakdownModels > 0) current.model_count = breakdownModels;\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n breakdownModels = 0;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {\n finalize();\n continue;\n }\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n continue;\n }\n\n const unitMatch = UNIT_HEADER_FULL.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n continue;\n }\n\n // Single-model units (characters, vehicles) appear compact-style even in\n // full exports: `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line.\n // Without this branch they fall through every matcher and vanish.\n const compactMatch = UNIT_HEADER_COMPACT.exec(line);\n if (compactMatch) {\n finalize();\n const leading_count = Number.parseInt(compactMatch[1], 10);\n const name = compactMatch[2].trim();\n const pts = Number.parseInt(compactMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, compactMatch[4]);\n continue;\n }\n\n const breakdown = MODEL_BREAKDOWN.exec(raw);\n if (breakdown && current) {\n breakdownModels += Number.parseInt(breakdown[1], 10);\n // Inline loadout after the model type; `N with` continuation lines for\n // the same group still arrive separately and are handled below.\n if (breakdown[3] !== undefined) applyWithGroup(current, breakdown[3]);\n continue;\n }\n\n if (WITH_PREFIX.test(line) && current) {\n applyWithGroup(current, line);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- multi-force detection --------------------------------------------------\n\n/** Heuristic for `multi_force`: are there units with \"ALLIED\" decorating\n * the body? wtc-full has an explicit `ALLIED UNITS` section header; compact\n * has no section markers but the user-facing summary header counts every unit\n * together, so detect from explicit section presence. */\nfunction detectMultiForce(text: string, format: \"wtc-compact\" | \"wtc-full\"): boolean {\n if (format === \"wtc-full\") {\n return /^ALLIED UNITS\\s*$/im.test(text);\n }\n // wtc-compact has no section header. Multi-force surfaces only via the\n // primary-faction summary; assume single-force unless we add a richer marker.\n return false;\n}\n\n// --- adapters ---------------------------------------------------------------\n\nfunction isWtcText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n // Both wtc formats begin with the FACTION KEYWORD header line (possibly\n // after some leading whitespace/fence characters).\n if (!decoded.includes(WTC_HEADER_PREFIX)) return null;\n return decoded;\n}\n\n/** Distinguishes wtc-full from wtc-compact: full has a line starting with\n * `\\d+ with ` at the start of a body line (compact only puts `N with` after\n * `:` on the same line as the unit header). */\nfunction isFullFormat(text: string): boolean {\n return /^[\\t ]*\\d+\\s+with\\b/m.test(text);\n}\n\n/** `•`-prefixed body lines. wtc-full uses them for per-model breakdowns; the GW\n * app format uses them for every wargear entry. wtc-compact never emits them,\n * so it's the one matcher that must exclude them to stay disjoint from GW. */\nfunction hasBullets(text: string): boolean {\n return /^[\\t ]*•/mu.test(text);\n}\n\nfunction parseWith(text: string, format: \"wtc-compact\" | \"wtc-full\"): ParsedRoster {\n const parsed = parseWtcHeader(text);\n if (!parsed) {\n throw new Error(`${format}: missing \"+ FACTION KEYWORD:\" header`);\n }\n const { header, bodyStart } = parsed;\n const body = text.split(/\\r?\\n/).slice(bodyStart).join(\"\\n\");\n const { units, enhancementPts } =\n format === \"wtc-full\" ? parseFullBody(body) : parseCompactBody(body);\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: computeTotal(units, enhancementPts),\n units,\n multi_force: detectMultiForce(text, format),\n };\n}\n\nexport const newRecruitWtcCompactAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-compact\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n // wtc-compact has no `N with` lines (that's wtc-full) and no `•` bullets\n // (that's the GW app format) — excluding both keeps the matcher disjoint.\n return !isFullFormat(text) && !hasBullets(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-compact: input is not a string\");\n return parseWith(text, \"wtc-compact\");\n },\n};\n\nexport const newRecruitWtcFullAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-full\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n return isFullFormat(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-full: input is not a string\");\n return parseWith(text, \"wtc-full\");\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"wtc-compact\" and \"wtc-full\" text adapters.\n *\n * Both formats open with a `++++++++` summary header carrying FACTION KEYWORD,\n * DETACHMENT, TOTAL ARMY POINTS, WARLORD, ENHANCEMENT(s), NUMBER OF UNITS, and\n * SECONDARY tournament-objective shorthand. The body diverges:\n *\n * - **wtc-compact** — one unit per line:\n * `[CharN: ]Nx <Unit> (P pts): <comma-separated wargear>`\n * followed optionally by `Enhancement: <Name> (+P pts)` on the next line.\n *\n * - **wtc-full** — uppercase section headers (`BATTLELINE`, `ALLIED UNITS`),\n * two-line unit blocks (`[CharN: ]Nx <Unit> (P pts)` then `N with <wargear>`),\n * `Enhancement: <Name> (+P pts)` on its own line, and per-model-type\n * breakdowns with `• Nx <ModelType>` + indented `N with <wargear>` lines.\n * Real exports also mix in compact-style lines: single-model units arrive as\n * `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line, and a model-type\n * bullet may inline its loadout after a colon (`• 1x Champion: Chainblades`,\n * `• 5x Eightbound: 5 with Chainblades`). The full-body parser accepts all\n * of these.\n *\n * The {@link Roster} pivot stores units at unit granularity — per-model-type\n * wargear breakdowns and `CharN:` slot numbers aren't modelled, so this adapter\n * collapses them: the parsed unit's `model_count` is summed from the breakdown\n * and its `wargear` is the union of every loadout under it. The `WARLORD` /\n * `Houndpack Lance Character` tokens are stripped from the wargear list (and\n * set `is_warlord`/`is_character` instead) so resolution doesn't try to look\n * them up as weapons. Round-trips are at Roster level, not byte-for-byte.\n *\n * Enhancement points (`+15 pts`) are subtracted from the displayed unit total\n * so `ParsedUnit.points` is the *base* unit cost — matching the ListForge\n * convention where the unit's own cost line is base and the enhancement is a\n * sibling cost line. `total_computed` walks every cost line just like ListForge\n * (base unit pts + each enhancement pts).\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport {\n classifyWargearList,\n factionFromKeyword,\n inferBattleSizeRaw,\n splitWargearList,\n stripParenthetical,\n} from \"./newrecruit-text.js\";\n\nconst WTC_HEADER_PREFIX = \"+ FACTION KEYWORD:\";\n\n// --- header parsing ---------------------------------------------------------\n\ninterface WtcHeader {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n declared_limit: number | null;\n total_reported: number | null;\n battle_size_raw: string | null;\n}\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 pointsLimit: /^\\+\\s*POINTS LIMIT:\\s*(\\d+)\\s*pts?\\s*$/i,\n listName: /^\\+\\s*LIST NAME:\\s*(.+?)\\s*$/i,\n} as const;\n\n/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */\nfunction parseWtcHeader(text: string): { header: WtcHeader; bodyStart: number } | null {\n const lines = text.split(/\\r?\\n/);\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let totalReported: number | null = null;\n let pointsLimit: number | null = null;\n let listName: string | null = null;\n\n // Two `+++++…` fence lines wrap the header. Find them.\n const fenceIndices: number[] = [];\n for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {\n if (/^\\++\\s*$/.test(lines[i])) fenceIndices.push(i);\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 totalReported = Number.parseInt(ptsMatch[1], 10);\n continue;\n }\n const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);\n if (limitMatch) {\n pointsLimit = Number.parseInt(limitMatch[1], 10);\n continue;\n }\n const nameMatch = HEADER_FIELDS.listName.exec(line);\n if (nameMatch) {\n listName = nameMatch[1];\n }\n }\n\n if (!sawFactionKeyword) return null;\n\n const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;\n // POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —\n // is the army's points ceiling. When the source carries only a single\n // figure (the tournament default), fall back to it.\n const declared_limit = pointsLimit ?? totalReported;\n const battle_size_raw = inferBattleSizeRaw(declared_limit);\n\n return {\n header: {\n name: listName ?? \"Imported roster\",\n faction_raw_name,\n detachment_raw_name,\n declared_limit,\n total_reported: totalReported,\n battle_size_raw,\n },\n bodyStart,\n };\n}\n\n// --- shared body helpers ----------------------------------------------------\n\nconst UNIT_HEADER_COMPACT =\n /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*:\\s*(.*)$/i;\nconst UNIT_HEADER_FULL = /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst ENHANCEMENT_LINE =\n /^Enhancement:\\s*(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst WITH_PREFIX = /^(\\d+)\\s+with\\s+(.*)$/i;\n// Optional trailing `: <wargear>` — NewRecruit inlines a model group's loadout\n// after the model type (`• 1x Champion: Chainblades`, `• 5x Eightbound: 5 with\n// Chainblades`) instead of always breaking it onto `N with` continuation lines.\nconst MODEL_BREAKDOWN =\n /^\\s*•\\s*(\\d+)x\\s+([^:]+?)(?:\\s*\\[[^\\]]*\\])?\\s*(?::\\s*(.+))?$/u;\nconst SECTION_HEADER = /^[A-Z][A-Z0-9 \\-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.\nconst HEADER_LINE = /^\\+/;\n\n/**\n * `N with X, Y, Z` means each of `N` models carries the same list — the weapon\n * counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when\n * the line has no `with` prefix.\n */\nfunction parseWithGroup(text: string): { multiplier: number; list: string } {\n const m = WITH_PREFIX.exec(text);\n if (m) {\n const n = Number.parseInt(m[1], 10);\n return { multiplier: n > 0 ? n : 1, list: m[2] };\n }\n return { multiplier: 1, list: text };\n}\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Total displayed pts from the header line; base computed once an enhancement is known. */\n displayed_pts: number | null;\n enhancement_pts: number;\n model_count: number;\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number, leading_count: number, is_character_prefix: boolean): UnitBuilder {\n return {\n raw_name: name,\n is_character: is_character_prefix,\n is_warlord: false,\n enhancement_raw_name: null,\n displayed_pts,\n enhancement_pts: 0,\n model_count: leading_count > 0 ? leading_count : 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyWithGroup(unit: UnitBuilder, listText: string): void {\n const { multiplier, list } = parseWithGroup(listText);\n const tokens = splitWargearList(list);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n // wtc never inlines the enhancement points in the wargear list (that's the\n // simple format) but classifyWargearList silently absorbs it if it shows up;\n // wtc's enhancement is always parsed off the explicit \"Enhancement:\" line.\n const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const displayed = unit.displayed_pts;\n const points = displayed === null ? null : displayed - unit.enhancement_pts;\n const wargear: ParsedWargear[] = [];\n for (const [raw_name, count] of unit.wargear) {\n wargear.push({ raw_name, count });\n }\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear,\n };\n}\n\n/** Compute total_computed by walking every parsed unit cost line. */\nfunction computeTotal(units: ParsedUnit[], enhancementPtsByIndex: number[]): number {\n let total = 0;\n for (let i = 0; i < units.length; i += 1) {\n total += units[i].points ?? 0;\n total += enhancementPtsByIndex[i] ?? 0;\n }\n return total;\n}\n\nfunction attachEnhancement(unit: UnitBuilder, raw_name: string, pts: number): void {\n unit.enhancement_raw_name = raw_name.trim();\n unit.enhancement_pts = pts;\n}\n\n// --- compact body parser ----------------------------------------------------\n\nfunction parseCompactBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n // Emit immediately so subsequent unit lines start fresh.\n finalize();\n continue;\n }\n\n const unitMatch = UNIT_HEADER_COMPACT.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, unitMatch[4]);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- full body parser -------------------------------------------------------\n\nfunction parseFullBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n let breakdownModels = 0;\n\n const finalize = (): void => {\n if (current) {\n if (breakdownModels > 0) current.model_count = breakdownModels;\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n breakdownModels = 0;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {\n finalize();\n continue;\n }\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n continue;\n }\n\n const unitMatch = UNIT_HEADER_FULL.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n continue;\n }\n\n // Single-model units (characters, vehicles) appear compact-style even in\n // full exports: `[CharN: ]Nx <Unit> (P pts): <wargear>` on one line.\n // Without this branch they fall through every matcher and vanish.\n const compactMatch = UNIT_HEADER_COMPACT.exec(line);\n if (compactMatch) {\n finalize();\n const leading_count = Number.parseInt(compactMatch[1], 10);\n const name = compactMatch[2].trim();\n const pts = Number.parseInt(compactMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, compactMatch[4]);\n continue;\n }\n\n const breakdown = MODEL_BREAKDOWN.exec(raw);\n if (breakdown && current) {\n breakdownModels += Number.parseInt(breakdown[1], 10);\n // Inline loadout after the model type; `N with` continuation lines for\n // the same group still arrive separately and are handled below.\n if (breakdown[3] !== undefined) applyWithGroup(current, breakdown[3]);\n continue;\n }\n\n if (WITH_PREFIX.test(line) && current) {\n applyWithGroup(current, line);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- multi-force detection --------------------------------------------------\n\n/** Heuristic for `multi_force`: are there units with \"ALLIED\" decorating\n * the body? wtc-full has an explicit `ALLIED UNITS` section header; compact\n * has no section markers but the user-facing summary header counts every unit\n * together, so detect from explicit section presence. */\nfunction detectMultiForce(text: string, format: \"wtc-compact\" | \"wtc-full\"): boolean {\n if (format === \"wtc-full\") {\n return /^ALLIED UNITS\\s*$/im.test(text);\n }\n // wtc-compact has no section header. Multi-force surfaces only via the\n // primary-faction summary; assume single-force unless we add a richer marker.\n return false;\n}\n\n// --- adapters ---------------------------------------------------------------\n\nfunction isWtcText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n // Both wtc formats begin with the FACTION KEYWORD header line (possibly\n // after some leading whitespace/fence characters).\n if (!decoded.includes(WTC_HEADER_PREFIX)) return null;\n return decoded;\n}\n\n/** Distinguishes wtc-full from wtc-compact: full has a line starting with\n * `\\d+ with ` at the start of a body line (compact only puts `N with` after\n * `:` on the same line as the unit header). */\nfunction isFullFormat(text: string): boolean {\n return /^[\\t ]*\\d+\\s+with\\b/m.test(text);\n}\n\n/** `•`-prefixed body lines. wtc-full uses them for per-model breakdowns; the GW\n * app format uses them for every wargear entry. wtc-compact never emits them,\n * so it's the one matcher that must exclude them to stay disjoint from GW. */\nfunction hasBullets(text: string): boolean {\n return /^[\\t ]*•/mu.test(text);\n}\n\nfunction parseWith(text: string, format: \"wtc-compact\" | \"wtc-full\"): ParsedRoster {\n const parsed = parseWtcHeader(text);\n if (!parsed) {\n throw new Error(`${format}: missing \"+ FACTION KEYWORD:\" header`);\n }\n const { header, bodyStart } = parsed;\n const body = text.split(/\\r?\\n/).slice(bodyStart).join(\"\\n\");\n const { units, enhancementPts } =\n format === \"wtc-full\" ? parseFullBody(body) : parseCompactBody(body);\n\n return {\n name: header.name,\n generated_by: null,\n faction_raw_name: header.faction_raw_name,\n detachment_raw_names: header.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: computeTotal(units, enhancementPts),\n units,\n multi_force: detectMultiForce(text, format),\n };\n}\n\nexport const newRecruitWtcCompactAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-compact\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n // wtc-compact has no `N with` lines (that's wtc-full) and no `•` bullets\n // (that's the GW app format) — excluding both keeps the matcher disjoint.\n return !isFullFormat(text) && !hasBullets(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-compact: input is not a string\");\n return parseWith(text, \"wtc-compact\");\n },\n};\n\nexport const newRecruitWtcFullAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-full\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n return isFullFormat(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-full: input is not a string\");\n return parseWith(text, \"wtc-full\");\n },\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/import/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,KAAK,EAIV,YAAY,EAGZ,MAAM,
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/import/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,KAAK,EAIV,YAAY,EAGZ,MAAM,EAEN,YAAY,EAIb,MAAM,YAAY,CAAC;AAoEpB,wBAAgB,OAAO,CACrB,MAAM,EAAE,YAAY,EACpB,EAAE,EAAE,OAAO,EACX,MAAM,GAAE,YAA0B,GACjC,MAAM,CA0FR"}
|
package/dist/import/resolve.js
CHANGED
|
@@ -42,6 +42,17 @@ function mapBattleSize(raw) {
|
|
|
42
42
|
return "incursion";
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
+
/** 11e detachment-point budget for a battle size; null when the size is unknown. */
|
|
46
|
+
function detachmentCap(battle_size) {
|
|
47
|
+
switch (battle_size) {
|
|
48
|
+
case "strike-force":
|
|
49
|
+
return 3;
|
|
50
|
+
case "incursion":
|
|
51
|
+
return 2;
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
export function resolve(parsed, ds, format = "listforge") {
|
|
46
57
|
const diag = new DiagnosticsBuilder();
|
|
47
58
|
if (parsed.multi_force) {
|
|
@@ -58,28 +69,41 @@ export function resolve(parsed, ds, format = "listforge") {
|
|
|
58
69
|
diag.warn("faction-unresolved", "Faction name did not match any 40kdc faction.", parsed.faction_raw_name);
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
|
-
// ---
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
// --- Detachments (each scoped to faction, then global fallback). ----------
|
|
73
|
+
// 11e lists may field several detachments under a detachment-point cap; the
|
|
74
|
+
// list preserves source order. `dp_cost` is looked up from the resolved
|
|
75
|
+
// detachment entity (no source format reports it).
|
|
76
|
+
const detachments = parsed.detachment_raw_names.map((raw_name) => {
|
|
77
|
+
const key = normalizeName(raw_name);
|
|
65
78
|
const scoped = faction_id
|
|
66
79
|
? ds.detachments.byFaction(faction_id).find((d) => normalizeName(d.name ?? "") === key)
|
|
67
80
|
: undefined;
|
|
68
|
-
const hit = scoped ?? ds.detachments.find(
|
|
81
|
+
const hit = scoped ?? ds.detachments.find(raw_name);
|
|
69
82
|
if (hit) {
|
|
70
|
-
|
|
83
|
+
return { ref: resolved(hit.id, raw_name), dp_cost: hit.detachment_points ?? null };
|
|
71
84
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
diag.warn("detachment-unresolved", "Detachment name did not match any 40kdc detachment.", raw_name);
|
|
86
|
+
return {
|
|
87
|
+
ref: unresolved(raw_name, toCandidates(ds.detachments.findAll(raw_name))),
|
|
88
|
+
dp_cost: null,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
const detachmentIds = detachments.map((d) => d.ref.id).filter((id) => id !== null);
|
|
76
92
|
// --- Battle size. ---------------------------------------------------------
|
|
77
93
|
const battle_size = mapBattleSize(parsed.battle_size_raw);
|
|
78
94
|
if (parsed.battle_size_raw && battle_size === null) {
|
|
79
95
|
diag.warn("battle-size-unmapped", "Battle size label could not be mapped.", parsed.battle_size_raw);
|
|
80
96
|
}
|
|
97
|
+
const detachment_cap = detachmentCap(battle_size);
|
|
98
|
+
// --- Detachment-point cap check (only when cap and every cost are known). --
|
|
99
|
+
if (detachment_cap !== null && detachments.length > 0 && detachments.every((d) => d.dp_cost !== null)) {
|
|
100
|
+
const spent = detachments.reduce((sum, d) => sum + (d.dp_cost ?? 0), 0);
|
|
101
|
+
if (spent > detachment_cap) {
|
|
102
|
+
diag.warn("detachment-points-exceeded", `Detachments cost ${spent} detachment points but the ${battle_size} budget is ${detachment_cap}.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
81
105
|
// --- Units (and their enhancements / wargear). ----------------------------
|
|
82
|
-
const units = parsed.units.map((u) => resolveUnit(u, faction_id,
|
|
106
|
+
const units = parsed.units.map((u) => resolveUnit(u, faction_id, detachmentIds, ds, diag));
|
|
83
107
|
// --- Leader attachments (second pass: needs all resolved unit ids). -------
|
|
84
108
|
inferLeaderAttachments(parsed.units, units, ds, diag);
|
|
85
109
|
// --- Points reconciliation (reported vs computed kept distinct). ----------
|
|
@@ -90,10 +114,11 @@ export function resolve(parsed, ds, format = "listforge") {
|
|
|
90
114
|
name: parsed.name,
|
|
91
115
|
source: { format, generated_by: parsed.generated_by },
|
|
92
116
|
faction_id,
|
|
93
|
-
|
|
117
|
+
detachments,
|
|
94
118
|
battle_size,
|
|
95
119
|
points: {
|
|
96
120
|
declared_limit: parsed.declared_limit,
|
|
121
|
+
detachment_cap,
|
|
97
122
|
total_reported: parsed.total_reported,
|
|
98
123
|
total_computed: parsed.total_computed,
|
|
99
124
|
},
|
|
@@ -102,7 +127,7 @@ export function resolve(parsed, ds, format = "listforge") {
|
|
|
102
127
|
diagnostics: diag.build(),
|
|
103
128
|
};
|
|
104
129
|
}
|
|
105
|
-
function resolveUnit(parsed, faction_id,
|
|
130
|
+
function resolveUnit(parsed, faction_id, detachmentIds, ds, diag) {
|
|
106
131
|
// Prefer a faction-scoped match (the same unit id recurs across factions),
|
|
107
132
|
// then fall back to a global name lookup.
|
|
108
133
|
const key = normalizeName(parsed.raw_name);
|
|
@@ -122,7 +147,7 @@ function resolveUnit(parsed, faction_id, detachment_id, ds, diag) {
|
|
|
122
147
|
diag.warn("unit-unresolved", "Unit name did not match any 40kdc unit.", parsed.raw_name);
|
|
123
148
|
}
|
|
124
149
|
const enhancement = parsed.enhancement_raw_name
|
|
125
|
-
? resolveEnhancement(parsed.enhancement_raw_name,
|
|
150
|
+
? resolveEnhancement(parsed.enhancement_raw_name, detachmentIds, ds, diag)
|
|
126
151
|
: null;
|
|
127
152
|
const enhancement_points = enhancement === null ? null : parsed.enhancement_points;
|
|
128
153
|
const wargear = parsed.wargear.map((w) => {
|
|
@@ -146,11 +171,14 @@ function resolveUnit(parsed, faction_id, detachment_id, ds, diag) {
|
|
|
146
171
|
leader_attachment: null,
|
|
147
172
|
};
|
|
148
173
|
}
|
|
149
|
-
function resolveEnhancement(raw_name,
|
|
174
|
+
function resolveEnhancement(raw_name, detachmentIds, ds, diag) {
|
|
150
175
|
const key = normalizeName(raw_name);
|
|
151
|
-
// Enhancements belong to a detachment, not a faction — scope
|
|
152
|
-
|
|
153
|
-
|
|
176
|
+
// Enhancements belong to a detachment, not a faction — scope to any of the
|
|
177
|
+
// roster's resolved detachments.
|
|
178
|
+
const scoped = detachmentIds.length > 0
|
|
179
|
+
? ds.enhancements.all.find((e) => e.detachment_id != null &&
|
|
180
|
+
detachmentIds.includes(e.detachment_id) &&
|
|
181
|
+
normalizeName(e.name ?? "") === key)
|
|
154
182
|
: undefined;
|
|
155
183
|
const hit = scoped ?? ds.enhancements.find(raw_name);
|
|
156
184
|
if (hit) {
|