@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
@@ -1 +1 @@
1
- {"version":3,"file":"listforge.js","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ListForge adapter: lower a decoded ListForge \"share JSON\" payload (a\n * BattleScribe-derived roster tree) to a {@link ParsedRoster}.\n *\n * The walk reads an ALLOWLIST of fields only — `name`, `number`, `type`,\n * `categories[].name`, `group`, and `costs` point values — and never touches\n * `rules[].description` or ability `profiles[].characteristics[].$text`, which\n * carry reproduced rules text. This keeps the importer's output free of\n * copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`):\n * - Configuration nodes (`type: \"upgrade\"`) named \"Detachment\" / \"Battle Size\"\n * carry the chosen value as their first child selection.\n * - Unit nodes (`type: \"model\" | \"unit\"`) carry role categories, a points cost,\n * and — nested anywhere beneath them — their wargear (weapon-category\n * selections), enhancement (a selection whose `group` starts \"Enhancements\"),\n * the \"Warlord\" marker, and model sub-selections.\n * - Every unit carries a `\"Faction: <Name>\"` category.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\nconst PTS_COST_NAME = \"pts\";\nconst FACTION_CATEGORY = /^Faction:\\s*(.+)$/;\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\nconst ENHANCEMENT_GROUP_PREFIX = \"Enhancements\";\nconst CHARACTER_CATEGORIES = new Set([\"Character\", \"Epic Hero\"]);\nconst WEAPON_CATEGORY_SUFFIX = \" Weapon\"; // \"Ranged Weapon\", \"Melee Weapon\", \"Psychic Weapon\"\n\n// --- Minimal structural views of the parts of the payload we read. ----------\n\ninterface RawCategory {\n name?: unknown;\n}\ninterface RawCost {\n name?: unknown;\n value?: unknown;\n}\ninterface RawSelection {\n name?: unknown;\n type?: unknown;\n number?: unknown;\n group?: unknown;\n categories?: unknown;\n costs?: unknown;\n selections?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction selectionName(sel: RawSelection): string {\n return asString(sel.name) ?? \"\";\n}\n\nfunction selectionType(sel: RawSelection): string {\n return asString(sel.type) ?? \"\";\n}\n\n/** A selection's multiplicity (`number`), defaulting to 1. */\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\n/** Point value from a selection's cost block, or null when absent. */\nfunction pointsOf(sel: RawSelection): number | null {\n for (const raw of asArray(sel.costs)) {\n const cost = raw as RawCost;\n if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === \"number\") {\n return cost.value;\n }\n }\n return null;\n}\n\nfunction categoryNames(sel: RawSelection): string[] {\n return asArray(sel.categories)\n .map((c) => asString((c as RawCategory).name))\n .filter((n): n is string => n !== null);\n}\n\nfunction childSelections(sel: RawSelection): RawSelection[] {\n return asArray(sel.selections) as RawSelection[];\n}\n\n/** Depth-first visit of a selection and everything beneath it. */\nfunction walk(sel: RawSelection, visit: (s: RawSelection) => void): void {\n visit(sel);\n for (const child of childSelections(sel)) walk(child, visit);\n}\n\nfunction isUnitSelection(sel: RawSelection): boolean {\n const type = selectionType(sel);\n return type === \"model\" || type === \"unit\";\n}\n\nfunction isCharacter(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));\n}\n\nfunction isWeaponSelection(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));\n}\n\nfunction isEnhancementSelection(sel: RawSelection): boolean {\n const group = asString(sel.group);\n return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);\n}\n\n/** Sum the model count of a unit from its nested model selections. */\nfunction modelCount(unit: RawSelection): number {\n let total = 0;\n walk(unit, (s) => {\n if (selectionType(s) === \"model\") total += selectionCount(s);\n });\n return total > 0 ? total : selectionCount(unit);\n}\n\n/** Build a parsed unit from a top-level unit selection. */\nfunction parseUnit(unit: RawSelection): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const node of childSelections(unit)) {\n walk(node, (s) => {\n if (isEnhancementSelection(s)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = selectionName(s);\n enhancement_points = pointsOf(s);\n }\n return;\n }\n if (selectionName(s) === \"Warlord\") {\n is_warlord = true;\n return;\n }\n if (isWeaponSelection(s)) {\n wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });\n }\n });\n }\n\n return {\n raw_name: selectionName(unit),\n is_character: isCharacter(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\n/** Value carried as the first child of a named configuration selection. */\nfunction configValue(\n selections: RawSelection[],\n configName: string,\n): string | null {\n const node = selections.find((s) => selectionName(s) === configName);\n if (!node) return null;\n const child = childSelections(node)[0];\n return child ? selectionName(child) : null;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\n/** First `\"Faction: X\"` category found anywhere; reports all distinct names. */\nfunction collectFactions(forces: RawSelection[]): string[] {\n const seen = new Set<string>();\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n for (const name of categoryNames(s)) {\n const match = FACTION_CATEGORY.exec(name);\n if (match) seen.add(match[1].trim());\n }\n });\n }\n }\n return [...seen];\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n}\ninterface RawPayload {\n name?: unknown;\n generatedBy?: unknown;\n roster?: unknown;\n}\n\nfunction rosterOf(decoded: unknown): RawRoster | null {\n if (!decoded || typeof decoded !== \"object\") return null;\n const roster = (decoded as RawPayload).roster;\n if (!roster || typeof roster !== \"object\") return null;\n if (!Array.isArray((roster as RawRoster).forces)) return null;\n return roster as RawRoster;\n}\n\nexport const listForgeAdapter: FormatAdapter = {\n id: \"listforge\",\n\n matches(decoded: unknown): boolean {\n return rosterOf(decoded) !== null;\n },\n\n parse(decoded: unknown): ParsedRoster {\n const payload = decoded as RawPayload;\n const roster = rosterOf(decoded);\n if (!roster) {\n throw new Error(\"listforge: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n // Configuration lives among each force's top-level selections.\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n for (const force of forces) {\n const top = childSelections(force);\n detachment_raw_name ??= configValue(top, \"Detachment\");\n battle_size_raw ??= configValue(top, \"Battle Size\");\n for (const sel of top) {\n if (isUnitSelection(sel)) units.push(parseUnit(sel));\n }\n }\n\n const factions = collectFactions(forces);\n const total_reported = pointsOf(roster as RawSelection);\n\n // Honest computed total: sum every cost line in the tree. A unit's own cost\n // and its nested enhancement's cost are distinct lines that together make up\n // the unit's army contribution, so a full walk reproduces the army total.\n let total_computed = 0;\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n const pts = pointsOf(s);\n if (pts) total_computed += pts;\n });\n }\n }\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by: asString(payload.generatedBy),\n faction_raw_name: factions[0] ?? null,\n detachment_raw_name,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n"]}
1
+ {"version":3,"file":"listforge.js","sourceRoot":"","sources":["../../src/import/listforge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ListForge adapter: lower a decoded ListForge \"share JSON\" payload (a\n * BattleScribe-derived roster tree) to a {@link ParsedRoster}.\n *\n * The walk reads an ALLOWLIST of fields only — `name`, `number`, `type`,\n * `categories[].name`, `group`, and `costs` point values — and never touches\n * `rules[].description` or ability `profiles[].characteristics[].$text`, which\n * carry reproduced rules text. This keeps the importer's output free of\n * copyrighted prose by construction.\n *\n * Selection-tree shape (recursive `selections`):\n * - Configuration nodes (`type: \"upgrade\"`) named \"Detachment\" / \"Battle Size\"\n * carry the chosen value as their first child selection.\n * - Unit nodes (`type: \"model\" | \"unit\"`) carry role categories, a points cost,\n * and — nested anywhere beneath them — their wargear (weapon-category\n * selections), enhancement (a selection whose `group` starts \"Enhancements\"),\n * the \"Warlord\" marker, and model sub-selections.\n * - Every unit carries a `\"Faction: <Name>\"` category.\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\n\nconst PTS_COST_NAME = \"pts\";\nconst FACTION_CATEGORY = /^Faction:\\s*(.+)$/;\nconst POINTS_LIMIT = /(\\d[\\d,]*)\\s*Point/i;\nconst ENHANCEMENT_GROUP_PREFIX = \"Enhancements\";\nconst CHARACTER_CATEGORIES = new Set([\"Character\", \"Epic Hero\"]);\nconst WEAPON_CATEGORY_SUFFIX = \" Weapon\"; // \"Ranged Weapon\", \"Melee Weapon\", \"Psychic Weapon\"\nconst NEWRECRUIT_XMLNS = \"http://www.battlescribe.net/schema/rosterSchema\";\nconst NEWRECRUIT_HOST_PREFIX = \"https://newrecruit\";\n\n// --- Minimal structural views of the parts of the payload we read. ----------\n\ninterface RawCategory {\n name?: unknown;\n}\ninterface RawCost {\n name?: unknown;\n value?: unknown;\n}\ninterface RawSelection {\n name?: unknown;\n type?: unknown;\n number?: unknown;\n group?: unknown;\n categories?: unknown;\n costs?: unknown;\n selections?: unknown;\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n\nfunction asString(value: unknown): string | null {\n return typeof value === \"string\" ? value : null;\n}\n\nfunction selectionName(sel: RawSelection): string {\n return asString(sel.name) ?? \"\";\n}\n\nfunction selectionType(sel: RawSelection): string {\n return asString(sel.type) ?? \"\";\n}\n\n/** A selection's multiplicity (`number`), defaulting to 1. */\nfunction selectionCount(sel: RawSelection): number {\n return typeof sel.number === \"number\" && sel.number > 0 ? sel.number : 1;\n}\n\n/** Point value from a selection's cost block, or null when absent. */\nfunction pointsOf(sel: RawSelection): number | null {\n for (const raw of asArray(sel.costs)) {\n const cost = raw as RawCost;\n if (asString(cost.name) === PTS_COST_NAME && typeof cost.value === \"number\") {\n return cost.value;\n }\n }\n return null;\n}\n\nfunction categoryNames(sel: RawSelection): string[] {\n return asArray(sel.categories)\n .map((c) => asString((c as RawCategory).name))\n .filter((n): n is string => n !== null);\n}\n\nfunction childSelections(sel: RawSelection): RawSelection[] {\n return asArray(sel.selections) as RawSelection[];\n}\n\n/** Depth-first visit of a selection and everything beneath it. */\nfunction walk(sel: RawSelection, visit: (s: RawSelection) => void): void {\n visit(sel);\n for (const child of childSelections(sel)) walk(child, visit);\n}\n\nfunction isUnitSelection(sel: RawSelection): boolean {\n const type = selectionType(sel);\n return type === \"model\" || type === \"unit\";\n}\n\nfunction isCharacter(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => CHARACTER_CATEGORIES.has(n));\n}\n\nfunction isWeaponSelection(sel: RawSelection): boolean {\n return categoryNames(sel).some((n) => n.endsWith(WEAPON_CATEGORY_SUFFIX));\n}\n\nfunction isEnhancementSelection(sel: RawSelection): boolean {\n const group = asString(sel.group);\n return group !== null && group.startsWith(ENHANCEMENT_GROUP_PREFIX);\n}\n\n/** Sum the model count of a unit from its nested model selections. */\nfunction modelCount(unit: RawSelection): number {\n let total = 0;\n walk(unit, (s) => {\n if (selectionType(s) === \"model\") total += selectionCount(s);\n });\n return total > 0 ? total : selectionCount(unit);\n}\n\n/** Build a parsed unit from a top-level unit selection. */\nfunction parseUnit(unit: RawSelection): ParsedUnit {\n const wargear: ParsedWargear[] = [];\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n let is_warlord = false;\n\n for (const node of childSelections(unit)) {\n walk(node, (s) => {\n if (isEnhancementSelection(s)) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = selectionName(s);\n enhancement_points = pointsOf(s);\n }\n return;\n }\n if (selectionName(s) === \"Warlord\") {\n is_warlord = true;\n return;\n }\n if (isWeaponSelection(s)) {\n wargear.push({ raw_name: selectionName(s), count: selectionCount(s) });\n }\n });\n }\n\n return {\n raw_name: selectionName(unit),\n is_character: isCharacter(unit),\n model_count: modelCount(unit),\n points: pointsOf(unit),\n is_warlord,\n enhancement_raw_name,\n enhancement_points,\n wargear,\n };\n}\n\n/** Value carried as the first child of a named configuration selection. */\nfunction configValue(\n selections: RawSelection[],\n configName: string,\n): string | null {\n const node = selections.find((s) => selectionName(s) === configName);\n if (!node) return null;\n const child = childSelections(node)[0];\n return child ? selectionName(child) : null;\n}\n\nfunction parseLimit(label: string | null): number | null {\n if (!label) return null;\n const match = POINTS_LIMIT.exec(label);\n if (!match) return null;\n return Number.parseInt(match[1].replace(/,/g, \"\"), 10);\n}\n\n/** First `\"Faction: X\"` category found anywhere; reports all distinct names. */\nfunction collectFactions(forces: RawSelection[]): string[] {\n const seen = new Set<string>();\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n for (const name of categoryNames(s)) {\n const match = FACTION_CATEGORY.exec(name);\n if (match) seen.add(match[1].trim());\n }\n });\n }\n }\n return [...seen];\n}\n\ninterface RawRoster {\n name?: unknown;\n costs?: unknown;\n forces?: unknown;\n xmlns?: unknown;\n generatedBy?: unknown;\n}\ninterface RawPayload {\n name?: unknown;\n generatedBy?: unknown;\n roster?: unknown;\n}\n\nfunction rosterOf(decoded: unknown): RawRoster | null {\n if (!decoded || typeof decoded !== \"object\") return null;\n const roster = (decoded as RawPayload).roster;\n if (!roster || typeof roster !== \"object\") return null;\n if (!Array.isArray((roster as RawRoster).forces)) return null;\n return roster as RawRoster;\n}\n\n/** Detect a NewRecruit-flavoured BattleScribe payload. ListForge's matcher\n * excludes these so the greedy first-match dispatcher routes them to the\n * NewRecruit adapter without falling through to here. */\nfunction hasNewRecruitSignature(decoded: unknown, roster: RawRoster): boolean {\n if (asString(roster.xmlns) === NEWRECRUIT_XMLNS) return true;\n const genBy =\n asString((decoded as RawPayload).generatedBy) ?? asString(roster.generatedBy);\n return genBy !== null && genBy.toLowerCase().startsWith(NEWRECRUIT_HOST_PREFIX);\n}\n\nexport const listForgeAdapter: FormatAdapter = {\n id: \"listforge\",\n\n matches(decoded: unknown): boolean {\n const roster = rosterOf(decoded);\n if (!roster) return false;\n return !hasNewRecruitSignature(decoded, roster);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const payload = decoded as RawPayload;\n const roster = rosterOf(decoded);\n if (!roster) {\n throw new Error(\"listforge: payload has no roster.forces array\");\n }\n\n const forces = asArray(roster.forces) as RawSelection[];\n\n // Configuration lives among each force's top-level selections.\n let detachment_raw_name: string | null = null;\n let battle_size_raw: string | null = null;\n const units: ParsedUnit[] = [];\n for (const force of forces) {\n const top = childSelections(force);\n detachment_raw_name ??= configValue(top, \"Detachment\");\n battle_size_raw ??= configValue(top, \"Battle Size\");\n for (const sel of top) {\n if (isUnitSelection(sel)) units.push(parseUnit(sel));\n }\n }\n\n const factions = collectFactions(forces);\n const total_reported = pointsOf(roster as RawSelection);\n\n // Honest computed total: sum every cost line in the tree. A unit's own cost\n // and its nested enhancement's cost are distinct lines that together make up\n // the unit's army contribution, so a full walk reproduces the army total.\n let total_computed = 0;\n for (const force of forces) {\n for (const sel of childSelections(force)) {\n walk(sel, (s) => {\n const pts = pointsOf(s);\n if (pts) total_computed += pts;\n });\n }\n }\n\n return {\n name: asString(payload.name) ?? asString(roster.name) ?? \"Imported roster\",\n generated_by: asString(payload.generatedBy),\n faction_raw_name: factions[0] ?? null,\n detachment_raw_name,\n battle_size_raw,\n declared_limit: parseLimit(battle_size_raw),\n total_reported,\n total_computed,\n units,\n multi_force: factions.length > 1,\n };\n },\n};\n"]}
@@ -43,6 +43,9 @@ export declare function classifyWargearList(tokens: readonly string[]): Classifi
43
43
  export declare function splitWargearList(text: string): string[];
44
44
  /** Strip a trailing parenthetical (e.g. "Houndpack Lance (Marked Prey)" → "Houndpack Lance"). */
45
45
  export declare function stripParenthetical(name: string): string;
46
+ /** Pull the primary faction out of a "Super - Sub" keyword, e.g.
47
+ * "Chaos - Chaos Knights" → "Chaos Knights". Shared by the wtc and GW headers. */
48
+ export declare function factionFromKeyword(value: string): string;
46
49
  /** Parse a `(\d+) pts` or `[\d+ pts]` suffix from a unit header line. */
47
50
  export declare function pointsFrom(token: string): number | null;
48
51
  //# sourceMappingURL=newrecruit-text.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"newrecruit-text.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAMtE;AAED,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,sFAAsF;IACtF,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,4EAA4E;IAC5E,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAwC/E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKvD;AAED,iGAAiG;AACjG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvD"}
1
+ {"version":3,"file":"newrecruit-text.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAMtE;AAED,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,sFAAsF;IACtF,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,4EAA4E;IAC5E,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAwC/E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKvD;AAED,iGAAiG;AACjG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED;kFACkF;AAClF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED,yEAAyE;AACzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvD"}
@@ -86,6 +86,12 @@ export function stripParenthetical(name) {
86
86
  const idx = name.indexOf("(");
87
87
  return idx >= 0 ? name.slice(0, idx).trim() : name.trim();
88
88
  }
89
+ /** Pull the primary faction out of a "Super - Sub" keyword, e.g.
90
+ * "Chaos - Chaos Knights" → "Chaos Knights". Shared by the wtc and GW headers. */
91
+ export function factionFromKeyword(value) {
92
+ const parts = value.split(" - ");
93
+ return (parts[parts.length - 1] ?? value).trim();
94
+ }
89
95
  /** Parse a `(\d+) pts` or `[\d+ pts]` suffix from a unit header line. */
90
96
  export function pointsFrom(token) {
91
97
  const m = /\(\s*(\d+)\s*pts?\s*\)|\[\s*(\d+)\s*pts?\s*\]/i.exec(token);
@@ -1 +1 @@
1
- {"version":3,"file":"newrecruit-text.js","sourceRoot":"","sources":["../../src/import/newrecruit-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Helpers shared by the three NewRecruit text adapters (wtc-compact, wtc-full,\n * simple). These are pure string-massage utilities: they take format-specific\n * tokens and turn them into the format-agnostic {@link ParsedRoster} pieces.\n *\n * No business knowledge of dataset entities lives here — name resolution is\n * still {@link resolve}'s job downstream.\n *\n * @packageDocumentation\n */\nimport type { ParsedWargear } from \"./types.js\";\n\n/** Tournament-standard battle sizes by points ceiling (10th ed). */\nconst BATTLE_SIZES: readonly { upper: number; label: string }[] = [\n { upper: 500, label: \"Combat Patrol (500 Point limit)\" },\n { upper: 1000, label: \"Incursion (1000 Point limit)\" },\n { upper: 2000, label: \"Strike Force (2000 Point limit)\" },\n { upper: 3000, label: \"Onslaught (3000 Point limit)\" },\n];\n\n/**\n * Synthesize a {@link ParsedRoster.battle_size_raw} from a points limit. The\n * wtc/simple formats don't carry the battle-size label explicitly — they only\n * report the total army points — so we map the limit to its standard label\n * (the same one {@link mapBattleSize} expects).\n */\nexport function inferBattleSizeRaw(limit: number | null): string | null {\n if (limit === null) return null;\n for (const { upper, label } of BATTLE_SIZES) {\n if (limit <= upper) return label;\n }\n return BATTLE_SIZES[BATTLE_SIZES.length - 1].label; // beyond Onslaught: cap at Onslaught\n}\n\n/** Outcome of classifying a comma-separated wargear list. */\nexport interface ClassifiedTokens {\n wargear: ParsedWargear[];\n is_warlord: boolean;\n is_character: boolean;\n /** Enhancement raw name, when one was inlined in the wargear list (simple format). */\n enhancement_raw_name: string | null;\n /** Enhancement points cost when given inline (simple format), else null. */\n enhancement_points: number | null;\n}\n\nconst NX_PREFIX = /^(\\d+)x\\s+(.+)$/;\nconst INLINE_PTS = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*\\]\\s*$/i;\nconst CHARACTER_SUFFIX = \" Character\";\nconst WARLORD_MARKER = \"Warlord\";\n\n/**\n * Classify each token in a comma-separated wargear list. Strips the markers\n * that aren't real wargear — `Warlord`, the detachment \"<Name> Character\"\n * keyword, and the inline `Name [N pts]` enhancement (simple format) — and\n * collects everything else as {@link ParsedWargear} with optional `Nx` count.\n *\n * Tokens are pre-split: pass `[\"Armoured feet\", \"2x War Dog autocannon\", ...]`.\n */\nexport function classifyWargearList(tokens: readonly string[]): ClassifiedTokens {\n const wargear: ParsedWargear[] = [];\n let is_warlord = false;\n let is_character = false;\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n\n for (const raw of tokens) {\n const token = raw.trim();\n if (!token) continue;\n\n if (token === WARLORD_MARKER) {\n is_warlord = true;\n continue;\n }\n if (token.endsWith(CHARACTER_SUFFIX)) {\n is_character = true;\n continue;\n }\n\n // Simple format inlines the enhancement as `Name [15 pts]`.\n const pts = INLINE_PTS.exec(token);\n if (pts) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = pts[1].trim();\n enhancement_points = Number.parseInt(pts[2], 10);\n }\n continue;\n }\n\n const nx = NX_PREFIX.exec(token);\n if (nx) {\n const count = Number.parseInt(nx[1], 10);\n wargear.push({ raw_name: nx[2].trim(), count: count > 0 ? count : 1 });\n } else {\n wargear.push({ raw_name: token, count: 1 });\n }\n }\n\n return { wargear, is_warlord, is_character, enhancement_raw_name, enhancement_points };\n}\n\n/**\n * Split a wargear list on top-level commas. (No nested parentheses with commas\n * are produced by NewRecruit, so a plain split is enough; the helper keeps\n * intent explicit for future format quirks.)\n */\nexport function splitWargearList(text: string): string[] {\n return text\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/** Strip a trailing parenthetical (e.g. \"Houndpack Lance (Marked Prey)\" → \"Houndpack Lance\"). */\nexport function stripParenthetical(name: string): string {\n const idx = name.indexOf(\"(\");\n return idx >= 0 ? name.slice(0, idx).trim() : name.trim();\n}\n\n/** Parse a `(\\d+) pts` or `[\\d+ pts]` suffix from a unit header line. */\nexport function pointsFrom(token: string): number | null {\n const m = /\\(\\s*(\\d+)\\s*pts?\\s*\\)|\\[\\s*(\\d+)\\s*pts?\\s*\\]/i.exec(token);\n if (!m) return null;\n return Number.parseInt(m[1] ?? m[2], 10);\n}\n"]}
1
+ {"version":3,"file":"newrecruit-text.js","sourceRoot":"","sources":["../../src/import/newrecruit-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Helpers shared by the three NewRecruit text adapters (wtc-compact, wtc-full,\n * simple). These are pure string-massage utilities: they take format-specific\n * tokens and turn them into the format-agnostic {@link ParsedRoster} pieces.\n *\n * No business knowledge of dataset entities lives here — name resolution is\n * still {@link resolve}'s job downstream.\n *\n * @packageDocumentation\n */\nimport type { ParsedWargear } from \"./types.js\";\n\n/** Tournament-standard battle sizes by points ceiling (10th ed). */\nconst BATTLE_SIZES: readonly { upper: number; label: string }[] = [\n { upper: 500, label: \"Combat Patrol (500 Point limit)\" },\n { upper: 1000, label: \"Incursion (1000 Point limit)\" },\n { upper: 2000, label: \"Strike Force (2000 Point limit)\" },\n { upper: 3000, label: \"Onslaught (3000 Point limit)\" },\n];\n\n/**\n * Synthesize a {@link ParsedRoster.battle_size_raw} from a points limit. The\n * wtc/simple formats don't carry the battle-size label explicitly — they only\n * report the total army points — so we map the limit to its standard label\n * (the same one {@link mapBattleSize} expects).\n */\nexport function inferBattleSizeRaw(limit: number | null): string | null {\n if (limit === null) return null;\n for (const { upper, label } of BATTLE_SIZES) {\n if (limit <= upper) return label;\n }\n return BATTLE_SIZES[BATTLE_SIZES.length - 1].label; // beyond Onslaught: cap at Onslaught\n}\n\n/** Outcome of classifying a comma-separated wargear list. */\nexport interface ClassifiedTokens {\n wargear: ParsedWargear[];\n is_warlord: boolean;\n is_character: boolean;\n /** Enhancement raw name, when one was inlined in the wargear list (simple format). */\n enhancement_raw_name: string | null;\n /** Enhancement points cost when given inline (simple format), else null. */\n enhancement_points: number | null;\n}\n\nconst NX_PREFIX = /^(\\d+)x\\s+(.+)$/;\nconst INLINE_PTS = /^(.+?)\\s*\\[\\s*(\\d+)\\s*pts?\\s*\\]\\s*$/i;\nconst CHARACTER_SUFFIX = \" Character\";\nconst WARLORD_MARKER = \"Warlord\";\n\n/**\n * Classify each token in a comma-separated wargear list. Strips the markers\n * that aren't real wargear — `Warlord`, the detachment \"<Name> Character\"\n * keyword, and the inline `Name [N pts]` enhancement (simple format) — and\n * collects everything else as {@link ParsedWargear} with optional `Nx` count.\n *\n * Tokens are pre-split: pass `[\"Armoured feet\", \"2x War Dog autocannon\", ...]`.\n */\nexport function classifyWargearList(tokens: readonly string[]): ClassifiedTokens {\n const wargear: ParsedWargear[] = [];\n let is_warlord = false;\n let is_character = false;\n let enhancement_raw_name: string | null = null;\n let enhancement_points: number | null = null;\n\n for (const raw of tokens) {\n const token = raw.trim();\n if (!token) continue;\n\n if (token === WARLORD_MARKER) {\n is_warlord = true;\n continue;\n }\n if (token.endsWith(CHARACTER_SUFFIX)) {\n is_character = true;\n continue;\n }\n\n // Simple format inlines the enhancement as `Name [15 pts]`.\n const pts = INLINE_PTS.exec(token);\n if (pts) {\n if (enhancement_raw_name === null) {\n enhancement_raw_name = pts[1].trim();\n enhancement_points = Number.parseInt(pts[2], 10);\n }\n continue;\n }\n\n const nx = NX_PREFIX.exec(token);\n if (nx) {\n const count = Number.parseInt(nx[1], 10);\n wargear.push({ raw_name: nx[2].trim(), count: count > 0 ? count : 1 });\n } else {\n wargear.push({ raw_name: token, count: 1 });\n }\n }\n\n return { wargear, is_warlord, is_character, enhancement_raw_name, enhancement_points };\n}\n\n/**\n * Split a wargear list on top-level commas. (No nested parentheses with commas\n * are produced by NewRecruit, so a plain split is enough; the helper keeps\n * intent explicit for future format quirks.)\n */\nexport function splitWargearList(text: string): string[] {\n return text\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/** Strip a trailing parenthetical (e.g. \"Houndpack Lance (Marked Prey)\" → \"Houndpack Lance\"). */\nexport function stripParenthetical(name: string): string {\n const idx = name.indexOf(\"(\");\n return idx >= 0 ? name.slice(0, idx).trim() : name.trim();\n}\n\n/** Pull the primary faction out of a \"Super - Sub\" keyword, e.g.\n * \"Chaos - Chaos Knights\" → \"Chaos Knights\". Shared by the wtc and GW headers. */\nexport function factionFromKeyword(value: string): string {\n const parts = value.split(\" - \");\n return (parts[parts.length - 1] ?? value).trim();\n}\n\n/** Parse a `(\\d+) pts` or `[\\d+ pts]` suffix from a unit header line. */\nexport function pointsFrom(token: string): number | null {\n const m = /\\(\\s*(\\d+)\\s*pts?\\s*\\)|\\[\\s*(\\d+)\\s*pts?\\s*\\]/i.exec(token);\n if (!m) return null;\n return Number.parseInt(m[1] ?? m[2], 10);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"newrecruit-wtc.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AA+WlD,eAAO,MAAM,2BAA2B,EAAE,aAczC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,aActC,CAAC"}
1
+ {"version":3,"file":"newrecruit-wtc.d.ts","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAiXlD,eAAO,MAAM,2BAA2B,EAAE,aAgBzC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,aActC,CAAC"}
@@ -1,4 +1,4 @@
1
- import { classifyWargearList, inferBattleSizeRaw, splitWargearList, stripParenthetical, } from "./newrecruit-text.js";
1
+ import { classifyWargearList, factionFromKeyword, inferBattleSizeRaw, splitWargearList, stripParenthetical, } from "./newrecruit-text.js";
2
2
  const WTC_HEADER_PREFIX = "+ FACTION KEYWORD:";
3
3
  const HEADER_FIELDS = {
4
4
  faction: /^\+\s*FACTION KEYWORD:\s*(.+?)\s*$/i,
@@ -7,11 +7,6 @@ const HEADER_FIELDS = {
7
7
  pointsLimit: /^\+\s*POINTS LIMIT:\s*(\d+)\s*pts?\s*$/i,
8
8
  listName: /^\+\s*LIST NAME:\s*(.+?)\s*$/i,
9
9
  };
10
- /** Pull the primary faction out of "Chaos - Chaos Knights" → "Chaos Knights". */
11
- function factionFromKeyword(value) {
12
- const parts = value.split(" - ");
13
- return (parts[parts.length - 1] ?? value).trim();
14
- }
15
10
  /** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */
16
11
  function parseWtcHeader(text) {
17
12
  const lines = text.split(/\r?\n/);
@@ -280,6 +275,12 @@ function isWtcText(decoded) {
280
275
  function isFullFormat(text) {
281
276
  return /^[\t ]*\d+\s+with\b/m.test(text);
282
277
  }
278
+ /** `•`-prefixed body lines. wtc-full uses them for per-model breakdowns; the GW
279
+ * app format uses them for every wargear entry. wtc-compact never emits them,
280
+ * so it's the one matcher that must exclude them to stay disjoint from GW. */
281
+ function hasBullets(text) {
282
+ return /^[\t ]*•/mu.test(text);
283
+ }
283
284
  function parseWith(text, format) {
284
285
  const parsed = parseWtcHeader(text);
285
286
  if (!parsed) {
@@ -307,7 +308,9 @@ export const newRecruitWtcCompactAdapter = {
307
308
  const text = isWtcText(decoded);
308
309
  if (text === null)
309
310
  return false;
310
- return !isFullFormat(text);
311
+ // wtc-compact has no `N with` lines (that's wtc-full) and no `•` bullets
312
+ // (that's the GW app format) — excluding both keeps the matcher disjoint.
313
+ return !isFullFormat(text) && !hasBullets(text);
311
314
  },
312
315
  parse(decoded) {
313
316
  const text = isWtcText(decoded);
@@ -1 +1 @@
1
- {"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"wtc-compact\" and \"wtc-full\" text adapters.\n *\n * Both formats open with a `++++++++` summary header carrying FACTION KEYWORD,\n * DETACHMENT, TOTAL ARMY POINTS, WARLORD, ENHANCEMENT(s), NUMBER OF UNITS, and\n * SECONDARY tournament-objective shorthand. The body diverges:\n *\n * - **wtc-compact** — one unit per line:\n * `[CharN: ]Nx <Unit> (P pts): <comma-separated wargear>`\n * followed optionally by `Enhancement: <Name> (+P pts)` on the next line.\n *\n * - **wtc-full** — uppercase section headers (`BATTLELINE`, `ALLIED UNITS`),\n * two-line unit blocks (`[CharN: ]Nx <Unit> (P pts)` then `N with <wargear>`),\n * `Enhancement: <Name> (+P pts)` on its own line, and per-model-type\n * breakdowns with `• Nx <ModelType>` + indented `N with <wargear>` lines.\n *\n * The {@link Roster} pivot stores units at unit granularity — per-model-type\n * wargear breakdowns and `CharN:` slot numbers aren't modelled, so this adapter\n * collapses them: the parsed unit's `model_count` is summed from the breakdown\n * and its `wargear` is the union of every loadout under it. The `WARLORD` /\n * `Houndpack Lance Character` tokens are stripped from the wargear list (and\n * set `is_warlord`/`is_character` instead) so resolution doesn't try to look\n * them up as weapons. Round-trips are at Roster level, not byte-for-byte.\n *\n * Enhancement points (`+15 pts`) are subtracted from the displayed unit total\n * so `ParsedUnit.points` is the *base* unit cost — matching the ListForge\n * convention where the unit's own cost line is base and the enhancement is a\n * sibling cost line. `total_computed` walks every cost line just like ListForge\n * (base unit pts + each enhancement pts).\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport {\n classifyWargearList,\n inferBattleSizeRaw,\n splitWargearList,\n stripParenthetical,\n} from \"./newrecruit-text.js\";\n\nconst WTC_HEADER_PREFIX = \"+ FACTION KEYWORD:\";\n\n// --- header parsing ---------------------------------------------------------\n\ninterface WtcHeader {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n declared_limit: number | null;\n total_reported: number | null;\n battle_size_raw: string | null;\n}\n\nconst HEADER_FIELDS = {\n faction: /^\\+\\s*FACTION KEYWORD:\\s*(.+?)\\s*$/i,\n detachment: /^\\+\\s*DETACHMENT:\\s*(.+?)\\s*$/i,\n totalPoints: /^\\+\\s*TOTAL ARMY POINTS:\\s*(\\d+)\\s*pts?\\s*$/i,\n pointsLimit: /^\\+\\s*POINTS LIMIT:\\s*(\\d+)\\s*pts?\\s*$/i,\n listName: /^\\+\\s*LIST NAME:\\s*(.+?)\\s*$/i,\n} as const;\n\n/** Pull the primary faction out of \"Chaos - Chaos Knights\" → \"Chaos Knights\". */\nfunction factionFromKeyword(value: string): string {\n const parts = value.split(\" - \");\n return (parts[parts.length - 1] ?? value).trim();\n}\n\n/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */\nfunction parseWtcHeader(text: string): { header: WtcHeader; bodyStart: number } | null {\n const lines = text.split(/\\r?\\n/);\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let totalReported: number | null = null;\n let pointsLimit: number | null = null;\n let listName: string | null = null;\n\n // Two `+++++…` fence lines wrap the header. Find them.\n const fenceIndices: number[] = [];\n for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {\n if (/^\\++\\s*$/.test(lines[i])) fenceIndices.push(i);\n }\n let sawFactionKeyword = false;\n for (const line of lines) {\n if (!line.startsWith(\"+\")) continue;\n const factionMatch = HEADER_FIELDS.faction.exec(line);\n if (factionMatch) {\n faction_raw_name = factionFromKeyword(factionMatch[1]);\n sawFactionKeyword = true;\n continue;\n }\n const detMatch = HEADER_FIELDS.detachment.exec(line);\n if (detMatch) {\n detachment_raw_name = stripParenthetical(detMatch[1]);\n continue;\n }\n const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);\n if (ptsMatch) {\n totalReported = Number.parseInt(ptsMatch[1], 10);\n continue;\n }\n const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);\n if (limitMatch) {\n pointsLimit = Number.parseInt(limitMatch[1], 10);\n continue;\n }\n const nameMatch = HEADER_FIELDS.listName.exec(line);\n if (nameMatch) {\n listName = nameMatch[1];\n }\n }\n\n if (!sawFactionKeyword) return null;\n\n const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;\n // POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —\n // is the army's points ceiling. When the source carries only a single\n // figure (the tournament default), fall back to it.\n const declared_limit = pointsLimit ?? totalReported;\n const battle_size_raw = inferBattleSizeRaw(declared_limit);\n\n return {\n header: {\n name: listName ?? \"Imported roster\",\n faction_raw_name,\n detachment_raw_name,\n declared_limit,\n total_reported: totalReported,\n battle_size_raw,\n },\n bodyStart,\n };\n}\n\n// --- shared body helpers ----------------------------------------------------\n\nconst UNIT_HEADER_COMPACT =\n /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*:\\s*(.*)$/i;\nconst UNIT_HEADER_FULL = /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst ENHANCEMENT_LINE =\n /^Enhancement:\\s*(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst WITH_PREFIX = /^(\\d+)\\s+with\\s+(.*)$/i;\nconst MODEL_BREAKDOWN = /^\\s*•\\s*(\\d+)x\\s+(.+?)(?:\\s*\\[[^\\]]*\\])?\\s*$/u;\nconst SECTION_HEADER = /^[A-Z][A-Z0-9 \\-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.\nconst HEADER_LINE = /^\\+/;\n\n/**\n * `N with X, Y, Z` means each of `N` models carries the same list — the weapon\n * counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when\n * the line has no `with` prefix.\n */\nfunction parseWithGroup(text: string): { multiplier: number; list: string } {\n const m = WITH_PREFIX.exec(text);\n if (m) {\n const n = Number.parseInt(m[1], 10);\n return { multiplier: n > 0 ? n : 1, list: m[2] };\n }\n return { multiplier: 1, list: text };\n}\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Total displayed pts from the header line; base computed once an enhancement is known. */\n displayed_pts: number | null;\n enhancement_pts: number;\n model_count: number;\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number, leading_count: number, is_character_prefix: boolean): UnitBuilder {\n return {\n raw_name: name,\n is_character: is_character_prefix,\n is_warlord: false,\n enhancement_raw_name: null,\n displayed_pts,\n enhancement_pts: 0,\n model_count: leading_count > 0 ? leading_count : 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyWithGroup(unit: UnitBuilder, listText: string): void {\n const { multiplier, list } = parseWithGroup(listText);\n const tokens = splitWargearList(list);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n // wtc never inlines the enhancement points in the wargear list (that's the\n // simple format) but classifyWargearList silently absorbs it if it shows up;\n // wtc's enhancement is always parsed off the explicit \"Enhancement:\" line.\n const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const displayed = unit.displayed_pts;\n const points = displayed === null ? null : displayed - unit.enhancement_pts;\n const wargear: ParsedWargear[] = [];\n for (const [raw_name, count] of unit.wargear) {\n wargear.push({ raw_name, count });\n }\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear,\n };\n}\n\n/** Compute total_computed by walking every parsed unit cost line. */\nfunction computeTotal(units: ParsedUnit[], enhancementPtsByIndex: number[]): number {\n let total = 0;\n for (let i = 0; i < units.length; i += 1) {\n total += units[i].points ?? 0;\n total += enhancementPtsByIndex[i] ?? 0;\n }\n return total;\n}\n\nfunction attachEnhancement(unit: UnitBuilder, raw_name: string, pts: number): void {\n unit.enhancement_raw_name = raw_name.trim();\n unit.enhancement_pts = pts;\n}\n\n// --- compact body parser ----------------------------------------------------\n\nfunction parseCompactBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n // Emit immediately so subsequent unit lines start fresh.\n finalize();\n continue;\n }\n\n const unitMatch = UNIT_HEADER_COMPACT.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, unitMatch[4]);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- full body parser -------------------------------------------------------\n\nfunction parseFullBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n let breakdownModels = 0;\n\n const finalize = (): void => {\n if (current) {\n if (breakdownModels > 0) current.model_count = breakdownModels;\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n breakdownModels = 0;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {\n finalize();\n continue;\n }\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n continue;\n }\n\n const unitMatch = UNIT_HEADER_FULL.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n continue;\n }\n\n const breakdown = MODEL_BREAKDOWN.exec(raw);\n if (breakdown && current) {\n breakdownModels += Number.parseInt(breakdown[1], 10);\n continue;\n }\n\n if (WITH_PREFIX.test(line) && current) {\n applyWithGroup(current, line);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- multi-force detection --------------------------------------------------\n\n/** Heuristic for `multi_force`: are there units with \"ALLIED\" decorating\n * the body? wtc-full has an explicit `ALLIED UNITS` section header; compact\n * has no section markers but the user-facing summary header counts every unit\n * together, so detect from explicit section presence. */\nfunction detectMultiForce(text: string, format: \"wtc-compact\" | \"wtc-full\"): boolean {\n if (format === \"wtc-full\") {\n return /^ALLIED UNITS\\s*$/im.test(text);\n }\n // wtc-compact has no section header. Multi-force surfaces only via the\n // primary-faction summary; assume single-force unless we add a richer marker.\n return false;\n}\n\n// --- adapters ---------------------------------------------------------------\n\nfunction isWtcText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n // Both wtc formats begin with the FACTION KEYWORD header line (possibly\n // after some leading whitespace/fence characters).\n if (!decoded.includes(WTC_HEADER_PREFIX)) return null;\n return decoded;\n}\n\n/** Distinguishes wtc-full from wtc-compact: full has a line starting with\n * `\\d+ with ` at the start of a body line (compact only puts `N with` after\n * `:` on the same line as the unit header). */\nfunction isFullFormat(text: string): boolean {\n return /^[\\t ]*\\d+\\s+with\\b/m.test(text);\n}\n\nfunction parseWith(text: string, format: \"wtc-compact\" | \"wtc-full\"): ParsedRoster {\n const parsed = parseWtcHeader(text);\n if (!parsed) {\n throw new Error(`${format}: missing \"+ FACTION KEYWORD:\" header`);\n }\n const { header, bodyStart } = parsed;\n const body = text.split(/\\r?\\n/).slice(bodyStart).join(\"\\n\");\n const { units, enhancementPts } =\n format === \"wtc-full\" ? parseFullBody(body) : parseCompactBody(body);\n\n return {\n name: header.name,\n generated_by: null,\n faction_raw_name: header.faction_raw_name,\n detachment_raw_name: header.detachment_raw_name,\n battle_size_raw: header.battle_size_raw,\n declared_limit: header.declared_limit,\n total_reported: header.total_reported,\n total_computed: computeTotal(units, enhancementPts),\n units,\n multi_force: detectMultiForce(text, format),\n };\n}\n\nexport const newRecruitWtcCompactAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-compact\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n return !isFullFormat(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-compact: input is not a string\");\n return parseWith(text, \"wtc-compact\");\n },\n};\n\nexport const newRecruitWtcFullAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-full\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n return isFullFormat(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-full: input is not a string\");\n return parseWith(text, \"wtc-full\");\n },\n};\n"]}
1
+ {"version":3,"file":"newrecruit-wtc.js","sourceRoot":"","sources":["../../src/import/newrecruit-wtc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NewRecruit \"wtc-compact\" and \"wtc-full\" text adapters.\n *\n * Both formats open with a `++++++++` summary header carrying FACTION KEYWORD,\n * DETACHMENT, TOTAL ARMY POINTS, WARLORD, ENHANCEMENT(s), NUMBER OF UNITS, and\n * SECONDARY tournament-objective shorthand. The body diverges:\n *\n * - **wtc-compact** — one unit per line:\n * `[CharN: ]Nx <Unit> (P pts): <comma-separated wargear>`\n * followed optionally by `Enhancement: <Name> (+P pts)` on the next line.\n *\n * - **wtc-full** — uppercase section headers (`BATTLELINE`, `ALLIED UNITS`),\n * two-line unit blocks (`[CharN: ]Nx <Unit> (P pts)` then `N with <wargear>`),\n * `Enhancement: <Name> (+P pts)` on its own line, and per-model-type\n * breakdowns with `• Nx <ModelType>` + indented `N with <wargear>` lines.\n *\n * The {@link Roster} pivot stores units at unit granularity — per-model-type\n * wargear breakdowns and `CharN:` slot numbers aren't modelled, so this adapter\n * collapses them: the parsed unit's `model_count` is summed from the breakdown\n * and its `wargear` is the union of every loadout under it. The `WARLORD` /\n * `Houndpack Lance Character` tokens are stripped from the wargear list (and\n * set `is_warlord`/`is_character` instead) so resolution doesn't try to look\n * them up as weapons. Round-trips are at Roster level, not byte-for-byte.\n *\n * Enhancement points (`+15 pts`) are subtracted from the displayed unit total\n * so `ParsedUnit.points` is the *base* unit cost — matching the ListForge\n * convention where the unit's own cost line is base and the enhancement is a\n * sibling cost line. `total_computed` walks every cost line just like ListForge\n * (base unit pts + each enhancement pts).\n *\n * @packageDocumentation\n */\nimport type { FormatAdapter } from \"./adapter.js\";\nimport type { ParsedRoster, ParsedUnit, ParsedWargear } from \"./types.js\";\nimport {\n classifyWargearList,\n factionFromKeyword,\n inferBattleSizeRaw,\n splitWargearList,\n stripParenthetical,\n} from \"./newrecruit-text.js\";\n\nconst WTC_HEADER_PREFIX = \"+ FACTION KEYWORD:\";\n\n// --- header parsing ---------------------------------------------------------\n\ninterface WtcHeader {\n name: string;\n faction_raw_name: string | null;\n detachment_raw_name: string | null;\n declared_limit: number | null;\n total_reported: number | null;\n battle_size_raw: string | null;\n}\n\nconst HEADER_FIELDS = {\n faction: /^\\+\\s*FACTION KEYWORD:\\s*(.+?)\\s*$/i,\n detachment: /^\\+\\s*DETACHMENT:\\s*(.+?)\\s*$/i,\n totalPoints: /^\\+\\s*TOTAL ARMY POINTS:\\s*(\\d+)\\s*pts?\\s*$/i,\n pointsLimit: /^\\+\\s*POINTS LIMIT:\\s*(\\d+)\\s*pts?\\s*$/i,\n listName: /^\\+\\s*LIST NAME:\\s*(.+?)\\s*$/i,\n} as const;\n\n/** Parse the leading `++++ ... ++++` block. Returns `null` if no header is found. */\nfunction parseWtcHeader(text: string): { header: WtcHeader; bodyStart: number } | null {\n const lines = text.split(/\\r?\\n/);\n let faction_raw_name: string | null = null;\n let detachment_raw_name: string | null = null;\n let totalReported: number | null = null;\n let pointsLimit: number | null = null;\n let listName: string | null = null;\n\n // Two `+++++…` fence lines wrap the header. Find them.\n const fenceIndices: number[] = [];\n for (let i = 0; i < lines.length && fenceIndices.length < 2; i += 1) {\n if (/^\\++\\s*$/.test(lines[i])) fenceIndices.push(i);\n }\n let sawFactionKeyword = false;\n for (const line of lines) {\n if (!line.startsWith(\"+\")) continue;\n const factionMatch = HEADER_FIELDS.faction.exec(line);\n if (factionMatch) {\n faction_raw_name = factionFromKeyword(factionMatch[1]);\n sawFactionKeyword = true;\n continue;\n }\n const detMatch = HEADER_FIELDS.detachment.exec(line);\n if (detMatch) {\n detachment_raw_name = stripParenthetical(detMatch[1]);\n continue;\n }\n const ptsMatch = HEADER_FIELDS.totalPoints.exec(line);\n if (ptsMatch) {\n totalReported = Number.parseInt(ptsMatch[1], 10);\n continue;\n }\n const limitMatch = HEADER_FIELDS.pointsLimit.exec(line);\n if (limitMatch) {\n pointsLimit = Number.parseInt(limitMatch[1], 10);\n continue;\n }\n const nameMatch = HEADER_FIELDS.listName.exec(line);\n if (nameMatch) {\n listName = nameMatch[1];\n }\n }\n\n if (!sawFactionKeyword) return null;\n\n const bodyStart = fenceIndices.length >= 2 ? fenceIndices[1] + 1 : 0;\n // POINTS LIMIT — the round-trip-friendly companion to TOTAL ARMY POINTS —\n // is the army's points ceiling. When the source carries only a single\n // figure (the tournament default), fall back to it.\n const declared_limit = pointsLimit ?? totalReported;\n const battle_size_raw = inferBattleSizeRaw(declared_limit);\n\n return {\n header: {\n name: listName ?? \"Imported roster\",\n faction_raw_name,\n detachment_raw_name,\n declared_limit,\n total_reported: totalReported,\n battle_size_raw,\n },\n bodyStart,\n };\n}\n\n// --- shared body helpers ----------------------------------------------------\n\nconst UNIT_HEADER_COMPACT =\n /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*:\\s*(.*)$/i;\nconst UNIT_HEADER_FULL = /^(?:Char\\d+:\\s*)?(\\d+)x\\s+(.+?)\\s*\\(\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst ENHANCEMENT_LINE =\n /^Enhancement:\\s*(.+?)\\s*\\(\\+\\s*(\\d+)\\s*pts?\\s*\\)\\s*$/i;\nconst WITH_PREFIX = /^(\\d+)\\s+with\\s+(.*)$/i;\nconst MODEL_BREAKDOWN = /^\\s*•\\s*(\\d+)x\\s+(.+?)(?:\\s*\\[[^\\]]*\\])?\\s*$/u;\nconst SECTION_HEADER = /^[A-Z][A-Z0-9 \\-/&]+$/; // BATTLELINE, ALLIED UNITS, etc.\nconst HEADER_LINE = /^\\+/;\n\n/**\n * `N with X, Y, Z` means each of `N` models carries the same list — the weapon\n * counts in the list multiply by `N`. Returns `{multiplier:1, list:text}` when\n * the line has no `with` prefix.\n */\nfunction parseWithGroup(text: string): { multiplier: number; list: string } {\n const m = WITH_PREFIX.exec(text);\n if (m) {\n const n = Number.parseInt(m[1], 10);\n return { multiplier: n > 0 ? n : 1, list: m[2] };\n }\n return { multiplier: 1, list: text };\n}\n\ninterface UnitBuilder {\n raw_name: string;\n is_character: boolean;\n is_warlord: boolean;\n enhancement_raw_name: string | null;\n /** Total displayed pts from the header line; base computed once an enhancement is known. */\n displayed_pts: number | null;\n enhancement_pts: number;\n model_count: number;\n wargear: Map<string, number>;\n}\n\nfunction newUnit(name: string, displayed_pts: number, leading_count: number, is_character_prefix: boolean): UnitBuilder {\n return {\n raw_name: name,\n is_character: is_character_prefix,\n is_warlord: false,\n enhancement_raw_name: null,\n displayed_pts,\n enhancement_pts: 0,\n model_count: leading_count > 0 ? leading_count : 1,\n wargear: new Map(),\n };\n}\n\nfunction addWargear(unit: UnitBuilder, items: ParsedWargear[]): void {\n for (const { raw_name, count } of items) {\n unit.wargear.set(raw_name, (unit.wargear.get(raw_name) ?? 0) + count);\n }\n}\n\nfunction applyWithGroup(unit: UnitBuilder, listText: string): void {\n const { multiplier, list } = parseWithGroup(listText);\n const tokens = splitWargearList(list);\n const cls = classifyWargearList(tokens);\n if (cls.is_warlord) unit.is_warlord = true;\n if (cls.is_character) unit.is_character = true;\n // wtc never inlines the enhancement points in the wargear list (that's the\n // simple format) but classifyWargearList silently absorbs it if it shows up;\n // wtc's enhancement is always parsed off the explicit \"Enhancement:\" line.\n const scaled = cls.wargear.map((w) => ({ raw_name: w.raw_name, count: w.count * multiplier }));\n addWargear(unit, scaled);\n}\n\nfunction finishUnit(unit: UnitBuilder): ParsedUnit {\n const displayed = unit.displayed_pts;\n const points = displayed === null ? null : displayed - unit.enhancement_pts;\n const wargear: ParsedWargear[] = [];\n for (const [raw_name, count] of unit.wargear) {\n wargear.push({ raw_name, count });\n }\n return {\n raw_name: unit.raw_name,\n is_character: unit.is_character,\n model_count: unit.model_count,\n points,\n is_warlord: unit.is_warlord,\n enhancement_raw_name: unit.enhancement_raw_name,\n enhancement_points: unit.enhancement_raw_name === null ? null : unit.enhancement_pts,\n wargear,\n };\n}\n\n/** Compute total_computed by walking every parsed unit cost line. */\nfunction computeTotal(units: ParsedUnit[], enhancementPtsByIndex: number[]): number {\n let total = 0;\n for (let i = 0; i < units.length; i += 1) {\n total += units[i].points ?? 0;\n total += enhancementPtsByIndex[i] ?? 0;\n }\n return total;\n}\n\nfunction attachEnhancement(unit: UnitBuilder, raw_name: string, pts: number): void {\n unit.enhancement_raw_name = raw_name.trim();\n unit.enhancement_pts = pts;\n}\n\n// --- compact body parser ----------------------------------------------------\n\nfunction parseCompactBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n\n const finalize = (): void => {\n if (current) {\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n // Emit immediately so subsequent unit lines start fresh.\n finalize();\n continue;\n }\n\n const unitMatch = UNIT_HEADER_COMPACT.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n applyWithGroup(current, unitMatch[4]);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- full body parser -------------------------------------------------------\n\nfunction parseFullBody(body: string): { units: ParsedUnit[]; enhancementPts: number[] } {\n const lines = body.split(/\\r?\\n/);\n const units: ParsedUnit[] = [];\n const enhancementPts: number[] = [];\n let current: UnitBuilder | null = null;\n let breakdownModels = 0;\n\n const finalize = (): void => {\n if (current) {\n if (breakdownModels > 0) current.model_count = breakdownModels;\n units.push(finishUnit(current));\n enhancementPts.push(current.enhancement_pts);\n current = null;\n breakdownModels = 0;\n }\n };\n\n for (const raw of lines) {\n const line = raw.trim();\n if (!line || HEADER_LINE.test(line) || /^\\++$/.test(line)) continue;\n if (SECTION_HEADER.test(line) && !UNIT_HEADER_FULL.test(line)) {\n finalize();\n continue;\n }\n\n const enhMatch = ENHANCEMENT_LINE.exec(line);\n if (enhMatch && current) {\n attachEnhancement(current, enhMatch[1], Number.parseInt(enhMatch[2], 10));\n continue;\n }\n\n const unitMatch = UNIT_HEADER_FULL.exec(line);\n if (unitMatch) {\n finalize();\n const leading_count = Number.parseInt(unitMatch[1], 10);\n const name = unitMatch[2].trim();\n const pts = Number.parseInt(unitMatch[3], 10);\n const is_character_prefix = /^Char\\d+:/i.test(line);\n current = newUnit(name, pts, leading_count, is_character_prefix);\n continue;\n }\n\n const breakdown = MODEL_BREAKDOWN.exec(raw);\n if (breakdown && current) {\n breakdownModels += Number.parseInt(breakdown[1], 10);\n continue;\n }\n\n if (WITH_PREFIX.test(line) && current) {\n applyWithGroup(current, line);\n continue;\n }\n }\n\n finalize();\n return { units, enhancementPts };\n}\n\n// --- multi-force detection --------------------------------------------------\n\n/** Heuristic for `multi_force`: are there units with \"ALLIED\" decorating\n * the body? wtc-full has an explicit `ALLIED UNITS` section header; compact\n * has no section markers but the user-facing summary header counts every unit\n * together, so detect from explicit section presence. */\nfunction detectMultiForce(text: string, format: \"wtc-compact\" | \"wtc-full\"): boolean {\n if (format === \"wtc-full\") {\n return /^ALLIED UNITS\\s*$/im.test(text);\n }\n // wtc-compact has no section header. Multi-force surfaces only via the\n // primary-faction summary; assume single-force unless we add a richer marker.\n return false;\n}\n\n// --- adapters ---------------------------------------------------------------\n\nfunction isWtcText(decoded: unknown): string | null {\n if (typeof decoded !== \"string\") return null;\n // Both wtc formats begin with the FACTION KEYWORD header line (possibly\n // after some leading whitespace/fence characters).\n if (!decoded.includes(WTC_HEADER_PREFIX)) return null;\n return decoded;\n}\n\n/** Distinguishes wtc-full from wtc-compact: full has a line starting with\n * `\\d+ with ` at the start of a body line (compact only puts `N with` after\n * `:` on the same line as the unit header). */\nfunction isFullFormat(text: string): boolean {\n return /^[\\t ]*\\d+\\s+with\\b/m.test(text);\n}\n\n/** `•`-prefixed body lines. wtc-full uses them for per-model breakdowns; the GW\n * app format uses them for every wargear entry. wtc-compact never emits them,\n * so it's the one matcher that must exclude them to stay disjoint from GW. */\nfunction hasBullets(text: string): boolean {\n return /^[\\t ]*•/mu.test(text);\n}\n\nfunction parseWith(text: string, format: \"wtc-compact\" | \"wtc-full\"): ParsedRoster {\n const parsed = parseWtcHeader(text);\n if (!parsed) {\n throw new Error(`${format}: missing \"+ FACTION KEYWORD:\" header`);\n }\n const { header, bodyStart } = parsed;\n const body = text.split(/\\r?\\n/).slice(bodyStart).join(\"\\n\");\n const { units, enhancementPts } =\n format === \"wtc-full\" ? parseFullBody(body) : parseCompactBody(body);\n\n return {\n name: header.name,\n generated_by: null,\n faction_raw_name: header.faction_raw_name,\n detachment_raw_name: header.detachment_raw_name,\n battle_size_raw: header.battle_size_raw,\n declared_limit: header.declared_limit,\n total_reported: header.total_reported,\n total_computed: computeTotal(units, enhancementPts),\n units,\n multi_force: detectMultiForce(text, format),\n };\n}\n\nexport const newRecruitWtcCompactAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-compact\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n // wtc-compact has no `N with` lines (that's wtc-full) and no `•` bullets\n // (that's the GW app format) — excluding both keeps the matcher disjoint.\n return !isFullFormat(text) && !hasBullets(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-compact: input is not a string\");\n return parseWith(text, \"wtc-compact\");\n },\n};\n\nexport const newRecruitWtcFullAdapter: FormatAdapter = {\n id: \"newrecruit-wtc-full\",\n\n matches(decoded: unknown): boolean {\n const text = isWtcText(decoded);\n if (text === null) return false;\n return isFullFormat(text);\n },\n\n parse(decoded: unknown): ParsedRoster {\n const text = isWtcText(decoded);\n if (text === null) throw new Error(\"newrecruit-wtc-full: input is not a string\");\n return parseWith(text, \"wtc-full\");\n },\n};\n"]}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Rosterizer adapter: lower a Rosterizer roster JSON payload to a
3
+ * {@link ParsedRoster}.
4
+ *
5
+ * Rosterizer (https://rosterizer.com) stores a roster as a `Roster` envelope
6
+ * with a recursive `Asset` tree under `snapshot` (or `history.present.roster`
7
+ * as a fallback). Every entity — faction, detachment, unit, weapon, ability,
8
+ * enhancement — is an `Asset` keyed by `Classification§Designation` (e.g.
9
+ * `"Unit§Tactical Squad"`). Children sit under `assets.included` (game pieces)
10
+ * and `assets.traits` (modifiers, abilities, markers).
11
+ *
12
+ * The schema is rulebook-agnostic, so the actual `Classification` strings come
13
+ * from whichever Rosterizer rulebook authored the roster. The constants below
14
+ * encode the 40K convention used by the consortium's reference rulebook; tune
15
+ * them here without touching parser logic if a real export disagrees.
16
+ *
17
+ * **IP safety**: the walk reads an ALLOWLIST — `item`, `designation`, `name`,
18
+ * `classification`, `quantity`, `meta.points`, `stats.Points.value`,
19
+ * `aspects.Visibility`, and the recursive `assets.included`/`assets.traits`
20
+ * children. Prose-bearing fields — `text`, `description`, `rules`, ability
21
+ * `stats`, `_layers`, `lineage`, `processed`, `classIdentity`, `bareResourceKey`
22
+ * — are never touched, so the importer's output is free of copyrighted prose
23
+ * by construction.
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+ import type { FormatAdapter } from "./adapter.js";
28
+ interface RawAsset {
29
+ item?: unknown;
30
+ name?: unknown;
31
+ designation?: unknown;
32
+ classification?: unknown;
33
+ quantity?: unknown;
34
+ aspects?: unknown;
35
+ assets?: unknown;
36
+ meta?: unknown;
37
+ stats?: unknown;
38
+ keywords?: unknown;
39
+ }
40
+ /** Split `Classification§Designation` into its two halves. Falls back to the
41
+ * raw `classification`/`designation` fields when `item` is absent. */
42
+ declare function splitItem(asset: RawAsset): {
43
+ classification: string;
44
+ designation: string;
45
+ };
46
+ /** A user-facing display name for an asset: `name` override beats the
47
+ * designation parsed out of the `item` key. */
48
+ declare function displayName(asset: RawAsset): string;
49
+ declare function classOf(asset: RawAsset): string;
50
+ /** Find the first child Asset with the given classification, if any. */
51
+ declare function findChildByClass(asset: RawAsset, cls: string): RawAsset | null;
52
+ export declare const rosterizerAdapter: FormatAdapter;
53
+ export declare const _internals: {
54
+ CLS_ROSTER: string;
55
+ CLS_FACTION: string;
56
+ CLS_DETACHMENT: string;
57
+ CLS_UNIT: string;
58
+ CLS_WEAPON: string;
59
+ CLS_ENHANCEMENT: string;
60
+ CLS_BATTLE_SIZE: string;
61
+ CLS_TRAIT: string;
62
+ DSG_WARLORD: string;
63
+ POINTS_STAT_KEYS: string[];
64
+ splitItem: typeof splitItem;
65
+ displayName: typeof displayName;
66
+ classOf: typeof classOf;
67
+ findChildByClass: typeof findChildByClass;
68
+ };
69
+ export {};
70
+ //# sourceMappingURL=rosterizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rosterizer.d.ts","sourceRoot":"","sources":["../../src/import/rosterizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAkClD,UAAU,QAAQ;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA6CD;sEACsE;AACtE,iBAAS,SAAS,CAAC,KAAK,EAAE,QAAQ,GAAG;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAenF;AAED;+CAC+C;AAC/C,iBAAS,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAE5C;AA4CD,iBAAS,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAExC;AAmHD,wEAAwE;AACxE,iBAAS,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAKvE;AASD,eAAO,MAAM,iBAAiB,EAAE,aAoG/B,CAAC;AAGF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;CAetB,CAAC"}