@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 @@
|
|
|
1
|
+
{"version":3,"file":"world-eaters.js","sourceRoot":"","sources":["../../../src/converters/configs/world-eaters.ts"],"names":[],"mappings":"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","sourcesContent":["import { registerFaction, type FactionConfig } from \"../faction-config.js\";\n\nconst worldEaters: FactionConfig = {\n sourceFactionId: \"WE\",\n factionId: \"world-eaters\",\n factionName: \"World Eaters\",\n factionAbilityName: \"Blessings of Khorne\",\n factionRuleId: \"blessings-of-khorne\",\n factionKeywords: [\"Chaos\", \"Khorne\", \"World Eaters\"],\n parentFactionId: null,\n aliases: [],\n compositionOverrides: {\n \"khorne-berzerkers\": [\n { name: \"Berzerker Champion\", profile_name: \"Khorne Berzerker\", min: 1, max: 1, default_weapon_ids: [\"bolt-pistol\", \"chainblade\"], is_leader_model: true },\n { name: \"Khorne Berzerker\", min: 9, max: 19, default_weapon_ids: [\"bolt-pistol\", \"chainblade\"], is_leader_model: false },\n ],\n \"jakhals\": [\n { name: \"Jakhal Pack Leader\", profile_name: \"Jakhal\", min: 1, max: 1, default_weapon_ids: [\"autopistol\", \"jakhal-chainblades\"], is_leader_model: true },\n { name: \"Dishonoured\", profile_name: \"Jakhal\", min: 1, max: 2, default_weapon_ids: [\"mauler-chainblade\"], is_leader_model: false },\n { name: \"Jakhal\", min: 8, max: 17, default_weapon_ids: [\"autopistol\", \"jakhal-chainblades\"], is_leader_model: false },\n ],\n \"eightbound\": [\n { name: \"Eightbound Champion\", profile_name: \"Eightbound\", min: 1, max: 1, default_weapon_ids: [\"chainblades\"], is_leader_model: true },\n { name: \"Eightbound\", min: 2, max: 5, default_weapon_ids: [\"chainblades\"], is_leader_model: false },\n ],\n \"exalted-eightbound\": [\n { name: \"Exalted Eightbound Champion\", profile_name: \"Exalted Eightbound\", min: 1, max: 1, default_weapon_ids: [\"chainblades\"], is_leader_model: true },\n { name: \"Exalted Eightbound\", min: 2, max: 5, default_weapon_ids: [\"chainblades\"], is_leader_model: false },\n ],\n \"chaos-terminators\": [\n { name: \"Terminator Champion\", profile_name: \"World Eaters Terminator\", min: 1, max: 1, default_weapon_ids: [\"combi-bolter\", \"accursed-weapon\"], is_leader_model: true },\n { name: \"Chaos Terminator\", profile_name: \"World Eaters Terminator\", min: 4, max: 4, default_weapon_ids: [\"combi-bolter\", \"accursed-weapon\"], is_leader_model: false },\n ],\n \"goremongers\": [\n { name: \"Goremonger Pack Leader\", profile_name: \"Goremongers\", min: 1, max: 1, default_weapon_ids: [\"autopistol\", \"chainblade\", \"close-combat-weapon\"], is_leader_model: true },\n { name: \"Goremonger\", profile_name: \"Goremongers\", min: 7, max: 7, default_weapon_ids: [\"autopistol\", \"chainblade\", \"close-combat-weapon\"], is_leader_model: false },\n ],\n \"chaos-spawn\": [\n { name: \"Chaos Spawn\", min: 2, max: 2, default_weapon_ids: [\"hideous-mutations\"], is_leader_model: false },\n ],\n },\n};\n\nregisterFaction(worldEaters);\n\nexport default worldEaters;\n"]}
|
|
@@ -51,3 +51,4 @@ export interface FactionConfig {
|
|
|
51
51
|
export declare function registerFaction(config: FactionConfig): void;
|
|
52
52
|
export declare function getFactionConfig(factionId: string): FactionConfig;
|
|
53
53
|
export declare function listFactions(): string[];
|
|
54
|
+
//# sourceMappingURL=faction-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"faction-config.d.ts","sourceRoot":"","sources":["../../src/converters/faction-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,eAAe,EAAE,MAAM,CAAC;IAExB,mEAAmE;IACnE,SAAS,EAAE,MAAM,CAAC;IAElB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IAEpB,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAC;IAE3B,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAC;IAEtB,8EAA8E;IAC9E,eAAe,EAAE,MAAM,EAAE,CAAC;IAE1B,iEAAiE;IACjE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B,oCAAoC;IACpC,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB;;;;OAIG;IACH,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAEnD;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE5B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAKD,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAE3D;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CASjE;AAED,wBAAgB,YAAY,IAAI,MAAM,EAAE,CAEvC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"faction-config.js","sourceRoot":"","sources":["../../src/converters/faction-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA0DH,wEAAwE;AACxE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;AAElD,MAAM,UAAU,eAAe,CAAC,MAAqB;IACnD,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,SAAiB;IAChD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,SAAS,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,oBAAoB,SAAS,iBAAiB,SAAS,EAAE,CAC1D,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACrC,CAAC","sourcesContent":["/**\n * Faction-specific configuration for the generic converter.\n *\n * Each faction provides a config that parameterizes the conversion pipeline.\n * Configs are stored in ./configs/ and imported by convert-faction.ts.\n */\n\nexport interface ModelEntry {\n name: string;\n profile_name?: string | null;\n min: number;\n max: number;\n default_weapon_ids?: string[];\n is_leader_model: boolean;\n}\n\nexport interface FactionConfig {\n /** Source faction ID in army-assist data (e.g., \"WE\", \"EC\"). */\n sourceFactionId: string;\n\n /** 40kdc entity ID (e.g., \"world-eaters\", \"emperors-children\"). */\n factionId: string;\n\n /** Display name (e.g., \"World Eaters\", \"Emperor's Children\"). */\n factionName: string;\n\n /** Faction ability name used for view selection on shared units. */\n factionAbilityName: string;\n\n /** 40kdc ID for the faction rule ability (e.g., \"blessings-of-khorne\"). */\n factionRuleId: string;\n\n /** Top-level faction keywords (e.g., [\"Chaos\", \"Khorne\", \"World Eaters\"]). */\n factionKeywords: string[];\n\n /** Parent faction ID if this is a subfaction, null otherwise. */\n parentFactionId: string | null;\n\n /** Aliases for the faction name. */\n aliases: string[];\n\n /**\n * Manual composition overrides for multi-model units.\n * Keyed by 40kdc unit ID (e.g., \"khorne-berzerkers\").\n * Single-model units (vehicles, characters) get auto-generated compositions.\n */\n compositionOverrides: Record<string, ModelEntry[]>;\n\n /**\n * For subfactions that share a source faction (e.g., SM chapters):\n * only include detachments whose names match this list.\n * Stratagems and enhancements are filtered to matching detachments.\n * If undefined, all detachments are included.\n */\n detachmentFilter?: string[];\n\n /**\n * If true, skip unit/weapon/leader-attachment/unit-composition generation.\n * Used by subfactions whose units are inherited from the parent faction.\n */\n skipUnits?: boolean;\n}\n\n/** Registry of all known faction configs, keyed by 40kdc faction ID. */\nconst registry = new Map<string, FactionConfig>();\n\nexport function registerFaction(config: FactionConfig): void {\n registry.set(config.factionId, config);\n}\n\nexport function getFactionConfig(factionId: string): FactionConfig {\n const config = registry.get(factionId);\n if (!config) {\n const available = [...registry.keys()].join(\", \");\n throw new Error(\n `Unknown faction \"${factionId}\". Available: ${available}`\n );\n }\n return config;\n}\n\nexport function listFactions(): string[] {\n return [...registry.keys()].sort();\n}\n"]}
|
|
@@ -12,3 +12,4 @@ export declare function parseStratagemType(typeStr: string): {
|
|
|
12
12
|
export declare function parsePlayerTurn(turn: string): string;
|
|
13
13
|
/** Map source phase names to schema phase enum values. Filters out invalid phases. */
|
|
14
14
|
export declare function mapPhases(phases: string[]): string[];
|
|
15
|
+
//# sourceMappingURL=id-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id-generator.d.ts","sourceRoot":"","sources":["../../src/converters/id-generator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,wDAAwD;AACxD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAe7C;AAED,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;CACxB,CAsBA;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAWpD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id-generator.js","sourceRoot":"","sources":["../../src/converters/id-generator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,iBAAiB,GAAG,8BAA8B,CAAC;AAEzD,wDAAwD;AACxD,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,EAAE,GAAG,IAAI;SACZ,SAAS,CAAC,KAAK,CAAC;SAChB,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC,mBAAmB;SACnD,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAM,iCAAiC;SACjE,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAK,6BAA6B;SAC7D,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAS,gCAAgC;IAEpE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,iBAAiB,EAAE,gBAAgB,IAAI,oCAAoC,CAC5E,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAIhD,wDAAwD;IACxD,6EAA6E;IAC7E,qEAAqE;IACrE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,iCAAiC,OAAO,GAAG,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAE9C,MAAM,OAAO,GAA2B;QACtC,eAAe,EAAE,eAAe;QAChC,gBAAgB,EAAE,gBAAgB;QAClC,WAAW,EAAE,WAAW;QACxB,OAAO,EAAE,SAAS;KACnB,CAAC;IACF,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,4BAA4B,OAAO,WAAW,OAAO,GAAG,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;AAClC,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IACxC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC9C,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,WAAW,CAAC;IAC/C,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,eAAe,CAAC;IACvD,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,GAAG,CAAC,CAAC;AACzD,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,SAAS,CAAC,MAAgB;IACxC,MAAM,QAAQ,GAA2B;QACvC,OAAO,EAAE,SAAS;QAClB,QAAQ,EAAE,UAAU;QACpB,QAAQ,EAAE,UAAU;QACpB,MAAM,EAAE,QAAQ;QAChB,KAAK,EAAE,OAAO;KACf,CAAC;IACF,OAAO,MAAM;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;SACrC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;AACjD,CAAC","sourcesContent":["/**\n * Generates kebab-case entity IDs from display names.\n */\n\nconst ENTITY_ID_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;\n\n/** Convert a display name to a kebab-case entity ID. */\nexport function nameToId(name: string): string {\n const id = name\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\") // strip diacritics\n .replace(/[''\\u2019]/g, \"\") // strip apostrophes/right-quotes\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\") // non-alphanumeric → hyphens\n .replace(/^-+|-+$/g, \"\"); // trim leading/trailing hyphens\n\n if (!ENTITY_ID_PATTERN.test(id)) {\n throw new Error(\n `Generated ID \"${id}\" from name \"${name}\" does not match entity-id pattern`\n );\n }\n return id;\n}\n\n/** Convert a stratagem type string to the schema enum value and detachment ID. */\nexport function parseStratagemType(typeStr: string): {\n type: string;\n detachmentName: string;\n} {\n // Format: \"Berzerker Warband - Battle Tactic Stratagem\"\n // Require spaces around the separator to avoid splitting on internal hyphens\n // (e.g., \"Spearhead-at-Arms\" has hyphens but the delimiter is \" - \")\n const match = typeStr.match(/^(.+)\\s+-\\s+(.+?)\\s*Stratagem$/i);\n if (!match) {\n throw new Error(`Cannot parse stratagem type: \"${typeStr}\"`);\n }\n const detachmentName = match[1].trim();\n const rawType = match[2].trim().toLowerCase();\n\n const typeMap: Record<string, string> = {\n \"battle tactic\": \"battle-tactic\",\n \"strategic ploy\": \"strategic-ploy\",\n \"epic deed\": \"epic-deed\",\n wargear: \"wargear\",\n };\n const type = typeMap[rawType];\n if (!type) {\n throw new Error(`Unknown stratagem type: \"${rawType}\" from \"${typeStr}\"`);\n }\n return { type, detachmentName };\n}\n\n/** Convert a player turn string to schema enum. */\nexport function parsePlayerTurn(turn: string): string {\n const lower = turn.toLowerCase().trim();\n if (lower.includes(\"either\")) return \"either\";\n if (lower.includes(\"your\")) return \"your-turn\";\n if (lower.includes(\"opponent\")) return \"opponent-turn\";\n throw new Error(`Cannot parse player turn: \"${turn}\"`);\n}\n\n/** Map source phase names to schema phase enum values. Filters out invalid phases. */\nexport function mapPhases(phases: string[]): string[] {\n const phaseMap: Record<string, string> = {\n command: \"command\",\n movement: \"movement\",\n shooting: \"shooting\",\n charge: \"charge\",\n fight: \"fight\",\n };\n return phases\n .map((p) => phaseMap[p.toLowerCase()])\n .filter((p): p is string => p !== undefined);\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyword-filter.d.ts","sourceRoot":"","sources":["../../src/converters/keyword-filter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,UAAU,aAAa;IACrB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAqCD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,aAAa,EAAE,EACzB,WAAW,EAAE,MAAM,GAClB,aAAa,CAsCf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyword-filter.js","sourceRoot":"","sources":["../../src/converters/keyword-filter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Filters keywords for shared units to return only the target faction's view.\n *\n * Source data stores all views' keywords in a flat list, grouped by faction\n * keyword markers (is_faction_keyword === \"true\"). Each group starts with one\n * or more faction keyword entries, followed by regular keywords for that view.\n */\n\nexport interface SourceKeyword {\n datasheet_id: string;\n keyword: string;\n model: string;\n is_faction_keyword: string;\n}\n\ninterface KeywordResult {\n factionKeywords: string[];\n regularKeywords: string[];\n}\n\ninterface KeywordGroup {\n factionKeywords: string[];\n regularKeywords: string[];\n}\n\n/**\n * Split keywords into view groups delimited by faction keyword entries.\n * Each group starts when a faction keyword is encountered after regular keywords\n * (or at the start).\n */\nfunction splitKeywordGroups(keywords: SourceKeyword[]): KeywordGroup[] {\n const groups: KeywordGroup[] = [];\n let current: KeywordGroup | null = null;\n\n for (const kw of keywords) {\n if (kw.is_faction_keyword === \"true\") {\n if (current === null || current.regularKeywords.length > 0) {\n // Start a new group when we see a faction keyword after regular ones\n // (or at the very start)\n if (current !== null) groups.push(current);\n current = { factionKeywords: [], regularKeywords: [] };\n }\n current!.factionKeywords.push(kw.keyword);\n } else {\n if (current === null) {\n current = { factionKeywords: [], regularKeywords: [] };\n }\n current.regularKeywords.push(kw.keyword);\n }\n }\n if (current !== null) groups.push(current);\n\n return groups;\n}\n\n/**\n * Get keywords for a specific faction from a shared unit's keyword list.\n * Finds the group whose faction keywords include `factionName` and returns\n * deduplicated faction + regular keywords for that view.\n *\n * For single-view units (one group), returns all keywords as-is.\n */\nexport function getKeywordsForFaction(\n keywords: SourceKeyword[],\n factionName: string\n): KeywordResult {\n const groups = splitKeywordGroups(keywords);\n\n if (groups.length <= 1) {\n // Single-view unit — return everything\n const group = groups[0] ?? { factionKeywords: [], regularKeywords: [] };\n return {\n factionKeywords: [...new Set(group.factionKeywords)],\n regularKeywords: [...new Set(group.regularKeywords)],\n };\n }\n\n // Multi-view unit — find the group matching the target faction\n for (const group of groups) {\n if (group.factionKeywords.includes(factionName)) {\n return {\n factionKeywords: [...new Set(group.factionKeywords)],\n regularKeywords: [...new Set(group.regularKeywords)],\n };\n }\n }\n\n // Fallback: faction name not found in any group. Return all keywords\n // deduplicated — better than throwing for edge cases.\n const allFaction = new Set<string>();\n const allRegular = new Set<string>();\n for (const group of groups) {\n group.factionKeywords.forEach((k) => allFaction.add(k));\n group.regularKeywords.forEach((k) => allRegular.add(k));\n }\n console.warn(\n `Warning: faction \"${factionName}\" not found in keyword groups for ` +\n `datasheet ${keywords[0]?.datasheet_id}. Returning all keywords.`\n );\n return {\n factionKeywords: [...allFaction],\n regularKeywords: [...allRegular],\n };\n}\n"]}
|
|
@@ -20,3 +20,4 @@ export declare function parseIntStat(s: string): number;
|
|
|
20
20
|
export declare function parseWeaponKeywords(description: string): string[];
|
|
21
21
|
/** Parse AP value. "0" → 0, "-2" → -2 */
|
|
22
22
|
export declare function parseAP(s: string): number;
|
|
23
|
+
//# sourceMappingURL=stat-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stat-parser.d.ts","sourceRoot":"","sources":["../../src/converters/stat-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM3C;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM1D;AAED,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAQzD;AAED,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAMtD;AAED,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAElD;AAED,mDAAmD;AACnD,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpD;AAED,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,sGAAsG;AACtG,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAMjE;AAED,yCAAyC;AACzC,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAMzC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stat-parser.js","sourceRoot":"","sources":["../../src/converters/stat-parser.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Parses army-assist display-format stat strings into typed values\n * for the 40kdc-data schemas.\n */\n\n/** Strip inch marks and parse to integer. \"8\\\"\" → 8, \"14\\\"\" → 14, \"*\" → 0 */\nexport function parseMove(s: string): number {\n const cleaned = s.replace(/[\"″]/g, \"\").trim();\n if (cleaned === \"\" || cleaned === \"-\" || cleaned === \"*\") return 0;\n const n = parseInt(cleaned, 10);\n if (isNaN(n)) throw new Error(`Cannot parse movement: \"${s}\"`);\n return n;\n}\n\n/** Parse target-number stat. \"3+\" → 3, \"6+\" → 6, \"\" → null */\nexport function parseTargetNumber(s: string): number | null {\n const cleaned = s.replace(\"+\", \"\").trim();\n if (cleaned === \"\" || cleaned === \"-\" || cleaned === \"N/A\") return null;\n const n = parseInt(cleaned, 10);\n if (isNaN(n)) throw new Error(`Cannot parse target number: \"${s}\"`);\n return n;\n}\n\n/** Parse a stat-value field. Returns integer for fixed values, string for dice expressions. */\nexport function parseStatValue(s: string): number | string {\n const cleaned = s.trim();\n if (cleaned === \"\" || cleaned === \"-\") return 0;\n // Dice expressions: D6, 2D6, D6+2, D3+1, etc.\n if (/^\\d*[dD]\\d/i.test(cleaned)) return cleaned;\n const n = parseInt(cleaned, 10);\n if (isNaN(n)) throw new Error(`Cannot parse stat value: \"${s}\"`);\n return n;\n}\n\n/** Parse weapon range. \"36\\\"\" → 36, \"Melee\" → \"Melee\", \"\" → \"Melee\", \"N/A\" → \"Melee\" */\nexport function parseRange(s: string): number | \"Melee\" {\n const cleaned = s.replace(/[\"″]/g, \"\").trim();\n if (cleaned === \"\" || cleaned.toLowerCase() === \"melee\" || cleaned === \"N/A\") return \"Melee\";\n const n = parseInt(cleaned, 10);\n if (isNaN(n)) throw new Error(`Cannot parse range: \"${s}\"`);\n return n;\n}\n\n/** Parse BS/WS. \"3+\" → 3, \"N/A\" or \"\" → null */\nexport function parseBSWS(s: string): number | null {\n return parseTargetNumber(s);\n}\n\n/** Parse invulnerable save. \"4+\" → 4, \"\" → null */\nexport function parseInvuln(s: string): number | null {\n return parseTargetNumber(s);\n}\n\n/** Parse toughness, wounds, OC — always integers. */\nexport function parseIntStat(s: string): number {\n const cleaned = s.trim();\n if (cleaned === \"\" || cleaned === \"-\") return 0;\n const n = parseInt(cleaned, 10);\n if (isNaN(n)) throw new Error(`Cannot parse int stat: \"${s}\"`);\n return n;\n}\n\n/** Parse weapon keywords from the description field. \"Pistol, Hazardous\" → [\"Pistol\", \"Hazardous\"] */\nexport function parseWeaponKeywords(description: string): string[] {\n if (!description || description.trim() === \"\") return [];\n return description\n .split(\",\")\n .map((k) => k.trim())\n .filter((k) => k.length > 0);\n}\n\n/** Parse AP value. \"0\" → 0, \"-2\" → -2 */\nexport function parseAP(s: string): number {\n const cleaned = s.trim();\n if (cleaned === \"\" || cleaned === \"-\" || cleaned === \"0\") return 0;\n const n = parseInt(cleaned, 10);\n if (isNaN(n)) throw new Error(`Cannot parse AP: \"${s}\"`);\n return n;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-selector.d.ts","sourceRoot":"","sources":["../../src/converters/view-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,UAAU,SAAS,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,EAAE,CAAC;CACd;AAED,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EACvD,OAAO,EAAE,CAAC,EAAE,GACX,SAAS,CAAC,CAAC,CAAC,EAAE,CAkBhB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,aAAa,EAAE,EAC1B,kBAAkB,EAAE,MAAM,GACzB,MAAM,CA4BR;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EACvD,OAAO,EAAE,CAAC,EAAE,EACZ,SAAS,EAAE,MAAM,GAChB,CAAC,EAAE,CASL;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EAC3D,OAAO,EAAE,CAAC,EAAE,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,CAAC,EAAE,CAuBL"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-selector.js","sourceRoot":"","sources":["../../src/converters/view-selector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Handles multi-view data for shared units.\n *\n * Shared units (e.g., Chaos Land Raider) appear in multiple factions with the\n * same UUID. Their abilities, models, and weapons tables contain repeated entry\n * groups — one per faction \"view\". Views are delimited by the line number\n * resetting to 1.\n *\n * For World Eaters, the correct view is identified by having\n * \"Blessings of Khorne\" as a Faction-type ability.\n */\n\nexport interface SourceAbility {\n datasheet_id: string;\n line: string;\n ability_id: string;\n model: string;\n name: string;\n description: string;\n type: string;\n parameter: string;\n phases: string[];\n}\n\ninterface ViewGroup<T extends { line: string }> {\n index: number;\n entries: T[];\n}\n\n/** Split an array of line-numbered entries into view groups by line-number resets. */\nexport function splitIntoViews<T extends { line: string }>(\n entries: T[]\n): ViewGroup<T>[] {\n const views: ViewGroup<T>[] = [];\n let current: T[] = [];\n let lastLine = Infinity;\n\n for (const entry of entries) {\n const line = parseInt(entry.line, 10);\n if (line <= lastLine && current.length > 0) {\n views.push({ index: views.length, entries: current });\n current = [];\n }\n current.push(entry);\n lastLine = line;\n }\n if (current.length > 0) {\n views.push({ index: views.length, entries: current });\n }\n return views;\n}\n\n/**\n * Find the view index for a faction's shared unit.\n * Identifies the correct view by matching the faction's primary ability name\n * among the Faction-type abilities in each view.\n * Returns 0 for faction-exclusive units (single view).\n */\nexport function findFactionViewIndex(\n abilities: SourceAbility[],\n factionAbilityName: string\n): number {\n if (abilities.length === 0) return 0;\n const views = splitIntoViews(abilities);\n if (views.length === 1) return 0;\n\n for (const view of views) {\n const hasAbility = view.entries.some(\n (a) =>\n a.type === \"Faction\" &&\n a.name === factionAbilityName\n );\n if (hasAbility) return view.index;\n }\n\n // Some shared units (e.g., SM vehicles used by Grey Knights, or units shared\n // with Agents of the Imperium) have views with no Faction-type ability.\n // Use the first such view when no direct match is found.\n const emptyFactionViews = views.filter(\n (v) => !v.entries.some((a) => a.type === \"Faction\")\n );\n if (emptyFactionViews.length > 0) {\n return emptyFactionViews[0].index;\n }\n\n throw new Error(\n `No \"${factionAbilityName}\" faction ability found in ${views.length} views ` +\n `for datasheet ${abilities[0]?.datasheet_id}`\n );\n}\n\n/** Extract entries for a specific view index from a line-numbered array. */\nexport function getViewEntries<T extends { line: string }>(\n entries: T[],\n viewIndex: number\n): T[] {\n if (entries.length === 0) return [];\n const views = splitIntoViews(entries);\n if (viewIndex >= views.length) {\n throw new Error(\n `View index ${viewIndex} out of range (${views.length} views available)`\n );\n }\n return views[viewIndex].entries;\n}\n\n/**\n * Split points entries (no line field) into views for a shared unit.\n *\n * Points entries are ordered by view. For simple cases (1 entry per view),\n * indexing by viewIndex works. For multi-squad-size units, views are\n * delimited by the model count resetting (decreasing from prev entry).\n */\nexport function getPointsForView<T extends { models: string }>(\n entries: T[],\n viewIndex: number,\n numViews: number\n): T[] {\n if (numViews <= 1) return entries;\n\n // Try splitting by model count resets (decrease signals new view)\n const views: T[][] = [[]];\n for (let i = 0; i < entries.length; i++) {\n const cur = parseInt(entries[i].models, 10);\n const prev =\n i > 0 ? parseInt(entries[i - 1].models, 10) : 0;\n if (i > 0 && cur <= prev) {\n views.push([]);\n }\n views[views.length - 1].push(entries[i]);\n }\n\n if (viewIndex < views.length) {\n return views[viewIndex];\n }\n\n // Fallback: evenly divide\n const perView = Math.ceil(entries.length / numViews);\n const start = viewIndex * perView;\n return entries.slice(start, start + perView);\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weapon-dedup.d.ts","sourceRoot":"","sources":["../../src/converters/weapon-dedup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAWH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,KAAK,EAAE;QACL,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACnB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACnB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACnB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACnB,EAAE,EAAE,MAAM,CAAC;QACX,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;KACpB,CAAC;IACF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,YAAY,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACtD;AAyDD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,EAC5C,WAAW,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAClD;IACD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,kEAAkE;IAClE,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CACzC,CAoEA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weapon-dedup.js","sourceRoot":"","sources":["../../src/converters/weapon-dedup.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Weapon profile merging and cross-unit deduplication.\n *\n * Source data splits multi-profile weapons into separate rows using an\n * en-dash delimiter: \"Plasma pistol – Standard\" / \"Plasma pistol – Supercharge\".\n * These become a single weapon entity with a profiles array.\n *\n * Cross-unit dedup: weapons with identical (name, type, all profile stats)\n * across different units are the same entity, referenced by a shared ID.\n */\n\nimport { nameToId } from \"./id-generator.js\";\nimport {\n parseRange,\n parseBSWS,\n parseStatValue,\n parseAP,\n parseWeaponKeywords,\n} from \"./stat-parser.js\";\n\nexport interface SourceWargear {\n datasheet_id: string;\n line: string;\n line_in_wargear: string;\n dice: string;\n name: string;\n description: string;\n range: string;\n type: string;\n A: string;\n BS_WS: string;\n S: string;\n AP: string;\n D: string;\n}\n\nexport interface WeaponProfile {\n name: string;\n range: number | \"Melee\";\n stats: {\n A: number | string;\n BS?: number | null;\n WS?: number | null;\n S: number | string;\n AP: number;\n D: number | string;\n };\n keywords: string[];\n}\n\nexport interface WeaponEntity {\n id: string;\n name: string;\n type: \"ranged\" | \"melee\";\n profiles: WeaponProfile[];\n game_version: { edition: string; dataslate: string };\n}\n\n// EN-DASH (U+2013) and regular hyphen used as profile separators\nconst PROFILE_SEPARATOR = /\\s*[\\u2013\\u2014-]\\s+/;\n\n/** Split \"Plasma pistol – Standard\" into { baseName: \"Plasma pistol\", profileName: \"Standard\" } */\nfunction splitProfileName(fullName: string): {\n baseName: string;\n profileName: string;\n} {\n const parts = fullName.split(PROFILE_SEPARATOR);\n if (parts.length >= 2) {\n return {\n baseName: parts[0].trim(),\n profileName: parts.slice(1).join(\" – \").trim(),\n };\n }\n return { baseName: fullName.trim(), profileName: fullName.trim() };\n}\n\n/** Build a single weapon profile from a source wargear row. */\nfunction buildProfile(row: SourceWargear, profileName: string): WeaponProfile {\n const range = parseRange(row.range);\n const isMelee = row.type === \"Melee\" || range === \"Melee\";\n\n const stats: WeaponProfile[\"stats\"] = {\n A: parseStatValue(row.A),\n S: parseStatValue(row.S),\n AP: parseAP(row.AP),\n D: parseStatValue(row.D),\n };\n\n if (isMelee) {\n stats.WS = parseBSWS(row.BS_WS);\n } else {\n stats.BS = parseBSWS(row.BS_WS);\n }\n\n return {\n name: profileName,\n range,\n stats,\n keywords: parseWeaponKeywords(row.description),\n };\n}\n\n/** Hash a weapon entity for cross-unit deduplication. */\nfunction weaponHash(name: string, type: string, profiles: WeaponProfile[]): string {\n const profileParts = profiles\n .map(\n (p) =>\n `${p.name}|${p.range}|${JSON.stringify(p.stats)}|${p.keywords.sort().join(\",\")}`\n )\n .sort();\n return `${name.toLowerCase()}|${type}|${profileParts.join(\";\")}`;\n}\n\n/**\n * Merge wargear rows for a single unit into weapon entities,\n * then deduplicate across all units.\n */\nexport function buildWeaponRegistry(\n allUnitWargear: Map<string, SourceWargear[]>,\n gameVersion: { edition: string; dataslate: string }\n): {\n weapons: WeaponEntity[];\n /** Maps (datasheetId) → Set of weapon entity IDs for that unit */\n unitWeaponIds: Map<string, Set<string>>;\n} {\n // Global dedup registry: hash → WeaponEntity\n const registry = new Map<string, WeaponEntity>();\n const unitWeaponIds = new Map<string, Set<string>>();\n\n for (const [datasheetId, rows] of allUnitWargear) {\n const weaponIds = new Set<string>();\n unitWeaponIds.set(datasheetId, weaponIds);\n\n // Group rows by base weapon name for profile merging\n const groups = new Map<string, { baseName: string; rows: SourceWargear[]; profileNames: string[] }>();\n\n for (const row of rows) {\n const { baseName, profileName } = splitProfileName(row.name);\n const key = baseName.toLowerCase();\n if (!groups.has(key)) {\n groups.set(key, { baseName, rows: [], profileNames: [] });\n }\n const group = groups.get(key)!;\n group.rows.push(row);\n group.profileNames.push(profileName);\n }\n\n for (const [, group] of groups) {\n const { baseName, rows: groupRows, profileNames } = group;\n const type = groupRows[0].type === \"Melee\" ? \"melee\" : \"ranged\";\n\n // Skip rows with obviously corrupt source data (shifted columns)\n const validRows = groupRows.filter((row, i) => {\n const s = parseStatValue(row.S);\n if (typeof s === \"number\" && s < 0) return false;\n const d = parseStatValue(row.D);\n if (type === \"ranged\" && d === 0 && row.D === \"\") return false;\n return true;\n });\n if (validRows.length === 0) continue;\n const validProfileNames = groupRows.map((row, i) => ({ row, name: profileNames[i] }))\n .filter(({ row }) => validRows.includes(row))\n .map(({ name }) => name);\n\n // If there's only one profile and its name matches the base name, use base name as profile name\n const profiles = validRows.map((row, i) => {\n const pName =\n validRows.length === 1 ? baseName : validProfileNames[i];\n return buildProfile(row, pName);\n });\n\n const hash = weaponHash(baseName, type, profiles);\n\n if (!registry.has(hash)) {\n const id = nameToId(baseName);\n registry.set(hash, {\n id,\n name: baseName,\n type: type as \"ranged\" | \"melee\",\n profiles,\n game_version: gameVersion,\n });\n }\n\n weaponIds.add(registry.get(hash)!.id);\n }\n }\n\n return {\n weapons: [...registry.values()],\n unitWeaponIds,\n };\n}\n"]}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flat `Buff` type every contribution flows through, and the
|
|
3
|
+
* {@link resolveBuffs} resolver that collapses a stack into a
|
|
4
|
+
* {@link ResolvedModifiers} read-out the engine can consume.
|
|
5
|
+
*
|
|
6
|
+
* The same shape carries weapon-keyword effects, ability buffs, stratagem
|
|
7
|
+
* effects, and manual UI toggles — reroll-stacking, hit/wound caps, and
|
|
8
|
+
* feel-no-pain-best-threshold all fall out of one resolver rather than each
|
|
9
|
+
* source kind reinventing precedence.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import type { Phase } from "../generated.js";
|
|
14
|
+
/** Where a buff originated. Drives stable tie-breaking inside `resolveBuffs`. */
|
|
15
|
+
export type BuffSource = {
|
|
16
|
+
kind: "weapon-keyword";
|
|
17
|
+
weaponId: string;
|
|
18
|
+
keywordId: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "ability";
|
|
21
|
+
abilityId: string;
|
|
22
|
+
abilityKind: "army" | "detachment" | "detachment-stratagem" | "unit" | "leader" | "support";
|
|
23
|
+
} | {
|
|
24
|
+
kind: "manual";
|
|
25
|
+
label: string;
|
|
26
|
+
};
|
|
27
|
+
/** A weapon-keyword reference (id + parameter map), as found on weapon profiles. */
|
|
28
|
+
export type WeaponKeywordRef = {
|
|
29
|
+
keyword_id: string;
|
|
30
|
+
parameters?: Record<string, unknown>;
|
|
31
|
+
};
|
|
32
|
+
/** One typed contribution; the engine reads `ResolvedModifiers` for the rest. */
|
|
33
|
+
export type BuffContribution = {
|
|
34
|
+
type: "hit-mod";
|
|
35
|
+
value: number;
|
|
36
|
+
} | {
|
|
37
|
+
type: "wound-mod";
|
|
38
|
+
value: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: "save-mod";
|
|
41
|
+
value: number;
|
|
42
|
+
} | {
|
|
43
|
+
type: "cover";
|
|
44
|
+
} | {
|
|
45
|
+
type: "reroll";
|
|
46
|
+
roll: "hit" | "wound" | "save" | "damage";
|
|
47
|
+
subset: "ones" | "all-failures";
|
|
48
|
+
} | {
|
|
49
|
+
type: "extra-keyword";
|
|
50
|
+
keywordRef: WeaponKeywordRef;
|
|
51
|
+
} | {
|
|
52
|
+
type: "feel-no-pain";
|
|
53
|
+
threshold: number;
|
|
54
|
+
} | {
|
|
55
|
+
type: "damage-mod";
|
|
56
|
+
value: number;
|
|
57
|
+
}
|
|
58
|
+
/** Additive modifier to the attacker's per-model attack count (A stat). */
|
|
59
|
+
| {
|
|
60
|
+
type: "attacks-mod";
|
|
61
|
+
value: number;
|
|
62
|
+
}
|
|
63
|
+
/** Additive modifier to the attacker's Strength stat. */
|
|
64
|
+
| {
|
|
65
|
+
type: "strength-mod";
|
|
66
|
+
value: number;
|
|
67
|
+
}
|
|
68
|
+
/** Additive modifier to the defender's Toughness stat. */
|
|
69
|
+
| {
|
|
70
|
+
type: "toughness-mod";
|
|
71
|
+
value: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Additive modifier to the attacker's weapon AP. AP is signed against the
|
|
75
|
+
* defender's save (negative = more piercing), so a value of `-1` here makes
|
|
76
|
+
* the weapon one AP more piercing.
|
|
77
|
+
*/
|
|
78
|
+
| {
|
|
79
|
+
type: "ap-mod";
|
|
80
|
+
value: number;
|
|
81
|
+
};
|
|
82
|
+
/** Optional gating; the resolver drops buffs whose gate fails. */
|
|
83
|
+
export type BuffApplicability = {
|
|
84
|
+
phases?: Phase[];
|
|
85
|
+
rollType?: "hit" | "wound" | "save" | "damage";
|
|
86
|
+
/** Target must carry this keyword (case-insensitive). */
|
|
87
|
+
requiresTargetKeyword?: string;
|
|
88
|
+
/** Attacker must carry this keyword (case-insensitive). */
|
|
89
|
+
requiresAttackerKeyword?: string;
|
|
90
|
+
};
|
|
91
|
+
/** A single buff: where it came from, when it applies, what it contributes. */
|
|
92
|
+
export type Buff = {
|
|
93
|
+
source: BuffSource;
|
|
94
|
+
applicableWhen?: BuffApplicability;
|
|
95
|
+
contribution: BuffContribution;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Shared engine context. Carries the phase plus a few attacker/target flags
|
|
99
|
+
* the keyword translator and the resolver both need. The engine fills it from
|
|
100
|
+
* its `EngineInput.context` plus the unit-keyword unions; the resolver reads
|
|
101
|
+
* only the subset relevant to its `applicableWhen` checks.
|
|
102
|
+
*/
|
|
103
|
+
export type EngineContext = {
|
|
104
|
+
phase: Phase;
|
|
105
|
+
/** Attacker has not moved this turn — Heavy fires its +1 to hit. */
|
|
106
|
+
attackerStationary?: boolean;
|
|
107
|
+
/** Within half the weapon's range — Melta / Rapid Fire fire. */
|
|
108
|
+
withinHalfRange?: boolean;
|
|
109
|
+
/** Attacker benefits from cover (mostly informational; cover applies to defenders). */
|
|
110
|
+
attackerInCover?: boolean;
|
|
111
|
+
/** Target is in cover — the resolver flips on `cover`, the engine applies +1 to save. */
|
|
112
|
+
targetInCover?: boolean;
|
|
113
|
+
/** Attacker keywords (union of unit.keywords + faction_keywords), lower-cased. */
|
|
114
|
+
attackerKeywords?: ReadonlyArray<string>;
|
|
115
|
+
/** Target keywords (union of unit.keywords + faction_keywords), lower-cased. */
|
|
116
|
+
targetKeywords?: ReadonlyArray<string>;
|
|
117
|
+
/**
|
|
118
|
+
* Sub-phase timing flag (e.g. `"start-of-phase"`, `"end-of-phase"`,
|
|
119
|
+
* `"on-destroyed"`). Consumed by the `timing-is` condition. Left undefined
|
|
120
|
+
* when the caller can't pin a sub-phase down — the condition then evaluates
|
|
121
|
+
* as `"unknown"` and the SPA surfaces a diagnostic.
|
|
122
|
+
*/
|
|
123
|
+
timing?: string;
|
|
124
|
+
};
|
|
125
|
+
/** Back-compat alias — `resolveBuffs` accepts the shared engine context. */
|
|
126
|
+
export type ResolveContext = EngineContext;
|
|
127
|
+
/** Read-out of a resolved buff stack, with provenance per field. */
|
|
128
|
+
export type ResolvedModifiers = {
|
|
129
|
+
hitMod: {
|
|
130
|
+
value: number;
|
|
131
|
+
dominantSource: BuffSource | null;
|
|
132
|
+
};
|
|
133
|
+
woundMod: {
|
|
134
|
+
value: number;
|
|
135
|
+
dominantSource: BuffSource | null;
|
|
136
|
+
};
|
|
137
|
+
saveMod: {
|
|
138
|
+
value: number;
|
|
139
|
+
sources: BuffSource[];
|
|
140
|
+
};
|
|
141
|
+
cover: {
|
|
142
|
+
active: boolean;
|
|
143
|
+
source: BuffSource | null;
|
|
144
|
+
};
|
|
145
|
+
rerolls: Partial<Record<"hit" | "wound" | "save" | "damage", {
|
|
146
|
+
subset: "ones" | "all-failures";
|
|
147
|
+
dominantSource: BuffSource;
|
|
148
|
+
}>>;
|
|
149
|
+
extraKeywords: {
|
|
150
|
+
keywordRef: WeaponKeywordRef;
|
|
151
|
+
source: BuffSource;
|
|
152
|
+
}[];
|
|
153
|
+
feelNoPain: {
|
|
154
|
+
threshold: number;
|
|
155
|
+
dominantSource: BuffSource;
|
|
156
|
+
} | null;
|
|
157
|
+
damageMod: {
|
|
158
|
+
value: number;
|
|
159
|
+
sources: BuffSource[];
|
|
160
|
+
};
|
|
161
|
+
attacksMod: {
|
|
162
|
+
value: number;
|
|
163
|
+
sources: BuffSource[];
|
|
164
|
+
};
|
|
165
|
+
strengthMod: {
|
|
166
|
+
value: number;
|
|
167
|
+
sources: BuffSource[];
|
|
168
|
+
};
|
|
169
|
+
toughnessMod: {
|
|
170
|
+
value: number;
|
|
171
|
+
sources: BuffSource[];
|
|
172
|
+
};
|
|
173
|
+
apMod: {
|
|
174
|
+
value: number;
|
|
175
|
+
sources: BuffSource[];
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Collapse a flat buff stack into a {@link ResolvedModifiers} read-out. Pure
|
|
180
|
+
* function; the engine — and any UI that wants to render the resolved table
|
|
181
|
+
* before crunching — both go through this.
|
|
182
|
+
*/
|
|
183
|
+
export declare function resolveBuffs(buffs: Buff[], ctx: ResolveContext): ResolvedModifiers;
|
|
184
|
+
//# sourceMappingURL=buffs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffs.d.ts","sourceRoot":"","sources":["../../src/cruncher/buffs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,iFAAiF;AACjF,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/D;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EACP,MAAM,GACN,YAAY,GACZ,sBAAsB,GACtB,MAAM,GACN,QAAQ,GACR,SAAS,CAAC;CACf,GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,oFAAoF;AACpF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC1C,MAAM,EAAE,MAAM,GAAG,cAAc,CAAC;CACjC,GACD;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACvC,2EAA2E;GACzE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACxC,yDAAyD;GACvD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACzC,0DAA0D;GACxD;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAC1C;;;;GAIG;GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,kEAAkE;AAClE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC/C,yDAAyD;IACzD,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,IAAI,GAAG;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,iBAAiB,CAAC;IACnC,YAAY,EAAE,gBAAgB,CAAC;CAChC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gEAAgE;IAChE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uFAAuF;IACvF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,yFAAyF;IACzF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,gFAAgF;IAChF,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,cAAc,GAAG,aAAa,CAAC;AAE3C,oEAAoE;AACpE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,UAAU,GAAG,IAAI,CAAA;KAAE,CAAC;IAC7D,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,UAAU,GAAG,IAAI,CAAA;KAAE,CAAC;IAC/D,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IAClD,KAAK,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;KAAE,CAAC;IACtD,OAAO,EAAE,OAAO,CACd,MAAM,CACJ,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,EACnC;QAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAAC;QAAC,cAAc,EAAE,UAAU,CAAA;KAAE,CAChE,CACF,CAAC;IACF,aAAa,EAAE;QAAE,UAAU,EAAE,gBAAgB,CAAC;QAAC,MAAM,EAAE,UAAU,CAAA;KAAE,EAAE,CAAC;IACtE,UAAU,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,UAAU,CAAA;KAAE,GAAG,IAAI,CAAC;IACrE,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IACpD,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IACrD,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IACtD,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IACvD,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;CACjD,CAAC;AAqCF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,cAAc,GAAG,iBAAiB,CA+FlF"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** Stable ordering used to break ties when multiple buffs claim the same field. */
|
|
2
|
+
const SOURCE_KIND_RANK = {
|
|
3
|
+
"ability:army": 0,
|
|
4
|
+
"ability:detachment": 1,
|
|
5
|
+
"ability:detachment-stratagem": 2,
|
|
6
|
+
"ability:unit": 3,
|
|
7
|
+
"ability:leader": 4,
|
|
8
|
+
"ability:support": 5,
|
|
9
|
+
manual: 6,
|
|
10
|
+
"weapon-keyword": 7,
|
|
11
|
+
};
|
|
12
|
+
function rank(s) {
|
|
13
|
+
if (s.kind === "ability")
|
|
14
|
+
return SOURCE_KIND_RANK[`ability:${s.abilityKind}`] ?? 99;
|
|
15
|
+
return SOURCE_KIND_RANK[s.kind] ?? 99;
|
|
16
|
+
}
|
|
17
|
+
function applies(buff, ctx) {
|
|
18
|
+
const w = buff.applicableWhen;
|
|
19
|
+
if (!w)
|
|
20
|
+
return true;
|
|
21
|
+
if (w.phases && w.phases.length > 0 && !w.phases.includes(ctx.phase))
|
|
22
|
+
return false;
|
|
23
|
+
if (w.rollType && buff.contribution.type === "reroll" && buff.contribution.roll !== w.rollType) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (w.requiresTargetKeyword) {
|
|
27
|
+
const target = ctx.targetKeywords ?? [];
|
|
28
|
+
if (!target.includes(w.requiresTargetKeyword.toLowerCase()))
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (w.requiresAttackerKeyword) {
|
|
32
|
+
const attacker = ctx.attackerKeywords ?? [];
|
|
33
|
+
if (!attacker.includes(w.requiresAttackerKeyword.toLowerCase()))
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Collapse a flat buff stack into a {@link ResolvedModifiers} read-out. Pure
|
|
40
|
+
* function; the engine — and any UI that wants to render the resolved table
|
|
41
|
+
* before crunching — both go through this.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveBuffs(buffs, ctx) {
|
|
44
|
+
const live = buffs.filter((b) => applies(b, ctx));
|
|
45
|
+
const out = {
|
|
46
|
+
hitMod: { value: 0, dominantSource: null },
|
|
47
|
+
woundMod: { value: 0, dominantSource: null },
|
|
48
|
+
saveMod: { value: 0, sources: [] },
|
|
49
|
+
cover: { active: false, source: null },
|
|
50
|
+
rerolls: {},
|
|
51
|
+
extraKeywords: [],
|
|
52
|
+
feelNoPain: null,
|
|
53
|
+
damageMod: { value: 0, sources: [] },
|
|
54
|
+
attacksMod: { value: 0, sources: [] },
|
|
55
|
+
strengthMod: { value: 0, sources: [] },
|
|
56
|
+
toughnessMod: { value: 0, sources: [] },
|
|
57
|
+
apMod: { value: 0, sources: [] },
|
|
58
|
+
};
|
|
59
|
+
// Hit / wound mods: sum, then cap at ±1, with dominant source picked from
|
|
60
|
+
// the contributors whose sign matches the surviving value.
|
|
61
|
+
const hitContribs = [];
|
|
62
|
+
const woundContribs = [];
|
|
63
|
+
for (const b of live) {
|
|
64
|
+
const c = b.contribution;
|
|
65
|
+
switch (c.type) {
|
|
66
|
+
case "hit-mod":
|
|
67
|
+
hitContribs.push({ value: c.value, source: b.source });
|
|
68
|
+
break;
|
|
69
|
+
case "wound-mod":
|
|
70
|
+
woundContribs.push({ value: c.value, source: b.source });
|
|
71
|
+
break;
|
|
72
|
+
case "save-mod":
|
|
73
|
+
out.saveMod.value += c.value;
|
|
74
|
+
out.saveMod.sources.push(b.source);
|
|
75
|
+
break;
|
|
76
|
+
case "cover":
|
|
77
|
+
if (!out.cover.active || rank(b.source) < rank(out.cover.source)) {
|
|
78
|
+
out.cover = { active: true, source: b.source };
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "reroll": {
|
|
82
|
+
const cur = out.rerolls[c.roll];
|
|
83
|
+
const incoming = c.subset;
|
|
84
|
+
if (!cur) {
|
|
85
|
+
out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const incomingStronger = (incoming === "all-failures" && cur.subset === "ones") ||
|
|
89
|
+
(incoming === cur.subset && rank(b.source) < rank(cur.dominantSource));
|
|
90
|
+
if (incomingStronger) {
|
|
91
|
+
out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "extra-keyword": {
|
|
97
|
+
const key = `${c.keywordRef.keyword_id}::${JSON.stringify(c.keywordRef.parameters ?? {})}`;
|
|
98
|
+
if (!out.extraKeywords.some((e) => keyOf(e.keywordRef) === key)) {
|
|
99
|
+
out.extraKeywords.push({ keywordRef: c.keywordRef, source: b.source });
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "feel-no-pain":
|
|
104
|
+
if (out.feelNoPain === null || c.threshold < out.feelNoPain.threshold) {
|
|
105
|
+
out.feelNoPain = { threshold: c.threshold, dominantSource: b.source };
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case "damage-mod":
|
|
109
|
+
out.damageMod.value += c.value;
|
|
110
|
+
out.damageMod.sources.push(b.source);
|
|
111
|
+
break;
|
|
112
|
+
case "attacks-mod":
|
|
113
|
+
out.attacksMod.value += c.value;
|
|
114
|
+
out.attacksMod.sources.push(b.source);
|
|
115
|
+
break;
|
|
116
|
+
case "strength-mod":
|
|
117
|
+
out.strengthMod.value += c.value;
|
|
118
|
+
out.strengthMod.sources.push(b.source);
|
|
119
|
+
break;
|
|
120
|
+
case "toughness-mod":
|
|
121
|
+
out.toughnessMod.value += c.value;
|
|
122
|
+
out.toughnessMod.sources.push(b.source);
|
|
123
|
+
break;
|
|
124
|
+
case "ap-mod":
|
|
125
|
+
out.apMod.value += c.value;
|
|
126
|
+
out.apMod.sources.push(b.source);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
out.hitMod = capModifier(hitContribs);
|
|
131
|
+
out.woundMod = capModifier(woundContribs);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function keyOf(ref) {
|
|
135
|
+
return `${ref.keyword_id}::${JSON.stringify(ref.parameters ?? {})}`;
|
|
136
|
+
}
|
|
137
|
+
/** Sum, clamp to ±1, then pick the dominant contributing source by rank. */
|
|
138
|
+
function capModifier(contribs) {
|
|
139
|
+
if (contribs.length === 0)
|
|
140
|
+
return { value: 0, dominantSource: null };
|
|
141
|
+
const sum = contribs.reduce((a, c) => a + c.value, 0);
|
|
142
|
+
const capped = Math.max(-1, Math.min(1, sum));
|
|
143
|
+
if (capped === 0)
|
|
144
|
+
return { value: 0, dominantSource: null };
|
|
145
|
+
const sign = Math.sign(capped);
|
|
146
|
+
const matching = contribs.filter((c) => Math.sign(c.value) === sign);
|
|
147
|
+
matching.sort((a, b) => rank(a.source) - rank(b.source));
|
|
148
|
+
return { value: capped, dominantSource: matching[0]?.source ?? null };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=buffs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buffs.js","sourceRoot":"","sources":["../../src/cruncher/buffs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The flat `Buff` type every contribution flows through, and the\n * {@link resolveBuffs} resolver that collapses a stack into a\n * {@link ResolvedModifiers} read-out the engine can consume.\n *\n * The same shape carries weapon-keyword effects, ability buffs, stratagem\n * effects, and manual UI toggles — reroll-stacking, hit/wound caps, and\n * feel-no-pain-best-threshold all fall out of one resolver rather than each\n * source kind reinventing precedence.\n *\n * @packageDocumentation\n */\nimport type { Phase } from \"../generated.js\";\n\n/** Where a buff originated. Drives stable tie-breaking inside `resolveBuffs`. */\nexport type BuffSource =\n | { kind: \"weapon-keyword\"; weaponId: string; keywordId: string }\n | {\n kind: \"ability\";\n abilityId: string;\n abilityKind:\n | \"army\"\n | \"detachment\"\n | \"detachment-stratagem\"\n | \"unit\"\n | \"leader\"\n | \"support\";\n }\n | { kind: \"manual\"; label: string };\n\n/** A weapon-keyword reference (id + parameter map), as found on weapon profiles. */\nexport type WeaponKeywordRef = {\n keyword_id: string;\n parameters?: Record<string, unknown>;\n};\n\n/** One typed contribution; the engine reads `ResolvedModifiers` for the rest. */\nexport type BuffContribution =\n | { type: \"hit-mod\"; value: number }\n | { type: \"wound-mod\"; value: number }\n | { type: \"save-mod\"; value: number }\n | { type: \"cover\" }\n | {\n type: \"reroll\";\n roll: \"hit\" | \"wound\" | \"save\" | \"damage\";\n subset: \"ones\" | \"all-failures\";\n }\n | { type: \"extra-keyword\"; keywordRef: WeaponKeywordRef }\n | { type: \"feel-no-pain\"; threshold: number }\n | { type: \"damage-mod\"; value: number }\n /** Additive modifier to the attacker's per-model attack count (A stat). */\n | { type: \"attacks-mod\"; value: number }\n /** Additive modifier to the attacker's Strength stat. */\n | { type: \"strength-mod\"; value: number }\n /** Additive modifier to the defender's Toughness stat. */\n | { type: \"toughness-mod\"; value: number }\n /**\n * Additive modifier to the attacker's weapon AP. AP is signed against the\n * defender's save (negative = more piercing), so a value of `-1` here makes\n * the weapon one AP more piercing.\n */\n | { type: \"ap-mod\"; value: number };\n\n/** Optional gating; the resolver drops buffs whose gate fails. */\nexport type BuffApplicability = {\n phases?: Phase[];\n rollType?: \"hit\" | \"wound\" | \"save\" | \"damage\";\n /** Target must carry this keyword (case-insensitive). */\n requiresTargetKeyword?: string;\n /** Attacker must carry this keyword (case-insensitive). */\n requiresAttackerKeyword?: string;\n};\n\n/** A single buff: where it came from, when it applies, what it contributes. */\nexport type Buff = {\n source: BuffSource;\n applicableWhen?: BuffApplicability;\n contribution: BuffContribution;\n};\n\n/**\n * Shared engine context. Carries the phase plus a few attacker/target flags\n * the keyword translator and the resolver both need. The engine fills it from\n * its `EngineInput.context` plus the unit-keyword unions; the resolver reads\n * only the subset relevant to its `applicableWhen` checks.\n */\nexport type EngineContext = {\n phase: Phase;\n /** Attacker has not moved this turn — Heavy fires its +1 to hit. */\n attackerStationary?: boolean;\n /** Within half the weapon's range — Melta / Rapid Fire fire. */\n withinHalfRange?: boolean;\n /** Attacker benefits from cover (mostly informational; cover applies to defenders). */\n attackerInCover?: boolean;\n /** Target is in cover — the resolver flips on `cover`, the engine applies +1 to save. */\n targetInCover?: boolean;\n /** Attacker keywords (union of unit.keywords + faction_keywords), lower-cased. */\n attackerKeywords?: ReadonlyArray<string>;\n /** Target keywords (union of unit.keywords + faction_keywords), lower-cased. */\n targetKeywords?: ReadonlyArray<string>;\n /**\n * Sub-phase timing flag (e.g. `\"start-of-phase\"`, `\"end-of-phase\"`,\n * `\"on-destroyed\"`). Consumed by the `timing-is` condition. Left undefined\n * when the caller can't pin a sub-phase down — the condition then evaluates\n * as `\"unknown\"` and the SPA surfaces a diagnostic.\n */\n timing?: string;\n};\n\n/** Back-compat alias — `resolveBuffs` accepts the shared engine context. */\nexport type ResolveContext = EngineContext;\n\n/** Read-out of a resolved buff stack, with provenance per field. */\nexport type ResolvedModifiers = {\n hitMod: { value: number; dominantSource: BuffSource | null };\n woundMod: { value: number; dominantSource: BuffSource | null };\n saveMod: { value: number; sources: BuffSource[] };\n cover: { active: boolean; source: BuffSource | null };\n rerolls: Partial<\n Record<\n \"hit\" | \"wound\" | \"save\" | \"damage\",\n { subset: \"ones\" | \"all-failures\"; dominantSource: BuffSource }\n >\n >;\n extraKeywords: { keywordRef: WeaponKeywordRef; source: BuffSource }[];\n feelNoPain: { threshold: number; dominantSource: BuffSource } | null;\n damageMod: { value: number; sources: BuffSource[] };\n attacksMod: { value: number; sources: BuffSource[] };\n strengthMod: { value: number; sources: BuffSource[] };\n toughnessMod: { value: number; sources: BuffSource[] };\n apMod: { value: number; sources: BuffSource[] };\n};\n\n/** Stable ordering used to break ties when multiple buffs claim the same field. */\nconst SOURCE_KIND_RANK: Record<string, number> = {\n \"ability:army\": 0,\n \"ability:detachment\": 1,\n \"ability:detachment-stratagem\": 2,\n \"ability:unit\": 3,\n \"ability:leader\": 4,\n \"ability:support\": 5,\n manual: 6,\n \"weapon-keyword\": 7,\n};\n\nfunction rank(s: BuffSource): number {\n if (s.kind === \"ability\") return SOURCE_KIND_RANK[`ability:${s.abilityKind}`] ?? 99;\n return SOURCE_KIND_RANK[s.kind] ?? 99;\n}\n\nfunction applies(buff: Buff, ctx: ResolveContext): boolean {\n const w = buff.applicableWhen;\n if (!w) return true;\n if (w.phases && w.phases.length > 0 && !w.phases.includes(ctx.phase)) return false;\n if (w.rollType && buff.contribution.type === \"reroll\" && buff.contribution.roll !== w.rollType) {\n return false;\n }\n if (w.requiresTargetKeyword) {\n const target = ctx.targetKeywords ?? [];\n if (!target.includes(w.requiresTargetKeyword.toLowerCase())) return false;\n }\n if (w.requiresAttackerKeyword) {\n const attacker = ctx.attackerKeywords ?? [];\n if (!attacker.includes(w.requiresAttackerKeyword.toLowerCase())) return false;\n }\n return true;\n}\n\n/**\n * Collapse a flat buff stack into a {@link ResolvedModifiers} read-out. Pure\n * function; the engine — and any UI that wants to render the resolved table\n * before crunching — both go through this.\n */\nexport function resolveBuffs(buffs: Buff[], ctx: ResolveContext): ResolvedModifiers {\n const live = buffs.filter((b) => applies(b, ctx));\n\n const out: ResolvedModifiers = {\n hitMod: { value: 0, dominantSource: null },\n woundMod: { value: 0, dominantSource: null },\n saveMod: { value: 0, sources: [] },\n cover: { active: false, source: null },\n rerolls: {},\n extraKeywords: [],\n feelNoPain: null,\n damageMod: { value: 0, sources: [] },\n attacksMod: { value: 0, sources: [] },\n strengthMod: { value: 0, sources: [] },\n toughnessMod: { value: 0, sources: [] },\n apMod: { value: 0, sources: [] },\n };\n\n // Hit / wound mods: sum, then cap at ±1, with dominant source picked from\n // the contributors whose sign matches the surviving value.\n const hitContribs: { value: number; source: BuffSource }[] = [];\n const woundContribs: { value: number; source: BuffSource }[] = [];\n\n for (const b of live) {\n const c = b.contribution;\n switch (c.type) {\n case \"hit-mod\":\n hitContribs.push({ value: c.value, source: b.source });\n break;\n case \"wound-mod\":\n woundContribs.push({ value: c.value, source: b.source });\n break;\n case \"save-mod\":\n out.saveMod.value += c.value;\n out.saveMod.sources.push(b.source);\n break;\n case \"cover\":\n if (!out.cover.active || rank(b.source) < rank(out.cover.source!)) {\n out.cover = { active: true, source: b.source };\n }\n break;\n case \"reroll\": {\n const cur = out.rerolls[c.roll];\n const incoming = c.subset;\n if (!cur) {\n out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };\n } else {\n const incomingStronger =\n (incoming === \"all-failures\" && cur.subset === \"ones\") ||\n (incoming === cur.subset && rank(b.source) < rank(cur.dominantSource));\n if (incomingStronger) {\n out.rerolls[c.roll] = { subset: incoming, dominantSource: b.source };\n }\n }\n break;\n }\n case \"extra-keyword\": {\n const key = `${c.keywordRef.keyword_id}::${JSON.stringify(c.keywordRef.parameters ?? {})}`;\n if (!out.extraKeywords.some((e) => keyOf(e.keywordRef) === key)) {\n out.extraKeywords.push({ keywordRef: c.keywordRef, source: b.source });\n }\n break;\n }\n case \"feel-no-pain\":\n if (out.feelNoPain === null || c.threshold < out.feelNoPain.threshold) {\n out.feelNoPain = { threshold: c.threshold, dominantSource: b.source };\n }\n break;\n case \"damage-mod\":\n out.damageMod.value += c.value;\n out.damageMod.sources.push(b.source);\n break;\n case \"attacks-mod\":\n out.attacksMod.value += c.value;\n out.attacksMod.sources.push(b.source);\n break;\n case \"strength-mod\":\n out.strengthMod.value += c.value;\n out.strengthMod.sources.push(b.source);\n break;\n case \"toughness-mod\":\n out.toughnessMod.value += c.value;\n out.toughnessMod.sources.push(b.source);\n break;\n case \"ap-mod\":\n out.apMod.value += c.value;\n out.apMod.sources.push(b.source);\n break;\n }\n }\n\n out.hitMod = capModifier(hitContribs);\n out.woundMod = capModifier(woundContribs);\n\n return out;\n}\n\nfunction keyOf(ref: WeaponKeywordRef): string {\n return `${ref.keyword_id}::${JSON.stringify(ref.parameters ?? {})}`;\n}\n\n/** Sum, clamp to ±1, then pick the dominant contributing source by rank. */\nfunction capModifier(\n contribs: { value: number; source: BuffSource }[],\n): { value: number; dominantSource: BuffSource | null } {\n if (contribs.length === 0) return { value: 0, dominantSource: null };\n const sum = contribs.reduce((a, c) => a + c.value, 0);\n const capped = Math.max(-1, Math.min(1, sum));\n if (capped === 0) return { value: 0, dominantSource: null };\n const sign = Math.sign(capped);\n const matching = contribs.filter((c) => Math.sign(c.value) === sign);\n matching.sort((a, b) => rank(a.source) - rank(b.source));\n return { value: capped, dominantSource: matching[0]?.source ?? null };\n}\n"]}
|