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'
|