@alpaca-software/40kdc-data 0.1.2 → 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.
Files changed (117) hide show
  1. package/README.md +2 -0
  2. package/dist/abilities-resolver/resolver.d.ts +13 -4
  3. package/dist/abilities-resolver/resolver.d.ts.map +1 -1
  4. package/dist/abilities-resolver/resolver.js +22 -15
  5. package/dist/abilities-resolver/resolver.js.map +1 -1
  6. package/dist/audit-coverage.d.ts +78 -0
  7. package/dist/audit-coverage.d.ts.map +1 -0
  8. package/dist/audit-coverage.js +341 -0
  9. package/dist/audit-coverage.js.map +1 -0
  10. package/dist/author-batch.d.ts +147 -0
  11. package/dist/author-batch.d.ts.map +1 -0
  12. package/dist/author-batch.js +675 -0
  13. package/dist/author-batch.js.map +1 -0
  14. package/dist/author-input.d.ts +37 -0
  15. package/dist/author-input.d.ts.map +1 -0
  16. package/dist/author-input.js +162 -0
  17. package/dist/author-input.js.map +1 -0
  18. package/dist/cli.js +7 -0
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/translate.d.ts.map +1 -1
  21. package/dist/commands/translate.js +9 -4
  22. package/dist/commands/translate.js.map +1 -1
  23. package/dist/cruncher/attribution.d.ts +66 -0
  24. package/dist/cruncher/attribution.d.ts.map +1 -0
  25. package/dist/cruncher/attribution.js +88 -0
  26. package/dist/cruncher/attribution.js.map +1 -0
  27. package/dist/cruncher/buffs.d.ts +23 -1
  28. package/dist/cruncher/buffs.d.ts.map +1 -1
  29. package/dist/cruncher/buffs.js +1 -1
  30. package/dist/cruncher/buffs.js.map +1 -1
  31. package/dist/cruncher/from-dsl.d.ts +32 -0
  32. package/dist/cruncher/from-dsl.d.ts.map +1 -1
  33. package/dist/cruncher/from-dsl.js +485 -40
  34. package/dist/cruncher/from-dsl.js.map +1 -1
  35. package/dist/cruncher/index.d.ts +1 -0
  36. package/dist/cruncher/index.d.ts.map +1 -1
  37. package/dist/cruncher/index.js +1 -0
  38. package/dist/cruncher/index.js.map +1 -1
  39. package/dist/data/bundle.generated.js +1 -1
  40. package/dist/data/bundle.generated.js.map +1 -1
  41. package/dist/data/collection.d.ts +9 -0
  42. package/dist/data/collection.d.ts.map +1 -1
  43. package/dist/data/collection.js +14 -0
  44. package/dist/data/collection.js.map +1 -1
  45. package/dist/data/dataset.d.ts +80 -2
  46. package/dist/data/dataset.d.ts.map +1 -1
  47. package/dist/data/dataset.js +143 -6
  48. package/dist/data/dataset.js.map +1 -1
  49. package/dist/data/entities.d.ts +2 -5
  50. package/dist/data/entities.d.ts.map +1 -1
  51. package/dist/data/entities.js.map +1 -1
  52. package/dist/data/index.d.ts +3 -2
  53. package/dist/data/index.d.ts.map +1 -1
  54. package/dist/data/index.js +1 -1
  55. package/dist/data/index.js.map +1 -1
  56. package/dist/data/roster-resolve.d.ts +26 -1
  57. package/dist/data/roster-resolve.d.ts.map +1 -1
  58. package/dist/data/roster-resolve.js +46 -0
  59. package/dist/data/roster-resolve.js.map +1 -1
  60. package/dist/export/index.d.ts +1 -0
  61. package/dist/export/index.d.ts.map +1 -1
  62. package/dist/export/index.js +3 -0
  63. package/dist/export/index.js.map +1 -1
  64. package/dist/export/rosterizer.d.ts +3 -0
  65. package/dist/export/rosterizer.d.ts.map +1 -0
  66. package/dist/export/rosterizer.js +144 -0
  67. package/dist/export/rosterizer.js.map +1 -0
  68. package/dist/export/serializer.d.ts +1 -1
  69. package/dist/export/serializer.d.ts.map +1 -1
  70. package/dist/export/serializer.js.map +1 -1
  71. package/dist/gen-conformance.js +212 -11
  72. package/dist/gen-conformance.js.map +1 -1
  73. package/dist/import/gw.d.ts +69 -0
  74. package/dist/import/gw.d.ts.map +1 -0
  75. package/dist/import/gw.js +245 -0
  76. package/dist/import/gw.js.map +1 -0
  77. package/dist/import/import-roster.d.ts +52 -3
  78. package/dist/import/import-roster.d.ts.map +1 -1
  79. package/dist/import/import-roster.js +114 -4
  80. package/dist/import/import-roster.js.map +1 -1
  81. package/dist/import/index.d.ts +2 -2
  82. package/dist/import/index.d.ts.map +1 -1
  83. package/dist/import/index.js +1 -1
  84. package/dist/import/index.js.map +1 -1
  85. package/dist/import/listforge.d.ts.map +1 -1
  86. package/dist/import/listforge.js +15 -1
  87. package/dist/import/listforge.js.map +1 -1
  88. package/dist/import/newrecruit-text.d.ts +3 -0
  89. package/dist/import/newrecruit-text.d.ts.map +1 -1
  90. package/dist/import/newrecruit-text.js +6 -0
  91. package/dist/import/newrecruit-text.js.map +1 -1
  92. package/dist/import/newrecruit-wtc.d.ts.map +1 -1
  93. package/dist/import/newrecruit-wtc.js +10 -7
  94. package/dist/import/newrecruit-wtc.js.map +1 -1
  95. package/dist/import/rosterizer.d.ts +70 -0
  96. package/dist/import/rosterizer.d.ts.map +1 -0
  97. package/dist/import/rosterizer.js +348 -0
  98. package/dist/import/rosterizer.js.map +1 -0
  99. package/dist/import/types.d.ts +1 -1
  100. package/dist/import/types.d.ts.map +1 -1
  101. package/dist/import/types.js.map +1 -1
  102. package/dist/index.d.ts +3 -3
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +2 -2
  105. package/dist/index.js.map +1 -1
  106. package/dist/migrations/2026-weapon-keywords.js +4 -0
  107. package/dist/migrations/2026-weapon-keywords.js.map +1 -1
  108. package/dist/runner.d.ts +38 -0
  109. package/dist/runner.d.ts.map +1 -0
  110. package/dist/runner.js +492 -0
  111. package/dist/runner.js.map +1 -0
  112. package/dist/scrub-ip.d.ts +14 -0
  113. package/dist/scrub-ip.d.ts.map +1 -0
  114. package/dist/scrub-ip.js +88 -0
  115. package/dist/scrub-ip.js.map +1 -0
  116. package/package.json +9 -2
  117. package/schemas/core/roster.schema.json +3 -1
@@ -29,6 +29,7 @@ export declare class Collection<T, V> implements Iterable<V> {
29
29
  private readonly byId;
30
30
  private readonly byNorm;
31
31
  private readonly byFactionId;
32
+ private readonly idOf;
32
33
  private readonly nameOf?;
33
34
  private readonly wrapFn;
34
35
  constructor(cfg: CollectionConfig<T, V>);
@@ -38,6 +39,14 @@ export declare class Collection<T, V> implements Iterable<V> {
38
39
  get size(): number;
39
40
  /** Look up by exact id. */
40
41
  get(id: string): V | undefined;
42
+ /**
43
+ * Look up by exact id *within a faction*. Use this when an id is shared
44
+ * across factions (e.g. `chaos-land-raider` lives under five Chaos factions)
45
+ * and a faction context is known — {@link get} would return whichever copy
46
+ * was registered first, which may belong to the wrong faction. Returns
47
+ * `undefined` when no record with that id belongs to `factionId`.
48
+ */
49
+ getInFaction(id: string, factionId: string): V | undefined;
41
50
  /** Whether a record with this exact id exists. */
42
51
  has(id: string): boolean;
43
52
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/data/collection.ts"],"names":[],"mappings":"AAiBA,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB,CAAC,CAAC,EAAE,CAAC;IACpC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAClC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IACzC,8EAA8E;IAC9E,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACnD,4CAA4C;IAC5C,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU,CAAC,CAAC,EAAE,CAAC,CAAE,YAAW,QAAQ,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0B;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;gBAE5B,GAAG,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC;IAsBvC,6DAA6D;IAC7D,IAAI,GAAG,IAAI,CAAC,EAAE,CAEb;IAED,kCAAkC;IAClC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,2BAA2B;IAC3B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAK9B,kDAAkD;IAClD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAIlC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE;IAc3B,gFAAgF;IAChF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,EAAE;IAIjC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC;CAGjC"}
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/data/collection.ts"],"names":[],"mappings":"AAiBA,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB,CAAC,CAAC,EAAE,CAAC;IACpC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,sEAAsE;IACtE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAClC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IACzC,8EAA8E;IAC9E,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACnD,4CAA4C;IAC5C,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU,CAAC,CAAC,EAAE,CAAC,CAAE,YAAW,QAAQ,CAAC,CAAC,CAAC;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAwB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA0B;IACtD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;gBAE5B,GAAG,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC;IAuBvC,6DAA6D;IAC7D,IAAI,GAAG,IAAI,CAAC,EAAE,CAEb;IAED,kCAAkC;IAClC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,2BAA2B;IAC3B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAK9B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAM1D,kDAAkD;IAClD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIxB;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAIlC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE;IAc3B,gFAAgF;IAChF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,EAAE;IAIjC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC;CAGjC"}
@@ -27,9 +27,11 @@ export class Collection {
27
27
  byId = new Map();
28
28
  byNorm = new Map();
29
29
  byFactionId = new Map();
30
+ idOf;
30
31
  nameOf;
31
32
  wrapFn;
32
33
  constructor(cfg) {
34
+ this.idOf = cfg.idOf;
33
35
  this.nameOf = cfg.nameOf;
34
36
  this.wrapFn = cfg.wrap;
35
37
  const dedupeKeyOf = cfg.dedupeKeyOf ?? cfg.idOf;
@@ -64,6 +66,18 @@ export class Collection {
64
66
  const item = this.byId.get(id);
65
67
  return item ? this.wrapFn(item) : undefined;
66
68
  }
69
+ /**
70
+ * Look up by exact id *within a faction*. Use this when an id is shared
71
+ * across factions (e.g. `chaos-land-raider` lives under five Chaos factions)
72
+ * and a faction context is known — {@link get} would return whichever copy
73
+ * was registered first, which may belong to the wrong faction. Returns
74
+ * `undefined` when no record with that id belongs to `factionId`.
75
+ */
76
+ getInFaction(id, factionId) {
77
+ const list = this.byFactionId.get(factionId);
78
+ const item = list?.find((i) => this.idOf(i) === id);
79
+ return item ? this.wrapFn(item) : undefined;
80
+ }
67
81
  /** Whether a record with this exact id exists. */
68
82
  has(id) {
69
83
  return this.byId.has(id);
@@ -1 +1 @@
1
- {"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/data/collection.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A queryable, iterable view over one entity collection.\n *\n * Indexes (by id, by normalized name, by faction) are built once at construction.\n * Records are deduplicated by {@link CollectionConfig.dedupeKeyOf} (default: id,\n * first occurrence wins). Some records are intentionally shared: the same unit\n * id (e.g. `ministorum-priest`) appears under several factions, so units dedupe\n * on `(faction_id, id)` to keep each faction's copy; identical core abilities\n * (e.g. `leader`) copied into many faction files dedupe away on `ability_id`.\n *\n * `get(id)`/`find` return the first match when an id is shared across factions;\n * use {@link Collection.byFaction} or {@link Collection.findAll} to disambiguate.\n *\n * @packageDocumentation\n */\nimport { normalizeName } from \"./normalize.js\";\n\n/** How a {@link Collection} reads keys and builds views from raw records. */\nexport interface CollectionConfig<T, V> {\n items: T[];\n /** Primary id of a record (e.g. `u => u.id`, `a => a.ability_id`). */\n idOf: (item: T) => string;\n /**\n * Uniqueness key used for deduplication. Defaults to {@link idOf}. Set to a\n * composite (e.g. `(faction_id, id)`) for records that share an id across\n * factions, so distinct copies are preserved rather than collapsed.\n */\n dedupeKeyOf?: (item: T) => string;\n /** Display name, if the record has one — drives {@link Collection.find}. */\n nameOf?: (item: T) => string | undefined;\n /** Owning faction id, if applicable — drives {@link Collection.byFaction}. */\n factionOf?: (item: T) => string | null | undefined;\n /** Wrap a raw record in its linked view. */\n wrap: (item: T) => V;\n}\n\n/**\n * A collection of one entity type, exposing id/name/faction lookups.\n *\n * Iterable: `for (const unit of units) { … }`.\n *\n * @typeParam T - the raw (generated) record type\n * @typeParam V - the linked view type returned to callers\n */\nexport class Collection<T, V> implements Iterable<V> {\n private readonly items: T[] = [];\n private readonly byId = new Map<string, T>();\n private readonly byNorm = new Map<string, T[]>();\n private readonly byFactionId = new Map<string, T[]>();\n private readonly nameOf?: (item: T) => string | undefined;\n private readonly wrapFn: (item: T) => V;\n\n constructor(cfg: CollectionConfig<T, V>) {\n this.nameOf = cfg.nameOf;\n this.wrapFn = cfg.wrap;\n const dedupeKeyOf = cfg.dedupeKeyOf ?? cfg.idOf;\n const seen = new Set<string>();\n for (const item of cfg.items) {\n const dedupeKey = dedupeKeyOf(item);\n if (seen.has(dedupeKey)) continue; // first-wins dedup\n seen.add(dedupeKey);\n this.items.push(item);\n\n const id = cfg.idOf(item);\n if (!this.byId.has(id)) this.byId.set(id, item); // first-wins for shared ids\n\n const name = cfg.nameOf?.(item);\n if (name) push(this.byNorm, normalizeName(name), item);\n\n const faction = cfg.factionOf?.(item);\n if (faction) push(this.byFactionId, faction, item);\n }\n }\n\n /** Every record, deduplicated by id, in first-seen order. */\n get all(): V[] {\n return this.items.map((item) => this.wrapFn(item));\n }\n\n /** Number of distinct records. */\n get size(): number {\n return this.items.length;\n }\n\n /** Look up by exact id. */\n get(id: string): V | undefined {\n const item = this.byId.get(id);\n return item ? this.wrapFn(item) : undefined;\n }\n\n /** Whether a record with this exact id exists. */\n has(id: string): boolean {\n return this.byId.has(id);\n }\n\n /**\n * Find one record by id or name. Name matching is diacritic- and\n * punctuation-insensitive (see {@link normalizeName}), trying, in order:\n * exact id → exact normalized name → normalized-name substring. Returns the\n * first match; names can repeat across factions, so use {@link findAll} or\n * {@link byFaction} when a query may be ambiguous.\n *\n * @example\n * units.find(\"Kharn\"); // resolves \"Khârn the Betrayer\"\n */\n find(query: string): V | undefined {\n return this.findAll(query)[0];\n }\n\n /**\n * All records matching a query, by the same rules as {@link find}. An exact id\n * match returns just that record; otherwise every normalized-name-exact match\n * is returned, falling back to every normalized-name-substring match. Useful\n * to surface (rather than silently collapse) names shared across factions.\n */\n findAll(query: string): V[] {\n const byId = this.byId.get(query);\n if (byId) return [this.wrapFn(byId)];\n\n const key = normalizeName(query);\n const exact = this.byNorm.get(key);\n if (exact && exact.length > 0) return exact.map((i) => this.wrapFn(i));\n\n if (!this.nameOf || key === \"\") return [];\n return this.items\n .filter((item) => normalizeName(this.nameOf!(item) ?? \"\").includes(key))\n .map((item) => this.wrapFn(item));\n }\n\n /** All records belonging to a faction id (empty if the type has no faction). */\n byFaction(factionId: string): V[] {\n return (this.byFactionId.get(factionId) ?? []).map((i) => this.wrapFn(i));\n }\n\n [Symbol.iterator](): Iterator<V> {\n return this.items.map((item) => this.wrapFn(item))[Symbol.iterator]();\n }\n}\n\nfunction push<K, T>(map: Map<K, T[]>, key: K, value: T): void {\n const existing = map.get(key);\n if (existing) existing.push(value);\n else map.set(key, [value]);\n}\n"]}
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/data/collection.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A queryable, iterable view over one entity collection.\n *\n * Indexes (by id, by normalized name, by faction) are built once at construction.\n * Records are deduplicated by {@link CollectionConfig.dedupeKeyOf} (default: id,\n * first occurrence wins). Some records are intentionally shared: the same unit\n * id (e.g. `ministorum-priest`) appears under several factions, so units dedupe\n * on `(faction_id, id)` to keep each faction's copy; identical core abilities\n * (e.g. `leader`) copied into many faction files dedupe away on `ability_id`.\n *\n * `get(id)`/`find` return the first match when an id is shared across factions;\n * use {@link Collection.byFaction} or {@link Collection.findAll} to disambiguate.\n *\n * @packageDocumentation\n */\nimport { normalizeName } from \"./normalize.js\";\n\n/** How a {@link Collection} reads keys and builds views from raw records. */\nexport interface CollectionConfig<T, V> {\n items: T[];\n /** Primary id of a record (e.g. `u => u.id`, `a => a.ability_id`). */\n idOf: (item: T) => string;\n /**\n * Uniqueness key used for deduplication. Defaults to {@link idOf}. Set to a\n * composite (e.g. `(faction_id, id)`) for records that share an id across\n * factions, so distinct copies are preserved rather than collapsed.\n */\n dedupeKeyOf?: (item: T) => string;\n /** Display name, if the record has one — drives {@link Collection.find}. */\n nameOf?: (item: T) => string | undefined;\n /** Owning faction id, if applicable — drives {@link Collection.byFaction}. */\n factionOf?: (item: T) => string | null | undefined;\n /** Wrap a raw record in its linked view. */\n wrap: (item: T) => V;\n}\n\n/**\n * A collection of one entity type, exposing id/name/faction lookups.\n *\n * Iterable: `for (const unit of units) { … }`.\n *\n * @typeParam T - the raw (generated) record type\n * @typeParam V - the linked view type returned to callers\n */\nexport class Collection<T, V> implements Iterable<V> {\n private readonly items: T[] = [];\n private readonly byId = new Map<string, T>();\n private readonly byNorm = new Map<string, T[]>();\n private readonly byFactionId = new Map<string, T[]>();\n private readonly idOf: (item: T) => string;\n private readonly nameOf?: (item: T) => string | undefined;\n private readonly wrapFn: (item: T) => V;\n\n constructor(cfg: CollectionConfig<T, V>) {\n this.idOf = cfg.idOf;\n this.nameOf = cfg.nameOf;\n this.wrapFn = cfg.wrap;\n const dedupeKeyOf = cfg.dedupeKeyOf ?? cfg.idOf;\n const seen = new Set<string>();\n for (const item of cfg.items) {\n const dedupeKey = dedupeKeyOf(item);\n if (seen.has(dedupeKey)) continue; // first-wins dedup\n seen.add(dedupeKey);\n this.items.push(item);\n\n const id = cfg.idOf(item);\n if (!this.byId.has(id)) this.byId.set(id, item); // first-wins for shared ids\n\n const name = cfg.nameOf?.(item);\n if (name) push(this.byNorm, normalizeName(name), item);\n\n const faction = cfg.factionOf?.(item);\n if (faction) push(this.byFactionId, faction, item);\n }\n }\n\n /** Every record, deduplicated by id, in first-seen order. */\n get all(): V[] {\n return this.items.map((item) => this.wrapFn(item));\n }\n\n /** Number of distinct records. */\n get size(): number {\n return this.items.length;\n }\n\n /** Look up by exact id. */\n get(id: string): V | undefined {\n const item = this.byId.get(id);\n return item ? this.wrapFn(item) : undefined;\n }\n\n /**\n * Look up by exact id *within a faction*. Use this when an id is shared\n * across factions (e.g. `chaos-land-raider` lives under five Chaos factions)\n * and a faction context is known — {@link get} would return whichever copy\n * was registered first, which may belong to the wrong faction. Returns\n * `undefined` when no record with that id belongs to `factionId`.\n */\n getInFaction(id: string, factionId: string): V | undefined {\n const list = this.byFactionId.get(factionId);\n const item = list?.find((i) => this.idOf(i) === id);\n return item ? this.wrapFn(item) : undefined;\n }\n\n /** Whether a record with this exact id exists. */\n has(id: string): boolean {\n return this.byId.has(id);\n }\n\n /**\n * Find one record by id or name. Name matching is diacritic- and\n * punctuation-insensitive (see {@link normalizeName}), trying, in order:\n * exact id → exact normalized name → normalized-name substring. Returns the\n * first match; names can repeat across factions, so use {@link findAll} or\n * {@link byFaction} when a query may be ambiguous.\n *\n * @example\n * units.find(\"Kharn\"); // resolves \"Khârn the Betrayer\"\n */\n find(query: string): V | undefined {\n return this.findAll(query)[0];\n }\n\n /**\n * All records matching a query, by the same rules as {@link find}. An exact id\n * match returns just that record; otherwise every normalized-name-exact match\n * is returned, falling back to every normalized-name-substring match. Useful\n * to surface (rather than silently collapse) names shared across factions.\n */\n findAll(query: string): V[] {\n const byId = this.byId.get(query);\n if (byId) return [this.wrapFn(byId)];\n\n const key = normalizeName(query);\n const exact = this.byNorm.get(key);\n if (exact && exact.length > 0) return exact.map((i) => this.wrapFn(i));\n\n if (!this.nameOf || key === \"\") return [];\n return this.items\n .filter((item) => normalizeName(this.nameOf!(item) ?? \"\").includes(key))\n .map((item) => this.wrapFn(item));\n }\n\n /** All records belonging to a faction id (empty if the type has no faction). */\n byFaction(factionId: string): V[] {\n return (this.byFactionId.get(factionId) ?? []).map((i) => this.wrapFn(i));\n }\n\n [Symbol.iterator](): Iterator<V> {\n return this.items.map((item) => this.wrapFn(item))[Symbol.iterator]();\n }\n}\n\nfunction push<K, T>(map: Map<K, T[]>, key: K, value: T): void {\n const existing = map.get(key);\n if (existing) existing.push(value);\n else map.set(key, [value]);\n}\n"]}
@@ -9,8 +9,38 @@ import type { DeploymentPattern, Detachment, Enhancement, ForceDisposition, Game
9
9
  import { Collection } from "./collection.js";
10
10
  import { AbilityView, FactionView, UnitView, WeaponKeywordView, WeaponView } from "./entities.js";
11
11
  import { type RawData } from "./types.js";
12
- import type { Buff, EngineContext } from "../cruncher/buffs.js";
12
+ import type { Buff, BuffSource, EngineContext } from "../cruncher/buffs.js";
13
13
  import { type EligibilityInput, type EligibleAbility } from "../abilities-resolver/index.js";
14
+ /**
15
+ * One toggleable buff lever for damage analysis: the contributions it adds and
16
+ * whether it's on by default. `enabled` is `true` for buffs that always apply
17
+ * (intrinsic keywords, unconditional abilities) and `false` for player
18
+ * decisions — stratagems (CP cost) and activatable gates (dice-pool options,
19
+ * `choice` branches, timing-gated activations). A consumer flips `enabled`,
20
+ * then crunches the enabled subset; an optimizer searches it.
21
+ *
22
+ * @see {@link Dataset.stackableBuffsFor}
23
+ */
24
+ export type StackableBuff = {
25
+ /** Stable toggle id (stable across re-enumeration of the same input). */
26
+ id: string;
27
+ /** Human label for the lever. */
28
+ label: string;
29
+ /** Contributions this lever adds when enabled (≥1). */
30
+ buffs: Buff[];
31
+ /** Default selection state. */
32
+ enabled: boolean;
33
+ /** Where the lever came from. */
34
+ source: BuffSource;
35
+ /** Id of the mutually-limited {@link StackableBuffGroup} this belongs to, if any. */
36
+ group?: string;
37
+ };
38
+ /** A pool of {@link StackableBuff} levers limited to `maxActivations` at once. */
39
+ export type StackableBuffGroup = {
40
+ id: string;
41
+ label: string;
42
+ maxActivations: number;
43
+ };
14
44
  /** The whole dataset, with linked accessors over every entity collection. */
15
45
  export declare class Dataset {
16
46
  readonly units: Collection<Unit, UnitView>;
@@ -53,6 +83,23 @@ export declare class Dataset {
53
83
  unitsWithWeapon(weaponId: string): UnitView[];
54
84
  /** Weapons whose profiles reference the given weapon-keyword id. */
55
85
  weaponsWithKeyword(keywordId: string): WeaponView[];
86
+ /**
87
+ * Leaders whose leader-attachment data lists `bodyguardUnitId` among its
88
+ * eligible body units, sorted by name. The attachment is stored on the
89
+ * leader pointing down to its bodyguards, so answering "which leaders can
90
+ * attach to this unit?" means scanning the attachment list. Returns an empty
91
+ * array for a unit that no leader can attach to (including leader units).
92
+ */
93
+ leadersAttachableTo(bodyguardUnitId: string): UnitView[];
94
+ /**
95
+ * The inverse of {@link leadersAttachableTo}: the body units the given
96
+ * leader can attach to, sorted by name. Scans the same leader-attachment
97
+ * data from the leader's side (`leader_id` matches; resolve each
98
+ * `eligible_bodyguard_ids` entry), deduped by id. Empty for a non-leader
99
+ * unit. Together the two queries give the bidirectional attachment graph the
100
+ * SPA needs to offer a partner dropdown from either end.
101
+ */
102
+ bodyguardsAttachableFrom(leaderUnitId: string): UnitView[];
56
103
  /**
57
104
  * Enumerate every ability that could apply to the given unit in `phase`,
58
105
  * grouped by source. The SPA uses this to render the abilities pane.
@@ -62,7 +109,7 @@ export declare class Dataset {
62
109
  * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:
63
110
  * intrinsic weapon-profile keywords plus every eligible ability whose DSL
64
111
  * effect translates to an attacker-side buff (army, detachment, unit,
65
- * leader, support, plus any stratagems the caller has opted into).
112
+ * attached members, support, plus any stratagems the caller has opted into).
66
113
  *
67
114
  * The result includes only buffs the buff layer can express today — the
68
115
  * `unsupported` half of the DSL→Buff translation is dropped here so callers
@@ -95,6 +142,37 @@ export declare class Dataset {
95
142
  defensiveBuffsFor(input: EligibilityInput & {
96
143
  optedInStratagemIds?: string[];
97
144
  }, context: EngineContext): Buff[];
145
+ /**
146
+ * Enumerate every attacker-side buff a unit could stack in `context` as a
147
+ * list of toggleable levers, plus the activation groups that limit them.
148
+ *
149
+ * Unlike {@link buffsFor} — which returns only the buffs that auto-apply —
150
+ * this surfaces the *player decisions* too: stratagems, and the activatable
151
+ * gates the DSL models as dice-pool options, `choice` branches, or
152
+ * timing-gated activations (e.g. Blessings of Khorne's three keyword grants).
153
+ * Each lever carries `enabled` (its default state) and, where it's part of a
154
+ * limited pool, a `group` id whose {@link StackableBuffGroup} caps how many
155
+ * can fire at once. The intended loop:
156
+ *
157
+ * ```ts
158
+ * const { buffs } = ds.stackableBuffsFor(input, ctx);
159
+ * const chosen = buffs.filter(b => b.enabled).flatMap(b => b.buffs);
160
+ * crunch({ ...profiles, buffs: chosen, context: ctx }, ds);
161
+ * ```
162
+ *
163
+ * Target/phase conditions a lever still carries (e.g. "vs Infantry") ride on
164
+ * each buff's `applicableWhen`, so toggling it on is always safe — the
165
+ * resolver gates it per-target.
166
+ */
167
+ stackableBuffsFor(input: EligibilityInput & {
168
+ weaponProfiles?: {
169
+ weaponId: string;
170
+ profileIndex: number;
171
+ }[];
172
+ }, context: EngineContext): {
173
+ buffs: StackableBuff[];
174
+ groups: StackableBuffGroup[];
175
+ };
98
176
  /** Shared implementation for buffsFor / defensiveBuffsFor. */
99
177
  private collectBuffs;
100
178
  private buildIndexes;
@@ -1 +1 @@
1
- {"version":3,"file":"dataset.d.ts","sourceRoot":"","sources":["../../src/data/dataset.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dataset.d.ts","sourceRoot":"","sources":["../../src/data/dataset.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,gBAAgB,EAChB,OAAO,EACP,cAAc,EACd,KAAK,EACL,YAAY,EACZ,aAAa,EACb,SAAS,EACT,UAAU,EACV,IAAI,EACJ,eAAe,EACf,aAAa,EACb,aAAa,EACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,WAAW,EACX,WAAW,EACX,QAAQ,EACR,iBAAiB,EACjB,UAAU,EACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAgB,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,gCAAgC,CAAC;AAExC;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,+BAA+B;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,iCAAiC;IACjC,MAAM,EAAE,UAAU,CAAC;IACnB,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,kFAAkF;AAClF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,6EAA6E;AAC7E,qBAAa,OAAO;IAElB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC;IACrE,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,aAAa,EAAE,iBAAiB,CAAC,CAAC;IACtE,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC;IACxE,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC;IAG1E,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IACzD,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAC5D,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IACtD,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IAClE,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAChD,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;IACrE,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IAClE,QAAQ,CAAC,kBAAkB,EAAE,UAAU,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;IAC9E,QAAQ,CAAC,iBAAiB,EAAE,UAAU,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;IAC3E,QAAQ,CAAC,aAAa,EAAE,UAAU,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IAG/D,QAAQ,CAAC,iBAAiB,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACxD,QAAQ,CAAC,gBAAgB,EAAE,SAAS,eAAe,EAAE,CAAC;IACtD,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,EAAE,SAAS,UAAU,EAAE,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,EAAE,SAAS,eAAe,EAAE,CAAC;IACtD,QAAQ,CAAC,aAAa,EAAE,SAAS,OAAO,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;IAEpE,gDAAgD;IAChD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,uCAAuC;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,sCAAsC;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmD;gBAExE,GAAG,GAAE,OAAwB;IA0DzC,0DAA0D;IAC1D,MAAM,CAAC,QAAQ,IAAI,OAAO;IAI1B,kEAAkE;IAClE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,EAAE;IAIxD,4CAA4C;IAC5C,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,EAAE;IAI/C,2CAA2C;IAC3C,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,EAAE;IAI7C,oEAAoE;IACpE,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,EAAE;IAInD;;;;;;OAMG;IACH,mBAAmB,CAAC,eAAe,EAAE,MAAM,GAAG,QAAQ,EAAE;IAQxD;;;;;;;OAOG;IACH,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,EAAE;IAgB1D;;;OAGG;IACH,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,KAAK,GAAG,eAAe,EAAE;IAI3E;;;;;;;;;;;;OAYG;IACH,QAAQ,CACN,KAAK,EAAE,gBAAgB,GAAG;QACxB,cAAc,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC9D,8DAA8D;QAC9D,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;KAChC,EACD,OAAO,EAAE,aAAa,GACrB,IAAI,EAAE;IAIT;;;;;;;;;;;;OAYG;IACH,iBAAiB,CACf,KAAK,EAAE,gBAAgB,GAAG;QAAE,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,EAC5D,OAAO,EAAE,aAAa,GACrB,IAAI,EAAE;IAIT;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,iBAAiB,CACf,KAAK,EAAE,gBAAgB,GAAG;QACxB,cAAc,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,YAAY,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;KAC/D,EACD,OAAO,EAAE,aAAa,GACrB;QAAE,KAAK,EAAE,aAAa,EAAE,CAAC;QAAC,MAAM,EAAE,kBAAkB,EAAE,CAAA;KAAE;IAqE3D,8DAA8D;IAC9D,OAAO,CAAC,YAAY;IAsCpB,OAAO,CAAC,YAAY;CA6BrB"}
@@ -111,6 +111,46 @@ export class Dataset {
111
111
  weaponsWithKeyword(keywordId) {
112
112
  return (this.weaponsByKeyword.get(keywordId) ?? []).map((w) => new WeaponView(w, this));
113
113
  }
114
+ /**
115
+ * Leaders whose leader-attachment data lists `bodyguardUnitId` among its
116
+ * eligible body units, sorted by name. The attachment is stored on the
117
+ * leader pointing down to its bodyguards, so answering "which leaders can
118
+ * attach to this unit?" means scanning the attachment list. Returns an empty
119
+ * array for a unit that no leader can attach to (including leader units).
120
+ */
121
+ leadersAttachableTo(bodyguardUnitId) {
122
+ return this.leaderAttachments
123
+ .filter((la) => la.eligible_bodyguard_ids.includes(bodyguardUnitId))
124
+ .map((la) => this.units.get(la.leader_id))
125
+ .filter((u) => u !== undefined)
126
+ .sort((a, b) => a.name.localeCompare(b.name));
127
+ }
128
+ /**
129
+ * The inverse of {@link leadersAttachableTo}: the body units the given
130
+ * leader can attach to, sorted by name. Scans the same leader-attachment
131
+ * data from the leader's side (`leader_id` matches; resolve each
132
+ * `eligible_bodyguard_ids` entry), deduped by id. Empty for a non-leader
133
+ * unit. Together the two queries give the bidirectional attachment graph the
134
+ * SPA needs to offer a partner dropdown from either end.
135
+ */
136
+ bodyguardsAttachableFrom(leaderUnitId) {
137
+ const seen = new Set();
138
+ const out = [];
139
+ for (const la of this.leaderAttachments) {
140
+ if (la.leader_id !== leaderUnitId)
141
+ continue;
142
+ for (const bodyguardId of la.eligible_bodyguard_ids) {
143
+ if (seen.has(bodyguardId))
144
+ continue;
145
+ const unit = this.units.get(bodyguardId);
146
+ if (!unit)
147
+ continue;
148
+ seen.add(bodyguardId);
149
+ out.push(unit);
150
+ }
151
+ }
152
+ return out.sort((a, b) => a.name.localeCompare(b.name));
153
+ }
114
154
  /**
115
155
  * Enumerate every ability that could apply to the given unit in `phase`,
116
156
  * grouped by source. The SPA uses this to render the abilities pane.
@@ -122,7 +162,7 @@ export class Dataset {
122
162
  * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:
123
163
  * intrinsic weapon-profile keywords plus every eligible ability whose DSL
124
164
  * effect translates to an attacker-side buff (army, detachment, unit,
125
- * leader, support, plus any stratagems the caller has opted into).
165
+ * attached members, support, plus any stratagems the caller has opted into).
126
166
  *
127
167
  * The result includes only buffs the buff layer can express today — the
128
168
  * `unsupported` half of the DSL→Buff translation is dropped here so callers
@@ -150,25 +190,117 @@ export class Dataset {
150
190
  defensiveBuffsFor(input, context) {
151
191
  return this.collectBuffs(input, context, "target");
152
192
  }
193
+ /**
194
+ * Enumerate every attacker-side buff a unit could stack in `context` as a
195
+ * list of toggleable levers, plus the activation groups that limit them.
196
+ *
197
+ * Unlike {@link buffsFor} — which returns only the buffs that auto-apply —
198
+ * this surfaces the *player decisions* too: stratagems, and the activatable
199
+ * gates the DSL models as dice-pool options, `choice` branches, or
200
+ * timing-gated activations (e.g. Blessings of Khorne's three keyword grants).
201
+ * Each lever carries `enabled` (its default state) and, where it's part of a
202
+ * limited pool, a `group` id whose {@link StackableBuffGroup} caps how many
203
+ * can fire at once. The intended loop:
204
+ *
205
+ * ```ts
206
+ * const { buffs } = ds.stackableBuffsFor(input, ctx);
207
+ * const chosen = buffs.filter(b => b.enabled).flatMap(b => b.buffs);
208
+ * crunch({ ...profiles, buffs: chosen, context: ctx }, ds);
209
+ * ```
210
+ *
211
+ * Target/phase conditions a lever still carries (e.g. "vs Infantry") ride on
212
+ * each buff's `applicableWhen`, so toggling it on is always safe — the
213
+ * resolver gates it per-target.
214
+ */
215
+ stackableBuffsFor(input, context) {
216
+ const buffs = [];
217
+ const groups = new Map();
218
+ // Surface the attachment fact to the DSL translator so `is-attached` /
219
+ // `model-is-leader` conditions can evaluate. Clone — never mutate the
220
+ // caller's context. An explicitly-set flag wins over the derivation.
221
+ const ctx = {
222
+ ...context,
223
+ attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,
224
+ };
225
+ // Intrinsic weapon-profile keywords — always on.
226
+ for (const ref of input.weaponProfiles ?? []) {
227
+ const weapon = this.weapons.get(ref.weaponId);
228
+ if (!weapon)
229
+ continue;
230
+ const wk = weapon.profileBuffs(ref.profileIndex, ctx);
231
+ if (wk.length === 0)
232
+ continue;
233
+ buffs.push({
234
+ id: `weapon:${ref.weaponId}:${ref.profileIndex}`,
235
+ label: `${weapon.name} keywords`,
236
+ buffs: wk,
237
+ enabled: true,
238
+ source: wk[0].source,
239
+ });
240
+ }
241
+ for (const entry of this.eligibleAbilities(input, ctx.phase)) {
242
+ const source = bufferSourceFromEligible(entry);
243
+ const { applied, activatable } = entry.ability.describeBuffs(source, ctx, "attacker");
244
+ // Stratagems cost CP — opt-in, not on by default.
245
+ const isStratagem = entry.source.kind === "detachment-stratagem";
246
+ if (applied.length > 0) {
247
+ buffs.push({
248
+ id: `${entry.source.kind}:${entry.ability.id}`,
249
+ label: entry.ability.name,
250
+ buffs: applied,
251
+ enabled: !isStratagem,
252
+ source,
253
+ });
254
+ }
255
+ for (const act of activatable) {
256
+ let groupId;
257
+ if (act.group) {
258
+ groupId = act.group.id;
259
+ if (!groups.has(groupId)) {
260
+ groups.set(groupId, {
261
+ id: groupId,
262
+ label: entry.ability.name,
263
+ maxActivations: act.group.maxActivations,
264
+ });
265
+ }
266
+ }
267
+ buffs.push({
268
+ id: act.id,
269
+ label: `${entry.ability.name} — ${act.label}`,
270
+ buffs: act.buffs,
271
+ enabled: false,
272
+ source,
273
+ group: groupId,
274
+ });
275
+ }
276
+ }
277
+ return { buffs, groups: [...groups.values()] };
278
+ }
153
279
  /** Shared implementation for buffsFor / defensiveBuffsFor. */
154
280
  collectBuffs(input, context, perspective) {
155
281
  const out = [];
282
+ // Surface the attachment fact to the DSL translator (see stackableBuffsFor).
283
+ // Clone — never mutate the caller's context; explicit flag wins.
284
+ const ctx = {
285
+ ...context,
286
+ attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,
287
+ };
156
288
  // Weapon-profile keywords are attacker-only.
157
289
  if (perspective === "attacker") {
158
290
  for (const ref of input.weaponProfiles ?? []) {
159
291
  const weapon = this.weapons.get(ref.weaponId);
160
292
  if (!weapon)
161
293
  continue;
162
- out.push(...weapon.profileBuffs(ref.profileIndex, context));
294
+ out.push(...weapon.profileBuffs(ref.profileIndex, ctx));
163
295
  }
164
296
  }
165
297
  const optedIn = new Set(input.optedInStratagemIds ?? []);
166
- for (const entry of this.eligibleAbilities(input, context.phase)) {
298
+ for (const entry of this.eligibleAbilities(input, ctx.phase)) {
167
299
  if (entry.source.kind === "detachment-stratagem" && !optedIn.has(entry.source.stratagemId)) {
168
300
  continue;
169
301
  }
170
302
  const source = bufferSourceFromEligible(entry);
171
- out.push(...entry.ability.getBuffs(source, context, perspective));
303
+ out.push(...entry.ability.getBuffs(source, ctx, perspective));
172
304
  }
173
305
  return out;
174
306
  }
@@ -235,8 +367,13 @@ function bufferSourceFromEligible(entry) {
235
367
  return { kind: "ability", abilityId, abilityKind: "detachment-stratagem" };
236
368
  case "unit":
237
369
  return { kind: "ability", abilityId, abilityKind: "unit" };
238
- case "leader":
239
- return { kind: "ability", abilityId, abilityKind: "leader" };
370
+ case "attached":
371
+ return {
372
+ kind: "ability",
373
+ abilityId,
374
+ abilityKind: "attached",
375
+ sourceUnitId: entry.source.unitId,
376
+ };
240
377
  case "support":
241
378
  return { kind: "ability", abilityId, abilityKind: "support" };
242
379
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dataset.js","sourceRoot":"","sources":["../../src/data/dataset.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * {@link Dataset} ties the embedded records together: it owns every\n * {@link Collection}, builds the cross-entity indexes once, and is the `this`\n * the linked views resolve against.\n *\n * @packageDocumentation\n */\nimport type {\n DeploymentPattern,\n Detachment,\n Enhancement,\n ForceDisposition,\n GameVersion,\n InteractionFlag,\n LeaderAttachment,\n Mission,\n MissionMatchup,\n Phase,\n ResourcePool,\n SecondaryCard,\n Stratagem,\n TimingFlag,\n Unit,\n UnitComposition,\n WargearOption,\n WeaponKeyword,\n} from \"../generated.js\";\nimport { Collection } from \"./collection.js\";\nimport {\n AbilityView,\n FactionView,\n UnitView,\n WeaponKeywordView,\n WeaponView,\n} from \"./entities.js\";\nimport { emptyRawData, type RawData } from \"./types.js\";\nimport { RAW_DATA } from \"./bundle.generated.js\";\nimport type { Buff, BuffSource, EngineContext } from \"../cruncher/buffs.js\";\nimport {\n resolveEligibleAbilities,\n type EligibilityInput,\n type EligibleAbility,\n} from \"../abilities-resolver/index.js\";\n\n/** The whole dataset, with linked accessors over every entity collection. */\nexport class Dataset {\n // Richly-linked collections.\n readonly units: Collection<Unit, UnitView>;\n readonly weapons: Collection<RawData[\"weapons\"][number], WeaponView>;\n readonly weaponKeywords: Collection<WeaponKeyword, WeaponKeywordView>;\n readonly factions: Collection<RawData[\"factions\"][number], FactionView>;\n readonly abilities: Collection<RawData[\"abilities\"][number], AbilityView>;\n\n // Id-bearing collections without bespoke views (records returned as-is).\n readonly detachments: Collection<Detachment, Detachment>;\n readonly enhancements: Collection<Enhancement, Enhancement>;\n readonly stratagems: Collection<Stratagem, Stratagem>;\n readonly wargearOptions: Collection<WargearOption, WargearOption>;\n readonly missions: Collection<Mission, Mission>;\n readonly missionMatchups: Collection<MissionMatchup, MissionMatchup>;\n readonly secondaryCards: Collection<SecondaryCard, SecondaryCard>;\n readonly deploymentPatterns: Collection<DeploymentPattern, DeploymentPattern>;\n readonly forceDispositions: Collection<ForceDisposition, ForceDisposition>;\n readonly resourcePools: Collection<ResourcePool, ResourcePool>;\n\n // Id-less collections, exposed as plain arrays.\n readonly leaderAttachments: readonly LeaderAttachment[];\n readonly unitCompositions: readonly UnitComposition[];\n readonly gameVersions: readonly GameVersion[];\n readonly timingFlags: readonly TimingFlag[];\n readonly interactionFlags: readonly InteractionFlag[];\n readonly phaseMappings: readonly RawData[\"phaseMappings\"][number][];\n\n /** `source_type:source_id` → unioned phases. */\n private readonly phaseIndex = new Map<string, Phase[]>();\n /** ability id → units that list it. */\n private readonly unitsByAbility = new Map<string, Unit[]>();\n /** weapon id → units that list it. */\n private readonly unitsByWeapon = new Map<string, Unit[]>();\n /** weapon-keyword id → weapons whose profiles reference it. */\n private readonly weaponsByKeyword = new Map<string, RawData[\"weapons\"][number][]>();\n\n constructor(raw: RawData = emptyRawData()) {\n this.units = new Collection({\n items: raw.units,\n idOf: (u) => u.id,\n // The same unit id is shared across factions (e.g. ministorum-priest);\n // keep each faction's copy, collapse only true within-faction duplicates.\n dedupeKeyOf: (u) => `${u.faction_id}::${u.id}`,\n nameOf: (u) => u.name,\n factionOf: (u) => u.faction_id,\n wrap: (u) => new UnitView(u, this),\n });\n this.weapons = new Collection({\n items: raw.weapons,\n idOf: (w) => w.id,\n nameOf: (w) => w.name,\n wrap: (w) => new WeaponView(w, this),\n });\n this.weaponKeywords = new Collection({\n items: raw.weaponKeywords,\n idOf: (k) => k.id,\n nameOf: (k) => k.name,\n wrap: (k) => new WeaponKeywordView(k, this),\n });\n this.factions = new Collection({\n items: raw.factions,\n idOf: (f) => f.id,\n nameOf: (f) => f.name,\n wrap: (f) => new FactionView(f, this),\n });\n this.abilities = new Collection({\n items: raw.abilities,\n idOf: (a) => a.ability_id,\n nameOf: (a) => a.name,\n factionOf: (a) => a.faction_id,\n wrap: (a) => new AbilityView(a, this),\n });\n\n this.detachments = idCollection(raw.detachments, (d) => d.faction_id);\n this.enhancements = idCollection(raw.enhancements);\n this.stratagems = idCollection(raw.stratagems);\n this.wargearOptions = idCollection(raw.wargearOptions);\n this.missions = idCollection(raw.missions);\n this.missionMatchups = idCollection(raw.missionMatchups);\n this.secondaryCards = idCollection(raw.secondaryCards);\n this.deploymentPatterns = idCollection(raw.deploymentPatterns);\n this.forceDispositions = idCollection(raw.forceDispositions);\n this.resourcePools = idCollection(raw.resourcePools);\n\n this.leaderAttachments = raw.leaderAttachments;\n this.unitCompositions = raw.unitCompositions;\n this.gameVersions = raw.gameVersions;\n this.timingFlags = raw.timingFlags;\n this.interactionFlags = raw.interactionFlags;\n this.phaseMappings = raw.phaseMappings;\n\n this.buildIndexes(raw);\n }\n\n /** The dataset built from the package's embedded data. */\n static embedded(): Dataset {\n return new Dataset(RAW_DATA);\n }\n\n /** Phases a source acts in, unioned across its phase-mappings. */\n phasesFor(sourceType: string, sourceId: string): Phase[] {\n return this.phaseIndex.get(`${sourceType}:${sourceId}`) ?? [];\n }\n\n /** Units that list the given ability id. */\n unitsWithAbility(abilityId: string): UnitView[] {\n return (this.unitsByAbility.get(abilityId) ?? []).map((u) => new UnitView(u, this));\n }\n\n /** Units that list the given weapon id. */\n unitsWithWeapon(weaponId: string): UnitView[] {\n return (this.unitsByWeapon.get(weaponId) ?? []).map((u) => new UnitView(u, this));\n }\n\n /** Weapons whose profiles reference the given weapon-keyword id. */\n weaponsWithKeyword(keywordId: string): WeaponView[] {\n return (this.weaponsByKeyword.get(keywordId) ?? []).map((w) => new WeaponView(w, this));\n }\n\n /**\n * Enumerate every ability that could apply to the given unit in `phase`,\n * grouped by source. The SPA uses this to render the abilities pane.\n */\n eligibleAbilities(input: EligibilityInput, phase: Phase): EligibleAbility[] {\n return resolveEligibleAbilities(this, input, phase);\n }\n\n /**\n * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:\n * intrinsic weapon-profile keywords plus every eligible ability whose DSL\n * effect translates to an attacker-side buff (army, detachment, unit,\n * leader, support, plus any stratagems the caller has opted into).\n *\n * The result includes only buffs the buff layer can express today — the\n * `unsupported` half of the DSL→Buff translation is dropped here so callers\n * who just want the stack don't need to thread diagnostics through. Use\n * {@link AbilityView.describeBuffs} when you need the diagnostics for an\n * individual ability. Symmetric to {@link defensiveBuffsFor}, which walks\n * the same eligibility set under target perspective.\n */\n buffsFor(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n /** Stratagem ids the caller has opted into spending CP on. */\n optedInStratagemIds?: string[];\n },\n context: EngineContext,\n ): Buff[] {\n return this.collectBuffs(input, context, \"attacker\");\n }\n\n /**\n * Defender-perspective buff stack for the chosen unit: walks the same\n * eligible-abilities set as {@link buffsFor} but translates each ability's\n * DSL effect as defensive (FNP, save mods from `stat-modifier Sv`,\n * toughness mods from `stat-modifier T`, save rerolls, incoming hit\n * penalties from `bs-modifier`). Use this when the chosen unit is being\n * crunched as the *target* — the engine reads `feelNoPain`/`saveMod`/\n * `toughnessMod` out of `resolveBuffs` so wiring the result into `crunch`\n * just means concatenating onto the existing `buffs` array.\n *\n * `weaponProfiles` are ignored under target perspective — weapon-keyword\n * effects ride with the firing weapon, not the receiving unit.\n */\n defensiveBuffsFor(\n input: EligibilityInput & { optedInStratagemIds?: string[] },\n context: EngineContext,\n ): Buff[] {\n return this.collectBuffs(input, context, \"target\");\n }\n\n /** Shared implementation for buffsFor / defensiveBuffsFor. */\n private collectBuffs(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n optedInStratagemIds?: string[];\n },\n context: EngineContext,\n perspective: \"attacker\" | \"target\",\n ): Buff[] {\n const out: Buff[] = [];\n\n // Weapon-profile keywords are attacker-only.\n if (perspective === \"attacker\") {\n for (const ref of input.weaponProfiles ?? []) {\n const weapon = this.weapons.get(ref.weaponId);\n if (!weapon) continue;\n out.push(...weapon.profileBuffs(ref.profileIndex, context));\n }\n }\n\n const optedIn = new Set(input.optedInStratagemIds ?? []);\n for (const entry of this.eligibleAbilities(input, context.phase)) {\n if (entry.source.kind === \"detachment-stratagem\" && !optedIn.has(entry.source.stratagemId)) {\n continue;\n }\n const source = bufferSourceFromEligible(entry);\n out.push(...entry.ability.getBuffs(source, context, perspective));\n }\n\n return out;\n }\n\n private buildIndexes(raw: RawData): void {\n for (const pm of raw.phaseMappings) {\n const key = `${pm.source_type}:${pm.source_id}`;\n const existing = this.phaseIndex.get(key) ?? [];\n for (const phase of pm.phases) {\n if (!existing.includes(phase)) existing.push(phase);\n }\n this.phaseIndex.set(key, existing);\n }\n for (const unit of raw.units) {\n for (const abilityId of unit.ability_ids ?? []) push(this.unitsByAbility, abilityId, unit);\n for (const weaponId of unit.weapon_ids ?? []) push(this.unitsByWeapon, weaponId, unit);\n }\n const seenByKeyword = new Map<string, Set<string>>();\n for (const weapon of raw.weapons) {\n for (const profile of weapon.profiles) {\n for (const ref of profile.keywords ?? []) {\n let seen = seenByKeyword.get(ref.keyword_id);\n if (!seen) {\n seen = new Set();\n seenByKeyword.set(ref.keyword_id, seen);\n }\n if (seen.has(weapon.id)) continue;\n seen.add(weapon.id);\n push(this.weaponsByKeyword, ref.keyword_id, weapon);\n }\n }\n }\n }\n}\n\n/** Build a passthrough collection for an id-bearing record type. */\nfunction idCollection<T extends { id: string }>(\n items: T[],\n factionOf?: (item: T) => string | null | undefined,\n): Collection<T, T> {\n return new Collection<T, T>({\n items,\n idOf: (i) => i.id,\n nameOf: (i) => (i as { name?: string }).name,\n factionOf,\n wrap: (i) => i,\n });\n}\n\nfunction push<T>(map: Map<string, T[]>, key: string, value: T): void {\n const existing = map.get(key);\n if (existing) existing.push(value);\n else map.set(key, [value]);\n}\n\n/** Map an EligibleAbility back to the BuffSource the translator expects. */\nfunction bufferSourceFromEligible(entry: EligibleAbility): BuffSource {\n const abilityId = entry.ability.id;\n switch (entry.source.kind) {\n case \"army\":\n return { kind: \"ability\", abilityId, abilityKind: \"army\" };\n case \"detachment\":\n return { kind: \"ability\", abilityId, abilityKind: \"detachment\" };\n case \"detachment-stratagem\":\n return { kind: \"ability\", abilityId, abilityKind: \"detachment-stratagem\" };\n case \"unit\":\n return { kind: \"ability\", abilityId, abilityKind: \"unit\" };\n case \"leader\":\n return { kind: \"ability\", abilityId, abilityKind: \"leader\" };\n case \"support\":\n return { kind: \"ability\", abilityId, abilityKind: \"support\" };\n }\n}\n"]}
1
+ {"version":3,"file":"dataset.js","sourceRoot":"","sources":["../../src/data/dataset.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * {@link Dataset} ties the embedded records together: it owns every\n * {@link Collection}, builds the cross-entity indexes once, and is the `this`\n * the linked views resolve against.\n *\n * @packageDocumentation\n */\nimport type {\n DeploymentPattern,\n Detachment,\n Enhancement,\n ForceDisposition,\n GameVersion,\n InteractionFlag,\n LeaderAttachment,\n Mission,\n MissionMatchup,\n Phase,\n ResourcePool,\n SecondaryCard,\n Stratagem,\n TimingFlag,\n Unit,\n UnitComposition,\n WargearOption,\n WeaponKeyword,\n} from \"../generated.js\";\nimport { Collection } from \"./collection.js\";\nimport {\n AbilityView,\n FactionView,\n UnitView,\n WeaponKeywordView,\n WeaponView,\n} from \"./entities.js\";\nimport { emptyRawData, type RawData } from \"./types.js\";\nimport { RAW_DATA } from \"./bundle.generated.js\";\nimport type { Buff, BuffSource, EngineContext } from \"../cruncher/buffs.js\";\nimport {\n resolveEligibleAbilities,\n type EligibilityInput,\n type EligibleAbility,\n} from \"../abilities-resolver/index.js\";\n\n/**\n * One toggleable buff lever for damage analysis: the contributions it adds and\n * whether it's on by default. `enabled` is `true` for buffs that always apply\n * (intrinsic keywords, unconditional abilities) and `false` for player\n * decisions — stratagems (CP cost) and activatable gates (dice-pool options,\n * `choice` branches, timing-gated activations). A consumer flips `enabled`,\n * then crunches the enabled subset; an optimizer searches it.\n *\n * @see {@link Dataset.stackableBuffsFor}\n */\nexport type StackableBuff = {\n /** Stable toggle id (stable across re-enumeration of the same input). */\n id: string;\n /** Human label for the lever. */\n label: string;\n /** Contributions this lever adds when enabled (≥1). */\n buffs: Buff[];\n /** Default selection state. */\n enabled: boolean;\n /** Where the lever came from. */\n source: BuffSource;\n /** Id of the mutually-limited {@link StackableBuffGroup} this belongs to, if any. */\n group?: string;\n};\n\n/** A pool of {@link StackableBuff} levers limited to `maxActivations` at once. */\nexport type StackableBuffGroup = {\n id: string;\n label: string;\n maxActivations: number;\n};\n\n/** The whole dataset, with linked accessors over every entity collection. */\nexport class Dataset {\n // Richly-linked collections.\n readonly units: Collection<Unit, UnitView>;\n readonly weapons: Collection<RawData[\"weapons\"][number], WeaponView>;\n readonly weaponKeywords: Collection<WeaponKeyword, WeaponKeywordView>;\n readonly factions: Collection<RawData[\"factions\"][number], FactionView>;\n readonly abilities: Collection<RawData[\"abilities\"][number], AbilityView>;\n\n // Id-bearing collections without bespoke views (records returned as-is).\n readonly detachments: Collection<Detachment, Detachment>;\n readonly enhancements: Collection<Enhancement, Enhancement>;\n readonly stratagems: Collection<Stratagem, Stratagem>;\n readonly wargearOptions: Collection<WargearOption, WargearOption>;\n readonly missions: Collection<Mission, Mission>;\n readonly missionMatchups: Collection<MissionMatchup, MissionMatchup>;\n readonly secondaryCards: Collection<SecondaryCard, SecondaryCard>;\n readonly deploymentPatterns: Collection<DeploymentPattern, DeploymentPattern>;\n readonly forceDispositions: Collection<ForceDisposition, ForceDisposition>;\n readonly resourcePools: Collection<ResourcePool, ResourcePool>;\n\n // Id-less collections, exposed as plain arrays.\n readonly leaderAttachments: readonly LeaderAttachment[];\n readonly unitCompositions: readonly UnitComposition[];\n readonly gameVersions: readonly GameVersion[];\n readonly timingFlags: readonly TimingFlag[];\n readonly interactionFlags: readonly InteractionFlag[];\n readonly phaseMappings: readonly RawData[\"phaseMappings\"][number][];\n\n /** `source_type:source_id` → unioned phases. */\n private readonly phaseIndex = new Map<string, Phase[]>();\n /** ability id → units that list it. */\n private readonly unitsByAbility = new Map<string, Unit[]>();\n /** weapon id → units that list it. */\n private readonly unitsByWeapon = new Map<string, Unit[]>();\n /** weapon-keyword id → weapons whose profiles reference it. */\n private readonly weaponsByKeyword = new Map<string, RawData[\"weapons\"][number][]>();\n\n constructor(raw: RawData = emptyRawData()) {\n this.units = new Collection({\n items: raw.units,\n idOf: (u) => u.id,\n // The same unit id is shared across factions (e.g. ministorum-priest);\n // keep each faction's copy, collapse only true within-faction duplicates.\n dedupeKeyOf: (u) => `${u.faction_id}::${u.id}`,\n nameOf: (u) => u.name,\n factionOf: (u) => u.faction_id,\n wrap: (u) => new UnitView(u, this),\n });\n this.weapons = new Collection({\n items: raw.weapons,\n idOf: (w) => w.id,\n nameOf: (w) => w.name,\n wrap: (w) => new WeaponView(w, this),\n });\n this.weaponKeywords = new Collection({\n items: raw.weaponKeywords,\n idOf: (k) => k.id,\n nameOf: (k) => k.name,\n wrap: (k) => new WeaponKeywordView(k, this),\n });\n this.factions = new Collection({\n items: raw.factions,\n idOf: (f) => f.id,\n nameOf: (f) => f.name,\n wrap: (f) => new FactionView(f, this),\n });\n this.abilities = new Collection({\n items: raw.abilities,\n idOf: (a) => a.ability_id,\n nameOf: (a) => a.name,\n factionOf: (a) => a.faction_id,\n wrap: (a) => new AbilityView(a, this),\n });\n\n this.detachments = idCollection(raw.detachments, (d) => d.faction_id);\n this.enhancements = idCollection(raw.enhancements);\n this.stratagems = idCollection(raw.stratagems);\n this.wargearOptions = idCollection(raw.wargearOptions);\n this.missions = idCollection(raw.missions);\n this.missionMatchups = idCollection(raw.missionMatchups);\n this.secondaryCards = idCollection(raw.secondaryCards);\n this.deploymentPatterns = idCollection(raw.deploymentPatterns);\n this.forceDispositions = idCollection(raw.forceDispositions);\n this.resourcePools = idCollection(raw.resourcePools);\n\n this.leaderAttachments = raw.leaderAttachments;\n this.unitCompositions = raw.unitCompositions;\n this.gameVersions = raw.gameVersions;\n this.timingFlags = raw.timingFlags;\n this.interactionFlags = raw.interactionFlags;\n this.phaseMappings = raw.phaseMappings;\n\n this.buildIndexes(raw);\n }\n\n /** The dataset built from the package's embedded data. */\n static embedded(): Dataset {\n return new Dataset(RAW_DATA);\n }\n\n /** Phases a source acts in, unioned across its phase-mappings. */\n phasesFor(sourceType: string, sourceId: string): Phase[] {\n return this.phaseIndex.get(`${sourceType}:${sourceId}`) ?? [];\n }\n\n /** Units that list the given ability id. */\n unitsWithAbility(abilityId: string): UnitView[] {\n return (this.unitsByAbility.get(abilityId) ?? []).map((u) => new UnitView(u, this));\n }\n\n /** Units that list the given weapon id. */\n unitsWithWeapon(weaponId: string): UnitView[] {\n return (this.unitsByWeapon.get(weaponId) ?? []).map((u) => new UnitView(u, this));\n }\n\n /** Weapons whose profiles reference the given weapon-keyword id. */\n weaponsWithKeyword(keywordId: string): WeaponView[] {\n return (this.weaponsByKeyword.get(keywordId) ?? []).map((w) => new WeaponView(w, this));\n }\n\n /**\n * Leaders whose leader-attachment data lists `bodyguardUnitId` among its\n * eligible body units, sorted by name. The attachment is stored on the\n * leader pointing down to its bodyguards, so answering \"which leaders can\n * attach to this unit?\" means scanning the attachment list. Returns an empty\n * array for a unit that no leader can attach to (including leader units).\n */\n leadersAttachableTo(bodyguardUnitId: string): UnitView[] {\n return this.leaderAttachments\n .filter((la) => la.eligible_bodyguard_ids.includes(bodyguardUnitId))\n .map((la) => this.units.get(la.leader_id))\n .filter((u): u is UnitView => u !== undefined)\n .sort((a, b) => a.name.localeCompare(b.name));\n }\n\n /**\n * The inverse of {@link leadersAttachableTo}: the body units the given\n * leader can attach to, sorted by name. Scans the same leader-attachment\n * data from the leader's side (`leader_id` matches; resolve each\n * `eligible_bodyguard_ids` entry), deduped by id. Empty for a non-leader\n * unit. Together the two queries give the bidirectional attachment graph the\n * SPA needs to offer a partner dropdown from either end.\n */\n bodyguardsAttachableFrom(leaderUnitId: string): UnitView[] {\n const seen = new Set<string>();\n const out: UnitView[] = [];\n for (const la of this.leaderAttachments) {\n if (la.leader_id !== leaderUnitId) continue;\n for (const bodyguardId of la.eligible_bodyguard_ids) {\n if (seen.has(bodyguardId)) continue;\n const unit = this.units.get(bodyguardId);\n if (!unit) continue;\n seen.add(bodyguardId);\n out.push(unit);\n }\n }\n return out.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n /**\n * Enumerate every ability that could apply to the given unit in `phase`,\n * grouped by source. The SPA uses this to render the abilities pane.\n */\n eligibleAbilities(input: EligibilityInput, phase: Phase): EligibleAbility[] {\n return resolveEligibleAbilities(this, input, phase);\n }\n\n /**\n * Attacker-perspective {@link Buff} stack for a (unit, phase) combination:\n * intrinsic weapon-profile keywords plus every eligible ability whose DSL\n * effect translates to an attacker-side buff (army, detachment, unit,\n * attached members, support, plus any stratagems the caller has opted into).\n *\n * The result includes only buffs the buff layer can express today — the\n * `unsupported` half of the DSL→Buff translation is dropped here so callers\n * who just want the stack don't need to thread diagnostics through. Use\n * {@link AbilityView.describeBuffs} when you need the diagnostics for an\n * individual ability. Symmetric to {@link defensiveBuffsFor}, which walks\n * the same eligibility set under target perspective.\n */\n buffsFor(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n /** Stratagem ids the caller has opted into spending CP on. */\n optedInStratagemIds?: string[];\n },\n context: EngineContext,\n ): Buff[] {\n return this.collectBuffs(input, context, \"attacker\");\n }\n\n /**\n * Defender-perspective buff stack for the chosen unit: walks the same\n * eligible-abilities set as {@link buffsFor} but translates each ability's\n * DSL effect as defensive (FNP, save mods from `stat-modifier Sv`,\n * toughness mods from `stat-modifier T`, save rerolls, incoming hit\n * penalties from `bs-modifier`). Use this when the chosen unit is being\n * crunched as the *target* — the engine reads `feelNoPain`/`saveMod`/\n * `toughnessMod` out of `resolveBuffs` so wiring the result into `crunch`\n * just means concatenating onto the existing `buffs` array.\n *\n * `weaponProfiles` are ignored under target perspective — weapon-keyword\n * effects ride with the firing weapon, not the receiving unit.\n */\n defensiveBuffsFor(\n input: EligibilityInput & { optedInStratagemIds?: string[] },\n context: EngineContext,\n ): Buff[] {\n return this.collectBuffs(input, context, \"target\");\n }\n\n /**\n * Enumerate every attacker-side buff a unit could stack in `context` as a\n * list of toggleable levers, plus the activation groups that limit them.\n *\n * Unlike {@link buffsFor} — which returns only the buffs that auto-apply —\n * this surfaces the *player decisions* too: stratagems, and the activatable\n * gates the DSL models as dice-pool options, `choice` branches, or\n * timing-gated activations (e.g. Blessings of Khorne's three keyword grants).\n * Each lever carries `enabled` (its default state) and, where it's part of a\n * limited pool, a `group` id whose {@link StackableBuffGroup} caps how many\n * can fire at once. The intended loop:\n *\n * ```ts\n * const { buffs } = ds.stackableBuffsFor(input, ctx);\n * const chosen = buffs.filter(b => b.enabled).flatMap(b => b.buffs);\n * crunch({ ...profiles, buffs: chosen, context: ctx }, ds);\n * ```\n *\n * Target/phase conditions a lever still carries (e.g. \"vs Infantry\") ride on\n * each buff's `applicableWhen`, so toggling it on is always safe — the\n * resolver gates it per-target.\n */\n stackableBuffsFor(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n },\n context: EngineContext,\n ): { buffs: StackableBuff[]; groups: StackableBuffGroup[] } {\n const buffs: StackableBuff[] = [];\n const groups = new Map<string, StackableBuffGroup>();\n\n // Surface the attachment fact to the DSL translator so `is-attached` /\n // `model-is-leader` conditions can evaluate. Clone — never mutate the\n // caller's context. An explicitly-set flag wins over the derivation.\n const ctx: EngineContext = {\n ...context,\n attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,\n };\n\n // Intrinsic weapon-profile keywords — always on.\n for (const ref of input.weaponProfiles ?? []) {\n const weapon = this.weapons.get(ref.weaponId);\n if (!weapon) continue;\n const wk = weapon.profileBuffs(ref.profileIndex, ctx);\n if (wk.length === 0) continue;\n buffs.push({\n id: `weapon:${ref.weaponId}:${ref.profileIndex}`,\n label: `${weapon.name} keywords`,\n buffs: wk,\n enabled: true,\n source: wk[0].source,\n });\n }\n\n for (const entry of this.eligibleAbilities(input, ctx.phase)) {\n const source = bufferSourceFromEligible(entry);\n const { applied, activatable } = entry.ability.describeBuffs(source, ctx, \"attacker\");\n // Stratagems cost CP — opt-in, not on by default.\n const isStratagem = entry.source.kind === \"detachment-stratagem\";\n\n if (applied.length > 0) {\n buffs.push({\n id: `${entry.source.kind}:${entry.ability.id}`,\n label: entry.ability.name,\n buffs: applied,\n enabled: !isStratagem,\n source,\n });\n }\n\n for (const act of activatable) {\n let groupId: string | undefined;\n if (act.group) {\n groupId = act.group.id;\n if (!groups.has(groupId)) {\n groups.set(groupId, {\n id: groupId,\n label: entry.ability.name,\n maxActivations: act.group.maxActivations,\n });\n }\n }\n buffs.push({\n id: act.id,\n label: `${entry.ability.name} — ${act.label}`,\n buffs: act.buffs,\n enabled: false,\n source,\n group: groupId,\n });\n }\n }\n\n return { buffs, groups: [...groups.values()] };\n }\n\n /** Shared implementation for buffsFor / defensiveBuffsFor. */\n private collectBuffs(\n input: EligibilityInput & {\n weaponProfiles?: { weaponId: string; profileIndex: number }[];\n optedInStratagemIds?: string[];\n },\n context: EngineContext,\n perspective: \"attacker\" | \"target\",\n ): Buff[] {\n const out: Buff[] = [];\n\n // Surface the attachment fact to the DSL translator (see stackableBuffsFor).\n // Clone — never mutate the caller's context; explicit flag wins.\n const ctx: EngineContext = {\n ...context,\n attackerAttached: context.attackerAttached ?? (input.attachedUnitIds?.length ?? 0) > 0,\n };\n\n // Weapon-profile keywords are attacker-only.\n if (perspective === \"attacker\") {\n for (const ref of input.weaponProfiles ?? []) {\n const weapon = this.weapons.get(ref.weaponId);\n if (!weapon) continue;\n out.push(...weapon.profileBuffs(ref.profileIndex, ctx));\n }\n }\n\n const optedIn = new Set(input.optedInStratagemIds ?? []);\n for (const entry of this.eligibleAbilities(input, ctx.phase)) {\n if (entry.source.kind === \"detachment-stratagem\" && !optedIn.has(entry.source.stratagemId)) {\n continue;\n }\n const source = bufferSourceFromEligible(entry);\n out.push(...entry.ability.getBuffs(source, ctx, perspective));\n }\n\n return out;\n }\n\n private buildIndexes(raw: RawData): void {\n for (const pm of raw.phaseMappings) {\n const key = `${pm.source_type}:${pm.source_id}`;\n const existing = this.phaseIndex.get(key) ?? [];\n for (const phase of pm.phases) {\n if (!existing.includes(phase)) existing.push(phase);\n }\n this.phaseIndex.set(key, existing);\n }\n for (const unit of raw.units) {\n for (const abilityId of unit.ability_ids ?? []) push(this.unitsByAbility, abilityId, unit);\n for (const weaponId of unit.weapon_ids ?? []) push(this.unitsByWeapon, weaponId, unit);\n }\n const seenByKeyword = new Map<string, Set<string>>();\n for (const weapon of raw.weapons) {\n for (const profile of weapon.profiles) {\n for (const ref of profile.keywords ?? []) {\n let seen = seenByKeyword.get(ref.keyword_id);\n if (!seen) {\n seen = new Set();\n seenByKeyword.set(ref.keyword_id, seen);\n }\n if (seen.has(weapon.id)) continue;\n seen.add(weapon.id);\n push(this.weaponsByKeyword, ref.keyword_id, weapon);\n }\n }\n }\n }\n}\n\n/** Build a passthrough collection for an id-bearing record type. */\nfunction idCollection<T extends { id: string }>(\n items: T[],\n factionOf?: (item: T) => string | null | undefined,\n): Collection<T, T> {\n return new Collection<T, T>({\n items,\n idOf: (i) => i.id,\n nameOf: (i) => (i as { name?: string }).name,\n factionOf,\n wrap: (i) => i,\n });\n}\n\nfunction push<T>(map: Map<string, T[]>, key: string, value: T): void {\n const existing = map.get(key);\n if (existing) existing.push(value);\n else map.set(key, [value]);\n}\n\n/** Map an EligibleAbility back to the BuffSource the translator expects. */\nfunction bufferSourceFromEligible(entry: EligibleAbility): BuffSource {\n const abilityId = entry.ability.id;\n switch (entry.source.kind) {\n case \"army\":\n return { kind: \"ability\", abilityId, abilityKind: \"army\" };\n case \"detachment\":\n return { kind: \"ability\", abilityId, abilityKind: \"detachment\" };\n case \"detachment-stratagem\":\n return { kind: \"ability\", abilityId, abilityKind: \"detachment-stratagem\" };\n case \"unit\":\n return { kind: \"ability\", abilityId, abilityKind: \"unit\" };\n case \"attached\":\n return {\n kind: \"ability\",\n abilityId,\n abilityKind: \"attached\",\n sourceUnitId: entry.source.unitId,\n };\n case \"support\":\n return { kind: \"ability\", abilityId, abilityKind: \"support\" };\n }\n}\n"]}
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { AbilityDSLEntry, Faction, Phase, Unit, Weapon, WeaponKeyword } from "../generated.js";
9
9
  import type { Buff, BuffSource, EngineContext } from "../cruncher/buffs.js";
10
- import { type TranslationPerspective, type UnsupportedFragment } from "../cruncher/from-dsl.js";
10
+ import { type EffectTranslation, type TranslationPerspective } from "../cruncher/from-dsl.js";
11
11
  import type { Dataset } from "./dataset.js";
12
12
  /** A unit, linked to its faction, weapons, and abilities. */
13
13
  export declare class UnitView {
@@ -70,10 +70,7 @@ export declare class AbilityView {
70
70
  * fragments the buff layer can't model. The SPA renders these as warnings
71
71
  * so users see which abilities have effects that need a manual toggle.
72
72
  */
73
- describeBuffs(source: BuffSource, context?: EngineContext, perspective?: TranslationPerspective): {
74
- applied: Buff[];
75
- unsupported: UnsupportedFragment[];
76
- };
73
+ describeBuffs(source: BuffSource, context?: EngineContext, perspective?: TranslationPerspective): EffectTranslation;
77
74
  }
78
75
  /** A weapon, linked to the units that carry it. */
79
76
  export declare class WeaponView {
@@ -1 +1 @@
1
- {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/data/entities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/data/entities.ts"],"names":[],"mappings":"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"}