forked from dazer/ShadowCraft-Engine
/
__init__.py
executable file
·724 lines (630 loc) · 31.3 KB
/
__init__.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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
import gettext
import __builtin__
import math
__builtin__._ = gettext.gettext
from shadowcraft.core import exceptions
from shadowcraft.calcs import armor_mitigation
from shadowcraft.objects import class_data
from shadowcraft.objects import talents
from shadowcraft.objects import procs
from shadowcraft.objects.procs import InvalidProcException
class DamageCalculator(object):
# This method holds the general interface for a damage calculator - the
# sorts of parameters and calculated values that will be need by many (or
# most) classes if they implement a damage calculator using this framework.
# Not saying that will happen, but I want to leave my options open.
# Any calculations that are specific to a particular class should go in
# calcs.<class>.<Class>DamageCalculator instead - for an example, see
# calcs.rogue.RogueDamageCalculator
TARGET_BASE_ARMOR_VALUES = {88:11977., 93:24835., 103:100000.}
AOE_TARGET_CAP = 20
# Override this in your class specfic subclass to list appropriate stats
# possible values are agi, str, spi, int, haste, crit, mastery
default_ep_stats = []
# normalize_ep_stat is the stat with value 1 EP, override in your subclass
normalize_ep_stat = None
def __init__(self, stats, talents, glyphs, buffs, race, settings=None, level=100, target_level=None, char_class='rogue'):
self.WOW_BUILD_TARGET = '6.0.3' # should reflect the game patch being targetted
self.SHADOWCRAFT_BUILD = '1.05' # <1 for beta builds, 1.00 is GM, >1 for any bug fixes, reset for each warcraft patch
self.tools = class_data.Util()
self.stats = stats
self.talents = talents
self.glyphs = glyphs
self.buffs = buffs
self.race = race
self.char_class = char_class
self.settings = settings
self.target_level = [target_level, level + 3][target_level is None] #assumes 3 levels higher if not explicit
#racials
if self.race.race_name == 'undead':
self.stats.procs.set_proc('touch_of_the_grave')
if self.race.race_name == 'goblin':
self.stats.procs.set_proc('rocket_barrage')
if self.settings.is_pvp:
self.level_difference = 0
self.base_one_hand_miss_rate = .00
self.base_parry_chance = .03
self.base_dodge_chance = .03
else:
self.level_difference = max(self.target_level - level, 0)
self.base_one_hand_miss_rate = 0
self.base_parry_chance = .01 * self.level_difference
self.base_dodge_chance = 0
self.dw_miss_penalty = .17
self._set_constants_for_class()
self.level = level
self.recalculate_hit_constants()
self.base_block_chance = .03 + .015 * self.level_difference
def __setattr__(self, name, value):
object.__setattr__(self, name, value)
if name == 'level':
self._set_constants_for_level()
def __getattr__(self, name):
# Any status we haven't assigned a value to, we don't have.
if name == 'calculating_ep':
return False
object.__getattribute__(self, name)
def _set_constants_for_level(self):
self.buffs.level = self.level
self.stats.level = self.level
self.race.level = self.level
self.stats.gear_buffs.level = self.level
# calculate and cache the level-dependent armor mitigation parameter
self.armor_mitigation_parameter = armor_mitigation.parameter(self.level)
# target level dependent constants
try:
self.target_base_armor = self.TARGET_BASE_ARMOR_VALUES[self.target_level]
except KeyError as e:
raise exceptions.InvalidInputException(_('There\'s no armor value for a target level {level}').format(level=str(e)))
self.crit_reduction = .01 * self.level_difference
def _set_constants_for_class(self):
# These factors are class-specific. Generaly those go in the class module,
# unless it's basic stuff like combat ratings or base stats that we can
# datamine for all classes/specs at once.
if self.talents.game_class != self.glyphs.game_class:
raise exceptions.InvalidInputException(_('You must specify the same class for your talents and glyphs'))
self.game_class = self.talents.game_class
def recalculate_hit_constants(self):
self.base_dw_miss_rate = self.base_one_hand_miss_rate + self.dw_miss_penalty
def get_adv_param(self, type, default_val, min_bound=-10000, max_bound=10000, ignore_bounds=False):
if type in self.settings.adv_params and not ignore_bounds:
return max( min(float(self.settings.adv_params[type]), max_bound), min_bound )
elif type in self.settings.adv_params:
return self.settings.adv_params[type]
else:
return default_val
raise exceptions.InvalidInputException(_('Improperly defined parameter type: '+type))
def add_exported_data(self, damage_breakdown):
#used explicitly to highjack data outputs to export additional data.
if self.get_version_number:
damage_breakdown['version_' + self.WOW_BUILD_TARGET + '_' + self.SHADOWCRAFT_BUILD] = [.0, 0]
def set_rppm_uptime(self, proc):
#http://iam.yellingontheinternet.com/2013/04/12/theorycraft-201-advanced-rppm/
haste = 1.
if proc.haste_scales:
haste *= self.stats.get_haste_multiplier_from_rating(self.base_stats['haste']) * self.true_haste_mod
#The 1.1307 is a value that increases the proc rate due to bad luck prevention. It /should/ be constant among all rppm proc styles
if not proc.icd:
if proc.max_stacks <= 1:
proc.uptime = 1.1307 * (1 - math.e ** (-1 * haste * proc.get_rppm_proc_rate() * proc.duration / 60))
else:
lambd = haste * proc.get_rppm_proc_rate() * proc.duration / 60
e_lambda = math.e ** lambd
e_minus_lambda = math.e ** (-1 * lambd)
proc.uptime = 1.1307 * (e_lambda - 1) * (1 - ((1 - e_minus_lambda) ** proc.max_stacks))
else:
mean_proc_time = 60. / (haste * proc.get_rppm_proc_rate()) + proc.icd - min(proc.icd, 10)
proc.uptime = 1.1307 * proc.duration / mean_proc_time
def set_uptime(self, proc, attacks_per_second, crit_rates):
if proc.is_real_ppm():
self.set_rppm_uptime(proc)
else:
procs_per_second = self.get_procs_per_second(proc, attacks_per_second, crit_rates)
if proc.icd:
proc.uptime = proc.duration / (proc.icd + 1. / procs_per_second)
else:
if procs_per_second >= 1:
self.set_uptime_for_ramping_proc(proc, procs_per_second)
else:
# See http://elitistjerks.com/f31/t20747-advanced_rogue_mechanics_discussion/#post621369
# for the derivation of this formula.
q = 1 - procs_per_second
Q = q ** proc.duration
if Q < .0001:
self.set_uptime_for_ramping_proc(proc, procs_per_second)
else:
P = 1 - Q
proc.uptime = P * (1 - P ** proc.max_stacks) / Q
def average_damage_breakdowns(self, aps_dict, denom=180):
final_breakdown = {}
#key: phase name
#number: place in tuple... tuple = (phase_length, dps_breakdown)
#entry: DPS skill_name
#denom: total duration (to divide phase duration by it)
for key in aps_dict:
for entry in aps_dict[key][1]:
if entry in final_breakdown:
final_breakdown[entry] += aps_dict[key][1][entry] * (aps_dict[key][0]/denom)
else:
final_breakdown[entry] = aps_dict[key][1][entry] * (aps_dict[key][0]/denom)
return final_breakdown
def ep_helper(self, stat):
setattr(self.stats, stat, getattr(self.stats, stat) + 1.)
dps = self.get_dps()
setattr(self.stats, stat, getattr(self.stats, stat) - 1.)
return dps
def get_ep(self, ep_stats=None, normalize_ep_stat=None, baseline_dps=None):
if not normalize_ep_stat:
normalize_ep_stat = self.normalize_ep_stat
if not ep_stats:
ep_stats = self.default_ep_stats
if baseline_dps == None:
baseline_dps = self.get_dps()
if normalize_ep_stat == 'dps':
normalize_dps_difference = 1.
else:
normalize_dps = self.ep_helper(normalize_ep_stat)
normalize_dps_difference = normalize_dps - baseline_dps
if normalize_dps_difference == 0:
normalize_dps_difference = 1
ep_values = {}
for stat in ep_stats:
ep_values[stat] = 1.0
if normalize_ep_stat != stat:
dps = self.ep_helper(stat)
ep_values[stat] = abs(dps - baseline_dps) / normalize_dps_difference
return ep_values
def get_weapon_ep(self, speed_list=None, dps=False, enchants=False, normalize_ep_stat=None):
if not normalize_ep_stat:
normalize_ep_stat = self.normalize_ep_stat
weapons = ('mh', 'oh')
if speed_list is not None or dps:
baseline_dps = self.get_dps()
normalize_dps = self.ep_helper(normalize_ep_stat)
for hand in weapons:
ep_values = {}
# Weapon dps EP
if dps:
getattr(self.stats, hand).weapon_dps += 1.
new_dps = self.get_dps()
ep = abs(new_dps - baseline_dps) / (normalize_dps - baseline_dps)
ep_values[hand + '_dps'] = ep
getattr(self.stats, hand).weapon_dps -= 1.
# Enchant EP
if enchants:
old_enchant = None
for enchant in getattr(self.stats, hand).allowed_melee_enchants:
if getattr(getattr(self.stats, hand), enchant):
old_enchant = enchant
getattr(self.stats, hand).del_enchant()
no_enchant_dps = self.get_dps()
no_enchant_normalize_dps = self.ep_helper(normalize_ep_stat)
for enchant in getattr(self.stats, hand).allowed_melee_enchants:
getattr(self.stats, hand).set_enchant(enchant)
new_dps = self.get_dps()
if new_dps != no_enchant_dps:
ep = abs(new_dps - no_enchant_dps) / (no_enchant_normalize_dps - no_enchant_dps)
ep_values[hand + '_' + enchant] = ep
getattr(self.stats, hand).set_enchant(old_enchant)
# Weapon speed EP
if speed_list is not None:
old_speed = getattr(self.stats, hand).speed
for speed in speed_list:
getattr(self.stats, hand).speed = speed
new_dps = self.get_dps()
ep = (new_dps - baseline_dps) / (normalize_dps - baseline_dps)
ep_values[hand + '_' + str(speed)] = ep
getattr(self.stats, hand).speed = old_speed
if hand == 'mh':
mh_ep_values = ep_values
elif hand == 'oh':
oh_ep_values = ep_values
return mh_ep_values, oh_ep_values
def get_weapon_type_ep(self, normalize_ep_stat=None):
if not normalize_ep_stat:
normalize_ep_stat = self.normalize_ep_stat
weapons = ('mh', 'oh')
baseline_dps = self.get_dps()
normalize_dps = self.ep_helper(normalize_ep_stat)
mh_ep_values = {}
oh_ep_values = {}
for hand in weapons:
ep_values = {}
old_type = getattr(self.stats, hand).type
for wtype in ('dagger', 'one-hander'):
getattr(self.stats, hand).type = wtype
new_dps = self.get_dps()
ep = (new_dps - baseline_dps) / (normalize_dps - baseline_dps)
ep_values[hand + '_type_' + wtype] = ep
getattr(self.stats, hand).type = old_type
if hand == 'mh':
mh_ep_values = ep_values
elif hand == 'oh':
oh_ep_values = ep_values
return mh_ep_values, oh_ep_values
def get_weapon_type_modifier_helper(self, setups=None):
# Use this method if you want to test different weapon setups. It will
# return one value per setup including the current one. It takes setups
# like this one:
# (
# {'hand':'mh', 'type':mh_type, 'speed':mh_speed},
# {'hand':'oh', 'type':oh_type, 'speed':oh_speed}
# )
modifiers = {}
weapons = ('mh', 'oh')
baseline_setup = []
for hand in weapons:
weapon = getattr(self.stats, hand)
baseline_setup.append((hand, weapon.speed, weapon.type))
modifiers[tuple(baseline_setup)] = 1
if not setups:
return modifiers
baseline_dps = self.get_dps()
for setup in setups:
current_setup = []
assert len(setup) == 2
for hand in setup:
if hand is not None:
weapon = getattr(self.stats, hand['hand'])
weapon.speed = hand['speed']
weapon.type = hand['type']
current_setup.append((hand['hand'], hand['speed'], hand['type']))
try:
new_dps = self.get_dps()
if new_dps != baseline_dps:
modifiers[tuple(current_setup)] = new_dps / baseline_dps
except InputNotModeledException:
modifiers[tuple(current_setup)] = _('not allowed')
for hand in baseline_setup:
hand_name, speed, type = hand
weapon = getattr(self.stats, hand_name)
weapon.speed = speed
weapon.type = type
return modifiers
def get_oh_weapon_modifier(self, setups, format=True):
# Override this in your modeler to pass default oh weapons to test.
modifiers = self.get_weapon_type_modifier_helper(setups)
if not format:
return modifiers
formatted_mods = {}
for setup in modifiers:
for hand in setup:
if hand[0] == 'mh':
continue
formatted_mods['_'.join((hand[0], str(hand[1]), hand[2]))] = modifiers[setup]
return formatted_mods
def get_dw_weapon_modifier(self, setups, format=True):
# Override this in your modeler to pass default dw setups to test.
modifiers = self.get_weapon_type_modifier_helper(setups)
pass
def get_2h_weapon_modifier(self, setups, format=True):
# Override this in your modeler to pass default 2h setups to test.
modifiers = self.get_weapon_type_modifier_helper(setups)
pass
def get_other_ep(self, list, normalize_ep_stat=None):
if not normalize_ep_stat:
normalize_ep_stat = self.normalize_ep_stat
# This method computes ep for every other buff/proc not covered by
# get_ep or get_weapon_ep. Weapon enchants, being tied to the
# weapons they are on, are computed by get_weapon_ep.
ep_values = {}
baseline_dps = self.get_dps()
if normalize_ep_stat == 'dps':
normalize_dps_difference = 1.
else:
normalize_dps = self.ep_helper(normalize_ep_stat)
normalize_dps_difference = normalize_dps - baseline_dps
procs_list = []
gear_buffs_list = []
for i in list:
if i in self.stats.procs.allowed_procs:
procs_list.append(i)
elif i in self.stats.gear_buffs.allowed_buffs:
gear_buffs_list.append(i)
else:
ep_values[i] = _('not allowed')
for i in gear_buffs_list:
# Note that activated abilites like trinkets, potions, or
# engineering gizmos are handled as gear buffs by the engine.
setattr(self.stats.gear_buffs, i, not getattr(self.stats.gear_buffs, i))
new_dps = self.get_dps()
ep_values[i] = abs(new_dps - baseline_dps) / (normalize_dps_difference)
setattr(self.stats.gear_buffs, i, not getattr(self.stats.gear_buffs, i))
for i in procs_list:
try:
if getattr(self.stats.procs, i):
delattr(self.stats.procs, i)
else:
self.stats.procs.set_proc(i)
new_dps = self.get_dps()
ep_values[i] = abs(new_dps - baseline_dps) / (normalize_dps_difference)
if getattr(self.stats.procs, i):
delattr(self.stats.procs, i)
else:
self.stats.procs.set_proc(i)
except InvalidProcException:
# Data for these procs is not complete/correct
ep_values[i] = _('not supported')
delattr(self.stats.procs, i)
return ep_values
def get_upgrades_ep(self, _list, normalize_ep_stat=None):
if not normalize_ep_stat:
normalize_ep_stat = self.normalize_ep_stat
# This method computes ep for every other buff/proc not covered by
# get_ep or get_weapon_ep. Weapon enchants, being tied to the
# weapons they are on, are computed by get_weapon_ep.
active_procs_cache = []
procs_list = []
ep_values = {}
for i in _list:
if i in self.stats.procs.allowed_procs:
procs_list.append( (i, _list[i]) )
if getattr(self.stats.procs, i):
active_procs_cache.append((i, getattr(self.stats.procs, i).item_level))
delattr(self.stats.procs, i)
else:
ep_values[i] = _('not allowed')
baseline_dps = self.get_dps()
normalize_dps = self.ep_helper(normalize_ep_stat)
for i in procs_list:
proc_name, item_levels = i
ep_values[proc_name] = {}
try:
if getattr(self.stats.procs, proc_name):
old_proc = getattr(self.stats.procs, proc_name)
delattr(self.stats.procs, proc_name)
base_dps = self.get_dps()
base_normalize_dps = self.ep_helper(normalize_ep_stat)
else:
old_proc = False
base_dps = baseline_dps
base_normalize_dps = normalize_dps
self.stats.procs.set_proc(proc_name)
proc = getattr(self.stats.procs, proc_name)
for group in item_levels:
if not isinstance(group, (list,tuple)):
group = group,
for l in group:
proc.item_level = l
proc.update_proc_value() # after setting item_level re-set the proc value
new_dps = self.get_dps()
if new_dps != base_dps:
ep = abs(new_dps - base_dps) / (base_normalize_dps - base_dps)
ep_values[proc_name][l] = ep
if old_proc:
self.stats.procs.set_proc(proc_name)
else:
delattr(self.stats.procs, proc_name)
except InvalidProcException:
# Data for these procs is not complete/correct
ep_values[i].append(_('not supported'))
delattr(self.stats.procs, proc_name)
for proc in active_procs_cache:
self.stats.procs.set_proc(proc[0])
getattr(self.stats.procs, proc[0]).item_level = proc[1]
return ep_values
# this function is in comparison to get_upgrades_ep a lot faster but not 100% accurate
# the error is around 1% which is accurate enough for the ranking in Shadowcraft-UI
def get_upgrades_ep_fast(self, _list, normalize_ep_stat=None):
if not normalize_ep_stat:
normalize_ep_stat = self.normalize_ep_stat
# This method computes ep for every other buff/proc not covered by
# get_ep or get_weapon_ep. Weapon enchants, being tied to the
# weapons they are on, are computed by get_weapon_ep.
active_procs_cache = []
procs_list = []
ep_values = {}
for i in _list:
if i in self.stats.procs.allowed_procs:
procs_list.append( (i, _list[i]) )
if getattr(self.stats.procs, i):
active_procs_cache.append((i, getattr(self.stats.procs, i).item_level))
delattr(self.stats.procs, i)
else:
ep_values[i] = _('not allowed')
baseline_dps = self.get_dps()
normalize_dps = self.ep_helper(normalize_ep_stat)
for i in procs_list:
proc_name, item_levels = i
ep_values[proc_name] = {}
try:
if getattr(self.stats.procs, proc_name):
old_proc = getattr(self.stats.procs, proc_name)
delattr(self.stats.procs, proc_name)
base_dps = self.get_dps()
base_normalize_dps = self.ep_helper(normalize_ep_stat)
else:
old_proc = False
base_dps = baseline_dps
base_normalize_dps = normalize_dps
self.stats.procs.set_proc(proc_name)
proc = getattr(self.stats.procs, proc_name)
for group in item_levels:
if not isinstance(group, (list,tuple)):
group = group,
if proc.scaling:
proc.item_level = group[0]
proc.update_proc_value() # after setting item_level re-set the proc value
item_level = proc.item_level
if proc.proc_name == 'Rune of Re-Origination':
scale_factor = 1/(1.15**((528-item_level)/15.0)) * proc.base_ppm
else:
scale_factor = self.tools.get_random_prop_point(item_level)
new_dps = self.get_dps()
if new_dps != base_dps:
for l in group:
ep = abs(new_dps - base_dps) / (base_normalize_dps - base_dps)
if l > proc.item_level:
if proc.proc_name == 'Rune of Re-Origination':
upgraded_scale_factor = 1/(1.15**((528-(l))/15.0)) * proc.base_ppm
else:
upgraded_scale_factor = self.tools.get_random_prop_point(l)
ep *= float(upgraded_scale_factor) / float(scale_factor)
ep_values[proc_name][l] = ep
if old_proc:
self.stats.procs.set_proc(proc_name)
else:
delattr(self.stats.procs, proc_name)
except InvalidProcException:
# Data for these procs is not complete/correct
ep_values[i].append(_('not supported'))
delattr(self.stats.procs, i)
for proc in active_procs_cache:
self.stats.procs.set_proc(proc[0])
getattr(self.stats.procs, proc[0]).item_level = proc[1]
return ep_values
def get_glyphs_ranking(self, list=None):
glyphs = []
glyphs_ranking = {}
baseline_dps = self.get_dps()
if list == None:
glyphs = self.glyphs.allowed_glyphs
else:
glyphs = list
for i in glyphs:
setattr(self.glyphs, i, not getattr(self.glyphs, i))
try:
new_dps = self.get_dps()
if new_dps != baseline_dps:
glyphs_ranking[i] = abs(new_dps - baseline_dps)
except:
glyphs_ranking[i] = _('not implemented')
setattr(self.glyphs, i, not getattr(self.glyphs, i))
return glyphs_ranking
def get_talents_ranking(self, list=None):
talents_ranking = {}
baseline_dps = self.get_dps()
talent_list = []
if list is None:
talent_list = self.talents.get_allowed_talents_for_level()
else:
talent_list = list
for talent in talent_list:
setattr(self.talents, talent, not getattr(self.talents, talent))
try:
new_dps = self.get_dps()
if new_dps != baseline_dps:
talents_ranking[talent] = abs(new_dps - baseline_dps)
except:
talents_ranking[talent] = _('not implemented')
setattr(self.talents, talent, not getattr(self.talents, talent))
return talents_ranking
def get_engine_info(self):
data = {
'wow_build_target': self.WOW_BUILD_TARGET,
'shadowcraft_build': self.SHADOWCRAFT_BUILD
}
return data
def get_dps(self):
# Overwrite this function with your calculations/simulations/whatever;
# this is what callers will (initially) be looking at.
pass
#def get_all_activated_stat_boosts(self):
# racial_boosts = self.race.get_racial_stat_boosts()
# gear_boosts = self.stats.gear_buffs.get_all_activated_boosts()
# return racial_boosts + gear_boosts
def armor_mitigation_multiplier(self, armor):
return armor_mitigation.multiplier(armor, cached_parameter=self.armor_mitigation_parameter)
def max_level_armor_multiplier(self):
return 3610.0 / (3610.0 + 1938.0)
def get_trinket_cd_reducer(self):
trinket_cd_reducer_value = .0
proc = getattr(self.stats.procs, 'assurance_of_consequence')
if proc and proc.scaling:
trinket_cd_reducer_value = 0.2532840073 / 100 * self.tools.get_random_prop_point(proc.item_level)
if self.level == 100:
trinket_cd_reducer_value *= 23./110.
return 1 / (1 + trinket_cd_reducer_value)
return 1
def armor_mitigate(self, damage, armor):
# Pass in raw physical damage and armor value, get armor-mitigated
# damage value.
return damage * self.armor_mitigation_multiplier(armor)
def melee_hit_chance(self, base_miss_chance, dodgeable, parryable, weapon_type, blockable=False, bonus_hit=0):
miss_chance = base_miss_chance
# Expertise represented as the reduced chance to be dodged, not true "Expertise".
if dodgeable:
dodge_chance = self.base_dodge_chance
else:
dodge_chance = 0
if parryable:
# Expertise will negate dodge and spell miss, *then* parry
parry_expertise = max(expertise - self.base_dodge_chance, 0)
parry_chance = max(self.base_parry_chance - parry_expertise, 0)
else:
parry_chance = 0
block_chance = self.base_block_chance * blockable
return (1 - (miss_chance + dodge_chance + parry_chance)) * (1 - block_chance)
def melee_spells_hit_chance(self, bonus_hit=0):
hit_chance = self.melee_hit_chance(self.base_one_hand_miss_rate, dodgeable=False, parryable=False, weapon_type=None)
return hit_chance
def one_hand_melee_hit_chance(self, dodgeable=False, parryable=False, weapon=None, bonus_hit=0):
# Most attacks by DPS aren't parryable due to positional negation. But
# if you ever want to attacking from the front, you can just set that
# to True.
if weapon == None:
weapon = self.stats.mh
hit_chance = self.melee_hit_chance(self.base_one_hand_miss_rate, dodgeable, parryable, weapon.type)
return hit_chance
def off_hand_melee_hit_chance(self, dodgeable=False, parryable=False, weapon=None, bonus_hit=0):
# Most attacks by DPS aren't parryable due to positional negation. But
# if you ever want to attacking from the front, you can just set that
# to True.
if weapon == None:
weapon = self.stats.oh
hit_chance = self.melee_hit_chance(self.base_one_hand_miss_rate, dodgeable, parryable, weapon.type)
return hit_chance
def dual_wield_mh_hit_chance(self, dodgeable=False, parryable=False, dw_miss=None):
# Most attacks by DPS aren't parryable due to positional negation. But
# if you ever want to attacking from the front, you can just set that
# to True.
hit_chance = self.dual_wield_hit_chance(dodgeable, parryable, self.stats.mh.type, dw_miss=dw_miss)
return hit_chance
def dual_wield_oh_hit_chance(self, dodgeable=False, parryable=False, dw_miss=None):
# Most attacks by DPS aren't parryable due to positional negation. But
# if you ever want to attacking from the front, you can just set that
# to True.
hit_chance = self.dual_wield_hit_chance(dodgeable, parryable, self.stats.oh.type, dw_miss=dw_miss)
return hit_chance
def dual_wield_hit_chance(self, dodgeable, parryable, weapon_type, dw_miss=None):
if not dw_miss:
dw_miss = self.base_dw_miss_rate
hit_chance = self.melee_hit_chance(dw_miss, dodgeable, parryable, weapon_type)
return hit_chance
def buff_melee_crit(self):
return self.buffs.buff_all_crit()
def crit_damage_modifiers(self, crit_damage_bonus_modifier=1):
# The obscure formulae for the different crit enhancers can be found here
# http://elitistjerks.com/f31/t13300-shaman_relentless_earthstorm_ele/#post404567
base_modifier = 2
if self.settings.is_pvp:
base_modifier = 1.5
crit_damage_modifier = self.stats.gear_buffs.metagem_crit_multiplier()
if self.race.might_of_the_mountain:
crit_damage_modifier *= 1.02 #2x base becomes 2.04x with MotM
total_modifier = 1 + (base_modifier * crit_damage_modifier - 1) * crit_damage_bonus_modifier
return total_modifier
def target_armor(self, armor=None):
# Passes base armor reduced by armor debuffs or overridden armor
if armor is None:
armor = self.target_base_armor
return armor #* self.buffs.armor_reduction_multiplier()
def raid_settings_modifiers(self, attack_kind, armor=None, affect_resil=True):
# This function wraps spell, bleed and physical debuffs from raid
# along with all-damage buff and armor reduction. It should be called
# from every damage dealing formula. Armor can be overridden if needed.
pvp_mod = 1.
if self.settings.is_pvp and affect_resil:
power = self.stats.get_pvp_power_multiplier_from_rating()
resil = self.stats.get_pvp_resil_multiplier_from_rating()
pvp_mod = power*(1.0 - resil)
armor = self.stats.pvp_target_armor
if attack_kind not in ('physical', 'spell', 'bleed'):
raise exceptions.InvalidInputException(_('Attacks must be categorized as physical, spell or bleed'))
elif attack_kind == 'spell':
return self.buffs.spell_damage_multiplier() * pvp_mod
elif attack_kind == 'bleed':
return self.buffs.bleed_damage_multiplier() * pvp_mod
elif attack_kind == 'physical':
armor_override = self.target_armor(armor)
return self.buffs.physical_damage_multiplier() * self.armor_mitigation_multiplier(armor_override) * pvp_mod