import { XMLParser } from "fast-xml-parser"; import type { Unit, UnitSize, Detachment, Enhancement, CrusadeHonour, KillTeamOperative } from "../types.js"; declare const parser: XMLParser; declare function ensureArray(val: T | T[] | undefined | null): T[]; /** * Recursively reshape a parsed BSData 11e JSON node into the same shape * fast-xml-parser produces for 10e XML, so every extractor in this file * (parseEntryNode, parseDetachments, parseEnhancements, collectAllProfiles, * etc.) can run against it unchanged: known attribute-like keys get an * `"@_"`-prefixed string alias, `"$text"` becomes `"#text"`, and known * flat-array containers get re-wrapped into the XML's nested plural/singular * shape. */ export declare function normalizeJsonNode(node: any): any; declare function extractFaction(catalogueName: string): string; /** * Many named abilities (Deep Strike, Infiltrators, Scouts, Lone Operative, * Feel No Pain, Leader, faction ones like Oath of Moment or Dark Pacts...) * aren't given to a unit as an inline — they're referenced via an * pointing at the shared rule text (in the game * system file or a catalogue's own rules/sharedRules), the same way a * Detachment's ability can be inline or an infoLink (see * extractDetachmentAbility). Without resolving these, any unit whose * abilities are ALL infoLink-based has an empty abilities list, and one * whose abilities are a mix (e.g. Chosen's inline inline abilities plus its * "Dark Pacts" infoLink) is silently missing the linked ones. * * A second, parallel indirection exists alongside it: , * pointing at a *profile* (in a catalogue's own top-level profiles/sharedProfiles, * or the game system's) rather than a . Confirmed via BSData/wh40k-10e: * Adeptus Custodes' Custodian Guard references its wargear-linked "Praesidium * Shield" and "Vexilla" abilities this way, and Tyranids' Hive Tyrant * references "Will of the Hive Mind" the same way — both were silently * dropped entirely before this was handled, since only `type="rule"` was * ever resolved. Only kept when the target's own typeId is Abilities-typed * (ABILITY_TYPE_ID) — some `type="profile"` infoLinks point at a plain stat * profile instead (e.g. a battle-damage-tier "Damaged: X Wounds Remaining" * profile), which isn't an ability and shouldn't be surfaced as one. * * Returns synthetic profile-shaped objects (matching what extractAbilities * expects) so callers can just concatenate them into the profiles array * rather than threading a second parallel "abilities" collection everywhere. * * Only resolved on entries whose own type is "unit" or "model" — i.e. * genuine datasheet/model slots (Chosen itself; a champion sub-model like * "Chosen Champion"). Weapon items are always type "upgrade" in BSData, and * carry this exact same infoLink shape for their own weapon-ability * keywords (e.g. a Combi-weapon's infoLinks to "Anti", "Devastating * Wounds", "Rapid Fire" — already surfaced via that profile's own Keywords * characteristic/parseWeaponKeywords). Checking the entry's own profile * type isn't enough to exclude these: some weapon items (e.g. a "Plasma * pistol" wrapper choosing between standard/supercharge firing modes) carry * their shared keywords like Pistol/Hazardous on the type="upgrade" wrapper * while the actual Ranged/Melee weapon profiles live one level down on its * child entries — without the type check those wrapper-level infoLinks were * misfiled as unit-level abilities instead. * * This also rules out extending profile-type resolution to type="upgrade" * entries in general (tried and reverted): a Crucible/made-to-order * character (e.g. Space Marines' "Champion of the Chapter [Crucible]") * reaches a shared "Abilities" entryLinkGroup — a pick-one-of-many menu of * every chapter's signature wargear ability (Ravenwing Bike, Thunderwolf * Mount, Death Company gear, ...) — the same way Adeptus Custodes' Vexilla * standard reaches its own single granted ability: both are `type="upgrade"` * entries with their own `type="profile"` infoLink. Allowing type="upgrade" * broadly resolved the menu's every option at once (one unit's ability count * went from ~6 to 42, mixing in other chapters' abilities wholesale) — the * same class of bug UNIVERSAL_OPTION_POOL_LINK_NAME_PATTERN exists to catch, * just under an entryLink named plain "Abilities" that doesn't match that * denylist. Vexilla itself stays unresolved as a result (a narrower, known * gap) rather than risk that regression. */ declare function ruleLinksToAbilityProfiles(entry: any, ruleIndex: Map, profileIndex?: Map): any[]; /** * Collect all profiles from an entry's own profiles plus every descendant * selectionEntry/selectionEntryGroup, at any nesting depth — e.g. weapon * profiles for units like Obliterators live 3 levels down (unit -> model * sub-entry -> "Wargear" group -> weapon entry -> profiles), which a * shallow 2-level-only traversal misses entirely. Does not follow * entryLinks (references to shared content elsewhere by id) — that's a * separate concern handled by collectAllProfilesWithGlobalLinks in * fetch-data.ts, which calls this for the inline tree first. * * `ruleIndex`, if given, also resolves each level's rule-type infoLinks * (see ruleLinksToAbilityProfiles) into the returned profile list. `profileIndex`, * if given, additionally resolves profile-type infoLinks the same way. */ declare function collectAllProfiles(entry: any, ruleIndex?: Map, profileIndex?: Map): any[]; /** * Derive a unit's model-count range by summing the count contribution of * each of its direct child selectionEntries/selectionEntryGroups — each * represents one named model slot or troop-type block (e.g. a mandatory * "Aspiring Champion" contributing exactly 1, plus a "4-9 Legionaries" * group contributing 4-9, for a 5-10 total). A child with no explicit count * constraint contributes exactly 1 if it has its own inline content (e.g. a * champion's weapon-option wrapper group, representing a single mandatory * model slot expressed via nested weapon choices rather than an explicit * count) — but contributes nothing if it's a pure reference to shared * content elsewhere (e.g. a "Crusade" rules group), since that isn't a * model slot at all. Hidden children (background/bookkeeping constructs) * are skipped entirely. A unit with no children at all (a vehicle or a * single-model character with no loadout sub-entries) is exactly 1 model. * * A top-level entry whose own type is "model" (rather than "unit") IS * itself one model — e.g. a named character or vehicle datasheet with no * separate wrapping "unit" container, whose children are typically wargear * or psychic-power choice groups rather than additional model slots — so it * starts from a baseline of 1 before summing children. A "unit"-typed entry * is a pure container whose entire count comes from its children (e.g. * Legionaries: 0 baseline + "Aspiring Champion" (1) + "4-9 Legionaries" * group = 5-10 total). * * Known limitation: a handful of units (e.g. Space Wolves' "Wolf Guard * Headtakers") express their entire composition as entryLinks to shared * library content rather than inline entries — hasOwnInlineContent can't * see through that reference to know the link target is real troop data * rather than an unrelated reference like a "Crusade" rules group, so such * units compute to a bare 0 before the floor below applies. Properly * resolving this would mean threading fetch-data.ts's global shared-entry * index into this function; not done given how rare the pattern is. * Falling back to exactly 1 is a deliberately conservative floor — never * confidently assert a unit has 0 models, since every real unit has at * least 1, even where the true composition (which may be larger) isn't * recoverable here. * * `children` excludes pure-weapon items (see isPureWeaponChild) before any * of the above runs — a standalone character's own top-level weapon * options (e.g. Chaos Daemons' The Changeling, whose direct children are * just "The Trickster's Staff" and "Infernal Flames") use the identical * mandatory min=1/max=1 "selections" constraint shape a real model slot * does, and without this exclusion each one was wrongly counted as its own * model — a single-model character computed to a 3-model unit. */ declare function extractUnitSize(entry: any): UnitSize; export { parser as xmlParser, ensureArray }; /** * Parse a single raw XML entry node (from sharedSelectionEntries or selectionEntries) * into a Unit object. Used by fetch-data.ts for cross-catalogue entryLink resolution. */ export declare function parseEntryNode(entry: any, faction: string, allProfiles?: any[]): Unit | null; export { extractFaction, collectAllProfiles, buildRuleIndex, buildProfileIndex, extractUnitSize, ruleLinksToAbilityProfiles, }; export type GameSystemResult = { id: string; name: string; rules: { id: string; name: string; description: string; }[]; }; export declare function parseGameSystem(xml: string): GameSystemResult; export declare function parseKillTeamGameSystem(xml: string): GameSystemResult; export declare function parseKillTeamCatalogue(xml: string): KillTeamOperative[]; export declare function parseCatalogue(xml: string): Unit[]; /** * Build an index of shared rules from a catalogue node. * Rules may be in , , or both. */ declare function buildRuleIndex(catNode: any): Map; /** * Build an index of shared profiles from a catalogue (or game system) node, * for resolving — the profile-based counterpart to * buildRuleIndex/ (see ruleLinksToAbilityProfiles). * Profiles may be in , , or both. */ declare function buildProfileIndex(catNode: any): Map; /** * Parse detachments from a catalogue node. * Detachments live inside a "Detachment" selectionEntryGroup as child selectionEntries, * each with elements (inline or via infoLink) describing the detachment ability. * * @param catNode - The catalogue XML node * @param faction - Faction name * @param globalIndex - Global shared entry index for resolving entryLinks * @param globalRuleIndex - Optional pre-built global rule index for resolving infoLinks across catalogues */ export declare function parseDetachments(catNode: any, faction: string, globalIndex?: Map, globalRuleIndex?: Map): Detachment[]; /** * Find all enhancement groups in the catalogue node. * Enhancement groups are sharedSelectionEntryGroups whose name contains "Enhancements". * * Two patterns exist: * 1. Nested: A parent "Enhancements" group with child selectionEntryGroups * named "[Detachment] Enhancements", each containing enhancement entries. * 2. Flat: A group named "[Detachment] Enhancements" (or "Enhancements") * with enhancement entries directly inside, using for detachment association. * 3. Separate: Additional standalone "[Detachment] Enhancements" groups at * the sharedSelectionEntryGroups level. */ export declare function parseEnhancements(catNode: any, faction: string, globalIndex?: Map): Enhancement[]; /** * Extract the generic (universal Battle Scars) and campaign-specific * (Battle Traits + Crusade Relics per campaign) Crusade Honours from the * game system file. Called once per game system, not per catalogue. */ export declare function parseGenericCrusadeHonours(gameSystemNode: any): CrusadeHonour[]; /** * Extract a catalogue's own faction-specific Crusade Honours: its Codex * Battle Traits, Codex Crusade Relics, and any Boon-style table (e.g. * Chaos Boons). Only considers the catalogue's own top-level shared groups * tagged `comment: "Crusade content"` — BSData's own marker for this * content, which avoids hardcoding "Codex" as if every faction used that * exact wording. */ export declare function parseFactionCrusadeHonours(catNode: any, faction: string): CrusadeHonour[]; //# sourceMappingURL=xml-parser.d.ts.map