@alpaca-software/40kdc-data 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/abilities-resolver/index.d.ts +9 -0
- package/dist/abilities-resolver/index.d.ts.map +1 -0
- package/dist/abilities-resolver/index.js +9 -0
- package/dist/abilities-resolver/index.js.map +1 -0
- package/dist/abilities-resolver/resolver.d.ts +73 -0
- package/dist/abilities-resolver/resolver.d.ts.map +1 -0
- package/dist/abilities-resolver/resolver.js +142 -0
- package/dist/abilities-resolver/resolver.js.map +1 -0
- package/dist/audit-coverage.d.ts +78 -0
- package/dist/audit-coverage.d.ts.map +1 -0
- package/dist/audit-coverage.js +341 -0
- package/dist/audit-coverage.js.map +1 -0
- package/dist/author-batch.d.ts +147 -0
- package/dist/author-batch.d.ts.map +1 -0
- package/dist/author-batch.js +675 -0
- package/dist/author-batch.js.map +1 -0
- package/dist/author-input.d.ts +37 -0
- package/dist/author-input.d.ts.map +1 -0
- package/dist/author-input.js +162 -0
- package/dist/author-input.js.map +1 -0
- package/dist/bundle-schemas.d.ts +1 -0
- package/dist/bundle-schemas.d.ts.map +1 -0
- package/dist/bundle-schemas.js +1 -0
- package/dist/bundle-schemas.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen-data.d.ts +1 -0
- package/dist/codegen-data.d.ts.map +1 -0
- package/dist/codegen-data.js +2 -0
- package/dist/codegen-data.js.map +1 -0
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +1 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/translate.d.ts +1 -0
- package/dist/commands/translate.d.ts.map +1 -0
- package/dist/commands/translate.js +10 -4
- package/dist/commands/translate.js.map +1 -0
- package/dist/commands/validate-all.d.ts +1 -0
- package/dist/commands/validate-all.d.ts.map +1 -0
- package/dist/commands/validate-all.js +1 -0
- package/dist/commands/validate-all.js.map +1 -0
- package/dist/commands/validate-core.d.ts +1 -0
- package/dist/commands/validate-core.d.ts.map +1 -0
- package/dist/commands/validate-core.js +1 -0
- package/dist/commands/validate-core.js.map +1 -0
- package/dist/commands/validate-enrichment.d.ts +1 -0
- package/dist/commands/validate-enrichment.d.ts.map +1 -0
- package/dist/commands/validate-enrichment.js +1 -0
- package/dist/commands/validate-enrichment.js.map +1 -0
- package/dist/convert-faction.d.ts +1 -0
- package/dist/convert-faction.d.ts.map +1 -0
- package/dist/convert-faction.js +1 -0
- package/dist/convert-faction.js.map +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts +1 -0
- package/dist/converters/configs/adepta-sororitas.d.ts.map +1 -0
- package/dist/converters/configs/adepta-sororitas.js +1 -0
- package/dist/converters/configs/adepta-sororitas.js.map +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts +1 -0
- package/dist/converters/configs/adeptus-astartes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-astartes.js +1 -0
- package/dist/converters/configs/adeptus-astartes.js.map +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts +1 -0
- package/dist/converters/configs/adeptus-custodes.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-custodes.js +1 -0
- package/dist/converters/configs/adeptus-custodes.js.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts +1 -0
- package/dist/converters/configs/adeptus-mechanicus.d.ts.map +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js +1 -0
- package/dist/converters/configs/adeptus-mechanicus.js.map +1 -0
- package/dist/converters/configs/aeldari.d.ts +1 -0
- package/dist/converters/configs/aeldari.d.ts.map +1 -0
- package/dist/converters/configs/aeldari.js +1 -0
- package/dist/converters/configs/aeldari.js.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts +1 -0
- package/dist/converters/configs/agents-of-the-imperium.d.ts.map +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js +1 -0
- package/dist/converters/configs/agents-of-the-imperium.js.map +1 -0
- package/dist/converters/configs/astra-militarum.d.ts +1 -0
- package/dist/converters/configs/astra-militarum.d.ts.map +1 -0
- package/dist/converters/configs/astra-militarum.js +1 -0
- package/dist/converters/configs/astra-militarum.js.map +1 -0
- package/dist/converters/configs/black-templars.d.ts +1 -0
- package/dist/converters/configs/black-templars.d.ts.map +1 -0
- package/dist/converters/configs/black-templars.js +1 -0
- package/dist/converters/configs/black-templars.js.map +1 -0
- package/dist/converters/configs/blood-angels.d.ts +1 -0
- package/dist/converters/configs/blood-angels.d.ts.map +1 -0
- package/dist/converters/configs/blood-angels.js +1 -0
- package/dist/converters/configs/blood-angels.js.map +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts +1 -0
- package/dist/converters/configs/chaos-daemons.d.ts.map +1 -0
- package/dist/converters/configs/chaos-daemons.js +1 -0
- package/dist/converters/configs/chaos-daemons.js.map +1 -0
- package/dist/converters/configs/chaos-knights.d.ts +1 -0
- package/dist/converters/configs/chaos-knights.d.ts.map +1 -0
- package/dist/converters/configs/chaos-knights.js +1 -0
- package/dist/converters/configs/chaos-knights.js.map +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts +1 -0
- package/dist/converters/configs/chaos-space-marines.d.ts.map +1 -0
- package/dist/converters/configs/chaos-space-marines.js +1 -0
- package/dist/converters/configs/chaos-space-marines.js.map +1 -0
- package/dist/converters/configs/crimson-fists.d.ts +1 -0
- package/dist/converters/configs/crimson-fists.d.ts.map +1 -0
- package/dist/converters/configs/crimson-fists.js +1 -0
- package/dist/converters/configs/crimson-fists.js.map +1 -0
- package/dist/converters/configs/dark-angels.d.ts +1 -0
- package/dist/converters/configs/dark-angels.d.ts.map +1 -0
- package/dist/converters/configs/dark-angels.js +1 -0
- package/dist/converters/configs/dark-angels.js.map +1 -0
- package/dist/converters/configs/death-guard.d.ts +1 -0
- package/dist/converters/configs/death-guard.d.ts.map +1 -0
- package/dist/converters/configs/death-guard.js +1 -0
- package/dist/converters/configs/death-guard.js.map +1 -0
- package/dist/converters/configs/deathwatch.d.ts +1 -0
- package/dist/converters/configs/deathwatch.d.ts.map +1 -0
- package/dist/converters/configs/deathwatch.js +1 -0
- package/dist/converters/configs/deathwatch.js.map +1 -0
- package/dist/converters/configs/drukhari.d.ts +1 -0
- package/dist/converters/configs/drukhari.d.ts.map +1 -0
- package/dist/converters/configs/drukhari.js +1 -0
- package/dist/converters/configs/drukhari.js.map +1 -0
- package/dist/converters/configs/emperors-children.d.ts +1 -0
- package/dist/converters/configs/emperors-children.d.ts.map +1 -0
- package/dist/converters/configs/emperors-children.js +1 -0
- package/dist/converters/configs/emperors-children.js.map +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts +1 -0
- package/dist/converters/configs/genestealer-cults.d.ts.map +1 -0
- package/dist/converters/configs/genestealer-cults.js +1 -0
- package/dist/converters/configs/genestealer-cults.js.map +1 -0
- package/dist/converters/configs/grey-knights.d.ts +1 -0
- package/dist/converters/configs/grey-knights.d.ts.map +1 -0
- package/dist/converters/configs/grey-knights.js +1 -0
- package/dist/converters/configs/grey-knights.js.map +1 -0
- package/dist/converters/configs/imperial-fists.d.ts +1 -0
- package/dist/converters/configs/imperial-fists.d.ts.map +1 -0
- package/dist/converters/configs/imperial-fists.js +1 -0
- package/dist/converters/configs/imperial-fists.js.map +1 -0
- package/dist/converters/configs/imperial-knights.d.ts +1 -0
- package/dist/converters/configs/imperial-knights.d.ts.map +1 -0
- package/dist/converters/configs/imperial-knights.js +1 -0
- package/dist/converters/configs/imperial-knights.js.map +1 -0
- package/dist/converters/configs/iron-hands.d.ts +1 -0
- package/dist/converters/configs/iron-hands.d.ts.map +1 -0
- package/dist/converters/configs/iron-hands.js +1 -0
- package/dist/converters/configs/iron-hands.js.map +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts +1 -0
- package/dist/converters/configs/leagues-of-votann.d.ts.map +1 -0
- package/dist/converters/configs/leagues-of-votann.js +1 -0
- package/dist/converters/configs/leagues-of-votann.js.map +1 -0
- package/dist/converters/configs/necrons.d.ts +1 -0
- package/dist/converters/configs/necrons.d.ts.map +1 -0
- package/dist/converters/configs/necrons.js +1 -0
- package/dist/converters/configs/necrons.js.map +1 -0
- package/dist/converters/configs/orks.d.ts +1 -0
- package/dist/converters/configs/orks.d.ts.map +1 -0
- package/dist/converters/configs/orks.js +1 -0
- package/dist/converters/configs/orks.js.map +1 -0
- package/dist/converters/configs/raven-guard.d.ts +1 -0
- package/dist/converters/configs/raven-guard.d.ts.map +1 -0
- package/dist/converters/configs/raven-guard.js +1 -0
- package/dist/converters/configs/raven-guard.js.map +1 -0
- package/dist/converters/configs/salamanders.d.ts +1 -0
- package/dist/converters/configs/salamanders.d.ts.map +1 -0
- package/dist/converters/configs/salamanders.js +1 -0
- package/dist/converters/configs/salamanders.js.map +1 -0
- package/dist/converters/configs/space-wolves.d.ts +1 -0
- package/dist/converters/configs/space-wolves.d.ts.map +1 -0
- package/dist/converters/configs/space-wolves.js +1 -0
- package/dist/converters/configs/space-wolves.js.map +1 -0
- package/dist/converters/configs/tau-empire.d.ts +1 -0
- package/dist/converters/configs/tau-empire.d.ts.map +1 -0
- package/dist/converters/configs/tau-empire.js +1 -0
- package/dist/converters/configs/tau-empire.js.map +1 -0
- package/dist/converters/configs/thousand-sons.d.ts +1 -0
- package/dist/converters/configs/thousand-sons.d.ts.map +1 -0
- package/dist/converters/configs/thousand-sons.js +1 -0
- package/dist/converters/configs/thousand-sons.js.map +1 -0
- package/dist/converters/configs/tyranids.d.ts +1 -0
- package/dist/converters/configs/tyranids.d.ts.map +1 -0
- package/dist/converters/configs/tyranids.js +1 -0
- package/dist/converters/configs/tyranids.js.map +1 -0
- package/dist/converters/configs/ultramarines.d.ts +1 -0
- package/dist/converters/configs/ultramarines.d.ts.map +1 -0
- package/dist/converters/configs/ultramarines.js +1 -0
- package/dist/converters/configs/ultramarines.js.map +1 -0
- package/dist/converters/configs/white-scars.d.ts +1 -0
- package/dist/converters/configs/white-scars.d.ts.map +1 -0
- package/dist/converters/configs/white-scars.js +1 -0
- package/dist/converters/configs/white-scars.js.map +1 -0
- package/dist/converters/configs/world-eaters.d.ts +1 -0
- package/dist/converters/configs/world-eaters.d.ts.map +1 -0
- package/dist/converters/configs/world-eaters.js +1 -0
- package/dist/converters/configs/world-eaters.js.map +1 -0
- package/dist/converters/faction-config.d.ts +1 -0
- package/dist/converters/faction-config.d.ts.map +1 -0
- package/dist/converters/faction-config.js +1 -0
- package/dist/converters/faction-config.js.map +1 -0
- package/dist/converters/id-generator.d.ts +1 -0
- package/dist/converters/id-generator.d.ts.map +1 -0
- package/dist/converters/id-generator.js +1 -0
- package/dist/converters/id-generator.js.map +1 -0
- package/dist/converters/keyword-filter.d.ts +1 -0
- package/dist/converters/keyword-filter.d.ts.map +1 -0
- package/dist/converters/keyword-filter.js +1 -0
- package/dist/converters/keyword-filter.js.map +1 -0
- package/dist/converters/stat-parser.d.ts +1 -0
- package/dist/converters/stat-parser.d.ts.map +1 -0
- package/dist/converters/stat-parser.js +1 -0
- package/dist/converters/stat-parser.js.map +1 -0
- package/dist/converters/view-selector.d.ts +1 -0
- package/dist/converters/view-selector.d.ts.map +1 -0
- package/dist/converters/view-selector.js +1 -0
- package/dist/converters/view-selector.js.map +1 -0
- package/dist/converters/weapon-dedup.d.ts +1 -0
- package/dist/converters/weapon-dedup.d.ts.map +1 -0
- package/dist/converters/weapon-dedup.js +1 -0
- package/dist/converters/weapon-dedup.js.map +1 -0
- package/dist/cruncher/attribution.d.ts +66 -0
- package/dist/cruncher/attribution.d.ts.map +1 -0
- package/dist/cruncher/attribution.js +88 -0
- package/dist/cruncher/attribution.js.map +1 -0
- package/dist/cruncher/buffs.d.ts +206 -0
- package/dist/cruncher/buffs.d.ts.map +1 -0
- package/dist/cruncher/buffs.js +150 -0
- package/dist/cruncher/buffs.js.map +1 -0
- package/dist/cruncher/engine.d.ts +50 -0
- package/dist/cruncher/engine.d.ts.map +1 -0
- package/dist/cruncher/engine.js +312 -0
- package/dist/cruncher/engine.js.map +1 -0
- package/dist/cruncher/from-dsl.d.ts +101 -0
- package/dist/cruncher/from-dsl.d.ts.map +1 -0
- package/dist/cruncher/from-dsl.js +968 -0
- package/dist/cruncher/from-dsl.js.map +1 -0
- package/dist/cruncher/from-keyword.d.ts +35 -0
- package/dist/cruncher/from-keyword.d.ts.map +1 -0
- package/dist/cruncher/from-keyword.js +159 -0
- package/dist/cruncher/from-keyword.js.map +1 -0
- package/dist/cruncher/get-buffs.d.ts +12 -0
- package/dist/cruncher/get-buffs.d.ts.map +1 -0
- package/dist/cruncher/get-buffs.js +7 -0
- package/dist/cruncher/get-buffs.js.map +1 -0
- package/dist/cruncher/index.d.ts +12 -0
- package/dist/cruncher/index.d.ts.map +1 -0
- package/dist/cruncher/index.js +12 -0
- package/dist/cruncher/index.js.map +1 -0
- package/dist/data/bundle.generated.d.ts +1 -0
- package/dist/data/bundle.generated.d.ts.map +1 -0
- package/dist/data/bundle.generated.js +2 -1
- package/dist/data/bundle.generated.js.map +1 -0
- package/dist/data/collection.d.ts +10 -0
- package/dist/data/collection.d.ts.map +1 -0
- package/dist/data/collection.js +15 -0
- package/dist/data/collection.js.map +1 -0
- package/dist/data/dataset.d.ts +132 -2
- package/dist/data/dataset.d.ts.map +1 -0
- package/dist/data/dataset.js +248 -1
- package/dist/data/dataset.js.map +1 -0
- package/dist/data/entities.d.ts +67 -2
- package/dist/data/entities.d.ts.map +1 -0
- package/dist/data/entities.js +122 -0
- package/dist/data/entities.js.map +1 -0
- package/dist/data/index.d.ts +10 -1
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +14 -1
- package/dist/data/index.js.map +1 -0
- package/dist/data/normalize.d.ts +1 -0
- package/dist/data/normalize.d.ts.map +1 -0
- package/dist/data/normalize.js +1 -0
- package/dist/data/normalize.js.map +1 -0
- package/dist/data/roster-resolve.d.ts +58 -0
- package/dist/data/roster-resolve.d.ts.map +1 -0
- package/dist/data/roster-resolve.js +82 -0
- package/dist/data/roster-resolve.js.map +1 -0
- package/dist/data/types.d.ts +4 -1
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +2 -0
- package/dist/data/types.js.map +1 -0
- package/dist/export/helpers.d.ts +33 -0
- package/dist/export/helpers.d.ts.map +1 -0
- package/dist/export/helpers.js +57 -0
- package/dist/export/helpers.js.map +1 -0
- package/dist/export/index.d.ts +22 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +28 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/newrecruit-json.d.ts +3 -0
- package/dist/export/newrecruit-json.d.ts.map +1 -0
- package/dist/export/newrecruit-json.js +140 -0
- package/dist/export/newrecruit-json.js.map +1 -0
- package/dist/export/newrecruit-simple.d.ts +3 -0
- package/dist/export/newrecruit-simple.d.ts.map +1 -0
- package/dist/export/newrecruit-simple.js +76 -0
- package/dist/export/newrecruit-simple.js.map +1 -0
- package/dist/export/newrecruit-wtc.d.ts +4 -0
- package/dist/export/newrecruit-wtc.d.ts.map +1 -0
- package/dist/export/newrecruit-wtc.js +142 -0
- package/dist/export/newrecruit-wtc.js.map +1 -0
- package/dist/export/roster-json.d.ts +3 -0
- package/dist/export/roster-json.d.ts.map +1 -0
- package/dist/export/roster-json.js +8 -0
- package/dist/export/roster-json.js.map +1 -0
- package/dist/export/rosterizer.d.ts +3 -0
- package/dist/export/rosterizer.d.ts.map +1 -0
- package/dist/export/rosterizer.js +144 -0
- package/dist/export/rosterizer.js.map +1 -0
- package/dist/export/serializer.d.ts +27 -0
- package/dist/export/serializer.d.ts.map +1 -0
- package/dist/export/serializer.js +2 -0
- package/dist/export/serializer.js.map +1 -0
- package/dist/gen-conformance.d.ts +1 -0
- package/dist/gen-conformance.d.ts.map +1 -0
- package/dist/gen-conformance.js +274 -12
- package/dist/gen-conformance.js.map +1 -0
- package/dist/generated.d.ts +194 -118
- package/dist/generated.d.ts.map +1 -0
- package/dist/generated.js +1 -0
- package/dist/generated.js.map +1 -0
- package/dist/import/adapter.d.ts +4 -3
- package/dist/import/adapter.d.ts.map +1 -0
- package/dist/import/adapter.js +1 -0
- package/dist/import/adapter.js.map +1 -0
- package/dist/import/decode.d.ts +1 -0
- package/dist/import/decode.d.ts.map +1 -0
- package/dist/import/decode.js +1 -0
- package/dist/import/decode.js.map +1 -0
- package/dist/import/gw.d.ts +69 -0
- package/dist/import/gw.d.ts.map +1 -0
- package/dist/import/gw.js +245 -0
- package/dist/import/gw.js.map +1 -0
- package/dist/import/import-roster.d.ts +84 -0
- package/dist/import/import-roster.d.ts.map +1 -0
- package/dist/import/import-roster.js +207 -0
- package/dist/import/import-roster.js.map +1 -0
- package/dist/import/index.d.ts +7 -3
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +5 -1
- package/dist/import/index.js.map +1 -0
- package/dist/import/listforge.d.ts +1 -0
- package/dist/import/listforge.d.ts.map +1 -0
- package/dist/import/listforge.js +22 -2
- package/dist/import/listforge.js.map +1 -0
- package/dist/import/newrecruit-json.d.ts +31 -0
- package/dist/import/newrecruit-json.d.ts.map +1 -0
- package/dist/import/newrecruit-json.js +224 -0
- package/dist/import/newrecruit-json.js.map +1 -0
- package/dist/import/newrecruit-simple.d.ts +29 -0
- package/dist/import/newrecruit-simple.d.ts.map +1 -0
- package/dist/import/newrecruit-simple.js +200 -0
- package/dist/import/newrecruit-simple.js.map +1 -0
- package/dist/import/newrecruit-text.d.ts +51 -0
- package/dist/import/newrecruit-text.d.ts.map +1 -0
- package/dist/import/newrecruit-text.js +102 -0
- package/dist/import/newrecruit-text.js.map +1 -0
- package/dist/import/newrecruit-wtc.d.ts +36 -0
- package/dist/import/newrecruit-wtc.d.ts.map +1 -0
- package/dist/import/newrecruit-wtc.js +337 -0
- package/dist/import/newrecruit-wtc.js.map +1 -0
- package/dist/import/resolve.d.ts +3 -2
- package/dist/import/resolve.d.ts.map +1 -0
- package/dist/import/resolve.js +5 -2
- package/dist/import/resolve.js.map +1 -0
- package/dist/import/rosterizer.d.ts +70 -0
- package/dist/import/rosterizer.d.ts.map +1 -0
- package/dist/import/rosterizer.js +348 -0
- package/dist/import/rosterizer.js.map +1 -0
- package/dist/import/types.d.ts +11 -1
- package/dist/import/types.d.ts.map +1 -0
- package/dist/import/types.js +1 -0
- package/dist/import/types.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -0
- package/dist/known-support-10e.d.ts +1 -0
- package/dist/known-support-10e.d.ts.map +1 -0
- package/dist/known-support-10e.js +1 -0
- package/dist/known-support-10e.js.map +1 -0
- package/dist/link-abilities.d.ts +41 -0
- package/dist/link-abilities.d.ts.map +1 -0
- package/dist/link-abilities.js +159 -0
- package/dist/link-abilities.js.map +1 -0
- package/dist/migrations/2026-weapon-keywords.d.ts +2 -0
- package/dist/migrations/2026-weapon-keywords.d.ts.map +1 -0
- package/dist/migrations/2026-weapon-keywords.js +247 -0
- package/dist/migrations/2026-weapon-keywords.js.map +1 -0
- package/dist/port-10e-faction.d.ts +1 -0
- package/dist/port-10e-faction.d.ts.map +1 -0
- package/dist/port-10e-faction.js +1 -0
- package/dist/port-10e-faction.js.map +1 -0
- package/dist/report.d.ts +1 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +1 -0
- package/dist/report.js.map +1 -0
- package/dist/rube-goldberg.d.ts +3 -0
- package/dist/rube-goldberg.d.ts.map +1 -0
- package/dist/rube-goldberg.js +109 -0
- package/dist/rube-goldberg.js.map +1 -0
- package/dist/runner.d.ts +38 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +492 -0
- package/dist/runner.js.map +1 -0
- package/dist/schema-loader.d.ts +1 -0
- package/dist/schema-loader.d.ts.map +1 -0
- package/dist/schema-loader.js +1 -0
- package/dist/schema-loader.js.map +1 -0
- package/dist/scrub-ip.d.ts +14 -0
- package/dist/scrub-ip.d.ts.map +1 -0
- package/dist/scrub-ip.js +88 -0
- package/dist/scrub-ip.js.map +1 -0
- package/dist/validate.d.ts +1 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +2 -0
- package/dist/validate.js.map +1 -0
- package/package.json +15 -3
- package/schemas/core/roster.schema.json +19 -4
- package/schemas/core/weapon-keyword.schema.json +31 -0
- package/schemas/core/weapon.schema.json +22 -1
- package/schemas/enrichment/ability-dsl/effect.schema.json +23 -1
- package/dist/import/import-listforge.d.ts +0 -23
- package/dist/import/import-listforge.js +0 -32
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { resolveBuffs, } from "./buffs.js";
|
|
2
|
+
import { Dataset } from "../data/dataset.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compute the expected per-stage projection for one (attacker, target, buffs)
|
|
5
|
+
* triple. The dataset defaults to the embedded one — pass an alternate when
|
|
6
|
+
* crunching against a different bundle (e.g. tests).
|
|
7
|
+
*/
|
|
8
|
+
export function crunch(input, dataset) {
|
|
9
|
+
const ds = dataset ?? lazyEmbeddedDataset();
|
|
10
|
+
const weaponProfile = input.attacker.weapon.profiles[input.attacker.profileIndex];
|
|
11
|
+
if (!weaponProfile) {
|
|
12
|
+
throw new RangeError(`crunch: attacker.profileIndex=${input.attacker.profileIndex} is out of range for weapon ${input.attacker.weapon.id}`);
|
|
13
|
+
}
|
|
14
|
+
const unitProfile = input.target.unit.profiles[input.target.profileIndex];
|
|
15
|
+
if (!unitProfile) {
|
|
16
|
+
throw new RangeError(`crunch: target.profileIndex=${input.target.profileIndex} is out of range for unit ${input.target.unit.id}`);
|
|
17
|
+
}
|
|
18
|
+
const targetKeywords = unitKeywordsLower(input.target.unit);
|
|
19
|
+
const ctx = {
|
|
20
|
+
...input.context,
|
|
21
|
+
targetKeywords: input.context.targetKeywords ?? targetKeywords,
|
|
22
|
+
};
|
|
23
|
+
// Auto-inject weapon-keyword buffs from the attacker profile, then append
|
|
24
|
+
// the caller-supplied stack. resolveBuffs deduplicates and ranks them.
|
|
25
|
+
const profileBuffs = profileBuffsFor(input.attacker, ds, ctx);
|
|
26
|
+
const resolved = resolveBuffs([...profileBuffs, ...input.buffs], ctx);
|
|
27
|
+
const stages = [];
|
|
28
|
+
// 1. Attacks
|
|
29
|
+
const isMelee = input.attacker.weapon.type === "melee";
|
|
30
|
+
const baseA = evalStatValue(weaponProfile.stats.A);
|
|
31
|
+
const attacksPerModel = baseA + resolved.attacksMod.value;
|
|
32
|
+
const rapidFire = findKeyword(resolved, "rapid-fire");
|
|
33
|
+
const halfRange = ctx.withinHalfRange === true;
|
|
34
|
+
const rapidFireExtraPerModel = rapidFire && halfRange ? evalStatValue(rapidFire.parameters?.value) : 0;
|
|
35
|
+
const blast = findKeyword(resolved, "blast");
|
|
36
|
+
const targetModelCount = input.target.modelCount ?? input.target.unit.model_count?.min ?? 1;
|
|
37
|
+
const blastExtraPerModel = blast ? Math.floor(targetModelCount / 5) : 0;
|
|
38
|
+
const attacks = input.modelsFiring * (attacksPerModel + rapidFireExtraPerModel + blastExtraPerModel);
|
|
39
|
+
stages.push({
|
|
40
|
+
name: "attacks",
|
|
41
|
+
expected: attacks,
|
|
42
|
+
detail: attacksDetail(input.modelsFiring, attacksPerModel, rapidFireExtraPerModel, blastExtraPerModel),
|
|
43
|
+
});
|
|
44
|
+
// 2. Hits
|
|
45
|
+
const hitStat = isMelee ? weaponProfile.stats.WS : weaponProfile.stats.BS;
|
|
46
|
+
const torrent = !!findKeyword(resolved, "torrent");
|
|
47
|
+
let hits;
|
|
48
|
+
let critHits;
|
|
49
|
+
let hitsDetail;
|
|
50
|
+
if (torrent) {
|
|
51
|
+
hits = attacks;
|
|
52
|
+
critHits = 0;
|
|
53
|
+
hitsDetail = `Torrent: auto-hits (${attacks.toFixed(4)})`;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
if (typeof hitStat !== "number") {
|
|
57
|
+
throw new Error(`crunch: weapon ${input.attacker.weapon.id} profile ${input.attacker.profileIndex} missing ${isMelee ? "WS" : "BS"}`);
|
|
58
|
+
}
|
|
59
|
+
const probs = checkProbabilities({
|
|
60
|
+
unmodifiedNeeded: hitStat,
|
|
61
|
+
modifier: resolved.hitMod.value,
|
|
62
|
+
reroll: resolved.rerolls.hit?.subset ?? "none",
|
|
63
|
+
autoFailOnOne: true,
|
|
64
|
+
autoPassOnSix: true,
|
|
65
|
+
critThreshold: 6,
|
|
66
|
+
});
|
|
67
|
+
hits = attacks * probs.pass;
|
|
68
|
+
critHits = attacks * probs.crit;
|
|
69
|
+
hitsDetail = `${isMelee ? "WS" : "BS"}${hitStat}+ (mod ${signed(resolved.hitMod.value)}, reroll ${resolved.rerolls.hit?.subset ?? "none"}) → P(hit)=${probs.pass.toFixed(4)}, P(crit)=${probs.crit.toFixed(4)}`;
|
|
70
|
+
}
|
|
71
|
+
const sustained = findKeyword(resolved, "sustained-hits");
|
|
72
|
+
if (sustained) {
|
|
73
|
+
hits += critHits * evalStatValue(sustained.parameters?.value);
|
|
74
|
+
hitsDetail += `; +Sustained Hits ${sustained.parameters?.value ?? 1} on ${critHits.toFixed(4)} crits`;
|
|
75
|
+
}
|
|
76
|
+
stages.push({ name: "hits", expected: hits, detail: hitsDetail });
|
|
77
|
+
// 3. Wounds
|
|
78
|
+
const S = evalStatValue(weaponProfile.stats.S) + resolved.strengthMod.value;
|
|
79
|
+
const T = unitProfile.T + resolved.toughnessMod.value;
|
|
80
|
+
const stdWoundNeeded = woundThreshold(S, T);
|
|
81
|
+
const anti = findKeyword(resolved, "anti");
|
|
82
|
+
let antiThreshold = 7; // unreachable
|
|
83
|
+
if (anti) {
|
|
84
|
+
const targetKw = anti.parameters?.target_keyword?.toLowerCase();
|
|
85
|
+
if (targetKw && targetKeywords.includes(targetKw)) {
|
|
86
|
+
const threshold = Number(anti.parameters?.threshold);
|
|
87
|
+
if (Number.isFinite(threshold))
|
|
88
|
+
antiThreshold = threshold;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const critWoundThreshold = Math.min(6, antiThreshold);
|
|
92
|
+
const hasLethal = !!findKeyword(resolved, "lethal-hits");
|
|
93
|
+
const hitsForWoundRoll = hasLethal ? hits - critHits : hits;
|
|
94
|
+
const lethalAutoWounds = hasLethal ? critHits : 0;
|
|
95
|
+
const woundProbs = checkProbabilities({
|
|
96
|
+
unmodifiedNeeded: stdWoundNeeded,
|
|
97
|
+
modifier: resolved.woundMod.value,
|
|
98
|
+
reroll: resolved.rerolls.wound?.subset ?? "none",
|
|
99
|
+
autoFailOnOne: true,
|
|
100
|
+
autoPassOnSix: true,
|
|
101
|
+
critThreshold: critWoundThreshold,
|
|
102
|
+
});
|
|
103
|
+
const regularWoundsFromRoll = hitsForWoundRoll * (woundProbs.pass - woundProbs.crit);
|
|
104
|
+
const critWoundsFromRoll = hitsForWoundRoll * woundProbs.crit;
|
|
105
|
+
const totalRegularWounds = regularWoundsFromRoll + lethalAutoWounds;
|
|
106
|
+
const hasDevastating = !!findKeyword(resolved, "devastating-wounds");
|
|
107
|
+
const mortalWoundsStream = hasDevastating ? critWoundsFromRoll : 0;
|
|
108
|
+
const regularWoundsForSaves = hasDevastating ? totalRegularWounds : totalRegularWounds + critWoundsFromRoll;
|
|
109
|
+
const totalWounds = regularWoundsForSaves + mortalWoundsStream;
|
|
110
|
+
stages.push({
|
|
111
|
+
name: "wounds",
|
|
112
|
+
expected: totalWounds,
|
|
113
|
+
detail: `S${S} vs T${T} → need ${stdWoundNeeded}+, anti ${antiThreshold <= 6 ? `${antiThreshold}+ (active)` : "n/a"}, P(wound)=${woundProbs.pass.toFixed(4)} (${critWoundsFromRoll.toFixed(4)} crit), lethal ${hasLethal ? "+" + lethalAutoWounds.toFixed(4) : "—"}, devastating ${hasDevastating ? mortalWoundsStream.toFixed(4) + " MW" : "—"}`,
|
|
114
|
+
});
|
|
115
|
+
// 4. Saves
|
|
116
|
+
const apMod = resolved.apMod.value;
|
|
117
|
+
const AP = weaponProfile.stats.AP + apMod;
|
|
118
|
+
const saveMod = resolved.saveMod.value;
|
|
119
|
+
const armorTargetRaw = unitProfile.Sv - AP - saveMod;
|
|
120
|
+
const ignoresCover = !!findKeyword(resolved, "ignores-cover");
|
|
121
|
+
const covered = resolved.cover.active && !ignoresCover && input.attacker.weapon.type === "ranged";
|
|
122
|
+
const armorAfterCover = covered ? Math.max(3, armorTargetRaw - 1) : armorTargetRaw;
|
|
123
|
+
const armorFinal = clamp(armorAfterCover, 2, 7);
|
|
124
|
+
const invuln = unitProfile.invuln_sv ?? null;
|
|
125
|
+
const effectiveSaveTarget = invuln !== null ? Math.min(armorFinal, invuln) : armorFinal;
|
|
126
|
+
const saveProbs = checkProbabilities({
|
|
127
|
+
unmodifiedNeeded: effectiveSaveTarget,
|
|
128
|
+
modifier: 0,
|
|
129
|
+
reroll: resolved.rerolls.save?.subset ?? "none",
|
|
130
|
+
autoFailOnOne: true,
|
|
131
|
+
autoPassOnSix: false,
|
|
132
|
+
critThreshold: 7,
|
|
133
|
+
});
|
|
134
|
+
const pSaved = effectiveSaveTarget >= 7 ? 0 : saveProbs.pass;
|
|
135
|
+
const unsaved = regularWoundsForSaves * (1 - pSaved);
|
|
136
|
+
stages.push({
|
|
137
|
+
name: "unsaved",
|
|
138
|
+
expected: unsaved,
|
|
139
|
+
detail: `Sv${unitProfile.Sv}+, AP${signed(AP)}${apMod !== 0 ? ` (apmod ${signed(apMod)})` : ""}${saveMod !== 0 ? `, savemod ${signed(saveMod)}` : ""}${covered ? ", cover (+1, cap 3+)" : ""} → effective ${effectiveSaveTarget}+ (P(save)=${pSaved.toFixed(4)})`,
|
|
140
|
+
});
|
|
141
|
+
// 5. Damage
|
|
142
|
+
const baseD = evalStatValue(weaponProfile.stats.D);
|
|
143
|
+
const melta = findKeyword(resolved, "melta");
|
|
144
|
+
const meltaBonus = melta && halfRange ? evalStatValue(melta.parameters?.value) : 0;
|
|
145
|
+
const damagePerHit = Math.max(0, baseD + meltaBonus + resolved.damageMod.value);
|
|
146
|
+
const damageMain = unsaved * damagePerHit;
|
|
147
|
+
const damageMortal = mortalWoundsStream * damagePerHit;
|
|
148
|
+
const damage = damageMain + damageMortal;
|
|
149
|
+
stages.push({
|
|
150
|
+
name: "damage",
|
|
151
|
+
expected: damage,
|
|
152
|
+
detail: `D ${baseD}${meltaBonus ? ` + Melta ${meltaBonus} (half range)` : ""}${resolved.damageMod.value !== 0 ? ` ${signed(resolved.damageMod.value)} (mod)` : ""} = ${damagePerHit} per hit; main ${damageMain.toFixed(4)}, mortal ${damageMortal.toFixed(4)}`,
|
|
153
|
+
});
|
|
154
|
+
// 6. FNP
|
|
155
|
+
let afterFnp = damage;
|
|
156
|
+
let fnpDetail = "no FNP";
|
|
157
|
+
const fnp = resolved.feelNoPain;
|
|
158
|
+
if (fnp) {
|
|
159
|
+
const pSucc = Math.max(0, Math.min(1, (7 - fnp.threshold) / 6));
|
|
160
|
+
afterFnp = damage * (1 - pSucc);
|
|
161
|
+
fnpDetail = `FNP ${fnp.threshold}+ (P=${pSucc.toFixed(4)})`;
|
|
162
|
+
}
|
|
163
|
+
// TODO M2: per-damage-point FNP rolls (e.g. Death Guard 5+ FNP only on
|
|
164
|
+
// mortals); the current model applies FNP linearly to expected damage.
|
|
165
|
+
stages.push({ name: "after-fnp", expected: afterFnp, detail: fnpDetail });
|
|
166
|
+
// 7. Models killed
|
|
167
|
+
const W = unitProfile.W;
|
|
168
|
+
const expectedModelsKilled = W > 0 ? Math.min(targetModelCount, afterFnp / W) : 0;
|
|
169
|
+
stages.push({
|
|
170
|
+
name: "models-killed",
|
|
171
|
+
expected: expectedModelsKilled,
|
|
172
|
+
detail: `W${W} per model, ${targetModelCount} models in target; ${afterFnp.toFixed(4)} damage / ${W} = ${(afterFnp / W).toFixed(4)} (capped at ${targetModelCount})`,
|
|
173
|
+
});
|
|
174
|
+
return { stages, resolved };
|
|
175
|
+
}
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Helpers
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
/** Lower-cased union of a unit's `keywords` + `faction_keywords`. */
|
|
180
|
+
function unitKeywordsLower(unit) {
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const k of unit.keywords ?? [])
|
|
183
|
+
out.push(String(k).toLowerCase());
|
|
184
|
+
for (const k of unit.faction_keywords ?? [])
|
|
185
|
+
out.push(String(k).toLowerCase());
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function profileBuffsFor(attacker, dataset, ctx) {
|
|
189
|
+
const weaponView = dataset.weapons.get(attacker.weapon.id);
|
|
190
|
+
if (!weaponView) {
|
|
191
|
+
// Weapon isn't in the dataset (probably a hand-built test fixture); fall
|
|
192
|
+
// back to walking its catalog keywords manually.
|
|
193
|
+
return manualWeaponKeywordBuffs(attacker, dataset, ctx);
|
|
194
|
+
}
|
|
195
|
+
return weaponView.profileBuffs(attacker.profileIndex, ctx);
|
|
196
|
+
}
|
|
197
|
+
function manualWeaponKeywordBuffs(attacker, dataset, ctx) {
|
|
198
|
+
const profile = attacker.weapon.profiles[attacker.profileIndex];
|
|
199
|
+
if (!profile)
|
|
200
|
+
return [];
|
|
201
|
+
const out = [];
|
|
202
|
+
for (const ref of profile.keywords ?? []) {
|
|
203
|
+
const view = dataset.weaponKeywords.get(ref.keyword_id);
|
|
204
|
+
if (!view)
|
|
205
|
+
continue;
|
|
206
|
+
out.push(...view.getBuffs(ref.parameters, attacker.weapon.id, ctx));
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function findKeyword(resolved, keywordId) {
|
|
211
|
+
return resolved.extraKeywords.find((e) => e.keywordRef.keyword_id === keywordId)?.keywordRef;
|
|
212
|
+
}
|
|
213
|
+
/** Standard 10e S-vs-T table → unmodified wound threshold (2..6). */
|
|
214
|
+
function woundThreshold(S, T) {
|
|
215
|
+
if (S >= 2 * T)
|
|
216
|
+
return 2;
|
|
217
|
+
if (S > T)
|
|
218
|
+
return 3;
|
|
219
|
+
if (S === T)
|
|
220
|
+
return 4;
|
|
221
|
+
if (S * 2 > T)
|
|
222
|
+
return 5;
|
|
223
|
+
return 6;
|
|
224
|
+
}
|
|
225
|
+
/** Probability a single die check passes (and the conditional crit rate). */
|
|
226
|
+
function checkProbabilities(args) {
|
|
227
|
+
function outcome(face) {
|
|
228
|
+
if (args.autoFailOnOne && face === 1)
|
|
229
|
+
return { pass: 0, crit: 0 };
|
|
230
|
+
if (face >= args.critThreshold)
|
|
231
|
+
return { pass: 1, crit: 1 };
|
|
232
|
+
if (args.autoPassOnSix && face === 6)
|
|
233
|
+
return { pass: 1, crit: 0 };
|
|
234
|
+
return (face + args.modifier) >= args.unmodifiedNeeded
|
|
235
|
+
? { pass: 1, crit: 0 }
|
|
236
|
+
: { pass: 0, crit: 0 };
|
|
237
|
+
}
|
|
238
|
+
let pass = 0;
|
|
239
|
+
let crit = 0;
|
|
240
|
+
for (let face = 1; face <= 6; face++) {
|
|
241
|
+
const initial = outcome(face);
|
|
242
|
+
if (initial.pass === 1) {
|
|
243
|
+
pass += 1 / 6;
|
|
244
|
+
crit += initial.crit / 6;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// Failed initial — eligible for reroll?
|
|
248
|
+
const eligible = args.reroll === "all-failures" || (args.reroll === "ones" && face === 1);
|
|
249
|
+
if (!eligible)
|
|
250
|
+
continue;
|
|
251
|
+
// Reroll: uniform over 1..6.
|
|
252
|
+
let rerollPass = 0;
|
|
253
|
+
let rerollCrit = 0;
|
|
254
|
+
for (let f2 = 1; f2 <= 6; f2++) {
|
|
255
|
+
const second = outcome(f2);
|
|
256
|
+
rerollPass += second.pass / 6;
|
|
257
|
+
rerollCrit += second.crit / 6;
|
|
258
|
+
}
|
|
259
|
+
pass += rerollPass / 6;
|
|
260
|
+
crit += rerollCrit / 6;
|
|
261
|
+
}
|
|
262
|
+
return { pass, crit };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Mean value of a stat (number or dice expression like `"D6"`, `"2D6"`,
|
|
266
|
+
* `"D3+1"`, `"D6-1"`). Unrecognised strings throw — better to crash than to
|
|
267
|
+
* silently return 0 and produce a confidently wrong damage projection.
|
|
268
|
+
*/
|
|
269
|
+
function evalStatValue(v) {
|
|
270
|
+
if (typeof v === "number")
|
|
271
|
+
return v;
|
|
272
|
+
if (typeof v !== "string")
|
|
273
|
+
return Number(v) || 0;
|
|
274
|
+
const trimmed = v.trim();
|
|
275
|
+
if (trimmed === "")
|
|
276
|
+
return 0;
|
|
277
|
+
const asNumber = Number(trimmed);
|
|
278
|
+
if (Number.isFinite(asNumber))
|
|
279
|
+
return asNumber;
|
|
280
|
+
const match = /^(\d*)D(\d+)([+-]\d+)?$/i.exec(trimmed);
|
|
281
|
+
if (!match)
|
|
282
|
+
throw new Error(`evalStatValue: cannot parse "${v}"`);
|
|
283
|
+
const count = match[1] === "" ? 1 : Number(match[1]);
|
|
284
|
+
const die = Number(match[2]);
|
|
285
|
+
const offset = match[3] ? Number(match[3]) : 0;
|
|
286
|
+
return count * (die + 1) / 2 + offset;
|
|
287
|
+
}
|
|
288
|
+
function clamp(n, lo, hi) {
|
|
289
|
+
return Math.max(lo, Math.min(hi, n));
|
|
290
|
+
}
|
|
291
|
+
function signed(n) {
|
|
292
|
+
if (n > 0)
|
|
293
|
+
return `+${n}`;
|
|
294
|
+
if (n < 0)
|
|
295
|
+
return `${n}`;
|
|
296
|
+
return "0";
|
|
297
|
+
}
|
|
298
|
+
function attacksDetail(models, per, rapidFire, blast) {
|
|
299
|
+
const parts = [`${models} × ${per}`];
|
|
300
|
+
if (rapidFire)
|
|
301
|
+
parts.push(`+ Rapid Fire ${rapidFire} (half range)`);
|
|
302
|
+
if (blast)
|
|
303
|
+
parts.push(`+ Blast ${blast}/model`);
|
|
304
|
+
return parts.join(" ");
|
|
305
|
+
}
|
|
306
|
+
let _embeddedDataset = null;
|
|
307
|
+
function lazyEmbeddedDataset() {
|
|
308
|
+
if (!_embeddedDataset)
|
|
309
|
+
_embeddedDataset = Dataset.embedded();
|
|
310
|
+
return _embeddedDataset;
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/cruncher/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The expected-value damage engine.\n *\n * Closed-form math over schema profiles + a flat {@link Buff} stack. No\n * sampling, no I/O. Auto-injects every weapon-keyword on the attacker's\n * profile as a buff (so callers don't have to enumerate intrinsics), then\n * resolves the stack via {@link resolveBuffs}, then walks\n * attacks → hits → wounds → unsaved → damage → after-fnp → models-killed.\n *\n * The dataset is required (and defaults to the embedded one) — without it\n * the engine can't look up weapon-keyword effects.\n */\nimport type { Phase, Unit, Weapon } from \"../generated.js\";\nimport {\n type Buff,\n type EngineContext,\n resolveBuffs,\n type ResolvedModifiers,\n type WeaponKeywordRef,\n} from \"./buffs.js\";\nimport { Dataset } from \"../data/dataset.js\";\n\nexport type AttackProfileRef = { weapon: Weapon; profileIndex: number };\nexport type TargetProfileRef = {\n unit: Unit;\n profileIndex: number;\n /** Override target model count (otherwise read from `unit.model_count.min`). */\n modelCount?: number;\n};\n\nexport type Stage = {\n name: \"attacks\" | \"hits\" | \"wounds\" | \"unsaved\" | \"damage\" | \"after-fnp\" | \"models-killed\";\n expected: number;\n detail: string;\n};\n\nexport type EngineInput = {\n attacker: AttackProfileRef;\n target: TargetProfileRef;\n modelsFiring: number;\n /** User / ability / manual buffs. Weapon-keyword buffs are auto-injected. */\n buffs: Buff[];\n context: EngineContext;\n};\n\nexport type EngineOutput = { stages: Stage[]; resolved: ResolvedModifiers };\n\n/**\n * Compute the expected per-stage projection for one (attacker, target, buffs)\n * triple. The dataset defaults to the embedded one — pass an alternate when\n * crunching against a different bundle (e.g. tests).\n */\nexport function crunch(input: EngineInput, dataset?: Dataset): EngineOutput {\n const ds = dataset ?? lazyEmbeddedDataset();\n const weaponProfile = input.attacker.weapon.profiles[input.attacker.profileIndex];\n if (!weaponProfile) {\n throw new RangeError(\n `crunch: attacker.profileIndex=${input.attacker.profileIndex} is out of range for weapon ${input.attacker.weapon.id}`,\n );\n }\n const unitProfile = input.target.unit.profiles[input.target.profileIndex];\n if (!unitProfile) {\n throw new RangeError(\n `crunch: target.profileIndex=${input.target.profileIndex} is out of range for unit ${input.target.unit.id}`,\n );\n }\n\n const targetKeywords = unitKeywordsLower(input.target.unit);\n const ctx: EngineContext = {\n ...input.context,\n targetKeywords: input.context.targetKeywords ?? targetKeywords,\n };\n\n // Auto-inject weapon-keyword buffs from the attacker profile, then append\n // the caller-supplied stack. resolveBuffs deduplicates and ranks them.\n const profileBuffs = profileBuffsFor(input.attacker, ds, ctx);\n const resolved = resolveBuffs([...profileBuffs, ...input.buffs], ctx);\n\n const stages: Stage[] = [];\n\n // 1. Attacks\n const isMelee = input.attacker.weapon.type === \"melee\";\n const baseA = evalStatValue(weaponProfile.stats.A);\n const attacksPerModel = baseA + resolved.attacksMod.value;\n const rapidFire = findKeyword(resolved, \"rapid-fire\");\n const halfRange = ctx.withinHalfRange === true;\n const rapidFireExtraPerModel = rapidFire && halfRange ? evalStatValue(rapidFire.parameters?.value) : 0;\n const blast = findKeyword(resolved, \"blast\");\n const targetModelCount = input.target.modelCount ?? input.target.unit.model_count?.min ?? 1;\n const blastExtraPerModel = blast ? Math.floor(targetModelCount / 5) : 0;\n const attacks = input.modelsFiring * (attacksPerModel + rapidFireExtraPerModel + blastExtraPerModel);\n stages.push({\n name: \"attacks\",\n expected: attacks,\n detail: attacksDetail(input.modelsFiring, attacksPerModel, rapidFireExtraPerModel, blastExtraPerModel),\n });\n\n // 2. Hits\n const hitStat = isMelee ? weaponProfile.stats.WS : weaponProfile.stats.BS;\n const torrent = !!findKeyword(resolved, \"torrent\");\n let hits: number;\n let critHits: number;\n let hitsDetail: string;\n if (torrent) {\n hits = attacks;\n critHits = 0;\n hitsDetail = `Torrent: auto-hits (${attacks.toFixed(4)})`;\n } else {\n if (typeof hitStat !== \"number\") {\n throw new Error(\n `crunch: weapon ${input.attacker.weapon.id} profile ${input.attacker.profileIndex} missing ${isMelee ? \"WS\" : \"BS\"}`,\n );\n }\n const probs = checkProbabilities({\n unmodifiedNeeded: hitStat,\n modifier: resolved.hitMod.value,\n reroll: resolved.rerolls.hit?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: true,\n critThreshold: 6,\n });\n hits = attacks * probs.pass;\n critHits = attacks * probs.crit;\n hitsDetail = `${isMelee ? \"WS\" : \"BS\"}${hitStat}+ (mod ${signed(resolved.hitMod.value)}, reroll ${resolved.rerolls.hit?.subset ?? \"none\"}) → P(hit)=${probs.pass.toFixed(4)}, P(crit)=${probs.crit.toFixed(4)}`;\n }\n const sustained = findKeyword(resolved, \"sustained-hits\");\n if (sustained) {\n hits += critHits * evalStatValue(sustained.parameters?.value);\n hitsDetail += `; +Sustained Hits ${sustained.parameters?.value ?? 1} on ${critHits.toFixed(4)} crits`;\n }\n stages.push({ name: \"hits\", expected: hits, detail: hitsDetail });\n\n // 3. Wounds\n const S = evalStatValue(weaponProfile.stats.S) + resolved.strengthMod.value;\n const T = unitProfile.T + resolved.toughnessMod.value;\n const stdWoundNeeded = woundThreshold(S, T);\n const anti = findKeyword(resolved, \"anti\");\n let antiThreshold = 7; // unreachable\n if (anti) {\n const targetKw = (anti.parameters?.target_keyword as string | undefined)?.toLowerCase();\n if (targetKw && targetKeywords.includes(targetKw)) {\n const threshold = Number(anti.parameters?.threshold);\n if (Number.isFinite(threshold)) antiThreshold = threshold;\n }\n }\n const critWoundThreshold = Math.min(6, antiThreshold);\n\n const hasLethal = !!findKeyword(resolved, \"lethal-hits\");\n const hitsForWoundRoll = hasLethal ? hits - critHits : hits;\n const lethalAutoWounds = hasLethal ? critHits : 0;\n\n const woundProbs = checkProbabilities({\n unmodifiedNeeded: stdWoundNeeded,\n modifier: resolved.woundMod.value,\n reroll: resolved.rerolls.wound?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: true,\n critThreshold: critWoundThreshold,\n });\n const regularWoundsFromRoll = hitsForWoundRoll * (woundProbs.pass - woundProbs.crit);\n const critWoundsFromRoll = hitsForWoundRoll * woundProbs.crit;\n const totalRegularWounds = regularWoundsFromRoll + lethalAutoWounds;\n const hasDevastating = !!findKeyword(resolved, \"devastating-wounds\");\n const mortalWoundsStream = hasDevastating ? critWoundsFromRoll : 0;\n const regularWoundsForSaves = hasDevastating ? totalRegularWounds : totalRegularWounds + critWoundsFromRoll;\n const totalWounds = regularWoundsForSaves + mortalWoundsStream;\n stages.push({\n name: \"wounds\",\n expected: totalWounds,\n detail: `S${S} vs T${T} → need ${stdWoundNeeded}+, anti ${antiThreshold <= 6 ? `${antiThreshold}+ (active)` : \"n/a\"}, P(wound)=${woundProbs.pass.toFixed(4)} (${critWoundsFromRoll.toFixed(4)} crit), lethal ${hasLethal ? \"+\" + lethalAutoWounds.toFixed(4) : \"—\"}, devastating ${hasDevastating ? mortalWoundsStream.toFixed(4) + \" MW\" : \"—\"}`,\n });\n\n // 4. Saves\n const apMod = resolved.apMod.value;\n const AP = weaponProfile.stats.AP + apMod;\n const saveMod = resolved.saveMod.value;\n const armorTargetRaw = unitProfile.Sv - AP - saveMod;\n const ignoresCover = !!findKeyword(resolved, \"ignores-cover\");\n const covered =\n resolved.cover.active && !ignoresCover && input.attacker.weapon.type === \"ranged\";\n const armorAfterCover = covered ? Math.max(3, armorTargetRaw - 1) : armorTargetRaw;\n const armorFinal = clamp(armorAfterCover, 2, 7);\n const invuln = unitProfile.invuln_sv ?? null;\n const effectiveSaveTarget = invuln !== null ? Math.min(armorFinal, invuln) : armorFinal;\n\n const saveProbs = checkProbabilities({\n unmodifiedNeeded: effectiveSaveTarget,\n modifier: 0,\n reroll: resolved.rerolls.save?.subset ?? \"none\",\n autoFailOnOne: true,\n autoPassOnSix: false,\n critThreshold: 7,\n });\n const pSaved = effectiveSaveTarget >= 7 ? 0 : saveProbs.pass;\n const unsaved = regularWoundsForSaves * (1 - pSaved);\n stages.push({\n name: \"unsaved\",\n expected: unsaved,\n detail: `Sv${unitProfile.Sv}+, AP${signed(AP)}${apMod !== 0 ? ` (apmod ${signed(apMod)})` : \"\"}${saveMod !== 0 ? `, savemod ${signed(saveMod)}` : \"\"}${covered ? \", cover (+1, cap 3+)\" : \"\"} → effective ${effectiveSaveTarget}+ (P(save)=${pSaved.toFixed(4)})`,\n });\n\n // 5. Damage\n const baseD = evalStatValue(weaponProfile.stats.D);\n const melta = findKeyword(resolved, \"melta\");\n const meltaBonus = melta && halfRange ? evalStatValue(melta.parameters?.value) : 0;\n const damagePerHit = Math.max(0, baseD + meltaBonus + resolved.damageMod.value);\n const damageMain = unsaved * damagePerHit;\n const damageMortal = mortalWoundsStream * damagePerHit;\n const damage = damageMain + damageMortal;\n stages.push({\n name: \"damage\",\n expected: damage,\n detail: `D ${baseD}${meltaBonus ? ` + Melta ${meltaBonus} (half range)` : \"\"}${resolved.damageMod.value !== 0 ? ` ${signed(resolved.damageMod.value)} (mod)` : \"\"} = ${damagePerHit} per hit; main ${damageMain.toFixed(4)}, mortal ${damageMortal.toFixed(4)}`,\n });\n\n // 6. FNP\n let afterFnp = damage;\n let fnpDetail = \"no FNP\";\n const fnp = resolved.feelNoPain;\n if (fnp) {\n const pSucc = Math.max(0, Math.min(1, (7 - fnp.threshold) / 6));\n afterFnp = damage * (1 - pSucc);\n fnpDetail = `FNP ${fnp.threshold}+ (P=${pSucc.toFixed(4)})`;\n }\n // TODO M2: per-damage-point FNP rolls (e.g. Death Guard 5+ FNP only on\n // mortals); the current model applies FNP linearly to expected damage.\n stages.push({ name: \"after-fnp\", expected: afterFnp, detail: fnpDetail });\n\n // 7. Models killed\n const W = unitProfile.W;\n const expectedModelsKilled = W > 0 ? Math.min(targetModelCount, afterFnp / W) : 0;\n stages.push({\n name: \"models-killed\",\n expected: expectedModelsKilled,\n detail: `W${W} per model, ${targetModelCount} models in target; ${afterFnp.toFixed(4)} damage / ${W} = ${(afterFnp / W).toFixed(4)} (capped at ${targetModelCount})`,\n });\n\n return { stages, resolved };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Lower-cased union of a unit's `keywords` + `faction_keywords`. */\nfunction unitKeywordsLower(unit: Unit): string[] {\n const out: string[] = [];\n for (const k of unit.keywords ?? []) out.push(String(k).toLowerCase());\n for (const k of unit.faction_keywords ?? []) out.push(String(k).toLowerCase());\n return out;\n}\n\nfunction profileBuffsFor(\n attacker: AttackProfileRef,\n dataset: Dataset,\n ctx: EngineContext,\n): Buff[] {\n const weaponView = dataset.weapons.get(attacker.weapon.id);\n if (!weaponView) {\n // Weapon isn't in the dataset (probably a hand-built test fixture); fall\n // back to walking its catalog keywords manually.\n return manualWeaponKeywordBuffs(attacker, dataset, ctx);\n }\n return weaponView.profileBuffs(attacker.profileIndex, ctx);\n}\n\nfunction manualWeaponKeywordBuffs(\n attacker: AttackProfileRef,\n dataset: Dataset,\n ctx: EngineContext,\n): Buff[] {\n const profile = attacker.weapon.profiles[attacker.profileIndex];\n if (!profile) return [];\n const out: Buff[] = [];\n for (const ref of profile.keywords ?? []) {\n const view = dataset.weaponKeywords.get(ref.keyword_id);\n if (!view) continue;\n out.push(\n ...view.getBuffs(\n ref.parameters as Record<string, unknown> | undefined,\n attacker.weapon.id,\n ctx,\n ),\n );\n }\n return out;\n}\n\nfunction findKeyword(\n resolved: ResolvedModifiers,\n keywordId: string,\n): WeaponKeywordRef | undefined {\n return resolved.extraKeywords.find((e) => e.keywordRef.keyword_id === keywordId)?.keywordRef;\n}\n\n/** Standard 10e S-vs-T table → unmodified wound threshold (2..6). */\nfunction woundThreshold(S: number, T: number): number {\n if (S >= 2 * T) return 2;\n if (S > T) return 3;\n if (S === T) return 4;\n if (S * 2 > T) return 5;\n return 6;\n}\n\n/** Probability a single die check passes (and the conditional crit rate). */\nfunction checkProbabilities(args: {\n unmodifiedNeeded: number;\n modifier: number;\n reroll: \"none\" | \"ones\" | \"all-failures\";\n autoFailOnOne: boolean;\n autoPassOnSix: boolean;\n /** Natural roll ≥ this is a crit. Use 7 to disable crits. */\n critThreshold: number;\n}): { pass: number; crit: number } {\n function outcome(face: number): { pass: number; crit: number } {\n if (args.autoFailOnOne && face === 1) return { pass: 0, crit: 0 };\n if (face >= args.critThreshold) return { pass: 1, crit: 1 };\n if (args.autoPassOnSix && face === 6) return { pass: 1, crit: 0 };\n return (face + args.modifier) >= args.unmodifiedNeeded\n ? { pass: 1, crit: 0 }\n : { pass: 0, crit: 0 };\n }\n\n let pass = 0;\n let crit = 0;\n for (let face = 1; face <= 6; face++) {\n const initial = outcome(face);\n if (initial.pass === 1) {\n pass += 1 / 6;\n crit += initial.crit / 6;\n continue;\n }\n // Failed initial — eligible for reroll?\n const eligible =\n args.reroll === \"all-failures\" || (args.reroll === \"ones\" && face === 1);\n if (!eligible) continue;\n // Reroll: uniform over 1..6.\n let rerollPass = 0;\n let rerollCrit = 0;\n for (let f2 = 1; f2 <= 6; f2++) {\n const second = outcome(f2);\n rerollPass += second.pass / 6;\n rerollCrit += second.crit / 6;\n }\n pass += rerollPass / 6;\n crit += rerollCrit / 6;\n }\n return { pass, crit };\n}\n\n/**\n * Mean value of a stat (number or dice expression like `\"D6\"`, `\"2D6\"`,\n * `\"D3+1\"`, `\"D6-1\"`). Unrecognised strings throw — better to crash than to\n * silently return 0 and produce a confidently wrong damage projection.\n */\nfunction evalStatValue(v: unknown): number {\n if (typeof v === \"number\") return v;\n if (typeof v !== \"string\") return Number(v) || 0;\n const trimmed = v.trim();\n if (trimmed === \"\") return 0;\n const asNumber = Number(trimmed);\n if (Number.isFinite(asNumber)) return asNumber;\n const match = /^(\\d*)D(\\d+)([+-]\\d+)?$/i.exec(trimmed);\n if (!match) throw new Error(`evalStatValue: cannot parse \"${v}\"`);\n const count = match[1] === \"\" ? 1 : Number(match[1]);\n const die = Number(match[2]);\n const offset = match[3] ? Number(match[3]) : 0;\n return count * (die + 1) / 2 + offset;\n}\n\nfunction clamp(n: number, lo: number, hi: number): number {\n return Math.max(lo, Math.min(hi, n));\n}\n\nfunction signed(n: number): string {\n if (n > 0) return `+${n}`;\n if (n < 0) return `${n}`;\n return \"0\";\n}\n\nfunction attacksDetail(\n models: number,\n per: number,\n rapidFire: number,\n blast: number,\n): string {\n const parts = [`${models} × ${per}`];\n if (rapidFire) parts.push(`+ Rapid Fire ${rapidFire} (half range)`);\n if (blast) parts.push(`+ Blast ${blast}/model`);\n return parts.join(\" \");\n}\n\nlet _embeddedDataset: Dataset | null = null;\nfunction lazyEmbeddedDataset(): Dataset {\n if (!_embeddedDataset) _embeddedDataset = Dataset.embedded();\n return _embeddedDataset;\n}\n\nexport type { Phase };\n"]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate an Ability DSL `effect` tree into the {@link Buff} stack it
|
|
3
|
+
* contributes (for an attacker-perspective crunch) along with a list of
|
|
4
|
+
* effect fragments the translator could not auto-apply.
|
|
5
|
+
*
|
|
6
|
+
* The buff layer is intentionally a subset of the DSL: it covers the math the
|
|
7
|
+
* cruncher's expected-value engine reads (rerolls, die-roll modifiers, S/A/T
|
|
8
|
+
* stat shifts, FNP, granted weapon keywords, cover) and reports everything
|
|
9
|
+
* else — choice nodes (player decisions), dice-gated effects (stochastic),
|
|
10
|
+
* defender-side bs-modifier, attack-restrictions, ability grants, mortal
|
|
11
|
+
* wound triggers — as `unsupported` so the SPA can surface "this ability has
|
|
12
|
+
* effects we can't auto-apply" rather than silently dropping them.
|
|
13
|
+
*
|
|
14
|
+
* The walker classifies an effect's `target` against the attacker
|
|
15
|
+
* perspective: `self`, `bearer`, `unit`, `attached-unit`, `attacker`, and
|
|
16
|
+
* `friendly-within-aura` are all treated as "applies to my unit". `defender`,
|
|
17
|
+
* `enemy-within-aura`, and `all-enemy` are dropped without being marked
|
|
18
|
+
* unsupported — those are defender-side mods and would surface from the
|
|
19
|
+
* target's perspective (M3 work), not the attacker's.
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
import type { Buff, BuffSource, EngineContext, WeaponKeywordRef } from "./buffs.js";
|
|
24
|
+
/** A fragment we couldn't translate. The SPA can render these as warnings. */
|
|
25
|
+
export type UnsupportedFragment = {
|
|
26
|
+
reason: string;
|
|
27
|
+
effectFragment: unknown;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A mutually-limited pool of {@link ActivatableBuff} levers. Dice-pool
|
|
31
|
+
* allocations cap how many options fire at once (`max_activations`); a `choice`
|
|
32
|
+
* lets the player pick exactly one. Levers sharing a `group.id` are subject to
|
|
33
|
+
* that cap — the SPA greys out further checkboxes once it's reached, and an
|
|
34
|
+
* optimizer enumerates subsets within it.
|
|
35
|
+
*/
|
|
36
|
+
export type ActivatableGroupRef = {
|
|
37
|
+
id: string;
|
|
38
|
+
maxActivations: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* A buff-bearing *player decision* the cruncher can't make on its own: a
|
|
42
|
+
* dice-pool option, a `choice` branch, or an activation gated on a timing the
|
|
43
|
+
* player controls (e.g. "start of phase"). It is not auto-applied — the
|
|
44
|
+
* consumer opts in (a checkbox, or an optimizer's search) and then folds
|
|
45
|
+
* {@link buffs} into the crunch. Conditions the activation still carries (a
|
|
46
|
+
* target keyword, a phase) ride on each buff's `applicableWhen`, so the
|
|
47
|
+
* resolver gates them per-target rather than the lever vanishing.
|
|
48
|
+
*/
|
|
49
|
+
export type ActivatableBuff = {
|
|
50
|
+
/** Stable toggle id, e.g. `"blessings-of-khorne#Warp Blades"`. */
|
|
51
|
+
id: string;
|
|
52
|
+
/** Human label for the lever (option name, or a summary of its buffs). */
|
|
53
|
+
label: string;
|
|
54
|
+
/** Contributions this activation adds when the player opts in (≥1). */
|
|
55
|
+
buffs: Buff[];
|
|
56
|
+
/** Set when the lever belongs to a mutually-limited pool. */
|
|
57
|
+
group?: ActivatableGroupRef;
|
|
58
|
+
};
|
|
59
|
+
export type EffectTranslation = {
|
|
60
|
+
applied: Buff[];
|
|
61
|
+
unsupported: UnsupportedFragment[];
|
|
62
|
+
/** Buffs sitting behind a player decision — see {@link ActivatableBuff}. */
|
|
63
|
+
activatable: ActivatableBuff[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Whose perspective the translation runs from.
|
|
67
|
+
*
|
|
68
|
+
* - `"attacker"`: the buffed unit is *firing*. `target: "unit"/"self"` etc.
|
|
69
|
+
* become attacker-side mods (re-rolls, hit/wound mods, A/S shifts, granted
|
|
70
|
+
* keywords). `target: "defender"` is silently dropped — that's incoming
|
|
71
|
+
* penalty math relevant when the buffed unit is the *target*, surfaced via
|
|
72
|
+
* the `"target"` perspective instead.
|
|
73
|
+
*
|
|
74
|
+
* - `"target"`: the buffed unit is *being shot at*. Defensive mods on the
|
|
75
|
+
* buffed unit (`stat-modifier T`, `stat-modifier Sv`, `feel-no-pain`,
|
|
76
|
+
* `roll-modifier save`) become defender-side buffs. Conversely, attacker-
|
|
77
|
+
* only mods (re-rolls, hit/wound mods, A/S shifts) drop silently because
|
|
78
|
+
* they describe what the buffed unit does when *attacking*.
|
|
79
|
+
*
|
|
80
|
+
* The bs-modifier effect (a -1 to incoming hit rolls, e.g. Benefit of Cover)
|
|
81
|
+
* becomes a `hit-mod` buff under target perspective so it stacks correctly
|
|
82
|
+
* with attacker-side modifiers in the resolver's ±1 cap.
|
|
83
|
+
*/
|
|
84
|
+
export type TranslationPerspective = "attacker" | "target";
|
|
85
|
+
/**
|
|
86
|
+
* Walk an ability DSL `effect` tree and produce the buff stack it contributes
|
|
87
|
+
* against `context` from the given `perspective`, plus an `unsupported` list
|
|
88
|
+
* naming any branches the buff layer can't express today.
|
|
89
|
+
*/
|
|
90
|
+
export declare function effectToBuffs(effect: unknown, source: BuffSource, context: EngineContext, perspective?: TranslationPerspective): EffectTranslation;
|
|
91
|
+
/**
|
|
92
|
+
* Parse a printed weapon-keyword string (e.g. `"Sustained Hits 1"`,
|
|
93
|
+
* `"Anti-INFANTRY 4+"`, `"Lethal Hits"`) into a `{keyword_id, parameters?}`
|
|
94
|
+
* catalog reference, or `null` if the form is unrecognised.
|
|
95
|
+
*
|
|
96
|
+
* Reverses the conventions baked into the M0 catalog: kebab-case ids,
|
|
97
|
+
* trailing number → `value`, embedded keyword + threshold → `target_keyword`
|
|
98
|
+
* + `threshold`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseKeywordGrant(raw: string): WeaponKeywordRef | null;
|
|
101
|
+
//# sourceMappingURL=from-dsl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"from-dsl.d.ts","sourceRoot":"","sources":["../../src/cruncher/from-dsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EACV,IAAI,EAGJ,UAAU,EACV,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,6DAA6D;IAC7D,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,4EAA4E;IAC5E,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAiB3D;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,aAAa,EACtB,WAAW,GAAE,sBAAmC,GAC/C,iBAAiB,CAKnB;AAi8BD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAuBtE"}
|