From 01293baa64fa905c5763020bd6c0b4903d41fc78 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 12 Jun 2021 15:32:53 -0400 Subject: Verified some creature attacks --- parser/scrapeToJson.py | 156 ++++++++----------------------------------------- 1 file changed, 24 insertions(+), 132 deletions(-) (limited to 'parser/scrapeToJson.py') diff --git a/parser/scrapeToJson.py b/parser/scrapeToJson.py index b1534c2..5eacb5a 100755 --- a/parser/scrapeToJson.py +++ b/parser/scrapeToJson.py @@ -1,12 +1,17 @@ #!/usr/bin/env python3 - import json import re import utils +import attacks + +class Creature(dict): + def getBonus(self, ability): + return (self['stats'][ability] - 10) // 2 def processMonster(data, weapons, armors, spells): names2names = {'ac': 'Armor Class', 'hp': 'Hit Points', 'speed': 'Speed', 'saves': 'Saving Throws', 'd_resistances': 'Damage Resistances?', 'd_vulnerabilities': 'Damage Vulnerabilities', 'd_immunities': 'Damage Immunities', 'c_immunities': 'Condition Immunities', 'senses': 'Senses', 'langs': 'Languages', 'skills': 'Skills'} - desc = {'entry': 'creatures'} + desc = Creature() + desc['entry'] = 'creatures' for name in names2names: m = re.search('(\*\*{}\.?\*\*)(.*)'.format(names2names[name]), data) if m: @@ -45,8 +50,6 @@ def processMonster(data, weapons, armors, spells): desc['size'] = description.split(' ')[0] desc['alignment'] = description.split(', ')[-1] desc['stats'] = {ability: int(score.strip().split(' ')[0]) for ability, score in zip(['str', 'dex', 'con', 'int', 'wis', 'cha'], re.findall('(?<=\|) *\d.*?(?=\|)', data))} - def getBonus(ability): - return (desc['stats'][ability] - 10) // 2 desc['inventory'] = [] # Fill with weapons and armor desc['observant'] = False # maybe set to true later # Add a few null-valued items that will be set when the creature is first generated @@ -91,15 +94,15 @@ def processMonster(data, weapons, armors, spells): #else: # print('Found {} armor {} (bonus = {})'.format(typ, name, bonus)) if typ == 'light': - armorBonus = bonus + getBonus('dex') + armorBonus = bonus + desc.getBonus('dex') elif typ == 'medium': - armorBonus = bonus + min(2, getBonus('dex')) + armorBonus = bonus + min(2, desc.getBonus('dex')) elif typ == 'heavy': armorBonus = bonus elif typ == 'misc' or typ == 'shield': armorBonus += bonus if armorBonus == 0 and not natural: # Got through all that and came up dry - armorBonus = 10 + getBonus('dex') + armorBonus = 10 + desc.getBonus('dex') if natural: desc['natural_armor'] = {'name': natural, 'bonus': correctAC - armorBonus} elif armorBonus != correctAC: @@ -111,6 +114,12 @@ def processMonster(data, weapons, armors, spells): description = re.search('(?s)(?<={}).*?(?=###|$)'.format('### Description'), data) if description: desc['text'] = description.group(0).strip() + else: + # Try looking for the last entry (**something**) with an empty line and then one or more paragraphs before end of file + description = re.search('(?s)(?<=\*\*).*\n\n(.*?)(?=###|$)', data) + if description: + #print(f'Found description without a header for {desc["name"]}') + desc['text'] = description.group(1).strip() # Next do sections names2sectHeads = {'feature': '\*\*Challenge\*\*', 'action': '### Actions', 'legendary_action': '### Legendary Actions', 'reaction': '### Reactions'} @@ -123,7 +132,7 @@ def processMonster(data, weapons, armors, spells): #text = re.match('(?s)(\s*\w[^\*].*?)([\r\n]+[\*#]|$)', '\n'.join(section.group(0).split('\n')[1:])) #if text and re.search('\w', text.group(1)): # desc[name]['_text'] = text.group(1).strip() - for m in re.findall('(?s)\n\*\*(.*?)\.?\*\*(.*?)(?=\n\*\*|$)', section.group(0)): + for m in re.findall('(?s)\n\*\*(.*?)\.?\*\*(.*?)(?=\n\*\*|\n\n|$)', section.group(0)): desc['features'].append({'entry': 'feature', 'name': m[0].lower(), 'text': m[1].strip(), 'type': name}) # Next, simplify and codify a few things # Guess the proficiency bonus @@ -134,148 +143,31 @@ def processMonster(data, weapons, armors, spells): if skillStr: for skill in skillStr.split(','): skillName, skillBonus, ability = utils.procSkill(skill) - abilityBonus = getBonus(ability) + abilityBonus = desc.getBonus(ability) profTimes = (skillBonus - abilityBonus) / desc['prof'] if round(profTimes) != profTimes: - print('Things came out funny for {}; skill {} has bonus {}, but proficiency is {} and the relevant ability ({}) gets {}'.format(desc['name'], skillName, skillBonus, desc['prof'], ability, getBonus(ability))) + print('Things came out funny for {}; skill {} has bonus {}, but proficiency is {} and the relevant ability ({}) gets {}'.format(desc['name'], skillName, skillBonus, desc['prof'], ability, desc.getBonus(ability))) desc['skills'][skillName] = round(profTimes) savesStr = desc['saves'] desc['saves'] = [] if savesStr: for save in savesStr.split(', '): ability = save.split(' ')[0].lower() - if int(save.split('+')[1]) != getBonus(ability) + desc['prof']: - print('Things came out funny for {}; {} save has bonus {}, but proficiency is {} and the relevant ability ({}) gets {}'.format(desc['name'], ability, int(save.split('+')[1]), desc['prof'], ability, getBonus(ability))) + if int(save.split('+')[1]) != desc.getBonus(ability) + desc['prof']: + print('Things came out funny for {}; {} save has bonus {}, but proficiency is {} and the relevant ability ({}) gets {}'.format(desc['name'], ability, int(save.split('+')[1]), desc['prof'], ability, desc.getBonus(ability))) desc['saves'].append(ability) for action in desc['features']: if re.match('.*Attack:', action['text']): - action['type'] = 'attack' - #toHit = int(re.search('\+(\d+) to hit', action['text']).group(1)) - #selectedAbility = None - #for ability in ['str', 'dex', 'int', 'wis', 'cha', 'con']: - # if desc['prof'] + getBonus(ability) == toHit: - # selectedAbility = ability - # break - #if not selectedAbility: - # print('Cannot find relevant ability for {}, proficiency = {}'.format(desc['name'], desc['prof'])) - # continue - #action['details']['ability'] = selectedAbility - 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('{} ([^,]*),'.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'] = [] - details['damage'] = [] - # It could be something like "1 piecring damage" (see sprite). - dmgSection = re.search('_Hit:_ .*?\.', action['text']).group(0) - for dmgMatch in re.findall('(?:plus |or )?\d+(?: \(\d+d\d+[\+− \d]*\))? [a-z]* damage', dmgSection): - isOr = dmgMatch.split(' ')[0] == 'or' - 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) - details['text'] = re.search('(?s)(_Hit:_ (?:\d+ [^\.]*\.)?)(.*)', action['text']).group(2).strip() - # We may need to move some parts of the name to the text - if '(' in action['name']: - 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']).items(): - action['attack'][name] = value - if action['attack']['weapon_type'] != 'unknown': - #desc['inventory'].append(action['attack']) - desc['inventory'].append({'entry': 'item', 'name': action['attack']['name'], 'type': 'weapons', 'text': action['text']}) + attacks.procAttackAction(desc, action, weapons) elif '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' + attacks.procSpellAction(desc, action, spells) # Remove weapon actions from features (they were just added to inventory) desc['features'] = [a for a in desc['features'] if 'attack' not in a or a['attack']['weapon_type'] == 'unknown'] # Get rid of precalculated passive perception # It's always the last item in senses passivePercep = int(desc['senses'].split(' ')[-1]) - shouldBe = 10 + getBonus('wis') + shouldBe = 10 + desc.getBonus('wis') if 'Perception' in desc['skills']: shouldBe += desc['skills']['Perception'] * desc['prof'] if passivePercep != shouldBe: -- cgit v1.2.3