@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,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"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { classifyWargearList, splitWargearList } from "./newrecruit-text.js";
|
|
2
|
+
const FIRST_LINE = /^(.+)\s-\s\[\s*(\d+)\s*pts?\s*\]\s*$/i;
|
|
3
|
+
const ROSTER_HEADER = /^#\s*\+\+\s*Army Roster\s*\+\+\s*\[\s*(\d+)\s*pts?\s*\]\s*$/i;
|
|
4
|
+
const SECTION_HEADER = /^##\s*(.+?)(?:\s*\[\s*(\d+)\s*pts?\s*\])?\s*$/;
|
|
5
|
+
const UNIT_LINE = /^(.+?)\s*\[\s*(\d+)\s*pts?\s*\](?:\s*:\s*(.*))?$/i;
|
|
6
|
+
const BULLET = /^\s*•\s*(\d+)x\s+(.+?)(?:\s*\[\s*(\d+)\s*pts?\s*\])?(?:\s*:\s*(.*))?\s*$/u;
|
|
7
|
+
function newUnit(name, displayed_pts) {
|
|
8
|
+
return {
|
|
9
|
+
raw_name: name,
|
|
10
|
+
is_character: false,
|
|
11
|
+
is_warlord: false,
|
|
12
|
+
enhancement_raw_name: null,
|
|
13
|
+
enhancement_pts: 0,
|
|
14
|
+
displayed_pts,
|
|
15
|
+
model_count: 1,
|
|
16
|
+
wargear: new Map(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function addWargear(unit, items) {
|
|
20
|
+
for (const { raw_name, count } of items) {
|
|
21
|
+
unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function applyTokens(unit, tokensCsv, multiplier = 1) {
|
|
25
|
+
const tokens = splitWargearList(tokensCsv);
|
|
26
|
+
const cls = classifyWargearList(tokens);
|
|
27
|
+
if (cls.is_warlord)
|
|
28
|
+
unit.is_warlord = true;
|
|
29
|
+
if (cls.is_character)
|
|
30
|
+
unit.is_character = true;
|
|
31
|
+
if (cls.enhancement_raw_name && unit.enhancement_raw_name === null) {
|
|
32
|
+
unit.enhancement_raw_name = cls.enhancement_raw_name;
|
|
33
|
+
unit.enhancement_pts = cls.enhancement_points ?? 0;
|
|
34
|
+
}
|
|
35
|
+
const scaled = cls.wargear.map((w) => ({
|
|
36
|
+
raw_name: w.raw_name,
|
|
37
|
+
count: w.count * multiplier,
|
|
38
|
+
}));
|
|
39
|
+
addWargear(unit, scaled);
|
|
40
|
+
}
|
|
41
|
+
function finishUnit(unit) {
|
|
42
|
+
const points = unit.displayed_pts === null ? null : unit.displayed_pts - unit.enhancement_pts;
|
|
43
|
+
return {
|
|
44
|
+
raw_name: unit.raw_name,
|
|
45
|
+
is_character: unit.is_character,
|
|
46
|
+
model_count: unit.model_count,
|
|
47
|
+
points,
|
|
48
|
+
is_warlord: unit.is_warlord,
|
|
49
|
+
enhancement_raw_name: unit.enhancement_raw_name,
|
|
50
|
+
enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,
|
|
51
|
+
wargear: [...unit.wargear].map(([raw_name, count]) => ({ raw_name, count })),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function parseFirstLine(line) {
|
|
55
|
+
const m = FIRST_LINE.exec(line);
|
|
56
|
+
if (!m)
|
|
57
|
+
return null;
|
|
58
|
+
const declared_limit = Number.parseInt(m[2], 10);
|
|
59
|
+
const parts = m[1].split(" - ").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
60
|
+
if (parts.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
const list_name = parts[parts.length - 1];
|
|
63
|
+
const faction = parts.length >= 2 ? parts[parts.length - 2] : null;
|
|
64
|
+
return { name: list_name, faction, declared_limit };
|
|
65
|
+
}
|
|
66
|
+
export const newRecruitSimpleAdapter = {
|
|
67
|
+
id: "newrecruit-simple",
|
|
68
|
+
matches(decoded) {
|
|
69
|
+
if (typeof decoded !== "string")
|
|
70
|
+
return false;
|
|
71
|
+
const lines = decoded.split(/\r?\n/);
|
|
72
|
+
const firstNonBlank = lines.find((l) => l.trim().length > 0);
|
|
73
|
+
if (!firstNonBlank)
|
|
74
|
+
return false;
|
|
75
|
+
if (!FIRST_LINE.test(firstNonBlank))
|
|
76
|
+
return false;
|
|
77
|
+
return /^#\s*\+\+\s*Army Roster\s*\+\+/m.test(decoded);
|
|
78
|
+
},
|
|
79
|
+
parse(decoded) {
|
|
80
|
+
if (typeof decoded !== "string") {
|
|
81
|
+
throw new Error("newrecruit-simple: input is not a string");
|
|
82
|
+
}
|
|
83
|
+
const lines = decoded.split(/\r?\n/);
|
|
84
|
+
let name = "Imported roster";
|
|
85
|
+
let faction_raw_name = null;
|
|
86
|
+
let declared_limit = null;
|
|
87
|
+
let total_reported = null;
|
|
88
|
+
let detachment_raw_name = null;
|
|
89
|
+
let battle_size_raw = null;
|
|
90
|
+
const units = [];
|
|
91
|
+
let current = null;
|
|
92
|
+
let multi_force = false;
|
|
93
|
+
let section = "preamble";
|
|
94
|
+
const enhancementPts = [];
|
|
95
|
+
const finalize = () => {
|
|
96
|
+
if (current) {
|
|
97
|
+
enhancementPts.push(current.enhancement_pts);
|
|
98
|
+
units.push(finishUnit(current));
|
|
99
|
+
current = null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
103
|
+
const raw = lines[i];
|
|
104
|
+
const line = raw.trim();
|
|
105
|
+
if (!line)
|
|
106
|
+
continue;
|
|
107
|
+
// First non-blank line carries `<breadcrumb> - <faction> - <list> - [N pts]`.
|
|
108
|
+
if (section === "preamble" && name === "Imported roster") {
|
|
109
|
+
const first = parseFirstLine(line);
|
|
110
|
+
if (first) {
|
|
111
|
+
name = first.name;
|
|
112
|
+
faction_raw_name = first.faction;
|
|
113
|
+
declared_limit = first.declared_limit;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const rosterMatch = ROSTER_HEADER.exec(line);
|
|
118
|
+
if (rosterMatch) {
|
|
119
|
+
total_reported = Number.parseInt(rosterMatch[1], 10);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const sectionMatch = SECTION_HEADER.exec(line);
|
|
123
|
+
if (sectionMatch) {
|
|
124
|
+
finalize();
|
|
125
|
+
const heading = sectionMatch[1].trim().toLowerCase();
|
|
126
|
+
if (heading === "configuration") {
|
|
127
|
+
section = "configuration";
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
section = "units";
|
|
131
|
+
if (heading.includes("allied"))
|
|
132
|
+
multi_force = true;
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (section === "configuration") {
|
|
137
|
+
const idx = line.indexOf(":");
|
|
138
|
+
if (idx > 0) {
|
|
139
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
140
|
+
const value = line.slice(idx + 1).trim();
|
|
141
|
+
if (key === "battle size")
|
|
142
|
+
battle_size_raw = value;
|
|
143
|
+
else if (key === "detachment")
|
|
144
|
+
detachment_raw_name = value;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Unit section. A bullet line extends the *current* unit.
|
|
149
|
+
const bulletMatch = BULLET.exec(raw);
|
|
150
|
+
if (bulletMatch && current) {
|
|
151
|
+
const count = Number.parseInt(bulletMatch[1], 10);
|
|
152
|
+
// Bullets may add to the unit's model count beyond the implicit 1 we
|
|
153
|
+
// set when we created it from the unit header.
|
|
154
|
+
if (current.wargear.size === 0 && current.model_count === 1) {
|
|
155
|
+
// First bullet: replace the implicit single-model assumption.
|
|
156
|
+
current.model_count = count;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
current.model_count += count;
|
|
160
|
+
}
|
|
161
|
+
if (bulletMatch[4])
|
|
162
|
+
applyTokens(current, bulletMatch[4], count);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const unitMatch = UNIT_LINE.exec(line);
|
|
166
|
+
if (unitMatch) {
|
|
167
|
+
finalize();
|
|
168
|
+
const unitName = unitMatch[1].trim();
|
|
169
|
+
const pts = Number.parseInt(unitMatch[2], 10);
|
|
170
|
+
current = newUnit(unitName, pts);
|
|
171
|
+
const inlineWargear = unitMatch[3]?.trim() ?? "";
|
|
172
|
+
if (inlineWargear.length > 0) {
|
|
173
|
+
applyTokens(current, inlineWargear, 1);
|
|
174
|
+
}
|
|
175
|
+
// Leave model_count at the default 1. If `•` bullet lines follow, the
|
|
176
|
+
// bullet handler resets model_count to the (summed) bullet counts.
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finalize();
|
|
181
|
+
let total_computed = 0;
|
|
182
|
+
for (let i = 0; i < units.length; i += 1) {
|
|
183
|
+
total_computed += units[i].points ?? 0;
|
|
184
|
+
total_computed += enhancementPts[i] ?? 0;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
name,
|
|
188
|
+
generated_by: null,
|
|
189
|
+
faction_raw_name,
|
|
190
|
+
detachment_raw_name,
|
|
191
|
+
battle_size_raw,
|
|
192
|
+
declared_limit,
|
|
193
|
+
total_reported,
|
|
194
|
+
total_computed,
|
|
195
|
+
units,
|
|
196
|
+
multi_force,
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=newrecruit-simple.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-simple.js","sourceRoot":"","sources":["../../src/import/newrecruit-simple.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"simple\" markdown-ish text adapter.\n *\n * Shape:\n * ```\n * <breadcrumb> - <faction> - <list name> - [N pts]\n *\n * # ++ Army Roster ++ [N pts]\n * ## Configuration\n * Battle Size: <Label>\n * Detachment: <Name>\n * Show/Hide Options: ...\n *\n * ## <Section> [N pts]\n * <Unit> [N pts]: <wargear>\n * <Unit> [N pts]:\n * • <count>x <ModelType>[ [N pts]]: <wargear>\n * ```\n *\n * Enhancements are inlined in the wargear list as `<Name> [N pts]` — the only\n * wargear token wearing a `[…]` pts suffix. `Warlord` and the detachment\n * \"<X> Character\" keyword are also stripped from the list and set as flags.\n * Per-model-type breakdowns under `•` lines are collapsed onto the parent unit.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport { classifyWargearList, splitWargearList } from \"./newrecruit-text.js\";\n\nconst FIRST_LINE = /^(.+)\\s-\\s\\[\\s*(\\d+)\\s*pts?\\s*\\]\\s*$/i;\nconst ROSTER_HEADER = /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+\\s*\\[\\s*(\\d+)\\s*pts?\\s*\\]\\s*$/i;\nconst SECTION_HEADER = /^##\\s*(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*\\])?\\s*$/;\nconst UNIT_LINE = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*\\](?:\\s*:\\s*(.*))?$/i;\nconst BULLET =\n /^\\s*•\\s*(\\d+)x\\s+(.+?)(?:\\s*\\[\\s*(\\d+)\\s*pts?\\s*\\])?(?:\\s*:\\s*(.*))?\\s*$/u;\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n enhancement_pts: number;\n displayed_pts: number | null;\n model_count: number;\n /** Aggregated wargear, keyed by name. Counts sum across `• Nx ModelType` breakdowns. */\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number | null): UnitBuilder {\n return {\n raw_name: name,\n is_character: false,\n is_warlord: false,\n enhancement_raw_name: null,\n enhancement_pts: 0,\n displayed_pts,\n model_count: 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyTokens(unit: UnitBuilder, tokensCsv: string, multiplier = 1): void {\n const tokens = splitWargearList(tokensCsv);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n if (cls.enhancement_raw_name && unit.enhancement_raw_name === null) {\n unit.enhancement_raw_name = cls.enhancement_raw_name;\n unit.enhancement_pts = cls.enhancement_points ?? 0;\n }\n const scaled = cls.wargear.map((w) => ({\n raw_name: w.raw_name,\n count: w.count * multiplier,\n }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const points =\n unit.displayed_pts === null ? null : unit.displayed_pts - unit.enhancement_pts;\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear: [...unit.wargear].map(([raw_name, count]) => ({ raw_name, count })),\n };\n}\n\nfunction parseFirstLine(line: string): { name: string; faction: string | null; declared_limit: number | null } | null {\n const m = FIRST_LINE.exec(line);\n if (!m) return null;\n const declared_limit = Number.parseInt(m[2], 10);\n const parts = m[1].split(\" - \").map((s) => s.trim()).filter((s) => s.length > 0);\n if (parts.length === 0) return null;\n const list_name = parts[parts.length - 1];\n const faction = parts.length >= 2 ? parts[parts.length - 2] : null;\n return { name: list_name, faction, declared_limit };\n}\n\ntype Section = \"preamble\" | \"configuration\" | \"units\";\n\nexport const newRecruitSimpleAdapter: FormatAdapter = {\n id: \"newrecruit-simple\",\n\n matches(decoded: unknown): boolean {\n if (typeof decoded !== \"string\") return false;\n const lines = decoded.split(/\\r?\\n/);\n const firstNonBlank = lines.find((l) => l.trim().length > 0);\n if (!firstNonBlank) return false;\n if (!FIRST_LINE.test(firstNonBlank)) return false;\n return /^#\\s*\\+\\+\\s*Army Roster\\s*\\+\\+/m.test(decoded);\n },\n\n parse(decoded: unknown): ParsedRoster {\n if (typeof decoded !== \"string\") {\n throw new Error(\"newrecruit-simple: input is not a string\");\n }\n const lines = decoded.split(/\\r?\\n/);\n\n let name = \"Imported roster\";\n let faction_raw_name: string | null = null;\n let declared_limit: number | null = null;\n let total_reported: number | null = null;\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n let current: UnitBuilder | null = null;\n let multi_force = false;\n let section: Section = \"preamble\";\n const enhancementPts: number[] = [];\n\n const finalize = (): void => {\n if (current) {\n enhancementPts.push(current.enhancement_pts);\n units.push(finishUnit(current));\n current = null;\n }\n };\n\n for (let i = 0; i < lines.length; i += 1) {\n const raw = lines[i];\n const line = raw.trim();\n if (!line) continue;\n\n // First non-blank line carries `<breadcrumb> - <faction> - <list> - [N pts]`.\n if (section === \"preamble\" && name === \"Imported roster\") {\n const first = parseFirstLine(line);\n if (first) {\n name = first.name;\n faction_raw_name = first.faction;\n declared_limit = first.declared_limit;\n continue;\n }\n }\n\n const rosterMatch = ROSTER_HEADER.exec(line);\n if (rosterMatch) {\n total_reported = Number.parseInt(rosterMatch[1], 10);\n continue;\n }\n\n const sectionMatch = SECTION_HEADER.exec(line);\n if (sectionMatch) {\n finalize();\n const heading = sectionMatch[1].trim().toLowerCase();\n if (heading === \"configuration\") {\n section = \"configuration\";\n } else {\n section = \"units\";\n if (heading.includes(\"allied\")) multi_force = true;\n }\n continue;\n }\n\n if (section === \"configuration\") {\n const idx = line.indexOf(\":\");\n if (idx > 0) {\n const key = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n if (key === \"battle size\") battle_size_raw = value;\n else if (key === \"detachment\") detachment_raw_name = value;\n }\n continue;\n }\n\n // Unit section. A bullet line extends the *current* unit.\n const bulletMatch = BULLET.exec(raw);\n if (bulletMatch && current) {\n const count = Number.parseInt(bulletMatch[1], 10);\n // Bullets may add to the unit's model count beyond the implicit 1 we\n // set when we created it from the unit header.\n if (current.wargear.size === 0 && current.model_count === 1) {\n // First bullet: replace the implicit single-model assumption.\n current.model_count = count;\n } else {\n current.model_count += count;\n }\n if (bulletMatch[4]) applyTokens(current, bulletMatch[4], count);\n continue;\n }\n\n const unitMatch = UNIT_LINE.exec(line);\n if (unitMatch) {\n finalize();\n const unitName = unitMatch[1].trim();\n const pts = Number.parseInt(unitMatch[2], 10);\n current = newUnit(unitName, pts);\n const inlineWargear = unitMatch[3]?.trim() ?? \"\";\n if (inlineWargear.length > 0) {\n applyTokens(current, inlineWargear, 1);\n }\n // Leave model_count at the default 1. If `•` bullet lines follow, the\n // bullet handler resets model_count to the (summed) bullet counts.\n continue;\n }\n }\n finalize();\n\n let total_computed = 0;\n for (let i = 0; i < units.length; i += 1) {\n total_computed += units[i].points ?? 0;\n total_computed += enhancementPts[i] ?? 0;\n }\n\n return {\n name,\n generated_by: null,\n faction_raw_name,\n detachment_raw_name,\n battle_size_raw,\n declared_limit,\n total_reported,\n total_computed,\n units,\n multi_force,\n };\n },\n};\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers shared by the three NewRecruit text adapters (wtc-compact, wtc-full,
|
|
3
|
+
* simple). These are pure string-massage utilities: they take format-specific
|
|
4
|
+
* tokens and turn them into the format-agnostic {@link ParsedRoster} pieces.
|
|
5
|
+
*
|
|
6
|
+
* No business knowledge of dataset entities lives here — name resolution is
|
|
7
|
+
* still {@link resolve}'s job downstream.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import type { ParsedWargear } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Synthesize a {@link ParsedRoster.battle_size_raw} from a points limit. The
|
|
14
|
+
* wtc/simple formats don't carry the battle-size label explicitly — they only
|
|
15
|
+
* report the total army points — so we map the limit to its standard label
|
|
16
|
+
* (the same one {@link mapBattleSize} expects).
|
|
17
|
+
*/
|
|
18
|
+
export declare function inferBattleSizeRaw(limit: number | null): string | null;
|
|
19
|
+
/** Outcome of classifying a comma-separated wargear list. */
|
|
20
|
+
export interface ClassifiedTokens {
|
|
21
|
+
wargear: ParsedWargear[];
|
|
22
|
+
is_warlord: boolean;
|
|
23
|
+
is_character: boolean;
|
|
24
|
+
/** Enhancement raw name, when one was inlined in the wargear list (simple format). */
|
|
25
|
+
enhancement_raw_name: string | null;
|
|
26
|
+
/** Enhancement points cost when given inline (simple format), else null. */
|
|
27
|
+
enhancement_points: number | null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Classify each token in a comma-separated wargear list. Strips the markers
|
|
31
|
+
* that aren't real wargear — `Warlord`, the detachment "<Name> Character"
|
|
32
|
+
* keyword, and the inline `Name [N pts]` enhancement (simple format) — and
|
|
33
|
+
* collects everything else as {@link ParsedWargear} with optional `Nx` count.
|
|
34
|
+
*
|
|
35
|
+
* Tokens are pre-split: pass `["Armoured feet", "2x War Dog autocannon", ...]`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function classifyWargearList(tokens: readonly string[]): ClassifiedTokens;
|
|
38
|
+
/**
|
|
39
|
+
* Split a wargear list on top-level commas. (No nested parentheses with commas
|
|
40
|
+
* are produced by NewRecruit, so a plain split is enough; the helper keeps
|
|
41
|
+
* intent explicit for future format quirks.)
|
|
42
|
+
*/
|
|
43
|
+
export declare function splitWargearList(text: string): string[];
|
|
44
|
+
/** Strip a trailing parenthetical (e.g. "Houndpack Lance (Marked Prey)" → "Houndpack Lance"). */
|
|
45
|
+
export declare function stripParenthetical(name: string): string;
|
|
46
|
+
/** Parse a `(\d+) pts` or `[\d+ pts]` suffix from a unit header line. */
|
|
47
|
+
export declare function pointsFrom(token: string): number | null;
|
|
48
|
+
//# sourceMappingURL=newrecruit-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-text.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAMtE;AAED,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,sFAAsF;IACtF,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,4EAA4E;IAC5E,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAwC/E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKvD;AAED,iGAAiG;AACjG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvD"}
|