@alpaca-software/40kdc-data 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- 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 +73 -0
- package/dist/abilities-resolver/resolver.d.ts.map +1 -0
- package/dist/abilities-resolver/resolver.js +142 -0
- package/dist/abilities-resolver/resolver.js.map +1 -0
- package/dist/audit-coverage.d.ts +78 -0
- package/dist/audit-coverage.d.ts.map +1 -0
- package/dist/audit-coverage.js +341 -0
- package/dist/audit-coverage.js.map +1 -0
- package/dist/author-batch.d.ts +147 -0
- package/dist/author-batch.d.ts.map +1 -0
- package/dist/author-batch.js +675 -0
- package/dist/author-batch.js.map +1 -0
- package/dist/author-input.d.ts +37 -0
- package/dist/author-input.d.ts.map +1 -0
- package/dist/author-input.js +162 -0
- package/dist/author-input.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 +9 -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 +10 -4
- 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/attribution.d.ts +66 -0
- package/dist/cruncher/attribution.d.ts.map +1 -0
- package/dist/cruncher/attribution.js +88 -0
- package/dist/cruncher/attribution.js.map +1 -0
- package/dist/cruncher/buffs.d.ts +206 -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 +101 -0
- package/dist/cruncher/from-dsl.d.ts.map +1 -0
- package/dist/cruncher/from-dsl.js +968 -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 +12 -0
- package/dist/cruncher/index.d.ts.map +1 -0
- package/dist/cruncher/index.js +12 -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 +10 -0
- package/dist/data/collection.d.ts.map +1 -0
- package/dist/data/collection.js +15 -0
- package/dist/data/collection.js.map +1 -0
- package/dist/data/dataset.d.ts +132 -2
- package/dist/data/dataset.d.ts.map +1 -0
- package/dist/data/dataset.js +248 -1
- package/dist/data/dataset.js.map +1 -0
- package/dist/data/entities.d.ts +67 -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 +10 -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 +58 -0
- package/dist/data/roster-resolve.d.ts.map +1 -0
- package/dist/data/roster-resolve.js +82 -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 +22 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +28 -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/rosterizer.d.ts +3 -0
- package/dist/export/rosterizer.d.ts.map +1 -0
- package/dist/export/rosterizer.js +144 -0
- package/dist/export/rosterizer.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 +274 -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/gw.d.ts +69 -0
- package/dist/import/gw.d.ts.map +1 -0
- package/dist/import/gw.js +245 -0
- package/dist/import/gw.js.map +1 -0
- package/dist/import/import-roster.d.ts +84 -0
- package/dist/import/import-roster.d.ts.map +1 -0
- package/dist/import/import-roster.js +207 -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 +22 -2
- 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 +51 -0
- package/dist/import/newrecruit-text.d.ts.map +1 -0
- package/dist/import/newrecruit-text.js +102 -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 +337 -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/rosterizer.d.ts +70 -0
- package/dist/import/rosterizer.d.ts.map +1 -0
- package/dist/import/rosterizer.js +348 -0
- package/dist/import/rosterizer.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 +247 -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/runner.d.ts +38 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +492 -0
- package/dist/runner.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/scrub-ip.d.ts +14 -0
- package/dist/scrub-ip.d.ts.map +1 -0
- package/dist/scrub-ip.js +88 -0
- package/dist/scrub-ip.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 +15 -3
- package/schemas/core/roster.schema.json +19 -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,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the roster exporters.
|
|
3
|
+
*
|
|
4
|
+
* Exporters are deterministic and Dataset-free: they read the Roster only and
|
|
5
|
+
* regenerate format-specific decoration (display names, Char-slot numbering,
|
|
6
|
+
* displayed unit totals) from what's stored. Anything the Roster doesn't model
|
|
7
|
+
* — char-slot numbers, the detachment "<X> Character" keyword, secondary-
|
|
8
|
+
* objective summaries — is either derived heuristically here or dropped.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import type { Roster, RosterUnit } from "../import/types.js";
|
|
13
|
+
/** Convert a kebab-case entity id ("chaos-knights") to a Title Case display
|
|
14
|
+
* name ("Chaos Knights"). This is the round-trip best-effort when the Roster
|
|
15
|
+
* doesn't store the source's raw faction/detachment name. */
|
|
16
|
+
export declare function titleCaseId(id: string | null): string | null;
|
|
17
|
+
/** Sum of unit base pts + enhancement pts (= the figure most text formats display). */
|
|
18
|
+
export declare function displayedUnitPoints(u: RosterUnit): number | null;
|
|
19
|
+
/** Sum of every unit's displayed total + every enhancement cost line. */
|
|
20
|
+
export declare function totalArmyPoints(roster: Roster): number;
|
|
21
|
+
/**
|
|
22
|
+
* Heuristic re-derivation of which units would carry a `CharN:` prefix on
|
|
23
|
+
* export to a wtc text format. The Roster doesn't track unit categories, so we
|
|
24
|
+
* approximate "is a character" as "is the warlord OR has an enhancement OR has
|
|
25
|
+
* a leader attachment". CharN: numbering follows declaration order.
|
|
26
|
+
*
|
|
27
|
+
* Returns a parallel array: `slot[i]` is the 1-based char index for unit i, or
|
|
28
|
+
* `null` if that unit doesn't get a CharN: prefix.
|
|
29
|
+
*/
|
|
30
|
+
export declare function charSlotAssignment(units: readonly RosterUnit[]): (number | null)[];
|
|
31
|
+
/** Pretty JSON with a trailing newline — matches the repo's 2-space convention. */
|
|
32
|
+
export declare function prettyJson(value: unknown): string;
|
|
33
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/export/helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE7D;;6DAE6D;AAC7D,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAO5D;AAED,uFAAuF;AACvF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAGhE;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAalF;AAED,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Convert a kebab-case entity id ("chaos-knights") to a Title Case display
|
|
2
|
+
* name ("Chaos Knights"). This is the round-trip best-effort when the Roster
|
|
3
|
+
* doesn't store the source's raw faction/detachment name. */
|
|
4
|
+
export function titleCaseId(id) {
|
|
5
|
+
if (id === null)
|
|
6
|
+
return null;
|
|
7
|
+
if (id.length === 0)
|
|
8
|
+
return id;
|
|
9
|
+
return id
|
|
10
|
+
.split("-")
|
|
11
|
+
.map((seg) => (seg.length === 0 ? seg : seg[0].toUpperCase() + seg.slice(1)))
|
|
12
|
+
.join(" ");
|
|
13
|
+
}
|
|
14
|
+
/** Sum of unit base pts + enhancement pts (= the figure most text formats display). */
|
|
15
|
+
export function displayedUnitPoints(u) {
|
|
16
|
+
if (u.points === null)
|
|
17
|
+
return null;
|
|
18
|
+
return u.points + (u.enhancement_points ?? 0);
|
|
19
|
+
}
|
|
20
|
+
/** Sum of every unit's displayed total + every enhancement cost line. */
|
|
21
|
+
export function totalArmyPoints(roster) {
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (const u of roster.units) {
|
|
24
|
+
total += u.points ?? 0;
|
|
25
|
+
total += u.enhancement_points ?? 0;
|
|
26
|
+
}
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Heuristic re-derivation of which units would carry a `CharN:` prefix on
|
|
31
|
+
* export to a wtc text format. The Roster doesn't track unit categories, so we
|
|
32
|
+
* approximate "is a character" as "is the warlord OR has an enhancement OR has
|
|
33
|
+
* a leader attachment". CharN: numbering follows declaration order.
|
|
34
|
+
*
|
|
35
|
+
* Returns a parallel array: `slot[i]` is the 1-based char index for unit i, or
|
|
36
|
+
* `null` if that unit doesn't get a CharN: prefix.
|
|
37
|
+
*/
|
|
38
|
+
export function charSlotAssignment(units) {
|
|
39
|
+
const result = [];
|
|
40
|
+
let next = 1;
|
|
41
|
+
for (const u of units) {
|
|
42
|
+
const isChar = u.is_warlord || u.enhancement !== null || u.leader_attachment !== null;
|
|
43
|
+
if (isChar) {
|
|
44
|
+
result.push(next);
|
|
45
|
+
next += 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
result.push(null);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/** Pretty JSON with a trailing newline — matches the repo's 2-space convention. */
|
|
54
|
+
export function prettyJson(value) {
|
|
55
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/export/helpers.ts"],"names":[],"mappings":"AAaA;;6DAE6D;AAC7D,MAAM,UAAU,WAAW,CAAC,EAAiB;IAC3C,IAAI,EAAE,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/B,OAAO,EAAE;SACN,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;SAC5E,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,mBAAmB,CAAC,CAAa;IAC/C,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC7B,KAAK,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;QACvB,KAAK,IAAI,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAA4B;IAC7D,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,WAAW,KAAK,IAAI,IAAI,CAAC,CAAC,iBAAiB,KAAK,IAAI,CAAC;QACtF,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClB,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC","sourcesContent":["/**\n * Shared helpers for the roster exporters.\n *\n * Exporters are deterministic and Dataset-free: they read the Roster only and\n * regenerate format-specific decoration (display names, Char-slot numbering,\n * displayed unit totals) from what's stored. Anything the Roster doesn't model\n * — char-slot numbers, the detachment \"<X> Character\" keyword, secondary-\n * objective summaries — is either derived heuristically here or dropped.\n *\n * @packageDocumentation\n */\nimport type { Roster, RosterUnit } from \"../import/types.js\";\n\n/** Convert a kebab-case entity id (\"chaos-knights\") to a Title Case display\n * name (\"Chaos Knights\"). This is the round-trip best-effort when the Roster\n * doesn't store the source's raw faction/detachment name. */\nexport function titleCaseId(id: string | null): string | null {\n if (id === null) return null;\n if (id.length === 0) return id;\n return id\n .split(\"-\")\n .map((seg) => (seg.length === 0 ? seg : seg[0].toUpperCase() + seg.slice(1)))\n .join(\" \");\n}\n\n/** Sum of unit base pts + enhancement pts (= the figure most text formats display). */\nexport function displayedUnitPoints(u: RosterUnit): number | null {\n if (u.points === null) return null;\n return u.points + (u.enhancement_points ?? 0);\n}\n\n/** Sum of every unit's displayed total + every enhancement cost line. */\nexport function totalArmyPoints(roster: Roster): number {\n let total = 0;\n for (const u of roster.units) {\n total += u.points ?? 0;\n total += u.enhancement_points ?? 0;\n }\n return total;\n}\n\n/**\n * Heuristic re-derivation of which units would carry a `CharN:` prefix on\n * export to a wtc text format. The Roster doesn't track unit categories, so we\n * approximate \"is a character\" as \"is the warlord OR has an enhancement OR has\n * a leader attachment\". CharN: numbering follows declaration order.\n *\n * Returns a parallel array: `slot[i]` is the 1-based char index for unit i, or\n * `null` if that unit doesn't get a CharN: prefix.\n */\nexport function charSlotAssignment(units: readonly RosterUnit[]): (number | null)[] {\n const result: (number | null)[] = [];\n let next = 1;\n for (const u of units) {\n const isChar = u.is_warlord || u.enhancement !== null || u.leader_attachment !== null;\n if (isChar) {\n result.push(next);\n next += 1;\n } else {\n result.push(null);\n }\n }\n return result;\n}\n\n/** Pretty JSON with a trailing newline — matches the repo's 2-space convention. */\nexport function prettyJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roster exporters — the symmetric counterpart to the importer.
|
|
3
|
+
*
|
|
4
|
+
* `exportRoster(roster, format)` dispatches to one of five registered
|
|
5
|
+
* serializers (NewRecruit JSON, the three NewRecruit text formats, and the
|
|
6
|
+
* canonical Roster JSON). Each serializer is deterministic and Dataset-free,
|
|
7
|
+
* so the TS and Rust mirrors can produce byte-identical output for
|
|
8
|
+
* cross-implementation conformance.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import type { Roster } from "../import/types.js";
|
|
13
|
+
import type { ExportFormat } from "./serializer.js";
|
|
14
|
+
export type { ExportFormat, RosterSerializer } from "./serializer.js";
|
|
15
|
+
export { newRecruitJsonSerializer } from "./newrecruit-json.js";
|
|
16
|
+
export { newRecruitSimpleSerializer } from "./newrecruit-simple.js";
|
|
17
|
+
export { newRecruitWtcCompactSerializer, newRecruitWtcFullSerializer, } from "./newrecruit-wtc.js";
|
|
18
|
+
export { rosterJsonSerializer } from "./roster-json.js";
|
|
19
|
+
export { rosterizerSerializer } from "./rosterizer.js";
|
|
20
|
+
/** Serialize a {@link Roster} into the named target format. */
|
|
21
|
+
export declare function exportRoster(roster: Roster, format: ExportFormat): string;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/export/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AASjD,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iBAAiB,CAAC;AAEtE,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAYvD,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAQzE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { newRecruitJsonSerializer } from "./newrecruit-json.js";
|
|
2
|
+
import { newRecruitSimpleSerializer } from "./newrecruit-simple.js";
|
|
3
|
+
import { newRecruitWtcCompactSerializer, newRecruitWtcFullSerializer, } from "./newrecruit-wtc.js";
|
|
4
|
+
import { rosterJsonSerializer } from "./roster-json.js";
|
|
5
|
+
import { rosterizerSerializer } from "./rosterizer.js";
|
|
6
|
+
export { newRecruitJsonSerializer } from "./newrecruit-json.js";
|
|
7
|
+
export { newRecruitSimpleSerializer } from "./newrecruit-simple.js";
|
|
8
|
+
export { newRecruitWtcCompactSerializer, newRecruitWtcFullSerializer, } from "./newrecruit-wtc.js";
|
|
9
|
+
export { rosterJsonSerializer } from "./roster-json.js";
|
|
10
|
+
export { rosterizerSerializer } from "./rosterizer.js";
|
|
11
|
+
/** All registered serializers, keyed by their {@link ExportFormat} id. */
|
|
12
|
+
const SERIALIZERS = [
|
|
13
|
+
newRecruitJsonSerializer,
|
|
14
|
+
newRecruitWtcCompactSerializer,
|
|
15
|
+
newRecruitWtcFullSerializer,
|
|
16
|
+
newRecruitSimpleSerializer,
|
|
17
|
+
rosterJsonSerializer,
|
|
18
|
+
rosterizerSerializer,
|
|
19
|
+
];
|
|
20
|
+
/** Serialize a {@link Roster} into the named target format. */
|
|
21
|
+
export function exportRoster(roster, format) {
|
|
22
|
+
const s = SERIALIZERS.find((s) => s.id === format);
|
|
23
|
+
if (!s) {
|
|
24
|
+
throw new Error(`unknown export format: ${format} (registered: ${SERIALIZERS.map((s) => s.id).join(", ")})`);
|
|
25
|
+
}
|
|
26
|
+
return s.serialize(roster);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/export/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAIvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAEvD,0EAA0E;AAC1E,MAAM,WAAW,GAAgC;IAC/C,wBAAwB;IACxB,8BAA8B;IAC9B,2BAA2B;IAC3B,0BAA0B;IAC1B,oBAAoB;IACpB,oBAAoB;CACrB,CAAC;AAEF,+DAA+D;AAC/D,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,MAAoB;IAC/D,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IACnD,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,KAAK,CACb,0BAA0B,MAAM,iBAAiB,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC5F,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC","sourcesContent":["/**\n * Roster exporters — the symmetric counterpart to the importer.\n *\n * `exportRoster(roster, format)` dispatches to one of five registered\n * serializers (NewRecruit JSON, the three NewRecruit text formats, and the\n * canonical Roster JSON). Each serializer is deterministic and Dataset-free,\n * so the TS and Rust mirrors can produce byte-identical output for\n * cross-implementation conformance.\n *\n * @packageDocumentation\n */\nimport type { Roster } from \"../import/types.js\";\nimport { newRecruitJsonSerializer } from \"./newrecruit-json.js\";\nimport { newRecruitSimpleSerializer } from \"./newrecruit-simple.js\";\nimport {\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n} from \"./newrecruit-wtc.js\";\nimport { rosterJsonSerializer } from \"./roster-json.js\";\nimport { rosterizerSerializer } from \"./rosterizer.js\";\nimport type { ExportFormat, RosterSerializer } from \"./serializer.js\";\n\nexport type { ExportFormat, RosterSerializer } from \"./serializer.js\";\nexport { newRecruitJsonSerializer } from \"./newrecruit-json.js\";\nexport { newRecruitSimpleSerializer } from \"./newrecruit-simple.js\";\nexport {\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n} from \"./newrecruit-wtc.js\";\nexport { rosterJsonSerializer } from \"./roster-json.js\";\nexport { rosterizerSerializer } from \"./rosterizer.js\";\n\n/** All registered serializers, keyed by their {@link ExportFormat} id. */\nconst SERIALIZERS: readonly RosterSerializer[] = [\n newRecruitJsonSerializer,\n newRecruitWtcCompactSerializer,\n newRecruitWtcFullSerializer,\n newRecruitSimpleSerializer,\n rosterJsonSerializer,\n rosterizerSerializer,\n];\n\n/** Serialize a {@link Roster} into the named target format. */\nexport function exportRoster(roster: Roster, format: ExportFormat): string {\n const s = SERIALIZERS.find((s) => s.id === format);\n if (!s) {\n throw new Error(\n `unknown export format: ${format} (registered: ${SERIALIZERS.map((s) => s.id).join(\", \")})`,\n );\n }\n return s.serialize(roster);\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-json.d.ts","sourceRoot":"","sources":["../../src/export/newrecruit-json.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAkJxD,eAAO,MAAM,wBAAwB,EAAE,gBAuCtC,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { prettyJson, titleCaseId, totalArmyPoints } from "./helpers.js";
|
|
2
|
+
const PTS_TYPE_ID = "pts-type";
|
|
3
|
+
const NEWRECRUIT_XMLNS = "http://www.battlescribe.net/schema/rosterSchema";
|
|
4
|
+
const NEWRECRUIT_GENERATED_BY = "https://newrecruit.eu";
|
|
5
|
+
/** Build a "Faction: <name>" category from the unit's roster context. */
|
|
6
|
+
function factionCategory(roster) {
|
|
7
|
+
const display = titleCaseId(roster.faction_id);
|
|
8
|
+
if (display === null)
|
|
9
|
+
return null;
|
|
10
|
+
return { name: `Faction: ${display}`, primary: false };
|
|
11
|
+
}
|
|
12
|
+
function wargearSelection(idx, w) {
|
|
13
|
+
return {
|
|
14
|
+
id: `w-${idx}`,
|
|
15
|
+
name: w.ref.raw_name,
|
|
16
|
+
type: "upgrade",
|
|
17
|
+
number: w.count,
|
|
18
|
+
// The NewRecruit importer recognises a wargear selection by a category
|
|
19
|
+
// ending in " Weapon" — emit a generic "Ranged Weapon" so we don't have
|
|
20
|
+
// to track ranged-vs-melee separation the Roster doesn't model.
|
|
21
|
+
categories: [{ name: "Ranged Weapon", primary: false }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function unitSelection(idx, u, faction) {
|
|
25
|
+
const inner = [];
|
|
26
|
+
if (u.is_warlord) {
|
|
27
|
+
inner.push({ id: `u${idx}-warlord`, name: "Warlord", type: "upgrade", number: 1 });
|
|
28
|
+
}
|
|
29
|
+
if (u.enhancement) {
|
|
30
|
+
const enhCost = u.enhancement_points === null
|
|
31
|
+
? undefined
|
|
32
|
+
: [{ name: "pts", typeId: PTS_TYPE_ID, value: u.enhancement_points }];
|
|
33
|
+
inner.push({
|
|
34
|
+
id: `u${idx}-enh`,
|
|
35
|
+
name: u.enhancement.raw_name,
|
|
36
|
+
type: "upgrade",
|
|
37
|
+
number: 1,
|
|
38
|
+
group: "Enhancements",
|
|
39
|
+
...(enhCost ? { costs: enhCost } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const wargearSelections = u.wargear.map((w, wi) => wargearSelection(wi, w));
|
|
43
|
+
const ownCategories = faction ? [faction] : [];
|
|
44
|
+
if (u.model_count <= 1) {
|
|
45
|
+
return {
|
|
46
|
+
id: `u-${idx}`,
|
|
47
|
+
name: u.ref.raw_name,
|
|
48
|
+
type: "model",
|
|
49
|
+
number: 1,
|
|
50
|
+
categories: ownCategories,
|
|
51
|
+
...(u.points === null ? {} : { costs: [{ name: "pts", typeId: PTS_TYPE_ID, value: u.points }] }),
|
|
52
|
+
selections: [...inner, ...wargearSelections],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Multi-model: wrap in a `type: "unit"` with a nested `type: "model"` that
|
|
56
|
+
// carries the model count and the (collapsed, per-unit) wargear.
|
|
57
|
+
return {
|
|
58
|
+
id: `u-${idx}`,
|
|
59
|
+
name: u.ref.raw_name,
|
|
60
|
+
type: "unit",
|
|
61
|
+
number: 1,
|
|
62
|
+
categories: ownCategories,
|
|
63
|
+
...(u.points === null ? {} : { costs: [{ name: "pts", typeId: PTS_TYPE_ID, value: u.points }] }),
|
|
64
|
+
selections: [
|
|
65
|
+
...inner,
|
|
66
|
+
{
|
|
67
|
+
id: `u${idx}-model`,
|
|
68
|
+
name: u.ref.raw_name,
|
|
69
|
+
type: "model",
|
|
70
|
+
number: u.model_count,
|
|
71
|
+
selections: wargearSelections,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function configSelection(name, value, idx) {
|
|
77
|
+
return {
|
|
78
|
+
id: `cfg-${idx}`,
|
|
79
|
+
name,
|
|
80
|
+
type: "upgrade",
|
|
81
|
+
number: 1,
|
|
82
|
+
categories: [{ name: "Configuration", primary: true }],
|
|
83
|
+
selections: [
|
|
84
|
+
{
|
|
85
|
+
id: `cfg-${idx}-val`,
|
|
86
|
+
name: value,
|
|
87
|
+
type: "upgrade",
|
|
88
|
+
number: 1,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function battleSizeLabel(roster) {
|
|
94
|
+
if (roster.battle_size === "strike-force") {
|
|
95
|
+
const limit = roster.points.declared_limit ?? 2000;
|
|
96
|
+
return `Strike Force (${limit} Point limit)`;
|
|
97
|
+
}
|
|
98
|
+
if (roster.battle_size === "incursion") {
|
|
99
|
+
const limit = roster.points.declared_limit ?? 1000;
|
|
100
|
+
return `Incursion (${limit} Point limit)`;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
export const newRecruitJsonSerializer = {
|
|
105
|
+
id: "newrecruit-json",
|
|
106
|
+
serialize(roster) {
|
|
107
|
+
const faction = factionCategory(roster);
|
|
108
|
+
const factionDisplay = titleCaseId(roster.faction_id) ?? "Unknown";
|
|
109
|
+
const detachmentDisplay = titleCaseId(roster.detachment_id);
|
|
110
|
+
const battleSize = battleSizeLabel(roster);
|
|
111
|
+
const config = [];
|
|
112
|
+
if (battleSize)
|
|
113
|
+
config.push(configSelection("Battle Size", battleSize, "battle-size"));
|
|
114
|
+
if (detachmentDisplay)
|
|
115
|
+
config.push(configSelection("Detachment", detachmentDisplay, "detachment"));
|
|
116
|
+
const force = {
|
|
117
|
+
id: "force-1",
|
|
118
|
+
name: "Army Roster",
|
|
119
|
+
catalogueName: factionDisplay,
|
|
120
|
+
selections: [
|
|
121
|
+
...config,
|
|
122
|
+
...roster.units.map((u, i) => unitSelection(i, u, faction)),
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
const total = totalArmyPoints(roster);
|
|
126
|
+
const payload = {
|
|
127
|
+
name: roster.name,
|
|
128
|
+
generatedBy: NEWRECRUIT_GENERATED_BY,
|
|
129
|
+
roster: {
|
|
130
|
+
name: roster.name,
|
|
131
|
+
xmlns: NEWRECRUIT_XMLNS,
|
|
132
|
+
generatedBy: NEWRECRUIT_GENERATED_BY,
|
|
133
|
+
costs: [{ name: "pts", typeId: PTS_TYPE_ID, value: total }],
|
|
134
|
+
forces: [force],
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
return prettyJson(payload);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=newrecruit-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-json.js","sourceRoot":"","sources":["../../src/export/newrecruit-json.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit JSON exporter — emits a BattleScribe-shaped roster skeleton that\n * round-trips through {@link newRecruitJsonAdapter}.\n *\n * The shape carries only fields the importer reads: `name`, `type`, `number`,\n * `costs[]`, `categories[].name`, `group`, and `catalogueName`. No `rules` /\n * `profiles` / `description` ever appear — we don't store them, and emitting\n * them would be an IP violation.\n *\n * Faction and detachment display names come from\n * {@link titleCaseId}(faction_id) — the Roster doesn't carry the source's raw\n * faction name, so we reconstruct it from the kebab-case id. This is the only\n * lossy hop in the JSON round-trip (e.g. `tau-empire` → \"Tau Empire\" rather\n * than the canonical \"T'au Empire\").\n *\n * @packageDocumentation\n */\nimport type { Roster, RosterUnit, RosterWargear } from \"../import/types.js\";\nimport { prettyJson, titleCaseId, totalArmyPoints } from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\nconst PTS_TYPE_ID = \"pts-type\";\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_GENERATED_BY = \"https://newrecruit.eu\";\n\ninterface JsonSelection {\n id: string;\n name: string;\n type: \"model\" | \"unit\" | \"upgrade\";\n number: number;\n group?: string;\n categories?: { name: string; primary: boolean }[];\n costs?: { name: string; typeId: string; value: number }[];\n selections?: JsonSelection[];\n}\n\ninterface JsonForce {\n id: string;\n name: string;\n catalogueName: string;\n selections: JsonSelection[];\n}\n\ninterface JsonRoster {\n name: string;\n xmlns: string;\n generatedBy: string;\n costs: { name: string; typeId: string; value: number }[];\n forces: JsonForce[];\n}\n\ninterface JsonPayload {\n name: string;\n generatedBy: string;\n roster: JsonRoster;\n}\n\n/** Build a \"Faction: <name>\" category from the unit's roster context. */\nfunction factionCategory(roster: Roster): { name: string; primary: boolean } | null {\n const display = titleCaseId(roster.faction_id);\n if (display === null) return null;\n return { name: `Faction: ${display}`, primary: false };\n}\n\nfunction wargearSelection(idx: number, w: RosterWargear): JsonSelection {\n return {\n id: `w-${idx}`,\n name: w.ref.raw_name,\n type: \"upgrade\",\n number: w.count,\n // The NewRecruit importer recognises a wargear selection by a category\n // ending in \" Weapon\" — emit a generic \"Ranged Weapon\" so we don't have\n // to track ranged-vs-melee separation the Roster doesn't model.\n categories: [{ name: \"Ranged Weapon\", primary: false }],\n };\n}\n\nfunction unitSelection(idx: number, u: RosterUnit, faction: { name: string; primary: boolean } | null): JsonSelection {\n const inner: JsonSelection[] = [];\n if (u.is_warlord) {\n inner.push({ id: `u${idx}-warlord`, name: \"Warlord\", type: \"upgrade\", number: 1 });\n }\n if (u.enhancement) {\n const enhCost =\n u.enhancement_points === null\n ? undefined\n : [{ name: \"pts\", typeId: PTS_TYPE_ID, value: u.enhancement_points }];\n inner.push({\n id: `u${idx}-enh`,\n name: u.enhancement.raw_name,\n type: \"upgrade\",\n number: 1,\n group: \"Enhancements\",\n ...(enhCost ? { costs: enhCost } : {}),\n });\n }\n\n const wargearSelections = u.wargear.map((w, wi) => wargearSelection(wi, w));\n\n const ownCategories = faction ? [faction] : [];\n\n if (u.model_count <= 1) {\n return {\n id: `u-${idx}`,\n name: u.ref.raw_name,\n type: \"model\",\n number: 1,\n categories: ownCategories,\n ...(u.points === null ? {} : { costs: [{ name: \"pts\", typeId: PTS_TYPE_ID, value: u.points }] }),\n selections: [...inner, ...wargearSelections],\n };\n }\n\n // Multi-model: wrap in a `type: \"unit\"` with a nested `type: \"model\"` that\n // carries the model count and the (collapsed, per-unit) wargear.\n return {\n id: `u-${idx}`,\n name: u.ref.raw_name,\n type: \"unit\",\n number: 1,\n categories: ownCategories,\n ...(u.points === null ? {} : { costs: [{ name: \"pts\", typeId: PTS_TYPE_ID, value: u.points }] }),\n selections: [\n ...inner,\n {\n id: `u${idx}-model`,\n name: u.ref.raw_name,\n type: \"model\",\n number: u.model_count,\n selections: wargearSelections,\n },\n ],\n };\n}\n\nfunction configSelection(name: string, value: string, idx: string): JsonSelection {\n return {\n id: `cfg-${idx}`,\n name,\n type: \"upgrade\",\n number: 1,\n categories: [{ name: \"Configuration\", primary: true }],\n selections: [\n {\n id: `cfg-${idx}-val`,\n name: value,\n type: \"upgrade\",\n number: 1,\n },\n ],\n };\n}\n\nfunction battleSizeLabel(roster: Roster): string | null {\n if (roster.battle_size === \"strike-force\") {\n const limit = roster.points.declared_limit ?? 2000;\n return `Strike Force (${limit} Point limit)`;\n }\n if (roster.battle_size === \"incursion\") {\n const limit = roster.points.declared_limit ?? 1000;\n return `Incursion (${limit} Point limit)`;\n }\n return null;\n}\n\nexport const newRecruitJsonSerializer: RosterSerializer = {\n id: \"newrecruit-json\",\n\n serialize(roster: Roster): string {\n const faction = factionCategory(roster);\n const factionDisplay = titleCaseId(roster.faction_id) ?? \"Unknown\";\n const detachmentDisplay = titleCaseId(roster.detachment_id);\n const battleSize = battleSizeLabel(roster);\n\n const config: JsonSelection[] = [];\n if (battleSize) config.push(configSelection(\"Battle Size\", battleSize, \"battle-size\"));\n if (detachmentDisplay) config.push(configSelection(\"Detachment\", detachmentDisplay, \"detachment\"));\n\n const force: JsonForce = {\n id: \"force-1\",\n name: \"Army Roster\",\n catalogueName: factionDisplay,\n selections: [\n ...config,\n ...roster.units.map((u, i) => unitSelection(i, u, faction)),\n ],\n };\n\n const total = totalArmyPoints(roster);\n\n const payload: JsonPayload = {\n name: roster.name,\n generatedBy: NEWRECRUIT_GENERATED_BY,\n roster: {\n name: roster.name,\n xmlns: NEWRECRUIT_XMLNS,\n generatedBy: NEWRECRUIT_GENERATED_BY,\n costs: [{ name: \"pts\", typeId: PTS_TYPE_ID, value: total }],\n forces: [force],\n },\n };\n\n return prettyJson(payload);\n },\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-simple.d.ts","sourceRoot":"","sources":["../../src/export/newrecruit-simple.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAkDxD,eAAO,MAAM,0BAA0B,EAAE,gBAgCxC,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { displayedUnitPoints, titleCaseId, totalArmyPoints } from "./helpers.js";
|
|
2
|
+
function battleSizeLabel(roster) {
|
|
3
|
+
if (roster.battle_size === "strike-force") {
|
|
4
|
+
return `Strike Force (${roster.points.declared_limit ?? 2000} Point limit)`;
|
|
5
|
+
}
|
|
6
|
+
if (roster.battle_size === "incursion") {
|
|
7
|
+
return `Incursion (${roster.points.declared_limit ?? 1000} Point limit)`;
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
/** Build the wargear list inline. For homogeneous multi-model units, divides
|
|
12
|
+
* counts by model_count so the per-model render is clean. */
|
|
13
|
+
function wargearText(u, perModelDivisor) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
if (u.enhancement) {
|
|
16
|
+
const ptsTag = u.enhancement_points === null ? "" : ` [${u.enhancement_points} pts]`;
|
|
17
|
+
parts.push(`${u.enhancement.raw_name}${ptsTag}`);
|
|
18
|
+
}
|
|
19
|
+
if (u.is_warlord)
|
|
20
|
+
parts.push("Warlord");
|
|
21
|
+
for (const w of u.wargear) {
|
|
22
|
+
const c = perModelDivisor > 0 ? w.count / perModelDivisor : w.count;
|
|
23
|
+
parts.push(c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name);
|
|
24
|
+
}
|
|
25
|
+
return parts.join(", ");
|
|
26
|
+
}
|
|
27
|
+
function unitText(u) {
|
|
28
|
+
const pts = displayedUnitPoints(u);
|
|
29
|
+
const ptsText = pts === null ? "" : `${pts} pts`;
|
|
30
|
+
if (u.model_count <= 1) {
|
|
31
|
+
return [`${u.ref.raw_name} [${ptsText}]: ${wargearText(u, 1)}`];
|
|
32
|
+
}
|
|
33
|
+
// Multi-model: homogeneous when every weapon count divides cleanly.
|
|
34
|
+
const divisible = u.wargear.every((w) => w.count % u.model_count === 0);
|
|
35
|
+
if (divisible) {
|
|
36
|
+
return [
|
|
37
|
+
`${u.ref.raw_name} [${ptsText}]:`,
|
|
38
|
+
`• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, u.model_count)}`,
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
// Heterogeneous fallback: render as a single bullet with full counts.
|
|
42
|
+
return [
|
|
43
|
+
`${u.ref.raw_name} [${ptsText}]:`,
|
|
44
|
+
`• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, 1)}`,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
export const newRecruitSimpleSerializer = {
|
|
48
|
+
id: "newrecruit-simple",
|
|
49
|
+
serialize(roster) {
|
|
50
|
+
const faction = titleCaseId(roster.faction_id) ?? "Unknown";
|
|
51
|
+
const detachment = titleCaseId(roster.detachment_id);
|
|
52
|
+
const battle = battleSizeLabel(roster);
|
|
53
|
+
const total = totalArmyPoints(roster);
|
|
54
|
+
const lines = [];
|
|
55
|
+
// First line carries the *declared limit* (the army's points ceiling); the
|
|
56
|
+
// `# ++ Army Roster ++` line carries the *reported total*. They differ
|
|
57
|
+
// when the list isn't filled to the cap.
|
|
58
|
+
const limit = roster.points.declared_limit ?? total;
|
|
59
|
+
lines.push(`${faction} - ${roster.name} - [${limit} pts]`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
lines.push(`# ++ Army Roster ++ [${total} pts]`);
|
|
62
|
+
lines.push("## Configuration");
|
|
63
|
+
if (battle)
|
|
64
|
+
lines.push(`Battle Size: ${battle}`);
|
|
65
|
+
if (detachment)
|
|
66
|
+
lines.push(`Detachment: ${detachment}`);
|
|
67
|
+
lines.push("");
|
|
68
|
+
// The Roster doesn't tag allied vs. battleline per unit; emit one section.
|
|
69
|
+
const sectionTotal = roster.units.reduce((acc, u) => acc + (u.points ?? 0) + (u.enhancement_points ?? 0), 0);
|
|
70
|
+
lines.push(`## Battleline [${sectionTotal} pts]`);
|
|
71
|
+
for (const u of roster.units)
|
|
72
|
+
lines.push(...unitText(u));
|
|
73
|
+
return lines.join("\n") + "\n";
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=newrecruit-simple.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-simple.js","sourceRoot":"","sources":["../../src/export/newrecruit-simple.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGjF,SAAS,eAAe,CAAC,MAAc;IACrC,IAAI,MAAM,CAAC,WAAW,KAAK,cAAc,EAAE,CAAC;QAC1C,OAAO,iBAAiB,MAAM,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,eAAe,CAAC;IAC9E,CAAC;IACD,IAAI,MAAM,CAAC,WAAW,KAAK,WAAW,EAAE,CAAC;QACvC,OAAO,cAAc,MAAM,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,eAAe,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;6DAC6D;AAC7D,SAAS,WAAW,CAAC,CAAa,EAAE,eAAuB;IACzD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QAClB,MAAM,MAAM,GAAG,CAAC,CAAC,kBAAkB,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,kBAAkB,OAAO,CAAC;QACrF,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,CAAC,QAAQ,GAAG,MAAM,EAAE,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,CAAC,UAAU;QAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,eAAe,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACpE,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,CAAa;IAC7B,MAAM,GAAG,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC;IAEjD,IAAI,CAAC,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,oEAAoE;IACpE,MAAM,SAAS,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC;IACxE,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI;YACjC,KAAK,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE;SAC1E,CAAC;IACJ,CAAC;IACD,sEAAsE;IACtE,OAAO;QACL,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI;QACjC,KAAK,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,GAAG,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE;KAC9D,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAqB;IAC1D,EAAE,EAAE,mBAAmB;IAEvB,SAAS,CAAC,MAAc;QACtB,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC;QAC5D,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QAEtC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,2EAA2E;QAC3E,uEAAuE;QACvE,yCAAyC;QACzC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,IAAI,KAAK,CAAC;QACpD,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,MAAM,MAAM,CAAC,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC;QAC3D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,wBAAwB,KAAK,OAAO,CAAC,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC/B,IAAI,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;QACjD,IAAI,UAAU;YAAE,KAAK,CAAC,IAAI,CAAC,eAAe,UAAU,EAAE,CAAC,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEf,2EAA2E;QAC3E,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CACtC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC,EAC/D,CAAC,CACF,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,kBAAkB,YAAY,OAAO,CAAC,CAAC;QAClD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAEzD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACjC,CAAC;CACF,CAAC","sourcesContent":["/**\n * NewRecruit \"simple\" markdown-ish text exporter.\n *\n * Shape:\n * ```\n * <faction> - <list name> - [N pts]\n *\n * # ++ Army Roster ++ [N pts]\n * ## Configuration\n * Battle Size: <Label>\n * Detachment: <Name>\n *\n * ## Battleline [N pts]\n * <Unit> [pts]: <wargear, …, EnhName [N pts], …>\n * <Multi-Unit> [pts]:\n * • <Nx> <ModelType>: <wargear>\n * ```\n *\n * Enhancements are inlined as `Name [N pts]` (the only place we re-emit a\n * `[N pts]` bracket on a token).\n *\n * @packageDocumentation\n */\nimport type { Roster, RosterUnit } from \"../import/types.js\";\nimport { displayedUnitPoints, titleCaseId, totalArmyPoints } from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\nfunction battleSizeLabel(roster: Roster): string | null {\n if (roster.battle_size === \"strike-force\") {\n return `Strike Force (${roster.points.declared_limit ?? 2000} Point limit)`;\n }\n if (roster.battle_size === \"incursion\") {\n return `Incursion (${roster.points.declared_limit ?? 1000} Point limit)`;\n }\n return null;\n}\n\n/** Build the wargear list inline. For homogeneous multi-model units, divides\n * counts by model_count so the per-model render is clean. */\nfunction wargearText(u: RosterUnit, perModelDivisor: number): string {\n const parts: string[] = [];\n if (u.enhancement) {\n const ptsTag = u.enhancement_points === null ? \"\" : ` [${u.enhancement_points} pts]`;\n parts.push(`${u.enhancement.raw_name}${ptsTag}`);\n }\n if (u.is_warlord) parts.push(\"Warlord\");\n for (const w of u.wargear) {\n const c = perModelDivisor > 0 ? w.count / perModelDivisor : w.count;\n parts.push(c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name);\n }\n return parts.join(\", \");\n}\n\nfunction unitText(u: RosterUnit): string[] {\n const pts = displayedUnitPoints(u);\n const ptsText = pts === null ? \"\" : `${pts} pts`;\n\n if (u.model_count <= 1) {\n return [`${u.ref.raw_name} [${ptsText}]: ${wargearText(u, 1)}`];\n }\n // Multi-model: homogeneous when every weapon count divides cleanly.\n const divisible = u.wargear.every((w) => w.count % u.model_count === 0);\n if (divisible) {\n return [\n `${u.ref.raw_name} [${ptsText}]:`,\n `• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, u.model_count)}`,\n ];\n }\n // Heterogeneous fallback: render as a single bullet with full counts.\n return [\n `${u.ref.raw_name} [${ptsText}]:`,\n `• ${u.model_count}x ${u.ref.raw_name}: ${wargearText(u, 1)}`,\n ];\n}\n\nexport const newRecruitSimpleSerializer: RosterSerializer = {\n id: \"newrecruit-simple\",\n\n serialize(roster: Roster): string {\n const faction = titleCaseId(roster.faction_id) ?? \"Unknown\";\n const detachment = titleCaseId(roster.detachment_id);\n const battle = battleSizeLabel(roster);\n const total = totalArmyPoints(roster);\n\n const lines: string[] = [];\n // First line carries the *declared limit* (the army's points ceiling); the\n // `# ++ Army Roster ++` line carries the *reported total*. They differ\n // when the list isn't filled to the cap.\n const limit = roster.points.declared_limit ?? total;\n lines.push(`${faction} - ${roster.name} - [${limit} pts]`);\n lines.push(\"\");\n lines.push(`# ++ Army Roster ++ [${total} pts]`);\n lines.push(\"## Configuration\");\n if (battle) lines.push(`Battle Size: ${battle}`);\n if (detachment) lines.push(`Detachment: ${detachment}`);\n lines.push(\"\");\n\n // The Roster doesn't tag allied vs. battleline per unit; emit one section.\n const sectionTotal = roster.units.reduce(\n (acc, u) => acc + (u.points ?? 0) + (u.enhancement_points ?? 0),\n 0,\n );\n lines.push(`## Battleline [${sectionTotal} pts]`);\n for (const u of roster.units) lines.push(...unitText(u));\n\n return lines.join(\"\\n\") + \"\\n\";\n },\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-wtc.d.ts","sourceRoot":"","sources":["../../src/export/newrecruit-wtc.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AA8DxD,eAAO,MAAM,8BAA8B,EAAE,gBAyB5C,CAAC;AAuBF,eAAO,MAAM,2BAA2B,EAAE,gBAgDzC,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { charSlotAssignment, displayedUnitPoints, titleCaseId, totalArmyPoints, } from "./helpers.js";
|
|
2
|
+
const FENCE = "+++++++++++++++++++++++++++++++++++++++++++++++";
|
|
3
|
+
function wargearListText(unit, includeWarlordTag) {
|
|
4
|
+
const parts = [];
|
|
5
|
+
for (const w of unit.wargear) {
|
|
6
|
+
parts.push(w.count > 1 ? `${w.count}x ${w.ref.raw_name}` : w.ref.raw_name);
|
|
7
|
+
}
|
|
8
|
+
if (includeWarlordTag && unit.is_warlord)
|
|
9
|
+
parts.push("Warlord");
|
|
10
|
+
return parts.join(", ");
|
|
11
|
+
}
|
|
12
|
+
function header(roster, units, charSlots) {
|
|
13
|
+
const faction = titleCaseId(roster.faction_id) ?? "Unknown";
|
|
14
|
+
const detachment = titleCaseId(roster.detachment_id);
|
|
15
|
+
const limit = roster.points.declared_limit ?? totalArmyPoints(roster);
|
|
16
|
+
const total = roster.points.total_reported ?? totalArmyPoints(roster);
|
|
17
|
+
const warlordIdx = units.findIndex((u) => u.is_warlord);
|
|
18
|
+
const warlord = warlordIdx >= 0
|
|
19
|
+
? `Char${charSlots[warlordIdx]}: ${units[warlordIdx].ref.raw_name}`
|
|
20
|
+
: "—";
|
|
21
|
+
const enhancementIdx = units.findIndex((u) => u.enhancement !== null);
|
|
22
|
+
let enhancement = "—";
|
|
23
|
+
if (enhancementIdx >= 0) {
|
|
24
|
+
const u = units[enhancementIdx];
|
|
25
|
+
enhancement = `${u.enhancement.raw_name} (on Char${charSlots[enhancementIdx]}: ${u.ref.raw_name})`;
|
|
26
|
+
}
|
|
27
|
+
const lines = [
|
|
28
|
+
FENCE,
|
|
29
|
+
`+ LIST NAME: ${roster.name}`,
|
|
30
|
+
`+ FACTION KEYWORD: ${faction}`,
|
|
31
|
+
`+ DETACHMENT: ${detachment ?? "—"}`,
|
|
32
|
+
`+ TOTAL ARMY POINTS: ${total}pts`,
|
|
33
|
+
`+ POINTS LIMIT: ${limit}pts`,
|
|
34
|
+
`+`,
|
|
35
|
+
`+ WARLORD: ${warlord}`,
|
|
36
|
+
`+ ENHANCEMENT: ${enhancement}`,
|
|
37
|
+
`+ NUMBER OF UNITS: ${units.length}`,
|
|
38
|
+
FENCE,
|
|
39
|
+
];
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
function isAlliedUnit(u, factionId) {
|
|
43
|
+
// Heuristic: the Roster doesn't tag allied units explicitly, but the
|
|
44
|
+
// multi-force diagnostic + the fact that we only carry the primary faction
|
|
45
|
+
// means non-primary-faction units aren't recognisable. The only fact we *do*
|
|
46
|
+
// have is `leader_attachment` and warlord/enhancement (which mark primary
|
|
47
|
+
// characters). For unit grouping in wtc-full we simply place everything in
|
|
48
|
+
// BATTLELINE unless the Roster's multi-force flag suggests there's an allied
|
|
49
|
+
// detachment. Since the flag is a diagnostic warning, not a per-unit tag,
|
|
50
|
+
// wtc-full export collapses to a single BATTLELINE section.
|
|
51
|
+
void u;
|
|
52
|
+
void factionId;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
export const newRecruitWtcCompactSerializer = {
|
|
56
|
+
id: "newrecruit-wtc-compact",
|
|
57
|
+
serialize(roster) {
|
|
58
|
+
const units = roster.units;
|
|
59
|
+
const slots = charSlotAssignment(units);
|
|
60
|
+
const lines = [header(roster, units, slots), ""];
|
|
61
|
+
for (let i = 0; i < units.length; i += 1) {
|
|
62
|
+
const u = units[i];
|
|
63
|
+
const prefix = slots[i] !== null ? `Char${slots[i]}: ` : "";
|
|
64
|
+
const pts = displayedUnitPoints(u);
|
|
65
|
+
const ptsText = pts === null ? "" : `${pts} pts`;
|
|
66
|
+
lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText}): ${wargearListText(u, true)}`);
|
|
67
|
+
if (u.enhancement) {
|
|
68
|
+
const enhText = u.enhancement_points === null
|
|
69
|
+
? `Enhancement: ${u.enhancement.raw_name}`
|
|
70
|
+
: `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;
|
|
71
|
+
lines.push(enhText);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n") + "\n";
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* For a multi-model unit, render its wargear as `N with <per-model list>` when
|
|
79
|
+
* the wargear divides evenly across models (the natural NewRecruit form).
|
|
80
|
+
* Otherwise emit `1 with <full Nx counts>` so the counts round-trip exactly.
|
|
81
|
+
*/
|
|
82
|
+
function multiModelWithLine(u) {
|
|
83
|
+
// Homogeneous when every weapon count divides cleanly by model_count.
|
|
84
|
+
const divisible = u.wargear.every((w) => w.count % u.model_count === 0);
|
|
85
|
+
if (divisible) {
|
|
86
|
+
const perModel = u.wargear
|
|
87
|
+
.map((w) => {
|
|
88
|
+
const c = w.count / u.model_count;
|
|
89
|
+
return c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name;
|
|
90
|
+
})
|
|
91
|
+
.filter((s) => s.length > 0);
|
|
92
|
+
if (u.is_warlord)
|
|
93
|
+
perModel.push("Warlord");
|
|
94
|
+
return `${u.model_count} with ${perModel.join(", ")}`;
|
|
95
|
+
}
|
|
96
|
+
return `1 with ${wargearListText(u, true)}`;
|
|
97
|
+
}
|
|
98
|
+
export const newRecruitWtcFullSerializer = {
|
|
99
|
+
id: "newrecruit-wtc-full",
|
|
100
|
+
serialize(roster) {
|
|
101
|
+
const units = roster.units;
|
|
102
|
+
const slots = charSlotAssignment(units);
|
|
103
|
+
const battlelineIdxs = [];
|
|
104
|
+
const alliedIdxs = [];
|
|
105
|
+
for (let i = 0; i < units.length; i += 1) {
|
|
106
|
+
if (isAlliedUnit(units[i], roster.faction_id))
|
|
107
|
+
alliedIdxs.push(i);
|
|
108
|
+
else
|
|
109
|
+
battlelineIdxs.push(i);
|
|
110
|
+
}
|
|
111
|
+
const lines = [header(roster, units, slots), "", "BATTLELINE", ""];
|
|
112
|
+
const emitUnit = (i) => {
|
|
113
|
+
const u = units[i];
|
|
114
|
+
const prefix = slots[i] !== null ? `Char${slots[i]}: ` : "";
|
|
115
|
+
const pts = displayedUnitPoints(u);
|
|
116
|
+
const ptsText = pts === null ? "" : `${pts} pts`;
|
|
117
|
+
lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText})`);
|
|
118
|
+
if (u.model_count > 1) {
|
|
119
|
+
lines.push(multiModelWithLine(u));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
lines.push(`1 with ${wargearListText(u, true)}`);
|
|
123
|
+
}
|
|
124
|
+
if (u.enhancement) {
|
|
125
|
+
const enhText = u.enhancement_points === null
|
|
126
|
+
? `Enhancement: ${u.enhancement.raw_name}`
|
|
127
|
+
: `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;
|
|
128
|
+
lines.push(enhText);
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
};
|
|
132
|
+
for (const i of battlelineIdxs)
|
|
133
|
+
emitUnit(i);
|
|
134
|
+
if (alliedIdxs.length > 0) {
|
|
135
|
+
lines.push("ALLIED UNITS", "");
|
|
136
|
+
for (const i of alliedIdxs)
|
|
137
|
+
emitUnit(i);
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=newrecruit-wtc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/export/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit wtc-compact and wtc-full text exporters.\n *\n * Both formats lead with a `++++++++` summary header and then list units. The\n * compact body packs each unit onto one line; the full body uses section\n * headers (`BATTLELINE` / `ALLIED UNITS`) and two-line unit blocks with\n * `N with <wargear>` and `• Nx <ModelType>` per-model breakdowns.\n *\n * Faction & detachment display names are reconstructed via\n * {@link titleCaseId}. `CharN:` numbering is re-derived heuristically from\n * `is_warlord || enhancement || leader_attachment` (see\n * {@link charSlotAssignment}). The `+ SECONDARY:` summary line is omitted —\n * tournament secondaries aren't modelled in the Roster.\n *\n * @packageDocumentation\n */\nimport type { Roster, RosterUnit } from \"../import/types.js\";\nimport {\n charSlotAssignment,\n displayedUnitPoints,\n titleCaseId,\n totalArmyPoints,\n} from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\nconst FENCE = \"+++++++++++++++++++++++++++++++++++++++++++++++\";\n\nfunction wargearListText(unit: RosterUnit, includeWarlordTag: boolean): string {\n const parts: string[] = [];\n for (const w of unit.wargear) {\n parts.push(w.count > 1 ? `${w.count}x ${w.ref.raw_name}` : w.ref.raw_name);\n }\n if (includeWarlordTag && unit.is_warlord) parts.push(\"Warlord\");\n return parts.join(\", \");\n}\n\nfunction header(roster: Roster, units: readonly RosterUnit[], charSlots: readonly (number | null)[]): string {\n const faction = titleCaseId(roster.faction_id) ?? \"Unknown\";\n const detachment = titleCaseId(roster.detachment_id);\n const limit = roster.points.declared_limit ?? totalArmyPoints(roster);\n const total = roster.points.total_reported ?? totalArmyPoints(roster);\n\n const warlordIdx = units.findIndex((u) => u.is_warlord);\n const warlord =\n warlordIdx >= 0\n ? `Char${charSlots[warlordIdx]}: ${units[warlordIdx].ref.raw_name}`\n : \"—\";\n\n const enhancementIdx = units.findIndex((u) => u.enhancement !== null);\n let enhancement = \"—\";\n if (enhancementIdx >= 0) {\n const u = units[enhancementIdx];\n enhancement = `${u.enhancement!.raw_name} (on Char${charSlots[enhancementIdx]}: ${u.ref.raw_name})`;\n }\n\n const lines: string[] = [\n FENCE,\n `+ LIST NAME: ${roster.name}`,\n `+ FACTION KEYWORD: ${faction}`,\n `+ DETACHMENT: ${detachment ?? \"—\"}`,\n `+ TOTAL ARMY POINTS: ${total}pts`,\n `+ POINTS LIMIT: ${limit}pts`,\n `+`,\n `+ WARLORD: ${warlord}`,\n `+ ENHANCEMENT: ${enhancement}`,\n `+ NUMBER OF UNITS: ${units.length}`,\n FENCE,\n ];\n return lines.join(\"\\n\");\n}\n\nfunction isAlliedUnit(u: RosterUnit, factionId: string | null): boolean {\n // Heuristic: the Roster doesn't tag allied units explicitly, but the\n // multi-force diagnostic + the fact that we only carry the primary faction\n // means non-primary-faction units aren't recognisable. The only fact we *do*\n // have is `leader_attachment` and warlord/enhancement (which mark primary\n // characters). For unit grouping in wtc-full we simply place everything in\n // BATTLELINE unless the Roster's multi-force flag suggests there's an allied\n // detachment. Since the flag is a diagnostic warning, not a per-unit tag,\n // wtc-full export collapses to a single BATTLELINE section.\n void u;\n void factionId;\n return false;\n}\n\nexport const newRecruitWtcCompactSerializer: RosterSerializer = {\n id: \"newrecruit-wtc-compact\",\n\n serialize(roster: Roster): string {\n const units = roster.units;\n const slots = charSlotAssignment(units);\n const lines: string[] = [header(roster, units, slots), \"\"];\n\n for (let i = 0; i < units.length; i += 1) {\n const u = units[i];\n const prefix = slots[i] !== null ? `Char${slots[i]}: ` : \"\";\n const pts = displayedUnitPoints(u);\n const ptsText = pts === null ? \"\" : `${pts} pts`;\n lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText}): ${wargearListText(u, true)}`);\n if (u.enhancement) {\n const enhText =\n u.enhancement_points === null\n ? `Enhancement: ${u.enhancement.raw_name}`\n : `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;\n lines.push(enhText);\n }\n }\n\n return lines.join(\"\\n\") + \"\\n\";\n },\n};\n\n/**\n * For a multi-model unit, render its wargear as `N with <per-model list>` when\n * the wargear divides evenly across models (the natural NewRecruit form).\n * Otherwise emit `1 with <full Nx counts>` so the counts round-trip exactly.\n */\nfunction multiModelWithLine(u: RosterUnit): string {\n // Homogeneous when every weapon count divides cleanly by model_count.\n const divisible = u.wargear.every((w) => w.count % u.model_count === 0);\n if (divisible) {\n const perModel = u.wargear\n .map((w) => {\n const c = w.count / u.model_count;\n return c > 1 ? `${c}x ${w.ref.raw_name}` : w.ref.raw_name;\n })\n .filter((s) => s.length > 0);\n if (u.is_warlord) perModel.push(\"Warlord\");\n return `${u.model_count} with ${perModel.join(\", \")}`;\n }\n return `1 with ${wargearListText(u, true)}`;\n}\n\nexport const newRecruitWtcFullSerializer: RosterSerializer = {\n id: \"newrecruit-wtc-full\",\n\n serialize(roster: Roster): string {\n const units = roster.units;\n const slots = charSlotAssignment(units);\n\n const battlelineIdxs: number[] = [];\n const alliedIdxs: number[] = [];\n for (let i = 0; i < units.length; i += 1) {\n if (isAlliedUnit(units[i], roster.faction_id)) alliedIdxs.push(i);\n else battlelineIdxs.push(i);\n }\n\n const lines: string[] = [header(roster, units, slots), \"\", \"BATTLELINE\", \"\"];\n\n const emitUnit = (i: number): void => {\n const u = units[i];\n const prefix = slots[i] !== null ? `Char${slots[i]}: ` : \"\";\n const pts = displayedUnitPoints(u);\n const ptsText = pts === null ? \"\" : `${pts} pts`;\n lines.push(`${prefix}${u.model_count}x ${u.ref.raw_name} (${ptsText})`);\n\n if (u.model_count > 1) {\n lines.push(multiModelWithLine(u));\n } else {\n lines.push(`1 with ${wargearListText(u, true)}`);\n }\n\n if (u.enhancement) {\n const enhText =\n u.enhancement_points === null\n ? `Enhancement: ${u.enhancement.raw_name}`\n : `Enhancement: ${u.enhancement.raw_name} (+${u.enhancement_points} pts)`;\n lines.push(enhText);\n }\n lines.push(\"\");\n };\n\n for (const i of battlelineIdxs) emitUnit(i);\n\n if (alliedIdxs.length > 0) {\n lines.push(\"ALLIED UNITS\", \"\");\n for (const i of alliedIdxs) emitUnit(i);\n }\n\n return lines.join(\"\\n\");\n },\n};\n"]}
|