Floppy/dataslate

View on GitHub
src/parsers/KillTeam2021/BattlescribeParser.ts

Summary

Maintainability
C
1 day
Test Coverage
import _ from 'lodash'
import { xpSelect } from '../Parser'
import { Roster, Operative, Weapon, Equipment, Action, PsychicPower } from '../../types/KillTeam2021'
import { Ability } from '../../types/Ability'

const stat = (name: string, model: Element): number => {
  const node = xpSelect(`bs:profiles/bs:profile[@typeName='Operative']//bs:characteristic[@name='${name}']/text()`, model, true)
  if (node !== null) {
    return parseInt(node.toString())
  } else { return 0 }
}

const parseWeapon = (weapon: Node): Weapon => {
  return {
    id: xpSelect('string(@id)', weapon, true).toString(),
    name: xpSelect('string(@name)', weapon, true).toString(),
    melee: xpSelect('string(@name)', weapon, true).toString().startsWith('⚔'),
    attacks: parseInt(xpSelect(".//bs:characteristic[@name='A']/text()", weapon, true).toString()),
    hit: parseInt(xpSelect(".//bs:characteristic[@name='WS/BS']/text()", weapon, true).toString()),
    damage: parseInt(xpSelect(".//bs:characteristic[@name='D']/text()", weapon, true).toString().split('/')[0]),
    specialRules: (xpSelect(".//bs:characteristic[@name='SR']/text()", weapon, true) ?? '-').toString(),
    criticalDamage: parseInt(xpSelect(".//bs:characteristic[@name='D']/text()", weapon, true).toString().split('/')[1]),
    criticalRules: (xpSelect(".//bs:characteristic[@name='!']/text()", weapon, true) ?? '-').toString()
  }
}

const parseAbility = (ability: Node): Ability => {
  return {
    id: xpSelect('string(@id)', ability, true).toString(),
    name: xpSelect('string(@name)', ability, true).toString(),
    description: (xpSelect(".//bs:characteristic[@name='Ability']/text()", ability, true) ?? '-').toString(),
    phases: [],
    rule: false
  }
}

const parseOperativeRule = (rule: Node): Ability => {
  return {
    id: xpSelect('string(@id)', rule, true).toString(),
    name: xpSelect('string(@name)', rule, true).toString(),
    description: (xpSelect('.//bs:description/text()', rule, true) ?? '-').toString(),
    phases: [],
    rule: true
  }
}

const parseBoonOfTzeentch = (boon: Node): Ability => {
  return {
    id: xpSelect('string(@id)', boon, true).toString(),
    name: xpSelect('string(@name)', boon).toString(),
    description: xpSelect('string(.//bs:profiles/bs:profile/bs:characteristics/bs:characteristic)', boon).toString(),
    phases: []
  }
}

const parsePsychicPower = (power: Node): PsychicPower => {
  const name = xpSelect('string(@name)', power, true).toString()
  const weapons = (xpSelect("..//bs:profile[@typeName='Weapons']", power) as Node[]).map(parseWeapon)
    .filter(weapon => weapon.name.toUpperCase().includes(name.toUpperCase()))

  const weapon = weapons.length > 0 ? weapons[0] : null

  return {
    id: xpSelect('string(@id)', power, true).toString(),
    name,
    description: (xpSelect(".//bs:characteristic[@name='Effect']/text()", power, true) ?? '-').toString(),
    weapon
  }
}

const parseAction = (action: Node, psychicDiscipline: string | null, psychicPowers: string | null): Action => {
  const name = xpSelect('string(@name)', action, true).toString() ?? ''
  let description = (xpSelect(".//bs:characteristic[@name='Unique Action']/text()", action, true) ?? '-').toString()

  if (psychicDiscipline !== null && name.toLowerCase().includes('psychic power')) {
    description += `

**Psychic Discipline**:` + psychicDiscipline
  }

  if (psychicPowers !== null && name.toLowerCase().includes('psychic power')) {
    description += `

**Available Powers**: ` + psychicPowers
  }

  return {
    id: xpSelect('string(@id)', action, true).toString(),
    name,
    description,
    cost: 1
  }
}

const parseEquipment = (equipment: Node): Equipment => {
  const description = xpSelect(".//bs:characteristic[@name='Equipment']/text()", equipment, true)
  return {
    id: xpSelect('string(@entryId)', equipment, true).toString(),
    name: xpSelect('string(@name)', equipment, true).toString(),
    cost: parseInt(xpSelect('string(.//bs:cost/@value)', equipment, true).toString()),
    description: description?.toString()
  }
}

const parseRule = (rule: Node): Ability => {
  return {
    id: xpSelect('string(@id)', rule, true).toString(),
    name: xpSelect('string(@name)', rule, true).toString(),
    description: (xpSelect('.//bs:description/text()', rule, true) ?? '-').toString(),
    phases: []
  }
}

const factionKeywords = [
  'Blooded',
  'Brood Coven',
  'Cadre Mercenary',
  'Chaos Daemons',
  'Commorrite',
  'Corsair Voidscarred',
  'Craftworld',
  'Death Guard',
  'Ecclesiarchy',
  'Forge World (Legends)',
  'Greenskin',
  'Grey Knight',
  'Hive Fleet',
  'Hunter Cadre',
  'Hunter Clade',
  'Imperial Guard',
  'Intercession Squad',
  'Kommando',
  'Legionary',
  'Novitiate',
  'Pathfinder',
  'Phobos Strike Team',
  'Space Marine',
  'Talons of the Emperor',
  'Thousand Sons (Legends)',
  'Tomb World',
  'Traitor Space Marine',
  'Troupe',
  'Troupe (Legends)',
  'Veteran Guardsman',
  'Voiddancer Troupe',
  'Warpcoven',
  'Wyrmblade'
]

const parseOperative = (model: Element): Operative => {
  const allKeywords = (xpSelect(".//bs:categories/bs:category[@primary='false']/@name", model) as Node[]).map((x) => (x.textContent ?? '').replace('💀', ''))
  const faction = _.intersection(allKeywords, factionKeywords).pop() ?? allKeywords.find((k) => (k === k.toUpperCase())) ?? null
  const keywords = _.remove(allKeywords, (x) => (x !== faction))

  const psychicDiscipline = xpSelect("string(.//bs:selection[./bs:selections/bs:selection/bs:profiles/bs:profile/@typeName='Psychic Power']/@name)", model, true).toString()
  const psychicPowers = (xpSelect(".//bs:profile[@typeName='Psychic Power']/@name", model) as Node[]).map((x) => x.nodeValue).join(', ')

  const actions = (xpSelect(".//bs:profile[@typeName='Unique Actions']", model) as Node[]).map((x) => parseAction(x, psychicDiscipline, psychicPowers))
  const abilities = (xpSelect(".//bs:profile[@typeName='Abilities']", model) as Node[]).map(parseAbility).concat(
    (xpSelect('./bs:rules/bs:rule', model) as Node[]).map(parseOperativeRule))

  const boonOfTzeentch = xpSelect(".//bs:selection[./bs:profiles/bs:profile/@typeName='Boon of Tzeentch']", model, true) as Node

  if (boonOfTzeentch !== undefined) {
    abilities.push(parseBoonOfTzeentch(boonOfTzeentch))
  }

  const details = {
    id: xpSelect('string(@id)', model, true).toString(),
    datacard: xpSelect('string(@name)', model, true).toString(),
    name: xpSelect('string(@customName)', model, true).toString(),
    stats: {
      movement: stat('M', model),
      actionPointLimit: stat('APL', model),
      groupActivation: stat('GA', model),
      defence: stat('DF', model),
      save: stat('SV', model),
      invulnerable_save: null,
      wounds: stat('W', model)
    },
    weapons: (xpSelect(".//bs:profile[@typeName='Weapons']", model) as Node[]).map(parseWeapon),
    equipment: (xpSelect(".//bs:selection[(@type='upgrade') and (.//bs:cost/@value!=\"0.0\")]", model) as Node[]).map(parseEquipment),
    abilities,
    actions,
    rules: (xpSelect('.//bs:rules/bs:rule', model) as Node[]).map(parseRule),
    leader: (xpSelect("string(.//bs:categories/bs:category[@primary='true']/@name)", model, true).toString() === 'Leader'),
    psychicDiscipline,
    psychicPowers,
    keywords,
    faction
  }
  return details
}

export const parseBattlescribeXML = (doc: Document): Roster => {
  const operatives = []
  const name = xpSelect('string(/bs:roster/@name)', doc, true).toString()
  const faction = xpSelect('string(//bs:force/@catalogueName)', doc, true).toString()
  for (const model of xpSelect('//bs:selection[@type=\'model\']', doc) as Element[]) {
    operatives.push(parseOperative(model))
  }

  const fireteams = (xpSelect('//bs:force/@name', doc) as Node[]).map((node) => { return node.nodeValue }) as string[]

  const psychicPowers = [] as PsychicPower[]

  (xpSelect(".//bs:profile[@typeName='Psychic Power']", doc) as Node[]).map(parsePsychicPower).forEach((power: PsychicPower) => {
    if (psychicPowers.find(p => p.name === power.name) === undefined) {
      psychicPowers.push(power)
    }
  })

  // Assign unique operative names if they don't have them
  const romanNumerals = [
    '', 'Ⅱ', 'Ⅲ', 'Ⅳ', 'Ⅴ',
    'Ⅵ', 'Ⅶ', 'Ⅷ', 'Ⅸ', 'Ⅹ',
    'ⅩⅠ', 'ⅩⅡ', 'ⅩⅢ', 'ⅩⅣ', 'ⅩⅤ',
    'ⅩⅥ', 'ⅩⅦ', 'ⅩⅧ', 'ⅩⅨ', 'ⅩⅩ'
  ]
  const counts: { [key: string]: number } = {}
  for (const o of operatives) {
    if (o.name === '') {
      if (counts[o.datacard] === undefined) {
        counts[o.datacard] = 0
      }
      o.name = o.datacard + ' ' + romanNumerals[counts[o.datacard]++]
    }
  }
  return {
    system: 'KillTeam2021',
    name,
    faction,
    operatives,
    psychicPowers,
    fireteams
  }
}