#!/usr/bin/env python3 import json import re from fractions import Fraction docsLoc = 'parser/5thSRD/docs/' skillsByAbility = {'str': ['Athletics'], 'dex': ['Acrobatics', 'Sleight of Hand', 'Stealth'], 'con': [], 'int': ['Arcana', 'History', 'Investigation', 'Nature', 'Religion'], 'wis': ['Animal Handling', 'Insight', 'Medicine', 'Perception', 'Survival'], 'cha': ['Deception', 'Intimidation', 'Performance', 'Persuasion']} armorByType = {'light': [('padded', 11), ('leather', 11), ('studded leather', 12)], 'medium': [('hide', 12), ('chain shirt', 13), ('scale mail', 14), ('breastplate', 14), ('half plate', 15)], 'heavy': [('ring mail', 14), ('chain mail', 16), ('splint', 17), ('plate', 18)], 'misc': [('shield', 2), ('ring of protection', 1)]} def procSkill(skillStr): skill = skillStr.strip() skillName = re.search('[^\+]*?(?= \+)', skill).group(0) skillBonus = int(re.search('\+(\d+)', skill).group(1)) ability = '' for a in skillsByAbility: if skillName in skillsByAbility[a]: ability = a if not ability: print('Could not find ability for skill {}'.format(skillName)) return skillName, skillBonus, ability def guessProficiency(desc): if desc['cr'] <= 1: return 2 def getBonus(ability): return (desc['stats'][ability] - 10) // 2 # Guess proficiency based on saves, skills, and attacks if desc['saves']: for save in desc['saves'].split(','): save = save.strip() ability = save.split(' ')[0].lower() saveBonus = int(save.split('+')[1]) return saveBonus - getBonus(ability) # This is the answer. skillGuesses = [] if desc['skills']: for skill in desc['skills'].split(','): skillName, skillBonus, ability = procSkill(skill) skillGuesses.append(skillBonus - getBonus(ability)) attackGuesses = [] for action in desc['actions'].values(): if re.match('.*Attack:', action): toHit = int(re.search('\+(\d+) to hit', action).group(1)) dmgBonusMatch = re.search('\d+d\d+ (\+|−) (\d+)\)', action) if dmgBonusMatch: dmgBonus = int(dmgBonusMatch.group(2)) if dmgBonusMatch.group(1) == '−': #print('We match here for the {}!'.format(desc['name'])) dmgBonus *= -1 else: dmgBonus = 0 if toHit - dmgBonus > 1: attackGuesses.append(toHit - dmgBonus) if not skillGuesses and not attackGuesses: print('We got here for the {}!'.format(desc['name'])) return 2 else: profGuesses = skillGuesses + attackGuesses if min(profGuesses) != 0 and any(guess % min(profGuesses) != 0 for guess in profGuesses): print('We had conflicting guesses for {}: {}'.format(desc['name'], profGuesses)) best = (0, 0) for guess in profGuesses: numHappy = sum(1 for other in profGuesses if other % guess == 0) if numHappy > best[1]: best = (guess, numHappy) return best[0] def cost2copper(cost): amnt = int(cost.split(' ')[0].replace(',', '')) den = cost.split(' ')[1] if den == 'pp': return amnt * 1000 elif den == 'gp': return amnt * 100 elif den == 'ep': return amnt * 50 elif den == 'sp': return amnt * 10 elif den == 'cp': return amnt def getArmor(): with open(docsLoc + '/adventuring/equipment/armor.md') as f: data = f.read() tables = re.search('(?sm)(?<=## Armor Table).*?(?=##)', data).group(0) armors = [] header = '' for armor in re.findall('\| (.*) \| (.*) \| (.*) \| (.*) \| (.*) \| (.*) \|', tables): armor = [part.strip().lower() for part in armor] if armor[1] == 'cost': header = armor[0] else: armors.append({'entry': 'item', 'type': 'armor', 'name': armor[0], 'cost': cost2copper(armor[1]), 'ac': int(armor[2].split(' ')[0]), 'strength': int(armor[3].replace('-', '0').split(' ')[-1]), 'disadvantage': armor[4] == 'disadvantage', 'weight': float(armor[5].split(' ')[0]), 'armor_type': header.split(' ')[0], 'text': '. Provided from PHB.'}) return armors weapons = [] def getWeapons(): global weapons if weapons: return weapons with open(docsLoc + '/adventuring/equipment/weapons.md') as f: data = f.read() special = {} for s in ['Lance', 'Net']: special[s.lower()] = re.search('(?<=\*\*{}.\*\*).*'.format(s), data).group(0).strip() tables = re.search('(?sm)## Weapons Table.*', data).group(0) weapons = [] header = '' for weapon in re.findall('\| (.*) \| (.*) \| (.*) \| (.*) \| (.*) \|', tables): weapon = [part.strip().lower() for part in weapon] if weapon[1] == 'cost': header = weapon[0] else: name = weapon[0] if ',' in name: parts = name.split(', ') name = parts[1] + ' ' + parts[0] if weapon[2] == '-': weapon[2] = '0d0 -' damage = {'dmg_type': weapon[2].split(' ')[1], 'is_or': False} if 'd' in weapon[2].split(' ')[0]: damage['dmg_die_count'] = int(weapon[2].split('d')[0]) damage['dmg_die_sides'] = int(weapon[2].split(' ')[0].split('d')[1]) else: damage['dmg_die_count'] = 1 damage['dmg_die_sides'] = 1 rang = [0, 0] reach = 5 properties = [] if weapon[4] != '-': properties = weapon[4].split(', ') for i, p in enumerate(list(properties)): if 'versatile' in p: properties[i] = 'versatile' elif 'range' in p: properties[i] = p.split(' (')[0] rang = [int(r) for r in p.split(' ')[-1][:-1].split('/')] if 'ammunition' in p: reach = 0 elif 'reach' in p: reach += 5 if name in special: properties.append(special[name]) weapons.append(formatWeapon(name, rang[0], rang[1], reach, [damage], 'Provided from PHB.', baseWeapon = {'cost': cost2copper(weapon[1]), 'weight': float(Fraction(weapon[3].split(' ')[0].replace('-', '0'))), 'properties': properties, 'weapon_type': header})) return weapons # damage is formatted {'dmg_die_count': ddc, 'dmg_die_sides': dds, 'dmg_type': dt, 'is_or': bool} def formatWeapon(name, rangeShort, rangeLong, reach, damage, text, properties=[], toHitOverride=None, abilityOverride=None, dmgBonusOverride=None, baseWeapon = {'cost': -1, 'weight': -1.0, 'weapon_type': 'unknown', 'properties': []}): if not properties: properties = baseWeapon['properties'] for weapon in weapons: if weapon['name'] == name: baseWeapon = weapon for dmg in damage: assert 'dmg_die_count' in dmg and 'dmg_die_sides' in dmg and 'dmg_type' in dmg and 'is_or' in dmg return {'entry': 'item', 'type': 'weapons', 'name': name, 'cost': baseWeapon['cost'], 'damage': damage, 'weight': baseWeapon['weight'], 'range': [rangeShort, rangeLong], 'reach': reach, 'properties': properties, 'weapon_type': baseWeapon['weapon_type'], 'text': text, 'toHitOverride': toHitOverride, 'dmgBonusOverride': dmgBonusOverride, 'abilityOverride': abilityOverride} spells = [] def getSpells(): global spells if spells: return spells names2names = {'name': 'name', 'level': 'level', 'type': 'school', 'classes': 'classes', 'casting_time': 'Casting Time', 'range': 'Range', 'components': 'Components', 'duration': 'Duration'} from pathlib import Path for s in Path(docsLoc + '/spellcasting/spells/').iterdir(): if s.name == 'index.md': continue with s.open() as f: data = f.read() spell = {'entry': 'spells'} for name in names2names: spell[name] = re.search('(?sm)[\*]*{}[\*:]* (.*?)^[a-zA-Z#\*]'.format(names2names[name]), data).group(1).strip() spell['name'] = spell['name'].lower() spell['level'] = int(spell['level']) spell['text'] = re.search('(?sm)\*\*Duration:?\*\*:? .*?$(.*)', data).group(1).strip() spell['classes'] = spell['classes'].split() spells.append(spell) return spells