aboutsummaryrefslogtreecommitdiff
path: root/parser/attacks.py
blob: 743a149b70ba6e669430c3ecd21eb1e1d15421a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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'