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