import re import utils with open('parser/verified.txt') as f: verified = f.read().split('\n') # action formatted {'entry': entry, 'name': name, 'text': text, 'type': type} def procAttackAction(desc, action, weapons): assert re.match('.*Attack:', action['text']) action['type'] = 'attack' details = {} details['range'] = [0, 0] details['reach'] = 0 for rangereach in ['range', 'reach']: #rangeMatch = re.search('{} (\d+(?:/\d+)?) ft'.format(rangereach), action['text']) rangeMatch = re.search('{}d? (\S+ ft)'.format(rangereach), action['text']) if rangeMatch: distance = rangeMatch.group(1) if '/' in distance and rangereach == 'range': distance = [int(part.split('ft')[0].strip()) for part in distance.split('/')] else: distance = int(distance.split('ft')[0].strip()) if rangereach == 'range': distance = [distance, distance] details[rangereach] = distance details['properties'] = [] toHit = int(re.search('\+(\d+) to hit', action['text']).group(1)) toHitOverride=None dmgBonusOverride=None abilityOverride=None if details['reach'] > 0 and toHit != desc['prof'] + desc.getBonus('str'): # Maybe it's dex? if desc.getBonus('dex') > desc.getBonus('str') and toHit == desc['prof'] + desc.getBonus('dex'): #print(f'Added finesse to {desc["name"]} attack {action["name"]}.') details['properties'].append('finesse') else: toHitOverride = toHit if details['reach'] == 0 and details['range'][0] == 0: toHitOverride = toHit elif details['reach'] == 0 and toHit != desc['prof'] + desc.getBonus('dex'): # Maybe it's str? if toHit == desc['prof'] + desc.getBonus('str'): abilityOverride = 'str' if 'spell' in action['text'].split(':')[0].lower(): # See if it's int, wis, or cha for a in ['int', 'wis', 'cha']: if toHit == desc['prof'] + desc.getBonus(a): abilityOverride = a break targetQuals = re.search('(?<=ft).*?(?=_Hit)', action['text']).group(0).split('ft')[-1] targetQuals = targetQuals.replace('.', '') if targetQuals.startswith(','): targetQuals = targetQuals[1:] targetQuals = targetQuals.strip() if targetQuals and targetQuals != 'one target' and targetQuals != 'one creature': details['properties'].append(targetQuals) details['damage'] = [] # It could be something like "1 piecring damage" (see sprite). dmgSection = re.search('_Hit:_ .*?\.', action['text']).group(0) if 'DC' in dmgSection: dmgSection = dmgSection.split('DC')[0] dmgMatch = 'foobar' # Something that won't match first = True for dmgMatch in re.findall('(?:plus |or )?\d+(?: \(\d+d\d+[\+− \d]*\))? [a-z]* damage', dmgSection): if first: first = False try: bonus = dmgMatch.split('(')[1].split(')')[0].split(' ')[-1] if 'd' in bonus: bonus = 0 bonus = int(bonus) if bonus != 0 and dmgMatch.split('(')[1].split(')')[0].split(' ')[-2] != '+': bonus *= -1 if not (bonus == desc.getBonus('str') or (('finesse' in details['properties'] or (details['reach'] == 0 and details['range'][0] > 0)) and bonus == max(desc.getBonus('str'), desc.getBonus('dex')))): dmgBonusOverride = bonus except: pass #print(f'Exception calculating damage bonus for {desc["name"]} attack {action["name"]}') isOr = 'or ' in dmgMatch if re.match('\d+ [a-z]* damage', dmgMatch): details['damage'].append({ 'dmg_die_count': int(dmgMatch.split(' ')[0]), 'dmg_die_sides': int(dmgMatch.split(' ')[0]), 'dmg_type': re.search('[a-z]+(?= damage)', dmgMatch).group(0), 'is_or': isOr # Always false }) else: toAppend = { 'dmg_die_count': int(re.search('\d+(?=d\d)', dmgMatch).group(0)), 'dmg_die_sides': int(re.search('(?<=\dd)\d+', dmgMatch).group(0)), 'dmg_type': re.search('[a-z]+(?= damage)', dmgMatch).group(0), 'is_or': isOr } if isOr and toAppend['dmg_type'] == details['damage'][-1]['dmg_type'] and toAppend['dmg_die_sides'] == details['damage'][-1]['dmg_die_sides'] + 2: details['properties'].append('versatile') else: details['damage'].append(toAppend) if abilityOverride: print(f'Overriding ability for {desc["name"]} ranged weapon {action["name"]}.') if toHitOverride: print(f'Overriding toHit bonus for {desc["name"]} weapon {action["name"]}.') if dmgBonusOverride: print(f'Overriding damage bonus for {desc["name"]} attack {action["name"]} to {bonus}') # Text is what we get when we split on the final dmgMatch texts = action['text'].split(dmgMatch)[1:] if len(texts) == 0: texts = [re.search('(?s)(?<=_Hit:_ ).*', action['text']).group(0).strip()] elif len(texts) > 1: print(f'ERROR processing wepon text for {desc["name"]}, attack {action["name"]}') details['text'] = texts[0].strip() # We may need to move some parts of the name elsewhere if '(' in action['name']: details['properties'].append('('.join(action['name'].split('(')[1:])[:-1]) #details['text'] = '(' + '('.join(action['name'].split('(')[1:]) + " " + details['text'] action['name'] = action['name'].split('(')[0].strip() if action['name'][-1] == '.' or action['name'][-1] == ':': action['name'] = action['name'][:-1].strip() action['attack'] = {} for name, value in utils.formatWeapon(action['name'], details['range'][0], details['range'][1], details['reach'], details['damage'], details['text'], properties=details['properties'], toHitOverride=toHitOverride, dmgBonusOverride=dmgBonusOverride, abilityOverride=abilityOverride).items(): action['attack'][name] = value # Differentiate between spell attacks and weapons attacks if 'spell' in action['text'].split(':')[0].lower(): action['attack']['type'] = 'spell attack' if desc['name'].lower() in verified: action['text'] = '' # Some weapons are non-standard, even if the name matches something "known". if (len(details['damage']) > 1 and not details['damage'][1]['is_or']) or (len(details['damage']) >= 1 and details['damage'][0]['dmg_die_count'] != (2 if action['name'] == 'greatsword' or action['name'] == 'maul' else 1)) or (len(details['damage']) >= 1 and action['name'] != 'blowgun' and details['damage'][0]['dmg_die_sides'] == 1): action['attack']['weapon_type'] = 'unknown' if action['attack']['weapon_type'] != 'unknown': desc['inventory'].append({'entry': 'item', 'name': action['attack']['name'], 'type': 'weapons', 'text': details['text']}) # action formatted {'entry': entry, 'name': name, 'text': text, 'type': type} def procSpellAction(desc, action, spells): assert 'spellcasting' in action['name'] action['type'] = 'spells' #print('{} has spellcasting!'.format(desc['name'])) abilities = ['Intelligence', 'Wisdom', 'Charisma'] for ability in abilities: if ability in action['text']: if 'spellcasting_ability' in action: print('Uh oh, both {} and {} present!'.format(action['spellcasting_ability'], ability)) exit(1) action['spellcasting_ability'] = ability.lower()[:3] if 'spellcasting_ability' not in action: print('Uh oh, no spellcasting ability!') exit(1) # Interpretation of slots differs if is innate or not action['innate'] = 'innate' in action['text'] # Now, break down by level action['levels'] = [] def getSpells(text): names = [] [names.extend(m.split(', ')) for m in re.findall('(?<=\*\*_).*?(?=_\*\*)', text)] #ret = [] for name in names: found = False for spell in spells: if spell['name'] == name: #ret.append(spell) found = True break if not found: print('Could not find spell: {}!!!!!!!!!!!!'.format(name)) #return ret return names # Sometimes it is in the name (as is case with mephits) if '/day' in action['name']: slots = int(re.search('(?<=\()\d+', action['name']).group(0)) spellNames = getSpells(action['text']) action['levels'].append({'slots': slots, 'spells': spellNames}) else: # Repair pit fiend m = re.match('(?sm)^(.+) \d+/day each: ', action['text']) if m: action['text'] = re.sub(re.escape(m.group(0)), m.group(0) + '**_', action['text']) action['text'] = re.sub(re.escape(m.group(1)), m.group(1) + '_**\n', action['text']) for line in action['text'].split('\n')[1:]: line = line.lower() if all(string not in line for string in ['(', '/day', 'at will']): continue #print('Searching line {}'.format(line)) # If it's "at will", then slots = 0 if 'at will' in line: slots = 0 else: slots = int(re.search('(\d+)( slot|/day)', line).group(1)) spellNames = getSpells(line) action['levels'].append({'slots': slots, 'spells': spellNames}) action['name'] = 'spellcasting'