aboutsummaryrefslogtreecommitdiff
path: root/parser/attacks.py
diff options
context:
space:
mode:
authorYour Name <you@example.com>2021-06-12 15:32:53 -0400
committerYour Name <you@example.com>2021-06-12 15:32:53 -0400
commit01293baa64fa905c5763020bd6c0b4903d41fc78 (patch)
tree4c49a63852fd84ead388a8fd092d64d2df7f9e1b /parser/attacks.py
parentb27700a7e0b281ece3dea23060c17e0cae28715d (diff)
downloaddmtool-01293baa64fa905c5763020bd6c0b4903d41fc78.tar.gz
dmtool-01293baa64fa905c5763020bd6c0b4903d41fc78.tar.bz2
dmtool-01293baa64fa905c5763020bd6c0b4903d41fc78.zip
Verified some creature attacks
Diffstat (limited to 'parser/attacks.py')
-rw-r--r--parser/attacks.py188
1 files changed, 188 insertions, 0 deletions
diff --git a/parser/attacks.py b/parser/attacks.py
new file mode 100644
index 0000000..743a149
--- /dev/null
+++ b/parser/attacks.py
@@ -0,0 +1,188 @@
+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'