'use strict'

import * as Settings from '../lib/miscellaneous-settings.js'
import { Advantage, Encumbrance, Equipment, Melee, Note, Ranged, Skill } from '../module/actor/actor-components.js'
import { GurpsActor } from '../module/actor/actor.js'
import * as HitLocations from '../module/hitlocation/hitlocation.js'
import getUserInput from '../module/utilities/get-user-input.js'
import { translate } from './i18n.js'
import { digitsOnly } from './jquery-helper.js'
import { sanitize } from './utilities.js'

Hooks.once('init', async function () {
  game.settings.registerMenu(Settings.SYSTEM_NAME, Settings.SETTING_MOOK_DEFAULT_EDITOR, {
    name: game.i18n.localize('GURPS.settingMookGenerator'),
    label: game.i18n.localize('GURPS.settingLabelMookGenerator'),
    hint: game.i18n.localize('GURPS.settingHintMookGenerator'),
    type: NpcInputDefaultEditor,
    restricted: true,
  })

  game.settings.register(Settings.SYSTEM_NAME, Settings.SETTING_MOOK_DEFAULT, {
    name: 'Mook Default',
    scope: 'world',
    config: false,
    type: Object,
    default: new Mook(),
    onChange: value => console.log(`Updated Mook Default: ${value}`),
  })
})

Hooks.on(`renderNpcInput`, (app, html, data) => {
  $(html).find('#npc-input-name').focus()
})

// Hooks.once("ready", () => { new NpcInput().render(true) });

const COMMENT_CHAR = '#'
const ERR = `???:`

// Keys we might see during attribute parsing, and whether we should skip them when looking for data
const POSSIBLE_ATTRIBUTE_KEYS = {
  basic: true, // This is the prefix to "basic speed", "basic move"
  speed: false,
  move: false,
  dodge: false,
  sm: false,
  parry: false,
  block: false,
  dr: false,
  damage: false,
  bl: false,
  fright: true, // prefix to "fright check"
  check: false,
  height: false,
  weight: false,
  age: false,
  dmg: false,
  hp: false,
  fp: false,
  skills: true,
  attributes: true,
  secondary: true,
  characteristics: true,
  advantages: true,
}

export class NpcInput extends FormApplication {
  constructor(actor, options = {}) {
    super(options)
    let m = game.settings.get(Settings.SYSTEM_NAME, Settings.SETTING_MOOK_DEFAULT)
    this.mook = m || new Mook()
    this.testing = true
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ['npc-input', 'sheet', 'actor', 'form'],
      id: 'npc-input',
      template: 'systems/gurps/templates/actor/npc-input.hbs',
      resizable: true,
      minimizable: false,
      width: 800,
      height: 700,
      title: 'Mook Generator',
    })
  }
  getData(options) {
    let data = super.getData(options)
    data.mook = this.mook
    data.mode = this.testing ? 'Test Mook' : 'Create Mook'
    data.displaybuttons = true
    return data
  }

  setTesting(t = true) {
    this.testing = t
    this.createButton.innerText = this.testing ? 'Test Mook' : 'Create Mook'
  }

  activateListeners(html) {
    super.activateListeners(html)

    html.find('.gcs-input-sm2').inputFilter(value => digitsOnly.test(value))
    html.find('input[type=text]').on('change paste keyup', ev => {
      let el = ev.currentTarget
      let k = el.dataset.key
      if (!!k) {
        this.mook[k] = el.value
        this.setTesting()
      }
    })

    html.find('.npc-input-ta').on('change', ev => {
      let el = ev.currentTarget
      let k = el.dataset.key
      if (!!k) {
        this.mook[k] = el.value
        this.setTesting()
      }
    })

    html.find('#npc-input-create').on('click keydown focusout', ev => {
      if (ev.type == 'click' || (ev.type == 'keydown' && ev.which == 13)) this.createMook(ev)
      else {
        ev.preventDefault()
        $(html).find('#npc-input-name').focus()
      }
    })
    html.find('#npc-input-import').on('click keydown', ev => {
      if (ev.type == 'click' || (ev.type == 'keydown' && ev.which == 13)) this.importStatBlock(ev)
    })
    this.createButton = html.find('#npc-input-create')[0]

    const frame = html[0]?.querySelector('.npc-input')
    if (!!frame)
      frame.addEventListener('drop', event => {
        event.preventDefault()
        if (event.originalEvent) event = event.originalEvent
        this.applyDrop(JSON.parse(event.dataTransfer.getData('text/plain')))
      })
  }

  applyDrop(data) {
    if (data.actorid) {
      const actorData = game.actors.get(data.actorid).system
      let element = foundry.utils.getProperty(actorData, data.key)
      let key = data.key.match(/^[^\.]+\.([^\.]+)/)[1]
      let tmp = ''
      let tmp2 = ''
      let notes = element.notes || ''
      switch (key) {
        case 'skills':
        case 'spells': {
          tmp += element.name + '-' + element.level
          break
        }
        case 'melee':
        case 'ranged': {
          if (element.acc) tmp2 += ' acc ' + element.acc
          if (element.range) tmp2 += ' range ' + element.range
          if (element.rcl) tmp2 += ' rcl ' + element.rcl
          if (element.rof) tmp2 += ' rof ' + element.rof
          if (element.shots) tmp2 += ' shots ' + element.shots
          if (element.st) tmp2 += ' st ' + element.st
          if (element.usage) tmp2 += ' usage' + element.usage
          if (element.bulk) tmp2 += ' bulk ' + element.bulk
          if (element.halfd) tmp2 += ' halfd ' + element.halfd
          if (element.max) tmp2 += ' max ' + element.max
          if (element.parry) tmp2 += ' parry ' + element.parry
          if (element.reach) tmp2 += ' reach ' + element.reach
          if (element.block) tmp2 += ' block ' + element.block
          tmp += element.name + ' (' + element.level + ') ' + element.damage
          break
        }
        case 'ads': {
          key = 'traits'
          tmp += element.name
          if (element.pageref) tmp += ' Page Ref: ' + element.pageref
          break
        }
        case 'notes': {
          tmp += element.notes
          notes = ''
          break
        }
        case 'equipment': {
          tmp += `${element.name}; ${element.count}; $${element.cost}; ${element.weight} lb`
          break
        }
      }
      let orig = this.mook[key]
      tmp = orig + (orig ? '\n' : '') + tmp
      tmp += tmp2
      if (notes) tmp += '\n# ' + notes.split('\n').join('\n# ').replaceAll(';', ',')
      this.mook[key] = tmp
    } else {
      if (data.otf) {
        this.mook.notes += ' [' + data.otf + ']'
      }
    }
    this.setTesting()
    this.render(true)
  }

  async importStatBlock(ev) {
    ev.preventDefault()

    let statblock = await getUserInput({
      title: translate('Import Stat Block'),
      label: translate('Import'),
      placeholder: translate('Paste your stat block here'),
      content: this.savedStatBlock || '',
      submitButton: true,
    })

    if (statblock.length < 3) statblock = EX[parseInt(statblock)]
    if (!!statblock) this.parseStatBlock(statblock)
    this.render(true)
  }

  async createMook(ev) {
    ev.preventDefault()
    if (this.testing) {
      let err = this.check()
      if (!!err) ui.notifications.warn('Unable to create Mook: ' + err)
      else this.setTesting(false)
    } else {
      let data = { name: this.mook.name, type: 'character' }
      let a = await GurpsActor.create(data, { renderSheet: false })
      await this.populate(a)
      await a.setFlag('core', 'sheetClass', 'gurps.GurpsActorNpcSheet')
      a.sheet.render(true)
    }
    this.render(true)
  }

  async populate(a) {
    let m = this.mook
    let data = a.system
    let att = data.attributes

    // + is a trick to force strings to ints
    att.ST.import = +m.st
    att.DX.import = +m.dx
    att.IQ.import = +m.iq
    att.HT.import = +m.ht
    att.WILL.import = +m.will
    att.PER.import = +m.per

    data.HP.max = +m.hp
    data.HP.value = +m.hp
    data.FP.max = +m.fp
    data.FP.value = +m.fp

    data.basicmove.value = parseInt(m.move)
    data.basicspeed.value = m.speed
    data.frightcheck = m.will
    if (!!m.check) data.frightcheck = m.check // Imported "fright check"

    data.hearing = m.per
    data.tastesmell = m.per
    data.touch = m.per
    data.vision = m.per
    if (!!m.parry) data.parry = parseInt(m.parry)
    if (!!m.block) data.block = parseInt(m.block)

    let ns = {}
    let nt = new Note(m.notes.trim())
    GURPS.put(ns, nt)

    // Since we removed the default hitlocations from template.json, we need to create at least one entry for the mook
    // But to maintain a similar look, we will just recreate the humanoid locations.
    let hls = {}
    for (const [key, value] of Object.entries(HitLocations.hitlocationDictionary.humanoid)) {
      let loc = { ...value, ...{ where: key } }
      if (key == HitLocations.HitLocation.TORSO) loc.import = m.dr
      GURPS.put(hls, loc)
    }

    let es = {}
    let e = new Encumbrance()
    e.level = 0
    e.current = true
    e.key = 'enc0'
    e.weight = 0
    e.move = parseInt(m.move)
    e.dodge = parseInt(m.dodge)
    GURPS.put(es, e)

    let melee = {}
    m.a_melee.forEach(me => GURPS.put(melee, me))

    let ranged = {}
    m.a_ranged.forEach(r => GURPS.put(ranged, r))

    let ts = {}
    ts.title = m.title
    let dt = new Date().toString().split(' ').splice(1, 3).join(' ')
    ts.createdon = dt
    if (!!m.sm) ts.sizemod = m.sm[0] == '-' ? m.sm : m.sm[0] == '+' ? '' : '+' + m.sm
    ts.appearance = m.desc
    ts.height = m.height
    ts.weight = m.weight
    ts.age = m.age

    let skills = {}
    m.a_skills.forEach(s => GURPS.put(skills, s))

    let spells = {}
    m.a_spells.forEach(s => GURPS.put(spells, s))

    let ads = {}
    m.a_traits.forEach(a => GURPS.put(ads, a))

    let eqt = {}
    m.a_equipment.forEach(e => GURPS.put(eqt, e))

    let dmg = m.damage || m.dmg || ''
    let thrst = dmg.split('/')[0]
    let swng = dmg.split('/')[1]
    if (!swng && !!thrst) {
      swng = thrst
      thrst = ''
    }

    let commit = {
      'system.attributes': att,
      'system.HP': data.HP,
      'system.FP': data.FP,
      'system.basicmove': data.basicmove,
      'system.basicspeed': data.basicspeed,
      'system.currentmove': parseInt(m.move),
      'system.currentdodge': parseInt(m.dodge),
      'system.frightcheck': data.frightcheck,
      'system.hearing': data.hearing,
      'system.tastesmell': data.tastesmell,
      'system.touch': data.touch,
      'system.vision': data.vision,
      'system.equippedparry': data.parry,
      'system.parry': data.parry,
      'system.equippedblock': data.block,
      'system.block': data.block,
      'system.notes': ns,
      'system.hitlocations': hls,
      'system.encumbrance': es,
      'system.melee': melee,
      'system.ranged': ranged,
      'system.traits': ts,
      'system.skills': skills,
      'system.spells': spells,
      'system.ads': ads,
      'system.swing': swng,
      'system.thrust': thrst,
      'system.equipment.carried': eqt,
    }

    await a.update(commit)
    await a.postImport()
    console.log('Created Mook:')
    console.log(a)
  }

  sanit(key) {
    this.mook[key] = sanitize(this.mook[key])
  }

  check() {
    let e = ''
    if (!this.mook.name) e = ', No Name'
    this.sanit('melee')
    this.sanit('ranged')
    this.sanit('traits')
    this.sanit('skills')
    this.sanit('spells')
    this.sanit('equipment')

    if (this.checkTraits()) e += ', Error in Traits'
    if (this.checkSkills()) e += ', Error in Skills'
    if (this.checkMelee()) e += ', Error in Melee'
    if (this.checkRanged()) e += ', Error in Ranged'
    if (this.checkSpells()) e += ', Error in Spells'
    if (this.checkEquipment()) e += ', Error in Equipment'
    return e.substr(2)
  }

  // return an array of string representing each line
  prep(text, delim) {
    var ans
    if (!!delim) ans = this.parseDelimLines(text, delim)
    else ans = text.split('\n')
    return ans.map(e => this.cleanLine(e)).filter(e => e.length > 0)
  }

  // Allow () to include delims without breaking line
  parseDelimLines(str, delim) {
    let arr = []
    let i = 0
    let line = ''
    let d = 0
    while (i < str.length) {
      let c = str[i++]
      if ((c == delim && d == 0) || c == '\n') {
        arr.push(line)
        line = ''
      } else {
        line += c
        if (c == '(') d++
        if (c == ')') d--
      }
    }
    if (!!line) arr.push(line)
    return arr
  }

  checkSkills() {
    const m = this.mook
    let txt = ''
    let arr = []
    this.prep(m.skills, ';').forEach(e => {
      if (e.includes(ERR)) return
      txt += '\n' + e
      if (e.startsWith(COMMENT_CHAR)) {
        if (arr.length > 0) this.addToNotes(arr, e.substr(1), ' ')
        return
      }
      const i = e.lastIndexOf('-')
      if (i < 1) return (txt += `\n${ERR} missing '-'`)
      const n = e.substring(0, i).trim()
      const v = e.substr(i + 1).trim()
      if (!v) return (txt += `\n${ERR} missing skill level`)
      if (isNaN(v)) return (txt += `\n${ERR} "${v}" is not a number`)
      arr.push(new Skill(n, v))
    })
    m.skills = txt.substr(1)
    m.a_skills = arr
    return txt.includes(ERR)
  }

  checkSpells() {
    const m = this.mook
    let txt = ''
    let arr = []
    this.prep(m.spells, ';').forEach(e => {
      if (e.includes(ERR)) return
      txt += '\n' + e
      if (e.startsWith(COMMENT_CHAR)) {
        if (arr.length > 0) this.addToNotes(arr, e.substr(1), ' ')
        return
      }
      const i = e.lastIndexOf('-')
      if (i < 1) return (txt += `\n${ERR} missing '-'`)
      const n = e.substring(0, i).trim()
      const v = e.substr(i + 1).trim()
      if (!v) return (txt += `\n${ERR} missing spell level`)
      if (isNaN(v)) return (txt += `\n${ERR} "${v}" is not a number`)
      arr.push(new Skill(n, v))
    })
    m.spell = txt.substr(1)
    m.a_spells = arr
    return txt.includes(ERR)
  }

  checkMelee() {
    const pats = [
      { regex: '(^usage|^Usage|^mode|^Mode)\\w+', var: 'mode' },
      { regex: '(^parry|^Parry)\\d+', var: 'parry' },
      { regex: '(^reach|^Reach)[\\w,]+', var: 'reach' },
      { regex: '(^st|^ST|^St)\\d+', var: 'st' },
      { regex: '(^block|^Block)\\d+', var: 'block' },
    ]
    const m = this.mook
    let txt = ''
    let arr = []
    this.prep(m.melee).forEach(e => {
      if (e.includes(ERR)) return
      txt += '\n' + e
      if (e.startsWith(COMMENT_CHAR)) {
        if (arr.length > 0) this.addToNotes(arr, e.substr(1), ' ')
        return
      }
      var me, remain
      let parse = e.replace(
        /(.*) ?\((\d+)\) (\d+)d(\d*)([-+]\d+)?([xX\*]\d+)?(\([.\d]+\))?(!)? ?(\w+\+?\+?)(.*)$/g,
        '$1~$2~$3~$4~$5~$6~$7~$8~$9~$10'
      )
      if (e != parse) {
        parse = parse.split('~')
        me = new Melee(
          parse[0].trim(),
          parse[1],
          parse[2] + 'd' + parse[3] + parse[4] + parse[5] + parse[6] + parse[7] + ' ' + parse[8]
        )
        remain = parse[9].trim()
      } else {
        parse = e.replace(/(.*) ?\(([ \w]+)\) "([^"]+)" ?(.*)$/g, '$1~$2~$3~$4')
        if (e == parse) return (txt += `\n${ERR} unable to find (level) and damage`)
        parse = parse.split('~')
        me = new Melee(parse[0].trim(), parse[1], parse[2])
        remain = parse[3].trim()
      }
      if (!!remain) {
        let ext = remain.replace(/ +/g, ' ').split(' ')
        if (ext.length % 2 != 0) return (txt += `\n${ERR} unable to parse "${remain}"`)
        for (let i = 0; i < ext.length; i += 2) {
          let s = ext[i] + ext[i + 1]
          let found = false
          pats.forEach(p => {
            if (s.match(new RegExp(p.regex))) {
              me[p.var] = ext[i + 1]
              found = true
            }
          })
          if (!found) return (txt += `\n${ERR} unknown pattern "${ext[i]} ${ext[i + 1]}"`)
        }
      }
      arr.push(me)
    })
    m.melee = txt.substr(1)
    m.a_melee = arr
    return txt.includes(ERR)
  }

  checkRanged() {
    const pats = [
      { regex: '(^acc|^Acc)\\d+', var: 'acc' },
      { regex: '(^rof|^RoF|^Rof)\\d+', var: 'rof' },
      { regex: '(^rcl|^Rcl)\\d+', var: 'rcl' },
      { regex: '(^usage|^Usage|^mode|^Mode)\\w+', var: 'mode' },
      { regex: '(^range|^Range)[xX\\d+.](\\/[xX\\d.]+)?', var: 'range' },
      { regex: '(^shots|^Shots)[\\w\\)\\(]+', var: 'shots' },
      { regex: '(^bulk|^Bulk)[\\w-]+', var: 'bulk' },
      { regex: '(^st|^ST|^St)\\d+', var: 'st' },
      { regex: '^halfd\\d+', var: 'halfd' },
      { regex: '^max\\d+', var: 'max' },
    ]
    const m = this.mook
    let txt = ''
    let arr = []
    this.prep(m.ranged).forEach(e => {
      if (e.includes(ERR)) return
      txt += '\n' + e
      if (e.startsWith(COMMENT_CHAR)) {
        if (arr.length > 0) this.addToNotes(arr, e.substr(1), ' ')
        return
      }
      let parse = e.replace(
        /(.*) ?\((\d+)\) (\d+)d(\d*)([-+]\d+)?([xX\*]\d+)?(\([.\d]+\))?(!)?:? ?([\w-+]+)(.*)/g,
        '$1~$2~$3~$4~$5~$6~$7~$8~$9~$10'
      )

      var r, remain
      if (e != parse) {
        parse = parse.split('~')
        r = new Ranged(
          parse[0].trim(),
          parse[1],
          parse[2] + 'd' + parse[3] + parse[4] + parse[5] + parse[6] + parse[7] + ' ' + parse[8]
        )
        remain = parse[9].trim()
      } else {
        parse = e.replace(/(.*) ?\(([ \w]+)\):? "([^"]+)" ?(.*)$/g, '$1~$2~$3~$4')
        if (e == parse) return (txt += `\n${ERR} unable to find (level) and damage`)
        parse = parse.split('~')
        r = new Ranged(parse[0].trim(), parse[1], parse[2])
        remain = parse[3].trim()
      }
      if (!!remain) {
        let ext = remain.trim().replace(/ +/g, ' ').split(' ')
        if (ext.length % 2 != 0) return (txt += `\n${ERR} unable to parse for `)
        for (let i = 0; i < ext.length; i += 2) {
          let s = ext[i] + ext[i + 1]
          let found = false
          pats.forEach(p => {
            if (s.match(new RegExp(p.regex))) {
              r[p.var] = ext[i + 1]
              found = true
            }
          })
          if (!found) return (txt += `\n${ERR} unknown pattern "${ext[i]} ${ext[i + 1]}"`)
        }
      }
      r.checkRange()
      arr.push(r)
    })
    m.ranged = txt.substr(1)
    m.a_ranged = arr
    return txt.includes(ERR)
  }

  addToNotes(arr, note, delim) {
    if (arr.length == 0) return
    let n = arr[arr.length - 1].notes
    if (!!n) n += delim + note
    else n = note
    arr[arr.length - 1].notes = n
  }

  checkTraits() {
    const m = this.mook
    let txt = ''
    let arr = []
    this.prep(m.traits, ';').forEach(e => {
      txt += '\n' + e
      if (e.startsWith(COMMENT_CHAR)) {
        if (arr.length > 0) this.addToNotes(arr, e.substr(1), '\n')
        return
      }
      arr.push(new Advantage(e))
    })
    m.traits = txt.substr(1)
    m.a_traits = arr
    return false
  }

  checkEquipment() {
    const m = this.mook
    let txt = ''
    let arr = []
    this.prep(m.equipment).forEach(e => {
      if (e.includes(ERR)) return
      txt += '\n' + e
      if (e.startsWith(COMMENT_CHAR)) {
        this.addToNotes(arr, e.substr(1), '\n')
        return
      }
      let a = e.split(';')
      if (a.length != 4) {
        return (txt += `\n${ERR} Expecting <name>; <qty>; $<cost>; <weight> lb\n`)
      } else {
        let eqt = new Equipment(a[0])
        eqt.count = parseInt(a[1])
        eqt.equipped = true
        eqt.carried = true
        let n = a[2].match(/ *\$ *([\?\d]+)/)
        if (n) eqt.cost = n[1]
        else {
          return (txt += `\n${ERR} Unable to find '$ <cost>'\n`)
        }
        n = a[3].match(/ *([\?\d.]+) * lbs?/)
        if (n) eqt.weight = n[1]
        else {
          return (txt += `\n${ERR} Unable to find '<weight> lb'\n`)
        }
        Equipment.calc(eqt)
        arr.push(eqt)
      }
    })
    m.equipment = txt.substr(1)
    m.a_equipment = arr
    return txt.includes(ERR)
  }

  preparePastedText(txt) {
    txt = sanitize(txt)
    txt = txt.replace(/\t/g, '; ') // replace tabs with '; '
    txt = txt.replace(/ +/g, ' ') // remove multiple spaces in a row
    txt = txt.replace(/[^ -~\n]+/g, '') // remove remaining non-ascii
    return this.cleanLine(txt) // trim and remove leading and trailing periods.
  }

  parseStatBlock(txt) {
    this.setTesting()
    this.statblk = this.preparePastedText(txt)
    this.savedStatBlock = this.statblk
    console.log(this.statblk)
    try {
      this.resetMook()
      this.checkForNameNotes()
      this.parseAttrs()
      this.parseAttacks()
      this.parseTraits()
      this.parseS('Skills', 'Spells', 'skills')
      this.parseS('Spells', 'Equipment', 'spells')
      this.parseEquipment()
      this.parseFinalNotes()
      this.parseAttacks(true)
    } catch (e) {
      console.log(e)
      ui.notifications.warn(e)
    }
  }

  resetMook() {
    this.mook = new Mook()
    this.mook.name = ''
    this.mook.notes = ''
    this.mook.melee = ''
    this.mook.ranged = ''
    this.mook.traits = ''
    this.mook.skills = ''
    this.mook.spells = ''
    this.mook.equipment = ''
  }

  gatherAttackLines(attblk, currentline, oldformat) {
    // read lines looking for (\d+) and any lines following that do NOT have (\d+)
    if (oldformat) return [attblk, currentline, this.nextToken()]
    var nextline
    ;[attblk, nextline] = this.nextTokenPrim(attblk, '\n', false, true)
    while (!!nextline && (nextline.match(/[Ss]hots.*\(\d+\)/) || !nextline.match(/\(\d+\)/))) {
      // If the new line doesn't contain a skill level "(\d+)" that isn't part of [Ss]hots assume it is still from the previous attack
      currentline += ' ' + nextline
      ;[attblk, nextline] = this.nextTokenPrim(attblk, '\n', false, true)
    }
    return [attblk, currentline, nextline]
  }

  findInLine(line, regex) {
    let re = new RegExp(regex)
    let m = line.match(re)
    if (!!m) line = line.replace(re, '')
    return [line, m]
  }

  parseEquipment() {
    if (!this.peek('Equipment')) return
    let line = this.nextline() // Skip equipment line
    line = this.nextline()
    while (!!line) {
      var m
      let cost = '?'
      let weight = '?'
      let qty = 1
      let name = ''
      ;[line, m] = this.findInLine(line, /\$([\.\d]+)[ ,\.]*/)
      if (!!m) cost = m[1]
      ;[line, m] = this.findInLine(line, /([\.\d]+) ?lbs?[ ,\.]*/)
      if (!!m) weight = m[1]
      ;[line, m] = this.findInLine(line, /^ *(\d+)/)
      if (!!m) qty = m[1]
      ;[line, m] = this.findInLine(line, /^([^(\.])+\((\d+)\)/)
      if (!!m) {
        qty = m[2]
        name = m[1]
      }
      line = line.replace(/\[[ \W]+/g, '') //clean up runs of non-word chars in brackets
      name += this.cleanLine(line)
      name = name.replace(';', ',')
      this.mook.equipment += name + '; ' + qty + '; $' + cost + '; ' + weight + ' lb\n'
      line = this.nextline()
      while (line === '') line = this.nextline()
    }
  }

  parseS(start, end, path) {
    this.trim()
    if (this.peek(start)) this.nextToken(start)
    let goUntil = this.peek(end)
    let line = this.nextline()
    if (!line) return
    line = this.cleanLine(line)
    let s = ''
    //    while (!!line && (line.match(/[^-]+-\d+/) || (!!goUntil && !line.startsWith(end)))) {
    while (!!line && line.match(/[^-]+-\d+/) && !line.startsWith(end)) {
      s += '\n' + this.cleanLine(line)
      line = this.nextToken('\n', false, true)
    }
    //if (!s) this.appendToNotes(`?? No ${start} matching pattern '${path}-lvl' found`);
    s = this.cleanLine(s)
    let delim = ';'
    if (s.includes(delim)) s = s.replace(/\n/g, ' ')
    // If has delims, then remove newlines
    else delim = '\n' // No ";", so assume one per line
    var l
    while (!!s) {
      ;[s, l] = this.nextTokenPrim(s, delim, false, true) // Start it off reading the first line

      // Remove skill type, rsl and points  Broadsword (A) DX+2 [8]-16
      l = this.cleanLine(l).replace(/ ?\([a-zA-Z]+\) [a-zA-Z]+([-+][0-9]+)? \[ *-?\d+\ *] */g, '')
      l = l.replace(/ [SDIH][TXQ][-+]?[0-9]* ?/, ' ')
      l = l.replace(/ ?\[ *-?\d+ *\],?\.? ?/g, ' ')
      l = this.cleanLine(l)
      let m = l.match(/([^-]+ *- *\d+)(.*)/)
      if (m) {
        this.mook[path] += '\n' + m[1]
        if (!!m[2]) this.mook[path] += '\n' + COMMENT_CHAR + m[2]
      } else if (!!l) this.mook[path] += '\n' + COMMENT_CHAR + `Unknown ${start} pattern ${l}`
    }
    if (!!line) this.pushToken(line)
  }

  parseTraits() {
    let n = this.peekskipto('Advantages/Disadvantages')
    n += this.peekskipto('Advantages')
    n += this.peekskipto('Traits')
    if (!!n) this.appendToNotes(ERR + ' Skipped before Traits: ' + n, '\n')
    this.trim()
    let trblk = this.nextToken('Skills', 'Spells', true)
    if (!trblk) return this.appendToNotes(ERR + " Looking for Traits block, unable to find 'Skills'") // Basically always need "Skills" or "Spells"
    let traits = ''
    if (trblk.includes(';')) trblk = trblk.replace(/\n/g, ' ')
    // if it contains ";", then remove newlines
    else {
      // no ";", maybe using commas?
      if (trblk.split(',').length > 2) trblk = trblk.replace(/,/g, ';') // Assume comma separated
    }
    trblk = trblk.replace(/disadvantages/gi, ';')
    trblk = trblk.replace(/perks/gi, ';')
    trblk = trblk.replace(/quirks/gi, ';')
    trblk = trblk.replace(/ ?\[-?\d+\*?\],?\.? ?/g, ' ')
    this.prep(trblk, ';').forEach(t => {
      t = t.replace(/\( *(\d+) *or less *\)/g, '($1)') // Compress some forms of CR rolls
      let m = t.match(/(.*)\((\d+)\)/)
      if (!!m) traits += `\n[CR: ${m[2]} ${m[1].trim()}]`
      // Convert CR roll into OtF
      else {
        if (t != 'and') traits += '\n' + t
      }
    })
    this.mook.traits = traits
  }

  parseAttacks(oldformat = false) {
    const rpats = [
      { regex: ' [Aa]cc *(\\d+) ?,?', var: 'acc' },
      { regex: ' [Rr]o[Ff] *(\\d+) ?,?', var: 'rof' },
      { regex: ' [Rr]cl *(\\d+) ?,?', var: 'rcl' },
      { regex: ' 1\\/2[Dd] *(\\d+) ?,?', var: 'halfd' },
      { regex: ' [Mm]ax *(\\d+) ?,?', var: 'max' },
      { regex: ' [Ss]hots *([\\w\\)\\(]+) ?,?', var: 'shots' },
      { regex: ' [Bb]ulk *([\\w-]+) ?,?', var: 'bulk' },
      { regex: ' [Ss][Tt] *(\\d+) ?,?', var: 'st' },
      { regex: ' ?[Rr]anged,? *with ?', var: '' },
      { regex: ' ?[Rr]anged,?', var: '' },
      { regex: ' ?[Rr]ange ([0-9/]+) *,?', var: 'range' },
    ]
    var attblk
    if (oldformat) {
      attblk = this.statblk
      if (!attblk) return
    } else {
      attblk = this.nextToken('Traits:', 'Advantages/Disadvantages:') // Look for either as start of ads/disads
      if (!attblk) {
        if (this.peek('Skills:')) attblk = this.nextToken('Skills:', 'junk')
        else if (!this.peek('\nWeapons:')) this.mook.melee = COMMENT_CHAR + 'No attacks found' // If Weapons aren't listedt later, show error.
      }
    }
    if (!attblk) return
    attblk = this.cleanLine(attblk)
    // assume a line is an attack if it contains '(n)'
    let line, nextline
    ;[attblk, line] = this.nextTokenPrim(attblk, '\n', false, true) // Start it off reading the first line
    ;[attblk, line, nextline] = this.gatherAttackLines(attblk, line, oldformat) // collect up any more lines.

    while (!!line) {
      save = line
      line = this.cleanLine(line)
      var name, lvl, dmg, save
      ;[line, name] = this.nextTokenPrim(line, '(', false)
      if (!name) {
        this.mook.melee += `${COMMENT_CHAR}Unrecognized attack: "${save}" `
        ;[attblk, line, nextline] = this.gatherAttackLines(attblk, nextline, oldformat) // collect up any more lines.
        continue
      }
      name = name.trim()
      ;[line, lvl] = this.nextTokenPrim(line, ')', '):')
      if (!lvl || isNaN(lvl)) {
        this.mook.melee += `${COMMENT_CHAR}No level or level is not a number "${save}"`
        ;[attblk, line, nextline] = this.gatherAttackLines(attblk, nextline, oldformat) // collect up any more lines.
        continue
      }
      //[line, dmg] = this.nextTokenPrim(line, ".", ",", true);		// find up to . or , or end of string
      ;[line, dmg] = this.nextTokenV2Prim(line, ['.', ',', ';'], true)
      let savedmg1 = dmg
      let note = ''
      ;[dmg, note] = this.mapDmg(line, dmg)
      if (!!dmg && !!note) note = '\n' + COMMENT_CHAR + this.cleanLine(note)
      if (!dmg) {
        // If not dmg formula, try one more time.
        ;[line, dmg] = this.nextTokenPrim(line, '.', ',', true) // find up to . or , or end of string
        let savedmg2 = dmg
        ;[dmg, note] = this.mapDmg(line, dmg)
        if (!dmg) {
          line = savedmg1 + ' ' + savedmg2 + ' ' + note // Nope, couldn't find anything, so reset the line
          note = ''
        } else if (!!note) note = '\n' + COMMENT_CHAR + this.cleanLine(note)
      }
      let regex = /.*[Rr]each (?<reach>[^\.]+)/g
      let result = regex.exec(line)
      let extra = ''
      let final = ''
      if (!!result?.groups?.reach) {
        // If it has Reach, it is definitely melee
        extra = ' reach ' + result.groups.reach.replace(/ /g, '')
        line = this.cleanLine(line.replace(/[Rr]each [^\.]+/, ''))
        if (!!line) note += '\n' + COMMENT_CHAR + line
        final = '\n' + name + ' (' + lvl + ') ' + dmg + extra
        this.mook.melee += final + note
      } else {
        let ranged = []
        rpats.forEach(p => {
          let re = new RegExp(p.regex)
          let match = line.match(re)
          if (!!match) {
            line = line.replace(re, '').trim()
            if (!!match[1]) ranged.push(p.var + ' ' + match[1])
          }
        })
        if (ranged.length > 0) {
          extra = ranged.join(' ')
          final = '\n' + name + ' (' + lvl + ') ' + dmg + ' ' + extra
          if (!!line) note += '\n' + COMMENT_CHAR + line
          this.mook.ranged += final + note
        } else {
          // but it may not have either, in which case we treat as melee
          final = '\n' + name + ' (' + lvl + ') ' + dmg + extra
          if (!!line) note += '\n' + COMMENT_CHAR + line
          this.mook.melee += final + note
        }
      }
      ;[attblk, line, nextline] = this.gatherAttackLines(attblk, nextline, oldformat) // collect up any more lines.
    }
  }

  mapDmg(line, dmg) {
    if (!dmg) return ['', '']
    dmg = dmg.trim().toLowerCase()
    let p = GURPS.DamageTables.parseDmg(dmg)
    if (p == dmg) return ['', line]
    let a = p.split('~')
    let roll = a[0] + 'd' + a[1] + a[2] + a[3] + a[4]
    let types = a[5].trim().split(' ')
    let m = GURPS.DamageTables.translate(types[0])
    if (!m) return ['', `Unrecognized damage type "${types[0]}" for "${line}"`]
    return [roll + ' ' + m, types.slice(1).join(' ')]
  }

  appendToNotes(t, suffix = ' ') {
    this.mook.notes += t + suffix
  }

  // If the first line does not contain a ":" (or "ST "), then it probably is not the start of the stat block
  checkForNameNotes() {
    this.trim()
    let line = this.nextline()
    if (!line) return
    let curregex = /ST.*\d+.*HP.*\d+/
    let oldregex = /ST.*\d+.*DX.*\d+/
    let forumregex = /^ST:? +\d?\d/

    let first = true
    while (!!line && !line.match(curregex) && !line.match(oldregex) && !line.match(forumregex)) {
      // Assume we haven't started the stat block yet
      // if the first line has 2 or fewer spaces, assume that it is a name.   Just guessing here
      if (first && (line.match(/ /g) || []).length < 3) this.mook.name = line
      else this.appendToNotes(line)
      line = this.nextline()
      first = false
    }
    if (!!this.mook.notes) this.mook.notes += '\n\n'
    this.pushToken(line)
  }

  parseFinalNotes() {
    let postProcessWeapons = this.stealahead('\nWeapons:')
    this.trim()
    let t = this.nextToken()
    if (!!t) {
      if (t == 'Class:') {
        this.appendToNotes(t + ' ' + this.nextline(), '\n')
        return this.parseFinalNotes()
      }
      if (t == 'Notes:') this.appendToNotes(this.statblk)
      else this.appendToNotes(t + ' ' + this.statblk)
    }
    this.mook.notes = this.mook.notes.trim()
    this.statblk = postProcessWeapons
  }

  pushToken(t) {
    this.statblk = t + '\n' + this.statblk
  }

  // This is the exhaustive test to see if we want to parse it as an attribute.
  // Note: we may want to parse some things just so we can safely skip over them
  isAttribute(a) {
    if (!a) return false
    if (a == 'Traits:' || a == 'Advantages/Disadvantages:') return false
    if (a.match(/\w+:/) || !!GURPS.attributepaths[a]) return true
    if (a.match(/\[\d+\],?/)) return true // points costs [20]     accept this to parse, to skip over it
    return POSSIBLE_ATTRIBUTE_KEYS.hasOwnProperty(a.toLowerCase())
  }

  // We know we accept it, however, it may be 'junk' that we are just trying to skip over.
  getAttrib(t) {
    if (t.match(/\[\d+\],?/)) return false // points costs [20]			// Don't count this as "any"
    let a = t.replace(/[^A-Za-z]/g, '') // remove anything but letters
    // Special case is attributes are listed as "basic speed" or "fright check"
    if (!!POSSIBLE_ATTRIBUTE_KEYS[a.toLowerCase()]) return ''
    return a
  }

  storeAttrib(attrib, value) {
    let val = value.replace(/[,;\.]$/g, '') // try to clean up the value by stripping crap off the end
    attrib
      .toLowerCase()
      .split('/')
      .forEach(a => (this.mook[a] = val)) // handles cases like "Parry/Block: 9"
    console.log('Storing attribute: ' + attrib + '=' + val)
  }

  parseAttrs() {
    var attr, val, saved
    this.trim()
    let unknowns = []
    unknowns.push([]) // Each line will have its own array of unparsed keys
    let line = this.nextline()
    saved = line
    let any = false
    do {
      ;[line, attr] = this.nextPrim(line)
      if (!line && !attr) {
        if (!any) break
        line = this.nextline()
        saved = line
        ;[line, attr] = this.nextPrim(line)
        any = false
        unknowns.push([])
      }
      while (this.isAttribute(attr)) {
        // Something we recognize
        let goodattr = this.getAttrib(attr) // An actual attribute
        if (goodattr !== false) any = true // Anything except "false" means "keep looking"
        if (!!goodattr) {
          ;[line, val] = this.nextPrim(line)
          if (!line && !val) {
            line = this.nextline()
            ;[line, val] = this.nextPrim(line)
          }
          this.storeAttrib(goodattr, val)
        }
        ;[line, attr] = this.nextPrim(line)
        if (!line && !attr) {
          if (!any) break
          line = this.nextline()
          saved = line
          ;[line, attr] = this.nextPrim(line)
          any = false
          unknowns.push([])
        }
      }
      if ('Traits:' == attr) break
      if (!!attr) unknowns.slice(-1)[0].push(attr)
    } while (true)
    unknowns.pop() // The last line didn't have any attributes, so not really errors.
    this.pushToken(saved) // We didn't use this line, so put it back
    let a = unknowns
      .map(a => a.join(' '))
      .join(' ')
      .trim() // collapse all unknowns into a string
    if (!!a) {
      // we parsed some things that did not work.
      this.appendToNotes(ERR + ' ' + a, '\n')
    }
  }

  stealahead(str) {
    let i = this.statblk.indexOf(str)
    if (i < 0) return ''
    let s = this.statblk.substr(i + str.length + 1)
    this.statblk = this.statblk.substring(0, 1)
    return s
  }

  peek(str) {
    return this.statblk.includes(str)
  }

  peekskipto(str) {
    return this.peek(str) ? this.nextToken(str, false) : ''
  }

  trim() {
    this.statblk = this.cleanLine(this.statblk)
  }

  nextToken(d1 = ' ', d2 = '\n', all = false) {
    let [s, t] = this.nextTokenPrim(this.statblk, d1, d2, all)
    this.statblk = s
    return t
  }

  nextline() {
    let [s, t] = this.nextlinePrim(this.statblk)
    this.statblk = s
    return t
  }

  next(delim = ' ') {
    let [s, t] = this.nextPrim(this.statblk, delim)
    this.statblk = s
    return t
  }

  nextlinePrim(txt) {
    return this.nextPrim(txt, '\n')
  }

  nextPrim(txt, delim = ' ') {
    return this.nextTokenPrim(txt, delim, false, true)
  }

  nextTokenPrim(str, d1 = ' ', d2 = '\n', all = false) {
    // d2 must be equal or longer in length than d1  ")" and "):"
    if (!str) return [str, undefined]
    let i = str.indexOf(d1)
    let j = str.indexOf(d2)
    if (i >= 0 && j >= 0) {
      if (j <= i) {
        d1 = d2 //
        i = j // Crappy hack to be able to search for 2 delims
      }
      let t = str.substring(0, i)
      return [str.substr(i + d1.length).trim(), t]
    }
    if (i >= 0) {
      let t = str.substring(0, i)
      return [str.substr(i + d1.length).trim(), t]
    }
    if (j >= 0) {
      let t = str.substring(0, j)
      return [str.substr(j + d2.length).trim(), t]
    }
    return all ? ['', str] : [str, undefined]
  }

  nextTokenV2Prim(str, arr, all = false) {
    if (!str) return [str, undefined]
    let best = Number.MAX_SAFE_INTEGER
    let bestIndex = -1
    for (let index = 0; index < arr.length; index++) {
      let d = arr[index]
      let cur = str.indexOf(d)
      if (cur >= 0 && cur < best) {
        best = cur
        bestIndex = index
      }
    }
    if (bestIndex >= 0) {
      let t = str.substring(0, best)
      return [str.substr(best + arr[bestIndex].length).trim(), t]
    } else {
      return all ? ['', str] : [str, undefined]
    }
  }

  cleanLine(line) {
    let start = line
    if (!line) return line
    let pat = '*,.:' // things that just clutter up the line
    if (pat.includes(line[0])) line = line.substr(1)
    if (pat.includes(line[line.length - 1])) line = line.substring(0, line.length - 1)
    line = line.trim()
    return start == line ? line : this.cleanLine(line)
  }
}

export class NpcInputDefaultEditor extends NpcInput {
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ['npc-input', 'sheet', 'actor'],
      id: 'npc-input',
      template: 'systems/gurps/templates/actor/npc-input.hbs',
      resizable: true,
      minimizable: false,
      width: 800,
      height: 650,
      title: 'Mook Generator Defaults Editor',
      closeOnSubmit: true,
    })
  }

  getData(options) {
    let data = super.getData(options)
    data.displaybuttons = false
    return data
  }

  close(options) {
    super.close(options)
    game.settings.set(Settings.SYSTEM_NAME, Settings.SETTING_MOOK_DEFAULT, this.mook)
  }

  setTesting(t = true) {
    // Do nothing in the Defaults Editor
  }
}

class Mook {
  constructor() {
    this.name = ''
    this.title = 'bad guy'
    this.desc = 'appearence'
    this.st = 10
    this.dx = 10
    this.iq = 10
    this.ht = 10
    this.dodge = 0
    this.parry = 0
    this.hp = 10
    this.will = 10
    this.per = 10
    this.fp = 10
    this.speed = 0
    this.move = 0
    this.sm = '+0'
    this.dr = 0
    this.notes = `Notes.   May include On-the-Fly formulas
[IQ to remember something] [Dodge] [+2 Blessed]`
    this.traits = `Ugly [-4 from everyone]
High Pain Threshold; [CR: 12 Gambling]`
    this.skills = `Barter-14
Search-13;Lockpicking-11`
    this.melee = `Punch (12) 1d-2 cr
Kick (11) 1d cr`
    this.ranged = `Slingshot (9) 1d-3 imp acc 2`
    this.spells = 'Create Water-11'
    this.equipment = 'Item; 1; $15; 0.5 lbs'
  }
}

let EX = [
  // 0

  `Goblin
Goblins are the smallest of the goblin-kin, and therefore
spend their days being bullied by orcs and tossed around by
angry hobgoblins. This has led to a cowardly disposition,
yet they�fre survivors, and deadlier in a fight than the typical
human. In combat, they prefer stealthy ambushes involving
ranged (and preferably poisoned) weapons, followed by running
away.
Goblins stand 2�h shorter than humans on average, but
like all goblin-kin are densely built and thus no lighter. They
resemble nothing so much as misshapen, hunchbacked elves
with pointy ears and needle-like teeth. Skin tones vary greatly,
but tend toward the greenish.
ST: 11 HP: 12 Speed: 6.00
DX: 11 Will: 10 Move: 4
IQ: 9 Per: 10
HT: 11 FP: 11 SM: 0
Dodge: 8 Parry/Block: 9 DR: 2
Bite (13): 1d-1 cutting. Reach C.
Kick (11): 1d+1 crushing (includes +1 for heavy boots).
Reach C, 1.
Long Knife (13): 1d cutting or 1d-1 impaling. Reach C, 1.
Punch (13): 1d-1 crushing. Reach C.
Shield Bash (12): 1d-1 crushing. Reach 1.
Short Bow (13): 1d-1(2) piercing + follow-up 2 points toxic
(HT to resist). Ranged, with Acc 1, 1/2D 110, Max 165,
Shots 1(2), Bulk -6.
Traits: Appearance (Ugly); Cowardice (12); Infravision;
Rapid Healing; Resistant to Disease 5; Resistant to Poison
5; Social Stigma (Savage).
Skills: Bow-13; Brawling-13; Knife-13; Shield-12; Stealth-12.
Class: Mundane.
Notes: Equipped with heavy leather armor (DR included
above; thoroughly lice-ridden, stinking, and unsalable),
small shield (DB 1), long knife, short bow, and 10 bodkin
arrows poisoned with monster drool. This puts the goblin
at Light encumbrance, as reflected in the stats. A nonwarrior
would have ST 10 (and lower damage), DX 10, HP
11, Speed 5.25, and reduced combat skills. Leaders have
IQ 10+ and higher skills, and often trade bow and arrows
for a saber (1d cutting or impaling) to wave around while
giving orders. Shamans have IQ 10+ and Power Investiture
1-3 . and goblin gods grant their clerics nasty wizardly
spells such as Deathtouch! Goblins are easily intimidated,
so they�fll negotiate if cornered . . . and backstab as soon as
they aren�ft.`,
  // 1
  `ST: 13 HP: 13 Speed: 5.00
DX: 11 Will: 9 Move: 5
IQ: 9 Per: 12
HT: 11 FP: 11 SM: 0
Dodge: 8 Parry: 9 DR: 1
Kick (11): 1d+1 crushing. Reach C, 1.
Punch (13): 1d crushing. Reach C.
Stone-Headed Club (12): 2d+3 crushing. Reach 1.
Traits: Animal Empathy; Appearance (Unattractive); Arm ST
1; Brachiator (Move 3); Social Stigma (Savage); Temperature
Tolerance 2 (Cold).
Skills: Axe/Mace-12; Brawling-13; Camouflage-12; Climbing-
13; Stealth-12; Tracking-12; Wrestling-12.
Class: Mundane.
Notes: Effective ST 15 when grappling, thanks to Arm ST
and Wrestling. These stats represent a wildman; females
are'nt often warriors, and have ST 12 (and lower damage),
DX 10, HP 12, and reduced combat skills, but superior
Camouflage and Stealth, and a tendency to climb up high
and pelt foes with large stones (Throwing-12, 1d-2 crushing)
to support their males and guard beasts. A wildman
generally carries a stone-tipped club (treat as a mace)
and wears hides (DR 1, included above); more advanced
gear is extremely unlikely, and wildman conscripts given
such equipment never get used to it: .2 to combat skills.
Spellcasters are always shamans with IQ 10+, Power Investiture
1-3, and druidic spells. Wildmen will negotiate with
anyone who hasn't violated one of their taboos.
`,
  // 2
  `STEALTH GOLEM
ST 21; DX 16*; IQ 11; HT 14.
Will 13; Per 16*; Speed 9.00; Dodge 12*; Move 9*.
SM 0; 300 lbs.
Traits: Absolute Direction; Automaton; Cannot Float; Danger
Sense*; Doesn't Breathe; DR 4; Fragile (Unnatural);
Indomitable; Machine; Night Vision 9*; Nocturnal (Can
function weakly out of direct sunlight); Payload 1; Perfect
Balance*; Reduced Consumption (Based on Powerstone);
Reprogrammable; Single-Minded; Unaging; Unfazeable;
Vacuum Support.
Skills: Acrobatics-15; Brawling-18; Climbing-18; Cloak-15;
Escape-16; Filch-16; Forced Entry-17; Garrote-18; Holdout-
15; Knife-18; Lockpicking-16; Observation-16; Pickpocket-
16; Search-17; Shadowing-14; Shortsword-16; Staff-16;
Stealth-18; Tactics-12; Traps-14.
* During daylight hours, the stealth golem has DX 10, Per
10, Dodge 8, Move 5, and loses these specific advantages.
Modify all skills accordingly.`,
  // 3

  `ST: 11 HP: 11 Speed: 8.00
DX: 13 Will: 8 Move: 8
IQ: 8 Per: 8
HT: 12 FP: N/A SM: 0
Dodge: 11 Parry/Block: 10 DR: 2
Bony Claw (14): 1d-1 crushing. Reach C.
Longbow (14): 1d+1 impaling. Ranged, with Acc 3, 1/2D 165,
Max 220, Shots 1(2), Bulk -8.
Shield Bash (14): 1d-1 crushing. Reach 1.
Weapon (14): Axe (1d+3 cutting), shortsword (1d+1 cutting
or 1d impaling), small mace (1d+3 crushing), spear (1d+1
impaling), etc. Reach 1.
Traits: Appearance (Monstrous); Automaton; Brittle; Cannot
Float; Cannot Learn; Dependency (Loses 1 HP per minute
in no-mana areas); Doesn�ft Breathe; Doesn�ft Eat or Drink;
Doesn�ft Sleep; High Pain Threshold; Immunity to Disease;
Immunity to Mind Control; Immunity to Poison; Indomitable;
Mute; No Blood; No Brain; No Eyes; No Sense of
Smell/Taste; No Vitals; Reprogrammable; Single-Minded;
Skinny; Temperature Tolerance 5 (Cold); Temperature
Tolerance 5 (Heat); Unfazeable; Unhealing (Total); Unliving;
Unnatural; Vulnerability (Crushing).
Skills: Bow-14; Brawling-14; Climbing-13; Knife-13; Shield-14;
Stealth-13; one of Axe/Mace-14, Shortsword-14, or
Spear-14.
Class: Undead.
Notes: Skull DR is still only 2. Unaffected by Death Vision or
Sense Life, but susceptible to Pentagram, Sense Spirit, and
Turn Zombie. This skeleton is made from a bandit, castle
guard, militiaman, or other low-end warrior, and equipped
as a skirmisher and archer: one-handed melee weapon,
small shield (DB 1), longbow, and 10-20 arrows. More
impressive fighters can have better combat stats and gear .
maybe even armor fit for a skeleton! Though not truly evil,
the magic animating it usually is. No undead servitor will
negotiate or reveal useful information.
`,
  // 4
  `Fire Elemental
A mobile flame with a roughly humanoid shape. In the
wild, these beings lurk in or near volcanoes and lava, but they
sometimes come out to play in (or set) wildfires. A fire elemental
is hard to harm: it has DR 6, is Diffuse, cannot be harmed
in any way by heat or fire, and tends to destroy wooden weapons
used to strike it!
ST: 15 HP: 17 Speed: 6.00
DX: 12 Will: 10 Move: 6/12
IQ: 8 Per: 8
HT: 12 FP: 12 SM: +1
Dodge: 9 Parry: N/A DR: 6
Fiery Blow (12): 1d burning + halo of flame, below. Reach
C, 1.
26 The Be stia ry
Firebolt (15): Costs 1 FP per use. 2d burning. Ranged, with
Acc 3, 1/2D 10, Max 100.
Halo of Flame: 2d burning to anyone touched by elemental
or touching it in close combat. This can destroy wooden
weapons (Damage to Objects, Exploits, pp. 55-56), though
the danger should be obvious beforehand.
Traits: Bad Temper (12); Diffuse; Doesn ft Breathe (but
see notes); Doesn ft Eat or Drink; Doesn ft Sleep;
Enhanced Move (Ground); Immunity to Disease;
Immunity to Heat/Fire; Immunity to Poison;
No Fine Manipulators; No Neck; Pyromania (9);
Weakness (1d HP if immersed in water, repeating
every minute).
Skills: Innate Attack (Projectile)-15.
Class: Elemental.
Notes: Fire elementals don ft breathe and can ft be
gassed or strangled, but require air in order to
burn, experiencing Suffocation (Exploits, p. 70)
without it: FP loss, and then HP loss until death. Summoned
elementals add Reprogrammable and Unnatural;
they vanish instantly if wounded to -1HP, and can also be
dismissed by the Banish spell (Spells, pp. 59-60).

`,
  // 5
  `ST: 23 HP: 23 Speed: 6.50
DX: 14 Will: 13 Move: 10 (Air Move 13)
IQ: 5 Per: 12
HT: 12 FP: 12 SM: +1
Dodge: 10 Parry: 12 (�2) DR: 2
Dragon's Head (16): Bite or horns, 2d+2 cutting. Horns count
as weapon, not as body part, both to attack and parry!
Reach C, 1.
Fire Breath (16): 2d+1 burning in a 1-yard-wide � 10-yardlong
cone that inflicts large-area injury (Exploits, p. 53);
see Area and Spreading Attacks (Exploits, pp. 45-46). Costs
2 FP per use, with no recharge time or limit on uses/day.
Front Claw (16): 2d+2 cutting. Reach C, 1.
Goat's Head (16): Horns, 2d+2 impaling. Treat as weapon,
not as body part, both to attack and parry! Reach C, 1.
Hind Claw (14): 2d+3 cutting. Reach C, 1.
Lion's Head (16): Bite, 2d+2 cutting. Reach C, 1.
Serpent's Head (16): Bite (at only ST 18), 1d+2 impaling +
follow-up 2d toxic, or 1d with a successful HT roll. Reach
C, 1.
Traits: 360� Vision; Bad Temper (9); Combat Reflexes; DR
2 vs. heat/fire only; Extra Attack 3; Extra-Flexible; Extra
Heads 3; Flight (Winged); Night Vision 5; Penetrating
Voice; Quadruped; Temperature Tolerance 2 (Heat); Wild
Animal.
Skills: Brawling-16; Innate Attack (Breath)-16.
`,
  // 6
  `Zombie
Rotting corpses reanimated by dark necromancy . not by
strange contagion or other  gnatural h causes . are by far the
most common undead servitors. There isn ft a lich (p. 40) out
there without a small army of these, and vampires (pp. 58-59)
employ them as well. Zombies cannot be bribed or corrupted,
but their mental faculties are so limited that they fre useful
only as fodder in a fight, or for menial tasks such as turning
winches and carrying palanquins.
Truly evil monsters turn cadavers into zombies by binding
evil spirits within or using mass possession. Turning
(Adventurers, p. 21) affects such undead. However, possession
lets the reanimator share up to its own level of Resist Good
(p. 11) with its servants . maybe even borrow their senses!
Zombies are Unliving and slightly harder to injure, but also
Unnatural and thus dispelled at -1 HP.
ST: 13 HP: 17 Speed: 6.00
DX: 12 Will: 8 Move: 4
IQ: 8 Per: 8
HT: 12 FP: N/A SM: 0
Dodge: 8 Parry/Block: 9 DR: 2
Punch (13): 1d-1 crushing. Reach C.
Shield Bash (13): 1d crushing. Reach 1.
Weapon (12 or 13): Axe (2d+1 cutting), broadsword (2d cutting
or 1d+2 impaling), mace (2d+2 crushing), morningstar
(2d+2 crushing), etc. Reach 1.
Traits: Appearance (Monstrous); Automaton; Bad Smell;
Cannot Learn; Dependency (Loses 1 HP per minute in
no-mana areas); Disturbing Voice; Doesn ft Breathe;
Doesn ft Eat or Drink; Doesn ft Sleep; High Pain Threshold;
Immunity to Disease; Immunity to Mind Control;
Immunity to Poison; Indomitable; No Blood; No Sense of
Smell/Taste; Reprogrammable; Single-Minded; Temperature
Tolerance 5 (Cold); Temperature Tolerance 5 (Heat);
Unfazeable; Unhealing (Total); Unliving; Unnatural.
Skills: Brawling-13; Shield-13; Wrestling-13; one of
Axe/Mace-13, Broadsword-13, or Flail-12.
Class: Undead.
Notes: Unaffected by Death Vision or Sense Life, but susceptible
to Pentagram, Sense Spirit, and Turn Zombie. Effective
grappling ST is 14, thanks to Wrestling. This zombie is
made from a beefy gang enforcer, foot soldier, or similar
melee fighter, and equipped as a bargain-basement shock
trooper: one-handed melee weapon, medium shield (DB
2), and heavy leather armor (DR 2, included above). This
results in Light encumbrance, which is already figured into
the stats. Zombies will rot, eventually becoming skeletons
(pp. 47-48) if they last long enough . though some are preserved
as mummies with IQ 10, No Brain, and No Vitals,
but which catch fire and burn for 1d-1 injury per second if
they receive a major wound from fire. Not truly evil, though
the magic animating it usually is. No undead servitor will
negotiate or reveal useful information.
`,
  // 7
  `ST: 18 HP: 18 Speed: 6.00
DX: 12 Will: 10 Move: 9
IQ: 10 Per: 11
HT: 12 FP: 12 SM: 0
Dodge: 10 Parry: 11 (unarmed) DR: 15 (not vs. silver)
Bite or Claw (14): 2d+1 cutting. Reach C.
Traits: Acute Hearing 3; Acute Taste and Smell 3; Alternate
Form (Human); Appearance (Hideous); Bloodlust
(12); Combat Reflexes; Discriminatory Smell; Disturbing
Voice; Dread (Wolfsbane; 1 yard); Gluttony (12); High
Pain Threshold; Immunity to Disease; Immunity to Poison;
Night Vision 2; No Fine Manipulators; Odious Racial
Habit (Eats other sapient beings, .3 reactions); Penetrating
Voice; Recovery; Regeneration (1 HP/second, but not vs.
damage from silver); Silence 1; Striking ST 4; Temperature
Tolerance 5 (Cold); Vulnerability (Silver).
Skills: Brawling-14; Stealth-12 (13 vs. Hearing if moving, 14 if
motionless); Tracking-15.
Class: Mundane.
Notes: Hearing roll is 14 and Smell roll is 18 for detecting
delvers! Individuals may be bigger (more ST, HP, and
Striking ST), sneakier (higher Night Vision and Silence), or
more skilled. Clawed hands prevent weapon use. Against a
group carrying wolfsbane and bristling with silver weapons,
werewolves will stay hidden or pretend to be human .
but if they can�ft, they�fll negotiate. Truly evil.


`,
  // 8
  `ST: 15 HP: 15 Speed: 8.00
DX: 18 Will: 15 Move: 8
IQ: 12 Per: 15 Weight: 100.150 lbs.
HT: 13 FP: 13 SM: 0
Dodge: 12 Parry: 14 DR: 2 (Tough Skin)
Fright Check: -4 (once maw is open)
Bite (20): 3d+1 cutting. Often aimed at the neck; see text. May
bite and use webbing on the same turn. Reach C.
Punch (20): 1d+1 crushing. Reach C.
Webbing (20): Binding ST 25 (p. B40) with Engulfing and
Sticky. Range 50, Acc 3, RoF 10, Rcl 1. The rate of fire may
be split up among multiple foes; e.g., three strands at the
commando, three at the sage, and four
at the warrior, resolved as three separate
attacks.
Traits: Ambidexterity; Appearance
(Beautiful); Clinging;
Combat Reflexes; Danger
Sense; Extra Attack (see Webbing,
above); Honest Face;
Infravision; Injury Tolerance
(No Brain; No Vitals; Unliving;
see notes); Restricted Diet
(People); Striking ST 12 (Bite
Only; Nuisance Effect, Hideous
Appearance); Subsonic
Hearing; Super Jump 2.
Skills: Acrobatics-18; Acting-14;
Brawling-20; Innate Attack
(Projectile)-20; Musical Instrument
(varies)-12; Jumping-20;
Sex Appeal-15; Singing-14;
Stealth-20.
Notes: Living being!  gUnliving h
simply reflects its odd physiology.
As well, it has a brain
and vitals, but not where
you fd expect; knowing where
to stab requires a successful
roll against Biology at -4,
Hidden Lore (Cryptozoology),
Theology (Shamanic) at -2, or
Veterinary. In combat, can
leap 11 yards forward or three
yards straight up; double this
out of combat, double it with
a running start, quadruple it
for both.
`,
  // 9
  `Guards
ST 10; DX 10; IQ 9; HT 11.
Damage 1d-2/1d; BL 20 lbs.; HP 10; Will 9; Per 10; FP 11.
Basic Speed 5.25; Basic Move 5; Dodge 8; Parry 9 (Brawling).
5�6�-6�; 150-170 lbs.
Advantages/Disadvantages: Cantonese (Native).
Skills: Brawling-13; Guns/TL8 (Pistol)-12; Guns/TL8
(SMG)-12; Knife-14.
`,
  // 10
  `The Mate
ST 11; DX 12; IQ 10; HT 11.
Damage 1d-1/1d+1; BL 24 lbs.; HP 11; Will 10; Per 12; FP 11.
Basic Speed 5.75; Basic Move 5; Dodge 9; Parry 10 (Knife).
5�8�; 155 lbs.
Advantages/Disadvantages: Acute Hearing 2; Cantonese
(Native); Combat Reflexes.
Skills: Brawling-14; Guns/TL8 (Pistol)-14; Guns/TL8
(SMG)-14; Knife-15.
`,
  // 11
  `Notwithstanding the page name, this guy is not a Nazi party member. He's just a German soldier in 1939. His main motivation is the same as that of many young men like him in his day, patriotism.
This is a very generic German infantryman, with no special personal Traits, save the most common ones.
His Attributes and Skills reflect the fact that in 1939, the average soldier was pretty thoroughly trained and physically conditioned; also, he belongs to a first-class division, in which most privates would be young, healthy and bright for their age.
Height: 5'11", weight: 155 lbs., age: 21.
ST: 11 	HP: 11 	Speed: 5.5
DX: 11 	Will: 11 	Move: 4
IQ: 11 	Per: 11 	
HT: 11 	FP: 11 	SM: 0
Dodge: 7 	Parry: 8 	DR: 4,0,2

Mauser Karabiner 98K 7.92mm Mauser (13): 7d pi
Bayonet, Fine (7): 1d cut, Reach C,1; 1d imp, Reach C
Rifle-fixed bayonet thrust (10): 1d+3 imp, Reach 1,2*
Straight rifle-butt thrust (8): 1d+1 cr, Reach 1
Punch (12): 1d-2 cr, Reach C
Kick (10): 1d cr, Reach C,1

Traits: Addiction (Tobacco); Duty (Heer; 15 or less; Extremely Hazardous); Fanaticism (Patriotism); Fit.
Skills: Armoury/TL6 (Small Arms)-10; Brawling-12; Camouflage-11; Climibing-10; Explosives/TL6 (Demolition)-10; First Aid/TL6-11; Gambling-10; Gunner/TL6 (Machine Gun)-11; Guns/TL6 (Light Machine Gun)-12; Guns/TL6 (Rifle)-13; Hiking-10; Jumping-11; Navigation/TL6 (Land)-10; Savoir-Faire (Military)-11; Scrounging-11; Soldier/TL6-12; Spear-10; Stealth-10; Survival (Woodlands)-10; Swimming-11; Teamster (Equines)-10; Throwing-10; Traps/TL6-10.
`,
  // 12
  `Crystal Rat-Men
The process which changes giant rats into rat-men imbues
some with strange abilities. Crystal rat-men have long crystalline
claws which cut through armor easily, and their skin
is studded with lumps of translucent stone which provide a
modicum of protection. They can even hurl needle-like crystal
spines (which shatter into uselessness after impact) at enemies
with a flick of the hand.
ST: 11 HP: 11 Speed: 6.50
DX: 13 Will: 10 Move: 6
IQ: 7 Per: 10
HT: 13 FP: 13 SM: 0
Dodge: 9 Parry: 10 (unarmed) DR: 2
Bite (15): 1d-(2) cutting. Reach C.
Kick (13): 1d(2) cutting. Reach C, 1.
Punch (15): 1d-1(2) cutting. Reach C.
Thrown Spine (15): 1d-2(2) impaling. Ranged, with Acc 0,
1/2D 5, Max 11.
Traits: Appearance (Ugly); Berserk (12); Fanaticism; High
Pain Threshold; Night Vision 5; Resistant to Disease 5;
Resistant to Poison 5; Spider Climb (Move 4).
Skills: Brawling-15; Innate Attack (Projectile)-15; Stealth-12.
Class: Mundane.
Notes: Unlike regular rat-men, crystal rat-men attack barehanded
but are capable of innate ranged attacks. However,
crystal rat-men are liable to be overtaken by rage when
they fight, so their battles tend to end spectacularly, one
way or another.`,
  // 13
  `Tsorvano
273 points
Tsorvano is an ex-brigand who met Halmaro years
ago at sword�s point, while Halmaro was an aggressive
young caravan-master. Neither one will say who won
that first encounter, but soon afterward Tsorvano
became the merchant�s loyal henchman.
Tsorvano wears his broadsword for everyday use, but
if he expects trouble, he�ll sling his greatsword on his back.
Early 40s; Swarthy, bald, hooked nose, brilliant
green eyes; 6�4�, 220 lbs.
Attributes: ST 13 [30], DX 14 [80], IQ 13 [60], HT 12
[20]
Secondary Characteristics: Dmg 1d/2d-1, BL 34, HP
13, Will 13, Per 13, FP 12, Basic Speed 6.50, Basic
Move 6, Dodge 9, Parry 11.
No armor, no encumbrance.
Advantages: Damage Resistance DR 1 (Tough Skin,
-40%) [3]; Wealth (Wealthy) [20].
Disadvantages: Miserly (12) [-10]; Sense of Duty
(Halmaro and Guild) [-10]; Stubborn [-5].
Quirks: Dotes on Halmaro�s daughters; Dislikes clerics,
will always wear the minimum for comfort and propriety;
Enjoys embarrassing his inferiors; Likes
open spaces; Very cold to strangers. [-5]
Skills: Broadsword (A) DX+2 [8]-16; Desert Survival
(A) Per+3 [12]-16; Fast-Draw (Two-Handed Sword)
(E) DX [1]-14; Fast-Draw (Knife) (E) DX [1]-14;
Fast-Talk (A) IQ+2 [8]-15; Knife (E) DX+3 [8]-17;
Kalba (musical instrument) (H) IQ+1 [8]-14;
Merchant (A) IQ+3 [12]-16; Mountain Survival (A)
Per+2 [8]-15; Two-Handed Sword (A) DX+3 [12]-17.
Languages: Lantrai-Native [0] (default); Shandassa-
Native [6]; Nomic-Native [6]; Ayuni Trade Pidgin-
Broken [0] (default).
Weapons: Broadsword: 2 dice cutting, 1d+1 crushing;
Greatsword: 2d+2 cutting, 1d+2 crushing; Large
Knife: 2d-3 cutting, 1 die impaling.`,
]
