forked from NerdEgghead/WotLK_cat_sim
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsim_utils.py
295 lines (235 loc) · 10.1 KB
/
sim_utils.py
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
"""Code for simulating the classic WoW feral cat DPS rotation."""
import numpy as np
import copy
import collections
import urllib
import multiprocessing
import psutil
def calc_white_damage(
low_end, high_end, miss_chance, crit_chance,
crit_multiplier=2.0
):
"""Execute single roll table for a melee white attack.
Arguments:
low_end (float): Low end base damage of the swing.
high_end (float): High end base damage of the swing.
miss_chance (float): Probability that the swing is avoided.
crit_chance (float): Probability of a critical strike.
crit_multiplier (float): Damage multiplier on crits.
Defaults to 2.0.
Returns:
damage_done (float): Damage done by the swing.
miss (bool): True if the attack was avoided.
crit (bool): True if the attack was a critical strike.
"""
outcome_roll = np.random.rand()
if outcome_roll < miss_chance:
return 0.0, True, False
base_dmg = low_end + np.random.rand() * (high_end - low_end)
if outcome_roll < miss_chance + 0.24:
glance_reduction = 0.15 + np.random.rand() * 0.2
return (1.0 - glance_reduction) * base_dmg, False, False
if outcome_roll < miss_chance + 0.24 + crit_chance:
return crit_multiplier * base_dmg, False, True
return base_dmg, False, False
def calc_yellow_damage(
low_end, high_end, miss_chance, crit_chance,
crit_multiplier=2.0
):
"""Execute 2-roll table for a melee spell.
Arguments:
low_end (float): Low end base damage of the ability.
high_end (float): High end base damage of the ability.
miss_chance (float): Probability that the ability is avoided.
crit_chance (float): Probability of a critical strike.
crit_multiplier (float): Damage multiplier on crits.
Defaults to 2.0.
Returns:
damage_done (float): Damage done by the ability.
miss (bool): True if the attack was avoided.
crit (bool): True if the attack was a critical strike.
"""
miss_roll = np.random.rand()
if miss_roll < miss_chance:
return 0.0, True, False
base_dmg = low_end + np.random.rand() * (high_end - low_end)
crit_roll = np.random.rand()
if crit_roll < crit_chance:
return crit_multiplier * base_dmg, False, True
return base_dmg, False, False
def calc_spell_damage(
low_end, high_end, miss_chance, crit_chance, crit_multiplier=1.5
):
"""Execute 2-roll table for a spell and adjust for resistances.
Arguments:
low_end (float): Low end base damage of the ability.
high_end (float): High end base damage of the ability.
miss_chance (float): Probability that the ability is avoided.
crit_chance (float): Probability of a critical strike.
crit_multiplier (float): Damage multiplier on crits.
Defaults to 1.5.
Returns:
damage_done (float): Damage done by the ability.
miss (bool): True if the attack was avoided.
crit (bool): True if the attack was a critical strike.
"""
base_dmg, miss, crit = calc_yellow_damage(low_end, high_end,
miss_chance, crit_chance, crit_multiplier)
# Adjust for resistances, hard coded for pure level based resist
if not miss:
resist_roll = np.random.rand()
if resist_roll < 0.55:
base_dmg *= 1.0
elif resist_roll < 0.85:
base_dmg *= 0.9
else:
base_dmg *= 0.8
return base_dmg, miss, crit
def piecewise_eval(t_fine, times, values):
"""Evaluate a piecewise constant function on a finer time mesh.
Arguments:
t_fine (np.ndarray): Desired mesh for evaluation.
times (np.ndarray): Breakpoints of piecewise function.
values (np.ndarray): Function values at the breakpoints.
Returns:
y_fine (np.ndarray): Function evaluated on the desired mesh.
"""
result = np.zeros_like(t_fine)
for i in range(len(times) - 1):
result += values[i] * ((t_fine >= times[i]) & (t_fine < times[i + 1]))
result += values[-1] * (t_fine >= times[-1])
return result
def calc_swing_timer(haste_rating, multiplier=1.0, cat_form=True):
"""Calculate swing timer given a total haste rating stat.
Arguments:
haste_rating (int): Player haste rating stat.
multiplier (float): Overall haste multiplier from multiplicative haste
buffs such as Bloodlust. Defaults to 1.
cat_form (bool): If True, calculate Cat Form swing timer. If False,
calculate Dire Bear Form swing timer. Defaults True.
Returns:
swing_timer (float): Hasted swing timer in seconds.
"""
base_timer = 1.0 if cat_form else 2.5
return base_timer / (multiplier * (1 + haste_rating / 2521))
def calc_haste_rating(swing_timer, multiplier=1.0, cat_form=True):
"""Calculate the haste rating that is consistent with a given swing timer.
Arguments:
swing_timer (float): Hasted swing timer in seconds.
multiplier (float): Overall haste multiplier from multiplicative haste
buffs such as Bloodlust. Defaults to 1.
cat_form (bool): If True, assume swing timer is for Cat Form. If False,
assume swing timer is for Dire Bear Form. Defaults True.
Returns:
haste_rating (float): Unrounded haste rating.
"""
base_timer = 1.0 if cat_form else 2.5
return 2521 * (base_timer / (swing_timer * multiplier) - 1)
def calc_hasted_gcd(haste_rating, multiplier=1.0):
"""Calculate GCD for spell casts given a total haste rating stat.
Arguments:
haste_rating (int): Player haste rating stat.
multiplier (float): Overall spell haste multiplier from multiplicative
haste buffs such as Bloodlust. Defaults to 1.
Returns:
spell_gcd (float): Hasted GCD in seconds.
"""
return max(1.5 / (multiplier * (1 + haste_rating / 3279)), 1.0)
def gen_import_link(
stat_weights, EP_name='Simmed Weights', multiplier=1.166, epic_gems=False
):
"""Generate 80upgrades stat weight import link from calculated weights.
Socket value is determined by the largest non-hit weight, as hit will
often be too close to cap to socket. Note that ArP gems may still run
into cap issues.
Arguments:
stat_weights (dict): Dictionary of weights generated by a Simulation
object. Required keys are: "1% hit", "1% crit", "1% haste",
"1% expertise", "1 Armor Pen", "1 Agility" and "1 Weapon Damage".
EP_name (str): Name for the EP set for auto-populating the 70upgrades
import interface. Defaults to "Simmed Weights".
multiplier (float): Scaling factor for raw primary stats. Defaults to
1.166 assuming Blessing of Kings, 3/3 Survival of the Fittest and
0/2 Improved Mark of the Wild.
epic_gems (bool): Whether Epic quality gems (20 Stats) should be
assumed for socket weight calculations. Defaults to False (Rare
quality +16 gems).
Returns:
import_link (str): Full URL for stat weight import into 80upgrades.
"""
link = 'https://eightyupgrades.com/ep/import?name='
# EP Name
link += urllib.parse.quote(EP_name)
# Attack Power and Strength
ap_weight = stat_weights['Attack Power']
fap_weight = 1.2 * ap_weight
str_weight = 2 * multiplier * ap_weight
link += '&31=%.2f&33=%.2f&4=%.2f' % (ap_weight, fap_weight, str_weight)
# Agility
# Due to bear weaving, agi is no longer directly derived from
# AP and crit.
agi_weight = stat_weights['Agility']
link += '&0=%.2f' % agi_weight
# Hit Rating and Expertise Rating
hit_weight = stat_weights['Hit Rating']
link += '&35=%.2f' % (hit_weight)
# Expertise Rating
expertise_weight = stat_weights['Expertise Rating']
link += '&46=%.2f' % (expertise_weight)
# Critical Strike Rating
crit_weight = stat_weights['Critical Strike Rating']
link += '&41=%.2f' % (crit_weight)
# Haste Rating
haste_weight = stat_weights['Haste Rating']
link += '&43=%.2f' % (haste_weight)
# Armor Penetration
arp_weight = stat_weights['Armor Pen Rating']
link += '&87=%.2f' % arp_weight
# Weapon Damage
link += '&51=%.2f' % stat_weights['Weapon Damage']
# Gems
gem_size = 20 if epic_gems else 16
gem_weight = gem_size * max(
str_weight, agi_weight, crit_weight, haste_weight, arp_weight
)
link += '&74=%.1f&75=%.1f&76=%.1f' % (gem_weight, gem_weight, gem_weight)
return link
def calc_ep_variance(
base_dps_vals, incremented_dps_vals, final_iteration_count,
bootstrap=True
):
"""Use the bootstrap method to estimate the error bar for a production run
of an EP calculation based on a test sample.
Arguments:
base_dps_vals (np.ndarray): Sample of reference DPS values.
incremented_dps_vals (np.ndarray): Sample of augmented DPS values with
a given stat incremented.
final_iteration_count (int): Total iteration count for the production
run.
bootstrap (bool): If True, do a full bootstrap resampling calculation
for the most accurate variance estimate, which can be expensive.
If False, use normal approximations for the two distributions.
Defaults True.
Returns:
ep_error_bar (float): Predicted standard error on EP estimate in the
production run if the current increment is used.
"""
if not bootstrap:
return np.sqrt(
(np.std(base_dps_vals)**2 + np.std(incremented_dps_vals)**2)
/ final_iteration_count
)
num_bootstrap_iterations = max(final_iteration_count, 100000)
bootstrap_ep_vals = np.zeros(num_bootstrap_iterations)
for i in range(num_bootstrap_iterations):
reference_sample = np.random.choice(
base_dps_vals, size=final_iteration_count, replace=True
)
augmented_sample = np.random.choice(
incremented_dps_vals, size=final_iteration_count, replace=True
)
bootstrap_ep_vals[i] = (
np.mean(augmented_sample) - np.mean(reference_sample)
)
ep_error_bar = np.std(bootstrap_ep_vals)
return ep_error_bar