@alpaca-software/40kdc-data 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abilities-resolver/index.d.ts +9 -0
- package/dist/abilities-resolver/index.d.ts.map +1 -0
- package/dist/abilities-resolver/index.js +9 -0
- package/dist/abilities-resolver/index.js.map +1 -0
- package/dist/abilities-resolver/resolver.d.ts +64 -0
- package/dist/abilities-resolver/resolver.d.ts.map +1 -0
- package/dist/abilities-resolver/resolver.js +135 -0
- package/dist/abilities-resolver/resolver.js.map +1 -0
- package/dist/bundle-schemas.d.ts +1 -0
- package/dist/bundle-schemas.d.ts.map +1 -0
- package/dist/bundle-schemas.js +1 -0
- package/dist/bundle-schemas.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen-data.d.ts +1 -0
- package/dist/codegen-data.d.ts.map +1 -0
- package/dist/codegen-data.js +2 -0
- package/dist/codegen-data.js.map +1 -0
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +1 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/translate.d.ts +1 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +1 -0
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/validate-all.d.ts +1 -0
- package/dist/commands/validate-all.d.ts.map +1 -0
- package/dist/commands/validate-all.js +1 -0
- package/dist/commands/validate-all.js.map +1 -0
- package/dist/commands/validate-core.d.ts +1 -0
- package/dist/commands/validate-core.d.ts.map +1 -0
- package/dist/commands/validate-core.js +1 -0
- package/dist/commands/validate-core.js.map +1 -0
- package/dist/commands/validate-enrichment.d.ts +1 -0
- package/dist/commands/validate-enrichment.d.ts.map +1 -0
- package/dist/commands/validate-enrichment.js +1 -0
- package/dist/commands/validate-enrichment.js.map +1 -0
- package/dist/convert-faction.d.ts +1 -0
- package/dist/convert-faction.d.ts.map +1 -0
- package/dist/convert-faction.js +1 -0
- package/dist/convert-faction.js.map +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts.map +1 -0
- package/dist/converters/configs/adepta-sororitas.js +1 -0
- package/dist/converters/configs/adepta-sororitas.js.map +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-astartes.js +1 -0
- package/dist/converters/configs/adeptus-astartes.js.map +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-custodes.js +1 -0
- package/dist/converters/configs/adeptus-custodes.js.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js.map +1 -0
- package/dist/converters/configs/aeldari.d.ts +1 -0
- package/dist/converters/configs/aeldari.d.ts.map +1 -0
- package/dist/converters/configs/aeldari.js +1 -0
- package/dist/converters/configs/aeldari.js.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js.map +1 -0
- package/dist/converters/configs/astra-militarum.d.ts +1 -0
- package/dist/converters/configs/astra-militarum.d.ts.map +1 -0
- package/dist/converters/configs/astra-militarum.js +1 -0
- package/dist/converters/configs/astra-militarum.js.map +1 -0
- package/dist/converters/configs/black-templars.d.ts +1 -0
- package/dist/converters/configs/black-templars.d.ts.map +1 -0
- package/dist/converters/configs/black-templars.js +1 -0
- package/dist/converters/configs/black-templars.js.map +1 -0
- package/dist/converters/configs/blood-angels.d.ts +1 -0
- package/dist/converters/configs/blood-angels.d.ts.map +1 -0
- package/dist/converters/configs/blood-angels.js +1 -0
- package/dist/converters/configs/blood-angels.js.map +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts.map +1 -0
- package/dist/converters/configs/chaos-daemons.js +1 -0
- package/dist/converters/configs/chaos-daemons.js.map +1 -0
- package/dist/converters/configs/chaos-knights.d.ts +1 -0
- package/dist/converters/configs/chaos-knights.d.ts.map +1 -0
- package/dist/converters/configs/chaos-knights.js +1 -0
- package/dist/converters/configs/chaos-knights.js.map +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts.map +1 -0
- package/dist/converters/configs/chaos-space-marines.js +1 -0
- package/dist/converters/configs/chaos-space-marines.js.map +1 -0
- package/dist/converters/configs/crimson-fists.d.ts +1 -0
- package/dist/converters/configs/crimson-fists.d.ts.map +1 -0
- package/dist/converters/configs/crimson-fists.js +1 -0
- package/dist/converters/configs/crimson-fists.js.map +1 -0
- package/dist/converters/configs/dark-angels.d.ts +1 -0
- package/dist/converters/configs/dark-angels.d.ts.map +1 -0
- package/dist/converters/configs/dark-angels.js +1 -0
- package/dist/converters/configs/dark-angels.js.map +1 -0
- package/dist/converters/configs/death-guard.d.ts +1 -0
- package/dist/converters/configs/death-guard.d.ts.map +1 -0
- package/dist/converters/configs/death-guard.js +1 -0
- package/dist/converters/configs/death-guard.js.map +1 -0
- package/dist/converters/configs/deathwatch.d.ts +1 -0
- package/dist/converters/configs/deathwatch.d.ts.map +1 -0
- package/dist/converters/configs/deathwatch.js +1 -0
- package/dist/converters/configs/deathwatch.js.map +1 -0
- package/dist/converters/configs/drukhari.d.ts +1 -0
- package/dist/converters/configs/drukhari.d.ts.map +1 -0
- package/dist/converters/configs/drukhari.js +1 -0
- package/dist/converters/configs/drukhari.js.map +1 -0
- package/dist/converters/configs/emperors-children.d.ts +1 -0
- package/dist/converters/configs/emperors-children.d.ts.map +1 -0
- package/dist/converters/configs/emperors-children.js +1 -0
- package/dist/converters/configs/emperors-children.js.map +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts.map +1 -0
- package/dist/converters/configs/genestealer-cults.js +1 -0
- package/dist/converters/configs/genestealer-cults.js.map +1 -0
- package/dist/converters/configs/grey-knights.d.ts +1 -0
- package/dist/converters/configs/grey-knights.d.ts.map +1 -0
- package/dist/converters/configs/grey-knights.js +1 -0
- package/dist/converters/configs/grey-knights.js.map +1 -0
- package/dist/converters/configs/imperial-fists.d.ts +1 -0
- package/dist/converters/configs/imperial-fists.d.ts.map +1 -0
- package/dist/converters/configs/imperial-fists.js +1 -0
- package/dist/converters/configs/imperial-fists.js.map +1 -0
- package/dist/converters/configs/imperial-knights.d.ts +1 -0
- package/dist/converters/configs/imperial-knights.d.ts.map +1 -0
- package/dist/converters/configs/imperial-knights.js +1 -0
- package/dist/converters/configs/imperial-knights.js.map +1 -0
- package/dist/converters/configs/iron-hands.d.ts +1 -0
- package/dist/converters/configs/iron-hands.d.ts.map +1 -0
- package/dist/converters/configs/iron-hands.js +1 -0
- package/dist/converters/configs/iron-hands.js.map +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts.map +1 -0
- package/dist/converters/configs/leagues-of-votann.js +1 -0
- package/dist/converters/configs/leagues-of-votann.js.map +1 -0
- package/dist/converters/configs/necrons.d.ts +1 -0
- package/dist/converters/configs/necrons.d.ts.map +1 -0
- package/dist/converters/configs/necrons.js +1 -0
- package/dist/converters/configs/necrons.js.map +1 -0
- package/dist/converters/configs/orks.d.ts +1 -0
- package/dist/converters/configs/orks.d.ts.map +1 -0
- package/dist/converters/configs/orks.js +1 -0
- package/dist/converters/configs/orks.js.map +1 -0
- package/dist/converters/configs/raven-guard.d.ts +1 -0
- package/dist/converters/configs/raven-guard.d.ts.map +1 -0
- package/dist/converters/configs/raven-guard.js +1 -0
- package/dist/converters/configs/raven-guard.js.map +1 -0
- package/dist/converters/configs/salamanders.d.ts +1 -0
- package/dist/converters/configs/salamanders.d.ts.map +1 -0
- package/dist/converters/configs/salamanders.js +1 -0
- package/dist/converters/configs/salamanders.js.map +1 -0
- package/dist/converters/configs/space-wolves.d.ts +1 -0
- package/dist/converters/configs/space-wolves.d.ts.map +1 -0
- package/dist/converters/configs/space-wolves.js +1 -0
- package/dist/converters/configs/space-wolves.js.map +1 -0
- package/dist/converters/configs/tau-empire.d.ts +1 -0
- package/dist/converters/configs/tau-empire.d.ts.map +1 -0
- package/dist/converters/configs/tau-empire.js +1 -0
- package/dist/converters/configs/tau-empire.js.map +1 -0
- package/dist/converters/configs/thousand-sons.d.ts +1 -0
- package/dist/converters/configs/thousand-sons.d.ts.map +1 -0
- package/dist/converters/configs/thousand-sons.js +1 -0
- package/dist/converters/configs/thousand-sons.js.map +1 -0
- package/dist/converters/configs/tyranids.d.ts +1 -0
- package/dist/converters/configs/tyranids.d.ts.map +1 -0
- package/dist/converters/configs/tyranids.js +1 -0
- package/dist/converters/configs/tyranids.js.map +1 -0
- package/dist/converters/configs/ultramarines.d.ts +1 -0
- package/dist/converters/configs/ultramarines.d.ts.map +1 -0
- package/dist/converters/configs/ultramarines.js +1 -0
- package/dist/converters/configs/ultramarines.js.map +1 -0
- package/dist/converters/configs/white-scars.d.ts +1 -0
- package/dist/converters/configs/white-scars.d.ts.map +1 -0
- package/dist/converters/configs/white-scars.js +1 -0
- package/dist/converters/configs/white-scars.js.map +1 -0
- package/dist/converters/configs/world-eaters.d.ts +1 -0
- package/dist/converters/configs/world-eaters.d.ts.map +1 -0
- package/dist/converters/configs/world-eaters.js +1 -0
- package/dist/converters/configs/world-eaters.js.map +1 -0
- package/dist/converters/faction-config.d.ts +1 -0
- package/dist/converters/faction-config.d.ts.map +1 -0
- package/dist/converters/faction-config.js +1 -0
- package/dist/converters/faction-config.js.map +1 -0
- package/dist/converters/id-generator.d.ts +1 -0
- package/dist/converters/id-generator.d.ts.map +1 -0
- package/dist/converters/id-generator.js +1 -0
- package/dist/converters/id-generator.js.map +1 -0
- package/dist/converters/keyword-filter.d.ts +1 -0
- package/dist/converters/keyword-filter.d.ts.map +1 -0
- package/dist/converters/keyword-filter.js +1 -0
- package/dist/converters/keyword-filter.js.map +1 -0
- package/dist/converters/stat-parser.d.ts +1 -0
- package/dist/converters/stat-parser.d.ts.map +1 -0
- package/dist/converters/stat-parser.js +1 -0
- package/dist/converters/stat-parser.js.map +1 -0
- package/dist/converters/view-selector.d.ts +1 -0
- package/dist/converters/view-selector.d.ts.map +1 -0
- package/dist/converters/view-selector.js +1 -0
- package/dist/converters/view-selector.js.map +1 -0
- package/dist/converters/weapon-dedup.d.ts +1 -0
- package/dist/converters/weapon-dedup.d.ts.map +1 -0
- package/dist/converters/weapon-dedup.js +1 -0
- package/dist/converters/weapon-dedup.js.map +1 -0
- package/dist/cruncher/buffs.d.ts +184 -0
- package/dist/cruncher/buffs.d.ts.map +1 -0
- package/dist/cruncher/buffs.js +150 -0
- package/dist/cruncher/buffs.js.map +1 -0
- package/dist/cruncher/engine.d.ts +50 -0
- package/dist/cruncher/engine.d.ts.map +1 -0
- package/dist/cruncher/engine.js +312 -0
- package/dist/cruncher/engine.js.map +1 -0
- package/dist/cruncher/from-dsl.d.ts +69 -0
- package/dist/cruncher/from-dsl.d.ts.map +1 -0
- package/dist/cruncher/from-dsl.js +523 -0
- package/dist/cruncher/from-dsl.js.map +1 -0
- package/dist/cruncher/from-keyword.d.ts +35 -0
- package/dist/cruncher/from-keyword.d.ts.map +1 -0
- package/dist/cruncher/from-keyword.js +159 -0
- package/dist/cruncher/from-keyword.js.map +1 -0
- package/dist/cruncher/get-buffs.d.ts +12 -0
- package/dist/cruncher/get-buffs.d.ts.map +1 -0
- package/dist/cruncher/get-buffs.js +7 -0
- package/dist/cruncher/get-buffs.js.map +1 -0
- package/dist/cruncher/index.d.ts +11 -0
- package/dist/cruncher/index.d.ts.map +1 -0
- package/dist/cruncher/index.js +11 -0
- package/dist/cruncher/index.js.map +1 -0
- package/dist/data/bundle.generated.d.ts +1 -0
- package/dist/data/bundle.generated.d.ts.map +1 -0
- package/dist/data/bundle.generated.js +2 -1
- package/dist/data/bundle.generated.js.map +1 -0
- package/dist/data/collection.d.ts +1 -0
- package/dist/data/collection.d.ts.map +1 -0
- package/dist/data/collection.js +1 -0
- package/dist/data/collection.js.map +1 -0
- package/dist/data/dataset.d.ts +54 -2
- package/dist/data/dataset.d.ts.map +1 -0
- package/dist/data/dataset.js +111 -1
- package/dist/data/dataset.js.map +1 -0
- package/dist/data/entities.d.ts +70 -2
- package/dist/data/entities.d.ts.map +1 -0
- package/dist/data/entities.js +122 -0
- package/dist/data/entities.js.map +1 -0
- package/dist/data/index.d.ts +9 -1
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +14 -1
- package/dist/data/index.js.map +1 -0
- package/dist/data/normalize.d.ts +1 -0
- package/dist/data/normalize.d.ts.map +1 -0
- package/dist/data/normalize.js +1 -0
- package/dist/data/normalize.js.map +1 -0
- package/dist/data/roster-resolve.d.ts +33 -0
- package/dist/data/roster-resolve.d.ts.map +1 -0
- package/dist/data/roster-resolve.js +36 -0
- package/dist/data/roster-resolve.js.map +1 -0
- package/dist/data/types.d.ts +4 -1
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +2 -0
- package/dist/data/types.js.map +1 -0
- package/dist/export/helpers.d.ts +33 -0
- package/dist/export/helpers.d.ts.map +1 -0
- package/dist/export/helpers.js +57 -0
- package/dist/export/helpers.js.map +1 -0
- package/dist/export/index.d.ts +21 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +25 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/newrecruit-json.d.ts +3 -0
- package/dist/export/newrecruit-json.d.ts.map +1 -0
- package/dist/export/newrecruit-json.js +140 -0
- package/dist/export/newrecruit-json.js.map +1 -0
- package/dist/export/newrecruit-simple.d.ts +3 -0
- package/dist/export/newrecruit-simple.d.ts.map +1 -0
- package/dist/export/newrecruit-simple.js +76 -0
- package/dist/export/newrecruit-simple.js.map +1 -0
- package/dist/export/newrecruit-wtc.d.ts +4 -0
- package/dist/export/newrecruit-wtc.d.ts.map +1 -0
- package/dist/export/newrecruit-wtc.js +142 -0
- package/dist/export/newrecruit-wtc.js.map +1 -0
- package/dist/export/roster-json.d.ts +3 -0
- package/dist/export/roster-json.d.ts.map +1 -0
- package/dist/export/roster-json.js +8 -0
- package/dist/export/roster-json.js.map +1 -0
- package/dist/export/serializer.d.ts +27 -0
- package/dist/export/serializer.d.ts.map +1 -0
- package/dist/export/serializer.js +2 -0
- package/dist/export/serializer.js.map +1 -0
- package/dist/gen-conformance.d.ts +1 -0
- package/dist/gen-conformance.d.ts.map +1 -0
- package/dist/gen-conformance.js +73 -12
- package/dist/gen-conformance.js.map +1 -0
- package/dist/generated.d.ts +194 -118
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +1 -0
- package/dist/generated.js.map +1 -0
- package/dist/import/adapter.d.ts +4 -3
- package/dist/import/adapter.d.ts.map +1 -0
- package/dist/import/adapter.js +1 -0
- package/dist/import/adapter.js.map +1 -0
- package/dist/import/decode.d.ts +1 -0
- package/dist/import/decode.d.ts.map +1 -0
- package/dist/import/decode.js +1 -0
- package/dist/import/decode.js.map +1 -0
- package/dist/import/import-roster.d.ts +35 -0
- package/dist/import/import-roster.d.ts.map +1 -0
- package/dist/import/import-roster.js +97 -0
- package/dist/import/import-roster.js.map +1 -0
- package/dist/import/index.d.ts +7 -3
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +5 -1
- package/dist/import/index.js.map +1 -0
- package/dist/import/listforge.d.ts +1 -0
- package/dist/import/listforge.d.ts.map +1 -0
- package/dist/import/listforge.js +7 -1
- package/dist/import/listforge.js.map +1 -0
- package/dist/import/newrecruit-json.d.ts +31 -0
- package/dist/import/newrecruit-json.d.ts.map +1 -0
- package/dist/import/newrecruit-json.js +224 -0
- package/dist/import/newrecruit-json.js.map +1 -0
- package/dist/import/newrecruit-simple.d.ts +29 -0
- package/dist/import/newrecruit-simple.d.ts.map +1 -0
- package/dist/import/newrecruit-simple.js +200 -0
- package/dist/import/newrecruit-simple.js.map +1 -0
- package/dist/import/newrecruit-text.d.ts +48 -0
- package/dist/import/newrecruit-text.d.ts.map +1 -0
- package/dist/import/newrecruit-text.js +96 -0
- package/dist/import/newrecruit-text.js.map +1 -0
- package/dist/import/newrecruit-wtc.d.ts +36 -0
- package/dist/import/newrecruit-wtc.d.ts.map +1 -0
- package/dist/import/newrecruit-wtc.js +334 -0
- package/dist/import/newrecruit-wtc.js.map +1 -0
- package/dist/import/resolve.d.ts +3 -2
- package/dist/import/resolve.d.ts.map +1 -0
- package/dist/import/resolve.js +5 -2
- package/dist/import/resolve.js.map +1 -0
- package/dist/import/types.d.ts +11 -1
- package/dist/import/types.d.ts.map +1 -0
- package/dist/import/types.js +1 -0
- package/dist/import/types.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -0
- package/dist/known-support-10e.d.ts +1 -0
- package/dist/known-support-10e.d.ts.map +1 -0
- package/dist/known-support-10e.js +1 -0
- package/dist/known-support-10e.js.map +1 -0
- package/dist/link-abilities.d.ts +41 -0
- package/dist/link-abilities.d.ts.map +1 -0
- package/dist/link-abilities.js +159 -0
- package/dist/link-abilities.js.map +1 -0
- package/dist/migrations/2026-weapon-keywords.d.ts +2 -0
- package/dist/migrations/2026-weapon-keywords.d.ts.map +1 -0
- package/dist/migrations/2026-weapon-keywords.js +243 -0
- package/dist/migrations/2026-weapon-keywords.js.map +1 -0
- package/dist/port-10e-faction.d.ts +1 -0
- package/dist/port-10e-faction.d.ts.map +1 -0
- package/dist/port-10e-faction.js +1 -0
- package/dist/port-10e-faction.js.map +1 -0
- package/dist/report.d.ts +1 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +1 -0
- package/dist/report.js.map +1 -0
- package/dist/rube-goldberg.d.ts +3 -0
- package/dist/rube-goldberg.d.ts.map +1 -0
- package/dist/rube-goldberg.js +109 -0
- package/dist/rube-goldberg.js.map +1 -0
- package/dist/schema-loader.d.ts +1 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/schema-loader.js +1 -0
- package/dist/schema-loader.js.map +1 -0
- package/dist/validate.d.ts +1 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +2 -0
- package/dist/validate.js.map +1 -0
- package/package.json +7 -2
- package/schemas/core/roster.schema.json +17 -4
- package/schemas/core/weapon-keyword.schema.json +31 -0
- package/schemas/core/weapon.schema.json +22 -1
- package/schemas/enrichment/ability-dsl/effect.schema.json +23 -1
- package/dist/import/import-listforge.d.ts +0 -23
- package/dist/import/import-listforge.js +0 -32
|
@@ -0,0 +1,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":"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","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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roster-json.d.ts","sourceRoot":"","sources":["../../src/export/roster-json.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,eAAO,MAAM,oBAAoB,EAAE,gBAKlC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roster-json.js","sourceRoot":"","sources":["../../src/export/roster-json.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,MAAM,CAAC,MAAM,oBAAoB,GAAqB;IACpD,EAAE,EAAE,aAAa;IACjB,SAAS,CAAC,MAAc;QACtB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;CACF,CAAC","sourcesContent":["/**\n * Canonical Roster JSON serializer — emits the {@link Roster} as 2-space JSON,\n * the same shape the importers consume. This is the lossless pivot, so the\n * pretty-printed text is exactly `roster.schema.json` shape.\n *\n * @packageDocumentation\n */\nimport type { Roster } from \"../import/types.js\";\nimport { prettyJson } from \"./helpers.js\";\nimport type { RosterSerializer } from \"./serializer.js\";\n\nexport const rosterJsonSerializer: RosterSerializer = {\n id: \"roster-json\",\n serialize(roster: Roster): string {\n return prettyJson(roster);\n },\n};\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The roster-serializer seam — symmetric counterpart to the
|
|
3
|
+
* {@link FormatAdapter} import seam.
|
|
4
|
+
*
|
|
5
|
+
* Each supported export target implements {@link RosterSerializer}: it takes a
|
|
6
|
+
* fully-resolved {@link Roster} and produces a deterministic string in that
|
|
7
|
+
* format. The seam stays Dataset-free so the TS and Rust mirrors can produce
|
|
8
|
+
* byte-identical output for conformance.
|
|
9
|
+
*
|
|
10
|
+
* Five targets are registered:
|
|
11
|
+
* - `newrecruit-json` — NewRecruit-shaped JSON skeleton (rules-free).
|
|
12
|
+
* - `newrecruit-wtc-compact` — tournament-friendly one-line-per-unit text.
|
|
13
|
+
* - `newrecruit-wtc-full` — tournament-friendly section-and-wargear text.
|
|
14
|
+
* - `newrecruit-simple` — markdown-ish text.
|
|
15
|
+
* - `roster-json` — canonical Roster JSON (the lossless pivot).
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
import type { Roster } from "../import/types.js";
|
|
20
|
+
/** Stable id for an export target. */
|
|
21
|
+
export type ExportFormat = "newrecruit-json" | "newrecruit-wtc-compact" | "newrecruit-wtc-full" | "newrecruit-simple" | "roster-json";
|
|
22
|
+
/** Serializes a {@link Roster} into one specific format. */
|
|
23
|
+
export interface RosterSerializer {
|
|
24
|
+
id: ExportFormat;
|
|
25
|
+
serialize(roster: Roster): string;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=serializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../src/export/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,sCAAsC;AACtC,MAAM,MAAM,YAAY,GACpB,iBAAiB,GACjB,wBAAwB,GACxB,qBAAqB,GACrB,mBAAmB,GACnB,aAAa,CAAC;AAElB,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,YAAY,CAAC;IACjB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.js","sourceRoot":"","sources":["../../src/export/serializer.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * The roster-serializer seam — symmetric counterpart to the\n * {@link FormatAdapter} import seam.\n *\n * Each supported export target implements {@link RosterSerializer}: it takes a\n * fully-resolved {@link Roster} and produces a deterministic string in that\n * format. The seam stays Dataset-free so the TS and Rust mirrors can produce\n * byte-identical output for conformance.\n *\n * Five targets are registered:\n * - `newrecruit-json` — NewRecruit-shaped JSON skeleton (rules-free).\n * - `newrecruit-wtc-compact` — tournament-friendly one-line-per-unit text.\n * - `newrecruit-wtc-full` — tournament-friendly section-and-wargear text.\n * - `newrecruit-simple` — markdown-ish text.\n * - `roster-json` — canonical Roster JSON (the lossless pivot).\n *\n * @packageDocumentation\n */\nimport type { Roster } from \"../import/types.js\";\n\n/** Stable id for an export target. */\nexport type ExportFormat =\n | \"newrecruit-json\"\n | \"newrecruit-wtc-compact\"\n | \"newrecruit-wtc-full\"\n | \"newrecruit-simple\"\n | \"roster-json\";\n\n/** Serializes a {@link Roster} into one specific format. */\nexport interface RosterSerializer {\n id: ExportFormat;\n serialize(roster: Roster): string;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gen-conformance.d.ts","sourceRoot":"","sources":["../src/gen-conformance.ts"],"names":[],"mappings":""}
|
package/dist/gen-conformance.js
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate the cross-implementation conformance corpus under repo-root
|
|
3
|
-
* `conformance/`. The TypeScript package is the reference implementation, so
|
|
4
|
-
* goldens it emits are what the Rust crate must reproduce byte-for-byte
|
|
3
|
+
* `conformance/`. The TypeScript package is the reference implementation, so
|
|
4
|
+
* the goldens it emits are what the Rust crate must reproduce byte-for-byte
|
|
5
5
|
* (structurally). Run via `npm run gen:conformance`; CI regenerates and asserts
|
|
6
6
|
* `git diff --exit-code conformance/` is clean.
|
|
7
7
|
*
|
|
8
8
|
* Outputs:
|
|
9
9
|
* - `conformance/normalize.json` — `[{ input, expected }]` for normalizeName.
|
|
10
|
-
* - `conformance/roster/<case>/expected.roster.json` — the resolved Roster
|
|
11
|
-
*
|
|
10
|
+
* - `conformance/roster/<case>/expected.roster.json` — the resolved Roster.
|
|
11
|
+
* - `conformance/roster/<case>/expected.<fmt>.{txt,json}` — every export
|
|
12
|
+
* target's golden output. The TS exporter is the oracle; the Rust mirror
|
|
13
|
+
* asserts byte-equal output for the same Roster.
|
|
14
|
+
* - `conformance/roster/<case>/input.newrecruit-{wtc-compact,wtc-full,simple}.txt`
|
|
15
|
+
* — text inputs derived from the seed by the exporter, so a re-import
|
|
16
|
+
* regression in either implementation surfaces immediately.
|
|
17
|
+
*
|
|
18
|
+
* Seeding: each `<case>/` carries one canonical input — either the legacy
|
|
19
|
+
* `input.json` (ListForge) or `input.newrecruit-json.json` (NewRecruit). Other
|
|
20
|
+
* inputs are derived.
|
|
12
21
|
*/
|
|
13
|
-
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
14
23
|
import { dirname, join } from "node:path";
|
|
15
24
|
import { fileURLToPath } from "node:url";
|
|
16
25
|
import { Dataset } from "./data/dataset.js";
|
|
17
26
|
import { normalizeName } from "./data/normalize.js";
|
|
18
|
-
import {
|
|
27
|
+
import { exportRoster } from "./export/index.js";
|
|
28
|
+
import { importRoster } from "./import/import-roster.js";
|
|
19
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
30
|
const REPO_ROOT = join(__dirname, "../..");
|
|
21
31
|
const CONFORMANCE = join(REPO_ROOT, "conformance");
|
|
22
|
-
/** Inputs for the normalize table — every rule, plus real accented/quoted names. */
|
|
23
32
|
const NORMALIZE_INPUTS = [
|
|
24
33
|
// NFD diacritic strip
|
|
25
34
|
"Khârn the Betrayer",
|
|
@@ -44,15 +53,47 @@ const NORMALIZE_INPUTS = [
|
|
|
44
53
|
"Khorne",
|
|
45
54
|
"Khârn",
|
|
46
55
|
];
|
|
47
|
-
/** Pretty JSON with a trailing newline (matches the repo's 2-space convention). */
|
|
48
56
|
function writeJson(path, value) {
|
|
49
57
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
50
58
|
}
|
|
59
|
+
function writeText(path, value) {
|
|
60
|
+
writeFileSync(path, value);
|
|
61
|
+
}
|
|
51
62
|
function genNormalize() {
|
|
52
63
|
const table = NORMALIZE_INPUTS.map((input) => ({ input, expected: normalizeName(input) }));
|
|
53
64
|
writeJson(join(CONFORMANCE, "normalize.json"), table);
|
|
54
65
|
console.log(`normalize.json: ${table.length} cases`);
|
|
55
66
|
}
|
|
67
|
+
/** Locate the canonical input for a fixture dir: prefer `input.json` (legacy
|
|
68
|
+
* ListForge), fall back to `input.newrecruit-json.json` (NewRecruit). */
|
|
69
|
+
function seedRoster(caseDir, ds) {
|
|
70
|
+
const candidates = ["input.json", "input.newrecruit-json.json"];
|
|
71
|
+
for (const name of candidates) {
|
|
72
|
+
const path = join(caseDir, name);
|
|
73
|
+
if (existsSync(path)) {
|
|
74
|
+
const decoded = JSON.parse(readFileSync(path, "utf8"));
|
|
75
|
+
return importRoster(decoded, { dataset: ds });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`no canonical input found in ${caseDir}`);
|
|
79
|
+
}
|
|
80
|
+
const TEXT_FORMATS = [
|
|
81
|
+
{
|
|
82
|
+
format: "newrecruit-wtc-compact",
|
|
83
|
+
inputName: "input.newrecruit-wtc-compact.txt",
|
|
84
|
+
goldenName: "expected.newrecruit-wtc-compact.txt",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
format: "newrecruit-wtc-full",
|
|
88
|
+
inputName: "input.newrecruit-wtc-full.txt",
|
|
89
|
+
goldenName: "expected.newrecruit-wtc-full.txt",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
format: "newrecruit-simple",
|
|
93
|
+
inputName: "input.newrecruit-simple.txt",
|
|
94
|
+
goldenName: "expected.newrecruit-simple.txt",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
56
97
|
function genRosters() {
|
|
57
98
|
const ds = Dataset.embedded();
|
|
58
99
|
const rosterDir = join(CONFORMANCE, "roster");
|
|
@@ -60,11 +101,31 @@ function genRosters() {
|
|
|
60
101
|
if (!entry.isDirectory())
|
|
61
102
|
continue;
|
|
62
103
|
const caseDir = join(rosterDir, entry.name);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
const seed = seedRoster(caseDir, ds);
|
|
105
|
+
writeJson(join(caseDir, "expected.roster.json"), seed);
|
|
106
|
+
// JSON export golden — NewRecruit-shaped skeleton.
|
|
107
|
+
const jsonOut = exportRoster(seed, "newrecruit-json");
|
|
108
|
+
writeJson(join(caseDir, "expected.newrecruit-json.json"), JSON.parse(jsonOut));
|
|
109
|
+
// Canonical Roster JSON export — should equal the resolved roster.
|
|
110
|
+
writeJson(join(caseDir, "expected.roster-json.json"), JSON.parse(exportRoster(seed, "roster-json")));
|
|
111
|
+
// Text exports: always write the export golden so the cross-implementation
|
|
112
|
+
// byte-equality check has something to compare against. Only write the
|
|
113
|
+
// `input.*.txt` round-trip seed when the fixture was authored for the
|
|
114
|
+
// NewRecruit pipeline — legacy ListForge fixtures carry decoration
|
|
115
|
+
// (multi-force warnings, leader-attachment inference) that the simple/wtc
|
|
116
|
+
// exporters can't fully preserve, so the round-trip would fail
|
|
117
|
+
// structurally rather than uncover a parser bug.
|
|
118
|
+
const isNewRecruitSeed = existsSync(join(caseDir, "input.newrecruit-json.json"));
|
|
119
|
+
for (const { format, inputName, goldenName } of TEXT_FORMATS) {
|
|
120
|
+
const out = exportRoster(seed, format);
|
|
121
|
+
writeText(join(caseDir, goldenName), out);
|
|
122
|
+
if (isNewRecruitSeed) {
|
|
123
|
+
writeText(join(caseDir, inputName), out);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log(`roster/${entry.name}: ${seed.units.length} units, ${seed.diagnostics.warnings.length} warnings`);
|
|
67
127
|
}
|
|
68
128
|
}
|
|
69
129
|
genNormalize();
|
|
70
130
|
genRosters();
|
|
131
|
+
//# sourceMappingURL=gen-conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gen-conformance.js","sourceRoot":"","sources":["../src/gen-conformance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate the cross-implementation conformance corpus under repo-root\n * `conformance/`. The TypeScript package is the reference implementation, so\n * the goldens it emits are what the Rust crate must reproduce byte-for-byte\n * (structurally). Run via `npm run gen:conformance`; CI regenerates and asserts\n * `git diff --exit-code conformance/` is clean.\n *\n * Outputs:\n * - `conformance/normalize.json` — `[{ input, expected }]` for normalizeName.\n * - `conformance/roster/<case>/expected.roster.json` — the resolved Roster.\n * - `conformance/roster/<case>/expected.<fmt>.{txt,json}` — every export\n * target's golden output. The TS exporter is the oracle; the Rust mirror\n * asserts byte-equal output for the same Roster.\n * - `conformance/roster/<case>/input.newrecruit-{wtc-compact,wtc-full,simple}.txt`\n * — text inputs derived from the seed by the exporter, so a re-import\n * regression in either implementation surfaces immediately.\n *\n * Seeding: each `<case>/` carries one canonical input — either the legacy\n * `input.json` (ListForge) or `input.newrecruit-json.json` (NewRecruit). Other\n * inputs are derived.\n */\nimport { readdirSync, readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Dataset } from \"./data/dataset.js\";\nimport { normalizeName } from \"./data/normalize.js\";\nimport { exportRoster, type ExportFormat } from \"./export/index.js\";\nimport { importRoster } from \"./import/import-roster.js\";\nimport type { Roster } from \"./import/types.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst REPO_ROOT = join(__dirname, \"../..\");\nconst CONFORMANCE = join(REPO_ROOT, \"conformance\");\n\nconst NORMALIZE_INPUTS = [\n // NFD diacritic strip\n \"Khârn the Betrayer\",\n \"Brôkhyr\",\n \"Ûthar\",\n \"Magnús\",\n // apostrophe / quote variants\n \"T'au\",\n \"Be’lakor\",\n \"Kor’sarro Khan\",\n \"Aetaos'rau'keres\",\n \"‘quoted’\",\n // whitespace / hyphen collapse + trim\n \"Brôkhyr Iron-master\",\n \" the betrayer \",\n \"space--marines\",\n // casefold\n \"KHÂRN THE BETRAYER\",\n // already-normalized (idempotence)\n \"kharn the betrayer\",\n // distinctness anchors (must NOT collapse together)\n \"Khorne\",\n \"Khârn\",\n];\n\nfunction writeJson(path: string, value: unknown): void {\n writeFileSync(path, `${JSON.stringify(value, null, 2)}\\n`);\n}\n\nfunction writeText(path: string, value: string): void {\n writeFileSync(path, value);\n}\n\nfunction genNormalize(): void {\n const table = NORMALIZE_INPUTS.map((input) => ({ input, expected: normalizeName(input) }));\n writeJson(join(CONFORMANCE, \"normalize.json\"), table);\n console.log(`normalize.json: ${table.length} cases`);\n}\n\n/** Locate the canonical input for a fixture dir: prefer `input.json` (legacy\n * ListForge), fall back to `input.newrecruit-json.json` (NewRecruit). */\nfunction seedRoster(caseDir: string, ds: Dataset): Roster {\n const candidates = [\"input.json\", \"input.newrecruit-json.json\"];\n for (const name of candidates) {\n const path = join(caseDir, name);\n if (existsSync(path)) {\n const decoded = JSON.parse(readFileSync(path, \"utf8\"));\n return importRoster(decoded, { dataset: ds });\n }\n }\n throw new Error(`no canonical input found in ${caseDir}`);\n}\n\nconst TEXT_FORMATS: { format: ExportFormat; inputName: string; goldenName: string }[] = [\n {\n format: \"newrecruit-wtc-compact\",\n inputName: \"input.newrecruit-wtc-compact.txt\",\n goldenName: \"expected.newrecruit-wtc-compact.txt\",\n },\n {\n format: \"newrecruit-wtc-full\",\n inputName: \"input.newrecruit-wtc-full.txt\",\n goldenName: \"expected.newrecruit-wtc-full.txt\",\n },\n {\n format: \"newrecruit-simple\",\n inputName: \"input.newrecruit-simple.txt\",\n goldenName: \"expected.newrecruit-simple.txt\",\n },\n];\n\nfunction genRosters(): void {\n const ds = Dataset.embedded();\n const rosterDir = join(CONFORMANCE, \"roster\");\n for (const entry of readdirSync(rosterDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const caseDir = join(rosterDir, entry.name);\n\n const seed = seedRoster(caseDir, ds);\n writeJson(join(caseDir, \"expected.roster.json\"), seed);\n\n // JSON export golden — NewRecruit-shaped skeleton.\n const jsonOut = exportRoster(seed, \"newrecruit-json\");\n writeJson(join(caseDir, \"expected.newrecruit-json.json\"), JSON.parse(jsonOut));\n\n // Canonical Roster JSON export — should equal the resolved roster.\n writeJson(join(caseDir, \"expected.roster-json.json\"), JSON.parse(exportRoster(seed, \"roster-json\")));\n\n // Text exports: always write the export golden so the cross-implementation\n // byte-equality check has something to compare against. Only write the\n // `input.*.txt` round-trip seed when the fixture was authored for the\n // NewRecruit pipeline — legacy ListForge fixtures carry decoration\n // (multi-force warnings, leader-attachment inference) that the simple/wtc\n // exporters can't fully preserve, so the round-trip would fail\n // structurally rather than uncover a parser bug.\n const isNewRecruitSeed = existsSync(join(caseDir, \"input.newrecruit-json.json\"));\n for (const { format, inputName, goldenName } of TEXT_FORMATS) {\n const out = exportRoster(seed, format);\n writeText(join(caseDir, goldenName), out);\n if (isNewRecruitSeed) {\n writeText(join(caseDir, inputName), out);\n }\n }\n\n console.log(\n `roster/${entry.name}: ${seed.units.length} units, ${seed.diagnostics.warnings.length} warnings`,\n );\n }\n}\n\ngenNormalize();\ngenRosters();\n"]}
|