@alpaca-software/40kdc-data 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abilities-resolver/index.d.ts +9 -0
- package/dist/abilities-resolver/index.d.ts.map +1 -0
- package/dist/abilities-resolver/index.js +9 -0
- package/dist/abilities-resolver/index.js.map +1 -0
- package/dist/abilities-resolver/resolver.d.ts +64 -0
- package/dist/abilities-resolver/resolver.d.ts.map +1 -0
- package/dist/abilities-resolver/resolver.js +135 -0
- package/dist/abilities-resolver/resolver.js.map +1 -0
- package/dist/bundle-schemas.d.ts +1 -0
- package/dist/bundle-schemas.d.ts.map +1 -0
- package/dist/bundle-schemas.js +12 -0
- package/dist/bundle-schemas.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen-data.d.ts +1 -0
- package/dist/codegen-data.d.ts.map +1 -0
- package/dist/codegen-data.js +2 -0
- package/dist/codegen-data.js.map +1 -0
- package/dist/commands/import.d.ts +7 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +103 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/translate.d.ts +1 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +1 -0
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/validate-all.d.ts +1 -0
- package/dist/commands/validate-all.d.ts.map +1 -0
- package/dist/commands/validate-all.js +1 -0
- package/dist/commands/validate-all.js.map +1 -0
- package/dist/commands/validate-core.d.ts +1 -0
- package/dist/commands/validate-core.d.ts.map +1 -0
- package/dist/commands/validate-core.js +1 -0
- package/dist/commands/validate-core.js.map +1 -0
- package/dist/commands/validate-enrichment.d.ts +1 -0
- package/dist/commands/validate-enrichment.d.ts.map +1 -0
- package/dist/commands/validate-enrichment.js +1 -0
- package/dist/commands/validate-enrichment.js.map +1 -0
- package/dist/convert-faction.d.ts +1 -0
- package/dist/convert-faction.d.ts.map +1 -0
- package/dist/convert-faction.js +1 -0
- package/dist/convert-faction.js.map +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts.map +1 -0
- package/dist/converters/configs/adepta-sororitas.js +1 -0
- package/dist/converters/configs/adepta-sororitas.js.map +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-astartes.js +1 -0
- package/dist/converters/configs/adeptus-astartes.js.map +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-custodes.js +1 -0
- package/dist/converters/configs/adeptus-custodes.js.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js.map +1 -0
- package/dist/converters/configs/aeldari.d.ts +1 -0
- package/dist/converters/configs/aeldari.d.ts.map +1 -0
- package/dist/converters/configs/aeldari.js +1 -0
- package/dist/converters/configs/aeldari.js.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js.map +1 -0
- package/dist/converters/configs/astra-militarum.d.ts +1 -0
- package/dist/converters/configs/astra-militarum.d.ts.map +1 -0
- package/dist/converters/configs/astra-militarum.js +1 -0
- package/dist/converters/configs/astra-militarum.js.map +1 -0
- package/dist/converters/configs/black-templars.d.ts +1 -0
- package/dist/converters/configs/black-templars.d.ts.map +1 -0
- package/dist/converters/configs/black-templars.js +1 -0
- package/dist/converters/configs/black-templars.js.map +1 -0
- package/dist/converters/configs/blood-angels.d.ts +1 -0
- package/dist/converters/configs/blood-angels.d.ts.map +1 -0
- package/dist/converters/configs/blood-angels.js +1 -0
- package/dist/converters/configs/blood-angels.js.map +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts.map +1 -0
- package/dist/converters/configs/chaos-daemons.js +1 -0
- package/dist/converters/configs/chaos-daemons.js.map +1 -0
- package/dist/converters/configs/chaos-knights.d.ts +1 -0
- package/dist/converters/configs/chaos-knights.d.ts.map +1 -0
- package/dist/converters/configs/chaos-knights.js +1 -0
- package/dist/converters/configs/chaos-knights.js.map +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts.map +1 -0
- package/dist/converters/configs/chaos-space-marines.js +1 -0
- package/dist/converters/configs/chaos-space-marines.js.map +1 -0
- package/dist/converters/configs/crimson-fists.d.ts +1 -0
- package/dist/converters/configs/crimson-fists.d.ts.map +1 -0
- package/dist/converters/configs/crimson-fists.js +1 -0
- package/dist/converters/configs/crimson-fists.js.map +1 -0
- package/dist/converters/configs/dark-angels.d.ts +1 -0
- package/dist/converters/configs/dark-angels.d.ts.map +1 -0
- package/dist/converters/configs/dark-angels.js +1 -0
- package/dist/converters/configs/dark-angels.js.map +1 -0
- package/dist/converters/configs/death-guard.d.ts +1 -0
- package/dist/converters/configs/death-guard.d.ts.map +1 -0
- package/dist/converters/configs/death-guard.js +1 -0
- package/dist/converters/configs/death-guard.js.map +1 -0
- package/dist/converters/configs/deathwatch.d.ts +1 -0
- package/dist/converters/configs/deathwatch.d.ts.map +1 -0
- package/dist/converters/configs/deathwatch.js +1 -0
- package/dist/converters/configs/deathwatch.js.map +1 -0
- package/dist/converters/configs/drukhari.d.ts +1 -0
- package/dist/converters/configs/drukhari.d.ts.map +1 -0
- package/dist/converters/configs/drukhari.js +1 -0
- package/dist/converters/configs/drukhari.js.map +1 -0
- package/dist/converters/configs/emperors-children.d.ts +1 -0
- package/dist/converters/configs/emperors-children.d.ts.map +1 -0
- package/dist/converters/configs/emperors-children.js +1 -0
- package/dist/converters/configs/emperors-children.js.map +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts.map +1 -0
- package/dist/converters/configs/genestealer-cults.js +1 -0
- package/dist/converters/configs/genestealer-cults.js.map +1 -0
- package/dist/converters/configs/grey-knights.d.ts +1 -0
- package/dist/converters/configs/grey-knights.d.ts.map +1 -0
- package/dist/converters/configs/grey-knights.js +1 -0
- package/dist/converters/configs/grey-knights.js.map +1 -0
- package/dist/converters/configs/imperial-fists.d.ts +1 -0
- package/dist/converters/configs/imperial-fists.d.ts.map +1 -0
- package/dist/converters/configs/imperial-fists.js +1 -0
- package/dist/converters/configs/imperial-fists.js.map +1 -0
- package/dist/converters/configs/imperial-knights.d.ts +1 -0
- package/dist/converters/configs/imperial-knights.d.ts.map +1 -0
- package/dist/converters/configs/imperial-knights.js +1 -0
- package/dist/converters/configs/imperial-knights.js.map +1 -0
- package/dist/converters/configs/iron-hands.d.ts +1 -0
- package/dist/converters/configs/iron-hands.d.ts.map +1 -0
- package/dist/converters/configs/iron-hands.js +1 -0
- package/dist/converters/configs/iron-hands.js.map +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts.map +1 -0
- package/dist/converters/configs/leagues-of-votann.js +1 -0
- package/dist/converters/configs/leagues-of-votann.js.map +1 -0
- package/dist/converters/configs/necrons.d.ts +1 -0
- package/dist/converters/configs/necrons.d.ts.map +1 -0
- package/dist/converters/configs/necrons.js +1 -0
- package/dist/converters/configs/necrons.js.map +1 -0
- package/dist/converters/configs/orks.d.ts +1 -0
- package/dist/converters/configs/orks.d.ts.map +1 -0
- package/dist/converters/configs/orks.js +1 -0
- package/dist/converters/configs/orks.js.map +1 -0
- package/dist/converters/configs/raven-guard.d.ts +1 -0
- package/dist/converters/configs/raven-guard.d.ts.map +1 -0
- package/dist/converters/configs/raven-guard.js +1 -0
- package/dist/converters/configs/raven-guard.js.map +1 -0
- package/dist/converters/configs/salamanders.d.ts +1 -0
- package/dist/converters/configs/salamanders.d.ts.map +1 -0
- package/dist/converters/configs/salamanders.js +1 -0
- package/dist/converters/configs/salamanders.js.map +1 -0
- package/dist/converters/configs/space-wolves.d.ts +1 -0
- package/dist/converters/configs/space-wolves.d.ts.map +1 -0
- package/dist/converters/configs/space-wolves.js +1 -0
- package/dist/converters/configs/space-wolves.js.map +1 -0
- package/dist/converters/configs/tau-empire.d.ts +1 -0
- package/dist/converters/configs/tau-empire.d.ts.map +1 -0
- package/dist/converters/configs/tau-empire.js +1 -0
- package/dist/converters/configs/tau-empire.js.map +1 -0
- package/dist/converters/configs/thousand-sons.d.ts +1 -0
- package/dist/converters/configs/thousand-sons.d.ts.map +1 -0
- package/dist/converters/configs/thousand-sons.js +1 -0
- package/dist/converters/configs/thousand-sons.js.map +1 -0
- package/dist/converters/configs/tyranids.d.ts +1 -0
- package/dist/converters/configs/tyranids.d.ts.map +1 -0
- package/dist/converters/configs/tyranids.js +1 -0
- package/dist/converters/configs/tyranids.js.map +1 -0
- package/dist/converters/configs/ultramarines.d.ts +1 -0
- package/dist/converters/configs/ultramarines.d.ts.map +1 -0
- package/dist/converters/configs/ultramarines.js +1 -0
- package/dist/converters/configs/ultramarines.js.map +1 -0
- package/dist/converters/configs/white-scars.d.ts +1 -0
- package/dist/converters/configs/white-scars.d.ts.map +1 -0
- package/dist/converters/configs/white-scars.js +1 -0
- package/dist/converters/configs/white-scars.js.map +1 -0
- package/dist/converters/configs/world-eaters.d.ts +1 -0
- package/dist/converters/configs/world-eaters.d.ts.map +1 -0
- package/dist/converters/configs/world-eaters.js +1 -0
- package/dist/converters/configs/world-eaters.js.map +1 -0
- package/dist/converters/faction-config.d.ts +1 -0
- package/dist/converters/faction-config.d.ts.map +1 -0
- package/dist/converters/faction-config.js +1 -0
- package/dist/converters/faction-config.js.map +1 -0
- package/dist/converters/id-generator.d.ts +1 -0
- package/dist/converters/id-generator.d.ts.map +1 -0
- package/dist/converters/id-generator.js +1 -0
- package/dist/converters/id-generator.js.map +1 -0
- package/dist/converters/keyword-filter.d.ts +1 -0
- package/dist/converters/keyword-filter.d.ts.map +1 -0
- package/dist/converters/keyword-filter.js +1 -0
- package/dist/converters/keyword-filter.js.map +1 -0
- package/dist/converters/stat-parser.d.ts +1 -0
- package/dist/converters/stat-parser.d.ts.map +1 -0
- package/dist/converters/stat-parser.js +1 -0
- package/dist/converters/stat-parser.js.map +1 -0
- package/dist/converters/view-selector.d.ts +1 -0
- package/dist/converters/view-selector.d.ts.map +1 -0
- package/dist/converters/view-selector.js +1 -0
- package/dist/converters/view-selector.js.map +1 -0
- package/dist/converters/weapon-dedup.d.ts +1 -0
- package/dist/converters/weapon-dedup.d.ts.map +1 -0
- package/dist/converters/weapon-dedup.js +1 -0
- package/dist/converters/weapon-dedup.js.map +1 -0
- package/dist/cruncher/buffs.d.ts +184 -0
- package/dist/cruncher/buffs.d.ts.map +1 -0
- package/dist/cruncher/buffs.js +150 -0
- package/dist/cruncher/buffs.js.map +1 -0
- package/dist/cruncher/engine.d.ts +50 -0
- package/dist/cruncher/engine.d.ts.map +1 -0
- package/dist/cruncher/engine.js +312 -0
- package/dist/cruncher/engine.js.map +1 -0
- package/dist/cruncher/from-dsl.d.ts +69 -0
- package/dist/cruncher/from-dsl.d.ts.map +1 -0
- package/dist/cruncher/from-dsl.js +523 -0
- package/dist/cruncher/from-dsl.js.map +1 -0
- package/dist/cruncher/from-keyword.d.ts +35 -0
- package/dist/cruncher/from-keyword.d.ts.map +1 -0
- package/dist/cruncher/from-keyword.js +159 -0
- package/dist/cruncher/from-keyword.js.map +1 -0
- package/dist/cruncher/get-buffs.d.ts +12 -0
- package/dist/cruncher/get-buffs.d.ts.map +1 -0
- package/dist/cruncher/get-buffs.js +7 -0
- package/dist/cruncher/get-buffs.js.map +1 -0
- package/dist/cruncher/index.d.ts +11 -0
- package/dist/cruncher/index.d.ts.map +1 -0
- package/dist/cruncher/index.js +11 -0
- package/dist/cruncher/index.js.map +1 -0
- package/dist/data/bundle.generated.d.ts +1 -0
- package/dist/data/bundle.generated.d.ts.map +1 -0
- package/dist/data/bundle.generated.js +2 -1
- package/dist/data/bundle.generated.js.map +1 -0
- package/dist/data/collection.d.ts +1 -0
- package/dist/data/collection.d.ts.map +1 -0
- package/dist/data/collection.js +1 -0
- package/dist/data/collection.js.map +1 -0
- package/dist/data/dataset.d.ts +54 -2
- package/dist/data/dataset.d.ts.map +1 -0
- package/dist/data/dataset.js +111 -1
- package/dist/data/dataset.js.map +1 -0
- package/dist/data/entities.d.ts +70 -2
- package/dist/data/entities.d.ts.map +1 -0
- package/dist/data/entities.js +122 -0
- package/dist/data/entities.js.map +1 -0
- package/dist/data/index.d.ts +9 -1
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +14 -1
- package/dist/data/index.js.map +1 -0
- package/dist/data/normalize.d.ts +1 -0
- package/dist/data/normalize.d.ts.map +1 -0
- package/dist/data/normalize.js +1 -0
- package/dist/data/normalize.js.map +1 -0
- package/dist/data/roster-resolve.d.ts +33 -0
- package/dist/data/roster-resolve.d.ts.map +1 -0
- package/dist/data/roster-resolve.js +36 -0
- package/dist/data/roster-resolve.js.map +1 -0
- package/dist/data/types.d.ts +4 -1
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +2 -0
- package/dist/data/types.js.map +1 -0
- package/dist/export/helpers.d.ts +33 -0
- package/dist/export/helpers.d.ts.map +1 -0
- package/dist/export/helpers.js +57 -0
- package/dist/export/helpers.js.map +1 -0
- package/dist/export/index.d.ts +21 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +25 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/newrecruit-json.d.ts +3 -0
- package/dist/export/newrecruit-json.d.ts.map +1 -0
- package/dist/export/newrecruit-json.js +140 -0
- package/dist/export/newrecruit-json.js.map +1 -0
- package/dist/export/newrecruit-simple.d.ts +3 -0
- package/dist/export/newrecruit-simple.d.ts.map +1 -0
- package/dist/export/newrecruit-simple.js +76 -0
- package/dist/export/newrecruit-simple.js.map +1 -0
- package/dist/export/newrecruit-wtc.d.ts +4 -0
- package/dist/export/newrecruit-wtc.d.ts.map +1 -0
- package/dist/export/newrecruit-wtc.js +142 -0
- package/dist/export/newrecruit-wtc.js.map +1 -0
- package/dist/export/roster-json.d.ts +3 -0
- package/dist/export/roster-json.d.ts.map +1 -0
- package/dist/export/roster-json.js +8 -0
- package/dist/export/roster-json.js.map +1 -0
- package/dist/export/serializer.d.ts +27 -0
- package/dist/export/serializer.d.ts.map +1 -0
- package/dist/export/serializer.js +2 -0
- package/dist/export/serializer.js.map +1 -0
- package/dist/gen-conformance.d.ts +2 -0
- package/dist/gen-conformance.d.ts.map +1 -0
- package/dist/gen-conformance.js +131 -0
- package/dist/gen-conformance.js.map +1 -0
- package/dist/generated.d.ts +194 -118
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +1 -0
- package/dist/generated.js.map +1 -0
- package/dist/import/adapter.d.ts +27 -0
- package/dist/import/adapter.d.ts.map +1 -0
- package/dist/import/adapter.js +10 -0
- package/dist/import/adapter.js.map +1 -0
- package/dist/import/decode.d.ts +7 -0
- package/dist/import/decode.d.ts.map +1 -0
- package/dist/import/decode.js +73 -0
- package/dist/import/decode.js.map +1 -0
- package/dist/import/import-roster.d.ts +35 -0
- package/dist/import/import-roster.d.ts.map +1 -0
- package/dist/import/import-roster.js +97 -0
- package/dist/import/import-roster.js.map +1 -0
- package/dist/import/index.d.ts +22 -0
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +19 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/listforge.d.ts +24 -0
- package/dist/import/listforge.d.ts.map +1 -0
- package/dist/import/listforge.js +201 -0
- package/dist/import/listforge.js.map +1 -0
- package/dist/import/newrecruit-json.d.ts +31 -0
- package/dist/import/newrecruit-json.d.ts.map +1 -0
- package/dist/import/newrecruit-json.js +224 -0
- package/dist/import/newrecruit-json.js.map +1 -0
- package/dist/import/newrecruit-simple.d.ts +29 -0
- package/dist/import/newrecruit-simple.d.ts.map +1 -0
- package/dist/import/newrecruit-simple.js +200 -0
- package/dist/import/newrecruit-simple.js.map +1 -0
- package/dist/import/newrecruit-text.d.ts +48 -0
- package/dist/import/newrecruit-text.d.ts.map +1 -0
- package/dist/import/newrecruit-text.js +96 -0
- package/dist/import/newrecruit-text.js.map +1 -0
- package/dist/import/newrecruit-wtc.d.ts +36 -0
- package/dist/import/newrecruit-wtc.d.ts.map +1 -0
- package/dist/import/newrecruit-wtc.js +334 -0
- package/dist/import/newrecruit-wtc.js.map +1 -0
- package/dist/import/resolve.d.ts +20 -0
- package/dist/import/resolve.d.ts.map +1 -0
- package/dist/import/resolve.js +190 -0
- package/dist/import/resolve.js.map +1 -0
- package/dist/import/types.d.ts +153 -0
- package/dist/import/types.d.ts.map +1 -0
- package/dist/import/types.js +20 -0
- package/dist/import/types.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/known-support-10e.d.ts +1 -0
- package/dist/known-support-10e.d.ts.map +1 -0
- package/dist/known-support-10e.js +1 -0
- package/dist/known-support-10e.js.map +1 -0
- package/dist/link-abilities.d.ts +41 -0
- package/dist/link-abilities.d.ts.map +1 -0
- package/dist/link-abilities.js +159 -0
- package/dist/link-abilities.js.map +1 -0
- package/dist/migrations/2026-weapon-keywords.d.ts +2 -0
- package/dist/migrations/2026-weapon-keywords.d.ts.map +1 -0
- package/dist/migrations/2026-weapon-keywords.js +243 -0
- package/dist/migrations/2026-weapon-keywords.js.map +1 -0
- package/dist/port-10e-faction.d.ts +1 -0
- package/dist/port-10e-faction.d.ts.map +1 -0
- package/dist/port-10e-faction.js +1 -0
- package/dist/port-10e-faction.js.map +1 -0
- package/dist/report.d.ts +1 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +1 -0
- package/dist/report.js.map +1 -0
- package/dist/rube-goldberg.d.ts +3 -0
- package/dist/rube-goldberg.d.ts.map +1 -0
- package/dist/rube-goldberg.js +109 -0
- package/dist/rube-goldberg.js.map +1 -0
- package/dist/schema-loader.d.ts +1 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/schema-loader.js +1 -0
- package/dist/schema-loader.js.map +1 -0
- package/dist/validate.d.ts +1 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +2 -0
- package/dist/validate.js.map +1 -0
- package/package.json +8 -2
- package/schemas/core/roster.schema.json +17 -4
- package/schemas/core/weapon-keyword.schema.json +31 -0
- package/schemas/core/weapon.schema.json +22 -1
- package/schemas/enrichment/ability-dsl/effect.schema.json +23 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Tournament-standard battle sizes by points ceiling (10th ed). */
|
|
2
|
+
const BATTLE_SIZES = [
|
|
3
|
+
{ upper: 500, label: "Combat Patrol (500 Point limit)" },
|
|
4
|
+
{ upper: 1000, label: "Incursion (1000 Point limit)" },
|
|
5
|
+
{ upper: 2000, label: "Strike Force (2000 Point limit)" },
|
|
6
|
+
{ upper: 3000, label: "Onslaught (3000 Point limit)" },
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Synthesize a {@link ParsedRoster.battle_size_raw} from a points limit. The
|
|
10
|
+
* wtc/simple formats don't carry the battle-size label explicitly — they only
|
|
11
|
+
* report the total army points — so we map the limit to its standard label
|
|
12
|
+
* (the same one {@link mapBattleSize} expects).
|
|
13
|
+
*/
|
|
14
|
+
export function inferBattleSizeRaw(limit) {
|
|
15
|
+
if (limit === null)
|
|
16
|
+
return null;
|
|
17
|
+
for (const { upper, label } of BATTLE_SIZES) {
|
|
18
|
+
if (limit <= upper)
|
|
19
|
+
return label;
|
|
20
|
+
}
|
|
21
|
+
return BATTLE_SIZES[BATTLE_SIZES.length - 1].label; // beyond Onslaught: cap at Onslaught
|
|
22
|
+
}
|
|
23
|
+
const NX_PREFIX = /^(\d+)x\s+(.+)$/;
|
|
24
|
+
const INLINE_PTS = /^(.+?)\s*\[\s*(\d+)\s*pts?\s*\]\s*$/i;
|
|
25
|
+
const CHARACTER_SUFFIX = " Character";
|
|
26
|
+
const WARLORD_MARKER = "Warlord";
|
|
27
|
+
/**
|
|
28
|
+
* Classify each token in a comma-separated wargear list. Strips the markers
|
|
29
|
+
* that aren't real wargear — `Warlord`, the detachment "<Name> Character"
|
|
30
|
+
* keyword, and the inline `Name [N pts]` enhancement (simple format) — and
|
|
31
|
+
* collects everything else as {@link ParsedWargear} with optional `Nx` count.
|
|
32
|
+
*
|
|
33
|
+
* Tokens are pre-split: pass `["Armoured feet", "2x War Dog autocannon", ...]`.
|
|
34
|
+
*/
|
|
35
|
+
export function classifyWargearList(tokens) {
|
|
36
|
+
const wargear = [];
|
|
37
|
+
let is_warlord = false;
|
|
38
|
+
let is_character = false;
|
|
39
|
+
let enhancement_raw_name = null;
|
|
40
|
+
let enhancement_points = null;
|
|
41
|
+
for (const raw of tokens) {
|
|
42
|
+
const token = raw.trim();
|
|
43
|
+
if (!token)
|
|
44
|
+
continue;
|
|
45
|
+
if (token === WARLORD_MARKER) {
|
|
46
|
+
is_warlord = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (token.endsWith(CHARACTER_SUFFIX)) {
|
|
50
|
+
is_character = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Simple format inlines the enhancement as `Name [15 pts]`.
|
|
54
|
+
const pts = INLINE_PTS.exec(token);
|
|
55
|
+
if (pts) {
|
|
56
|
+
if (enhancement_raw_name === null) {
|
|
57
|
+
enhancement_raw_name = pts[1].trim();
|
|
58
|
+
enhancement_points = Number.parseInt(pts[2], 10);
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const nx = NX_PREFIX.exec(token);
|
|
63
|
+
if (nx) {
|
|
64
|
+
const count = Number.parseInt(nx[1], 10);
|
|
65
|
+
wargear.push({ raw_name: nx[2].trim(), count: count > 0 ? count : 1 });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
wargear.push({ raw_name: token, count: 1 });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { wargear, is_warlord, is_character, enhancement_raw_name, enhancement_points };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Split a wargear list on top-level commas. (No nested parentheses with commas
|
|
75
|
+
* are produced by NewRecruit, so a plain split is enough; the helper keeps
|
|
76
|
+
* intent explicit for future format quirks.)
|
|
77
|
+
*/
|
|
78
|
+
export function splitWargearList(text) {
|
|
79
|
+
return text
|
|
80
|
+
.split(",")
|
|
81
|
+
.map((s) => s.trim())
|
|
82
|
+
.filter((s) => s.length > 0);
|
|
83
|
+
}
|
|
84
|
+
/** Strip a trailing parenthetical (e.g. "Houndpack Lance (Marked Prey)" → "Houndpack Lance"). */
|
|
85
|
+
export function stripParenthetical(name) {
|
|
86
|
+
const idx = name.indexOf("(");
|
|
87
|
+
return idx >= 0 ? name.slice(0, idx).trim() : name.trim();
|
|
88
|
+
}
|
|
89
|
+
/** Parse a `(\d+) pts` or `[\d+ pts]` suffix from a unit header line. */
|
|
90
|
+
export function pointsFrom(token) {
|
|
91
|
+
const m = /\(\s*(\d+)\s*pts?\s*\)|\[\s*(\d+)\s*pts?\s*\]/i.exec(token);
|
|
92
|
+
if (!m)
|
|
93
|
+
return null;
|
|
94
|
+
return Number.parseInt(m[1] ?? m[2], 10);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=newrecruit-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-text.js","sourceRoot":"","sources":["../../src/import/newrecruit-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Helpers shared by the three NewRecruit text adapters (wtc-compact, wtc-full,\n * simple). These are pure string-massage utilities: they take format-specific\n * tokens and turn them into the format-agnostic {@link ParsedRoster} pieces.\n *\n * No business knowledge of dataset entities lives here — name resolution is\n * still {@link resolve}'s job downstream.\n *\n * @packageDocumentation\n */\nimport type { ParsedWargear } from \"./types.js\";\n\n/** Tournament-standard battle sizes by points ceiling (10th ed). */\nconst BATTLE_SIZES: readonly { upper: number; label: string }[] = [\n { upper: 500, label: \"Combat Patrol (500 Point limit)\" },\n { upper: 1000, label: \"Incursion (1000 Point limit)\" },\n { upper: 2000, label: \"Strike Force (2000 Point limit)\" },\n { upper: 3000, label: \"Onslaught (3000 Point limit)\" },\n];\n\n/**\n * Synthesize a {@link ParsedRoster.battle_size_raw} from a points limit. The\n * wtc/simple formats don't carry the battle-size label explicitly — they only\n * report the total army points — so we map the limit to its standard label\n * (the same one {@link mapBattleSize} expects).\n */\nexport function inferBattleSizeRaw(limit: number | null): string | null {\n if (limit === null) return null;\n for (const { upper, label } of BATTLE_SIZES) {\n if (limit <= upper) return label;\n }\n return BATTLE_SIZES[BATTLE_SIZES.length - 1].label; // beyond Onslaught: cap at Onslaught\n}\n\n/** Outcome of classifying a comma-separated wargear list. */\nexport interface ClassifiedTokens {\n wargear: ParsedWargear[];\n is_warlord: boolean;\n is_character: boolean;\n /** Enhancement raw name, when one was inlined in the wargear list (simple format). */\n enhancement_raw_name: string | null;\n /** Enhancement points cost when given inline (simple format), else null. */\n enhancement_points: number | null;\n}\n\nconst NX_PREFIX = /^(\\d+)x\\s+(.+)$/;\nconst INLINE_PTS = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*\\]\\s*$/i;\nconst CHARACTER_SUFFIX = \" Character\";\nconst WARLORD_MARKER = \"Warlord\";\n\n/**\n * Classify each token in a comma-separated wargear list. Strips the markers\n * that aren't real wargear — `Warlord`, the detachment \"<Name> Character\"\n * keyword, and the inline `Name [N pts]` enhancement (simple format) — and\n * collects everything else as {@link ParsedWargear} with optional `Nx` count.\n *\n * Tokens are pre-split: pass `[\"Armoured feet\", \"2x War Dog autocannon\", ...]`.\n */\nexport function classifyWargearList(tokens: readonly string[]): ClassifiedTokens {\n const wargear: ParsedWargear[] = [];\n let is_warlord = false;\n let is_character = false;\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n\n for (const raw of tokens) {\n const token = raw.trim();\n if (!token) continue;\n\n if (token === WARLORD_MARKER) {\n is_warlord = true;\n continue;\n }\n if (token.endsWith(CHARACTER_SUFFIX)) {\n is_character = true;\n continue;\n }\n\n // Simple format inlines the enhancement as `Name [15 pts]`.\n const pts = INLINE_PTS.exec(token);\n if (pts) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = pts[1].trim();\n enhancement_points = Number.parseInt(pts[2], 10);\n }\n continue;\n }\n\n const nx = NX_PREFIX.exec(token);\n if (nx) {\n const count = Number.parseInt(nx[1], 10);\n wargear.push({ raw_name: nx[2].trim(), count: count > 0 ? count : 1 });\n } else {\n wargear.push({ raw_name: token, count: 1 });\n }\n }\n\n return { wargear, is_warlord, is_character, enhancement_raw_name, enhancement_points };\n}\n\n/**\n * Split a wargear list on top-level commas. (No nested parentheses with commas\n * are produced by NewRecruit, so a plain split is enough; the helper keeps\n * intent explicit for future format quirks.)\n */\nexport function splitWargearList(text: string): string[] {\n return text\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/** Strip a trailing parenthetical (e.g. \"Houndpack Lance (Marked Prey)\" → \"Houndpack Lance\"). */\nexport function stripParenthetical(name: string): string {\n const idx = name.indexOf(\"(\");\n return idx >= 0 ? name.slice(0, idx).trim() : name.trim();\n}\n\n/** Parse a `(\\d+) pts` or `[\\d+ pts]` suffix from a unit header line. */\nexport function pointsFrom(token: string): number | null {\n const m = /\\(\\s*(\\d+)\\s*pts?\\s*\\)|\\[\\s*(\\d+)\\s*pts?\\s*\\]/i.exec(token);\n if (!m) return null;\n return Number.parseInt(m[1] ?? m[2], 10);\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NewRecruit "wtc-compact" and "wtc-full" text adapters.
|
|
3
|
+
*
|
|
4
|
+
* Both formats open with a `++++++++` summary header carrying FACTION KEYWORD,
|
|
5
|
+
* DETACHMENT, TOTAL ARMY POINTS, WARLORD, ENHANCEMENT(s), NUMBER OF UNITS, and
|
|
6
|
+
* SECONDARY tournament-objective shorthand. The body diverges:
|
|
7
|
+
*
|
|
8
|
+
* - **wtc-compact** — one unit per line:
|
|
9
|
+
* `[CharN: ]Nx <Unit> (P pts): <comma-separated wargear>`
|
|
10
|
+
* followed optionally by `Enhancement: <Name> (+P pts)` on the next line.
|
|
11
|
+
*
|
|
12
|
+
* - **wtc-full** — uppercase section headers (`BATTLELINE`, `ALLIED UNITS`),
|
|
13
|
+
* two-line unit blocks (`[CharN: ]Nx <Unit> (P pts)` then `N with <wargear>`),
|
|
14
|
+
* `Enhancement: <Name> (+P pts)` on its own line, and per-model-type
|
|
15
|
+
* breakdowns with `• Nx <ModelType>` + indented `N with <wargear>` lines.
|
|
16
|
+
*
|
|
17
|
+
* The {@link Roster} pivot stores units at unit granularity — per-model-type
|
|
18
|
+
* wargear breakdowns and `CharN:` slot numbers aren't modelled, so this adapter
|
|
19
|
+
* collapses them: the parsed unit's `model_count` is summed from the breakdown
|
|
20
|
+
* and its `wargear` is the union of every loadout under it. The `WARLORD` /
|
|
21
|
+
* `Houndpack Lance Character` tokens are stripped from the wargear list (and
|
|
22
|
+
* set `is_warlord`/`is_character` instead) so resolution doesn't try to look
|
|
23
|
+
* them up as weapons. Round-trips are at Roster level, not byte-for-byte.
|
|
24
|
+
*
|
|
25
|
+
* Enhancement points (`+15 pts`) are subtracted from the displayed unit total
|
|
26
|
+
* so `ParsedUnit.points` is the *base* unit cost — matching the ListForge
|
|
27
|
+
* convention where the unit's own cost line is base and the enhancement is a
|
|
28
|
+
* sibling cost line. `total_computed` walks every cost line just like ListForge
|
|
29
|
+
* (base unit pts + each enhancement pts).
|
|
30
|
+
*
|
|
31
|
+
* @packageDocumentation
|
|
32
|
+
*/
|
|
33
|
+
import type { FormatAdapter } from "./adapter.js";
|
|
34
|
+
export declare const newRecruitWtcCompactAdapter: FormatAdapter;
|
|
35
|
+
export declare const newRecruitWtcFullAdapter: FormatAdapter;
|
|
36
|
+
//# sourceMappingURL=newrecruit-wtc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-wtc.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+WlD,eAAO,MAAM,2BAA2B,EAAE,aAczC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,aActC,CAAC"}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { classifyWargearList, inferBattleSizeRaw, splitWargearList, stripParenthetical, } from "./newrecruit-text.js";
|
|
2
|
+
const WTC_HEADER_PREFIX = "+ FACTION KEYWORD:";
|
|
3
|
+
const HEADER_FIELDS = {
|
|
4
|
+
faction: /^\+\s*FACTION KEYWORD:\s*(.+?)\s*$/i,
|
|
5
|
+
detachment: /^\+\s*DETACHMENT:\s*(.+?)\s*$/i,
|
|
6
|
+
totalPoints: /^\+\s*TOTAL ARMY POINTS:\s*(\d+)\s*pts?\s*$/i,
|
|
7
|
+
pointsLimit: /^\+\s*POINTS LIMIT:\s*(\d+)\s*pts?\s*$/i,
|
|
8
|
+
listName: /^\+\s*LIST NAME:\s*(.+?)\s*$/i,
|
|
9
|
+
};
|
|
10
|
+
/** Pull the primary faction out of "Chaos - Chaos Knights" → "Chaos Knights". */
|
|
11
|
+
function factionFromKeyword(value) {
|
|
12
|
+
const parts = value.split(" - ");
|
|
13
|
+
return (parts[parts.length - 1] ?? value).trim();
|
|
14
|
+
}
|
|
15
|
+
/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */
|
|
16
|
+
function parseWtcHeader(text) {
|
|
17
|
+
const lines = text.split(/\r?\n/);
|
|
18
|
+
let faction_raw_name = null;
|
|
19
|
+
let detachment_raw_name = null;
|
|
20
|
+
let totalReported = null;
|
|
21
|
+
let pointsLimit = null;
|
|
22
|
+
let listName = null;
|
|
23
|
+
// Two `+++++…` fence lines wrap the header. Find them.
|
|
24
|
+
const fenceIndices = [];
|
|
25
|
+
for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {
|
|
26
|
+
if (/^\++\s*$/.test(lines[i]))
|
|
27
|
+
fenceIndices.push(i);
|
|
28
|
+
}
|
|
29
|
+
let sawFactionKeyword = false;
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (!line.startsWith("+"))
|
|
32
|
+
continue;
|
|
33
|
+
const factionMatch = HEADER_FIELDS.faction.exec(line);
|
|
34
|
+
if (factionMatch) {
|
|
35
|
+
faction_raw_name = factionFromKeyword(factionMatch[1]);
|
|
36
|
+
sawFactionKeyword = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const detMatch = HEADER_FIELDS.detachment.exec(line);
|
|
40
|
+
if (detMatch) {
|
|
41
|
+
detachment_raw_name = stripParenthetical(detMatch[1]);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);
|
|
45
|
+
if (ptsMatch) {
|
|
46
|
+
totalReported = Number.parseInt(ptsMatch[1], 10);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);
|
|
50
|
+
if (limitMatch) {
|
|
51
|
+
pointsLimit = Number.parseInt(limitMatch[1], 10);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const nameMatch = HEADER_FIELDS.listName.exec(line);
|
|
55
|
+
if (nameMatch) {
|
|
56
|
+
listName = nameMatch[1];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!sawFactionKeyword)
|
|
60
|
+
return null;
|
|
61
|
+
const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;
|
|
62
|
+
// POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —
|
|
63
|
+
// is the army's points ceiling. When the source carries only a single
|
|
64
|
+
// figure (the tournament default), fall back to it.
|
|
65
|
+
const declared_limit = pointsLimit ?? totalReported;
|
|
66
|
+
const battle_size_raw = inferBattleSizeRaw(declared_limit);
|
|
67
|
+
return {
|
|
68
|
+
header: {
|
|
69
|
+
name: listName ?? "Imported roster",
|
|
70
|
+
faction_raw_name,
|
|
71
|
+
detachment_raw_name,
|
|
72
|
+
declared_limit,
|
|
73
|
+
total_reported: totalReported,
|
|
74
|
+
battle_size_raw,
|
|
75
|
+
},
|
|
76
|
+
bodyStart,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// --- shared body helpers ----------------------------------------------------
|
|
80
|
+
const UNIT_HEADER_COMPACT = /^(?:Char\d+:\s*)?(\d+)x\s+(.+?)\s*\(\s*(\d+)\s*pts?\s*\)\s*:\s*(.*)$/i;
|
|
81
|
+
const UNIT_HEADER_FULL = /^(?:Char\d+:\s*)?(\d+)x\s+(.+?)\s*\(\s*(\d+)\s*pts?\s*\)\s*$/i;
|
|
82
|
+
const ENHANCEMENT_LINE = /^Enhancement:\s*(.+?)\s*\(\+\s*(\d+)\s*pts?\s*\)\s*$/i;
|
|
83
|
+
const WITH_PREFIX = /^(\d+)\s+with\s+(.*)$/i;
|
|
84
|
+
const MODEL_BREAKDOWN = /^\s*•\s*(\d+)x\s+(.+?)(?:\s*\[[^\]]*\])?\s*$/u;
|
|
85
|
+
const SECTION_HEADER = /^[A-Z][A-Z0-9 \-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.
|
|
86
|
+
const HEADER_LINE = /^\+/;
|
|
87
|
+
/**
|
|
88
|
+
* `N with X, Y, Z` means each of `N` models carries the same list — the weapon
|
|
89
|
+
* counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when
|
|
90
|
+
* the line has no `with` prefix.
|
|
91
|
+
*/
|
|
92
|
+
function parseWithGroup(text) {
|
|
93
|
+
const m = WITH_PREFIX.exec(text);
|
|
94
|
+
if (m) {
|
|
95
|
+
const n = Number.parseInt(m[1], 10);
|
|
96
|
+
return { multiplier: n > 0 ? n : 1, list: m[2] };
|
|
97
|
+
}
|
|
98
|
+
return { multiplier: 1, list: text };
|
|
99
|
+
}
|
|
100
|
+
function newUnit(name, displayed_pts, leading_count, is_character_prefix) {
|
|
101
|
+
return {
|
|
102
|
+
raw_name: name,
|
|
103
|
+
is_character: is_character_prefix,
|
|
104
|
+
is_warlord: false,
|
|
105
|
+
enhancement_raw_name: null,
|
|
106
|
+
displayed_pts,
|
|
107
|
+
enhancement_pts: 0,
|
|
108
|
+
model_count: leading_count > 0 ? leading_count : 1,
|
|
109
|
+
wargear: new Map(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function addWargear(unit, items) {
|
|
113
|
+
for (const { raw_name, count } of items) {
|
|
114
|
+
unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function applyWithGroup(unit, listText) {
|
|
118
|
+
const { multiplier, list } = parseWithGroup(listText);
|
|
119
|
+
const tokens = splitWargearList(list);
|
|
120
|
+
const cls = classifyWargearList(tokens);
|
|
121
|
+
if (cls.is_warlord)
|
|
122
|
+
unit.is_warlord = true;
|
|
123
|
+
if (cls.is_character)
|
|
124
|
+
unit.is_character = true;
|
|
125
|
+
// wtc never inlines the enhancement points in the wargear list (that's the
|
|
126
|
+
// simple format) but classifyWargearList silently absorbs it if it shows up;
|
|
127
|
+
// wtc's enhancement is always parsed off the explicit "Enhancement:" line.
|
|
128
|
+
const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));
|
|
129
|
+
addWargear(unit, scaled);
|
|
130
|
+
}
|
|
131
|
+
function finishUnit(unit) {
|
|
132
|
+
const displayed = unit.displayed_pts;
|
|
133
|
+
const points = displayed === null ? null : displayed - unit.enhancement_pts;
|
|
134
|
+
const wargear = [];
|
|
135
|
+
for (const [raw_name, count] of unit.wargear) {
|
|
136
|
+
wargear.push({ raw_name, count });
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
raw_name: unit.raw_name,
|
|
140
|
+
is_character: unit.is_character,
|
|
141
|
+
model_count: unit.model_count,
|
|
142
|
+
points,
|
|
143
|
+
is_warlord: unit.is_warlord,
|
|
144
|
+
enhancement_raw_name: unit.enhancement_raw_name,
|
|
145
|
+
enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,
|
|
146
|
+
wargear,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Compute total_computed by walking every parsed unit cost line. */
|
|
150
|
+
function computeTotal(units, enhancementPtsByIndex) {
|
|
151
|
+
let total = 0;
|
|
152
|
+
for (let i = 0; i < units.length; i += 1) {
|
|
153
|
+
total += units[i].points ?? 0;
|
|
154
|
+
total += enhancementPtsByIndex[i] ?? 0;
|
|
155
|
+
}
|
|
156
|
+
return total;
|
|
157
|
+
}
|
|
158
|
+
function attachEnhancement(unit, raw_name, pts) {
|
|
159
|
+
unit.enhancement_raw_name = raw_name.trim();
|
|
160
|
+
unit.enhancement_pts = pts;
|
|
161
|
+
}
|
|
162
|
+
// --- compact body parser ----------------------------------------------------
|
|
163
|
+
function parseCompactBody(body) {
|
|
164
|
+
const lines = body.split(/\r?\n/);
|
|
165
|
+
const units = [];
|
|
166
|
+
const enhancementPts = [];
|
|
167
|
+
let current = null;
|
|
168
|
+
const finalize = () => {
|
|
169
|
+
if (current) {
|
|
170
|
+
units.push(finishUnit(current));
|
|
171
|
+
enhancementPts.push(current.enhancement_pts);
|
|
172
|
+
current = null;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
for (const raw of lines) {
|
|
176
|
+
const line = raw.trim();
|
|
177
|
+
if (!line || HEADER_LINE.test(line) || /^\++$/.test(line))
|
|
178
|
+
continue;
|
|
179
|
+
const enhMatch = ENHANCEMENT_LINE.exec(line);
|
|
180
|
+
if (enhMatch && current) {
|
|
181
|
+
attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));
|
|
182
|
+
// Emit immediately so subsequent unit lines start fresh.
|
|
183
|
+
finalize();
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const unitMatch = UNIT_HEADER_COMPACT.exec(line);
|
|
187
|
+
if (unitMatch) {
|
|
188
|
+
finalize();
|
|
189
|
+
const leading_count = Number.parseInt(unitMatch[1], 10);
|
|
190
|
+
const name = unitMatch[2].trim();
|
|
191
|
+
const pts = Number.parseInt(unitMatch[3], 10);
|
|
192
|
+
const is_character_prefix = /^Char\d+:/i.test(line);
|
|
193
|
+
current = newUnit(name, pts, leading_count, is_character_prefix);
|
|
194
|
+
applyWithGroup(current, unitMatch[4]);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
finalize();
|
|
199
|
+
return { units, enhancementPts };
|
|
200
|
+
}
|
|
201
|
+
// --- full body parser -------------------------------------------------------
|
|
202
|
+
function parseFullBody(body) {
|
|
203
|
+
const lines = body.split(/\r?\n/);
|
|
204
|
+
const units = [];
|
|
205
|
+
const enhancementPts = [];
|
|
206
|
+
let current = null;
|
|
207
|
+
let breakdownModels = 0;
|
|
208
|
+
const finalize = () => {
|
|
209
|
+
if (current) {
|
|
210
|
+
if (breakdownModels > 0)
|
|
211
|
+
current.model_count = breakdownModels;
|
|
212
|
+
units.push(finishUnit(current));
|
|
213
|
+
enhancementPts.push(current.enhancement_pts);
|
|
214
|
+
current = null;
|
|
215
|
+
breakdownModels = 0;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
for (const raw of lines) {
|
|
219
|
+
const line = raw.trim();
|
|
220
|
+
if (!line || HEADER_LINE.test(line) || /^\++$/.test(line))
|
|
221
|
+
continue;
|
|
222
|
+
if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {
|
|
223
|
+
finalize();
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const enhMatch = ENHANCEMENT_LINE.exec(line);
|
|
227
|
+
if (enhMatch && current) {
|
|
228
|
+
attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const unitMatch = UNIT_HEADER_FULL.exec(line);
|
|
232
|
+
if (unitMatch) {
|
|
233
|
+
finalize();
|
|
234
|
+
const leading_count = Number.parseInt(unitMatch[1], 10);
|
|
235
|
+
const name = unitMatch[2].trim();
|
|
236
|
+
const pts = Number.parseInt(unitMatch[3], 10);
|
|
237
|
+
const is_character_prefix = /^Char\d+:/i.test(line);
|
|
238
|
+
current = newUnit(name, pts, leading_count, is_character_prefix);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const breakdown = MODEL_BREAKDOWN.exec(raw);
|
|
242
|
+
if (breakdown && current) {
|
|
243
|
+
breakdownModels += Number.parseInt(breakdown[1], 10);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (WITH_PREFIX.test(line) && current) {
|
|
247
|
+
applyWithGroup(current, line);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
finalize();
|
|
252
|
+
return { units, enhancementPts };
|
|
253
|
+
}
|
|
254
|
+
// --- multi-force detection --------------------------------------------------
|
|
255
|
+
/** Heuristic for `multi_force`: are there units with "ALLIED" decorating
|
|
256
|
+
* the body? wtc-full has an explicit `ALLIED UNITS` section header; compact
|
|
257
|
+
* has no section markers but the user-facing summary header counts every unit
|
|
258
|
+
* together, so detect from explicit section presence. */
|
|
259
|
+
function detectMultiForce(text, format) {
|
|
260
|
+
if (format === "wtc-full") {
|
|
261
|
+
return /^ALLIED UNITS\s*$/im.test(text);
|
|
262
|
+
}
|
|
263
|
+
// wtc-compact has no section header. Multi-force surfaces only via the
|
|
264
|
+
// primary-faction summary; assume single-force unless we add a richer marker.
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
// --- adapters ---------------------------------------------------------------
|
|
268
|
+
function isWtcText(decoded) {
|
|
269
|
+
if (typeof decoded !== "string")
|
|
270
|
+
return null;
|
|
271
|
+
// Both wtc formats begin with the FACTION KEYWORD header line (possibly
|
|
272
|
+
// after some leading whitespace/fence characters).
|
|
273
|
+
if (!decoded.includes(WTC_HEADER_PREFIX))
|
|
274
|
+
return null;
|
|
275
|
+
return decoded;
|
|
276
|
+
}
|
|
277
|
+
/** Distinguishes wtc-full from wtc-compact: full has a line starting with
|
|
278
|
+
* `\d+ with ` at the start of a body line (compact only puts `N with` after
|
|
279
|
+
* `:` on the same line as the unit header). */
|
|
280
|
+
function isFullFormat(text) {
|
|
281
|
+
return /^[\t ]*\d+\s+with\b/m.test(text);
|
|
282
|
+
}
|
|
283
|
+
function parseWith(text, format) {
|
|
284
|
+
const parsed = parseWtcHeader(text);
|
|
285
|
+
if (!parsed) {
|
|
286
|
+
throw new Error(`${format}: missing "+ FACTION KEYWORD:" header`);
|
|
287
|
+
}
|
|
288
|
+
const { header, bodyStart } = parsed;
|
|
289
|
+
const body = text.split(/\r?\n/).slice(bodyStart).join("\n");
|
|
290
|
+
const { units, enhancementPts } = format === "wtc-full" ? parseFullBody(body) : parseCompactBody(body);
|
|
291
|
+
return {
|
|
292
|
+
name: header.name,
|
|
293
|
+
generated_by: null,
|
|
294
|
+
faction_raw_name: header.faction_raw_name,
|
|
295
|
+
detachment_raw_name: header.detachment_raw_name,
|
|
296
|
+
battle_size_raw: header.battle_size_raw,
|
|
297
|
+
declared_limit: header.declared_limit,
|
|
298
|
+
total_reported: header.total_reported,
|
|
299
|
+
total_computed: computeTotal(units, enhancementPts),
|
|
300
|
+
units,
|
|
301
|
+
multi_force: detectMultiForce(text, format),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
export const newRecruitWtcCompactAdapter = {
|
|
305
|
+
id: "newrecruit-wtc-compact",
|
|
306
|
+
matches(decoded) {
|
|
307
|
+
const text = isWtcText(decoded);
|
|
308
|
+
if (text === null)
|
|
309
|
+
return false;
|
|
310
|
+
return !isFullFormat(text);
|
|
311
|
+
},
|
|
312
|
+
parse(decoded) {
|
|
313
|
+
const text = isWtcText(decoded);
|
|
314
|
+
if (text === null)
|
|
315
|
+
throw new Error("newrecruit-wtc-compact: input is not a string");
|
|
316
|
+
return parseWith(text, "wtc-compact");
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
export const newRecruitWtcFullAdapter = {
|
|
320
|
+
id: "newrecruit-wtc-full",
|
|
321
|
+
matches(decoded) {
|
|
322
|
+
const text = isWtcText(decoded);
|
|
323
|
+
if (text === null)
|
|
324
|
+
return false;
|
|
325
|
+
return isFullFormat(text);
|
|
326
|
+
},
|
|
327
|
+
parse(decoded) {
|
|
328
|
+
const text = isWtcText(decoded);
|
|
329
|
+
if (text === null)
|
|
330
|
+
throw new Error("newrecruit-wtc-full: input is not a string");
|
|
331
|
+
return parseWith(text, "wtc-full");
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
//# sourceMappingURL=newrecruit-wtc.js.map
|
|
@@ -0,0 +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 *\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 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/** Pull the primary faction out of \"Chaos - Chaos Knights\" → \"Chaos Knights\". */\nfunction factionFromKeyword(value: string): string {\n const parts = value.split(\" - \");\n return (parts[parts.length - 1] ?? value).trim();\n}\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;\nconst MODEL_BREAKDOWN = /^\\s*•\\s*(\\d+)x\\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 const breakdown = MODEL_BREAKDOWN.exec(raw);\n if (breakdown && current) {\n breakdownModels += Number.parseInt(breakdown[1], 10);\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\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 return !isFullFormat(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"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a {@link ParsedRoster} onto 40kdc entity ids, producing a {@link Roster}.
|
|
3
|
+
*
|
|
4
|
+
* Resolution is lenient: a name that doesn't match a 40kdc entity yields a
|
|
5
|
+
* {@link ResolvedRef} with `id: null`, `resolved: false`, and up to five
|
|
6
|
+
* candidate suggestions — the roster is never dropped or rejected. Everything
|
|
7
|
+
* that didn't resolve cleanly is summarised in the {@link Diagnostics} block.
|
|
8
|
+
*
|
|
9
|
+
* Matching reuses the dataset's own lookups ({@link Collection.find},
|
|
10
|
+
* {@link Collection.findAll}, {@link Collection.byFaction}) and
|
|
11
|
+
* {@link normalizeName}; there is no bespoke fuzzy matcher. Faction is resolved
|
|
12
|
+
* first so unit/detachment/enhancement lookups can be scoped to it — the same
|
|
13
|
+
* unit id can appear under several factions, so scoping disambiguates.
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import type { Dataset } from "../data/dataset.js";
|
|
18
|
+
import type { ParsedRoster, Roster, RosterFormat } from "./types.js";
|
|
19
|
+
export declare function resolve(parsed: ParsedRoster, ds: Dataset, format?: RosterFormat): Roster;
|
|
20
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +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,EACN,YAAY,EAIb,MAAM,YAAY,CAAC;AAwDpB,wBAAgB,OAAO,CACrB,MAAM,EAAE,YAAY,EACpB,EAAE,EAAE,OAAO,EACX,MAAM,GAAE,YAA0B,GACjC,MAAM,CAuER"}
|