diff options
author | Your Name <you@example.com> | 2021-06-12 15:32:53 -0400 |
---|---|---|
committer | Your Name <you@example.com> | 2021-06-12 15:32:53 -0400 |
commit | 01293baa64fa905c5763020bd6c0b4903d41fc78 (patch) | |
tree | 4c49a63852fd84ead388a8fd092d64d2df7f9e1b /parser/attacks.py | |
parent | b27700a7e0b281ece3dea23060c17e0cae28715d (diff) | |
download | dmtool-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.py | 188 |
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' |