aboutsummaryrefslogtreecommitdiff
path: root/parser/scrapeToJson.py
diff options
context:
space:
mode:
Diffstat (limited to 'parser/scrapeToJson.py')
-rwxr-xr-xparser/scrapeToJson.py156
1 files changed, 24 insertions, 132 deletions
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: